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 { /// /// ZLG CAN/LIN 读写配置三栏管理弹窗 ViewModel。 /// 左侧:写入/读取配置;右侧:信号全集候选池;统一保存落库。 /// public class DialogZlgCanLinRwConfigViewModel : DialogViewModel { private readonly IFreeSql _freeSql; private readonly ILogService _logService; private readonly LogicRuleService _logicRuleService; private long _canLinConfigProId; private bool _enableHardwareCycleSchedule = true; private bool _useCanFdSchedule; /// /// 是否启用“加入定时调度表(ZLG auto_send)”能力。 /// 说明:该能力仅适用于 ZLG CAN/CANFD 硬件 auto_send;LIN 当前使用软件调度,不允许写入 CANScheduleConfig。 /// public bool EnableHardwareCycleSchedule { get { return _enableHardwareCycleSchedule; } private set { _enableHardwareCycleSchedule = value; RaisePropertyChanged(); RaisePropertyChanged(nameof(CanAddCycleTimeSch)); } } /// /// 是否允许执行“加入定时调度表”操作。 /// 说明: /// - 需要 打开(调用方允许硬件调度表能力); /// - 且弹窗处于可编辑态()。 /// public bool CanAddCycleTimeSch { get { return EnableHardwareCycleSchedule && IsEditable; } } /// /// 是否使用 CANFD 调度表(决定“加入定时调度表”写入 还是 )。 /// public bool UseCanFdSchedule { get { return _useCanFdSchedule; } private set { _useCanFdSchedule = value; RaisePropertyChanged(); } } /// /// 构造函数。 /// /// FreeSql。 /// 日志。 /// 逻辑规则服务。 public DialogZlgCanLinRwConfigViewModel(IFreeSql freeSql, ILogService logService, LogicRuleService logicRuleService) { _freeSql = freeSql; _logService = logService; _logicRuleService = logicRuleService; Title = "读写设置"; WriteConfigs = new ObservableCollection(); ReadConfigs = new ObservableCollection(); SignalCandidates = new ObservableCollection(); SignalTree = new ObservableCollection(); SignalCandidatesView = CollectionViewSource.GetDefaultView(SignalCandidates); SignalCandidatesView.Filter = FilterSignalCandidate; WriteNameCbxItems = new ObservableCollection() { 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() { 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; } /// /// 是否允许编辑(由调用方根据 Active/打开状态决定)。 /// public bool IsEditable { get; private set; } /// /// 逻辑规则集合(下拉框 ItemsSource)。 /// public IReadOnlyList LogicRuleDtos => _logicRuleService.LogicRuleDtos; /// /// 写入配置“名称”下拉框集合(参考 CANConfigViewModel)。 /// public ObservableCollection WriteNameCbxItems { get; private set; } /// /// 读取配置“名称”下拉框集合(参考 CANConfigViewModel)。 /// public ObservableCollection ReadNameCbxItems { get; private set; } /// /// 写入配置集合。 /// public ObservableCollection WriteConfigs { get; private set; } /// /// 读取配置集合。 /// public ObservableCollection ReadConfigs { get; private set; } /// /// 信号候选集合(右侧池)。 /// public ObservableCollection SignalCandidates { get; private set; } /// /// 信号树(按帧分组)。 /// public ObservableCollection SignalTree { get; private set; } /// /// 候选信号视图(含过滤)。 /// public ICollectionView SignalCandidatesView { get; private set; } private string? _signalFilterText; /// /// 信号过滤文本(按 MsgName/SignalName/Name/Desc 匹配)。 /// public string? SignalFilterText { get { return _signalFilterText; } set { _signalFilterText = value; RaisePropertyChanged(); SignalCandidatesView.Refresh(); RebuildSignalTree(); } } /// /// 当前选中的候选信号。 /// public SignalCandidate? SelectedSignalCandidate { get; set; } /// /// 当前选中的写入配置行。 /// public CanLinRWConfigDto? SelectedWriteConfig { get; set; } private DelegateCommand? _addCycleTimeSch; /// /// 将当前选中的写入配置对应的报文加入“硬件定时调度表”(ZLG auto_send)。 /// /// /// 说明: /// - 调度表按“报文/帧(MsgFrameName)”维度配置;同一报文可包含多个写入信号,但调度表只需要一条。 /// - 该命令会直接落库到 ,随后由主界面刷新配置程序时加载显示。 /// public DelegateCommand AddCycleTimeSch => _addCycleTimeSch ??= new DelegateCommand(AddCycleTimeSchMethod); /// /// 加入定时调度表命令处理。 /// private void AddCycleTimeSchMethod() { if (!EnableHardwareCycleSchedule) { MessageBox.Show("当前模式不支持加入硬件定时调度表(LIN 使用软件调度表)", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); return; } if (!IsEditable) { MessageBox.Show("当前状态禁止修改", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); return; } if (_canLinConfigProId <= 0) { MessageBox.Show("配置程序ID无效,无法加入调度表", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); return; } if (SelectedWriteConfig == null) { MessageBox.Show("请先选中写入配置中的一行", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); return; } if (string.IsNullOrWhiteSpace(SelectedWriteConfig.MsgFrameName)) { MessageBox.Show("写入配置的【消息名称】为空,无法加入调度表", "提示", MessageBoxButton.OK, MessageBoxImage.Hand); return; } var msgName = SelectedWriteConfig.MsgFrameName.Trim(); try { var exists = UseCanFdSchedule ? _freeSql.Select() .Where(a => a.CanLinConfigProId == _canLinConfigProId) .Where(a => a.MsgName == msgName) .Any() : _freeSql.Select() .Where(a => a.CanLinConfigProId == _canLinConfigProId) .Where(a => a.MsgName == msgName) .Any(); if (exists) { MessageBox.Show($"该报文已在定时调度表中:{msgName}", "提示", MessageBoxButton.OK, MessageBoxImage.Information); return; } if (UseCanFdSchedule) { _freeSql.Insert(new CANFdScheduleConfig { CanLinConfigProId = _canLinConfigProId, MsgName = msgName, Cycle = 100, OrderSend = 1, SchTabIndex = 0, }).ExecuteAffrows(); } else { _freeSql.Insert(new CANScheduleConfig { CanLinConfigProId = _canLinConfigProId, MsgName = msgName, Cycle = 100, OrderSend = 1, SchTabIndex = 0, }).ExecuteAffrows(); } MessageBox.Show($"已加入定时调度表:{msgName}(默认周期 100ms,可在调度表中修改)", "提示", MessageBoxButton.OK, MessageBoxImage.Information); } catch (Exception ex) { _logService.Error($"加入定时调度表失败:{msgName},{ex}"); MessageBox.Show(ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error); } } /// /// 当前选中的读取配置行。 /// public CanLinRWConfigDto? SelectedReadConfig { get; set; } private DelegateCommand? _signalTreeSelectionChangedCmd; /// /// 右侧信号树选中变化(仅当选中叶子节点时回写 SelectedSignalCandidate)。 /// public DelegateCommand SignalTreeSelectionChangedCmd => _signalTreeSelectionChangedCmd ??= new DelegateCommand(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; /// /// 将右侧选中信号添加到写入配置。 /// 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; /// /// 将右侧选中信号添加到读取配置。 /// 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; /// /// 从写入配置移除当前选中行。 /// 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; /// /// 从读取配置移除当前选中行。 /// 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; /// /// 保存并落库。 /// 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; /// /// 取消。 /// public DelegateCommand CancelCmd => _cancelCmd ??= new DelegateCommand(CancelCmdMethod); private void CancelCmdMethod() { RaiseRequestClose(new DialogResult(ButtonResult.Cancel)); } /// /// 弹窗打开时接收参数。 /// /// 参数。 public override void OnDialogOpened(IDialogParameters parameters) { _canLinConfigProId = parameters.GetValue("CanLinConfigProId"); EnableHardwareCycleSchedule = parameters.ContainsKey("EnableHardwareCycleSchedule") ? parameters.GetValue("EnableHardwareCycleSchedule") : true; // 兼容参数命名:调用方传入的 key 为 "IsCanFdSchedule"。 UseCanFdSchedule = parameters.ContainsKey("IsCanFdSchedule") ? parameters.GetValue("IsCanFdSchedule") : false; IsEditable = parameters.ContainsKey("IsEditable") ? parameters.GetValue("IsEditable") : true; RaisePropertyChanged(nameof(IsEditable)); RaisePropertyChanged(nameof(CanAddCycleTimeSch)); if (parameters.ContainsKey("WriteConfigs")) { var list = parameters.GetValue>("WriteConfigs") ?? new ObservableCollection(); WriteConfigs = list; RaisePropertyChanged(nameof(WriteConfigs)); } if (parameters.ContainsKey("ReadConfigs")) { var list = parameters.GetValue>("ReadConfigs") ?? new ObservableCollection(); ReadConfigs = list; RaisePropertyChanged(nameof(ReadConfigs)); } if (parameters.ContainsKey("SignalCandidates")) { var list = parameters.GetValue>("SignalCandidates") ?? new ObservableCollection(); SignalCandidates = list; RaisePropertyChanged(nameof(SignalCandidates)); SignalCandidatesView = CollectionViewSource.GetDefaultView(SignalCandidates); SignalCandidatesView.Filter = FilterSignalCandidate; RaisePropertyChanged(nameof(SignalCandidatesView)); } RebuildSignalTree(); if (parameters.ContainsKey("Title")) { Title = parameters.GetValue("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(signalNodes) }; SignalTree.Add(node); } RaisePropertyChanged(nameof(SignalTree)); } /// /// 计算候选信号是否已被添加到写入/读取。 /// 0=未添加,1=已添加到写入,2=已添加到读取,3=同时存在于写入与读取。 /// /// 候选信号。 /// AddedInfo 标志值。 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() .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(); // 以 (RWInfo + SignalName) 作为“业务主键”对齐数据库: // - desiredKeySet:本次保存期望存在的 key 集合 // - existingByKey:当前数据库已存在的 key -> 实体 var desiredKeySet = new HashSet(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)) { // 保持与 UI 一致:用户在弹窗中移除的项,需要同步删除数据库记录。 _freeSql.Delete(old.Id).ExecuteAffrows(); } } // Upsert:按 key(RWInfo + SignalName)更新或插入 foreach (var item in desiredAll) { var key = BuildKey(item.Rw, item.SignalName); if (existingByKey.TryGetValue(key, out var old)) { _freeSql.Update(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(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 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 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( 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( 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 { /// /// 读写类型。 /// public RW Rw { get; } /// /// 信号名称。 /// public string SignalName { get; } /// /// 原始 DTO。 /// public CanLinRWConfigDto Dto { get; } public DesiredItem(RW rw, string signalName, CanLinRWConfigDto dto) { Rw = rw; SignalName = signalName; Dto = dto; } } /// /// 右侧信号候选项。 /// public class SignalCandidate { /// /// 消息名称/帧名称。 /// public string? MsgName { get; set; } /// /// 信号名称。 /// public string? SignalName { get; set; } /// /// 配置名称(若解析层已有中文名则传入)。 /// public string? Name { get; set; } /// /// 描述。 /// public string? Desc { get; set; } /// /// 已添加标记。 /// 0=未添加,1=已添加到写入,2=已添加到读取,3=同时存在于写入与读取。 /// public int AddedInfo { get; set; } } /// /// 帧节点。 /// public class SignalFrameNode { /// /// 帧名。 /// public string FrameName { get; set; } = string.Empty; /// /// 帧内信号集合。 /// public ObservableCollection Signals { get; set; } = new ObservableCollection(); } } }