添加项目文件。
This commit is contained in:
54
UIShare/Behaviors/MouseDoubleClickBehavior.cs
Normal file
54
UIShare/Behaviors/MouseDoubleClickBehavior.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using Microsoft.Xaml.Behaviors;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace UIShare.Behaviors
|
||||
{
|
||||
//给Border添加双击事件
|
||||
public class MouseDoubleClickBehavior : Behavior<Border>
|
||||
{
|
||||
public static readonly DependencyProperty CommandProperty =
|
||||
DependencyProperty.Register("Command", typeof(ICommand), typeof(MouseDoubleClickBehavior), new PropertyMetadata(null));
|
||||
|
||||
public ICommand Command
|
||||
{
|
||||
get => (ICommand)GetValue(CommandProperty);
|
||||
set => SetValue(CommandProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty CommandParameterProperty =
|
||||
DependencyProperty.Register("CommandParameter", typeof(object), typeof(MouseDoubleClickBehavior), new PropertyMetadata(null));
|
||||
|
||||
public object CommandParameter
|
||||
{
|
||||
get => GetValue(CommandParameterProperty);
|
||||
set => SetValue(CommandParameterProperty, value);
|
||||
}
|
||||
|
||||
protected override void OnAttached()
|
||||
{
|
||||
base.OnAttached();
|
||||
// 确保 Border 的 Background 不为 null,否则无法触发点击事件
|
||||
this.AssociatedObject.MouseLeftButtonDown += OnMouseLeftButtonDown;
|
||||
}
|
||||
|
||||
protected override void OnDetaching()
|
||||
{
|
||||
base.OnDetaching();
|
||||
this.AssociatedObject.MouseLeftButtonDown -= OnMouseLeftButtonDown;
|
||||
}
|
||||
|
||||
private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
// 判断点击次数是否为 2
|
||||
if (e.ClickCount == 2)
|
||||
{
|
||||
if (Command != null && Command.CanExecute(CommandParameter))
|
||||
{
|
||||
Command.Execute(CommandParameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
59
UIShare/Behaviors/TabControlSelectionChangedBehavior.cs
Normal file
59
UIShare/Behaviors/TabControlSelectionChangedBehavior.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using Microsoft.Xaml.Behaviors;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace UIShare.Behaviors
|
||||
{
|
||||
public class TabControlSelectionChangedBehavior : Behavior<TabControl>
|
||||
{
|
||||
public static readonly DependencyProperty CommandProperty =
|
||||
DependencyProperty.Register(
|
||||
"Command", typeof(ICommand), typeof(TabControlSelectionChangedBehavior), new PropertyMetadata(null));
|
||||
|
||||
public static readonly DependencyProperty CommandParameterProperty =
|
||||
DependencyProperty.Register(
|
||||
"CommandParameter", typeof(object), typeof(TabControlSelectionChangedBehavior), new PropertyMetadata(null));
|
||||
|
||||
public ICommand Command
|
||||
{
|
||||
get { return (ICommand)GetValue(CommandProperty); }
|
||||
set { SetValue(CommandProperty, value); }
|
||||
}
|
||||
|
||||
public object CommandParameter
|
||||
{
|
||||
get { return GetValue(CommandParameterProperty); }
|
||||
set { SetValue(CommandParameterProperty, value); }
|
||||
}
|
||||
|
||||
protected override void OnAttached()
|
||||
{
|
||||
base.OnAttached();
|
||||
if (AssociatedObject != null)
|
||||
{
|
||||
AssociatedObject.SelectionChanged += OnSelectionChanged;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDetaching()
|
||||
{
|
||||
if (AssociatedObject != null)
|
||||
{
|
||||
AssociatedObject.SelectionChanged -= OnSelectionChanged;
|
||||
}
|
||||
base.OnDetaching();
|
||||
}
|
||||
|
||||
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
var tabItem = AssociatedObject.SelectedItem as TabItem;
|
||||
// 获取选中 TabItem 的 Header
|
||||
if (Command != null && Command.CanExecute(tabItem.Header))
|
||||
{
|
||||
// 使用选中 TabItem 的 Header 作为 CommandParameter
|
||||
Command.Execute(((TabItem)AssociatedObject.SelectedItem)?.Header);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
UIShare/Converters/BoolArrayConverter.cs
Normal file
40
UIShare/Converters/BoolArrayConverter.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace UIShare.Converters
|
||||
{
|
||||
public class BoolArrayConverter : IValueConverter
|
||||
{
|
||||
// bool[] -> string
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is bool[] arr)
|
||||
return "[" + string.Join(",", arr.Select(b => b.ToString().ToLower())) + "]";
|
||||
return "";
|
||||
}
|
||||
|
||||
// string -> bool[]
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is string s)
|
||||
{
|
||||
s = s.Trim('[', ']', ' ');
|
||||
if (string.IsNullOrWhiteSpace(s)) return Array.Empty<bool>();
|
||||
var parts = s.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
return parts.Select(p =>
|
||||
{
|
||||
if (bool.TryParse(p, out var b)) return b;
|
||||
if (p == "1") return true;
|
||||
if (p == "0") return false;
|
||||
return false;
|
||||
}).ToArray();
|
||||
}
|
||||
return Array.Empty<bool>();
|
||||
}
|
||||
}
|
||||
}
|
||||
40
UIShare/Converters/BooleanToVisibilityConverter.cs
Normal file
40
UIShare/Converters/BooleanToVisibilityConverter.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
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 UIShare.Converters
|
||||
{
|
||||
public class BooleanToVisibilityConverter : IValueConverter
|
||||
{
|
||||
// 同时接受 "invert" / "inverse" / "inverted" / "reverse"(大小写不敏感)作为反转参数,
|
||||
// 避免因 XAML 处用了 ConverterParameter=Inverse 而静默失效。
|
||||
private static bool IsInvert(object parameter)
|
||||
{
|
||||
var s = parameter?.ToString()?.ToLowerInvariant();
|
||||
return s == "invert" || s == "inverse" || s == "inverted" || s == "reverse";
|
||||
}
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
bool input = value is bool b && b;
|
||||
if (IsInvert(parameter))
|
||||
input = !input;
|
||||
|
||||
return input ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
bool output = value is Visibility v && v == Visibility.Visible;
|
||||
if (IsInvert(parameter))
|
||||
output = !output;
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
}
|
||||
49
UIShare/Converters/DeviceNameConverter.cs
Normal file
49
UIShare/Converters/DeviceNameConverter.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace UIShare.Converters
|
||||
{
|
||||
public class DeviceNameConverter : IValueConverter
|
||||
{
|
||||
private readonly string[] specialName = { "奇偶" };
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is string name)
|
||||
{
|
||||
if (specialName.Contains(name))
|
||||
{
|
||||
if (parameter?.ToString() == "Inverse")
|
||||
{
|
||||
return Visibility.Visible;
|
||||
}
|
||||
else if (parameter?.ToString() == "Items")
|
||||
{
|
||||
switch (name)
|
||||
{
|
||||
case "奇偶":
|
||||
return new List<string> { "无", "奇", "偶" };
|
||||
}
|
||||
}
|
||||
return Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
if (parameter?.ToString() == "Inverse")
|
||||
{
|
||||
return Visibility.Collapsed;
|
||||
}
|
||||
return Visibility.Visible;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
82
UIShare/Converters/EnumValueConverter.cs
Normal file
82
UIShare/Converters/EnumValueConverter.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace UIShare.Converters
|
||||
{
|
||||
public class EnumValueConverter : IMultiValueConverter
|
||||
{
|
||||
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
// 验证输入参数
|
||||
if (values.Length < 2 || values[0] == null || values[1] == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 获取枚举类型
|
||||
Type enumType = values[0] as Type;
|
||||
if (enumType == null || !enumType.IsEnum)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取数值
|
||||
object value = values[1];
|
||||
|
||||
// 确保数值类型匹配枚举的底层类型
|
||||
Type underlyingType = Enum.GetUnderlyingType(enumType);
|
||||
object convertedValue;
|
||||
|
||||
try
|
||||
{
|
||||
convertedValue = System.Convert.ChangeType(value, underlyingType);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 如果转换失败,尝试直接使用原始值
|
||||
convertedValue = value;
|
||||
}
|
||||
|
||||
// 将数值转换为枚举值
|
||||
return Enum.ToObject(enumType, convertedValue);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 发生任何异常时返回null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 获取枚举值的底层数值
|
||||
Type enumType = value.GetType();
|
||||
if (!enumType.IsEnum)
|
||||
{
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
Type underlyingType = Enum.GetUnderlyingType(enumType);
|
||||
object numericValue = System.Convert.ChangeType(value, underlyingType);
|
||||
|
||||
// 返回枚举类型和对应的数值
|
||||
return [enumType, numericValue];
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [null, null];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
UIShare/Converters/EnumValuesConverter.cs
Normal file
23
UIShare/Converters/EnumValuesConverter.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace UIShare.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
74
UIShare/Converters/FilteredParametersConverter.cs
Normal file
74
UIShare/Converters/FilteredParametersConverter.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
|
||||
using UIShare.UIViewModel;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace UIShare.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<ParameterVM>()
|
||||
.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;
|
||||
|
||||
// 如果类型完全相同,则匹配
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
40
UIShare/Converters/HexConverter.cs
Normal file
40
UIShare/Converters/HexConverter.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace UIShare.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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
22
UIShare/Converters/InverseBooleanConverter.cs
Normal file
22
UIShare/Converters/InverseBooleanConverter.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace UIShare.Converters
|
||||
{
|
||||
public class InverseBooleanConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
// null 视为 false,再取反 → true
|
||||
var b = value as bool?;
|
||||
return !(b ?? false);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
var b = value as bool?;
|
||||
return !(b ?? false);
|
||||
}
|
||||
}
|
||||
}
|
||||
36
UIShare/Converters/IsEnumTypeConverter .cs
Normal file
36
UIShare/Converters/IsEnumTypeConverter .cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace UIShare.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
29
UIShare/Converters/LessThanConverter.cs
Normal file
29
UIShare/Converters/LessThanConverter.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace UIShare.Converters // 确保命名空间跟你项目一致
|
||||
{
|
||||
public class LessThanConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is double actualWidth && parameter != null)
|
||||
{
|
||||
if (double.TryParse(parameter.ToString(), out double targetWidth))
|
||||
{
|
||||
// 如果实际宽度 小于 设定的阈值(比如600),返回 True
|
||||
//return actualWidth < targetWidth;
|
||||
//工控机屏幕较大直接给1000
|
||||
return actualWidth < 1000;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
41
UIShare/Converters/ParameterCategoryToStringConverter.cs
Normal file
41
UIShare/Converters/ParameterCategoryToStringConverter.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
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 UIShare.UIViewModel.ParameterVM;
|
||||
|
||||
|
||||
|
||||
namespace UIShare.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
41
UIShare/Converters/ParameterCategoryToVisibilityConverter.cs
Normal file
41
UIShare/Converters/ParameterCategoryToVisibilityConverter.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
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 UIShare.UIViewModel.ParameterVM;
|
||||
|
||||
|
||||
|
||||
namespace UIShare.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
31
UIShare/Converters/ParameterTypeToBoolConverter.cs
Normal file
31
UIShare/Converters/ParameterTypeToBoolConverter.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace UIShare.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
37
UIShare/Converters/ParameterValueToStringConverter.cs
Normal file
37
UIShare/Converters/ParameterValueToStringConverter.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
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 UIShare.Converters
|
||||
{
|
||||
public class ParameterValueToStringConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is IEnumerable enumerable && !(value is string))
|
||||
{
|
||||
var elements = enumerable.Cast<object>().Select(item => item?.ToString() ?? "null");
|
||||
return $"[{string.Join(", ", elements)}]";
|
||||
}
|
||||
else if(value != null)
|
||||
{
|
||||
return value.ToString()!;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value != null)
|
||||
{
|
||||
return value.ToString()!;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
35
UIShare/Converters/StringToVisibilityConverter.cs
Normal file
35
UIShare/Converters/StringToVisibilityConverter.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace UIShare.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
38
UIShare/Converters/TimeSpanToStringConverter.cs
Normal file
38
UIShare/Converters/TimeSpanToStringConverter.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace UIShare.Converters
|
||||
{
|
||||
public class TimeSpanToStringConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is TimeSpan timeSpan)
|
||||
{
|
||||
// 如果天数大于0,则显示天数
|
||||
if (timeSpan.Days > 0)
|
||||
{
|
||||
return $"{timeSpan.Days}天{timeSpan.Hours:D2}:{timeSpan.Minutes:D2}:{timeSpan.Seconds:D2}";
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果不超过一天,则只显示时:分:秒
|
||||
return $"{timeSpan.Hours:D2}:{timeSpan.Minutes:D2}:{timeSpan.Seconds:D2}";
|
||||
}
|
||||
}
|
||||
|
||||
return "00:00:00";
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
// 通常不需要从字符串转换回TimeSpan,所以这里返回UnsetValue
|
||||
return System.Windows.DependencyProperty.UnsetValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
157
UIShare/GlobalVariable/CANSignalBroadcaster.cs
Normal file
157
UIShare/GlobalVariable/CANSignalBroadcaster.cs
Normal file
@@ -0,0 +1,157 @@
|
||||
using Model.Models;
|
||||
using Prism.Events;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UIShare.PubEvent;
|
||||
using ZLGUSBCANFD;
|
||||
|
||||
namespace UIShare.GlobalVariable
|
||||
{
|
||||
/// <summary>
|
||||
/// CAN 信号广播器(全局单例):
|
||||
/// 统一订阅 GlobalInfo.CanPool 中所有已实例化的 ZLGCANFD 设备,
|
||||
/// 将 DBC 解码后的信号值广播给所有引用该 CAN 设备的作用域,
|
||||
/// 并同步检查各作用域 ValueLimitList 的超限报警。
|
||||
/// </summary>
|
||||
public class CANSignalBroadcaster : IDisposable
|
||||
{
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly GlobalInfo _globalInfo;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>已订阅解码事件的 CANFD 实例 → 委托 映射</summary>
|
||||
private readonly Dictionary<ZLGCANFD, Action<uint, ZDBC.DBCMessage>> _handlers = new();
|
||||
|
||||
public CANSignalBroadcaster(GlobalInfo globalInfo, IEventAggregator eventAggregator)
|
||||
{
|
||||
_globalInfo = globalInfo;
|
||||
_eventAggregator = eventAggregator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 扫描 GlobalInfo.CanPool 中已创建的 CANFD 实例并订阅解码事件。
|
||||
/// 幂等:已订阅的实例不会重复订阅。
|
||||
/// </summary>
|
||||
public void Discover()
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
foreach (var kvp in _globalInfo.CanPool)
|
||||
{
|
||||
string fingerprint = kvp.Key;
|
||||
var lazy = kvp.Value;
|
||||
if (!lazy.IsValueCreated || lazy.Value == null) continue;
|
||||
|
||||
var canfd = lazy.Value;
|
||||
if (_handlers.ContainsKey(canfd)) continue;
|
||||
|
||||
// 使用闭包捕获指纹,回调时即可区分不同 CAN 卡
|
||||
Action<uint, ZDBC.DBCMessage> handler = (channel, msg) => OnDbcMessageDecoded(fingerprint, channel, msg);
|
||||
canfd.OnDbcMessageDecoded += handler;
|
||||
_handlers[canfd] = handler;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动广播器(目前 Discover 已完成订阅,此处保留以兼容 HardwareDataBroadcaster 的使用模式)。
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
Discover();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DBC 解码回调:提取所有信号,向引用该 CAN 设备的作用域广播,并检查报警。
|
||||
/// </summary>
|
||||
private void OnDbcMessageDecoded(string canFingerprint, uint channel, ZDBC.DBCMessage msg)
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
string signalFingerprint = BuildFingerprint(canFingerprint, channel);
|
||||
var now = DateTime.Now;
|
||||
|
||||
// 获取引用该 CAN 设备的所有作用域
|
||||
var scopes = GetScopesForFingerprint(canFingerprint);
|
||||
if (scopes.Count == 0) return;
|
||||
|
||||
for (int i = 0; i < msg.nSignalCount; i++)
|
||||
{
|
||||
var signal = msg.vSignals[i];
|
||||
string signalName = Encoding.Default.GetString(signal.strName).TrimEnd('\0');
|
||||
if (string.IsNullOrEmpty(signalName)) continue;
|
||||
|
||||
double physicalValue = signal.nRawvalue * signal.nFactor + signal.nOffset;
|
||||
string methodName = BuildMethodName(msg.nID, signalName);
|
||||
|
||||
foreach (var scope in scopes)
|
||||
{
|
||||
_eventAggregator.GetEvent<HardwareDataReportedEvent>().Publish(new HardwareReportArgs
|
||||
{
|
||||
Scope = scope,
|
||||
HardwareFingerprint = signalFingerprint,
|
||||
MethodName = methodName,
|
||||
Value = physicalValue,
|
||||
Time = now
|
||||
});
|
||||
|
||||
string MonitorStatus = ValueLimitAlarmHelper.CheckAlarm(scope, signalFingerprint, methodName, physicalValue, _globalInfo);
|
||||
if (MonitorStatus != "" && MonitorStatus != "未报警")
|
||||
{
|
||||
_eventAggregator.GetEvent<AlarmEvent>().Publish((scope,canFingerprint, MonitorStatus));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>获取指定硬件指纹当前被哪些作用域引用</summary>
|
||||
private List<string> GetScopesForFingerprint(string fingerprint)
|
||||
{
|
||||
if (_globalInfo.DeviceAndScopeDic.TryGetValue(fingerprint, out var lazy))
|
||||
{
|
||||
var scopeList = lazy.Value;
|
||||
lock (scopeList) return scopeList.ToList();
|
||||
}
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成 CAN 信号的 DisplayName 格式:"{MessageName}.{SignalName}"
|
||||
/// </summary>
|
||||
public static string BuildDisplayName(string messageName, string signalName)
|
||||
{
|
||||
return $"{messageName}.{signalName}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成 CAN 信号的 MethodName 格式:"{MessageId:X}.{SignalName}"
|
||||
/// </summary>
|
||||
public static string BuildMethodName(uint messageId, string signalName)
|
||||
{
|
||||
return $"{messageId:X}.{signalName}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成 CAN 信号的 Fingerprint 格式:"{canDeviceFingerprint}:{channel}"
|
||||
/// canDeviceFingerprint 来自 DeviceManager.ExtractHardwareFingerprint,如 "CAN:0"
|
||||
/// </summary>
|
||||
public static string BuildFingerprint(string canDeviceFingerprint, uint channel)
|
||||
{
|
||||
return $"{canDeviceFingerprint}:{channel}";
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
foreach (var kvp in _handlers)
|
||||
{
|
||||
if (kvp.Key != null)
|
||||
kvp.Key.OnDbcMessageDecoded -= kvp.Value;
|
||||
}
|
||||
_handlers.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
139
UIShare/GlobalVariable/ConfigService.cs
Normal file
139
UIShare/GlobalVariable/ConfigService.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Logger;
|
||||
using UIShare.UIViewModel;
|
||||
|
||||
namespace UIShare.GlobalVariable
|
||||
{
|
||||
public static class ConfigService
|
||||
{
|
||||
private static readonly object _fileLock = new();
|
||||
/// <summary>
|
||||
/// 根据标题查询配置文件是否存在
|
||||
/// </summary>
|
||||
public static bool IsExit(string title)
|
||||
{
|
||||
if (string.IsNullOrEmpty(title))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string configPath = Path.Combine(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ACP"), $"{title}.json");
|
||||
|
||||
if (!File.Exists(configPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/// <summary>
|
||||
/// 根据标题(格子标识)加载独立的配置文件
|
||||
/// </summary>
|
||||
public static SystemConfig Load(string title)
|
||||
{
|
||||
if (string.IsNullOrEmpty(title))
|
||||
{
|
||||
throw new ArgumentException("配置标题不能为空", nameof(title));
|
||||
}
|
||||
|
||||
// 临时实例化一个对象以获取默认的 SystemPath
|
||||
var dummy = new SystemConfig();
|
||||
string configPath = Path.Combine(dummy.SystemPath, $"{title}.json");
|
||||
|
||||
if (!File.Exists(configPath))
|
||||
{
|
||||
// 如果不存在,创建一个带 Title 的默认配置并保存
|
||||
var defaultConfig = new SystemConfig { Title = title };
|
||||
EnsureDefaultCanDevice(defaultConfig);
|
||||
Save(defaultConfig);
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
lock (_fileLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
string json = File.ReadAllText(configPath);
|
||||
var config = JsonConvert.DeserializeObject<SystemConfig>(json, new JsonSerializerSettings
|
||||
{
|
||||
TypeNameHandling = TypeNameHandling.All
|
||||
});
|
||||
|
||||
config ??= new SystemConfig { Title = title };
|
||||
EnsureDefaultCanDevice(config);
|
||||
return config;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.ErrorWithNotify(title, $"格子 [{title}] 配置加载失败: {ex.Message}");
|
||||
var fallback = new SystemConfig { Title = title };
|
||||
EnsureDefaultCanDevice(fallback);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存指定的配置实例
|
||||
/// </summary>
|
||||
public static void Save(SystemConfig config)
|
||||
{
|
||||
if (config == null || string.IsNullOrEmpty(config.Title)) return;
|
||||
|
||||
lock (_fileLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(config.SystemPath))
|
||||
Directory.CreateDirectory(config.SystemPath);
|
||||
|
||||
string configPath = Path.Combine(config.SystemPath, $"{config.Title}.json");
|
||||
|
||||
string json = JsonConvert.SerializeObject(config, Formatting.Indented, new JsonSerializerSettings
|
||||
{
|
||||
TypeNameHandling = TypeNameHandling.All
|
||||
});
|
||||
|
||||
File.WriteAllText(configPath, json);
|
||||
LoggerHelper.InfoWithNotify(config.Title, $"配置 [{config.Title}] 已保存。");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.ErrorWithNotify(config.Title, $"配置 [{config.Title}] 保存失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确保配置中至少包含一个 CAN 设备(对应 SystemConfig.CANFD)。
|
||||
/// 旧配置或空配置会自动升级,使用户在设置界面能看到 CAN 设备。
|
||||
/// </summary>
|
||||
public static void EnsureDefaultCanDevice(SystemConfig config)
|
||||
{
|
||||
if (config.DeviceList == null)
|
||||
{
|
||||
config.DeviceList = new ObservableCollection<DeviceInfoVM>();
|
||||
}
|
||||
|
||||
bool hasCan = config.DeviceList.Any(d =>
|
||||
string.Equals(d?.ConnectionType, "CAN", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(d?.DeviceType, "ZLGCANFD", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!hasCan)
|
||||
{
|
||||
config.DeviceList.Add(new DeviceInfoVM
|
||||
{
|
||||
DeviceName = "CAN",
|
||||
DeviceType = "ZLGCANFD",
|
||||
Remark = "周立功 CANFD 接口卡",
|
||||
ConnectionType = "CAN",
|
||||
IsEnabled = true,
|
||||
IsConnected = false
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
676
UIShare/GlobalVariable/DeviceManager.cs
Normal file
676
UIShare/GlobalVariable/DeviceManager.cs
Normal file
@@ -0,0 +1,676 @@
|
||||
using DeviceCommand.Base;
|
||||
using DeviceCommand.Devices;
|
||||
using Logger;
|
||||
using Model.Models;
|
||||
using Prism.Events;
|
||||
using Prism.Ioc;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Ports;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UIShare.PubEvent;
|
||||
using UIShare.UIViewModel;
|
||||
using ZLGUSBCANFD;
|
||||
|
||||
namespace UIShare.GlobalVariable
|
||||
{
|
||||
/// <summary>
|
||||
/// 设备管理器:根据 <see cref="SystemConfig.DeviceList"/> 反射实例化所有启用的设备,
|
||||
/// 通过 <see cref="IBaseInterface"/> 多态统一管理,避免为每种设备单独硬编码字段。
|
||||
/// </summary>
|
||||
public class DeviceManager:IDisposable
|
||||
{
|
||||
private object _lockObj = new object();
|
||||
public SystemConfig _systemConfig { get; set; }
|
||||
private readonly GlobalInfo _globalInfo;
|
||||
private readonly string _scopeName;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
|
||||
/// <summary>按 DeviceName 索引的设备字典,便于业务层按名取实例。</summary>
|
||||
public IDictionary<string, IBaseInterface> DeviceMap { get; private set; }
|
||||
= new Dictionary<string, IBaseInterface>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>类名 → Type 的反射缓存(仅扫描一次)。</summary>
|
||||
private static readonly IReadOnlyDictionary<string, Type> _deviceTypeMap = BuildDeviceTypeMap();
|
||||
public ZLGCANFD CANFD { get; set; }
|
||||
public IOBoardGroup IOGroup { get; set; }
|
||||
|
||||
public DeviceManager(SystemConfig systemConfig, GlobalInfo globalInfo, IEventAggregator eventAggregator)
|
||||
{
|
||||
_systemConfig = systemConfig;
|
||||
_globalInfo = globalInfo;
|
||||
_eventAggregator = eventAggregator;
|
||||
// 用 SystemConfig.Title 作为作用域唯一标识,无需反查 ConfigDic
|
||||
_scopeName = _systemConfig.Title;
|
||||
InitDevices();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据设备配置提取唯一的硬件指纹字符串。
|
||||
/// <para>Tcp → "Tcp:IP:Port";Serial → "Serial:PortName";无法识别则返回空字符串。</para>
|
||||
/// </summary>
|
||||
public static string ExtractHardwareFingerprint(DeviceInfoVM config)
|
||||
{
|
||||
if (string.Equals(config.ConnectionType, "Tcp", StringComparison.OrdinalIgnoreCase)
|
||||
&& config.TcpConfig != null)
|
||||
{
|
||||
return $"Tcp:{config.TcpConfig.IPAddress}:{config.TcpConfig.Port}";
|
||||
}
|
||||
|
||||
if (string.Equals(config.ConnectionType, "Serial", StringComparison.OrdinalIgnoreCase)
|
||||
&& config.SerialPortConfig != null)
|
||||
{
|
||||
return $"Serial:{config.SerialPortConfig.PortName}";
|
||||
}
|
||||
|
||||
if (string.Equals(config.ConnectionType, "CAN", StringComparison.OrdinalIgnoreCase)
|
||||
&& config.CANConfig != null)
|
||||
{
|
||||
return $"CAN:{config.CANConfig.DeviceIndex}";
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前作用域配置的 CAN 设备硬件指纹(与 GlobalInfo.CanPool 的 Key 一致)。
|
||||
/// 如 "CAN:0",其中 0 是 CANConfig.DeviceIndex。
|
||||
/// </summary>
|
||||
public string GetCanDeviceFingerprint()
|
||||
{
|
||||
if (_systemConfig?.DeviceList == null) return string.Empty;
|
||||
var canConfig = _systemConfig.DeviceList.FirstOrDefault(d =>
|
||||
d != null && d.IsEnabled &&
|
||||
string.Equals(d.ConnectionType, "CAN", StringComparison.OrdinalIgnoreCase));
|
||||
return canConfig != null ? ExtractHardwareFingerprint(canConfig) : string.Empty;
|
||||
}
|
||||
|
||||
private void InitDevices()
|
||||
{
|
||||
DeviceMap = new Dictionary<string, IBaseInterface>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (_systemConfig?.DeviceList == null) return;
|
||||
|
||||
foreach (var config in _systemConfig.DeviceList)
|
||||
{
|
||||
if (config == null || !config.IsEnabled) continue;
|
||||
|
||||
// CAN 设备:ZLGCANFD 不实现 IBaseInterface,通过 SystemConfig.CANFD 单独管理,
|
||||
// 按指纹从全局 CanPool 创建/复用实例,并注册作用域引用计数。
|
||||
if (string.Equals(config.ConnectionType, "CAN", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var fingerprint = ExtractHardwareFingerprint(config);
|
||||
if (string.IsNullOrEmpty(fingerprint))
|
||||
{
|
||||
LoggerHelper.Warn($"设备 [{config.DeviceName}] 无法提取硬件指纹(连接方式={config.ConnectionType}),已跳过。");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (config.CANConfig == null)
|
||||
{
|
||||
LoggerHelper.Warn($"设备 [{config.DeviceName}] 缺少 CAN 连接参数,已跳过。");
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 按指纹全局唯一创建 ZLGCANFD 实例(maxChannels 默认 4,对应 USBCANFD-400U)
|
||||
// 波特率与终端电阻从 CANConfigVM 传入,初始化并启动通道 时直接使用
|
||||
var canLazy = _globalInfo.CanPool.GetOrAdd(fingerprint, key => new Lazy<ZLGCANFD>(() =>
|
||||
new ZLGCANFD(config.CANConfig.DeviceType, config.CANConfig.DeviceIndex, 4,
|
||||
config.CANConfig.ABitBaud, config.CANConfig.DBitBaud, config.CANConfig.EnableTerminalResistance)));
|
||||
|
||||
_systemConfig.CANFD = canLazy.Value;
|
||||
CANFD = canLazy.Value;
|
||||
|
||||
// 注册作用域引用计数
|
||||
if (!string.IsNullOrEmpty(_scopeName))
|
||||
{
|
||||
var scopeList = _globalInfo.DeviceAndScopeDic.GetOrAdd(fingerprint,
|
||||
_ => new Lazy<List<string>>(() => new List<string>())).Value;
|
||||
lock (scopeList)
|
||||
{
|
||||
if (!scopeList.Contains(_scopeName))
|
||||
scopeList.Add(_scopeName);
|
||||
}
|
||||
}
|
||||
|
||||
LoggerHelper.Info($"已加载 CAN 设备 [{config.DeviceName}] 指纹={fingerprint}(通过 SystemConfig.CANFD 管理)");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.ErrorWithNotify(_scopeName, $"CAN 设备 [{config.DeviceName}] 实例化失败:{ex.Message}");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(config.DeviceType) ||
|
||||
!_deviceTypeMap.TryGetValue(config.DeviceType, out var deviceType))
|
||||
{
|
||||
LoggerHelper.Warn($"未识别的设备类型 [{config.DeviceType}],已跳过 [{config.DeviceName}]。");
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 第一步:提取硬件指纹,为空则跳过
|
||||
var fingerprint = ExtractHardwareFingerprint(config);
|
||||
if (string.IsNullOrEmpty(fingerprint))
|
||||
{
|
||||
LoggerHelper.Warn($"设备 [{config.DeviceName}] 无法提取硬件指纹(连接方式={config.ConnectionType}),已跳过。");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 第二步:原子化获取或添加 Lazy 包装盒,确保同一指纹全局只创建一个实例
|
||||
var lazy = _globalInfo.HardwarePool.GetOrAdd(fingerprint, key => new Lazy<IBaseInterface>(() =>
|
||||
{
|
||||
return config.ConnectionType switch
|
||||
{
|
||||
"Tcp" => CreateTcpDevice(deviceType, config.TcpConfig)!,
|
||||
"Serial" => CreateSerialDevice(deviceType, config.SerialPortConfig)!,
|
||||
_ => null!
|
||||
};
|
||||
}));
|
||||
|
||||
// 第三步:安全拆盒,Lazy 内部线程锁保证只实例化一次
|
||||
var instance = lazy.Value;
|
||||
|
||||
if (instance == null)
|
||||
{
|
||||
LoggerHelper.Warn($"设备 [{config.DeviceName}] 连接方式 [{config.ConnectionType}] 不支持,已跳过。");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 第四步:绑定逻辑名 → 同一物理设备可被多个台架名称映射
|
||||
if (!string.IsNullOrWhiteSpace(config.DeviceName))
|
||||
{
|
||||
DeviceMap[config.DeviceName] = instance;
|
||||
}
|
||||
|
||||
// 第五步:将当前作用域注册到指纹的作用域列表,用于引用计数与安全销毁
|
||||
if (!string.IsNullOrEmpty(_scopeName))
|
||||
{
|
||||
var scopeList = _globalInfo.DeviceAndScopeDic.GetOrAdd(fingerprint,
|
||||
_ => new Lazy<List<string>>(() => new List<string>())).Value;
|
||||
lock (scopeList)
|
||||
{
|
||||
if (!scopeList.Contains(_scopeName))
|
||||
scopeList.Add(_scopeName);
|
||||
}
|
||||
}
|
||||
|
||||
LoggerHelper.Info($"已加载设备 [{config.DeviceName} / {config.DeviceType} / {config.ConnectionType}] 指纹={fingerprint}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var inner = ex.InnerException?.Message ?? ex.Message;
|
||||
LoggerHelper.ErrorWithNotify(_scopeName, $"设备 [{config.DeviceName}] 实例化失败:{inner}");
|
||||
}
|
||||
}
|
||||
|
||||
// IOGroup 初始化:从已实例化的设备中取出前两个 IOBoard 实例组建 IOBoardGroup
|
||||
try
|
||||
{
|
||||
var ioBoards = DeviceMap.Values.OfType<IOBoard>().Take(2).ToList();
|
||||
if (ioBoards.Count == 2)
|
||||
{
|
||||
IOGroup = new IOBoardGroup(ioBoards[0], ioBoards[1]);
|
||||
LoggerHelper.Info($"IOBoardGroup 已初始化,Board1={ioBoards[0].GetType().Name}, Board2={ioBoards[1].GetType().Name}。");
|
||||
}
|
||||
else
|
||||
{
|
||||
LoggerHelper.Warn($"IOBoardGroup 初始化跳过:需要 2 个 IOBoard 实例,当前仅找到 {ioBoards.Count} 个。");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.ErrorWithNotify(_scopeName, $"IOBoardGroup 初始化失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ConnectAllDevices(CancellationToken ct = default)
|
||||
{
|
||||
if (_systemConfig?.DeviceList == null) return;
|
||||
|
||||
var tasks = new List<Task>();
|
||||
foreach (var info in _systemConfig.DeviceList)
|
||||
{
|
||||
if (info == null || !info.IsEnabled) continue;
|
||||
if (string.IsNullOrWhiteSpace(info.DeviceName)) continue;
|
||||
|
||||
// CAN 设备:直接打开 CAN 卡
|
||||
if (string.Equals(info.ConnectionType, "CAN", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
tasks.Add(ConnectCanAsync(info));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!DeviceMap.TryGetValue(info.DeviceName, out var device)) continue;
|
||||
tasks.Add(ConnectInternalAsync(info, device, ct));
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
|
||||
public async Task ConnectSpecifiedDevice(string deviceName, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(deviceName))
|
||||
{
|
||||
LoggerHelper.Warn("ConnectSpecifiedDevice:设备名为空。");
|
||||
return;
|
||||
}
|
||||
|
||||
var info = _systemConfig?.DeviceList?
|
||||
.FirstOrDefault(d => d != null && string.Equals(d.DeviceName, deviceName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (info == null)
|
||||
{
|
||||
LoggerHelper.Warn($"ConnectSpecifiedDevice:未找到设备配置 [{deviceName}]。");
|
||||
return;
|
||||
}
|
||||
|
||||
// CAN 设备:直接打开 CAN 卡
|
||||
if (string.Equals(info.ConnectionType, "CAN", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await ConnectCanAsync(info);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!DeviceMap.TryGetValue(deviceName, out var device))
|
||||
{
|
||||
LoggerHelper.Warn($"ConnectSpecifiedDevice:未找到设备 [{deviceName}]。");
|
||||
return;
|
||||
}
|
||||
|
||||
await ConnectInternalAsync(info, device, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步关闭指定设备,释放底层连接并更新 UI 状态
|
||||
/// </summary>
|
||||
public async Task CloseDeviceAsync(string deviceName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(deviceName)) return;
|
||||
|
||||
var info = _systemConfig?.DeviceList?
|
||||
.FirstOrDefault(d => d != null && string.Equals(d.DeviceName, deviceName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
// CAN 设备:直接关闭 CAN 卡
|
||||
if (info != null && string.Equals(info.ConnectionType, "CAN", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await CloseCanAsync(info);
|
||||
return;
|
||||
}
|
||||
|
||||
IBaseInterface? device;
|
||||
|
||||
lock (_lockObj)
|
||||
{
|
||||
if (!DeviceMap.TryGetValue(deviceName, out device)) return;
|
||||
}
|
||||
|
||||
await CloseInternalAsync(info, device);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步关闭所有设备
|
||||
/// </summary>
|
||||
public async Task CloseAllDevicesAsync()
|
||||
{
|
||||
List<Task> tasks = new List<Task>();
|
||||
|
||||
lock (_lockObj)
|
||||
{
|
||||
if (DeviceMap.Count == 0 && (CANFD == null)) return;
|
||||
|
||||
foreach (var kvp in DeviceMap)
|
||||
{
|
||||
string deviceName = kvp.Key;
|
||||
var device = kvp.Value;
|
||||
var info = _systemConfig?.DeviceList?
|
||||
.FirstOrDefault(d => d != null && string.Equals(d.DeviceName, deviceName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
tasks.Add(CloseInternalAsync(info, device));
|
||||
}
|
||||
}
|
||||
|
||||
// CAN 设备:直接关闭 CAN 卡
|
||||
if (CANFD != null)
|
||||
{
|
||||
var canInfo = _systemConfig?.DeviceList?
|
||||
.FirstOrDefault(d => d != null && string.Equals(d.ConnectionType, "CAN", StringComparison.OrdinalIgnoreCase));
|
||||
if (canInfo != null)
|
||||
{
|
||||
tasks.Add(CloseCanAsync(canInfo));
|
||||
}
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
LoggerHelper.Info("所有设备已执行关闭操作。");
|
||||
}
|
||||
|
||||
#region 辅助方法
|
||||
private async Task CloseInternalAsync(DeviceInfoVM? info, IBaseInterface device)
|
||||
{
|
||||
string name = info?.DeviceName ?? device.GetType().Name;
|
||||
string conn = info?.ConnectionType ?? "?";
|
||||
|
||||
try
|
||||
{
|
||||
// 如果设备本身已经是断开状态,直接更新 UI 并返回
|
||||
if (!device.IsConnected)
|
||||
{
|
||||
if (info != null) info.IsConnected = false;
|
||||
LoggerHelper.Info($"设备 [{name}] 本就处于断开状态。");
|
||||
return;
|
||||
}
|
||||
await Task.Run(() => device.Close());
|
||||
|
||||
LoggerHelper.Info($"设备 [{name}/{conn}] 已成功关闭连接。");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var inner = ex.InnerException?.Message ?? ex.Message;
|
||||
LoggerHelper.Error($"设备 [{name}/{conn}] 关闭连接时出现异常: {inner}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 无论关闭时是否抛出异常,均强制同步 UI 状态为未连接
|
||||
if (info != null)
|
||||
{
|
||||
info.IsConnected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
private async Task ConnectInternalAsync(DeviceInfoVM? info, IBaseInterface device, CancellationToken ct)
|
||||
{
|
||||
string name = info?.DeviceName ?? device.GetType().Name;
|
||||
string conn = info?.ConnectionType ?? "?";
|
||||
|
||||
try
|
||||
{
|
||||
if (device.IsConnected)
|
||||
{
|
||||
if (info != null) info.IsConnected = true;
|
||||
LoggerHelper.Info($"设备 [{name}] 已连接,跳过。");
|
||||
return;
|
||||
}
|
||||
|
||||
bool ok = conn switch
|
||||
{
|
||||
"Tcp" => await ConnectTcpAsync(name, device, ct),
|
||||
"Serial" => await ConnectSerialAsync(name, device, ct),
|
||||
_ => false
|
||||
};
|
||||
|
||||
if (info != null) info.IsConnected = ok;
|
||||
|
||||
if (ok)
|
||||
LoggerHelper.Info($"设备 [{name}/{conn}] 连接成功。");
|
||||
else
|
||||
LoggerHelper.Warn($"设备 [{name}/{conn}] 连接失败。");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
if (info != null) info.IsConnected = false;
|
||||
LoggerHelper.Warn($"设备 [{name}/{conn}] 连接已取消。");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (info != null) info.IsConnected = false;
|
||||
var inner = ex.InnerException?.Message ?? ex.Message;
|
||||
LoggerHelper.ErrorWithNotify(_scopeName, $"设备 [{name}/{conn}] 连接异常:{inner}");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<bool> ConnectTcpAsync(string name, IBaseInterface device, CancellationToken ct)
|
||||
{
|
||||
if (device is ITcp tcp)
|
||||
{
|
||||
return await tcp.ConnectAsync(ct);
|
||||
}
|
||||
if (device is IModbusDevice modbusTCP)
|
||||
{
|
||||
return await modbusTCP.ConnectAsync(ct);
|
||||
}
|
||||
LoggerHelper.Warn($"设备 [{name}] 配置为 Tcp 但未实现 ITcp,实际类型为 {device.GetType().Name}。");
|
||||
return false;
|
||||
}
|
||||
|
||||
private static async Task<bool> ConnectSerialAsync(string name, IBaseInterface device, CancellationToken ct)
|
||||
{
|
||||
if (device is not ISerialPort sp)
|
||||
{
|
||||
LoggerHelper.Warn($"设备 [{name}] 配置为 Serial 但未实现 ISerialPort,实际类型为 {device.GetType().Name}。");
|
||||
return false;
|
||||
}
|
||||
return await sp.ConnectAsync(ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打开 CAN 卡:打开设备 + 初始化并启动所有通道 + 自动加载 DBC + 启动信号广播
|
||||
/// </summary>
|
||||
private async Task<bool> ConnectCanAsync(DeviceInfoVM info)
|
||||
{
|
||||
string name = info.DeviceName ?? "CAN";
|
||||
|
||||
try
|
||||
{
|
||||
if (CANFD == null)
|
||||
{
|
||||
LoggerHelper.Warn($"CAN 设备 [{name}] 尚未实例化,无法连接。");
|
||||
info.IsConnected = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (info.IsConnected)
|
||||
{
|
||||
LoggerHelper.Info($"CAN 设备 [{name}] 已连接,跳过。");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ok = await Task.Run(() =>
|
||||
{
|
||||
if (!CANFD.打开设备()) return false;
|
||||
// 自动加载 DBC 文件
|
||||
if (_systemConfig?.DBCAutoLoadList != null)
|
||||
{
|
||||
foreach (var item in _systemConfig.DBCAutoLoadList)
|
||||
{
|
||||
if (item.DBCChannel < 0 || item.DBCChannel >= 4) continue;
|
||||
if (string.IsNullOrWhiteSpace(item.DBCFilePath)) continue;
|
||||
|
||||
if (!File.Exists(item.DBCFilePath))
|
||||
{
|
||||
// 发布 DBC 卸载事件,通知监控系统清除对应信号
|
||||
_eventAggregator.GetEvent<DBCUnloadedEvent>().Publish(new DBCUnloadedArgs
|
||||
{
|
||||
Channel = (uint)item.DBCChannel,
|
||||
Scope = _scopeName
|
||||
});
|
||||
LoggerHelper.Warn($"CAN 通道 {item.DBCChannel} 自动加载 DBC 失败:文件不存在 [{item.DBCFilePath}]");
|
||||
continue;
|
||||
}
|
||||
CANFD.初始化并启动通道((uint)item.DBCChannel);
|
||||
bool loadOk = CANFD.加载通道DBC文件((uint)item.DBCChannel, item.DBCFilePath);
|
||||
if (loadOk)
|
||||
{
|
||||
LoggerHelper.Info($"CAN 通道 {item.DBCChannel} 已自动加载 DBC:{item.DBCFilePath}");
|
||||
// 发布 DBC 加载完成事件,通知监控系统刷新信号列表
|
||||
_eventAggregator.GetEvent<DBCLoadedEvent>().Publish(new DBCLoadedArgs
|
||||
{
|
||||
Channel = (uint)item.DBCChannel,
|
||||
Scope = _scopeName
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
LoggerHelper.Warn($"CAN 通道 {item.DBCChannel} 自动加载 DBC 失败:{item.DBCFilePath}");
|
||||
_eventAggregator.GetEvent<DBCUnloadedEvent>().Publish(new DBCUnloadedArgs
|
||||
{
|
||||
Channel = (uint)item.DBCChannel,
|
||||
Scope = _scopeName
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
info.IsConnected = ok;
|
||||
|
||||
if (ok)
|
||||
LoggerHelper.Info($"CAN 设备 [{name}] 连接成功,已初始化 {CANFD.DBCParser.MaxChannels} 个通道。");
|
||||
else
|
||||
LoggerHelper.Warn($"CAN 设备 [{name}] 连接失败。");
|
||||
|
||||
return ok;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
info.IsConnected = false;
|
||||
var inner = ex.InnerException?.Message ?? ex.Message;
|
||||
LoggerHelper.ErrorWithNotify(_scopeName, $"CAN 设备 [{name}] 连接异常:{inner}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 关闭 CAN 卡
|
||||
/// </summary>
|
||||
private async Task CloseCanAsync(DeviceInfoVM info)
|
||||
{
|
||||
string name = info.DeviceName ?? "CAN";
|
||||
|
||||
try
|
||||
{
|
||||
if (CANFD == null)
|
||||
{
|
||||
info.IsConnected = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!info.IsConnected)
|
||||
{
|
||||
LoggerHelper.Info($"CAN 设备 [{name}] 本就处于断开状态。");
|
||||
return;
|
||||
}
|
||||
|
||||
await Task.Run(() => CANFD.关闭CAN卡设备());
|
||||
LoggerHelper.Info($"CAN 设备 [{name}] 已成功关闭连接。");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var inner = ex.InnerException?.Message ?? ex.Message;
|
||||
LoggerHelper.Error($"CAN 设备 [{name}] 关闭连接时出现异常: {inner}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
info.IsConnected = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, Type> BuildDeviceTypeMap()
|
||||
{
|
||||
try
|
||||
{
|
||||
return typeof(IBaseInterface).Assembly
|
||||
.GetTypes()
|
||||
.Where(t => t.IsClass && !t.IsAbstract && typeof(IBaseInterface).IsAssignableFrom(t))
|
||||
.ToDictionary(t => t.Name, t => t, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
catch (ReflectionTypeLoadException ex)
|
||||
{
|
||||
LoggerHelper.Error($"扫描设备类型失败:{ex.Message}");
|
||||
return new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
private static IBaseInterface? CreateTcpDevice(Type type, TcpConfigVM? vm)
|
||||
{
|
||||
vm ??= new TcpConfigVM();
|
||||
var cfg = new TcpConfig
|
||||
{
|
||||
IPAddress = vm.IPAddress,
|
||||
Port = vm.Port,
|
||||
SendTimeout = vm.SendTimeout,
|
||||
ReceiveTimeout = vm.ReceiveTimeout
|
||||
};
|
||||
return Activator.CreateInstance(type, cfg) as IBaseInterface;
|
||||
}
|
||||
private static IBaseInterface? CreateSerialDevice(Type type, SerialPortConfigVM? vm)
|
||||
{
|
||||
vm ??= new SerialPortConfigVM();
|
||||
var cfg = new SerialPortConfig
|
||||
{
|
||||
PortName = vm.PortName,
|
||||
BaudRate = vm.BaudRate,
|
||||
DataBits = vm.DataBits,
|
||||
StopBits = Enum.TryParse<StopBits>(vm.StopBits, true, out var sb) ? sb : StopBits.One,
|
||||
Parity = Enum.TryParse<Parity>(vm.Parity, true, out var pa) ? pa : Parity.None,
|
||||
ReadTimeout = vm.ReadTimeout,
|
||||
WriteTimeout = vm.WriteTimeout
|
||||
};
|
||||
return Activator.CreateInstance(type, cfg) as IBaseInterface;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_scopeName)) return;
|
||||
|
||||
// 遍历当前作用域用到的所有指纹,逐一移除本作用域的引用
|
||||
var fingerprintsToRemove = new List<string>();
|
||||
|
||||
foreach (var kvp in _globalInfo.DeviceAndScopeDic)
|
||||
{
|
||||
string fingerprint = kvp.Key;
|
||||
var scopeList = kvp.Value.IsValueCreated ? kvp.Value.Value : null;
|
||||
if (scopeList == null) continue;
|
||||
|
||||
lock (scopeList)
|
||||
{
|
||||
scopeList.Remove(_scopeName);
|
||||
|
||||
// 引用归零 → 标记为待清理
|
||||
if (scopeList.Count == 0)
|
||||
fingerprintsToRemove.Add(fingerprint);
|
||||
}
|
||||
}
|
||||
|
||||
// 对引用归零的指纹:销毁设备实例 + 从全局池中移除
|
||||
foreach (var fingerprint in fingerprintsToRemove)
|
||||
{
|
||||
// 尝试从 HardwarePool 取出并销毁
|
||||
if (_globalInfo.HardwarePool.TryRemove(fingerprint, out var lazy))
|
||||
{
|
||||
if (lazy.IsValueCreated && lazy.Value is IBaseInterface device)
|
||||
{
|
||||
try { device.Close(); }
|
||||
catch { /* 销毁时忽略异常 */ }
|
||||
LoggerHelper.Info($"指纹 [{fingerprint}] 无作用域引用,已销毁设备实例。");
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试从 CanPool 取出并销毁
|
||||
if (_globalInfo.CanPool.TryRemove(fingerprint, out var canLazy))
|
||||
{
|
||||
if (canLazy.IsValueCreated)
|
||||
{
|
||||
try { canLazy.Value.Dispose(); }
|
||||
catch { /* 销毁时忽略异常 */ }
|
||||
LoggerHelper.Info($"指纹 [{fingerprint}] 无作用域引用,已销毁 CAN 设备实例。");
|
||||
}
|
||||
}
|
||||
|
||||
// 同步清除 DeviceAndScopeDic 中的空条目
|
||||
_globalInfo.DeviceAndScopeDic.TryRemove(fingerprint, out _);
|
||||
}
|
||||
|
||||
DeviceMap.Clear();
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
58
UIShare/GlobalVariable/GlobalInfo.cs
Normal file
58
UIShare/GlobalVariable/GlobalInfo.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using DeviceCommand.Base;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using ZLGUSBCANFD;
|
||||
|
||||
namespace UIShare.GlobalVariable
|
||||
{
|
||||
public class GlobalInfo:BindableBase
|
||||
{
|
||||
public event EventHandler? ScopeChanged;
|
||||
public Dictionary<string,ScopedContext> ContextDic { get; set; }
|
||||
public Dictionary<string,StepRunning> StepRunningDic { get; set; }
|
||||
public Dictionary<string, SystemConfig> ConfigDic { get; set; }
|
||||
public Dictionary<string, IScopedProvider> ScopeDic { get; set; }
|
||||
|
||||
/// <summary>硬件指纹 → 设备实例的并发池,确保同一物理硬件全局只创建一个驱动实例。</summary>
|
||||
public ConcurrentDictionary<string, Lazy<IBaseInterface>> HardwarePool { get; set; }
|
||||
|
||||
/// <summary>CAN 硬件指纹 → ZLGCANFD 实例的并发池,确保同一 CAN 卡全局只创建一个驱动实例。</summary>
|
||||
public ConcurrentDictionary<string, Lazy<ZLGCANFD>> CanPool { get; set; }
|
||||
|
||||
/// <summary>硬件指纹 → 正在使用该设备的作用域名称列表,用于引用计数与安全销毁。</summary>
|
||||
public ConcurrentDictionary<string, Lazy<List<string>>> DeviceAndScopeDic { get; set; }
|
||||
|
||||
public String UserName { get; set; } = "Not Logged in";
|
||||
public bool IsAdmin { get; set; } = true;
|
||||
public string CurrentOpeningScope;
|
||||
private string _currentScope = "default";
|
||||
public string CurrentScope
|
||||
{
|
||||
get => _currentScope;
|
||||
set
|
||||
{
|
||||
if (_currentScope != value)
|
||||
{
|
||||
_currentScope = value;
|
||||
ScopeChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
public GlobalInfo()
|
||||
{
|
||||
ContextDic = new();
|
||||
StepRunningDic = new();
|
||||
ConfigDic = new();
|
||||
ScopeDic = new();
|
||||
HardwarePool = new ConcurrentDictionary<string, Lazy<IBaseInterface>>(StringComparer.OrdinalIgnoreCase);
|
||||
CanPool = new ConcurrentDictionary<string, Lazy<ZLGCANFD>>(StringComparer.OrdinalIgnoreCase);
|
||||
DeviceAndScopeDic = new ConcurrentDictionary<string, Lazy<List<string>>>(StringComparer.OrdinalIgnoreCase);
|
||||
CurrentScope = "default";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
214
UIShare/GlobalVariable/HardwareDataBroadcaster.cs
Normal file
214
UIShare/GlobalVariable/HardwareDataBroadcaster.cs
Normal file
@@ -0,0 +1,214 @@
|
||||
using Common.Attributes;
|
||||
using DeviceCommand.Base;
|
||||
using Model.Models;
|
||||
using Prism.Events;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Threading;
|
||||
using UIShare.PubEvent;
|
||||
|
||||
namespace UIShare.GlobalVariable
|
||||
{
|
||||
/// <summary>
|
||||
/// 硬件数据广播器(中间层,全局单例):
|
||||
/// 直接基于 GlobalInfo.HardwarePool 中的设备指纹字典进行统一采样,
|
||||
/// 每个物理设备只采样一次,然后向所有引用该设备的作用域分别广播 HardwareDataReportedEvent。
|
||||
/// 解决多作用域共享同一物理设备时的重复采样问题。
|
||||
/// DeviceCommand 层无需引用 Prism,完全解耦。
|
||||
/// </summary>
|
||||
public class HardwareDataBroadcaster : IDisposable
|
||||
{
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly GlobalInfo _globalInfo;
|
||||
|
||||
/// <summary>采样定时器</summary>
|
||||
private readonly DispatcherTimer _sampleTimer;
|
||||
|
||||
/// <summary>
|
||||
/// 已注册的采样项:(Fingerprint, MethodName, 编译后的委托, DeviceInstance)
|
||||
/// 委托签名统一为 Func<object, CancellationToken, Task<string>>
|
||||
/// - 第一个参数 = 设备实例(open delegate 风格,编译时已做类型转换)
|
||||
/// - 第二个参数 = CancellationToken(无参方法会忽略它)
|
||||
/// - 返回值 = Task<string>
|
||||
/// </summary>
|
||||
private readonly List<(string Fingerprint, string MethodName, Func<object, CancellationToken, Task<string>> Invoker, object Device)> _registeredMethods = new();
|
||||
|
||||
private CancellationTokenSource? _cts;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>采样间隔(默认 1000ms)</summary>
|
||||
public TimeSpan SampleInterval
|
||||
{
|
||||
get => _sampleTimer.Interval;
|
||||
set => _sampleTimer.Interval = value;
|
||||
}
|
||||
|
||||
public HardwareDataBroadcaster(GlobalInfo globalInfo, IEventAggregator eventAggregator)
|
||||
{
|
||||
_globalInfo = globalInfo;
|
||||
_eventAggregator = eventAggregator;
|
||||
|
||||
_sampleTimer = new DispatcherTimer(DispatcherPriority.Normal)
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(1000)
|
||||
};
|
||||
_sampleTimer.Tick += OnSampleTick;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 MethodInfo 编译为强类型委托,后续调用零反射开销。
|
||||
/// 统一签名为 Func<object, CancellationToken, Task<string>>:
|
||||
/// - 无参方法:忽略 CancellationToken
|
||||
/// - 带 CancellationToken 方法:直接透传
|
||||
/// </summary>
|
||||
private static Func<object, CancellationToken, Task<string>> BuildInvoker(MethodInfo method)
|
||||
{
|
||||
var deviceParam = Expression.Parameter(typeof(object), "device");
|
||||
var ctParam = Expression.Parameter(typeof(CancellationToken), "ct");
|
||||
|
||||
// 将 object 转型为方法的声明类型
|
||||
var castDevice = Expression.Convert(deviceParam, method.DeclaringType!);
|
||||
|
||||
// 根据方法签名决定传参
|
||||
var parms = method.GetParameters();
|
||||
Expression[] callArgs = parms.Length == 1
|
||||
? new Expression[] { ctParam }
|
||||
: Array.Empty<Expression>();
|
||||
|
||||
var call = Expression.Call(castDevice, method, callArgs);
|
||||
|
||||
return Expression.Lambda<Func<object, CancellationToken, Task<string>>>(
|
||||
call, deviceParam, ctParam).Compile();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 扫描 GlobalInfo.HardwarePool 中所有已创建的物理设备,
|
||||
/// 反查可监测方法并编译为委托。每个指纹只注册一次,反射只执行一次。
|
||||
/// </summary>
|
||||
public void Discover()
|
||||
{
|
||||
_registeredMethods.Clear();
|
||||
|
||||
foreach (var poolEntry in _globalInfo.HardwarePool)
|
||||
{
|
||||
string fingerprint = poolEntry.Key;
|
||||
var lazy = poolEntry.Value;
|
||||
|
||||
// 只监控已经实例化的设备
|
||||
if (!lazy.IsValueCreated || lazy.Value == null) continue;
|
||||
|
||||
var device = lazy.Value;
|
||||
var deviceType = device.GetType();
|
||||
|
||||
var methods = deviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(m =>
|
||||
{
|
||||
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
|
||||
if (m.ReturnType != typeof(Task<string>)) return false;
|
||||
var parms = m.GetParameters();
|
||||
return parms.Length == 0 ||
|
||||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
|
||||
});
|
||||
|
||||
foreach (var method in methods)
|
||||
{
|
||||
var invoker = BuildInvoker(method);
|
||||
_registeredMethods.Add((fingerprint, method.Name, invoker, device));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>获取指定硬件指纹当前被哪些作用域引用</summary>
|
||||
private List<string> GetScopesForFingerprint(string fingerprint)
|
||||
{
|
||||
if (_globalInfo.DeviceAndScopeDic.TryGetValue(fingerprint, out var lazy))
|
||||
{
|
||||
var scopeList = lazy.Value;
|
||||
lock (scopeList) return scopeList.ToList();
|
||||
}
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
/// <summary>启动采样广播(幂等)</summary>
|
||||
public void Start()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_cts ??= new CancellationTokenSource();
|
||||
if (!_sampleTimer.IsEnabled) _sampleTimer.Start();
|
||||
}
|
||||
|
||||
/// <summary>停止采样</summary>
|
||||
public void Stop()
|
||||
{
|
||||
_sampleTimer.Stop();
|
||||
}
|
||||
|
||||
private void OnSampleTick(object? sender, EventArgs e)
|
||||
{
|
||||
if (_registeredMethods.Count == 0 || _disposed) return;
|
||||
|
||||
var token = _cts?.Token ?? CancellationToken.None;
|
||||
var now = DateTime.Now;
|
||||
|
||||
foreach (var entry in _registeredMethods)
|
||||
{
|
||||
// fire-and-forget:每个通道独立采样,完成后自行广播
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// 直接委托调用,零反射开销
|
||||
string raw = await entry.Invoker(entry.Device, token).ConfigureAwait(false);
|
||||
|
||||
if (!double.TryParse(raw, out double value)) return;
|
||||
|
||||
// 向所有引用该物理设备的作用域分别广播
|
||||
var scopes = GetScopesForFingerprint(entry.Fingerprint);
|
||||
foreach (var scope in scopes)
|
||||
{
|
||||
_eventAggregator.GetEvent<HardwareDataReportedEvent>().Publish(new HardwareReportArgs
|
||||
{
|
||||
Scope = scope,
|
||||
HardwareFingerprint = entry.Fingerprint,
|
||||
MethodName = entry.MethodName,
|
||||
Value = value,
|
||||
Time = now
|
||||
});
|
||||
|
||||
// 同步检查该作用域 ValueLimitList 是否超限
|
||||
string MonitorStatus= ValueLimitAlarmHelper.CheckAlarm(scope, entry.Fingerprint, entry.MethodName, value, _globalInfo);
|
||||
if (MonitorStatus != "" && MonitorStatus != "未报警")
|
||||
{
|
||||
_eventAggregator.GetEvent<AlarmEvent>().Publish((scope,entry.Fingerprint, MonitorStatus));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 单个通道故障不干扰其他通道
|
||||
}
|
||||
}, token);
|
||||
}
|
||||
|
||||
// 不在 UI 线程 await——任务在线程池上自行完成并广播事件,
|
||||
// DispatcherTimer 按设定间隔准时触发下一次 Tick。
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
_sampleTimer.Stop();
|
||||
_sampleTimer.Tick -= OnSampleTick;
|
||||
try { _cts?.Cancel(); } catch { }
|
||||
_cts?.Dispose();
|
||||
_cts = null;
|
||||
_registeredMethods.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
48
UIShare/GlobalVariable/ScopeLogDispatcher.cs
Normal file
48
UIShare/GlobalVariable/ScopeLogDispatcher.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using Logger;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace UIShare.GlobalVariable
|
||||
{
|
||||
/// <summary>
|
||||
/// 作用域日志分发器:根据显式传入的 scope 参数把日志路由到对应
|
||||
/// <see cref="ScopedContext.LogBuffer"/>,实现每个台架/作用域拥有独立 LogArea。
|
||||
/// 直接写入 ConcurrentQueue,不走 Progress<T> / SynchronizationContext,避免 UI 线程压力。
|
||||
/// </summary>
|
||||
public class ScopeLogDispatcher : IProgress<(string scope, string message, string color, int depth)>
|
||||
{
|
||||
private readonly GlobalInfo _globalInfo;
|
||||
|
||||
/// <summary>Brush 缓存,避免每次都 new BrushConverter</summary>
|
||||
private static readonly ConcurrentDictionary<string, Brush> _brushCache = new();
|
||||
|
||||
public ScopeLogDispatcher(GlobalInfo globalInfo)
|
||||
{
|
||||
_globalInfo = globalInfo ?? throw new ArgumentNullException(nameof(globalInfo));
|
||||
}
|
||||
|
||||
public void Report((string scope, string message, string color, int depth) value)
|
||||
{
|
||||
var scope = value.scope;
|
||||
if (string.IsNullOrEmpty(scope)) return;
|
||||
|
||||
if (!_globalInfo.ContextDic.TryGetValue(scope, out var context)) return;
|
||||
|
||||
Brush brush = _brushCache.GetOrAdd(value.color, color =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return (Brush)new BrushConverter().ConvertFromString(color);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Brushes.Black;
|
||||
}
|
||||
});
|
||||
|
||||
// 直接入队到后台线程安全的 ConcurrentQueue,不走 SynchronizationContext
|
||||
context.LogBuffer.Enqueue((value.message, brush, value.depth));
|
||||
}
|
||||
}
|
||||
}
|
||||
55
UIShare/GlobalVariable/ScopedContext.cs
Normal file
55
UIShare/GlobalVariable/ScopedContext.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using DeviceCommand.Base;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Media;
|
||||
using UIShare.UIViewModel;
|
||||
|
||||
namespace UIShare.GlobalVariable
|
||||
{
|
||||
public class ScopedContext
|
||||
{
|
||||
private static readonly Random _randomSeed = new Random();
|
||||
public ProgramVM Program { get; set; } = new();
|
||||
public String SelectedStepList { get; set; } = "主程序";
|
||||
public string CurrentFilePath { get; set; }
|
||||
public bool? IsStop { get; set; }
|
||||
public bool SingleStep { get; set; }
|
||||
public string RunState { get; set; } = "运行";
|
||||
public TimeSpan RunningTime { get; set; } = TimeSpan.Zero;
|
||||
public Stopwatch SW { get; set; } = new();
|
||||
public bool IsTerminate { get; set; } = false;
|
||||
public ObservableCollection<Assembly> Assemblies { get; set; } = new();
|
||||
public PackIconKind RunIcon { get; set; } = PackIconKind.Play;
|
||||
public StepVM SelectedStep { get; set; }
|
||||
public ParameterVM SelectedParameter { get; set; }
|
||||
|
||||
public List<IBaseInterface> DeviceList { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 日志缓冲队列:后台线程(ScopeLogDispatcher)直接入队,
|
||||
/// UI 线程(DispatcherTimer)定时出队并刷新到 ObservableCollection。
|
||||
/// 不走 Progress<T> / SynchronizationContext,避免 Post 淹没 UI 消息队列。
|
||||
/// </summary>
|
||||
public ConcurrentQueue<(string Message, Brush Color, int Depth)> LogBuffer { get; } = new();
|
||||
|
||||
// 【新增测试属性】:每个实例被 new 出来时独一无二的随机身份
|
||||
// 证 ID
|
||||
public int DebugRandomId { get; private set; }
|
||||
public ScopedContext()
|
||||
{
|
||||
lock (_randomSeed)
|
||||
{
|
||||
// 每次诞生一个新上下文,就在 10000 到 99999 之间随机摇一个数
|
||||
DebugRandomId = _randomSeed.Next(10000, 100000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
846
UIShare/GlobalVariable/StepRunning.cs
Normal file
846
UIShare/GlobalVariable/StepRunning.cs
Normal file
@@ -0,0 +1,846 @@
|
||||
using UIShare.UIViewModel;
|
||||
using UIShare.PubEvent;
|
||||
using Common.Tools;
|
||||
using Logger;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
using Model.Entity;
|
||||
using Service.Interface;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UIShare.GlobalVariable;
|
||||
using static UIShare.UIViewModel.ParameterVM;
|
||||
|
||||
|
||||
namespace UIShare
|
||||
{
|
||||
public class StepRunning:IDisposable
|
||||
{
|
||||
private ScopedContext _scopedContext;
|
||||
private SystemConfig _systemConfig;
|
||||
private DeviceManager _deviceManager;
|
||||
//private Devices _devices;
|
||||
private IContainerProvider containerProvider;
|
||||
private IEventAggregator _eventAggregator;
|
||||
private ITestReportService _testReportService;
|
||||
|
||||
private readonly Dictionary<Guid, ParameterVM> tmpParameters = [];
|
||||
|
||||
private readonly Stopwatch stepStopwatch = new();
|
||||
|
||||
private readonly Stack<Stopwatch> loopStopwatchStack = new();
|
||||
|
||||
private readonly Stack<LoopContext> loopStack = new();
|
||||
|
||||
public CancellationTokenSource stepCTS = new();
|
||||
public CancellationTokenSource errorStepCTS = new();
|
||||
private bool SubSingleStep = false;
|
||||
|
||||
/// <summary>标记是否已被注销,执行方法据此安全退出</summary>
|
||||
private volatile bool _disposed = false;
|
||||
|
||||
public Guid TestRoundID;
|
||||
public StepRunning(ScopedContext ScopedContext, SystemConfig systemConfig,IEventAggregator eventAggregator, DeviceManager deviceManager, ITestReportService testReportService)
|
||||
{
|
||||
_scopedContext = ScopedContext;
|
||||
_systemConfig = systemConfig;
|
||||
_eventAggregator = eventAggregator;
|
||||
_deviceManager= deviceManager;
|
||||
_testReportService = testReportService;
|
||||
//_devices = containerProvider.Resolve<Devices>();
|
||||
}
|
||||
public async Task<bool> ExecuteErrorSteps(ProgramVM program, int depth = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_disposed) return false;
|
||||
int index = 0;
|
||||
bool stepSuccess = false;
|
||||
if (depth == 0)
|
||||
{
|
||||
loopStack.Clear();
|
||||
loopStopwatchStack.Clear();
|
||||
ResetAllStepStatus(program.ErrorStepCollection);
|
||||
tmpParameters.Clear();
|
||||
TestRoundID = Guid.NewGuid();
|
||||
}
|
||||
foreach (var item in program.Parameters)
|
||||
{
|
||||
tmpParameters.TryAdd(item.ID, item);
|
||||
}
|
||||
|
||||
while (index < program.ErrorStepCollection.Count)
|
||||
{
|
||||
if (_disposed || cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
var step = program.ErrorStepCollection[index];
|
||||
if (!step.IsUsed)
|
||||
{
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
step.Result = 0;
|
||||
if (step.StepType == "循环开始")
|
||||
{
|
||||
var endStep = program.ErrorStepCollection.FirstOrDefault(x => x.LoopStartStepId == step.ID);
|
||||
if (endStep != null)
|
||||
{
|
||||
endStep.Result = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
LoggerHelper.ErrorWithNotify(_systemConfig.Title, "程序循环指令未闭合,请检查后重试");
|
||||
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;
|
||||
LoggerHelper.InfoWithNotify(_systemConfig.Title, $"循环开始,共{context.LoopCount}次", depth);
|
||||
index++;
|
||||
await SaveStepRecordAsync(step, depth, true);
|
||||
}
|
||||
|
||||
// 处理循环结束
|
||||
else if (step.StepType == "循环结束")
|
||||
{
|
||||
if (loopStack.Count == 0)
|
||||
{
|
||||
LoggerHelper.ErrorWithNotify(_systemConfig.Title, "未匹配的循环结束指令", depth: depth);
|
||||
step.Result = 2;
|
||||
index++;
|
||||
await SaveStepRecordAsync(step, depth, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
var context = loopStack.Peek();
|
||||
context.CurrentLoop++;
|
||||
|
||||
// 更新循环开始步骤的显示
|
||||
context.LoopStartStep!.CurrentLoopCount = context.LoopCount - context.CurrentLoop;
|
||||
|
||||
if (context.CurrentLoop < context.LoopCount)
|
||||
{
|
||||
// 继续循环:跳转到循环开始后的第一条指令
|
||||
index = context.StartIndex + 1;
|
||||
LoggerHelper.InfoWithNotify(_systemConfig.Title, $"循环第{context.CurrentLoop}次结束,跳回开始,剩余{context.LoopCount - context.CurrentLoop}次", depth);
|
||||
await SaveStepRecordAsync(step, depth, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 循环结束
|
||||
loopStack.Pop();
|
||||
var loopStopwatch = loopStopwatchStack.Peek();
|
||||
index++;
|
||||
LoggerHelper.InfoWithNotify(_systemConfig.Title, $"循环结束,共执行{context.LoopCount}次", depth);
|
||||
if (depth == 0 && loopStopwatch.IsRunning)
|
||||
{
|
||||
loopStopwatch.Stop();
|
||||
step.RunTime = (int)loopStopwatch.ElapsedMilliseconds;
|
||||
step.Result = 1;
|
||||
program.ErrorStepCollection.First(x => x.ID == step.LoopStartStepId).Result = 1;
|
||||
loopStopwatchStack.Pop();
|
||||
}
|
||||
await SaveStepRecordAsync(step, depth, true);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理普通步骤
|
||||
else
|
||||
{
|
||||
if (depth == 0)
|
||||
{
|
||||
stepStopwatch.Restart();
|
||||
}
|
||||
|
||||
if (step.SubProgram != null)
|
||||
{
|
||||
if (_scopedContext.SingleStep)//子程序的单步执行将执行完保存下的所有Method
|
||||
{
|
||||
SubSingleStep = true;
|
||||
_scopedContext.SingleStep = false;
|
||||
}
|
||||
LoggerHelper.InfoWithNotify(_systemConfig.Title, $"开始执行子程序 [ {step.Index} ] [ {step.Name} ] ", depth);
|
||||
stepSuccess = await ExecuteSteps(step.SubProgram, depth + 1, cancellationToken);
|
||||
UpdateCurrentStepResult(step, true, stepSuccess, depth);
|
||||
if (SubSingleStep)
|
||||
{
|
||||
SubSingleStep = false;
|
||||
_scopedContext.SingleStep = true;
|
||||
}
|
||||
}
|
||||
else if (step.Method != null)
|
||||
{
|
||||
LoggerHelper.InfoWithNotify(_systemConfig.Title, $"开始执行指令 [ {step.Index} ] [ {step.Method!.FullName}.{step.Method.Name} ] ", depth);
|
||||
await ExecuteMethodStep(step, tmpParameters, depth, cancellationToken);
|
||||
stepSuccess = step.Result == 1;
|
||||
if (step.NGGotoStepID != null && !stepSuccess)
|
||||
{
|
||||
var tmp = program.ErrorStepCollection.FirstOrDefault(x => x.ID == step.NGGotoStepID);
|
||||
if (tmp != null)
|
||||
{
|
||||
index = tmp.Index - 2;
|
||||
LoggerHelper.InfoWithNotify(_systemConfig.Title, $"指令跳转 [ {tmp.Index} ] [ {tmp.Name} ]", depth);
|
||||
}
|
||||
}
|
||||
if (step.OKGotoStepID != null && stepSuccess)
|
||||
{
|
||||
var tmp = program.ErrorStepCollection.FirstOrDefault(x => x.ID == step.OKGotoStepID);
|
||||
if (tmp != null)
|
||||
{
|
||||
index = tmp.Index - 2;
|
||||
LoggerHelper.InfoWithNotify(_systemConfig.Title, $"指令跳转 [ {tmp.Index} ] [ {tmp.Name} ]", depth);
|
||||
}
|
||||
}
|
||||
}
|
||||
index++;
|
||||
|
||||
if (depth == 0 && stepStopwatch.IsRunning)
|
||||
{
|
||||
stepStopwatch.Stop();
|
||||
step.RunTime = (int)stepStopwatch.ElapsedMilliseconds;
|
||||
}
|
||||
await SaveStepRecordAsync(step, depth, true);
|
||||
}
|
||||
}
|
||||
|
||||
return loopStack.Count == 0 && stepSuccess;
|
||||
}
|
||||
public async Task<bool> ExecuteSteps(ProgramVM program, int depth = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_disposed) return false;
|
||||
int index = 0;
|
||||
bool stepSuccess = false;
|
||||
if (depth == 0)
|
||||
{
|
||||
loopStack.Clear();
|
||||
loopStopwatchStack.Clear();
|
||||
ResetAllStepStatus(program.StepCollection);
|
||||
tmpParameters.Clear();
|
||||
TestRoundID = Guid.NewGuid();
|
||||
}
|
||||
foreach (var item in program.Parameters)
|
||||
{
|
||||
tmpParameters.TryAdd(item.ID, item);
|
||||
}
|
||||
|
||||
while (index < program.StepCollection.Count)
|
||||
{
|
||||
while (!_disposed && _scopedContext.IsStop == true)
|
||||
{
|
||||
await Task.Delay(50, cancellationToken);
|
||||
}
|
||||
if (_disposed || 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
|
||||
{
|
||||
LoggerHelper.ErrorWithNotify(_systemConfig.Title, "程序循环指令未闭合,请检查后重试");
|
||||
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;
|
||||
LoggerHelper.InfoWithNotify(_systemConfig.Title, $"循环开始({step.Name}),共{context.LoopCount}次", depth);
|
||||
index++;
|
||||
await SaveStepRecordAsync(step, depth, false);
|
||||
}
|
||||
|
||||
// 处理循环结束
|
||||
else if (step.StepType == "循环结束")
|
||||
{
|
||||
if (loopStack.Count == 0)
|
||||
{
|
||||
LoggerHelper.ErrorWithNotify(_systemConfig.Title, "未匹配的循环结束指令", depth:depth);
|
||||
step.Result = 2;
|
||||
index++;
|
||||
await SaveStepRecordAsync(step, depth, false);
|
||||
continue;
|
||||
}
|
||||
|
||||
var context = loopStack.Peek();
|
||||
context.CurrentLoop++;
|
||||
|
||||
// 更新循环开始步骤的显示
|
||||
context.LoopStartStep!.CurrentLoopCount = context.LoopCount - context.CurrentLoop;
|
||||
|
||||
if (context.CurrentLoop < context.LoopCount)
|
||||
{
|
||||
// 继续循环:跳转到循环开始后的第一条指令
|
||||
index = context.StartIndex + 1;
|
||||
LoggerHelper.InfoWithNotify(_systemConfig.Title, $"循环第{context.CurrentLoop}次结束,跳回开始,剩余{context.LoopCount - context.CurrentLoop}次", depth);
|
||||
await SaveStepRecordAsync(step, depth, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 循环结束
|
||||
loopStack.Pop();
|
||||
var loopStopwatch = loopStopwatchStack.Peek();
|
||||
index++;
|
||||
LoggerHelper.InfoWithNotify(_systemConfig.Title, $"循环结束,共执行{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();
|
||||
}
|
||||
await SaveStepRecordAsync(step, depth, false);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理普通步骤
|
||||
else
|
||||
{
|
||||
if (depth == 0)
|
||||
{
|
||||
stepStopwatch.Restart();
|
||||
}
|
||||
|
||||
if (step.SubProgram != null)
|
||||
{
|
||||
if (_scopedContext.SingleStep)//子程序的单步执行将执行完保存下的所有Method
|
||||
{
|
||||
SubSingleStep = true;
|
||||
_scopedContext.SingleStep = false;
|
||||
}
|
||||
LoggerHelper.InfoWithNotify(_systemConfig.Title, $"开始执行子程序 [ {step.Index} ] [ {step.Name} ] ", depth);
|
||||
stepSuccess = await ExecuteSteps(step.SubProgram, depth + 1, cancellationToken);
|
||||
UpdateCurrentStepResult(step, true, stepSuccess, depth);
|
||||
if (SubSingleStep)
|
||||
{
|
||||
SubSingleStep = false;
|
||||
_scopedContext.SingleStep = true;
|
||||
}
|
||||
}
|
||||
else if (step.Method != null)
|
||||
{
|
||||
LoggerHelper.InfoWithNotify(_systemConfig.Title, $"开始执行指令 [ {step.Index} ] [ {step.Method!.FullName}.{step.Method.Name} ] ", depth);
|
||||
await ExecuteMethodStep(step, tmpParameters, depth, cancellationToken);
|
||||
stepSuccess = step.Result == 1;
|
||||
if (step.NGGotoStepID != null && !stepSuccess)
|
||||
{
|
||||
var tmp = program.StepCollection.FirstOrDefault(x => x.ID == step.NGGotoStepID);
|
||||
if (tmp != null)
|
||||
{
|
||||
index = tmp.Index - 2;
|
||||
LoggerHelper.InfoWithNotify(_systemConfig.Title, $"指令跳转 [ {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;
|
||||
LoggerHelper.InfoWithNotify(_systemConfig.Title, $"指令跳转 [ {tmp.Index} ] [ {tmp.Name} ]", depth);
|
||||
}
|
||||
}
|
||||
}
|
||||
index++;
|
||||
|
||||
if (depth == 0 && stepStopwatch.IsRunning)
|
||||
{
|
||||
stepStopwatch.Stop();
|
||||
step.RunTime = (int)stepStopwatch.ElapsedMilliseconds;
|
||||
}
|
||||
if (_scopedContext.SingleStep)
|
||||
{
|
||||
_scopedContext.IsStop = true;
|
||||
_scopedContext.RunState = "运行";
|
||||
_scopedContext.SingleStep = false;
|
||||
_eventAggregator.GetEvent<RunSingalCompletedEvent>().Publish("Play");
|
||||
}
|
||||
await SaveStepRecordAsync(step, depth, false);
|
||||
}
|
||||
}
|
||||
|
||||
return loopStack.Count == 0 && stepSuccess;
|
||||
}
|
||||
|
||||
public async Task ExecuteMethodStep(StepVM step, Dictionary<Guid, ParameterVM> parameters, int depth, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_disposed) return;
|
||||
try
|
||||
{
|
||||
if(_scopedContext.Program.StepCollection.Count>1)
|
||||
_scopedContext.SelectedStep = null;
|
||||
await Task.Delay(_systemConfig.PerformanceLevel, cancellationToken);
|
||||
|
||||
|
||||
// 1. 查找类型
|
||||
Type? targetType = null;
|
||||
foreach (var assembly in _scopedContext.Assemblies)
|
||||
{
|
||||
targetType = assembly.GetType(step.Method!.FullName!);
|
||||
if (targetType != null) break;
|
||||
}
|
||||
if (targetType == null)
|
||||
{
|
||||
LoggerHelper.ErrorWithNotify(_systemConfig.Title, $"指令 [ {step.Index} ] 执行错误:未找到类型 {step.Method!.FullName}", depth: depth);
|
||||
step.Result = 2;
|
||||
}
|
||||
|
||||
// 2. 创建实例(仅当方法不是静态时才需要)
|
||||
object? instance = null;
|
||||
bool isMethod = false;
|
||||
|
||||
// 3. 准备参数
|
||||
var inputParams = new List<object?>();
|
||||
var paramTypes = new List<Type>();
|
||||
ParameterVM? 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);
|
||||
}
|
||||
// 常规类型转换
|
||||
else
|
||||
{
|
||||
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;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (param.Type.BaseType == typeof(Enum))
|
||||
{
|
||||
actualValue = Enum.Parse(param.Type, param.Value!.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)
|
||||
{
|
||||
LoggerHelper.WarnWithNotify(_systemConfig.Title, $"指令 [ {step.Index} ] 执行错误:参数 {param.Name} 类型转换失败: {ex.Message}", depth: depth);
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
{
|
||||
LoggerHelper.ErrorWithNotify(_systemConfig.Title, $"指令 [ {step.Index} ] 执行错误:未找到方法{step.Method.Name}", depth: depth);
|
||||
step.Result = 2;
|
||||
}
|
||||
|
||||
// 检查是否是静态方法
|
||||
bool isStaticMethod = method!.IsStatic;
|
||||
|
||||
// 如果是实例方法,需要创建实例
|
||||
if (!isStaticMethod)
|
||||
{
|
||||
try
|
||||
{
|
||||
if(targetType.Name== "ZLGCANFD")
|
||||
{
|
||||
instance = _deviceManager.CANFD;
|
||||
}
|
||||
else if (targetType.Name == "IOBoardGroup")
|
||||
{
|
||||
instance = _deviceManager.IOGroup;
|
||||
}
|
||||
else instance = _deviceManager.DeviceMap[targetType.Name];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.ErrorWithNotify(_systemConfig.Title, $"指令 [ {step.Index} ] 执行错误:创建实例失败 - {ex.Message}", depth: depth);
|
||||
step.Result = 2;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 执行方法
|
||||
|
||||
object? returnValue = method.Invoke(instance, inputParams.ToArray());
|
||||
try
|
||||
{
|
||||
// 处理异步方法
|
||||
if (returnValue is Task task)
|
||||
{
|
||||
await task.ConfigureAwait(false);
|
||||
// 获取结果(如果是Task<T>)
|
||||
if (task.GetType().IsGenericType)
|
||||
{
|
||||
var returnValueProperty = task.GetType().GetProperty("Result");
|
||||
returnValue = returnValueProperty?.GetValue(task);
|
||||
}
|
||||
else
|
||||
{
|
||||
returnValue = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理VoidTaskreturnValue类型
|
||||
if (returnValue != null && returnValue.GetType().FullName == "System.Threading.Tasks.VoidTaskreturnValue")
|
||||
{
|
||||
returnValue = null;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.ErrorWithNotify(_systemConfig.Title, $"指令 [ {step.Index} ] 执行错误: {ex.InnerException?.Message ?? ex.Message}", depth: depth);
|
||||
step.Result = 2;
|
||||
return;
|
||||
}
|
||||
|
||||
// 6. 处理输出
|
||||
bool paraResult = true; //记录参数上下限是否NG
|
||||
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;
|
||||
paraResult = tmp.Item1;
|
||||
if (tmp.Item2 != null)
|
||||
{
|
||||
LoggerHelper.WarnWithNotify(_systemConfig.Title, tmp.Item2);
|
||||
}
|
||||
|
||||
}
|
||||
var returnType = returnValue?.GetType();
|
||||
if (returnType != null)
|
||||
{
|
||||
if (!returnType.IsArray)
|
||||
{
|
||||
LoggerHelper.SuccessWithNotify(_systemConfig.Title, $"输出 [ {outputParam.Name} ] = {returnValue} ({returnType.Name})", depth);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (returnValue is IEnumerable enumerable)
|
||||
{
|
||||
var elements = enumerable.Cast<object>().Select(item => item?.ToString() ?? "null");
|
||||
LoggerHelper.SuccessWithNotify(_systemConfig.Title, $"输出 [ {outputParam.Name} ] = [ {string.Join(", ", elements)} ] ({returnType.Name})", depth);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LoggerHelper.SuccessWithNotify(_systemConfig.Title, $"指令 [ {step.Index} ] 执行成功", depth);
|
||||
UpdateCurrentStepResult(step, paraResult: paraResult, depth: depth);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.ErrorWithNotify(_systemConfig.Title, $"指令 [ {step.Index} ] 执行错误: {ex.InnerException?.Message ?? ex.Message}", depth: depth);
|
||||
step.Result = 2;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将单个步骤的执行结果保存到数据库(测试报告)。
|
||||
/// 同一次运行的所有步骤共享 TestRoundID,导出时按此 Guid 查询。
|
||||
/// </summary>
|
||||
private async Task SaveStepRecordAsync(StepVM step, int depth, bool isErrorStep)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 获取输出参数值
|
||||
string? outputValue = null;
|
||||
if (step.Method != null)
|
||||
{
|
||||
var outputParam = step.Method.Parameters.FirstOrDefault(p => p.Category == ParameterCategory.Output);
|
||||
if (outputParam?.Value != null)
|
||||
outputValue = outputParam.Value.ToString();
|
||||
}
|
||||
|
||||
var entity = new TestReportEntity
|
||||
{
|
||||
TestRoundId = TestRoundID,
|
||||
Scope = _systemConfig.Title,
|
||||
FileName = _systemConfig.CurrentACPFile ?? "",
|
||||
StepIndex = step.Index,
|
||||
StepName = step.Name ?? "",
|
||||
StepType = step.StepType ?? "普通步骤",
|
||||
MethodName = step.Method?.Name,
|
||||
MethodFullName = step.Method?.FullName,
|
||||
Result = step.Result switch
|
||||
{
|
||||
-1 => "未执行",
|
||||
0 => "执行中",
|
||||
1 => "成功",
|
||||
2 => "失败",
|
||||
_ => "未知"
|
||||
},
|
||||
RunTimeMs = step.RunTime,
|
||||
OutputValue = outputValue,
|
||||
Depth = depth,
|
||||
IsErrorStep = isErrorStep,
|
||||
LoopRemaining = step.CurrentLoopCount,
|
||||
CreateTime = DateTime.Now
|
||||
};
|
||||
|
||||
var result = await _testReportService.InsertAsync(entity);
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
LoggerHelper.Error($"保存步骤记录失败 [{step.Index}]: {result.Msg}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.Error($"保存步骤记录失败 [{step.Index}]: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetAllStepStatus(ObservableCollection<StepVM> StepCollection)
|
||||
{
|
||||
foreach (var step in StepCollection)
|
||||
{
|
||||
step.Result = -1;
|
||||
step.RunTime = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateCurrentStepResult(StepVM step, bool paraResult = true, bool stepResult = true, int depth = 0)
|
||||
{
|
||||
if (stepResult && paraResult)
|
||||
{
|
||||
if (string.IsNullOrEmpty(step.OKExpression))
|
||||
{
|
||||
step.Result = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
Dictionary<string, object> paraDic = [];
|
||||
foreach (var item in tmpParameters)
|
||||
{
|
||||
paraDic.TryAdd(item.Value.Name, item.Value.Value!);
|
||||
}
|
||||
if (step.SubProgram != null)
|
||||
{
|
||||
foreach (var item in step.SubProgram.Parameters.Where(x => x.Category == ParameterCategory.Output))
|
||||
{
|
||||
paraDic.TryAdd(item.Name, item.Value!);
|
||||
}
|
||||
}
|
||||
else if (step.Method != null)
|
||||
{
|
||||
foreach (var item in step.Method.Parameters.Where(x => x.Category == ParameterCategory.Output))
|
||||
{
|
||||
paraDic.TryAdd(item.Name, item.Value!);
|
||||
}
|
||||
}
|
||||
bool re = ExpressionEvaluator.EvaluateExpression(step.OKExpression, paraDic);
|
||||
step.Result = re ? 1 : 2;
|
||||
if (step.Result == 2)
|
||||
{
|
||||
LoggerHelper.WarnWithNotify(_systemConfig.Title, $"指令 [ {step.Index} ] NG:条件表达式验证失败", depth: depth);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!paraResult)
|
||||
{
|
||||
LoggerHelper.WarnWithNotify(_systemConfig.Title, "参数限值校验失败", depth: depth);
|
||||
}
|
||||
step.Result = 2;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
// 1. 唤醒暂停循环(IsStop 可能卡住后台线程)
|
||||
try
|
||||
{
|
||||
if (_scopedContext != null)
|
||||
{
|
||||
_scopedContext.IsStop = false;
|
||||
_scopedContext.IsTerminate = true;
|
||||
_scopedContext.RunState = "运行";
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
try
|
||||
{
|
||||
if (stepCTS != null && !stepCTS.IsCancellationRequested) stepCTS.Cancel();
|
||||
}
|
||||
catch (ObjectDisposedException) { }
|
||||
|
||||
try
|
||||
{
|
||||
if (errorStepCTS != null && !errorStepCTS.IsCancellationRequested) errorStepCTS.Cancel();
|
||||
}
|
||||
catch (ObjectDisposedException) { }
|
||||
|
||||
tmpParameters.Clear();
|
||||
loopStack.Clear();
|
||||
loopStopwatchStack.Clear();
|
||||
stepStopwatch.Stop();
|
||||
}
|
||||
|
||||
|
||||
|
||||
#region 私有类
|
||||
|
||||
private class LoopContext
|
||||
{
|
||||
public int LoopCount { get; set; }
|
||||
public int CurrentLoop { get; set; }
|
||||
public int StartIndex { get; set; }
|
||||
public StepVM? LoopStartStep { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
175
UIShare/GlobalVariable/SystemConfig.cs
Normal file
175
UIShare/GlobalVariable/SystemConfig.cs
Normal file
@@ -0,0 +1,175 @@
|
||||
using Logger;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UIShare.UIViewModel;
|
||||
using ZLGUSBCANFD;
|
||||
using static UIShare.UIViewModel.ParameterVM;
|
||||
|
||||
namespace UIShare.GlobalVariable
|
||||
{
|
||||
public class SystemConfig
|
||||
{
|
||||
[JsonIgnore]
|
||||
public string SystemPath { get; set; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ACP");
|
||||
[JsonIgnore]
|
||||
public string DLLFilePath { get; set; } = @"D:\ACP\指令\";
|
||||
[JsonIgnore]
|
||||
public string TSMasterName { get; set; } = "ACP测试上位机";
|
||||
public string SubProgramFilePath { get; set; } = @"D:\ACP\子程序\";
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string CurrentACPFile { get; set; }
|
||||
public int PerformanceLevel { get; set; } = 50;
|
||||
public string DefaultProgramFilePath { get; set; } = "";
|
||||
public string DefaultBLFFilePath { get; set; } = "";
|
||||
public string DefaultDBCFilePath { get; set; } = "";
|
||||
public ObservableCollection<DeviceInfoVM> DeviceList = new();
|
||||
public ObservableCollection<SharedParameter> SharedParameterList = new();
|
||||
public ObservableCollection<CANSignalConfig> ConfigurationList = new();
|
||||
public ObservableCollection<AutoDBCLoadItem> DBCAutoLoadList = new();
|
||||
public ObservableCollection<ValueLimitVM> ValueLimitList = new();
|
||||
[JsonIgnore]
|
||||
public ObservableCollection<MonitorChannelVM> Channels = new();
|
||||
/// <summary>
|
||||
/// 监测通道持久化配置列表(包含 Fingerprint、MethodName、IsDisplayed)
|
||||
/// </summary>
|
||||
public ObservableCollection<MonitorChannelConfig> MonitorChannels = new();
|
||||
public ZLGCANFD CANFD = new();
|
||||
[JsonIgnore]
|
||||
public ObservableCollection<ParameterVM> ParameterList = new()
|
||||
{
|
||||
new ParameterVM
|
||||
{
|
||||
Category = ParameterCategory.Input,
|
||||
Type = typeof(int),
|
||||
Name = "台架序号",
|
||||
Value = 1,
|
||||
IsEditable=false
|
||||
},
|
||||
new ParameterVM
|
||||
{
|
||||
Category = ParameterCategory.Input,
|
||||
Type = typeof(int),
|
||||
Name = "直流负载通道",
|
||||
Value = 1,
|
||||
IsEditable=false
|
||||
},
|
||||
new ParameterVM
|
||||
{
|
||||
Category = ParameterCategory.Input,
|
||||
Type = typeof(int),
|
||||
Name = "交流电源通道",
|
||||
Value = 1,
|
||||
IsEditable=false
|
||||
},
|
||||
new ParameterVM
|
||||
{
|
||||
Category = ParameterCategory.Input,
|
||||
Type = typeof(int),
|
||||
Name = "CAN通道",
|
||||
Value = 0,
|
||||
IsEditable=false
|
||||
},
|
||||
new ParameterVM
|
||||
{
|
||||
Category = ParameterCategory.Input,
|
||||
Type = typeof(int),
|
||||
Name = "示波器通道1",
|
||||
Value = 0,
|
||||
IsEditable=false
|
||||
},
|
||||
new ParameterVM
|
||||
{
|
||||
Category = ParameterCategory.Input,
|
||||
Type = typeof(int),
|
||||
Name = "示波器通道2",
|
||||
Value = 0,
|
||||
IsEditable=false
|
||||
},
|
||||
new ParameterVM
|
||||
{
|
||||
Category = ParameterCategory.Input,
|
||||
Type = typeof(int),
|
||||
Name = "功率分析仪通道1",
|
||||
Value = 0,
|
||||
IsEditable=false
|
||||
},
|
||||
new ParameterVM
|
||||
{
|
||||
Category = ParameterCategory.Input,
|
||||
Type = typeof(int),
|
||||
Name = "功率分析仪通道2",
|
||||
Value = 0,
|
||||
IsEditable=false
|
||||
},
|
||||
};
|
||||
// public ObservableCollection<DeviceInfoVM> DeviceList { get; set; } = new()
|
||||
//{
|
||||
// new DeviceInfoVM
|
||||
// {
|
||||
// DeviceName = "IT7800E",
|
||||
// DeviceType = "IT7800E",
|
||||
// Remark = "交流可编程电源供应器",
|
||||
// ConnectionType = "Tcp",
|
||||
// IsEnabled = true,
|
||||
// IsConnected = false
|
||||
// },
|
||||
|
||||
// new DeviceInfoVM
|
||||
// {
|
||||
// DeviceName = "N36200",
|
||||
// DeviceType = "N36200",
|
||||
// Remark = "宽范围可编程直流电源",
|
||||
// ConnectionType = "Tcp",
|
||||
// IsEnabled = true,
|
||||
// IsConnected = false
|
||||
// },
|
||||
|
||||
// new DeviceInfoVM
|
||||
// {
|
||||
// DeviceName = "N36600",
|
||||
// DeviceType = "N36600",
|
||||
// Remark = "便携式宽范围可编程直流电源",
|
||||
// ConnectionType = "Tcp",
|
||||
// IsEnabled = false,
|
||||
// IsConnected = false
|
||||
// },
|
||||
|
||||
// new DeviceInfoVM
|
||||
// {
|
||||
// DeviceName = "N69200",
|
||||
// DeviceType = "N69200",
|
||||
// Remark = "可编程直流电子负载",
|
||||
// ConnectionType = "Tcp",
|
||||
// IsEnabled = true,
|
||||
// IsConnected = false
|
||||
// },
|
||||
|
||||
// new DeviceInfoVM
|
||||
// {
|
||||
// DeviceName = "SDS2000X_HD",
|
||||
// DeviceType = "SDS2000X_HD",
|
||||
// Remark = "数字存储示波器",
|
||||
// ConnectionType = "Tcp",
|
||||
// IsEnabled = true,
|
||||
// IsConnected = false
|
||||
// },
|
||||
|
||||
// new DeviceInfoVM
|
||||
// {
|
||||
// DeviceName = "SPAW7000",
|
||||
// DeviceType = "SPAW7000",
|
||||
// Remark = "功率分析记录仪",
|
||||
// ConnectionType = "Tcp",
|
||||
// IsEnabled = true,
|
||||
// IsConnected = false
|
||||
// }
|
||||
//};
|
||||
}
|
||||
}
|
||||
56
UIShare/GlobalVariable/ValueLimitAlarmHelper.cs
Normal file
56
UIShare/GlobalVariable/ValueLimitAlarmHelper.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System.Linq;
|
||||
using UIShare.UIViewModel;
|
||||
|
||||
namespace UIShare.GlobalVariable
|
||||
{
|
||||
/// <summary>
|
||||
/// 值限制报警检查辅助类:供各广播器在采样到信号值时统一判断是否超限。
|
||||
/// </summary>
|
||||
public static class ValueLimitAlarmHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 根据指定作用域的 ValueLimitList 检查当前值是否超限,并更新报警状态。
|
||||
/// </summary>
|
||||
/// <param name="scope">作用域名称</param>
|
||||
/// <param name="fingerprint">硬件指纹</param>
|
||||
/// <param name="methodName">方法名/信号标识</param>
|
||||
/// <param name="value">当前采样值</param>
|
||||
/// <param name="globalInfo">全局信息</param>
|
||||
public static string CheckAlarm(string scope, string fingerprint, string methodName, double value, GlobalInfo globalInfo)
|
||||
{
|
||||
if (globalInfo?.ConfigDic == null) return "";
|
||||
if (!globalInfo.ConfigDic.TryGetValue(scope, out var systemConfig)) return "";
|
||||
if (systemConfig.ValueLimitList == null) return "";
|
||||
|
||||
var limit = systemConfig.ValueLimitList.FirstOrDefault(x =>
|
||||
x.Fingerprint == fingerprint && x.MethodName == methodName);
|
||||
if (limit == null) return "";
|
||||
if (value > limit.UpperExtreme)
|
||||
{
|
||||
limit.IsAlarm = true;
|
||||
limit.AlarmSatus = AlarmStatus.超上极限;
|
||||
}
|
||||
else if (value < limit.LowerExtreme)
|
||||
{
|
||||
limit.IsAlarm = true;
|
||||
limit.AlarmSatus = AlarmStatus.超下极限;
|
||||
}
|
||||
else if (value > limit.Upper)
|
||||
{
|
||||
limit.IsAlarm = true;
|
||||
limit.AlarmSatus = AlarmStatus.超上限;
|
||||
}
|
||||
else if (value < limit.Lower)
|
||||
{
|
||||
limit.IsAlarm = true;
|
||||
limit.AlarmSatus = AlarmStatus.超下限;
|
||||
}
|
||||
else
|
||||
{
|
||||
limit.IsAlarm = false;
|
||||
limit.AlarmSatus = AlarmStatus.未报警;
|
||||
}
|
||||
return limit.AlarmSatus.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
52
UIShare/Helpers/PasswordBoxHelper.cs
Normal file
52
UIShare/Helpers/PasswordBoxHelper.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace UIShare.Helpers
|
||||
{
|
||||
public static class PasswordBoxHelper
|
||||
{
|
||||
public static readonly DependencyProperty PasswordProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"Password",
|
||||
typeof(string),
|
||||
typeof(PasswordBoxHelper),
|
||||
new FrameworkPropertyMetadata(
|
||||
string.Empty,
|
||||
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
|
||||
OnPasswordPropertyChanged));
|
||||
|
||||
public static void SetPassword(DependencyObject d, string value)
|
||||
=> d.SetValue(PasswordProperty, value);
|
||||
|
||||
public static string GetPassword(DependencyObject d)
|
||||
=> (string)d.GetValue(PasswordProperty);
|
||||
|
||||
private static void OnPasswordPropertyChanged(
|
||||
DependencyObject d,
|
||||
DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is PasswordBox pb)
|
||||
{
|
||||
pb.PasswordChanged -= PasswordChanged;
|
||||
if (pb.Password != (string)e.NewValue)
|
||||
{
|
||||
pb.Password = (string)e.NewValue;
|
||||
}
|
||||
pb.PasswordChanged += PasswordChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private static void PasswordChanged(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is PasswordBox pb)
|
||||
{
|
||||
SetPassword(pb, pb.Password);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
41
UIShare/Helpers/WindowDragHelper.cs
Normal file
41
UIShare/Helpers/WindowDragHelper.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace UIShare.Helpers
|
||||
{
|
||||
public static class WindowDragHelper
|
||||
{
|
||||
public static readonly DependencyProperty EnableWindowDragProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"EnableWindowDrag",
|
||||
typeof(bool),
|
||||
typeof(WindowDragHelper),
|
||||
new PropertyMetadata(false, OnEnableWindowDragChanged));
|
||||
|
||||
public static bool GetEnableWindowDrag(DependencyObject obj) =>
|
||||
(bool)obj.GetValue(EnableWindowDragProperty);
|
||||
|
||||
public static void SetEnableWindowDrag(DependencyObject obj, bool value) =>
|
||||
obj.SetValue(EnableWindowDragProperty, value);
|
||||
|
||||
private static void OnEnableWindowDragChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is FrameworkElement element)
|
||||
{
|
||||
if ((bool)e.NewValue)
|
||||
element.MouseLeftButtonDown += Element_MouseLeftButtonDown;
|
||||
else
|
||||
element.MouseLeftButtonDown -= Element_MouseLeftButtonDown;
|
||||
}
|
||||
}
|
||||
|
||||
private static void Element_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is FrameworkElement element)
|
||||
{
|
||||
var window = Window.GetWindow(element);
|
||||
window?.DragMove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
UIShare/PubEvent/AddDialogTabEvent.cs
Normal file
21
UIShare/PubEvent/AddDialogTabEvent.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using UIShare.UIViewModel;
|
||||
|
||||
namespace UIShare.PubEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// 其他模块向弹窗管理器添加 Tab 的事件。
|
||||
/// 发布方:任何需要将弹窗托管到 DialogManagerView 的模块。
|
||||
/// 订阅方:<c>DialogMangerViewModel</c>(仅一处订阅)。
|
||||
/// <code>
|
||||
/// // 示例:在某个 ViewModel 中发布
|
||||
/// _eventAggregator.GetEvent<AddDialogTabEvent>().Publish(new DialogTabInfo
|
||||
/// {
|
||||
/// Title = "设备配置",
|
||||
/// Content = new DeviceConfigView()
|
||||
/// });
|
||||
/// </code>
|
||||
/// </summary>
|
||||
public class AddDialogTabEvent : PubSubEvent<DialogTabInfo>
|
||||
{
|
||||
}
|
||||
}
|
||||
12
UIShare/PubEvent/AlarmEvent.cs
Normal file
12
UIShare/PubEvent/AlarmEvent.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace UIShare.PubEvent
|
||||
{
|
||||
public class AlarmEvent : PubSubEvent<(string Scope, string Fingerprint, string Status)>
|
||||
{
|
||||
}
|
||||
}
|
||||
12
UIShare/PubEvent/CancelMinimizeEvent.cs
Normal file
12
UIShare/PubEvent/CancelMinimizeEvent.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace UIShare.PubEvent
|
||||
{
|
||||
public class CancelMinimizeEvent:PubSubEvent
|
||||
{
|
||||
}
|
||||
}
|
||||
12
UIShare/PubEvent/ChangeCurrentTagEvent.cs
Normal file
12
UIShare/PubEvent/ChangeCurrentTagEvent.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace UIShare.PubEvent
|
||||
{
|
||||
public class ChangeCurrentTagEvent:PubSubEvent<string>
|
||||
{
|
||||
}
|
||||
}
|
||||
12
UIShare/PubEvent/CollectedCANMessageChangedEvent.cs
Normal file
12
UIShare/PubEvent/CollectedCANMessageChangedEvent.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace UIShare.PubEvent
|
||||
{
|
||||
public class CollectedCANMessageChangedEvent:PubSubEvent
|
||||
{
|
||||
}
|
||||
}
|
||||
12
UIShare/PubEvent/ConnectionChangeEvent.cs
Normal file
12
UIShare/PubEvent/ConnectionChangeEvent.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace UIShare.PubEvent
|
||||
{
|
||||
public class ConnectionChangeEvent:PubSubEvent<(string,bool)>
|
||||
{
|
||||
}
|
||||
}
|
||||
13
UIShare/PubEvent/CurveDataEvent.cs
Normal file
13
UIShare/PubEvent/CurveDataEvent.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace UIShare.PubEvent
|
||||
{
|
||||
public class CurveDataEvent:PubSubEvent<(string, Dictionary<string,double>)>
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
12
UIShare/PubEvent/CurveInteractionEvent.cs
Normal file
12
UIShare/PubEvent/CurveInteractionEvent.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace UIShare.PubEvent
|
||||
{
|
||||
public class CurveInteractionEvent:PubSubEvent<(bool,double)>
|
||||
{
|
||||
}
|
||||
}
|
||||
17
UIShare/PubEvent/DBCLoadedEvent.cs
Normal file
17
UIShare/PubEvent/DBCLoadedEvent.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace UIShare.PubEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// DBC 加载完成事件:通知订阅者某个通道的 DBC 已加载,可以刷新信号列表。
|
||||
/// </summary>
|
||||
public class DBCLoadedEvent : PubSubEvent<DBCLoadedArgs>
|
||||
{
|
||||
}
|
||||
|
||||
public class DBCLoadedArgs
|
||||
{
|
||||
public uint Channel { get; set; }
|
||||
public string Scope { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
18
UIShare/PubEvent/DBCUnloadedEvent.cs
Normal file
18
UIShare/PubEvent/DBCUnloadedEvent.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace UIShare.PubEvent
|
||||
{
|
||||
public class DBCUnloadedEvent : PubSubEvent<DBCUnloadedArgs>
|
||||
{
|
||||
}
|
||||
|
||||
public class DBCUnloadedArgs
|
||||
{
|
||||
public uint Channel { get; set; }
|
||||
public string Scope { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
12
UIShare/PubEvent/DeletedStepEvent.cs
Normal file
12
UIShare/PubEvent/DeletedStepEvent.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace UIShare.PubEvent
|
||||
{
|
||||
public class DeletedStepEvent : PubSubEvent<Guid>
|
||||
{
|
||||
}
|
||||
}
|
||||
12
UIShare/PubEvent/EditSetpEvent.cs
Normal file
12
UIShare/PubEvent/EditSetpEvent.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace UIShare.PubEvent
|
||||
{
|
||||
public class EditSetpEvent : PubSubEvent
|
||||
{
|
||||
}
|
||||
}
|
||||
12
UIShare/PubEvent/ExpandViewEvent.cs
Normal file
12
UIShare/PubEvent/ExpandViewEvent.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace UIShare.PubEvent
|
||||
{
|
||||
public class ExpandViewEvent:PubSubEvent<string>
|
||||
{
|
||||
}
|
||||
}
|
||||
13
UIShare/PubEvent/HardwareDataReportedEvent.cs
Normal file
13
UIShare/PubEvent/HardwareDataReportedEvent.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Model.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace UIShare.PubEvent
|
||||
{
|
||||
public class HardwareDataReportedEvent : PubSubEvent<HardwareReportArgs>
|
||||
{
|
||||
}
|
||||
}
|
||||
12
UIShare/PubEvent/LoginSuccessEvent.cs
Normal file
12
UIShare/PubEvent/LoginSuccessEvent.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace UIShare.PubEvent
|
||||
{
|
||||
public class LoginSuccessEvent:PubSubEvent
|
||||
{
|
||||
}
|
||||
}
|
||||
12
UIShare/PubEvent/OverlayEvent.cs
Normal file
12
UIShare/PubEvent/OverlayEvent.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace UIShare.PubEvent
|
||||
{
|
||||
public class OverlayEvent : PubSubEvent<bool>
|
||||
{
|
||||
}
|
||||
}
|
||||
12
UIShare/PubEvent/ParamsChangedEvent.cs
Normal file
12
UIShare/PubEvent/ParamsChangedEvent.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace UIShare.PubEvent
|
||||
{
|
||||
public class ParamsChangedEvent:PubSubEvent
|
||||
{
|
||||
}
|
||||
}
|
||||
12
UIShare/PubEvent/RunSingalCompletedEvent.cs
Normal file
12
UIShare/PubEvent/RunSingalCompletedEvent.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace UIShare.PubEvent
|
||||
{
|
||||
public class RunSingalCompletedEvent : PubSubEvent<string>
|
||||
{
|
||||
}
|
||||
}
|
||||
12
UIShare/PubEvent/SettingChangedEvent.cs
Normal file
12
UIShare/PubEvent/SettingChangedEvent.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace UIShare.PubEvent
|
||||
{
|
||||
public class SettingChangedEvent:PubSubEvent
|
||||
{
|
||||
}
|
||||
}
|
||||
12
UIShare/PubEvent/SilenceBuzzerEvent.cs
Normal file
12
UIShare/PubEvent/SilenceBuzzerEvent.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace UIShare.PubEvent
|
||||
{
|
||||
public class SilenceBuzzerEvent:PubSubEvent<string>
|
||||
{
|
||||
}
|
||||
}
|
||||
12
UIShare/PubEvent/StartProcessEvent.cs
Normal file
12
UIShare/PubEvent/StartProcessEvent.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace UIShare.PubEvent
|
||||
{
|
||||
public class StartProcessEvent:PubSubEvent<(string,bool)>
|
||||
{
|
||||
}
|
||||
}
|
||||
12
UIShare/PubEvent/WaitingEvent.cs
Normal file
12
UIShare/PubEvent/WaitingEvent.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace UIShare.PubEvent
|
||||
{
|
||||
public class WaitingEvent : PubSubEvent<bool>
|
||||
{
|
||||
}
|
||||
}
|
||||
22
UIShare/Styles/CommonStyle.xaml
Normal file
22
UIShare/Styles/CommonStyle.xaml
Normal file
@@ -0,0 +1,22 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<!-- 引用MaterialDesign和MahApps的资源 -->
|
||||
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.MahApps;component/Themes/MaterialDesignTheme.MahApps.Fonts.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.MahApps;component/Themes/MaterialDesignTheme.MahApps.Flyout.xaml" />
|
||||
|
||||
<!-- MahApps资源 -->
|
||||
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/Light.Blue.xaml" />
|
||||
|
||||
<!-- Material Design资源 -->
|
||||
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Light.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesign3.Defaults.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/MaterialDesignColors;component/Themes/Recommended/Primary/MaterialDesignColor.DeepPurple.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/MaterialDesignColors;component/Themes/Recommended/Secondary/MaterialDesignColor.Lime.xaml" />
|
||||
<!--自定义style-->
|
||||
<ResourceDictionary Source="/UIShare;component/Styles/WindowStyle.xaml"></ResourceDictionary>
|
||||
<ResourceDictionary Source="/UIShare;component/Styles/DialogControlStyle.xaml"></ResourceDictionary>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
63
UIShare/Styles/DialogControlStyle.xaml
Normal file
63
UIShare/Styles/DialogControlStyle.xaml
Normal file
@@ -0,0 +1,63 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes">
|
||||
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Light.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesign3.Defaults.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<Style x:Key="CmdBtn" TargetType="Button" BasedOn="{StaticResource MaterialDesignRaisedButton}">
|
||||
<Setter Property="Height" Value="32"/>
|
||||
<Setter Property="Padding" Value="12,0"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="Margin" Value="4,0"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="WarnBtn" TargetType="Button" BasedOn="{StaticResource MaterialDesignRaisedButton}">
|
||||
<Setter Property="Height" Value="32"/>
|
||||
<Setter Property="Padding" Value="12,0"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="Margin" Value="4,0"/>
|
||||
<Setter Property="Background" Value="#EF6C00"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="NumInput" TargetType="TextBox" BasedOn="{StaticResource MaterialDesignOutlinedTextBox}">
|
||||
<Setter Property="Width" Value="100"/>
|
||||
<Setter Property="Height" Value="32"/>
|
||||
<Setter Property="Padding" Value="5,0"/>
|
||||
<Setter Property="materialDesign:TextFieldAssist.TextBoxViewMargin" Value="6,2,6,2"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="Margin" Value="4,0"/>
|
||||
<Setter Property="materialDesign:HintAssist.IsFloating" Value="False"/>
|
||||
<Setter Property="materialDesign:HintAssist.Hint" Value=""/>
|
||||
|
||||
<Setter Property="AutomationProperties.Name" Value=""/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="MeasureBox" TargetType="TextBox" BasedOn="{StaticResource MaterialDesignOutlinedTextBox}">
|
||||
<Setter Property="Width" Value="110"/>
|
||||
<Setter Property="Height" Value="32"/>
|
||||
<Setter Property="Padding" Value="5,0"/>
|
||||
<Setter Property="materialDesign:TextFieldAssist.TextBoxViewMargin" Value="6,2,6,2"/>
|
||||
<Setter Property="IsReadOnly" Value="True"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="Margin" Value="4,0"/>
|
||||
<Setter Property="Background" Value="#F5F5F5"/>
|
||||
<Setter Property="materialDesign:HintAssist.IsFloating" Value="False"/>
|
||||
<Setter Property="materialDesign:HintAssist.Hint" Value=""/>
|
||||
|
||||
<Setter Property="AutomationProperties.Name" Value=""/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="ParamLabel" TargetType="TextBlock">
|
||||
<Setter Property="Width" Value="80"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="Margin" Value="0,0,4,0"/>
|
||||
</Style>
|
||||
|
||||
</ResourceDictionary>
|
||||
23
UIShare/Styles/WindowStyle.xaml
Normal file
23
UIShare/Styles/WindowStyle.xaml
Normal file
@@ -0,0 +1,23 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" >
|
||||
|
||||
<Style x:Key="DialogUserManageStyle"
|
||||
TargetType="Window">
|
||||
<Setter Property="WindowStyle"
|
||||
Value="None" />
|
||||
<Setter Property="Topmost"
|
||||
Value="True" />
|
||||
<Setter Property="ResizeMode"
|
||||
Value="NoResize" />
|
||||
<Setter Property="ShowInTaskbar"
|
||||
Value="False" />
|
||||
<Setter Property="AllowsTransparency"
|
||||
Value="true" />
|
||||
<Setter Property="Background"
|
||||
Value="Transparent" />
|
||||
<Setter Property="SizeToContent"
|
||||
Value="WidthAndHeight" />
|
||||
</Style>
|
||||
|
||||
|
||||
</ResourceDictionary>
|
||||
28
UIShare/UIShare.csproj
Normal file
28
UIShare/UIShare.csproj
Normal file
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseWPF>true</UseWPF>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MahApps.Metro" Version="3.0.0-rc0529" />
|
||||
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.142" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.5-beta1" />
|
||||
<PackageReference Include="Prism.Unity" Version="9.0.537" />
|
||||
<PackageReference Include="MaterialDesignColors" Version="5.3.0" />
|
||||
<PackageReference Include="MaterialDesignThemes" Version="5.3.0" />
|
||||
<PackageReference Include="MaterialDesignThemes.MahApps" Version="5.3.0" />
|
||||
<PackageReference Include="Notifications.Wpf.Core" Version="2.0.1" />
|
||||
<PackageReference Include="OxyPlot.Wpf" Version="2.2.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Common\Common.csproj" />
|
||||
<ProjectReference Include="..\DeviceCommand\DeviceCommand.csproj" />
|
||||
<ProjectReference Include="..\Logger\Logger.csproj" />
|
||||
<ProjectReference Include="..\ORM\ORM.csproj" />
|
||||
<ProjectReference Include="..\Service\Service.csproj" />
|
||||
<ProjectReference Include="..\ZLGUSBCANFD\ZLGUSBCANFD.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
33
UIShare/UIViewModel/AutoDBCLoadItemVM.cs
Normal file
33
UIShare/UIViewModel/AutoDBCLoadItemVM.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
namespace UIShare.UIViewModel
|
||||
{
|
||||
/// <summary>
|
||||
/// CAN 通道枚举(对应 ZLGCANFD 通道选择)
|
||||
/// </summary>
|
||||
public enum APP_CHANNEL
|
||||
{
|
||||
CH0 = 0,
|
||||
CH1 = 1,
|
||||
CH2 = 2,
|
||||
CH3 = 3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DBC 自动加载配置项
|
||||
/// </summary>
|
||||
public class AutoDBCLoadItem : BindableBase
|
||||
{
|
||||
private int _dbcChannel;
|
||||
public int DBCChannel
|
||||
{
|
||||
get => _dbcChannel;
|
||||
set => SetProperty(ref _dbcChannel, value);
|
||||
}
|
||||
|
||||
private string _dbcFilePath;
|
||||
public string DBCFilePath
|
||||
{
|
||||
get => _dbcFilePath;
|
||||
set => SetProperty(ref _dbcFilePath, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
76
UIShare/UIViewModel/CANConfigVM.cs
Normal file
76
UIShare/UIViewModel/CANConfigVM.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using Prism.Mvvm;
|
||||
|
||||
namespace UIShare.UIViewModel
|
||||
{
|
||||
/// <summary>
|
||||
/// CAN 连接配置(对应 ZLGCANFD 构造函数 + 初始化并启动通道 参数)。
|
||||
/// </summary>
|
||||
public class CANConfigVM : BindableBase
|
||||
{
|
||||
// ===== ZLGCANFD 构造函数参数 =====
|
||||
|
||||
private uint _deviceType = 43;
|
||||
/// <summary>设备类型号(43 = USBCANFD-400U)</summary>
|
||||
public uint DeviceType
|
||||
{
|
||||
get => _deviceType;
|
||||
set => SetProperty(ref _deviceType, value);
|
||||
}
|
||||
|
||||
private uint _deviceIndex = 0;
|
||||
/// <summary>设备索引</summary>
|
||||
public uint DeviceIndex
|
||||
{
|
||||
get => _deviceIndex;
|
||||
set => SetProperty(ref _deviceIndex, value);
|
||||
}
|
||||
|
||||
private string _abitBaud = "500000";
|
||||
/// <summary>仲裁域波特率</summary>
|
||||
public string ABitBaud
|
||||
{
|
||||
get => _abitBaud;
|
||||
set => SetProperty(ref _abitBaud, value);
|
||||
}
|
||||
|
||||
private string _dbitBaud = "2000000";
|
||||
/// <summary>数据域波特率</summary>
|
||||
public string DBitBaud
|
||||
{
|
||||
get => _dbitBaud;
|
||||
set => SetProperty(ref _dbitBaud, value);
|
||||
}
|
||||
|
||||
private bool _enableTerminalResistance = true;
|
||||
/// <summary>是否开启终端电阻</summary>
|
||||
public bool EnableTerminalResistance
|
||||
{
|
||||
get => _enableTerminalResistance;
|
||||
set => SetProperty(ref _enableTerminalResistance, value);
|
||||
}
|
||||
|
||||
public CANConfigVM() { }
|
||||
|
||||
/// <summary>拷贝构造,用于对话框编辑副本。</summary>
|
||||
public CANConfigVM(CANConfigVM? src)
|
||||
{
|
||||
if (src == null) return;
|
||||
DeviceType = src.DeviceType;
|
||||
DeviceIndex = src.DeviceIndex;
|
||||
ABitBaud = src.ABitBaud;
|
||||
DBitBaud = src.DBitBaud;
|
||||
EnableTerminalResistance = src.EnableTerminalResistance;
|
||||
}
|
||||
|
||||
/// <summary>把字段拷回目标对象(保存时用)。</summary>
|
||||
public void CopyTo(CANConfigVM? dst)
|
||||
{
|
||||
if (dst == null) return;
|
||||
dst.DeviceType = DeviceType;
|
||||
dst.DeviceIndex = DeviceIndex;
|
||||
dst.ABitBaud = ABitBaud;
|
||||
dst.DBitBaud = DBitBaud;
|
||||
dst.EnableTerminalResistance = EnableTerminalResistance;
|
||||
}
|
||||
}
|
||||
}
|
||||
15
UIShare/UIViewModel/CANSignalConfigVM.cs
Normal file
15
UIShare/UIViewModel/CANSignalConfigVM.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace UIShare.UIViewModel
|
||||
{
|
||||
public class CANSignalConfig
|
||||
{
|
||||
public int Channel { get; set; }
|
||||
public int MessageID { get; set; }
|
||||
public string MessageName { get; set; }
|
||||
public string SignalName { get; set; }
|
||||
public int CollectionInterval { get; set; }
|
||||
public Guid CollectionID { get; set; }
|
||||
public string MethodName {get;set;}
|
||||
}
|
||||
}
|
||||
42
UIShare/UIViewModel/CanMessageShowVM.cs
Normal file
42
UIShare/UIViewModel/CanMessageShowVM.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
namespace UIShare.UIViewModel
|
||||
{
|
||||
public class CanMessageShowVM : BindableBase
|
||||
{
|
||||
// 字段声明
|
||||
private byte _通道;
|
||||
private int _报文ID;
|
||||
private double _时间戳;
|
||||
private byte _长度;
|
||||
|
||||
|
||||
// 通道属性
|
||||
public byte 通道
|
||||
{
|
||||
get { return _通道; }
|
||||
set { SetProperty(ref _通道, value); }
|
||||
}
|
||||
|
||||
// 报文ID属性
|
||||
public int 报文ID
|
||||
{
|
||||
get { return _报文ID; }
|
||||
set { SetProperty(ref _报文ID, value); }
|
||||
}
|
||||
|
||||
// 时间戳属性
|
||||
public double 时间戳
|
||||
{
|
||||
get { return _时间戳; }
|
||||
set { SetProperty(ref _时间戳, value); }
|
||||
}
|
||||
|
||||
// 长度属性
|
||||
public byte 长度
|
||||
{
|
||||
get { return _长度; }
|
||||
set { SetProperty(ref _长度, value); }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
22
UIShare/UIViewModel/CustomPanelItemVM.cs
Normal file
22
UIShare/UIViewModel/CustomPanelItemVM.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Prism.Mvvm;
|
||||
|
||||
namespace UIShare.UIViewModel
|
||||
{
|
||||
public class CustomPanelItemVM : BindableBase
|
||||
{
|
||||
private string _name;
|
||||
|
||||
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set => SetProperty(ref _name, value);
|
||||
}
|
||||
private string _pointY;
|
||||
public string PointY
|
||||
{
|
||||
get => _pointY;
|
||||
set => SetProperty(ref _pointY, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
80
UIShare/UIViewModel/DeviceInfoVM.cs
Normal file
80
UIShare/UIViewModel/DeviceInfoVM.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace UIShare.UIViewModel
|
||||
{
|
||||
public class DeviceInfoVM : BindableBase
|
||||
{
|
||||
private string _deviceName;
|
||||
public string DeviceName
|
||||
{
|
||||
get => _deviceName;
|
||||
set => SetProperty(ref _deviceName, value);
|
||||
}
|
||||
|
||||
private string _deviceType;
|
||||
public string DeviceType
|
||||
{
|
||||
get => _deviceType;
|
||||
set => SetProperty(ref _deviceType, value);
|
||||
}
|
||||
|
||||
private string _remark;
|
||||
public string Remark
|
||||
{
|
||||
get => _remark;
|
||||
set => SetProperty(ref _remark, value);
|
||||
}
|
||||
|
||||
private bool _isEnabled;
|
||||
public bool IsEnabled
|
||||
{
|
||||
get => _isEnabled;
|
||||
set => SetProperty(ref _isEnabled, value);
|
||||
}
|
||||
|
||||
private bool _isConnected=false;
|
||||
[JsonIgnore]
|
||||
public bool IsConnected
|
||||
{
|
||||
get => _isConnected;
|
||||
set => SetProperty(ref _isConnected, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 连接方式:"None" / "TCP" / "Serial"。
|
||||
/// 用于决定 SettingView 上"配置..."按钮打开哪一个对话框。
|
||||
/// </summary>
|
||||
private string _connectionType ;
|
||||
public string ConnectionType
|
||||
{
|
||||
get => _connectionType;
|
||||
set => SetProperty(ref _connectionType, value);
|
||||
}
|
||||
|
||||
/// <summary>TCP 连接参数(首次访问时自动初始化,便于 XAML 直接绑定)。</summary>
|
||||
private TcpConfigVM _tcpConfig = new();
|
||||
public TcpConfigVM TcpConfig
|
||||
{
|
||||
get => _tcpConfig;
|
||||
set => SetProperty(ref _tcpConfig, value);
|
||||
}
|
||||
|
||||
/// <summary>串口连接参数(首次访问时自动初始化,便于 XAML 直接绑定)。</summary>
|
||||
private SerialPortConfigVM _serialPortConfig = new();
|
||||
public SerialPortConfigVM SerialPortConfig
|
||||
{
|
||||
get => _serialPortConfig;
|
||||
set => SetProperty(ref _serialPortConfig, value);
|
||||
}
|
||||
|
||||
/// <summary>CAN 连接参数(对应 ZLGCANFD 构造 + 通道初始化参数)。</summary>
|
||||
private CANConfigVM _canConfig = new();
|
||||
public CANConfigVM CANConfig
|
||||
{
|
||||
get => _canConfig;
|
||||
set => SetProperty(ref _canConfig, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
UIShare/UIViewModel/DialogTabInfo.cs
Normal file
25
UIShare/UIViewModel/DialogTabInfo.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
namespace UIShare.UIViewModel
|
||||
{
|
||||
/// <summary>
|
||||
/// 弹窗 Tab 信息载体(事件负载,轻量 POCO)。
|
||||
/// 其他模块发布 <c>AddDialogTabEvent</c> 时填充此对象,
|
||||
/// DialogMangerViewModel 接收后创建对应的 Tab 项。
|
||||
/// </summary>
|
||||
public class DialogTabInfo
|
||||
{
|
||||
/// <summary>Tab 标题(显示在标签页上)。</summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 设备硬件指纹(如 "Tcp:192.168.1.100:502"),用于去重判断,
|
||||
/// 防止同一物理设备被重复打开多个 Tab。
|
||||
/// </summary>
|
||||
public string Fingerprint { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Tab 内容:传入一个已实例化的 <see cref="System.Windows.FrameworkElement"/>(通常是 UserControl),
|
||||
/// 由 ContentControl 直接承载展示。
|
||||
/// </summary>
|
||||
public object? Content { get; set; }
|
||||
}
|
||||
}
|
||||
68
UIShare/UIViewModel/DialogTabItemVM.cs
Normal file
68
UIShare/UIViewModel/DialogTabItemVM.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using Prism.Commands;
|
||||
using Prism.Mvvm;
|
||||
using System;
|
||||
|
||||
namespace DeviceEditModule.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// 弹窗管理器中单个 Tab 项的 ViewModel。
|
||||
/// 由 <see cref="DialogMangerViewModel"/> 在收到 AddDialogTabEvent 时创建,
|
||||
/// 通过构造函数注入选中/关闭的回调,保持与父 ViewModel 的低耦合。
|
||||
/// </summary>
|
||||
public class DialogTabItemVM : BindableBase
|
||||
{
|
||||
#region 属性
|
||||
|
||||
private string _title = string.Empty;
|
||||
/// <summary>Tab 标签页上显示的标题。</summary>
|
||||
public string Title
|
||||
{
|
||||
get => _title;
|
||||
set => SetProperty(ref _title, value);
|
||||
}
|
||||
|
||||
private string _fingerprint = string.Empty;
|
||||
/// <summary>设备硬件指纹,用于去重字典的 Key。</summary>
|
||||
public string Fingerprint
|
||||
{
|
||||
get => _fingerprint;
|
||||
set => SetProperty(ref _fingerprint, value);
|
||||
}
|
||||
|
||||
private object? _content;
|
||||
/// <summary>Tab 内容区域(通常为 UserControl 实例)。</summary>
|
||||
public object? Content
|
||||
{
|
||||
get => _content;
|
||||
set => SetProperty(ref _content, value);
|
||||
}
|
||||
|
||||
private bool _isSelected;
|
||||
/// <summary>是否为当前激活 Tab(用于切换选中态样式)。</summary>
|
||||
public bool IsSelected
|
||||
{
|
||||
get => _isSelected;
|
||||
set => SetProperty(ref _isSelected, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 命令
|
||||
|
||||
/// <summary>点击 Tab 标题激活该 Tab。</summary>
|
||||
public DelegateCommand SelectCommand { get; }
|
||||
|
||||
/// <summary>点击 Tab 上的 × 关闭并移除该 Tab。</summary>
|
||||
public DelegateCommand CloseCommand { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
/// <param name="onSelect">父 VM 提供的激活回调。</param>
|
||||
/// <param name="onClose">父 VM 提供的关闭回调。</param>
|
||||
public DialogTabItemVM(Action<DialogTabItemVM> onSelect, Action<DialogTabItemVM> onClose)
|
||||
{
|
||||
SelectCommand = new DelegateCommand(() => onSelect(this));
|
||||
CloseCommand = new DelegateCommand(() => onClose(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
36
UIShare/UIViewModel/InstructionNodeVM.cs
Normal file
36
UIShare/UIViewModel/InstructionNodeVM.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using Prism.Mvvm; // 确保引入了 Prism 命名空间
|
||||
|
||||
namespace UIShare.UIViewModel
|
||||
{
|
||||
public class InstructionNodeVM : BindableBase
|
||||
{
|
||||
private string _name = string.Empty;
|
||||
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set => SetProperty(ref _name, value);
|
||||
}
|
||||
|
||||
private ObservableCollection<InstructionNodeVM> _children = new();
|
||||
public ObservableCollection<InstructionNodeVM> Children
|
||||
{
|
||||
get => _children;
|
||||
set => SetProperty(ref _children, value);
|
||||
}
|
||||
|
||||
private object? _tag;
|
||||
public object? Tag
|
||||
{
|
||||
get => _tag;
|
||||
set => SetProperty(ref _tag, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
40
UIShare/UIViewModel/MethodVM.cs
Normal file
40
UIShare/UIViewModel/MethodVM.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace UIShare.UIViewModel
|
||||
{
|
||||
public class MethodVM
|
||||
{
|
||||
|
||||
#region 构造函数
|
||||
|
||||
public MethodVM()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public MethodVM(MethodVM source)
|
||||
{
|
||||
if (source == null) return;
|
||||
|
||||
Name = source.Name;
|
||||
FullName = source.FullName;
|
||||
|
||||
// 深拷贝参数
|
||||
Parameters = new ObservableCollection<ParameterVM>(
|
||||
source.Parameters.Select(p => new ParameterVM(p)));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public string? Name { get; set; }
|
||||
|
||||
public string? FullName { get; set; }
|
||||
|
||||
public ObservableCollection<ParameterVM> Parameters { get; set; } = [];
|
||||
}
|
||||
}
|
||||
94
UIShare/UIViewModel/MonitorChannel.cs
Normal file
94
UIShare/UIViewModel/MonitorChannel.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using OxyPlot.Series;
|
||||
using OxyPlot;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using Prism.Mvvm;
|
||||
using NCalc;
|
||||
|
||||
namespace UIShare.UIViewModel
|
||||
{
|
||||
/// <summary>
|
||||
/// 监测通道:一个设备方法对应一个通道。
|
||||
/// 数据记录(DataPoints)与图表显示(Series)完全分离:
|
||||
/// - IsMonitored=true 时始终记录 DataPoints
|
||||
/// - IsDisplayed=true 时才创建 LineSeries 并绘制
|
||||
/// </summary>
|
||||
public class MonitorChannelVM : BindableBase
|
||||
{
|
||||
// ===== 标识 =====
|
||||
public string DeviceName { get; init; } = string.Empty;
|
||||
/// <summary>硬件指纹(物理设备唯一标识,用于匹配广播事件)</summary>
|
||||
public string Fingerprint { get; init; } = string.Empty;
|
||||
public string MethodName { get; init; } = string.Empty;
|
||||
public string DisplayName { get; init; } = string.Empty;
|
||||
|
||||
// ===== 状态 =====
|
||||
private bool _isMonitored = true;
|
||||
/// <summary>是否在记录数据点(添加后始终为 true)</summary>
|
||||
public bool IsMonitored
|
||||
{
|
||||
get => _isMonitored;
|
||||
set => SetProperty(ref _isMonitored, value);
|
||||
}
|
||||
|
||||
private bool _isDisplayed = true;
|
||||
/// <summary>是否在 OxyPlot 上显示</summary>
|
||||
public bool IsDisplayed
|
||||
{
|
||||
get => _isDisplayed;
|
||||
set => SetProperty(ref _isDisplayed, value);
|
||||
}
|
||||
|
||||
// ===== 数学变换 =====
|
||||
private string? _mathExpression;
|
||||
/// <summary>可选数学变换表达式,如 "x*0.001"。为空则 DisplayValue=RawValue</summary>
|
||||
public string? MathExpression
|
||||
{
|
||||
get => _mathExpression;
|
||||
set => SetProperty(ref _mathExpression, value);
|
||||
}
|
||||
|
||||
// ===== 颜色 =====
|
||||
public OxyColor Color { get; set; } = OxyColors.SteelBlue;
|
||||
|
||||
// ===== 数据存储 =====
|
||||
/// <summary>所有采样数据点(线程安全),与 OxyPlot 无关</summary>
|
||||
public ConcurrentQueue<(DateTime Time, double RawValue, double DisplayValue)> DataPoints { get; } = new();
|
||||
|
||||
/// <summary>最大缓冲数据点数,超出则丢弃最旧的</summary>
|
||||
public int MaxBuffer { get; set; } = 5000;
|
||||
|
||||
// ===== OxyPlot Series(仅 IsDisplayed=true 时存在)=====
|
||||
private LineSeries? _series;
|
||||
public LineSeries? Series
|
||||
{
|
||||
get => _series;
|
||||
set => SetProperty(ref _series, value);
|
||||
}
|
||||
|
||||
// ===== 辅助方法 =====
|
||||
/// <summary>记录一个数据点:RawValue 经数学变换后得到 DisplayValue,一并入队</summary>
|
||||
public void Record(DateTime time, double rawValue)
|
||||
{
|
||||
double displayValue = rawValue;
|
||||
|
||||
DataPoints.Enqueue((time, rawValue, displayValue));
|
||||
|
||||
// 超出缓冲上限时批量丢弃旧数据
|
||||
while (DataPoints.Count > MaxBuffer)
|
||||
{
|
||||
DataPoints.TryDequeue(out _);
|
||||
}
|
||||
|
||||
// 如果正在显示,同步到 Series
|
||||
if (_series != null)
|
||||
{
|
||||
_series.Points.Add(new DataPoint(time.ToOADate(), displayValue));
|
||||
while (_series.Points.Count > 10000)
|
||||
{
|
||||
_series.Points.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
UIShare/UIViewModel/MonitorChannelConfig.cs
Normal file
17
UIShare/UIViewModel/MonitorChannelConfig.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace UIShare.UIViewModel
|
||||
{
|
||||
/// <summary>
|
||||
/// 监测通道持久化配置(用于 JSON 保存/恢复)。
|
||||
/// </summary>
|
||||
public class MonitorChannelConfig
|
||||
{
|
||||
/// <summary>硬件指纹(物理设备唯一标识)</summary>
|
||||
public string Fingerprint { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>监测方法名</summary>
|
||||
public string MethodName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>是否在图表上显示该通道的线图</summary>
|
||||
public bool IsDisplayed { get; set; } = true;
|
||||
}
|
||||
}
|
||||
311
UIShare/UIViewModel/ParameterVM.cs
Normal file
311
UIShare/UIViewModel/ParameterVM.cs
Normal file
@@ -0,0 +1,311 @@
|
||||
using Newtonsoft.Json;
|
||||
using Prism.Mvvm; // 引入 Prism 的 BindableBase
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace UIShare.UIViewModel
|
||||
{
|
||||
|
||||
|
||||
public class ParameterVM : BindableBase
|
||||
{
|
||||
#region 构造函数
|
||||
|
||||
public ParameterVM()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public ParameterVM(ParameterVM source)
|
||||
{
|
||||
if (source == null) return;
|
||||
|
||||
ID = source.ID;
|
||||
Name = source.Name;
|
||||
Type = source.Type;
|
||||
Category = source.Category;
|
||||
IsUseVar = source.IsUseVar;
|
||||
VariableName = source.VariableName;
|
||||
VariableID = source.VariableID;
|
||||
Value = source.Value;
|
||||
LowerLimit = source.LowerLimit;
|
||||
UpperLimit = source.UpperLimit;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private Guid _id = Guid.NewGuid();
|
||||
public Guid ID
|
||||
{
|
||||
get => _id;
|
||||
set => SetProperty(ref _id, value);
|
||||
}
|
||||
|
||||
private bool _isVisible = true;
|
||||
public bool IsVisible
|
||||
{
|
||||
get => _isVisible;
|
||||
set => SetProperty(ref _isVisible, value);
|
||||
}
|
||||
private bool _isEditable = true;
|
||||
public bool IsEditable
|
||||
{
|
||||
get => _isEditable;
|
||||
set => SetProperty(ref _isEditable, value);
|
||||
}
|
||||
|
||||
private string _name;
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set => SetProperty(ref _name, value);
|
||||
}
|
||||
|
||||
private Type _type = typeof(string);
|
||||
public Type Type
|
||||
{
|
||||
get => _type;
|
||||
set => SetProperty(ref _type, value);
|
||||
}
|
||||
|
||||
private ParameterCategory _category = ParameterCategory.Temp;
|
||||
public ParameterCategory Category
|
||||
{
|
||||
get => _category;
|
||||
set => SetProperty(ref _category, value);
|
||||
}
|
||||
|
||||
|
||||
private object? _value;
|
||||
public object? Value
|
||||
{
|
||||
get => _value;
|
||||
set => SetProperty(ref _value, value);
|
||||
}
|
||||
|
||||
private object? _lowerLimit;
|
||||
public object? LowerLimit
|
||||
{
|
||||
get => _lowerLimit;
|
||||
set => SetProperty(ref _lowerLimit, value);
|
||||
}
|
||||
|
||||
private object? _upperLimit;
|
||||
public object? UpperLimit
|
||||
{
|
||||
get => _upperLimit;
|
||||
set => SetProperty(ref _upperLimit, value);
|
||||
}
|
||||
|
||||
private bool _result = true;
|
||||
public bool Result
|
||||
{
|
||||
get => _result;
|
||||
set => SetProperty(ref _result, value);
|
||||
}
|
||||
|
||||
private bool _isUseVar;
|
||||
public bool IsUseVar
|
||||
{
|
||||
get => _isUseVar;
|
||||
set => SetProperty(ref _isUseVar, value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private string? _variableName;
|
||||
public string? VariableName
|
||||
{
|
||||
get => _variableName;
|
||||
set => SetProperty(ref _variableName, value);
|
||||
}
|
||||
|
||||
private Guid? _variableID;
|
||||
public Guid? VariableID
|
||||
{
|
||||
get => _variableID;
|
||||
set => SetProperty(ref _variableID, value);
|
||||
}
|
||||
|
||||
public enum ParameterCategory
|
||||
{
|
||||
Input,
|
||||
Output,
|
||||
Temp
|
||||
}
|
||||
|
||||
public object? GetActualValue(Dictionary<Guid, ParameterVM> paraList)
|
||||
{
|
||||
HashSet<Guid> visitedIds = new HashSet<Guid>();
|
||||
ParameterVM 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParameterVM? next = paraList[(Guid)current.VariableID!];
|
||||
if (next == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
current = next;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public ParameterVM? GetCurrentParameter(Dictionary<Guid, ParameterVM> paraList)
|
||||
{
|
||||
HashSet<Guid> visitedIds = new HashSet<Guid>();
|
||||
ParameterVM 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParameterVM? 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}) ] 不可比较");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
UIShare/UIViewModel/ProgramVM.cs
Normal file
51
UIShare/UIViewModel/ProgramVM.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using Prism.Mvvm; // 引入 Prism 的 BindableBase
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
|
||||
namespace UIShare.UIViewModel
|
||||
{
|
||||
public class ProgramVM : BindableBase
|
||||
{
|
||||
#region 构造函数
|
||||
|
||||
public ProgramVM()
|
||||
{
|
||||
// 可以进行初始化操作
|
||||
}
|
||||
|
||||
public ProgramVM(ProgramVM source)
|
||||
{
|
||||
ID = source.ID;
|
||||
StepCollection = new ObservableCollection<StepVM>(source.StepCollection.Select(p => new StepVM(p)));
|
||||
ErrorStepCollection = new ObservableCollection<StepVM>(source.ErrorStepCollection.Select(p => new StepVM(p)));
|
||||
Parameters = new ObservableCollection<ParameterVM>(source.Parameters.Select(p => new ParameterVM(p)));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public Guid ID { get; set; } = Guid.NewGuid();
|
||||
|
||||
private ObservableCollection<StepVM> _stepCollection = new ObservableCollection<StepVM>();
|
||||
public ObservableCollection<StepVM> StepCollection
|
||||
{
|
||||
get => _stepCollection;
|
||||
set => SetProperty(ref _stepCollection, value);
|
||||
}
|
||||
private ObservableCollection<StepVM> _errorStepCollection = new ObservableCollection<StepVM>();
|
||||
public ObservableCollection<StepVM> ErrorStepCollection
|
||||
{
|
||||
get => _errorStepCollection;
|
||||
set => SetProperty(ref _errorStepCollection, value);
|
||||
}
|
||||
|
||||
private ObservableCollection<ParameterVM> _parameters = new ObservableCollection<ParameterVM>();
|
||||
public ObservableCollection<ParameterVM> Parameters
|
||||
{
|
||||
get => _parameters;
|
||||
set => SetProperty(ref _parameters, value);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
92
UIShare/UIViewModel/SerialPortConfigVM.cs
Normal file
92
UIShare/UIViewModel/SerialPortConfigVM.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace UIShare.UIViewModel
|
||||
{
|
||||
/// <summary>
|
||||
/// 串口连接配置(与 DeviceCommand.Base.Serial_Port 保持字段一致)。
|
||||
/// StopBits / Parity 用字符串保存,避免 UIShare 引入 System.IO.Ports 依赖。
|
||||
/// </summary>
|
||||
public class SerialPortConfigVM : BindableBase
|
||||
{
|
||||
private string _portName = "COM1";
|
||||
public string PortName
|
||||
{
|
||||
get => _portName;
|
||||
set => SetProperty(ref _portName, value);
|
||||
}
|
||||
|
||||
private int _baudRate = 9600;
|
||||
public int BaudRate
|
||||
{
|
||||
get => _baudRate;
|
||||
set => SetProperty(ref _baudRate, value);
|
||||
}
|
||||
|
||||
private int _dataBits = 8;
|
||||
public int DataBits
|
||||
{
|
||||
get => _dataBits;
|
||||
set => SetProperty(ref _dataBits, value);
|
||||
}
|
||||
|
||||
// 取值:"One" / "OnePointFive" / "Two" / "None"
|
||||
private string _stopBits = "One";
|
||||
public string StopBits
|
||||
{
|
||||
get => _stopBits;
|
||||
set => SetProperty(ref _stopBits, value);
|
||||
}
|
||||
|
||||
// 取值:"None" / "Odd" / "Even" / "Mark" / "Space"
|
||||
private string _parity = "None";
|
||||
public string Parity
|
||||
{
|
||||
get => _parity;
|
||||
set => SetProperty(ref _parity, value);
|
||||
}
|
||||
|
||||
private int _readTimeout = 3000;
|
||||
public int ReadTimeout
|
||||
{
|
||||
get => _readTimeout;
|
||||
set => SetProperty(ref _readTimeout, value);
|
||||
}
|
||||
|
||||
private int _writeTimeout = 3000;
|
||||
public int WriteTimeout
|
||||
{
|
||||
get => _writeTimeout;
|
||||
set => SetProperty(ref _writeTimeout, value);
|
||||
}
|
||||
|
||||
public SerialPortConfigVM() { }
|
||||
|
||||
public SerialPortConfigVM(SerialPortConfigVM? src)
|
||||
{
|
||||
if (src == null) return;
|
||||
PortName = src.PortName;
|
||||
BaudRate = src.BaudRate;
|
||||
DataBits = src.DataBits;
|
||||
StopBits = src.StopBits;
|
||||
Parity = src.Parity;
|
||||
ReadTimeout = src.ReadTimeout;
|
||||
WriteTimeout = src.WriteTimeout;
|
||||
}
|
||||
|
||||
public void CopyTo(SerialPortConfigVM? dst)
|
||||
{
|
||||
if (dst == null) return;
|
||||
dst.PortName = PortName;
|
||||
dst.BaudRate = BaudRate;
|
||||
dst.DataBits = DataBits;
|
||||
dst.StopBits = StopBits;
|
||||
dst.Parity = Parity;
|
||||
dst.ReadTimeout = ReadTimeout;
|
||||
dst.WriteTimeout = WriteTimeout;
|
||||
}
|
||||
}
|
||||
}
|
||||
30
UIShare/UIViewModel/SharedParameter.cs
Normal file
30
UIShare/UIViewModel/SharedParameter.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace UIShare.UIViewModel
|
||||
{
|
||||
public class SharedParameter:BindableBase
|
||||
{
|
||||
private string _id;
|
||||
public string Id
|
||||
{
|
||||
get => _id;
|
||||
set => SetProperty(ref _id, value);
|
||||
}
|
||||
private string _parameterName;
|
||||
public string ParameterName
|
||||
{
|
||||
get => _parameterName;
|
||||
set => SetProperty(ref _parameterName, value);
|
||||
}
|
||||
private int _value;
|
||||
public int Value
|
||||
{
|
||||
get => _value;
|
||||
set => SetProperty(ref _value, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
178
UIShare/UIViewModel/StepVM.cs
Normal file
178
UIShare/UIViewModel/StepVM.cs
Normal file
@@ -0,0 +1,178 @@
|
||||
using Newtonsoft.Json;
|
||||
using Prism.Mvvm; // 引入 Prism 的 BindableBase
|
||||
using System;
|
||||
|
||||
namespace UIShare.UIViewModel
|
||||
{
|
||||
public class StepVM : BindableBase
|
||||
{
|
||||
#region 构造函数
|
||||
|
||||
public StepVM() { }
|
||||
|
||||
public StepVM(StepVM 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;
|
||||
GotoSettingString = source.GotoSettingString;
|
||||
NGGotoStepID = source.NGGotoStepID;
|
||||
Description = source.Description;
|
||||
IsUsed = source.IsUsed;
|
||||
|
||||
if (source.Method != null)
|
||||
{
|
||||
Method = new MethodVM(source.Method);
|
||||
}
|
||||
if (source.SubProgram != null)
|
||||
{
|
||||
SubProgram = new ProgramVM(source.SubProgram);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private Guid _id = Guid.NewGuid();
|
||||
|
||||
public Guid ID
|
||||
{
|
||||
get => _id;
|
||||
set => SetProperty(ref _id, value);
|
||||
}
|
||||
|
||||
private bool _isUsed = true;
|
||||
|
||||
public bool IsUsed
|
||||
{
|
||||
get => _isUsed;
|
||||
set => SetProperty(ref _isUsed, value);
|
||||
}
|
||||
|
||||
private int _index;
|
||||
|
||||
public int Index
|
||||
{
|
||||
get => _index;
|
||||
set => SetProperty(ref _index, value);
|
||||
}
|
||||
|
||||
private string? _name;
|
||||
|
||||
public string? Name
|
||||
{
|
||||
get => _name;
|
||||
set => SetProperty(ref _name, value);
|
||||
}
|
||||
|
||||
private string? _stepType;
|
||||
|
||||
public string? StepType
|
||||
{
|
||||
get => _stepType;
|
||||
set => SetProperty(ref _stepType, value);
|
||||
}
|
||||
|
||||
private MethodVM? _method;
|
||||
|
||||
public MethodVM? Method
|
||||
{
|
||||
get => _method;
|
||||
set => SetProperty(ref _method, value);
|
||||
}
|
||||
|
||||
private ProgramVM? _subProgram;
|
||||
|
||||
public ProgramVM? SubProgram
|
||||
{
|
||||
get => _subProgram;
|
||||
set => SetProperty(ref _subProgram, value);
|
||||
}
|
||||
|
||||
private int? _loopCount;
|
||||
|
||||
public int? LoopCount
|
||||
{
|
||||
get => _loopCount;
|
||||
set => SetProperty(ref _loopCount, value);
|
||||
}
|
||||
[JsonIgnore]
|
||||
private int? _currentLoopCount;
|
||||
[JsonIgnore]
|
||||
public int? CurrentLoopCount
|
||||
{
|
||||
get => _currentLoopCount;
|
||||
set => SetProperty(ref _currentLoopCount, value);
|
||||
}
|
||||
|
||||
private Guid? _loopStartStepId;
|
||||
|
||||
public Guid? LoopStartStepId
|
||||
{
|
||||
get => _loopStartStepId;
|
||||
set => SetProperty(ref _loopStartStepId, value);
|
||||
}
|
||||
[JsonIgnore]
|
||||
private int _result = -1;
|
||||
[JsonIgnore]
|
||||
public int Result
|
||||
{
|
||||
get => _result;
|
||||
set => SetProperty(ref _result, value);
|
||||
}
|
||||
[JsonIgnore]
|
||||
private int? _runTime;
|
||||
[JsonIgnore]
|
||||
public int? RunTime
|
||||
{
|
||||
get => _runTime;
|
||||
set => SetProperty(ref _runTime, value);
|
||||
}
|
||||
|
||||
private string? _okExpression;
|
||||
|
||||
public string? OKExpression
|
||||
{
|
||||
get => _okExpression;
|
||||
set => SetProperty(ref _okExpression, value);
|
||||
}
|
||||
|
||||
private string _gotoSettingString = "";
|
||||
|
||||
public string GotoSettingString
|
||||
{
|
||||
get => _gotoSettingString;
|
||||
set => SetProperty(ref _gotoSettingString, value);
|
||||
}
|
||||
|
||||
private Guid? _okGotoStepID;
|
||||
|
||||
public Guid? OKGotoStepID
|
||||
{
|
||||
get => _okGotoStepID;
|
||||
set => SetProperty(ref _okGotoStepID, value);
|
||||
}
|
||||
|
||||
private Guid? _ngGotoStepID;
|
||||
|
||||
public Guid? NGGotoStepID
|
||||
{
|
||||
get => _ngGotoStepID;
|
||||
set => SetProperty(ref _ngGotoStepID, value);
|
||||
}
|
||||
|
||||
private string? _description;
|
||||
|
||||
public string? Description
|
||||
{
|
||||
get => _description;
|
||||
set => SetProperty(ref _description, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
28
UIShare/UIViewModel/SubProgramItemVM.cs
Normal file
28
UIShare/UIViewModel/SubProgramItemVM.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace UIShare.UIViewModel
|
||||
{
|
||||
public class SubProgramItemVM : BindableBase
|
||||
{
|
||||
|
||||
private string _name="";
|
||||
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set => SetProperty(ref _name, value);
|
||||
}
|
||||
private string _filePath = "";
|
||||
|
||||
public string FilePath
|
||||
{
|
||||
get => _filePath;
|
||||
set => SetProperty(ref _filePath, value);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
62
UIShare/UIViewModel/TcpConfigVM.cs
Normal file
62
UIShare/UIViewModel/TcpConfigVM.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using Prism.Mvvm;
|
||||
|
||||
namespace UIShare.UIViewModel
|
||||
{
|
||||
/// <summary>
|
||||
/// TCP 连接配置(与 DeviceCommand.Base.Tcp 保持字段一致)。
|
||||
/// </summary>
|
||||
public class TcpConfigVM : BindableBase
|
||||
{
|
||||
private string _ipAddress = "127.0.0.1";
|
||||
public string IPAddress
|
||||
{
|
||||
get => _ipAddress;
|
||||
set => SetProperty(ref _ipAddress, value);
|
||||
}
|
||||
|
||||
private int _port = 502;
|
||||
public int Port
|
||||
{
|
||||
get => _port;
|
||||
set => SetProperty(ref _port, value);
|
||||
}
|
||||
|
||||
private int _sendTimeout = 3000;
|
||||
public int SendTimeout
|
||||
{
|
||||
get => _sendTimeout;
|
||||
set => SetProperty(ref _sendTimeout, value);
|
||||
}
|
||||
|
||||
private int _receiveTimeout = 3000;
|
||||
public int ReceiveTimeout
|
||||
{
|
||||
get => _receiveTimeout;
|
||||
set => SetProperty(ref _receiveTimeout, value);
|
||||
}
|
||||
|
||||
public TcpConfigVM() { }
|
||||
|
||||
/// <summary>拷贝构造,用于对话框编辑副本。</summary>
|
||||
public TcpConfigVM(TcpConfigVM? src)
|
||||
{
|
||||
if (src == null) return;
|
||||
IPAddress = src.IPAddress;
|
||||
Port = src.Port;
|
||||
SendTimeout = src.SendTimeout;
|
||||
ReceiveTimeout = src.ReceiveTimeout;
|
||||
}
|
||||
|
||||
/// <summary>把字段拷回目标对象(保存时用)。</summary>
|
||||
public void CopyTo(TcpConfigVM? dst)
|
||||
{
|
||||
if (dst == null) return;
|
||||
dst.IPAddress = IPAddress;
|
||||
dst.Port = Port;
|
||||
dst.SendTimeout = SendTimeout;
|
||||
dst.ReceiveTimeout = ReceiveTimeout;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
117
UIShare/UIViewModel/ValueLimitVM.cs
Normal file
117
UIShare/UIViewModel/ValueLimitVM.cs
Normal file
@@ -0,0 +1,117 @@
|
||||
using Prism.Mvvm;
|
||||
|
||||
namespace UIShare.UIViewModel
|
||||
{
|
||||
// 继承 BindableBase 使得属性变更时能实时通知 UI 刷新
|
||||
public class ValueLimitVM : BindableBase
|
||||
{
|
||||
private string _signalName = string.Empty;
|
||||
private string _fingerprint = string.Empty;
|
||||
private string _methodName = string.Empty;
|
||||
private string _displayName = string.Empty;
|
||||
private double _upper;
|
||||
private double _lower;
|
||||
private double _upperExtreme;
|
||||
private double _lowerExtreme;
|
||||
private bool _isAlarm = false;
|
||||
private AlarmStatus _alarmStatus = AlarmStatus.未报警;
|
||||
|
||||
/// <summary>
|
||||
/// 信号名(显示用)
|
||||
/// </summary>
|
||||
public string SignalName
|
||||
{
|
||||
get => _signalName;
|
||||
set => SetProperty(ref _signalName, value);
|
||||
}
|
||||
/// <summary>
|
||||
/// 信号名(显示用)
|
||||
/// </summary>
|
||||
public string DisplayName
|
||||
{
|
||||
get => _displayName;
|
||||
set => SetProperty(ref _displayName, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 硬件指纹(物理设备唯一标识,用于超限匹配)
|
||||
/// </summary>
|
||||
public string Fingerprint
|
||||
{
|
||||
get => _fingerprint;
|
||||
set => SetProperty(ref _fingerprint, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 方法名(唯一标识,用于超限匹配)
|
||||
/// </summary>
|
||||
public string MethodName
|
||||
{
|
||||
get => _methodName;
|
||||
set => SetProperty(ref _methodName, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上限
|
||||
/// </summary>
|
||||
public double Upper
|
||||
{
|
||||
get => _upper;
|
||||
set => SetProperty(ref _upper, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下限
|
||||
/// </summary>
|
||||
public double Lower
|
||||
{
|
||||
get => _lower;
|
||||
set => SetProperty(ref _lower, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上极限
|
||||
/// </summary>
|
||||
public double UpperExtreme
|
||||
{
|
||||
get => _upperExtreme;
|
||||
set => SetProperty(ref _upperExtreme, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下极限
|
||||
/// </summary>
|
||||
public double LowerExtreme
|
||||
{
|
||||
get => _lowerExtreme;
|
||||
set => SetProperty(ref _lowerExtreme, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否报警
|
||||
/// </summary>
|
||||
public bool IsAlarm
|
||||
{
|
||||
get => _isAlarm;
|
||||
set => SetProperty(ref _isAlarm, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 警报状态类型
|
||||
/// </summary>
|
||||
public AlarmStatus AlarmSatus
|
||||
{
|
||||
get => _alarmStatus;
|
||||
set => SetProperty(ref _alarmStatus, value);
|
||||
}
|
||||
}
|
||||
|
||||
public enum AlarmStatus
|
||||
{
|
||||
未报警 = 0, // 未报警
|
||||
超上限 = 1, // 超上限
|
||||
超下限 = 2, // 超下限
|
||||
超上极限 = 3, // 超上极限
|
||||
超下极限 = 4, // 超下极限
|
||||
}
|
||||
}
|
||||
57
UIShare/ViewModelBase/DialogViewModelBase.cs
Normal file
57
UIShare/ViewModelBase/DialogViewModelBase.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using Notifications.Wpf.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Prism.Dialogs;
|
||||
|
||||
namespace UIShare.ViewModelBase
|
||||
{
|
||||
public abstract class DialogViewModelBase : BindableBase,IDialogAware
|
||||
{
|
||||
public DialogCloseListener RequestClose { get; set; }
|
||||
public IEventAggregator _eventAggregator;
|
||||
public IDialogService _dialogService;
|
||||
private INotificationManager _notificationManager;
|
||||
public DialogViewModelBase(IContainerProvider containerProvider)
|
||||
{
|
||||
_eventAggregator = containerProvider.Resolve<IEventAggregator>();
|
||||
_dialogService = containerProvider.Resolve<IDialogService>();
|
||||
_notificationManager = containerProvider.Resolve<INotificationManager>();
|
||||
}
|
||||
|
||||
protected void ShowInfoMessageBox(string Message, Action callback)
|
||||
{
|
||||
var dialogParams = new DialogParameters();
|
||||
dialogParams.Add("Title", "提示");
|
||||
dialogParams.Add("Message", Message);
|
||||
dialogParams.Add("Icon", "info");
|
||||
dialogParams.Add("ShowOk", true);
|
||||
_dialogService.ShowDialog("MessageBox", dialogParams, result =>
|
||||
{
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
protected void ShowErrorMessageBox(string Message, Action callback)
|
||||
{
|
||||
var dialogParams = new DialogParameters();
|
||||
dialogParams.Add("Title", "错误");
|
||||
dialogParams.Add("Message", Message);
|
||||
dialogParams.Add("Icon", "info");
|
||||
dialogParams.Add("ShowOk", true);
|
||||
_dialogService.ShowDialog("MessageBox", dialogParams, result =>
|
||||
{
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
#region Dialog
|
||||
|
||||
public virtual bool CanCloseDialog() => true;
|
||||
public virtual void OnDialogClosed() { }
|
||||
public virtual void OnDialogOpened(IDialogParameters parameters) { }
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
65
UIShare/ViewModelBase/NavigateViewModelBase.cs
Normal file
65
UIShare/ViewModelBase/NavigateViewModelBase.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using Notifications.Wpf.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UIShare.GlobalVariable;
|
||||
|
||||
namespace UIShare.ViewModelBase
|
||||
{
|
||||
public abstract class NavigateViewModelBase : BindableBase, INavigationAware
|
||||
{
|
||||
public DialogCloseListener RequestClose { get; set; }
|
||||
public IEventAggregator _eventAggregator;
|
||||
public IDialogService _dialogService;
|
||||
public IRegionManager _regionManager;
|
||||
public IModuleManager _moduleManager;
|
||||
private INotificationManager _notificationManager;
|
||||
private GlobalInfo _globalInfo;
|
||||
public NavigateViewModelBase(IContainerProvider containerProvider)
|
||||
{
|
||||
_globalInfo=containerProvider.Resolve<GlobalInfo>();
|
||||
_eventAggregator = containerProvider.Resolve<IEventAggregator>();
|
||||
_dialogService = containerProvider.Resolve<IDialogService>();
|
||||
_moduleManager = containerProvider.Resolve<IModuleManager>();
|
||||
_regionManager = containerProvider.Resolve<IRegionManager>();
|
||||
_notificationManager = containerProvider.Resolve<INotificationManager>();
|
||||
}
|
||||
|
||||
protected void ShowInfoMessageBox(string Message,Action callback)
|
||||
{
|
||||
var dialogParams = new DialogParameters();
|
||||
dialogParams.Add("Title", "提示");
|
||||
dialogParams.Add("Message", Message);
|
||||
dialogParams.Add("Icon", "info");
|
||||
dialogParams.Add("ShowOk", true);
|
||||
_dialogService.ShowDialog("MessageBox", dialogParams, result =>
|
||||
{
|
||||
callback();
|
||||
});
|
||||
}
|
||||
protected void ShowErrorMessageBox(string Message,Action callback)
|
||||
{
|
||||
var dialogParams = new DialogParameters();
|
||||
dialogParams.Add("Title", "错误");
|
||||
dialogParams.Add("Message", Message);
|
||||
dialogParams.Add("Icon", "error");
|
||||
dialogParams.Add("ShowOk", true);
|
||||
_dialogService.ShowDialog("MessageBox", dialogParams, result =>
|
||||
{
|
||||
callback();
|
||||
});
|
||||
}
|
||||
#region Navigation
|
||||
|
||||
public virtual void OnNavigatedTo(NavigationContext navigationContext) { }
|
||||
|
||||
public virtual bool IsNavigationTarget(NavigationContext navigationContext) => true;
|
||||
|
||||
public virtual void OnNavigatedFrom(NavigationContext navigationContext) { }
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user