using HMI.UI.Util; using Panuon.WPF.UI; using Plugin.DeviceConfig.Entity; using Plugin.DeviceConfig.Model; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Windows; using System.Windows.Controls; namespace Plugin.DeviceConfig { /// /// DeviceEditModal.xaml 的交互逻辑 /// public partial class DeviceEditModal : WindowX { private DeviceConfigModel _model; private DeviceDefinition _device; private ObservableCollection _channels = new(); private bool _isNew; private bool _execModeSyncing; public DeviceEditModal() { DataContext = this; InitializeComponent(); } /// /// 打开弹窗 /// public bool? Open(DeviceDefinition device, DeviceConfigModel model) { _model = model; _device = device; _isNew = string.IsNullOrEmpty(device.DeviceId); // 初始化枚举下拉 cbKind.ItemsSource = Enum.GetValues(typeof(DeviceKind)).Cast().ToList(); cbTransport.ItemsSource = Enum.GetValues(typeof(TransportType)).Cast().ToList(); cbTransport.SelectionChanged += CbTransport_SelectionChanged; // 执行位置:仲裁域/BrokerId 候选由配置枚举 cbAuthority.ItemsSource = V3AuthorityCatalog.AuthorityIds; cbBrokerId.ItemsSource = V3AuthorityCatalog.BrokerIds; tbOwnerProcessId.Text = System.Diagnostics.Process.GetCurrentProcess().Id.ToString(); // 填充数据 tbDeviceId.Text = device.DeviceId; tbDeviceId.IsReadOnly = !_isNew; cbKind.SelectedItem = device.Kind; tbDriverType.Text = device.DriverType; cbTransport.SelectedItem = device.Transport; tbEndpoint.Text = device.Endpoint; // 执行位置回显:旧数据 AuthorityId 为空时默认本机 Local 域 _execModeSyncing = true; rbLocal.IsChecked = device.ExecutionMode == ExecutionModeKind.Local; rbBroker.IsChecked = device.ExecutionMode == ExecutionModeKind.Broker; cbAuthority.SelectedItem = string.IsNullOrWhiteSpace(device.AuthorityId) ? V3AuthorityCatalog.LocalAuthorityId : device.AuthorityId; if (cbAuthority.SelectedItem == null) { // 存量值不在当前候选枚举中:保留展示并追加 var list = V3AuthorityCatalog.AuthorityIds.ToList(); list.Add(device.AuthorityId); cbAuthority.ItemsSource = list; cbAuthority.SelectedItem = device.AuthorityId; } cbBrokerId.SelectedItem = device.BrokerId; if (!string.IsNullOrWhiteSpace(device.OwnerProcessId)) { tbOwnerProcessId.Text = device.OwnerProcessId; } _execModeSyncing = false; RefreshExecModeUi(); RefreshSessionKeyCandidates(); // 默认驱动类型建议 & 端点提示 UpdateDriverHint(); UpdateEndpointHint(); // 加载通道 if (!_isNew && !string.IsNullOrEmpty(device.DeviceId)) { var chList = _model.GetChannelsByDevice(device.DeviceId); _channels = new ObservableCollection(chList); } else { _channels = new ObservableCollection(); } channelListView.ItemsSource = _channels; // 通道 SessionKey 已加载,重新生成会话键候选(Broker 模式) RefreshSessionKeyCandidates(); return ShowDialog(); } #region 通道操作 private void AddChannel_Click(object sender, RoutedEventArgs e) { try { if (!int.TryParse(tbChId.Text, out int chId) || chId < 0) { ToastHelper.Warning("请输入有效的通道编号(非负整数)"); return; } if (_channels.Any(c => c.ChannelId == chId)) { ToastHelper.Warning($"通道 {chId} 已存在"); return; } double.TryParse(tbChVolt.Text, out double volt); double.TryParse(tbChCurr.Text, out double curr); var ch = new DeviceChannel { DeviceId = _device.DeviceId ?? tbDeviceId.Text, ChannelId = chId, Capability = tbChCap.Text ?? string.Empty, MaxVoltage = volt, MaxCurrent = curr, }; _channels.Add(ch); ClearChannelInputs(); } catch (Exception ex) { ToastHelper.Error($"新增通道失败: {ex.Message}"); } } private void EditChannel_Click(object sender, RoutedEventArgs e) { try { if (sender is Button btn && btn.Tag is DeviceChannel ch) { tbChId.Text = ch.ChannelId.ToString(); tbChCap.Text = ch.Capability; tbChVolt.Text = ch.MaxVoltage.ToString("F2"); tbChCurr.Text = ch.MaxCurrent.ToString("F2"); } } catch (Exception ex) { ToastHelper.Error($"加载通道数据失败: {ex.Message}"); } } private void DeleteChannel_Click(object sender, RoutedEventArgs e) { try { if (sender is Button btn && btn.Tag is DeviceChannel ch) { if (MsgBoxHelper.WarnMessage(this, $"确定要删除通道 {ch.ChannelId} 吗?") == MessageBoxResult.OK) { _channels.Remove(ch); } } } catch (Exception ex) { ToastHelper.Error($"删除通道失败: {ex.Message}"); } } private void SaveChannel_Click(object sender, RoutedEventArgs e) { try { if (!int.TryParse(tbChId.Text, out int chId) || chId < 0) { ToastHelper.Warning("请输入有效的通道编号"); return; } double.TryParse(tbChVolt.Text, out double volt); double.TryParse(tbChCurr.Text, out double curr); var existing = _channels.FirstOrDefault(c => c.ChannelId == chId); if (existing != null) { existing.Capability = tbChCap.Text ?? string.Empty; existing.MaxVoltage = volt; existing.MaxCurrent = curr; } else { _channels.Add(new DeviceChannel { DeviceId = _device.DeviceId ?? tbDeviceId.Text, ChannelId = chId, Capability = tbChCap.Text ?? string.Empty, MaxVoltage = volt, MaxCurrent = curr, }); } ClearChannelInputs(); ToastHelper.Success("通道已保存"); } catch (Exception ex) { ToastHelper.Error($"保存通道失败: {ex.Message}"); } } private void DelChannel_Click(object sender, RoutedEventArgs e) { try { var selected = channelListView.SelectedItem as DeviceChannel; if (selected == null) { ToastHelper.Warning("请先选中要删除的通道"); return; } if (MsgBoxHelper.WarnMessage(this, $"确定要删除通道 {selected.ChannelId} 吗?") == MessageBoxResult.OK) { _channels.Remove(selected); } } catch (Exception ex) { ToastHelper.Error($"删除通道失败: {ex.Message}"); } } private void ClearChannelInputs() { tbChId.Text = string.Empty; tbChCap.Text = string.Empty; tbChVolt.Text = string.Empty; tbChCurr.Text = string.Empty; } #endregion #region 执行位置(V3.0 §05) /// 当前是否 Broker 模式 private bool IsBrokerMode => rbBroker.IsChecked == true; private void ExecMode_Changed(object sender, RoutedEventArgs e) { // XAML 初始化阶段 rbLocal.IsChecked=True 会提前触发,此时其余控件字段尚未创建 if (_execModeSyncing || rbBroker == null || cbAuthority == null) return; _execModeSyncing = true; // Broker 设备的 AuthorityId 即其 Broker 域;切回 Local 时使用本机默认域 if (IsBrokerMode) { // 当前仍为 local 域(或未选)→ 切到首个 Broker 域 if (V3AuthorityCatalog.IsLocalAuthority(cbAuthority.SelectedItem as string) || cbAuthority.SelectedItem == null) { cbAuthority.SelectedItem = V3AuthorityCatalog.BrokerIds.FirstOrDefault() ?? V3AuthorityCatalog.AuthorityIds.LastOrDefault(a => !V3AuthorityCatalog.IsLocalAuthority(a)); } } else { cbAuthority.SelectedItem = V3AuthorityCatalog.LocalAuthorityId; } _execModeSyncing = false; RefreshExecModeUi(); } private void CbAuthority_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (_execModeSyncing || rbLocal == null || rbBroker == null || cbBrokerId == null) return; var auth = cbAuthority.SelectedItem as string; if (string.IsNullOrEmpty(auth)) return; _execModeSyncing = true; // 选中 local- 域→Local;选中 Broker 域→Broker 并同步 BrokerId if (V3AuthorityCatalog.IsLocalAuthority(auth)) { rbLocal.IsChecked = true; } else { rbBroker.IsChecked = true; cbBrokerId.SelectedItem = V3AuthorityCatalog.BrokerIds.Contains(auth) ? auth : cbBrokerId.SelectedItem; } _execModeSyncing = false; RefreshExecModeUi(); } /// 根据模式切换 Broker 字段可见性与只读派生的访问策略 private void RefreshExecModeUi() { spBroker.Visibility = IsBrokerMode ? Visibility.Visible : Visibility.Collapsed; tbAccessPolicy.Text = (IsBrokerMode ? ExecutionModeKind.Broker : ExecutionModeKind.Local) switch { ExecutionModeKind.Broker => "共享仲裁 · Broker SessionGate 代理", _ => "独占直连 · 本进程持有物理会话" }; if (IsBrokerMode) { RefreshSessionKeyCandidates(); } } /// /// PhysicalSessionKey 候选:由端点规范化的写死字符串(V3.0 §04:地址别名必须归一为同一物理身份) /// private void RefreshSessionKeyCandidates() { var deviceId = tbDeviceId.Text?.Trim() ?? string.Empty; var transport = cbTransport.SelectedItem is TransportType tt ? tt : TransportType.Tcp; var endpoint = tbEndpoint.Text?.Trim() ?? string.Empty; var candidates = new List(); if (transport == TransportType.Can) { var bus = endpoint.Split(':')[0].Trim(); if (!string.IsNullOrEmpty(bus)) candidates.Add($"can/{bus}/main"); } else if (transport == TransportType.Tcp && endpoint.Contains(":")) { candidates.Add($"tcp/{endpoint}/main"); } else if (transport == TransportType.Serial) { var com = endpoint.Split(':')[0].Trim(); if (!string.IsNullOrEmpty(com)) candidates.Add($"serial/{com}/main"); } else if (transport == TransportType.Visa) { candidates.Add($"visa/{deviceId}/main"); } else if (transport == TransportType.SDK) { candidates.Add($"sdk/{deviceId}/main"); } if (!string.IsNullOrEmpty(deviceId)) candidates.Add($"{deviceId}/main"); // 已存通道 SessionKey 也列入候选(去重) foreach (var key in _channels.Select(c => c.SessionKey) .Where(k => !string.IsNullOrWhiteSpace(k)).Distinct()) { candidates.Add(key); } candidates = candidates.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); string? current = _device?.PhysicalSessionKey; cbSessionKey.ItemsSource = candidates; cbSessionKey.Text = !string.IsNullOrWhiteSpace(current) ? current : (candidates.Count > 0 ? candidates[0] : string.Empty); } #endregion #region 保存/取消 private void SaveBtn_Click(object sender, RoutedEventArgs e) { try { // 验证 var deviceId = tbDeviceId.Text?.Trim(); if (string.IsNullOrEmpty(deviceId)) { ToastHelper.Warning("设备ID不能为空"); return; } if (cbKind.SelectedItem is not DeviceKind kind) { ToastHelper.Warning("请选择设备类型"); return; } if (cbTransport.SelectedItem is not TransportType transport) { ToastHelper.Warning("请选择传输方式"); return; } var endpoint = tbEndpoint.Text?.Trim(); if (string.IsNullOrEmpty(endpoint)) { ToastHelper.Warning("连接端点不能为空"); return; } // 验证 Endpoint 格式 if (transport == TransportType.Tcp) { if (!endpoint.Contains(":") && !endpoint.Contains(".")) { ToastHelper.Warning("TCP端点格式不正确,应为 IP:Port 格式"); return; } } else if (transport == TransportType.Can) { // CAN 端点格式:总线名称:节点编号 如 "CAN_BUS_1:0" var parts = endpoint.Split(':'); if (parts.Length != 2 || string.IsNullOrWhiteSpace(parts[0]) || !int.TryParse(parts[1], out int nodeIdx) || nodeIdx < 0) { ToastHelper.Warning("CAN端点格式不正确,应为 总线名称:节点编号(如 CAN_BUS_1:0)"); return; } } // 检查 DeviceId 是否重复(新增时) if (_isNew) { var existing = _model.GetDeviceById(deviceId); if (existing != null) { ToastHelper.Warning($"设备ID [{deviceId}] 已存在"); return; } } // 执行位置(V3.0 §05)校验 var authorityId = cbAuthority.SelectedItem as string ?? string.Empty; if (string.IsNullOrWhiteSpace(authorityId)) { ToastHelper.Warning("仲裁域不能为空,请从候选中选择"); return; } string brokerId = string.Empty, sessionKey = string.Empty, ownerPid = string.Empty; if (IsBrokerMode) { brokerId = cbBrokerId.SelectedItem as string ?? string.Empty; if (string.IsNullOrWhiteSpace(brokerId)) { ToastHelper.Warning("共享仪器(Broker)必须指定 BrokerId"); return; } sessionKey = cbSessionKey.Text?.Trim() ?? string.Empty; if (string.IsNullOrWhiteSpace(sessionKey)) { ToastHelper.Warning("共享仪器(Broker)必须指定物理会话键"); return; } ownerPid = tbOwnerProcessId.Text?.Trim() ?? string.Empty; } // 保存设备 _device.DeviceId = deviceId; _device.Kind = kind; _device.DriverType = tbDriverType.Text?.Trim() ?? string.Empty; _device.Transport = transport; _device.Endpoint = endpoint; _device.ExecutionMode = IsBrokerMode ? ExecutionModeKind.Broker : ExecutionModeKind.Local; _device.AuthorityId = authorityId; _device.BrokerId = brokerId; _device.PhysicalSessionKey = sessionKey; _device.OwnerProcessId = ownerPid; _model.SaveDevice(_device); // 保存通道 foreach (var ch in _channels) { ch.DeviceId = deviceId; _model.SaveChannel(ch); } DialogResult = true; Close(); } catch (Exception ex) { ToastHelper.Error($"保存失败: {ex.Message}"); } } private void CancelBtn_Click(object sender, RoutedEventArgs e) { DialogResult = false; Close(); } private void CbTransport_SelectionChanged(object sender, SelectionChangedEventArgs e) { UpdateEndpointHint(); if (IsBrokerMode) { RefreshSessionKeyCandidates(); } } /// /// 根据传输方式更新端点输入框提示 /// private void UpdateEndpointHint() { if (cbTransport.SelectedItem is TransportType tt) { switch (tt) { case TransportType.Can: Panuon.WPF.UI.TextBoxHelper.SetWatermark(tbEndpoint, "CAN总线端点,格式: 总线名称:节点编号(如 CAN_BUS_1:0)"); break; case TransportType.Serial: Panuon.WPF.UI.TextBoxHelper.SetWatermark(tbEndpoint, "串口号,如 COM3:115200:8:N:1"); break; case TransportType.Visa: Panuon.WPF.UI.TextBoxHelper.SetWatermark(tbEndpoint, "VISG 地址,如 TCPIP0::192.168.1.100::inst0::INSTR"); break; case TransportType.SDK: Panuon.WPF.UI.TextBoxHelper.SetWatermark(tbEndpoint, "SDK 设备标识,如 SDK://DeviceName/Instance"); break; default: Panuon.WPF.UI.TextBoxHelper.SetWatermark(tbEndpoint, "例: 192.168.10.124:30000 或 COM3"); break; } } } /// /// 根据设备类型建议默认驱动类型 /// private void UpdateDriverHint() { // 保留现有驱动类型,不自动修改。仅当为空时提示默认值 if (string.IsNullOrWhiteSpace(tbDriverType.Text) && cbKind.SelectedItem is DeviceKind kind) { string hint = kind switch { DeviceKind.CanDevice => "CAN", DeviceKind.PowerSupply => "SCPI", DeviceKind.PlcDevice => "S7", DeviceKind.Chamber => "Tcp", DeviceKind.DAQ => "Tcp", _ => "" }; if (!string.IsNullOrEmpty(hint)) { tbDriverType.Text = hint; } } } #endregion } }