Files
CapMachine/CapMachine.Wpf/ViewModels/DialogZlgCanLinRwConfigViewModel.cs
2026-02-06 12:34:34 +08:00

754 lines
28 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using CapMachine.Core;
using CapMachine.Model.CANLIN;
using CapMachine.Wpf.Dtos;
using CapMachine.Wpf.Services;
using FreeSql;
using Prism.Commands;
using Prism.Services.Dialogs;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Data;
namespace CapMachine.Wpf.ViewModels
{
/// <summary>
/// ZLG CAN/LIN 读写配置三栏管理弹窗 ViewModel。
/// 左侧:写入/读取配置;右侧:信号全集候选池;统一保存落库。
/// </summary>
public class DialogZlgCanLinRwConfigViewModel : DialogViewModel
{
private readonly IFreeSql _freeSql;
private readonly ILogService _logService;
private readonly LogicRuleService _logicRuleService;
private long _canLinConfigProId;
/// <summary>
/// 构造函数。
/// </summary>
/// <param name="freeSql">FreeSql。</param>
/// <param name="logService">日志。</param>
/// <param name="logicRuleService">逻辑规则服务。</param>
public DialogZlgCanLinRwConfigViewModel(IFreeSql freeSql, ILogService logService, LogicRuleService logicRuleService)
{
_freeSql = freeSql;
_logService = logService;
_logicRuleService = logicRuleService;
Title = "读写设置";
WriteConfigs = new ObservableCollection<CanLinRWConfigDto>();
ReadConfigs = new ObservableCollection<CanLinRWConfigDto>();
SignalCandidates = new ObservableCollection<SignalCandidate>();
SignalTree = new ObservableCollection<SignalFrameNode>();
SignalCandidatesView = CollectionViewSource.GetDefaultView(SignalCandidates);
SignalCandidatesView.Filter = FilterSignalCandidate;
WriteNameCbxItems = new ObservableCollection<CbxItems>()
{
new CbxItems(){ Key="转速",Text="转速"},
new CbxItems(){ Key="功率限制",Text="功率限制"},
new CbxItems(){ Key="使能",Text="使能"},
new CbxItems(){ Key="Anti_Sleep",Text="Anti_Sleep"},
new CbxItems(){ Key="PTC使能",Text="PTC使能"},
new CbxItems(){ Key="PTC功率",Text="PTC功率"},
new CbxItems(){ Key="PTC水流量",Text="PTC水流量"},
new CbxItems(){ Key="PTC水温",Text="PTC水温"},
};
ReadNameCbxItems = new ObservableCollection<CbxItems>()
{
new CbxItems(){ Key="通讯Cmp转速",Text="通讯Cmp转速"},
new CbxItems(){ Key="通讯Cmp母线电压",Text="通讯Cmp母线电压"},
new CbxItems(){ Key="通讯Cmp母线电流",Text="通讯Cmp母线电流"},
new CbxItems(){ Key="通讯Cmp逆变器温度",Text="通讯Cmp逆变器温度"},
new CbxItems(){ Key="通讯Cmp相电流",Text="通讯Cmp相电流"},
new CbxItems(){ Key="通讯Cmp功率",Text="通讯Cmp功率"},
new CbxItems(){ Key="通讯Cmp芯片温度",Text="通讯Cmp芯片温度"},
new CbxItems(){ Key="通讯PTC入水温度",Text="通讯PTC入水温度"},
new CbxItems(){ Key="通讯PTC出水温度",Text="通讯PTC出水温度"},
new CbxItems(){ Key="通讯PTC峰值电流",Text="通讯PTC峰值电流"},
new CbxItems(){ Key="通讯PTC母线电流",Text="通讯PTC母线电流"},
new CbxItems(){ Key="通讯PTC膜温",Text="通讯PTC膜温"},
new CbxItems(){ Key="通讯PTC模块温度",Text="通讯PTC模块温度"},
};
IsEditable = true;
}
/// <summary>
/// 是否允许编辑(由调用方根据 Active/打开状态决定)。
/// </summary>
public bool IsEditable { get; private set; }
/// <summary>
/// 逻辑规则集合(下拉框 ItemsSource
/// </summary>
public IReadOnlyList<LogicRuleDto> LogicRuleDtos => _logicRuleService.LogicRuleDtos;
/// <summary>
/// 写入配置“名称”下拉框集合(参考 CANConfigViewModel
/// </summary>
public ObservableCollection<CbxItems> WriteNameCbxItems { get; private set; }
/// <summary>
/// 读取配置“名称”下拉框集合(参考 CANConfigViewModel
/// </summary>
public ObservableCollection<CbxItems> ReadNameCbxItems { get; private set; }
/// <summary>
/// 写入配置集合。
/// </summary>
public ObservableCollection<CanLinRWConfigDto> WriteConfigs { get; private set; }
/// <summary>
/// 读取配置集合。
/// </summary>
public ObservableCollection<CanLinRWConfigDto> ReadConfigs { get; private set; }
/// <summary>
/// 信号候选集合(右侧池)。
/// </summary>
public ObservableCollection<SignalCandidate> SignalCandidates { get; private set; }
/// <summary>
/// 信号树(按帧分组)。
/// </summary>
public ObservableCollection<SignalFrameNode> SignalTree { get; private set; }
/// <summary>
/// 候选信号视图(含过滤)。
/// </summary>
public ICollectionView SignalCandidatesView { get; private set; }
private string? _signalFilterText;
/// <summary>
/// 信号过滤文本(按 MsgName/SignalName/Name/Desc 匹配)。
/// </summary>
public string? SignalFilterText
{
get { return _signalFilterText; }
set
{
_signalFilterText = value;
RaisePropertyChanged();
SignalCandidatesView.Refresh();
RebuildSignalTree();
}
}
/// <summary>
/// 当前选中的候选信号。
/// </summary>
public SignalCandidate? SelectedSignalCandidate { get; set; }
/// <summary>
/// 当前选中的写入配置行。
/// </summary>
public CanLinRWConfigDto? SelectedWriteConfig { get; set; }
/// <summary>
/// 当前选中的读取配置行。
/// </summary>
public CanLinRWConfigDto? SelectedReadConfig { get; set; }
private DelegateCommand<object>? _signalTreeSelectionChangedCmd;
/// <summary>
/// 右侧信号树选中变化(仅当选中叶子节点时回写 SelectedSignalCandidate
/// </summary>
public DelegateCommand<object> SignalTreeSelectionChangedCmd =>
_signalTreeSelectionChangedCmd ??= new DelegateCommand<object>(SignalTreeSelectionChangedCmdMethod);
private void SignalTreeSelectionChangedCmdMethod(object par)
{
if (par is SignalCandidate leaf)
{
SelectedSignalCandidate = leaf;
RaisePropertyChanged(nameof(SelectedSignalCandidate));
return;
}
if (par is SignalFrameNode)
{
// 选中父节点时不变更 SelectedSignalCandidate
return;
}
}
private DelegateCommand? _addToWriteCmd;
/// <summary>
/// 将右侧选中信号添加到写入配置。
/// </summary>
public DelegateCommand AddToWriteCmd => _addToWriteCmd ??= new DelegateCommand(AddToWriteCmdMethod);
private void AddToWriteCmdMethod()
{
if (!IsEditable)
{
MessageBox.Show("当前状态禁止修改", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
return;
}
if (SelectedSignalCandidate == null)
{
MessageBox.Show("请先在右侧信号集合中选中一条", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
return;
}
if (string.IsNullOrWhiteSpace(SelectedSignalCandidate.SignalName) || string.IsNullOrWhiteSpace(SelectedSignalCandidate.MsgName))
{
MessageBox.Show("选中的信号数据不完整MsgName/SignalName 为空)", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
return;
}
if (WriteConfigs.Any(a => string.Equals(a.SignalName, SelectedSignalCandidate.SignalName, StringComparison.Ordinal) && a.RWInfo == RW.Write))
{
MessageBox.Show("该信号已在写入配置中,无需重复添加", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
return;
}
if (ReadConfigs.Any(a => string.Equals(a.SignalName, SelectedSignalCandidate.SignalName, StringComparison.Ordinal) && a.RWInfo == RW.Read))
{
MessageBox.Show("该信号已在读取配置中,同一个信号不允许同时配置为写入与读取", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
return;
}
WriteConfigs.Add(new CanLinRWConfigDto
{
Id = 0,
RWInfo = RW.Write,
Name = string.IsNullOrWhiteSpace(SelectedSignalCandidate.Name) ? SelectedSignalCandidate.SignalName : SelectedSignalCandidate.Name,
MsgFrameName = SelectedSignalCandidate.MsgName,
SignalName = SelectedSignalCandidate.SignalName,
DefautValue = "0",
LogicRuleId = 0,
});
RebuildSignalTree();
}
private DelegateCommand? _addToReadCmd;
/// <summary>
/// 将右侧选中信号添加到读取配置。
/// </summary>
public DelegateCommand AddToReadCmd => _addToReadCmd ??= new DelegateCommand(AddToReadCmdMethod);
private void AddToReadCmdMethod()
{
if (!IsEditable)
{
MessageBox.Show("当前状态禁止修改", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
return;
}
if (SelectedSignalCandidate == null)
{
MessageBox.Show("请先在右侧信号集合中选中一条", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
return;
}
if (string.IsNullOrWhiteSpace(SelectedSignalCandidate.SignalName) || string.IsNullOrWhiteSpace(SelectedSignalCandidate.MsgName))
{
MessageBox.Show("选中的信号数据不完整MsgName/SignalName 为空)", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
return;
}
if (ReadConfigs.Any(a => string.Equals(a.SignalName, SelectedSignalCandidate.SignalName, StringComparison.Ordinal) && a.RWInfo == RW.Read))
{
MessageBox.Show("该信号已在读取配置中,无需重复添加", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
return;
}
if (WriteConfigs.Any(a => string.Equals(a.SignalName, SelectedSignalCandidate.SignalName, StringComparison.Ordinal) && a.RWInfo == RW.Write))
{
MessageBox.Show("该信号已在写入配置中,同一个信号不允许同时配置为写入与读取", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
return;
}
ReadConfigs.Add(new CanLinRWConfigDto
{
Id = 0,
RWInfo = RW.Read,
Name = string.IsNullOrWhiteSpace(SelectedSignalCandidate.Name) ? SelectedSignalCandidate.SignalName : SelectedSignalCandidate.Name,
MsgFrameName = SelectedSignalCandidate.MsgName,
SignalName = SelectedSignalCandidate.SignalName,
DefautValue = "0",
LogicRuleId = 0,
});
RebuildSignalTree();
}
private DelegateCommand? _removeWriteCmd;
/// <summary>
/// 从写入配置移除当前选中行。
/// </summary>
public DelegateCommand RemoveWriteCmd => _removeWriteCmd ??= new DelegateCommand(RemoveWriteCmdMethod);
private void RemoveWriteCmdMethod()
{
if (!IsEditable)
{
MessageBox.Show("当前状态禁止修改", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
return;
}
if (SelectedWriteConfig == null)
{
MessageBox.Show("请先选中写入列表中的一行", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
return;
}
WriteConfigs.Remove(SelectedWriteConfig);
SelectedWriteConfig = null;
RaisePropertyChanged(nameof(SelectedWriteConfig));
RebuildSignalTree();
}
private DelegateCommand? _removeReadCmd;
/// <summary>
/// 从读取配置移除当前选中行。
/// </summary>
public DelegateCommand RemoveReadCmd => _removeReadCmd ??= new DelegateCommand(RemoveReadCmdMethod);
private void RemoveReadCmdMethod()
{
if (!IsEditable)
{
MessageBox.Show("当前状态禁止修改", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
return;
}
if (SelectedReadConfig == null)
{
MessageBox.Show("请先选中读取列表中的一行", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
return;
}
ReadConfigs.Remove(SelectedReadConfig);
SelectedReadConfig = null;
RaisePropertyChanged(nameof(SelectedReadConfig));
RebuildSignalTree();
}
private DelegateCommand? _saveCmd;
/// <summary>
/// 保存并落库。
/// </summary>
public DelegateCommand SaveCmd => _saveCmd ??= new DelegateCommand(SaveCmdMethod);
private void SaveCmdMethod()
{
if (!IsEditable)
{
MessageBox.Show("当前状态禁止修改", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
return;
}
if (_canLinConfigProId <= 0)
{
MessageBox.Show("配置程序ID无效无法保存", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
return;
}
try
{
PersistRwConfigs();
var pars = new DialogParameters
{
{ "Saved", true }
};
RaiseRequestClose(new DialogResult(ButtonResult.OK, pars));
}
catch (Exception ex)
{
_logService.Error($"ZLG 读写设置保存失败:{ex}");
MessageBox.Show(ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private DelegateCommand? _cancelCmd;
/// <summary>
/// 取消。
/// </summary>
public DelegateCommand CancelCmd => _cancelCmd ??= new DelegateCommand(CancelCmdMethod);
private void CancelCmdMethod()
{
RaiseRequestClose(new DialogResult(ButtonResult.Cancel));
}
/// <summary>
/// 弹窗打开时接收参数。
/// </summary>
/// <param name="parameters">参数。</param>
public override void OnDialogOpened(IDialogParameters parameters)
{
_canLinConfigProId = parameters.GetValue<long>("CanLinConfigProId");
IsEditable = parameters.ContainsKey("IsEditable") ? parameters.GetValue<bool>("IsEditable") : true;
RaisePropertyChanged(nameof(IsEditable));
if (parameters.ContainsKey("WriteConfigs"))
{
var list = parameters.GetValue<ObservableCollection<CanLinRWConfigDto>>("WriteConfigs") ?? new ObservableCollection<CanLinRWConfigDto>();
WriteConfigs = list;
RaisePropertyChanged(nameof(WriteConfigs));
}
if (parameters.ContainsKey("ReadConfigs"))
{
var list = parameters.GetValue<ObservableCollection<CanLinRWConfigDto>>("ReadConfigs") ?? new ObservableCollection<CanLinRWConfigDto>();
ReadConfigs = list;
RaisePropertyChanged(nameof(ReadConfigs));
}
if (parameters.ContainsKey("SignalCandidates"))
{
var list = parameters.GetValue<ObservableCollection<SignalCandidate>>("SignalCandidates") ?? new ObservableCollection<SignalCandidate>();
SignalCandidates = list;
RaisePropertyChanged(nameof(SignalCandidates));
SignalCandidatesView = CollectionViewSource.GetDefaultView(SignalCandidates);
SignalCandidatesView.Filter = FilterSignalCandidate;
RaisePropertyChanged(nameof(SignalCandidatesView));
}
RebuildSignalTree();
if (parameters.ContainsKey("Title"))
{
Title = parameters.GetValue<string>("Title") ?? Title;
}
}
private bool FilterSignalCandidate(object obj)
{
if (obj is not SignalCandidate item)
{
return false;
}
if (string.IsNullOrWhiteSpace(SignalFilterText))
{
return true;
}
var key = SignalFilterText.Trim();
return ContainsIgnoreCase(item.MsgName, key)
|| ContainsIgnoreCase(item.SignalName, key)
|| ContainsIgnoreCase(item.Name, key)
|| ContainsIgnoreCase(item.Desc, key);
}
private void RebuildSignalTree()
{
// 依据过滤条件 + 全量候选池生成树。
// 树数据源独立于 ICollectionView避免 TreeView 过滤复杂度。
var filtered = SignalCandidates
.Where(a => FilterSignalCandidate(a))
.ToList();
var groups = filtered
.GroupBy(a => string.IsNullOrWhiteSpace(a.MsgName) ? "(未命名帧)" : a.MsgName!.Trim(), StringComparer.Ordinal)
.OrderBy(a => a.Key, StringComparer.Ordinal)
.ToList();
SignalTree.Clear();
foreach (var g in groups)
{
var signalNodes = g
.OrderBy(a => a.SignalName ?? string.Empty, StringComparer.Ordinal)
.ThenBy(a => a.Name ?? string.Empty, StringComparer.Ordinal)
.Select(a => new SignalCandidate
{
MsgName = a.MsgName,
SignalName = a.SignalName,
Name = a.Name,
Desc = a.Desc,
AddedInfo = ComputeAddedInfo(a),
})
.ToList();
var node = new SignalFrameNode
{
FrameName = g.Key,
Signals = new ObservableCollection<SignalCandidate>(signalNodes)
};
SignalTree.Add(node);
}
RaisePropertyChanged(nameof(SignalTree));
}
/// <summary>
/// 计算候选信号是否已被添加到写入/读取。
/// 0=未添加1=已添加到写入2=已添加到读取3=同时存在于写入与读取。
/// </summary>
/// <param name="candidate">候选信号。</param>
/// <returns>AddedInfo 标志值。</returns>
private int ComputeAddedInfo(SignalCandidate candidate)
{
if (candidate == null || string.IsNullOrWhiteSpace(candidate.SignalName))
{
return 0;
}
var signal = candidate.SignalName;
var inWrite = WriteConfigs.Any(a => a.RWInfo == RW.Write && string.Equals(a.SignalName, signal, StringComparison.Ordinal));
var inRead = ReadConfigs.Any(a => a.RWInfo == RW.Read && string.Equals(a.SignalName, signal, StringComparison.Ordinal));
if (inWrite && inRead) return 3;
if (inWrite) return 1;
if (inRead) return 2;
return 0;
}
private static bool ContainsIgnoreCase(string? src, string key)
{
if (string.IsNullOrEmpty(src)) return false;
return src.IndexOf(key, StringComparison.OrdinalIgnoreCase) >= 0;
}
private void PersistRwConfigs()
{
// 互斥约束:同一 SignalName 不允许同时出现在写入与读取
EnsureNoWriteReadConflict();
// 规范化 DTO空值/默认值防御)
NormalizeRwConfigs(WriteConfigs, RW.Write);
NormalizeRwConfigs(ReadConfigs, RW.Read);
// 防重复:同一 SignalName 在同一 RW 列表中只允许一条
EnsureNoDuplicateSignal(WriteConfigs, RW.Write);
EnsureNoDuplicateSignal(ReadConfigs, RW.Read);
var existing = _freeSql.Select<CanLinRWConfig>()
.Where(a => a.CanLinConfigProId == _canLinConfigProId)
.Where(a => a.RWInfo == RW.Write || a.RWInfo == RW.Read)
.ToList();
var desiredWrite = WriteConfigs
.Where(a => !string.IsNullOrWhiteSpace(a.SignalName))
.Select(a => new DesiredItem(RW.Write, a.SignalName!, a))
.ToList();
var desiredRead = ReadConfigs
.Where(a => !string.IsNullOrWhiteSpace(a.SignalName))
.Select(a => new DesiredItem(RW.Read, a.SignalName!, a))
.ToList();
var desiredAll = desiredWrite.Concat(desiredRead).ToList();
var desiredKeySet = new HashSet<string>(desiredAll.Select(a => BuildKey(a.Rw, a.SignalName)), StringComparer.Ordinal);
var existingByKey = existing.ToDictionary(a => BuildKey(a.RWInfo, a.SignalName ?? string.Empty), a => a, StringComparer.Ordinal);
// 删除DB 中存在,但目标集合里不存在
foreach (var old in existing)
{
var key = BuildKey(old.RWInfo, old.SignalName ?? string.Empty);
if (!desiredKeySet.Contains(key))
{
_freeSql.Delete<CanLinRWConfig>(old.Id).ExecuteAffrows();
}
}
// Upsert按 keyRWInfo + SignalName更新或插入
foreach (var item in desiredAll)
{
var key = BuildKey(item.Rw, item.SignalName);
if (existingByKey.TryGetValue(key, out var old))
{
_freeSql.Update<CanLinRWConfig>(old.Id)
.Set(a => a.Name, item.Dto.Name)
.Set(a => a.MsgFrameName, item.Dto.MsgFrameName)
.Set(a => a.SignalName, item.Dto.SignalName)
.Set(a => a.DefautValue, item.Dto.DefautValue)
.Set(a => a.LogicRuleId, item.Dto.LogicRuleId)
.ExecuteAffrows();
}
else
{
_freeSql.Insert<CanLinRWConfig>(new CanLinRWConfig
{
CanLinConfigProId = _canLinConfigProId,
RWInfo = item.Rw,
Name = item.Dto.Name,
MsgFrameName = item.Dto.MsgFrameName,
SignalName = item.Dto.SignalName,
DefautValue = item.Dto.DefautValue,
LogicRuleId = item.Dto.LogicRuleId,
}).ExecuteAffrows();
}
}
}
private static void NormalizeRwConfigs(IEnumerable<CanLinRWConfigDto> list, RW rw)
{
foreach (var item in list)
{
item.RWInfo = rw;
if (string.IsNullOrWhiteSpace(item.SignalName))
{
continue;
}
if (string.IsNullOrWhiteSpace(item.Name))
{
item.Name = item.SignalName;
}
if (string.IsNullOrWhiteSpace(item.MsgFrameName))
{
item.MsgFrameName = string.Empty;
}
if (string.IsNullOrWhiteSpace(item.DefautValue))
{
item.DefautValue = "0";
}
if (item.LogicRuleId < 0)
{
item.LogicRuleId = 0;
}
}
}
private static void EnsureNoDuplicateSignal(IEnumerable<CanLinRWConfigDto> list, RW rw)
{
var duplicates = list
.Where(a => !string.IsNullOrWhiteSpace(a.SignalName))
.GroupBy(a => a.SignalName!, StringComparer.Ordinal)
.Where(g => g.Count() > 1)
.Select(g => g.Key)
.ToList();
if (duplicates.Count > 0)
{
throw new InvalidOperationException($"{rw} 配置中存在重复信号:{string.Join(",", duplicates)}");
}
}
private void EnsureNoWriteReadConflict()
{
var writeSet = new HashSet<string>(
WriteConfigs
.Where(a => a.RWInfo == RW.Write)
.Select(a => a.SignalName)
.Where(a => !string.IsNullOrWhiteSpace(a))
.Select(a => a!),
StringComparer.Ordinal);
var readSet = new HashSet<string>(
ReadConfigs
.Where(a => a.RWInfo == RW.Read)
.Select(a => a.SignalName)
.Where(a => !string.IsNullOrWhiteSpace(a))
.Select(a => a!),
StringComparer.Ordinal);
writeSet.IntersectWith(readSet);
if (writeSet.Count > 0)
{
throw new InvalidOperationException($"同一信号不允许同时配置为写入与读取,冲突信号:{string.Join(",", writeSet)}");
}
}
private static string BuildKey(RW rw, string signalName)
{
return $"{(int)rw}:{signalName}";
}
private readonly struct DesiredItem
{
/// <summary>
/// 读写类型。
/// </summary>
public RW Rw { get; }
/// <summary>
/// 信号名称。
/// </summary>
public string SignalName { get; }
/// <summary>
/// 原始 DTO。
/// </summary>
public CanLinRWConfigDto Dto { get; }
public DesiredItem(RW rw, string signalName, CanLinRWConfigDto dto)
{
Rw = rw;
SignalName = signalName;
Dto = dto;
}
}
/// <summary>
/// 右侧信号候选项。
/// </summary>
public class SignalCandidate
{
/// <summary>
/// 消息名称/帧名称。
/// </summary>
public string? MsgName { get; set; }
/// <summary>
/// 信号名称。
/// </summary>
public string? SignalName { get; set; }
/// <summary>
/// 配置名称(若解析层已有中文名则传入)。
/// </summary>
public string? Name { get; set; }
/// <summary>
/// 描述。
/// </summary>
public string? Desc { get; set; }
/// <summary>
/// 已添加标记。
/// 0=未添加1=已添加到写入2=已添加到读取3=同时存在于写入与读取。
/// </summary>
public int AddedInfo { get; set; }
}
/// <summary>
/// 帧节点。
/// </summary>
public class SignalFrameNode
{
/// <summary>
/// 帧名。
/// </summary>
public string FrameName { get; set; } = string.Empty;
/// <summary>
/// 帧内信号集合。
/// </summary>
public ObservableCollection<SignalCandidate> Signals { get; set; } = new ObservableCollection<SignalCandidate>();
}
}
}