正式开始BDU项目前对设备指令的第一次备份
This commit is contained in:
parent
27be073925
commit
cdf7fed4a4
4
.editorconfig
Normal file
4
.editorconfig
Normal file
@ -0,0 +1,4 @@
|
||||
[*.cs]
|
||||
|
||||
# CA1416: 验证平台兼容性
|
||||
dotnet_diagnostic.CA1416.severity = silent
|
||||
66
ATS.sln
Normal file
66
ATS.sln
Normal 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
44
ATS/ATS.csproj
Normal 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
170
ATS/App.xaml
Normal 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
25
ATS/App.xaml.cs
Normal 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
10
ATS/AssemblyInfo.cs
Normal 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)
|
||||
)]
|
||||
27
ATS/Converters/BoolInverseConverter.cs
Normal file
27
ATS/Converters/BoolInverseConverter.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
33
ATS/Converters/BooleanToVisibilityConverter.cs
Normal file
33
ATS/Converters/BooleanToVisibilityConverter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
49
ATS/Converters/DeviceNameConverter.cs
Normal file
49
ATS/Converters/DeviceNameConverter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
33
ATS/Converters/DeviceSettingWindowConverter.cs
Normal file
33
ATS/Converters/DeviceSettingWindowConverter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
82
ATS/Converters/EnumValueConverter.cs
Normal file
82
ATS/Converters/EnumValueConverter.cs
Normal 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
ATS/Converters/EnumValuesConverter.cs
Normal file
23
ATS/Converters/EnumValuesConverter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
76
ATS/Converters/FilteredParametersConverter.cs
Normal file
76
ATS/Converters/FilteredParametersConverter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
29
ATS/Converters/GuidToParameterNameConverter.cs
Normal file
29
ATS/Converters/GuidToParameterNameConverter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
38
ATS/Converters/HexConverter.cs
Normal file
38
ATS/Converters/HexConverter.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
75
ATS/Converters/HexToDecimalConverter.cs
Normal file
75
ATS/Converters/HexToDecimalConverter.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
36
ATS/Converters/IsEnumTypeConverter .cs
Normal file
36
ATS/Converters/IsEnumTypeConverter .cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
39
ATS/Converters/ParameterCategoryToStringConverter.cs
Normal file
39
ATS/Converters/ParameterCategoryToStringConverter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
39
ATS/Converters/ParameterCategoryToVisibilityConverter.cs
Normal file
39
ATS/Converters/ParameterCategoryToVisibilityConverter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
119
ATS/Converters/ParameterToGotoSettingStringConverter.cs
Normal file
119
ATS/Converters/ParameterToGotoSettingStringConverter.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
31
ATS/Converters/ParameterTypeToBoolConverter.cs
Normal file
31
ATS/Converters/ParameterTypeToBoolConverter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
38
ATS/Converters/ParameterValueToStringConverter.cs
Normal file
38
ATS/Converters/ParameterValueToStringConverter.cs
Normal 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 "";
|
||||
}
|
||||
}
|
||||
}
|
||||
39
ATS/Converters/StepResultToStringConverter.cs
Normal file
39
ATS/Converters/StepResultToStringConverter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
35
ATS/Converters/StringToVisibilityConverter.cs
Normal file
35
ATS/Converters/StringToVisibilityConverter.cs
Normal 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
3
ATS/FodyWeavers.xml
Normal 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
433
ATS/Logic/DeviceConnect.cs
Normal 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
799
ATS/Logic/StepRunning.cs
Normal 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
|
||||
|
||||
}
|
||||
}
|
||||
18
ATS/Models/CANSignalModel.cs
Normal file
18
ATS/Models/CANSignalModel.cs
Normal 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; }//记录间隔
|
||||
}
|
||||
}
|
||||
15
ATS/Models/DeviceConnectSettingModel.cs
Normal file
15
ATS/Models/DeviceConnectSettingModel.cs
Normal 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
54
ATS/Models/DeviceModel.cs
Normal 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
42
ATS/Models/MethodModel.cs
Normal 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; } = [];
|
||||
}
|
||||
}
|
||||
287
ATS/Models/ParameterModel.cs
Normal file
287
ATS/Models/ParameterModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
ATS/Models/ProgramModel.cs
Normal file
41
ATS/Models/ProgramModel.cs
Normal 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
54
ATS/Models/ReportModel.cs
Normal 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
106
ATS/Models/StepModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
15
ATS/Models/SubProgramItem.cs
Normal file
15
ATS/Models/SubProgramItem.cs
Normal 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
45
ATS/Models/UserModel.cs
Normal 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
80
ATS/PreDefineDevices.json
Normal 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
101
ATS/SystemConfig.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
195
ATS/Tools/DataGridExportToExcel.cs
Normal file
195
ATS/Tools/DataGridExportToExcel.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
104
ATS/Tools/ExpressionEvaluator.cs
Normal file
104
ATS/Tools/ExpressionEvaluator.cs
Normal 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
114
ATS/Tools/Log.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
32
ATS/Tools/SecurityDongle.cs
Normal file
32
ATS/Tools/SecurityDongle.cs
Normal 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
BIN
ATS/ViKey.dll
Normal file
Binary file not shown.
43
ATS/Views/CANCatchSingalView.xaml
Normal file
43
ATS/Views/CANCatchSingalView.xaml
Normal 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>
|
||||
116
ATS/Views/CANCatchSingalView.xaml.cs
Normal file
116
ATS/Views/CANCatchSingalView.xaml.cs
Normal 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}")];
|
||||
}
|
||||
}
|
||||
}
|
||||
46
ATS/Views/CommandTreeView.xaml
Normal file
46
ATS/Views/CommandTreeView.xaml
Normal 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>
|
||||
841
ATS/Views/CommandTreeView.xaml.cs
Normal file
841
ATS/Views/CommandTreeView.xaml.cs
Normal 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
33
ATS/Views/LogArea.xaml
Normal 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
96
ATS/Views/LogArea.xaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
124
ATS/Views/ParametersManager.xaml
Normal file
124
ATS/Views/ParametersManager.xaml
Normal 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>
|
||||
262
ATS/Views/ParametersManager.xaml.cs
Normal file
262
ATS/Views/ParametersManager.xaml.cs
Normal 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} ] 成功");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
404
ATS/Views/SingleStepEdit.xaml
Normal file
404
ATS/Views/SingleStepEdit.xaml
Normal 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>
|
||||
130
ATS/Views/SingleStepEdit.xaml.cs
Normal file
130
ATS/Views/SingleStepEdit.xaml.cs
Normal 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
136
ATS/Views/StepsManager.xaml
Normal 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>
|
||||
311
ATS/Views/StepsManager.xaml.cs
Normal file
311
ATS/Views/StepsManager.xaml.cs
Normal 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
136
ATS/Views/ToolBar.xaml
Normal 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
613
ATS/Views/ToolBar.xaml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
174
ATS/Windows/DeviceManageWindow.xaml
Normal file
174
ATS/Windows/DeviceManageWindow.xaml
Normal 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>
|
||||
286
ATS/Windows/DeviceManageWindow.xaml.cs
Normal file
286
ATS/Windows/DeviceManageWindow.xaml.cs
Normal 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
|
||||
}
|
||||
}
|
||||
126
ATS/Windows/DeviceSettingWindow.xaml
Normal file
126
ATS/Windows/DeviceSettingWindow.xaml
Normal 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>
|
||||
261
ATS/Windows/DeviceSettingWindow.xaml.cs
Normal file
261
ATS/Windows/DeviceSettingWindow.xaml.cs
Normal 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
154
ATS/Windows/Login.xaml
Normal 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
97
ATS/Windows/Login.xaml.cs
Normal 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
102
ATS/Windows/MainWindow.xaml
Normal 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>
|
||||
244
ATS/Windows/MainWindow.xaml.cs
Normal file
244
ATS/Windows/MainWindow.xaml.cs
Normal 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; // 当前嵌套层级
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
122
ATS/Windows/ParameterSettingWindow.xaml
Normal file
122
ATS/Windows/ParameterSettingWindow.xaml
Normal 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>
|
||||
107
ATS/Windows/ParameterSettingWindow.xaml.cs
Normal file
107
ATS/Windows/ParameterSettingWindow.xaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
115
ATS/Windows/SystemConfigWindow.xaml
Normal file
115
ATS/Windows/SystemConfigWindow.xaml
Normal 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>
|
||||
87
ATS/Windows/SystemConfigWindow.xaml.cs
Normal file
87
ATS/Windows/SystemConfigWindow.xaml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
147
ATS/Windows/TestDataInfomationWindow.xaml
Normal file
147
ATS/Windows/TestDataInfomationWindow.xaml
Normal 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>
|
||||
188
ATS/Windows/TestDataInfomationWindow.xaml.cs
Normal file
188
ATS/Windows/TestDataInfomationWindow.xaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
85
ATS/Windows/UserSettingWindow.xaml
Normal file
85
ATS/Windows/UserSettingWindow.xaml
Normal 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>
|
||||
77
ATS/Windows/UserSettingWindow.xaml.cs
Normal file
77
ATS/Windows/UserSettingWindow.xaml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
98
ATS/Windows/UsersManage.xaml
Normal file
98
ATS/Windows/UsersManage.xaml
Normal 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>
|
||||
154
ATS/Windows/UsersManage.xaml.cs
Normal file
154
ATS/Windows/UsersManage.xaml.cs
Normal 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
25
ATS_DBContext/ATS_DB.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
22
ATS_DBContext/ATS_DBContext.csproj
Normal file
22
ATS_DBContext/ATS_DBContext.csproj
Normal 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>
|
||||
33
ATS_DBContext/Models/DataModel.cs
Normal file
33
ATS_DBContext/Models/DataModel.cs
Normal 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
14
Command/Command.csproj
Normal 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>
|
||||
599
Command/CommandApplication.cs
Normal file
599
Command/CommandApplication.cs
Normal 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
373
Command/CommandArray.cs
Normal 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
489
Command/CommandMath.cs
Normal 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 Param2:2 返回值:9</example>
|
||||
/// <example>Param1:2 Param2:3 返回值:8</example>
|
||||
/// <example>Param1:5 Param2:4 返回值: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 Param2:2 返回值:9</example>
|
||||
/// <example>Param1:2 Param2:3 返回值:8</example>
|
||||
/// <example>Param1:5 Param2:4 返回值: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
|
||||
}
|
||||
}
|
||||
155
Command/CommandRadixChange.cs
Normal file
155
Command/CommandRadixChange.cs
Normal 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
|
||||
|
||||
}
|
||||
}
|
||||
198
Command/CommandStringProcessing.cs
Normal file
198
Command/CommandStringProcessing.cs
Normal 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
465
Command/CommandSystem.cs
Normal 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
97
Command/CommandTime.cs
Normal 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
60
Command/Delay.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
45
Common/Attributes/ATSCommandAttribute.cs
Normal file
45
Common/Attributes/ATSCommandAttribute.cs
Normal 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
9
Common/Common.csproj
Normal file
@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
610
DeviceCommand/Base/ModbusRtu_Serial.cs
Normal file
610
DeviceCommand/Base/ModbusRtu_Serial.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
518
DeviceCommand/Base/ModbusRtu_Tcp.cs
Normal file
518
DeviceCommand/Base/ModbusRtu_Tcp.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
476
DeviceCommand/Base/ModbusRtu_Udp.cs
Normal file
476
DeviceCommand/Base/ModbusRtu_Udp.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
354
DeviceCommand/Base/ModbusTcp.cs
Normal file
354
DeviceCommand/Base/ModbusTcp.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
607
DeviceCommand/Base/SerialPort.cs
Normal file
607
DeviceCommand/Base/SerialPort.cs
Normal 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
262
DeviceCommand/Base/Tcp.cs
Normal 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
308
DeviceCommand/Base/Udp.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
256
DeviceCommand/Device/AMETEKSGX.cs
Normal file
256
DeviceCommand/Device/AMETEKSGX.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
326
DeviceCommand/Device/IT_N6322B.cs
Normal file
326
DeviceCommand/Device/IT_N6322B.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
231
DeviceCommand/Device/KeySight_Truevolt.cs
Normal file
231
DeviceCommand/Device/KeySight_Truevolt.cs
Normal 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
Loading…
Reference in New Issue
Block a user