diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..cb53e99 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# CA1416: 验证平台兼容性 +dotnet_diagnostic.CA1416.severity = silent diff --git a/ATS.sln b/ATS.sln new file mode 100644 index 0000000..349e881 --- /dev/null +++ b/ATS.sln @@ -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 diff --git a/ATS/ATS.csproj b/ATS/ATS.csproj new file mode 100644 index 0000000..74fce97 --- /dev/null +++ b/ATS/ATS.csproj @@ -0,0 +1,44 @@ + + + + WinExe + net8.0-windows + enable + enable + true + preview + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ..\TOSUNCAN\Interop.TSMasterAPI.dll + + + + diff --git a/ATS/App.xaml b/ATS/App.xaml new file mode 100644 index 0000000..2d2c712 --- /dev/null +++ b/ATS/App.xaml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pack://application:,,,/BVC;component/字体/MiSans-Normal.ttf#misans + + + + + + + + + + + + + + + + + + + + diff --git a/ATS/App.xaml.cs b/ATS/App.xaml.cs new file mode 100644 index 0000000..fcaad57 --- /dev/null +++ b/ATS/App.xaml.cs @@ -0,0 +1,25 @@ +using ATS.Tools; +using System.Configuration; +using System.Data; +using System.Windows; + +namespace ATS +{ + /// + /// Interaction logic for App.xaml + /// + 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); + } + } + +} diff --git a/ATS/AssemblyInfo.cs b/ATS/AssemblyInfo.cs new file mode 100644 index 0000000..b0ec827 --- /dev/null +++ b/ATS/AssemblyInfo.cs @@ -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) +)] diff --git a/ATS/Converters/BoolInverseConverter.cs b/ATS/Converters/BoolInverseConverter.cs new file mode 100644 index 0000000..05cdccf --- /dev/null +++ b/ATS/Converters/BoolInverseConverter.cs @@ -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; + } + } +} diff --git a/ATS/Converters/BooleanToVisibilityConverter.cs b/ATS/Converters/BooleanToVisibilityConverter.cs new file mode 100644 index 0000000..a93f291 --- /dev/null +++ b/ATS/Converters/BooleanToVisibilityConverter.cs @@ -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(); + } + } +} diff --git a/ATS/Converters/DeviceNameConverter.cs b/ATS/Converters/DeviceNameConverter.cs new file mode 100644 index 0000000..1f8087e --- /dev/null +++ b/ATS/Converters/DeviceNameConverter.cs @@ -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 { "无", "奇", "偶" }; + } + } + 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(); + } + } +} diff --git a/ATS/Converters/DeviceSettingWindowConverter.cs b/ATS/Converters/DeviceSettingWindowConverter.cs new file mode 100644 index 0000000..8b80a76 --- /dev/null +++ b/ATS/Converters/DeviceSettingWindowConverter.cs @@ -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>(value.ToString()); + } + else + { + return value; + } + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/ATS/Converters/EnumValueConverter.cs b/ATS/Converters/EnumValueConverter.cs new file mode 100644 index 0000000..9bcef69 --- /dev/null +++ b/ATS/Converters/EnumValueConverter.cs @@ -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]; + } + } + } +} \ No newline at end of file diff --git a/ATS/Converters/EnumValuesConverter.cs b/ATS/Converters/EnumValuesConverter.cs new file mode 100644 index 0000000..51b5807 --- /dev/null +++ b/ATS/Converters/EnumValuesConverter.cs @@ -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(); + } + } +} \ No newline at end of file diff --git a/ATS/Converters/FilteredParametersConverter.cs b/ATS/Converters/FilteredParametersConverter.cs new file mode 100644 index 0000000..531218c --- /dev/null +++ b/ATS/Converters/FilteredParametersConverter.cs @@ -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() + .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(); + } + } +} \ No newline at end of file diff --git a/ATS/Converters/GuidToParameterNameConverter.cs b/ATS/Converters/GuidToParameterNameConverter.cs new file mode 100644 index 0000000..89a1b1d --- /dev/null +++ b/ATS/Converters/GuidToParameterNameConverter.cs @@ -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(); + } + } +} diff --git a/ATS/Converters/HexConverter.cs b/ATS/Converters/HexConverter.cs new file mode 100644 index 0000000..78d7512 --- /dev/null +++ b/ATS/Converters/HexConverter.cs @@ -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; + } + } +} diff --git a/ATS/Converters/HexToDecimalConverter.cs b/ATS/Converters/HexToDecimalConverter.cs new file mode 100644 index 0000000..17bcd14 --- /dev/null +++ b/ATS/Converters/HexToDecimalConverter.cs @@ -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; + } + } +} diff --git a/ATS/Converters/IsEnumTypeConverter .cs b/ATS/Converters/IsEnumTypeConverter .cs new file mode 100644 index 0000000..00eedb1 --- /dev/null +++ b/ATS/Converters/IsEnumTypeConverter .cs @@ -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(); + } + } +} diff --git a/ATS/Converters/ParameterCategoryToStringConverter.cs b/ATS/Converters/ParameterCategoryToStringConverter.cs new file mode 100644 index 0000000..d1d4c76 --- /dev/null +++ b/ATS/Converters/ParameterCategoryToStringConverter.cs @@ -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(); + } + } +} diff --git a/ATS/Converters/ParameterCategoryToVisibilityConverter.cs b/ATS/Converters/ParameterCategoryToVisibilityConverter.cs new file mode 100644 index 0000000..6ed94fc --- /dev/null +++ b/ATS/Converters/ParameterCategoryToVisibilityConverter.cs @@ -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(); + } + } +} diff --git a/ATS/Converters/ParameterToGotoSettingStringConverter.cs b/ATS/Converters/ParameterToGotoSettingStringConverter.cs new file mode 100644 index 0000000..58e903a --- /dev/null +++ b/ATS/Converters/ParameterToGotoSettingStringConverter.cs @@ -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; + } + } +} diff --git a/ATS/Converters/ParameterTypeToBoolConverter.cs b/ATS/Converters/ParameterTypeToBoolConverter.cs new file mode 100644 index 0000000..f00819c --- /dev/null +++ b/ATS/Converters/ParameterTypeToBoolConverter.cs @@ -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(); + } + } +} diff --git a/ATS/Converters/ParameterValueToStringConverter.cs b/ATS/Converters/ParameterValueToStringConverter.cs new file mode 100644 index 0000000..20b393a --- /dev/null +++ b/ATS/Converters/ParameterValueToStringConverter.cs @@ -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().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 ""; + } + } +} diff --git a/ATS/Converters/StepResultToStringConverter.cs b/ATS/Converters/StepResultToStringConverter.cs new file mode 100644 index 0000000..015edb1 --- /dev/null +++ b/ATS/Converters/StepResultToStringConverter.cs @@ -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(); + } + } +} diff --git a/ATS/Converters/StringToVisibilityConverter.cs b/ATS/Converters/StringToVisibilityConverter.cs new file mode 100644 index 0000000..29c5c7b --- /dev/null +++ b/ATS/Converters/StringToVisibilityConverter.cs @@ -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(); + } + } +} diff --git a/ATS/FodyWeavers.xml b/ATS/FodyWeavers.xml new file mode 100644 index 0000000..d5abfed --- /dev/null +++ b/ATS/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ATS/Logic/DeviceConnect.cs b/ATS/Logic/DeviceConnect.cs new file mode 100644 index 0000000..356804b --- /dev/null +++ b/ATS/Logic/DeviceConnect.cs @@ -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>(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>(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>(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>(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>(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>(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>(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>(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>(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>(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>(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>(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>(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>(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", + _ => "", + }; + } + } +} diff --git a/ATS/Logic/StepRunning.cs b/ATS/Logic/StepRunning.cs new file mode 100644 index 0000000..503c70e --- /dev/null +++ b/ATS/Logic/StepRunning.cs @@ -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 tmpParameters = []; + + private static readonly Stopwatch stepStopwatch = new(); + + private static readonly Stack loopStopwatchStack = new(); + + private static readonly Stack loopStack = new(); + + public static CancellationTokenSource stepCTS = new(); + private static bool SubSingleStep = false; + + private static Guid TestRoundID; + + public static async Task 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(); + } + + 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 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(); + var paramTypes = new List(); + 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) + 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().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(); + } + + // 始终写入报告,但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 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(string[] parts, out T[] result, Func 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 + + } +} \ No newline at end of file diff --git a/ATS/Models/CANSignalModel.cs b/ATS/Models/CANSignalModel.cs new file mode 100644 index 0000000..2c42b83 --- /dev/null +++ b/ATS/Models/CANSignalModel.cs @@ -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; }//记录间隔 + } +} diff --git a/ATS/Models/DeviceConnectSettingModel.cs b/ATS/Models/DeviceConnectSettingModel.cs new file mode 100644 index 0000000..8045f88 --- /dev/null +++ b/ATS/Models/DeviceConnectSettingModel.cs @@ -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; } = ""; + } +} diff --git a/ATS/Models/DeviceModel.cs b/ATS/Models/DeviceModel.cs new file mode 100644 index 0000000..6b720c8 --- /dev/null +++ b/ATS/Models/DeviceModel.cs @@ -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; } = ""; + } +} diff --git a/ATS/Models/MethodModel.cs b/ATS/Models/MethodModel.cs new file mode 100644 index 0000000..334b71b --- /dev/null +++ b/ATS/Models/MethodModel.cs @@ -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( + source.Parameters.Select(p => new ParameterModel(p))); + } + + #endregion + + public string? Name { get; set; } + + public string? FullName { get; set; } + + public ObservableCollection Parameters { get; set; } = []; + } +} diff --git a/ATS/Models/ParameterModel.cs b/ATS/Models/ParameterModel.cs new file mode 100644 index 0000000..53aa7e5 --- /dev/null +++ b/ATS/Models/ParameterModel.cs @@ -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; } + + /// + /// 是否输出到报告(仅对输出参数有效,默认为false) + /// + public bool IsOutputToReport { get; set; } = false; + + public enum ParameterCategory + { + Input, + Output, + Temp + } + + public object? GetActualValue(Dictionary paraList) + { + // 用于检测循环引用的哈希集合 + HashSet 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; + } + + /// + /// 获取方法步骤中的返回值映射的主程序参数 + /// + /// + /// + public ParameterModel? GetCurrentParameter(Dictionary paraList) + { + // 用于检测循环引用的哈希集合 + HashSet visitedIds = new HashSet(); + 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; + } + } +} diff --git a/ATS/Models/ProgramModel.cs b/ATS/Models/ProgramModel.cs new file mode 100644 index 0000000..a5074ec --- /dev/null +++ b/ATS/Models/ProgramModel.cs @@ -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 StepCollection { get; set; } = []; + + public ObservableCollection Parameters { get; set; } = []; + + public ObservableCollection Devices { get; set; } = []; + } +} diff --git a/ATS/Models/ReportModel.cs b/ATS/Models/ReportModel.cs new file mode 100644 index 0000000..c6e6975 --- /dev/null +++ b/ATS/Models/ReportModel.cs @@ -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 ReportList { get; set; } = new(); + } + + +} diff --git a/ATS/Models/StepModel.cs b/ATS/Models/StepModel.cs new file mode 100644 index 0000000..03398a0 --- /dev/null +++ b/ATS/Models/StepModel.cs @@ -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; } + + /// + /// 仅LoopStart使用 + /// + public int? LoopCount { get; set; } + + /// + /// 运行时循环计数 + /// + [JsonIgnore] + public int? CurrentLoopCount { get; set; } + + /// + /// 仅LoopEnd使用,关联LoopStart + /// + public Guid? LoopStartStepId { get; set; } + + /// + /// 初始:-1 运行中:0 成功:1 异常:2 + /// + [JsonIgnore] + public int Result { get; set; } = -1; + + [JsonIgnore] + public int? RunTime { get; set; } + + public string? OKExpression { get; set; } + + /// + /// 默认为0/0不跳转,第一位为OK跳转的步骤序号,第二位为NG跳转的步骤序号 + /// + 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; } + } +} diff --git a/ATS/Models/SubProgramItem.cs b/ATS/Models/SubProgramItem.cs new file mode 100644 index 0000000..55b28bd --- /dev/null +++ b/ATS/Models/SubProgramItem.cs @@ -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; } = ""; + } +} diff --git a/ATS/Models/UserModel.cs b/ATS/Models/UserModel.cs new file mode 100644 index 0000000..11e2987 --- /dev/null +++ b/ATS/Models/UserModel.cs @@ -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; } + + /// + /// 0:用户;1:管理员;2:超级管理员 + /// + public int Role { get; set; } = 0; + + public DateTime LoginTime { get; set; } + + public int LoginCount { get; set; } + } +} diff --git a/ATS/PreDefineDevices.json b/ATS/PreDefineDevices.json new file mode 100644 index 0000000..bd992eb --- /dev/null +++ b/ATS/PreDefineDevices.json @@ -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连接示例" + } +] \ No newline at end of file diff --git a/ATS/SystemConfig.cs b/ATS/SystemConfig.cs new file mode 100644 index 0000000..b727555 --- /dev/null +++ b/ATS/SystemConfig.cs @@ -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(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}"); + } + } + } + } +} \ No newline at end of file diff --git a/ATS/Tools/DataGridExportToExcel.cs b/ATS/Tools/DataGridExportToExcel.cs new file mode 100644 index 0000000..1ea9712 --- /dev/null +++ b/ATS/Tools/DataGridExportToExcel.cs @@ -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}"); + } + } + } +} diff --git a/ATS/Tools/ExpressionEvaluator.cs b/ATS/Tools/ExpressionEvaluator.cs new file mode 100644 index 0000000..559035b --- /dev/null +++ b/ATS/Tools/ExpressionEvaluator.cs @@ -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? 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) PreprocessExpression( + string expression, + Dictionary? variables) + { + if (variables == null || variables.Count == 0) + return (expression, variables ?? new Dictionary()); + + // 生成变量名映射 (中文 -> 英文别名) + var chineseToAlias = new Dictionary(); + var aliasToValue = new Dictionary(); + 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(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); + } + } +} \ No newline at end of file diff --git a/ATS/Tools/Log.cs b/ATS/Tools/Log.cs new file mode 100644 index 0000000..188ae06 --- /dev/null +++ b/ATS/Tools/Log.cs @@ -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? 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; + } + } +} diff --git a/ATS/Tools/SecurityDongle.cs b/ATS/Tools/SecurityDongle.cs new file mode 100644 index 0000000..926828b --- /dev/null +++ b/ATS/Tools/SecurityDongle.cs @@ -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; + } + } +} diff --git a/ATS/ViKey.dll b/ATS/ViKey.dll new file mode 100644 index 0000000..39d2b36 Binary files /dev/null and b/ATS/ViKey.dll differ diff --git a/ATS/Views/CANCatchSingalView.xaml b/ATS/Views/CANCatchSingalView.xaml new file mode 100644 index 0000000..2d51aac --- /dev/null +++ b/ATS/Views/CANCatchSingalView.xaml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ATS/Windows/Login.xaml.cs b/ATS/Windows/Login.xaml.cs new file mode 100644 index 0000000..0c4eadc --- /dev/null +++ b/ATS/Windows/Login.xaml.cs @@ -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 +{ + /// + /// Login.xaml 的交互逻辑 + /// + public partial class Login : MetroWindow + { + public List UserList { get; set; } = new List(); + + 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>(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); + } + } + } +} diff --git a/ATS/Windows/MainWindow.xaml b/ATS/Windows/MainWindow.xaml new file mode 100644 index 0000000..dd27d7c --- /dev/null +++ b/ATS/Windows/MainWindow.xaml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ATS/Windows/MainWindow.xaml.cs b/ATS/Windows/MainWindow.xaml.cs new file mode 100644 index 0000000..cc081ca --- /dev/null +++ b/ATS/Windows/MainWindow.xaml.cs @@ -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 +{ + /// + /// MainWindow.xaml 的交互逻辑 + /// + [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 辅助方法 + + /// + /// 根据序号重新对步骤进行排序 + /// + 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 _parentProgramStack = new(); + private Stack _programPathStack = new(); // 用于显示标题路径 + private Stack _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 _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; // 当前嵌套层级 + + + + } +} diff --git a/ATS/Windows/ParameterSettingWindow.xaml b/ATS/Windows/ParameterSettingWindow.xaml new file mode 100644 index 0000000..18435ad --- /dev/null +++ b/ATS/Windows/ParameterSettingWindow.xaml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +