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
|
{
|
/// <summary>
|
/// DeviceEditModal.xaml 的交互逻辑
|
/// </summary>
|
public partial class DeviceEditModal : WindowX
|
{
|
private DeviceConfigModel _model;
|
private DeviceDefinition _device;
|
private ObservableCollection<DeviceChannel> _channels = new();
|
private bool _isNew;
|
private bool _execModeSyncing;
|
|
public DeviceEditModal()
|
{
|
DataContext = this;
|
InitializeComponent();
|
}
|
|
/// <summary>
|
/// 打开弹窗
|
/// </summary>
|
public bool? Open(DeviceDefinition device, DeviceConfigModel model)
|
{
|
_model = model;
|
_device = device;
|
_isNew = string.IsNullOrEmpty(device.DeviceId);
|
|
// 初始化枚举下拉
|
cbKind.ItemsSource = Enum.GetValues(typeof(DeviceKind)).Cast<DeviceKind>().ToList();
|
cbTransport.ItemsSource = Enum.GetValues(typeof(TransportType)).Cast<TransportType>().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<DeviceChannel>(chList);
|
}
|
else
|
{
|
_channels = new ObservableCollection<DeviceChannel>();
|
}
|
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)
|
|
/// <summary>当前是否 Broker 模式</summary>
|
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();
|
}
|
|
/// <summary>根据模式切换 Broker 字段可见性与只读派生的访问策略</summary>
|
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();
|
}
|
}
|
|
/// <summary>
|
/// PhysicalSessionKey 候选:由端点规范化的写死字符串(V3.0 §04:地址别名必须归一为同一物理身份)
|
/// </summary>
|
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<string>();
|
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();
|
}
|
}
|
|
/// <summary>
|
/// 根据传输方式更新端点输入框提示
|
/// </summary>
|
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;
|
}
|
}
|
}
|
|
/// <summary>
|
/// 根据设备类型建议默认驱动类型
|
/// </summary>
|
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
|
}
|
}
|