Compare commits
11 Commits
db8baed9c7
...
c0a24af4cb
| Author | SHA1 | Date | |
|---|---|---|---|
| c0a24af4cb | |||
| c4ec348217 | |||
| 74d544c288 | |||
| 2dfc819bcc | |||
| 0ed080f63c | |||
| 8e44be4894 | |||
| aebbd88e23 | |||
| 2fe307c142 | |||
| 3318c720b7 | |||
| b6b767cf0a | |||
| 9171e0b0c1 |
@@ -1,4 +1,5 @@
|
|||||||
|
|
||||||
|
using DeviceCommand.Device;
|
||||||
using Logger;
|
using Logger;
|
||||||
using MaterialDesignThemes.Wpf;
|
using MaterialDesignThemes.Wpf;
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
@@ -152,6 +153,7 @@ namespace ADP.ViewModels
|
|||||||
public ICommand SelectCANMessageCommand { get; set; }
|
public ICommand SelectCANMessageCommand { get; set; }
|
||||||
public ICommand MonitorValueSettingCommand { get; set; }
|
public ICommand MonitorValueSettingCommand { get; set; }
|
||||||
public ICommand GetFileStringCommand { get; set; }
|
public ICommand GetFileStringCommand { get; set; }
|
||||||
|
public ICommand SilenceBuzzerCommand { get; set; }
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public ShellViewModel(IContainerProvider containerProvider)
|
public ShellViewModel(IContainerProvider containerProvider)
|
||||||
@@ -185,6 +187,7 @@ namespace ADP.ViewModels
|
|||||||
SelectCANMessageCommand = new DelegateCommand(SelectCANMessage);
|
SelectCANMessageCommand = new DelegateCommand(SelectCANMessage);
|
||||||
MonitorValueSettingCommand = new DelegateCommand(MonitorValueSetting);
|
MonitorValueSettingCommand = new DelegateCommand(MonitorValueSetting);
|
||||||
GetFileStringCommand = new DelegateCommand(GetFileString);
|
GetFileStringCommand = new DelegateCommand(GetFileString);
|
||||||
|
SilenceBuzzerCommand = new AsyncDelegateCommand(SilenceBuzzer);
|
||||||
|
|
||||||
_globalInfo.ContextDic.Add("default", new ScopedContext());
|
_globalInfo.ContextDic.Add("default", new ScopedContext());
|
||||||
|
|
||||||
@@ -218,6 +221,12 @@ namespace ADP.ViewModels
|
|||||||
}
|
}
|
||||||
|
|
||||||
#region 命令处理与事件
|
#region 命令处理与事件
|
||||||
|
|
||||||
|
private async Task SilenceBuzzer()
|
||||||
|
{
|
||||||
|
_eventAggregator.GetEvent<SilenceBuzzerEvent>().Publish(_globalInfo.CurrentScope);
|
||||||
|
|
||||||
|
}
|
||||||
private void GetFileString()
|
private void GetFileString()
|
||||||
{
|
{
|
||||||
var openFileDialog = new OpenFileDialog
|
var openFileDialog = new OpenFileDialog
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ Command="{Binding GetFileStringCommand}">
|
|||||||
</MenuItem.Icon>
|
</MenuItem.Icon>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<!-- 蜂鸣器消音 -->
|
<!-- 弹窗管理器 -->
|
||||||
<MenuItem Header="弹窗管理器"
|
<MenuItem Header="弹窗管理器"
|
||||||
FontSize="14"
|
FontSize="14"
|
||||||
Height="50"
|
Height="50"
|
||||||
|
|||||||
164
BenchMovementModule/HardwareDrive/GantryControlBase.cs
Normal file
164
BenchMovementModule/HardwareDrive/GantryControlBase.cs
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
using DeviceCommand.Base;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BenchMovementModule.HardwareDrive
|
||||||
|
{
|
||||||
|
public abstract class GantryControlBase
|
||||||
|
{
|
||||||
|
protected readonly IModbusDevice _device;
|
||||||
|
protected readonly byte _slaveAddress;
|
||||||
|
|
||||||
|
// 默认 PLC Modbus 映射地址
|
||||||
|
protected const ushort ADDR_HM_X_ENABLE = 0xC100; // HM0: X轴使能
|
||||||
|
protected const ushort ADDR_HM_Y_ENABLE = 0xC101; // HM1: Y轴使能
|
||||||
|
protected const ushort ADDR_HM_Z_ENABLE = 0xC102; // HM2: Z轴使能
|
||||||
|
|
||||||
|
protected const ushort ADDR_M_X_START = 30; // M30: X轴指定位置启动
|
||||||
|
protected const ushort ADDR_M_Y_START = 31; // M31: Y轴指定位置启动
|
||||||
|
protected const ushort ADDR_M_Z_START = 35; // M35: Z轴指定位置启动
|
||||||
|
|
||||||
|
protected const ushort ADDR_M_ALL_STOP = 40; // M40: 全停
|
||||||
|
protected const ushort ADDR_M_X_STOP = 41; // M41: X轴停止
|
||||||
|
protected const ushort ADDR_M_Y_STOP = 42; // M42: Y轴停止
|
||||||
|
protected const ushort ADDR_M_Z_STOP = 43; // M43: Z轴停止
|
||||||
|
|
||||||
|
protected const ushort ADDR_M_HOME = 100; // M100: 回原点触发
|
||||||
|
|
||||||
|
protected const ushort ADDR_HD_X_SPEED = 0xA166; // HD230: X轴速度 (32位)
|
||||||
|
protected const ushort ADDR_HD_Y_SPEED = 0xA17A; // HD250: Y轴速度 (32位)
|
||||||
|
protected const ushort ADDR_HD_Z_SPEED = 0xA170; // HD240: Z轴速度 (32位)
|
||||||
|
|
||||||
|
protected const ushort ADDR_HD_X_TARGET = 0xA10C; // HD140: X轴指定位置 (32位)
|
||||||
|
protected const ushort ADDR_HD_Y_TARGET = 0xA120; // HD160: Y轴指定位置 (32位)
|
||||||
|
protected const ushort ADDR_HD_Z_TARGET = 0xA116; // HD150: Z轴指定位置 (32位)
|
||||||
|
|
||||||
|
public bool IsConnected => _device?.IsConnected ?? false;
|
||||||
|
|
||||||
|
protected GantryControlBase(IModbusDevice device, byte slaveAddress = 1)
|
||||||
|
{
|
||||||
|
_device = device ?? throw new ArgumentNullException(nameof(device));
|
||||||
|
_slaveAddress = slaveAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ConnectAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _device.ConnectAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Close()
|
||||||
|
{
|
||||||
|
_device.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 轴使能控制
|
||||||
|
/// </summary>
|
||||||
|
public async Task SetAxisEnableAsync(int axis, bool enable, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
ushort address = axis switch
|
||||||
|
{
|
||||||
|
1 => ADDR_HM_X_ENABLE, // X
|
||||||
|
2 => ADDR_HM_Y_ENABLE, // Y
|
||||||
|
3 => ADDR_HM_Z_ENABLE, // Z
|
||||||
|
_ => throw new ArgumentException("轴通道错误,仅支持 1(X), 2(Y), 3(Z)")
|
||||||
|
};
|
||||||
|
|
||||||
|
await _device.WriteSingleCoilAsync(_slaveAddress, address, enable, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 左右移动(X轴)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="position">目标绝对位置(脉冲数/单位值)</param>
|
||||||
|
/// <param name="speed">运行速度</param>
|
||||||
|
public async Task MoveLeftRightAsync(int position, int speed, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
// 1. 写入速度与目标位置(32位数据,写入连续的2个保持寄存器)
|
||||||
|
await _device.WriteMultipleRegistersAsync(_slaveAddress, ADDR_HD_X_SPEED, Int32ToUshorts(speed), ct);
|
||||||
|
await _device.WriteMultipleRegistersAsync(_slaveAddress, ADDR_HD_X_TARGET, Int32ToUshorts(position), ct);
|
||||||
|
|
||||||
|
// 2. 边缘触发启动信号 M30
|
||||||
|
await TriggerCoilAsync(ADDR_M_X_START, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 上下移动(Z轴)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="position">目标绝对位置</param>
|
||||||
|
/// <param name="speed">运行速度</param>
|
||||||
|
public async Task MoveUpDownAsync(int position, int speed, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
// 1. 写入速度与目标位置(32位数据)
|
||||||
|
await _device.WriteMultipleRegistersAsync(_slaveAddress, ADDR_HD_Z_SPEED, Int32ToUshorts(speed), ct);
|
||||||
|
await _device.WriteMultipleRegistersAsync(_slaveAddress, ADDR_HD_Z_TARGET, Int32ToUshorts(position), ct);
|
||||||
|
|
||||||
|
// 2. 边缘触发启动信号 M35
|
||||||
|
await TriggerCoilAsync(ADDR_M_Z_START, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 前后移动(Y轴 - 预留拓展)
|
||||||
|
/// </summary>
|
||||||
|
public async Task MoveFrontBackAsync(int position, int speed, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await _device.WriteMultipleRegistersAsync(_slaveAddress, ADDR_HD_Y_SPEED, Int32ToUshorts(speed), ct);
|
||||||
|
await _device.WriteMultipleRegistersAsync(_slaveAddress, ADDR_HD_Y_TARGET, Int32ToUshorts(position), ct);
|
||||||
|
await TriggerCoilAsync(ADDR_M_Y_START, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全局回零
|
||||||
|
/// </summary>
|
||||||
|
public async Task HomeAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await TriggerCoilAsync(ADDR_M_HOME, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 轴停止
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="axis">1:X轴, 2:Y轴, 3:Z轴, 0:全停</param>
|
||||||
|
public async Task StopAsync(int axis = 0, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
ushort address = axis switch
|
||||||
|
{
|
||||||
|
0 => ADDR_M_ALL_STOP,
|
||||||
|
1 => ADDR_M_X_STOP,
|
||||||
|
2 => ADDR_M_Y_STOP,
|
||||||
|
3 => ADDR_M_Z_STOP,
|
||||||
|
_ => ADDR_M_ALL_STOP
|
||||||
|
};
|
||||||
|
|
||||||
|
await TriggerCoilAsync(address, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region 内部工具方法
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 触发一个点动信号(置 1 后,延时 100ms 自动置 0)
|
||||||
|
/// </summary>
|
||||||
|
protected async Task TriggerCoilAsync(ushort address, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await _device.WriteSingleCoilAsync(_slaveAddress, address, true, ct);
|
||||||
|
await Task.Delay(100, ct); // 给予 PLC 扫描周期足够的响应时间
|
||||||
|
await _device.WriteSingleCoilAsync(_slaveAddress, address, false, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将 32 位整型数据转换为 Modbus 的 2 个 16 位无符号整数(高低字转换)
|
||||||
|
/// </summary>
|
||||||
|
protected ushort[] Int32ToUshorts(int value)
|
||||||
|
{
|
||||||
|
// 本处采用低字在前(CDAB),如果PLC高字在前(ABCD),可将返回数组顺序颠倒
|
||||||
|
ushort lowWord = (ushort)(value & 0xFFFF);
|
||||||
|
ushort highWord = (ushort)((value >> 16) & 0xFFFF);
|
||||||
|
return new ushort[] { lowWord, highWord };
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
28
BenchMovementModule/HardwareDrive/GantryControlRtu.cs
Normal file
28
BenchMovementModule/HardwareDrive/GantryControlRtu.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using DeviceCommand.Base;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO.Ports;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BenchMovementModule.HardwareDrive
|
||||||
|
{
|
||||||
|
public class GantryControlRtu : GantryControlBase
|
||||||
|
{
|
||||||
|
// 暴露出底层的 ModbusRtu 实例以便修改串口配置
|
||||||
|
public ModbusRtu RtuDevice => (ModbusRtu)_device;
|
||||||
|
|
||||||
|
public GantryControlRtu(string portName, int baudRate = 9600, byte slaveAddress = 1)
|
||||||
|
: base(new ModbusRtu(), slaveAddress)
|
||||||
|
{
|
||||||
|
RtuDevice.ConfigureDevice(portName, baudRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GantryControlRtu(string portName, int baudRate, int dataBits, StopBits stopBits, Parity parity, byte slaveAddress = 1)
|
||||||
|
: base(new ModbusRtu(), slaveAddress)
|
||||||
|
{
|
||||||
|
RtuDevice.ConfigureDevice(portName, baudRate, dataBits, stopBits, parity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
BenchMovementModule/HardwareDrive/GantryControlTcp.cs
Normal file
27
BenchMovementModule/HardwareDrive/GantryControlTcp.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using DeviceCommand.Base;
|
||||||
|
using Model.Models;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BenchMovementModule.HardwareDrive
|
||||||
|
{
|
||||||
|
public class GantryControlTcp : GantryControlBase
|
||||||
|
{
|
||||||
|
// 暴露出底层的 ModbusTcp 实例以便修改网络配置
|
||||||
|
public ModbusTcp TcpDevice => (ModbusTcp)_device;
|
||||||
|
|
||||||
|
public GantryControlTcp(TcpConfig config, byte slaveAddress = 1)
|
||||||
|
: base(new ModbusTcp(config), slaveAddress)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public GantryControlTcp(string ipAddress, int port = 502, byte slaveAddress = 1)
|
||||||
|
: base(new ModbusTcp(), slaveAddress)
|
||||||
|
{
|
||||||
|
TcpDevice.ConfigureDevice(ipAddress, port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -204,6 +204,7 @@ namespace CANModule.ViewModels
|
|||||||
{
|
{
|
||||||
CollectionID = Guid.NewGuid(),
|
CollectionID = Guid.NewGuid(),
|
||||||
Channel = MessageChannel,
|
Channel = MessageChannel,
|
||||||
|
MethodName = CANSignalBroadcaster.BuildMethodName((uint)decimalValue, SelectedSignal),
|
||||||
MessageName = ExtractMessageName(SelectedMessage),
|
MessageName = ExtractMessageName(SelectedMessage),
|
||||||
SignalName = SelectedSignal,
|
SignalName = SelectedSignal,
|
||||||
MessageID = decimalValue,
|
MessageID = decimalValue,
|
||||||
|
|||||||
@@ -452,7 +452,7 @@ namespace CANModule.ViewModels
|
|||||||
|
|
||||||
if (targetMsg != null)
|
if (targetMsg != null)
|
||||||
{
|
{
|
||||||
_canfd.设置报文((uint)MessageSendChannel, targetMsg.msg_id.ToString(), SelectedSignal, SValue, sendPeriod);
|
_canfd.设置报文((uint)MessageSendChannel, targetMsg.msg_id.ToString(), SelectedSignal, SValue, sendPeriod, IsDirectlySend);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -603,10 +603,10 @@ namespace CANModule.ViewModels
|
|||||||
private void OnDbcMessageDecoded(uint channel, ZDBC.DBCMessage msg)
|
private void OnDbcMessageDecoded(uint channel, ZDBC.DBCMessage msg)
|
||||||
{
|
{
|
||||||
// DBC 解码后的报文回调
|
// DBC 解码后的报文回调
|
||||||
Application.Current.Dispatcher.Invoke(() =>
|
Application.Current.Dispatcher.BeginInvoke(() =>
|
||||||
{
|
{
|
||||||
var msgName = System.Text.Encoding.Default.GetString(msg.strName).TrimEnd('\0');
|
var msgName = System.Text.Encoding.Default.GetString(msg.strName).TrimEnd('\0');
|
||||||
var find = CanMessageList.FirstOrDefault(s => s.报文ID == (int)msg.nID);
|
var find = CanMessageList.FirstOrDefault(s => s.报文ID == (int)msg.nID&& s.通道==channel);
|
||||||
if (find != null)
|
if (find != null)
|
||||||
{
|
{
|
||||||
find.通道 = (byte)channel;
|
find.通道 = (byte)channel;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using NModbus;
|
using Model.Models;
|
||||||
|
using NModbus;
|
||||||
using System;
|
using System;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
@@ -19,7 +20,11 @@ namespace DeviceCommand.Base
|
|||||||
public bool IsConnected => _tcpClient?.Connected ?? false;
|
public bool IsConnected => _tcpClient?.Connected ?? false;
|
||||||
|
|
||||||
protected readonly SemaphoreSlim _commLock = new(1, 1);
|
protected readonly SemaphoreSlim _commLock = new(1, 1);
|
||||||
|
public ModbusTcp(TcpConfig config) : this()
|
||||||
|
{
|
||||||
|
if (config == null) return;
|
||||||
|
ConfigureDevice(config.IPAddress, config.Port, config.SendTimeout, config.ReceiveTimeout);
|
||||||
|
}
|
||||||
public ModbusTcp()
|
public ModbusTcp()
|
||||||
{
|
{
|
||||||
_tcpClient = new TcpClient();
|
_tcpClient = new TcpClient();
|
||||||
|
|||||||
@@ -230,7 +230,89 @@ namespace DeviceCommand.Base
|
|||||||
_commLock.Release();
|
_commLock.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#region 扩展:读取所有可用的二进制网络字节 (无 SCPI 块头解析)
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 无锁核心方法:发送命令并一次性读取所有回传的二进制原始数据包(不进行 # 协议头解析,专用于读取纯文件流如 PNG)
|
||||||
|
/// </summary>
|
||||||
|
private async Task<byte[]> LoglessReadAllBytesAsync(string queryCommand, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (!IsConnected) throw new InvalidOperationException("TCP未连接。");
|
||||||
|
|
||||||
|
// 1. 发送查询命令(例如 :PRINt? PNG\n)
|
||||||
|
await LoglessSendAsync(Encoding.UTF8.GetBytes(queryCommand), ct);
|
||||||
|
|
||||||
|
NetworkStream stream = _tcpClient.GetStream();
|
||||||
|
|
||||||
|
// 2. 鼎阳示波器的截图数据大概在 100KB - 800KB 左右,使用动态内存流接收整个包
|
||||||
|
using (var ms = new MemoryStream())
|
||||||
|
{
|
||||||
|
byte[] buffer = new byte[8192]; // 8KB 缓冲区
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 先给设备短暂的反应时间,等待数据到达网络缓冲区
|
||||||
|
int delayCount = 0;
|
||||||
|
while (!_tcpClient.GetStream().DataAvailable && delayCount < 50)
|
||||||
|
{
|
||||||
|
await Task.Delay(10, ct);
|
||||||
|
delayCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 循环读取,直到网络流中没有更多数据
|
||||||
|
do
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
int read = ReceiveTimeout > 0
|
||||||
|
? await stream.ReadAsync(buffer, 0, buffer.Length, ct)
|
||||||
|
.WaitAsync(TimeSpan.FromMilliseconds(ReceiveTimeout), ct)
|
||||||
|
.ConfigureAwait(false)
|
||||||
|
: await stream.ReadAsync(buffer, 0, buffer.Length, ct).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (read == 0) break; // 远程流关闭
|
||||||
|
|
||||||
|
ms.Write(buffer, 0, read);
|
||||||
|
|
||||||
|
// 如果流里没有剩余数据了,退出读取(防止 ReadAsync 在没有数据时无限阻塞等待)
|
||||||
|
if (!stream.DataAvailable)
|
||||||
|
{
|
||||||
|
// 极短延时再确认一次,防止分包网络延迟引起的“假结束”
|
||||||
|
await Task.Delay(30, ct);
|
||||||
|
if (!stream.DataAvailable)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (true);
|
||||||
|
|
||||||
|
return ms.ToArray();
|
||||||
|
}
|
||||||
|
catch (TimeoutException ex)
|
||||||
|
{
|
||||||
|
await ResetConnectionAsync(ct);
|
||||||
|
throw new TimeoutException($"读取二进制大包超时(等待:{ReceiveTimeout} ms),链路已重置。", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 【公开方法】发送命令并读取设备回传的全部原始二进制字节数组(不带协议头解析,直接返回整个字节缓冲区)
|
||||||
|
/// </summary>
|
||||||
|
public async Task<byte[]> ReadAllBytesAsync(string queryCommand, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await _commLock.WaitAsync(ct);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await LoglessReadAllBytesAsync(queryCommand, ct);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_commLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_tcpClient?.Dispose();
|
_tcpClient?.Dispose();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Common.Attributes;
|
using Common.Attributes;
|
||||||
using DeviceCommand.Base;
|
using DeviceCommand.Base;
|
||||||
|
using Model.Models;
|
||||||
using NModbus;
|
using NModbus;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -13,10 +14,10 @@ namespace DeviceCommand.Device
|
|||||||
[ADPCommand]
|
[ADPCommand]
|
||||||
public class IOBoard : ModbusTcp
|
public class IOBoard : ModbusTcp
|
||||||
{
|
{
|
||||||
//只有两个,八台产品公用两两分组各用一个
|
|
||||||
public IOBoard(string Ip地址, int 端口, int 发送超时, int 接收超时)
|
public IOBoard(TcpConfig config) : base(config)
|
||||||
{
|
{
|
||||||
ConfigureDevice(Ip地址, 端口, 发送超时, 接收超时);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -229,26 +229,9 @@ namespace DeviceCommand.Device
|
|||||||
return await WriteReadAsync($"MEAS:VAP?{SCPIDelimiter}", SCPIDelimiter, ct);
|
return await WriteReadAsync($"MEAS:VAP?{SCPIDelimiter}", SCPIDelimiter, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 3.5.5 查询通道输出端子上测得的电池容量
|
|
||||||
/// </summary>
|
|
||||||
public virtual async Task<string> 查询测得电池容量(CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
// 修正:去除前缀冒号
|
|
||||||
return await WriteReadAsync($"MEAS:CAP?{SCPIDelimiter}", SCPIDelimiter, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.5.6 查询通道输出端子上测得的恒流输出时间长度 (单位: ms)
|
/// 3.5.5 查询通道输出端子上测得的输出时间长度
|
||||||
/// </summary>
|
|
||||||
public virtual async Task<string> 查询恒流输出时间长度(CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
// 修正:去除前缀冒号
|
|
||||||
return await WriteReadAsync($"MEAS:TIM:CC?{SCPIDelimiter}", SCPIDelimiter, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 3.5.7 查询通道输出端子上测得的输出时间长度
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task<string> 查询总输出时间长度(CancellationToken ct = default)
|
public virtual async Task<string> 查询总输出时间长度(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
@@ -286,7 +269,7 @@ namespace DeviceCommand.Device
|
|||||||
{
|
{
|
||||||
// 修正:去除前缀冒号
|
// 修正:去除前缀冒号
|
||||||
string 状态 = 开启 ? "ON" : "OFF";
|
string 状态 = 开启 ? "ON" : "OFF";
|
||||||
await SendAsync($"OUTP:TIM {状态}{SCPIDelimiter}", ct);
|
await SendAsync($"OUTP:TIME {状态}{SCPIDelimiter}", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -295,7 +278,7 @@ namespace DeviceCommand.Device
|
|||||||
public virtual async Task 设置定时器时间(double 秒数, CancellationToken ct = default)
|
public virtual async Task 设置定时器时间(double 秒数, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:去除前缀冒号
|
// 修正:去除前缀冒号
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, "OUTP:TIM:DATA {0:F1}{1}", 秒数, SCPIDelimiter);
|
string cmd = string.Format(CultureInfo.InvariantCulture, "OUTP:TIME:DATA {0:F1}{1}", 秒数, SCPIDelimiter);
|
||||||
await SendAsync(cmd, ct);
|
await SendAsync(cmd, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,7 +383,14 @@ namespace DeviceCommand.Device
|
|||||||
string cmd = string.Format(CultureInfo.InvariantCulture, "VOLT:LIM {0:F3}{1}", 电压, SCPIDelimiter);
|
string cmd = string.Format(CultureInfo.InvariantCulture, "VOLT:LIM {0:F3}{1}", 电压, SCPIDelimiter);
|
||||||
await SendAsync(cmd, ct);
|
await SendAsync(cmd, ct);
|
||||||
}
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// 3.7.2.9 清除电压上限限制(恢复为设备物理允许的最大电压值)
|
||||||
|
/// </summary>
|
||||||
|
public virtual async Task 清除电压上限限制(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
// 发送 MAXimum 设为最大值以达到“清除限制”的效果
|
||||||
|
await SendAsync($"VOLT:LIMIT MAX{SCPIDelimiter}", ct); // 对应手册 3.7.2.8
|
||||||
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.7.3.1 设置输出功率值 (W)
|
/// 3.7.3.1 设置输出功率值 (W)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using DeviceCommand.Base;
|
|||||||
using Model.Models;
|
using Model.Models;
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@@ -11,12 +12,11 @@ namespace DeviceCommand.Device
|
|||||||
[ADPCommand]
|
[ADPCommand]
|
||||||
public class SDS2000X_HD : Tcp
|
public class SDS2000X_HD : Tcp
|
||||||
{
|
{
|
||||||
// 示波器底层 Socket 字符串命令通常以换行符 \n 结束
|
// 示波器底层 Socket 字符串命令以换行符 \n 结束
|
||||||
private const string ScpiDelimiter = "\n";
|
private const string ScpiDelimiter = "\n";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 构造函数:传入 <see cref="TcpConfig"/> 一次性初始化示波器通信参数。
|
/// 构造函数:传入 <see cref="TcpConfig"/> 一次性初始化示波器通信参数。
|
||||||
/// 鼎阳示波器网口 Socket 默认端口通常为 5025,请在配置中设置。
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SDS2000X_HD(TcpConfig config) : base(config)
|
public SDS2000X_HD(TcpConfig config) : base(config)
|
||||||
{
|
{
|
||||||
@@ -49,8 +49,7 @@ namespace DeviceCommand.Device
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 【补充】查询先前操作是否完成。
|
/// 查询先前操作是否完成。
|
||||||
/// 在重置设备(*RST)或切换大物理量程后调用,返回 "1" 代表示波器继电器切换就绪,防止后续指令引发阻塞。
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task<bool> 检查操作完成_OPC(CancellationToken ct = default)
|
public virtual async Task<bool> 检查操作完成_OPC(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
@@ -63,65 +62,82 @@ namespace DeviceCommand.Device
|
|||||||
#region 2. 运行与捕获控制 (Run / Stop / Single)
|
#region 2. 运行与捕获控制 (Run / Stop / Single)
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 控制示波器开始捕获波形 (等同于按下前端面板的 Run 键)
|
/// 控制示波器开始捕获波形
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 启动捕获_RUN(CancellationToken ct = default)
|
public virtual async Task 启动捕获_RUN(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($"RUN{ScpiDelimiter}", ct);
|
await SendAsync($":TRIGger:RUN{ScpiDelimiter}", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 停止捕获波形 (等同于按下前端面板的 Stop 键)
|
/// 停止捕获波形
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 停止捕获_STOP(CancellationToken ct = default)
|
public virtual async Task 停止捕获_STOP(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($"STOP{ScpiDelimiter}", ct);
|
await SendAsync($"TRIGger:STOP{ScpiDelimiter}", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 强制示波器进入单次触发捕获模式 (常用于捕捉充电瞬间的过冲浪涌波形)
|
/// 强制示波器进入单次触发捕获模式
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 单次触发_SINGLE(CancellationToken ct = default)
|
public virtual async Task 单次触发_SINGLE(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($"SINGle{ScpiDelimiter}", ct);
|
await SendAsync($":TRIGger:MODE SINGle{ScpiDelimiter}", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 触发一次波形采样 (当触发源设为 Manual 时使用)
|
/// 触发一次波形采样
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 强制触发(CancellationToken ct = default)
|
public virtual async Task 强制触发(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($"*TRG{ScpiDelimiter}", ct);
|
await SendAsync($"::TRIGger:MODE FTRIG{ScpiDelimiter}", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 【补充】设置触发模式 (AUTO, NORM, SINGLE)
|
/// 示波器触发控制模式(符合 SDS2000X-HD 规范)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 设置触发模式(string mode, CancellationToken ct = default)
|
public enum TriggerMode
|
||||||
{
|
{
|
||||||
string modeUpper = mode.ToUpper();
|
/// <summary>
|
||||||
if (modeUpper != "AUTO" && modeUpper != "NORM" && modeUpper != "SINGLE")
|
/// 自动触发模式 (即使无触发信号也周期性刷屏)
|
||||||
throw new ArgumentException("触发模式只能为 AUTO, NORM, 或 SINGLE");
|
/// </summary>
|
||||||
|
AUTO,
|
||||||
|
|
||||||
await SendAsync($"TRMD {modeUpper}{ScpiDelimiter}", ct);
|
/// <summary>
|
||||||
|
/// 普通触发模式 (仅当满足触发条件时才刷新)
|
||||||
|
/// </summary>
|
||||||
|
NORM,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单次触发模式 (捕捉到一次满足条件的信号后立刻 STOP)
|
||||||
|
/// </summary>
|
||||||
|
SINGLE
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// 设置触发模式 (AUTO 自动, NORM 普通, SINGLE 单次)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mode">触发模式枚举</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
public virtual async Task 设置触发模式(TriggerMode mode, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
// 例如发送:TRMD AUTO\n、TRMD NORM\n 或 TRMD SINGLE\n
|
||||||
|
string cmd = $"TRMD {mode}{ScpiDelimiter}";
|
||||||
|
await SendAsync(cmd, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region 3. Channel 垂直控制子系统 (C1 ~ C4)
|
#region 3. Channel 垂直控制子系统 (C1 ~ C4)
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 开启或关闭指定的模拟通道
|
/// 开启或关闭指定的模拟通道 (符合手册 57 页规范)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 设置通道开关(int channel, bool enable, CancellationToken ct = default)
|
public virtual async Task 设置通道开关(int channel, bool enable, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:根据手册规范,使用 1/0 比 ON/OFF 在高低版本固件中兼容性更稳定
|
string state = enable ? "ON" : "OFF";
|
||||||
string state = enable ? "1" : "0";
|
|
||||||
await SendAsync($"C{channel}:TRAce {state}{ScpiDelimiter}", ct);
|
await SendAsync($"C{channel}:TRAce {state}{ScpiDelimiter}", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置指定通道的垂直电压档位 (Volts/Div,单位: V,例如 0.05 代表 50mV/div)
|
/// 设置指定通道的垂直电压档位 (Volts/Div)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 设置通道电压档位(int channel, double volts, CancellationToken ct = default)
|
public virtual async Task 设置通道电压档位(int channel, double volts, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
@@ -130,7 +146,7 @@ namespace DeviceCommand.Device
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 【补充验证】查询指定通道当前的电压档位 (用于 Setup-Verify 闭环验证逻辑)
|
/// 查询指定通道当前的电压档位
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task<string> 查询通道电压档位(int channel, CancellationToken ct = default)
|
public virtual async Task<string> 查询通道电压档位(int channel, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
@@ -138,22 +154,29 @@ namespace DeviceCommand.Device
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置指定通道的垂直偏移量 (Offset,单位: V)
|
/// 设置指定通道的垂直偏移量 (Offset)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 设置通道垂直偏移(int channel, double offset, CancellationToken ct = default)
|
public virtual async Task 设置通道垂直偏移(int channel, double offset, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, "C{0}:OFST {1:F4}{2}", channel, offset, ScpiDelimiter);
|
string cmd = string.Format(CultureInfo.InvariantCulture, "C{0}:OFST {1:F4}{2}", channel, offset, ScpiDelimiter);
|
||||||
await SendAsync(cmd, ct);
|
await SendAsync(cmd, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置通道的输入阻抗与耦合模式
|
/// 示波器通道输入阻抗类型。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="coupling">合法参数:A1M (交流1M), D1M (直流1M), D50 (直流50欧)</param>
|
public enum ImpedanceType
|
||||||
public virtual async Task 设置通道耦合与阻抗(int channel, string coupling, CancellationToken ct = default)
|
|
||||||
{
|
{
|
||||||
string coupUpper = coupling.ToUpper();
|
/// <summary>50Ω 阻抗</summary>
|
||||||
await SendAsync($"C{channel}:COUPling {coupUpper}{ScpiDelimiter}", ct);
|
FIFty,
|
||||||
|
|
||||||
|
/// <summary>1MΩ 阻抗</summary>
|
||||||
|
ONEM
|
||||||
|
}
|
||||||
|
public virtual async Task 设置通道阻抗(int channel, ImpedanceType impedance, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
// 例如发送:C1:IMPedance FIFty\n 或 C1:IMPedance ONEM\n
|
||||||
|
string cmd = $"CHANnel{channel}:IMPedance {impedance}{ScpiDelimiter}";
|
||||||
|
await SendAsync(cmd, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -161,21 +184,19 @@ namespace DeviceCommand.Device
|
|||||||
#region 4. Timebase 水平时基子系统
|
#region 4. Timebase 水平时基子系统
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置示波器的水平时基档位 (Time/Div,单位: s,例如 0.001 代表 1ms/div)
|
/// 设置示波器的水平时基档位 (Time/Div)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 设置水平时基(double scale, CancellationToken ct = default)
|
public virtual async Task 设置水平时基(double scale, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:统一使用更精简且符合手册定义的简写形式 TDIV
|
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, "TDIV {0:E6}{1}", scale, ScpiDelimiter);
|
string cmd = string.Format(CultureInfo.InvariantCulture, "TDIV {0:E6}{1}", scale, ScpiDelimiter);
|
||||||
await SendAsync(cmd, ct);
|
await SendAsync(cmd, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置示波器的触发水平延迟位置 (Horizontal Delay,单位: s)
|
/// 设置示波器的触发水平延迟位置 (Horizontal Delay)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 设置水平延迟(double delay, CancellationToken ct = default)
|
public virtual async Task 设置水平延迟(double delay, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:采用手册标准简写 TRDL 效率更高
|
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, "TRDL {0:E6}{1}", delay, ScpiDelimiter);
|
string cmd = string.Format(CultureInfo.InvariantCulture, "TRDL {0:E6}{1}", delay, ScpiDelimiter);
|
||||||
await SendAsync(cmd, ct);
|
await SendAsync(cmd, ct);
|
||||||
}
|
}
|
||||||
@@ -185,12 +206,12 @@ namespace DeviceCommand.Device
|
|||||||
#region 5. Trigger 触发子系统
|
#region 5. Trigger 触发子系统
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置边沿触发的电平值 (Trigger Level,单位: V)
|
/// 设置边沿触发的电平值 (Trigger Level)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 设置触发电平(double level, CancellationToken ct = default)
|
public virtual async Task 设置触发电平(double level, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:采用标准简写 TRLV
|
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, "TRLV {0:F3}{1}", level, ScpiDelimiter);
|
string cmd = string.Format(CultureInfo.InvariantCulture, "TRIGger:EDGE:LEVel {0:F3}{1}", level, ScpiDelimiter);
|
||||||
await SendAsync(cmd, ct);
|
await SendAsync(cmd, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,30 +220,24 @@ namespace DeviceCommand.Device
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 设置触发源(string source, CancellationToken ct = default)
|
public virtual async Task 设置触发源(string source, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:采用标准简写 TRSE 体系配置指令
|
await SendAsync($"TRSE EDGE,SR,{source.ToUpper()}{ScpiDelimiter}", ct);
|
||||||
await SendAsync($"TRIGger:SOURce {source.ToUpper()}{ScpiDelimiter}", ct);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region 6. Measure 自动测量参数回读 (高频轮询核心)
|
#region 6. Measure 自动测量参数回读
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询指定通道自动测量项的当前实时测量数值
|
/// 查询指定通道自动测量项的当前实时测量数值
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="channel">通道号 (1-4)</param>
|
|
||||||
/// <param name="paramName">参数名称助记符:
|
|
||||||
/// PKPK(峰峰值), MAX(最大值), MIN(最小值), AMPL(振幅值),
|
|
||||||
/// FREQ(频率), PER(周期), MEAN(平均值), RMS(均方根) 等</param>
|
|
||||||
public virtual async Task<string> 查询通道测量项参数(int channel, string paramName, CancellationToken ct = default)
|
public virtual async Task<string> 查询通道测量项参数(int channel, string paramName, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 优化:采用更兼容的测量读取指令语法格式
|
|
||||||
string query = string.Format(CultureInfo.InvariantCulture, "C{0}:PAVA? {1}{2}", channel, paramName.ToUpper(), ScpiDelimiter);
|
string query = string.Format(CultureInfo.InvariantCulture, "C{0}:PAVA? {1}{2}", channel, paramName.ToUpper(), ScpiDelimiter);
|
||||||
return await WriteReadAsync(query, ScpiDelimiter, ct);
|
return await WriteReadAsync(query, ScpiDelimiter, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 轮询便捷接口:查询指定通道的电压峰峰值 (Vpp)
|
/// 查询指定通道的电压峰峰值 (Vpp)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task<string> 查询实际电压峰峰值(int channel, CancellationToken ct = default)
|
public virtual async Task<string> 查询实际电压峰峰值(int channel, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
@@ -230,7 +245,7 @@ namespace DeviceCommand.Device
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 轮询便捷接口:查询指定通道的频率值 (Frequency)
|
/// 查询指定通道的频率值 (Frequency)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task<string> 查询实际频率(int channel, CancellationToken ct = default)
|
public virtual async Task<string> 查询实际频率(int channel, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
@@ -238,7 +253,7 @@ namespace DeviceCommand.Device
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 轮询便捷接口:查询指定通道的真均方根电压值 (Vrms)
|
/// 查询指定通道的真均方根电压值 (Vrms)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task<string> 查询实际电压均方根(int channel, CancellationToken ct = default)
|
public virtual async Task<string> 查询实际电压均方根(int channel, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
@@ -246,5 +261,60 @@ namespace DeviceCommand.Device
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region 7. 屏幕截图导出子系统
|
||||||
|
/// 【一键获取并保存截图】
|
||||||
|
/// 自动下发 :PRINt? PNG 指令,接收示波器返回的纯 PNG 二进制流并直接保存到指定路径。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="saveFilePath">本地绝对保存路径 (例如: @"D:\Oscilloscope\Screen_01.png")</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>返回是否获取并保存成功</returns>
|
||||||
|
public virtual async Task<bool> 获取屏幕图像并保存(string saveFilePath, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 1. 调用我们在基类 Tcp 中全新实现的 ReadRawAsync
|
||||||
|
// 它会在内部下发 ":PRINt? PNG\n",自动接收完整的网络字节流
|
||||||
|
byte[] pureImageBytes = await ReadAllBytesAsync($":PRINt? PNG{ScpiDelimiter}", ct);
|
||||||
|
|
||||||
|
// 2. 校验返回数据
|
||||||
|
if (pureImageBytes == null || pureImageBytes.Length < 8)
|
||||||
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine("错误:接收到的图片数据为空或长度不足。");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 核心校验:验证是否为标准的 PNG 文件格式 (PNG 头通常为 89 50 4E 47)
|
||||||
|
if (pureImageBytes[0] != 0x89 || pureImageBytes[1] != 'P' || pureImageBytes[2] != 'N' || pureImageBytes[3] != 'G')
|
||||||
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine("错误:接收到的二进制数据不符合标准 PNG 格式头!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 如果传入的目标文件夹路径不存在,自动帮其创建
|
||||||
|
string directory = Path.GetDirectoryName(saveFilePath);
|
||||||
|
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 使用文件流直接将 PNG 字节数据落地写入本地硬盘
|
||||||
|
using (FileStream fs = new FileStream(saveFilePath, FileMode.Create, FileAccess.Write))
|
||||||
|
{
|
||||||
|
await fs.WriteAsync(pureImageBytes, 0, pureImageBytes.Length, ct);
|
||||||
|
await fs.FlushAsync(ct); // 强制刷新,确保数据完整落地
|
||||||
|
}
|
||||||
|
|
||||||
|
System.Diagnostics.Debug.WriteLine($"成功:截图已保存至路径: {saveFilePath}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine($"崩溃:保存截图时发生未知异常: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,108 +58,142 @@ namespace DeviceCommand.Device
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region 2. MEASure / NUMeric 核心数据测量与轮询 (高频轮询核心)
|
#region 2. NUMeric 核心数据测量与轮询 (动态单项读取)
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询指定通道的实时 RMS 电压值 (单位: V)
|
/// 查询指定通道的实时 RMS 电压值 (单位: V)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="channel">通道号 (例如: 1, 2, 3...)</param>
|
|
||||||
public virtual async Task<string> 查询实际电压(int channel, CancellationToken ct = default)
|
public virtual async Task<string> 查询实际电压(int channel, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string query = string.Format(CultureInfo.InvariantCulture, ":MEASure:NUMeric:VALue? U,{0}{1}", channel, ScpiDelimiter);
|
// 1. 先配置 ITEM1 为指定通道的电压有效值 (URMS)
|
||||||
|
string setItem = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:ITEM1 URMS,{0}{1}", channel, ScpiDelimiter);
|
||||||
|
await SendAsync(setItem, ct);
|
||||||
|
|
||||||
|
// 2. 再读取 ITEM1 的值
|
||||||
|
string query = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:VALue? 1{0}", ScpiDelimiter);
|
||||||
return await WriteReadAsync(query, ScpiDelimiter, ct);
|
return await WriteReadAsync(query, ScpiDelimiter, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询指定通道的实时 RMS 电流值 (单位: A)
|
/// 查询指定通道的实时 RMS 电流值 (单位: A)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="channel">通道号 (例如: 1, 2, 3...)</param>
|
|
||||||
public virtual async Task<string> 查询实际电流(int channel, CancellationToken ct = default)
|
public virtual async Task<string> 查询实际电流(int channel, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string query = string.Format(CultureInfo.InvariantCulture, ":MEASure:NUMeric:VALue? I,{0}{1}", channel, ScpiDelimiter);
|
// 1. 配置 ITEM1 为指定通道的电流有效值 (IRMS)
|
||||||
|
string setItem = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:ITEM1 IRMS,{0}{1}", channel, ScpiDelimiter);
|
||||||
|
await SendAsync(setItem, ct);
|
||||||
|
|
||||||
|
// 2. 读取 ITEM1 的值
|
||||||
|
string query = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:VALue? 1{0}", ScpiDelimiter);
|
||||||
return await WriteReadAsync(query, ScpiDelimiter, ct);
|
return await WriteReadAsync(query, ScpiDelimiter, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询指定通道的实时有功功率值 (单位: W)
|
/// 查询指定通道的实时有功功率值 (单位: W)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="channel">通道号 (例如: 1, 2, 3...)</param>
|
|
||||||
public virtual async Task<string> 查询实际功率(int channel, CancellationToken ct = default)
|
public virtual async Task<string> 查询实际功率(int channel, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string query = string.Format(CultureInfo.InvariantCulture, ":MEASure:NUMeric:VALue? P,{0}{1}", channel, ScpiDelimiter);
|
// 1. 配置 ITEM1 为指定通道的有功功率 (P)
|
||||||
|
string setItem = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:ITEM1 P,{0}{1}", channel, ScpiDelimiter);
|
||||||
|
await SendAsync(setItem, ct);
|
||||||
|
|
||||||
|
// 2. 读取 ITEM1 的值
|
||||||
|
string query = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:VALue? 1{0}", ScpiDelimiter);
|
||||||
return await WriteReadAsync(query, ScpiDelimiter, ct);
|
return await WriteReadAsync(query, ScpiDelimiter, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询指定通道的实时频率值 (单位: Hz)
|
/// 查询指定通道的实时频率值 (单位: Hz)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="channel">通道号 (例如: 1, 2, 3...)</param>
|
|
||||||
public virtual async Task<string> 查询频率(int channel, CancellationToken ct = default)
|
public virtual async Task<string> 查询频率(int channel, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string query = string.Format(CultureInfo.InvariantCulture, ":MEASure:NUMeric:VALue? FREQuency,{0}{1}", channel, ScpiDelimiter);
|
// 1. 配置 ITEM1 为指定通道的电压频率 (FU)
|
||||||
|
string setItem = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:ITEM1 FU,{0}{1}", channel, ScpiDelimiter);
|
||||||
|
await SendAsync(setItem, ct);
|
||||||
|
|
||||||
|
// 2. 读取 ITEM1 的值
|
||||||
|
string query = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:VALue? 1{0}", ScpiDelimiter);
|
||||||
return await WriteReadAsync(query, ScpiDelimiter, ct);
|
return await WriteReadAsync(query, ScpiDelimiter, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询指定通道的功率因数 (Power Factor)
|
/// 查询指定通道的功率因数 (Power Factor)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="channel">通道号 (例如: 1, 2, 3...)</param>
|
|
||||||
public virtual async Task<string> 查询功率因数(int channel, CancellationToken ct = default)
|
public virtual async Task<string> 查询功率因数(int channel, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string query = string.Format(CultureInfo.InvariantCulture, ":MEASure:NUMeric:VALue? PF,{0}{1}", channel, ScpiDelimiter);
|
// 1. 配置 ITEM1 为指定通道的功率因数 (LAMBda)
|
||||||
return await WriteReadAsync(query, ScpiDelimiter, ct);
|
string setItem = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:ITEM1 LAMBda,{0}{1}", channel, ScpiDelimiter);
|
||||||
}
|
await SendAsync(setItem, ct);
|
||||||
|
|
||||||
/// <summary>
|
// 2. 读取 ITEM1 的值
|
||||||
/// 自定义组合参数批量读取接口
|
string query = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:VALue? 1{0}", ScpiDelimiter);
|
||||||
/// </summary>
|
|
||||||
/// <param name="parameter">参数助记符 (如 "U,I,P" 或 "S,Q,LAMBda")</param>
|
|
||||||
/// <param name="channel">通道号</param>
|
|
||||||
public virtual async Task<string> 查询自定义测量组合(string parameter, int channel, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
string query = string.Format(CultureInfo.InvariantCulture, ":MEASure:NUMeric:VALue? {0},{1}{2}", parameter, channel, ScpiDelimiter);
|
|
||||||
return await WriteReadAsync(query, ScpiDelimiter, ct);
|
return await WriteReadAsync(query, ScpiDelimiter, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region 3. INPut 通道电气参数配置
|
#region 3. INPut 通道电气参数配置
|
||||||
|
/// <summary>
|
||||||
|
/// SPAW7000 电压量程枚举 (单位: V)
|
||||||
|
/// </summary>
|
||||||
|
public enum VoltageRange
|
||||||
|
{
|
||||||
|
V_15 = 15,
|
||||||
|
V_30 = 30,
|
||||||
|
V_60 = 60,
|
||||||
|
V_100 = 100,
|
||||||
|
V_150 = 150,
|
||||||
|
V_300 = 300,
|
||||||
|
V_600 = 600,
|
||||||
|
V_1000 = 1000
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置指定通道的电压量程 (例如: 15, 30, 60, 150, 300, 600, 1000)
|
/// SPAW7000 电流量程枚举 (单位: A)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 设置电压量程(int channel, double range, CancellationToken ct = default)
|
public enum CurrentRange
|
||||||
{
|
{
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, ":INPut:VOLTage:RANGe {0},{1:F1}{2}", channel, range, ScpiDelimiter);
|
A_1 = 1,
|
||||||
|
A_2 = 2,
|
||||||
|
A_5 = 5,
|
||||||
|
A_10 = 10,
|
||||||
|
A_20 = 20,
|
||||||
|
A_50 = 50
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// 设置指定通道的电压量程
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="channel">通道号/单元号 (1 - 7)</param>
|
||||||
|
/// <param name="range">电压量程枚举</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
public virtual async Task 设置电压量程(int channel, VoltageRange range, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
// 正确格式例如: :INPut:VOLTage:RANGe:ELEMent1 300\n
|
||||||
|
string cmd = string.Format(CultureInfo.InvariantCulture,
|
||||||
|
":INPut:VOLTage:RANGe:ELEMent{0} {1}{2}",
|
||||||
|
channel, (int)range, ScpiDelimiter);
|
||||||
|
|
||||||
await SendAsync(cmd, ct);
|
await SendAsync(cmd, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置指定通道的电流量程 (取决于接线单元或传感器输入类型)
|
/// 设置指定通道的电流量程
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 设置电流量程(int channel, double range, CancellationToken ct = default)
|
/// <param name="channel">通道号/单元号 (1 - 7)</param>
|
||||||
|
/// <param name="range">电流量程枚举</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
public virtual async Task 设置电流量程(int channel, CurrentRange range, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, ":INPut:CURRent:RANGe {0},{1:F3}{2}", channel, range, ScpiDelimiter);
|
// 正确格式例如: :INPut:CURRent:RANGe:ELEMent1 5\n
|
||||||
|
string cmd = string.Format(CultureInfo.InvariantCulture,
|
||||||
|
":INPut:CURRent:RANGe:ELEMent{0} {1}{2}",
|
||||||
|
channel, (int)range, ScpiDelimiter);
|
||||||
|
|
||||||
await SendAsync(cmd, ct);
|
await SendAsync(cmd, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 设置通道的耦合模式 (AC, DC, ACDC)
|
|
||||||
/// </summary>
|
|
||||||
public virtual async Task 设置通道耦合模式(int channel, string mode, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
string modeUpper = mode.ToUpper();
|
|
||||||
if (modeUpper != "AC" && modeUpper != "DC" && modeUpper != "ACDC")
|
|
||||||
throw new ArgumentException("耦合模式只能为 AC, DC, 或 ACDC");
|
|
||||||
|
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, ":INPut:COUPling {0},{1}{2}", channel, modeUpper, ScpiDelimiter);
|
|
||||||
await SendAsync(cmd, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region 4. SYSTem 系统设置与状态查询
|
#region 4. SYSTem 系统设置与状态查询
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 14. 查询仪器型号名称
|
/// 14. 查询仪器型号名称
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ namespace DeviceEditModule
|
|||||||
containerRegistry.RegisterForNavigation<N69200View>("N69200View");
|
containerRegistry.RegisterForNavigation<N69200View>("N69200View");
|
||||||
containerRegistry.RegisterForNavigation<SDS2000X_HDView>("SDS2000X_HDView");
|
containerRegistry.RegisterForNavigation<SDS2000X_HDView>("SDS2000X_HDView");
|
||||||
containerRegistry.RegisterForNavigation<SPAW7000View>("SPAW7000View");
|
containerRegistry.RegisterForNavigation<SPAW7000View>("SPAW7000View");
|
||||||
|
containerRegistry.Register<IT7800EViewModel>();
|
||||||
|
containerRegistry.Register<N36200ViewModel>();
|
||||||
|
containerRegistry.Register<N36600ViewModel>();
|
||||||
|
containerRegistry.Register<N69200ViewModel>();
|
||||||
|
containerRegistry.Register<SDS2000X_HDViewModel>();
|
||||||
|
containerRegistry.Register<SPAW7000ViewModel>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,14 @@ using DeviceCommand.Device;
|
|||||||
using Prism.Commands;
|
using Prism.Commands;
|
||||||
using Prism.Ioc;
|
using Prism.Ioc;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using UIShare.GlobalVariable;
|
using UIShare.GlobalVariable;
|
||||||
using UIShare.ViewModelBase;
|
using UIShare.ViewModelBase;
|
||||||
|
using static DeviceCommand.Device.SDS2000X_HD;
|
||||||
|
|
||||||
namespace DeviceEditModule.ViewModels
|
namespace DeviceEditModule.ViewModels
|
||||||
{
|
{
|
||||||
@@ -142,24 +146,28 @@ namespace DeviceEditModule.ViewModels
|
|||||||
|
|
||||||
#region 命令
|
#region 命令
|
||||||
|
|
||||||
public ICommand QueryIdentityCommand { get; }
|
public ICommand QueryIdentityCommand { get; }
|
||||||
public ICommand ResetDeviceCommand { get; }
|
public ICommand ResetDeviceCommand { get; }
|
||||||
public ICommand RunCommand { get; }
|
public ICommand RunCommand { get; }
|
||||||
public ICommand StopCommand { get; }
|
public ICommand StopCommand { get; }
|
||||||
public ICommand SingleCommand { get; }
|
public ICommand SingleCommand { get; }
|
||||||
public ICommand ForceTriggerCommand { get; }
|
public ICommand ForceTriggerCommand { get; }
|
||||||
public ICommand SetChannelOnCommand { get; }
|
public ICommand SetChannelOnCommand { get; }
|
||||||
public ICommand SetChannelOffCommand { get; }
|
public ICommand SetChannelOffCommand { get; }
|
||||||
public ICommand SetVoltsDivCommand { get; }
|
public ICommand SetVoltsDivCommand { get; }
|
||||||
public ICommand SetOffsetCommand { get; }
|
public ICommand SetOffsetCommand { get; }
|
||||||
|
|
||||||
public ICommand SetTimeBaseCommand { get; }
|
// 🛠️ 补全:设置阻抗的 Command
|
||||||
public ICommand SetTriggerLevelCommand { get; }
|
public ICommand SetImpedance50Command { get; }
|
||||||
|
public ICommand SetImpedance1MCommand { get; }
|
||||||
|
|
||||||
|
public ICommand SetTimeBaseCommand { get; }
|
||||||
|
public ICommand SetTriggerLevelCommand { get; }
|
||||||
public ICommand SetTriggerSourceCommand { get; }
|
public ICommand SetTriggerSourceCommand { get; }
|
||||||
public ICommand QueryMeasurementsCommand{ get; }
|
public ICommand QueryMeasurementsCommand { get; }
|
||||||
public ICommand QueryVppCommand { get; }
|
public ICommand QueryVppCommand { get; }
|
||||||
public ICommand QueryFrequencyCommand { get; }
|
public ICommand QueryFrequencyCommand { get; }
|
||||||
public ICommand QueryRmsCommand { get; }
|
public ICommand QueryRmsCommand { get; }
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -167,32 +175,73 @@ namespace DeviceEditModule.ViewModels
|
|||||||
{
|
{
|
||||||
_deviceManager = containerProvider.Resolve<DeviceManager>();
|
_deviceManager = containerProvider.Resolve<DeviceManager>();
|
||||||
|
|
||||||
QueryIdentityCommand = new DelegateCommand(async () => await Exec(async () => AppendLog("IDN: " + await _device!.查询设备标识(Ct()))));
|
QueryIdentityCommand = new DelegateCommand(async () => await Exec(async () => AppendLog("IDN: " + await _device!.查询设备标识(Ct()))));
|
||||||
ResetDeviceCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.重置设备(Ct()); AppendLog("设备已重置"); }));
|
ResetDeviceCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.重置设备(Ct()); AppendLog("设备已重置"); }));
|
||||||
RunCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.启动捕获_RUN(Ct()); AppendLog("已开始捕获"); }));
|
RunCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.启动捕获_RUN(Ct()); AppendLog("已开始捕获"); }));
|
||||||
StopCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.停止捕获_STOP(Ct()); AppendLog("已停止捕获"); }));
|
StopCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.停止捕获_STOP(Ct()); AppendLog("已停止捕获"); }));
|
||||||
SingleCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.单次触发_SINGLE(Ct()); AppendLog("已触发单次捕获"); }));
|
SingleCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.单次触发_SINGLE(Ct()); AppendLog("已触发单次捕获"); }));
|
||||||
ForceTriggerCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.强制触发(Ct()); AppendLog("已强制触发"); }));
|
ForceTriggerCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.强制触发(Ct()); AppendLog("已强制触发"); }));
|
||||||
SetChannelOnCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置通道开关(Channel, true, Ct()); AppendLog($"通道 {Channel} 已开启"); }));
|
SetChannelOnCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置通道开关(Channel, true, Ct()); AppendLog($"通道 {Channel} 已开启"); }));
|
||||||
SetChannelOffCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置通道开关(Channel, false, Ct()); AppendLog($"通道 {Channel} 已关闭"); }));
|
SetChannelOffCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置通道开关(Channel, false, Ct()); AppendLog($"通道 {Channel} 已关闭"); }));
|
||||||
SetVoltsDivCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置通道电压档位(Channel, VoltsPerDiv, Ct()); AppendLog($"C{Channel} 电压档位已设为 {VoltsPerDiv} V/div"); }));
|
SetVoltsDivCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置通道电压档位(Channel, VoltsPerDiv, Ct()); AppendLog($"C{Channel} 电压档位已设为 {VoltsPerDiv} V/div"); }));
|
||||||
SetOffsetCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置通道垂直偏移(Channel, Offset, Ct()); AppendLog($"C{Channel} 垂直偏移已设为 {Offset} V"); }));
|
SetOffsetCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置通道垂直偏移(Channel, Offset, Ct()); AppendLog($"C{Channel} 垂直偏移已设为 {Offset} V"); }));
|
||||||
|
|
||||||
SetTimeBaseCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置水平时基(TimeBase, Ct()); AppendLog($"水平时基已设为 {TimeBase} s/div"); }));
|
// 🛠️ 绑定:设置 50Ω 和 1MΩ 阻抗控制
|
||||||
SetTriggerLevelCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置触发电平(TriggerLevel, Ct()); AppendLog($"触发电平已设为 {TriggerLevel} V"); }));
|
SetImpedance50Command = new DelegateCommand(async () => await Exec(async () =>
|
||||||
|
{
|
||||||
|
await _device!.设置通道阻抗(Channel, ImpedanceType.FIFty, Ct());
|
||||||
|
Is50Ohm = true;
|
||||||
|
AppendLog($"C{Channel} 输入阻抗已设为 50Ω");
|
||||||
|
}));
|
||||||
|
|
||||||
|
SetImpedance1MCommand = new DelegateCommand(async () => await Exec(async () =>
|
||||||
|
{
|
||||||
|
await _device!.设置通道阻抗(Channel, ImpedanceType.ONEM, Ct());
|
||||||
|
Is50Ohm = false;
|
||||||
|
AppendLog($"C{Channel} 输入阻抗已设为 1MΩ");
|
||||||
|
}));
|
||||||
|
|
||||||
|
SetTimeBaseCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置水平时基(TimeBase, Ct()); AppendLog($"水平时基已设为 {TimeBase} s/div"); }));
|
||||||
|
SetTriggerLevelCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置触发电平(TriggerLevel, Ct()); AppendLog($"触发电平已设为 {TriggerLevel} V"); }));
|
||||||
SetTriggerSourceCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置触发源(TriggerSource, Ct()); AppendLog($"触发源已设为 {TriggerSource}"); }));
|
SetTriggerSourceCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置触发源(TriggerSource, Ct()); AppendLog($"触发源已设为 {TriggerSource}"); }));
|
||||||
|
|
||||||
QueryMeasurementsCommand = new DelegateCommand(async () => await Exec(async () =>
|
QueryMeasurementsCommand = new DelegateCommand(async () => await Exec(async () =>
|
||||||
{
|
{
|
||||||
MeasuredVpp = await _device!.查询实际电压峰峰值(Channel, Ct());
|
// 1. 保留设备原生吐出的完整原始字符串
|
||||||
MeasuredFrequency = await _device!.查询实际频率(Channel, Ct());
|
string rawVpp = await _device!.查询实际电压峰峰值(Channel, Ct());
|
||||||
MeasuredRms = await _device!.查询实际电压均方根(Channel, Ct());
|
string rawFreq = await _device!.查询实际频率(Channel, Ct());
|
||||||
AppendLog($"C{Channel} 测量 → Vpp:{MeasuredVpp} Freq:{MeasuredFrequency}Hz RMS:{MeasuredRms}V");
|
string rawRms = await _device!.查询实际电压均方根(Channel, Ct());
|
||||||
|
|
||||||
|
// 2. 清洗数据并将科学计数法转为常规小数后赋值给 UI 属性
|
||||||
|
MeasuredVpp = ParseMeasurement(rawVpp);
|
||||||
|
MeasuredFrequency = ParseMeasurement(rawFreq);
|
||||||
|
MeasuredRms = ParseMeasurement(rawRms);
|
||||||
|
|
||||||
|
// 3. 在日志中同时体现设备原始报文与解析呈现值,极方便联调
|
||||||
|
AppendLog($"C{Channel} 测量原始数据 → Vpp:{rawVpp.Trim()} Freq:{rawFreq.Trim()} RMS:{rawRms.Trim()}");
|
||||||
|
AppendLog($"C{Channel} 界面呈现数值 → Vpp:{MeasuredVpp}V Freq:{MeasuredFrequency}Hz RMS:{MeasuredRms}V");
|
||||||
}));
|
}));
|
||||||
|
|
||||||
QueryVppCommand = new DelegateCommand(async () => await Exec(async () => { MeasuredVpp = await _device!.查询实际电压峰峰值(Channel, Ct()); AppendLog($"C{Channel} Vpp: {MeasuredVpp}"); }));
|
QueryVppCommand = new DelegateCommand(async () => await Exec(async () =>
|
||||||
QueryFrequencyCommand = new DelegateCommand(async () => await Exec(async () => { MeasuredFrequency = await _device!.查询实际频率(Channel, Ct()); AppendLog($"C{Channel} Freq: {MeasuredFrequency}"); }));
|
{
|
||||||
QueryRmsCommand = new DelegateCommand(async () => await Exec(async () => { MeasuredRms = await _device!.查询实际电压均方根(Channel, Ct()); AppendLog($"C{Channel} RMS: {MeasuredRms}"); }));
|
string raw = await _device!.查询实际电压峰峰值(Channel, Ct());
|
||||||
|
MeasuredVpp = ParseMeasurement(raw);
|
||||||
|
AppendLog($"C{Channel} Vpp: {MeasuredVpp} (Raw: {raw.Trim()})");
|
||||||
|
}));
|
||||||
|
|
||||||
|
QueryFrequencyCommand = new DelegateCommand(async () => await Exec(async () =>
|
||||||
|
{
|
||||||
|
string raw = await _device!.查询实际频率(Channel, Ct());
|
||||||
|
MeasuredFrequency = ParseMeasurement(raw);
|
||||||
|
AppendLog($"C{Channel} Freq: {MeasuredFrequency} (Raw: {raw.Trim()})");
|
||||||
|
}));
|
||||||
|
|
||||||
|
QueryRmsCommand = new DelegateCommand(async () => await Exec(async () =>
|
||||||
|
{
|
||||||
|
string raw = await _device!.查询实际电压均方根(Channel, Ct());
|
||||||
|
MeasuredRms = ParseMeasurement(raw);
|
||||||
|
AppendLog($"C{Channel} RMS: {MeasuredRms} (Raw: {raw.Trim()})");
|
||||||
|
}));
|
||||||
|
|
||||||
Initialize();
|
Initialize();
|
||||||
}
|
}
|
||||||
@@ -224,8 +273,8 @@ namespace DeviceEditModule.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_device = found;
|
_device = found;
|
||||||
DeviceName = foundName ?? "SDS2000X_HD (未找到)";
|
DeviceName = foundName ?? "SDS2000X_HD (未找到)";
|
||||||
IsConnected = _device?.IsConnected ?? false;
|
IsConnected = _device?.IsConnected ?? false;
|
||||||
|
|
||||||
AppendLog(found != null
|
AppendLog(found != null
|
||||||
@@ -281,6 +330,52 @@ namespace DeviceEditModule.ViewModels
|
|||||||
: line + "\n" + ResponseLog;
|
: line + "\n" + ResponseLog;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 清洗示波器原始返回的测量字符串(例如 "C1:PAVA RMS,5.57E-03V")并安全转化为无科学计数法的小数形式。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="rawResponse">设备原始应答数据</param>
|
||||||
|
/// <returns>可直接绑定到 UI 呈现的字符串数字</returns>
|
||||||
|
private string ParseMeasurement(string rawResponse)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(rawResponse)) return "—";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string cleanData = rawResponse.Trim();
|
||||||
|
|
||||||
|
// 1. 斩断报头,提取逗号后面的具体内容(如 "5.57E-03V" 或 "****")
|
||||||
|
int commaIndex = cleanData.IndexOf(',');
|
||||||
|
if (commaIndex == -1) return "—";
|
||||||
|
|
||||||
|
string valStr = cleanData.Substring(commaIndex + 1);
|
||||||
|
|
||||||
|
var match = Regex.Match(valStr, @"[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?");
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
valStr = match.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 校验并拦截设备未测出时的无效星号 "****"
|
||||||
|
if (valStr.Contains("*") || string.IsNullOrWhiteSpace(valStr))
|
||||||
|
{
|
||||||
|
return "0"; // 回归为零或 "—",防止触发数据异常
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 解析科学计数法,并重新以不带科学计数法的小数样式展开
|
||||||
|
if (double.TryParse(valStr, NumberStyles.Any, CultureInfo.InvariantCulture, out double result))
|
||||||
|
{
|
||||||
|
// "0.######" 样式会自动消除末尾无用的冗余零,并显示为普通小数(如 0.00557)
|
||||||
|
return result.ToString("0.######", CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 捕获可能产生的边缘转换故障,保证轮询线程绝不崩溃
|
||||||
|
}
|
||||||
|
|
||||||
|
return "—";
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ using DeviceCommand.Device;
|
|||||||
using Prism.Commands;
|
using Prism.Commands;
|
||||||
using Prism.Ioc;
|
using Prism.Ioc;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using UIShare.GlobalVariable;
|
using UIShare.GlobalVariable;
|
||||||
using UIShare.ViewModelBase;
|
using UIShare.ViewModelBase;
|
||||||
@@ -57,29 +59,27 @@ namespace DeviceEditModule.ViewModels
|
|||||||
set => SetProperty(ref _channel, value);
|
set => SetProperty(ref _channel, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private double _voltageRange = 300.0;
|
private SPAW7000.VoltageRange _voltageRange = SPAW7000.VoltageRange.V_300;
|
||||||
/// <summary>电压量程(V)。</summary>
|
/// <summary>电压量程枚举。</summary>
|
||||||
public double VoltageRange
|
public SPAW7000.VoltageRange VoltageRange
|
||||||
{
|
{
|
||||||
get => _voltageRange;
|
get => _voltageRange;
|
||||||
set => SetProperty(ref _voltageRange, value);
|
set => SetProperty(ref _voltageRange, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private double _currentRange = 5.0;
|
private SPAW7000.CurrentRange _currentRange = SPAW7000.CurrentRange.A_5;
|
||||||
/// <summary>电流量程(A)。</summary>
|
/// <summary>电流量程枚举。</summary>
|
||||||
public double CurrentRange
|
public SPAW7000.CurrentRange CurrentRange
|
||||||
{
|
{
|
||||||
get => _currentRange;
|
get => _currentRange;
|
||||||
set => SetProperty(ref _currentRange, value);
|
set => SetProperty(ref _currentRange, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _couplingMode = "DC";
|
public SPAW7000.VoltageRange[] VoltageRangeOptions { get; } =
|
||||||
/// <summary>耦合模式:AC / DC / ACDC。</summary>
|
(SPAW7000.VoltageRange[])Enum.GetValues(typeof(SPAW7000.VoltageRange));
|
||||||
public string CouplingMode
|
|
||||||
{
|
public SPAW7000.CurrentRange[] CurrentRangeOptions { get; } =
|
||||||
get => _couplingMode;
|
(SPAW7000.CurrentRange[])Enum.GetValues(typeof(SPAW7000.CurrentRange));
|
||||||
set => SetProperty(ref _couplingMode, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int _resolution = 6;
|
private int _resolution = 6;
|
||||||
/// <summary>显示分辨率(5 或 6)。</summary>
|
/// <summary>显示分辨率(5 或 6)。</summary>
|
||||||
@@ -162,19 +162,18 @@ namespace DeviceEditModule.ViewModels
|
|||||||
|
|
||||||
#region 命令
|
#region 命令
|
||||||
|
|
||||||
public ICommand QueryIdentityCommand { get; }
|
public ICommand QueryIdentityCommand { get; }
|
||||||
public ICommand ResetDeviceCommand { get; }
|
public ICommand ResetDeviceCommand { get; }
|
||||||
public ICommand QueryAllMeasureCommand { get; }
|
public ICommand QueryAllMeasureCommand { get; }
|
||||||
public ICommand SetVoltageRangeCommand { get; }
|
public ICommand SetVoltageRangeCommand { get; }
|
||||||
public ICommand SetCurrentRangeCommand { get; }
|
public ICommand SetCurrentRangeCommand { get; }
|
||||||
public ICommand SetCouplingModeCommand { get; }
|
public ICommand SetResolutionCommand { get; }
|
||||||
public ICommand SetResolutionCommand { get; }
|
public ICommand SetBrightnessCommand { get; }
|
||||||
public ICommand SetBrightnessCommand { get; }
|
public ICommand SetTouchLockOnCommand { get; }
|
||||||
public ICommand SetTouchLockOnCommand { get; }
|
public ICommand SetTouchLockOffCommand { get; }
|
||||||
public ICommand SetTouchLockOffCommand { get; }
|
public ICommand QueryModelCommand { get; }
|
||||||
public ICommand QueryModelCommand { get; }
|
public ICommand QuerySerialCommand { get; }
|
||||||
public ICommand QuerySerialCommand { get; }
|
public ICommand QueryStatusByteCommand { get; }
|
||||||
public ICommand QueryStatusByteCommand { get; }
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -182,26 +181,34 @@ namespace DeviceEditModule.ViewModels
|
|||||||
{
|
{
|
||||||
_deviceManager = containerProvider.Resolve<DeviceManager>();
|
_deviceManager = containerProvider.Resolve<DeviceManager>();
|
||||||
|
|
||||||
QueryIdentityCommand = new DelegateCommand(async () => await Exec(async () => AppendLog("IDN: " + await _device!.查询设备标识(Ct()))));
|
QueryIdentityCommand = new DelegateCommand(async () => await Exec(async () => AppendLog("IDN: " + await _device!.查询设备标识(Ct()))));
|
||||||
ResetDeviceCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.重置设备(Ct()); AppendLog("设备已重置"); }));
|
ResetDeviceCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.重置设备(Ct()); AppendLog("设备已重置"); }));
|
||||||
SetVoltageRangeCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置电压量程(Channel, VoltageRange, Ct()); AppendLog($"通道 {Channel} 电压量程已设为 {VoltageRange} V"); }));
|
SetVoltageRangeCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置电压量程(Channel, VoltageRange, Ct()); AppendLog($"通道 {Channel} 电压量程已设为 {(int)VoltageRange} V"); }));
|
||||||
SetCurrentRangeCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置电流量程(Channel, CurrentRange, Ct()); AppendLog($"通道 {Channel} 电流量程已设为 {CurrentRange} A"); }));
|
SetCurrentRangeCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置电流量程(Channel, CurrentRange, Ct()); AppendLog($"通道 {Channel} 电流量程已设为 {(int)CurrentRange} A"); }));
|
||||||
SetCouplingModeCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置通道耦合模式(Channel, CouplingMode, Ct()); AppendLog($"通道 {Channel} 耦合模式已设为 {CouplingMode}"); }));
|
SetResolutionCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置显示分辨率(Resolution, Ct()); AppendLog($"显示分辨率已设为 {Resolution} 位"); }));
|
||||||
SetResolutionCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置显示分辨率(Resolution, Ct()); AppendLog($"显示分辨率已设为 {Resolution} 位"); }));
|
SetBrightnessCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置显示亮度(Brightness, Ct()); AppendLog($"屏幕亮度已设为 {Brightness}"); }));
|
||||||
SetBrightnessCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置显示亮度(Brightness, Ct()); AppendLog($"屏幕亮度已设为 {Brightness}"); }));
|
SetTouchLockOnCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置屏幕触摸锁定(true, Ct()); AppendLog("屏幕触摸已锁定"); }));
|
||||||
SetTouchLockOnCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置屏幕触摸锁定(true, Ct()); AppendLog("屏幕触摸已锁定"); }));
|
|
||||||
SetTouchLockOffCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置屏幕触摸锁定(false, Ct()); AppendLog("屏幕触摸已解锁"); }));
|
SetTouchLockOffCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.设置屏幕触摸锁定(false, Ct()); AppendLog("屏幕触摸已解锁"); }));
|
||||||
QueryModelCommand = new DelegateCommand(async () => await Exec(async () => { DeviceModel = await _device!.查询设备型号(Ct()); AppendLog($"型号: {DeviceModel}"); }));
|
QueryModelCommand = new DelegateCommand(async () => await Exec(async () => { DeviceModel = await _device!.查询设备型号(Ct()); AppendLog($"型号: {DeviceModel}"); }));
|
||||||
QuerySerialCommand = new DelegateCommand(async () => await Exec(async () => { DeviceSerial = await _device!.查询设备序列号(Ct()); AppendLog($"序列号: {DeviceSerial}"); }));
|
QuerySerialCommand = new DelegateCommand(async () => await Exec(async () => { DeviceSerial = await _device!.查询设备序列号(Ct()); AppendLog($"序列号: {DeviceSerial}"); }));
|
||||||
QueryStatusByteCommand = new DelegateCommand(async () => await Exec(async () => AppendLog("STB: " + await _device!.读取状态字节(Ct()))));
|
QueryStatusByteCommand = new DelegateCommand(async () => await Exec(async () => AppendLog("STB: " + await _device!.读取状态字节(Ct()))));
|
||||||
|
|
||||||
|
// 只修改这里:保持你原本的 5 次驱动查询调用不变,仅对读回来的科学计数法进行两位小数格式化
|
||||||
QueryAllMeasureCommand = new DelegateCommand(async () => await Exec(async () =>
|
QueryAllMeasureCommand = new DelegateCommand(async () => await Exec(async () =>
|
||||||
{
|
{
|
||||||
MeasuredVoltage = await _device!.查询实际电压(Channel, Ct());
|
string rawU = await _device!.查询实际电压(Channel, Ct());
|
||||||
MeasuredCurrent = await _device!.查询实际电流(Channel, Ct());
|
string rawI = await _device!.查询实际电流(Channel, Ct());
|
||||||
MeasuredPower = await _device!.查询实际功率(Channel, Ct());
|
string rawP = await _device!.查询实际功率(Channel, Ct());
|
||||||
MeasuredFrequency = await _device!.查询频率(Channel, Ct());
|
string rawF = await _device!.查询频率(Channel, Ct());
|
||||||
MeasuredPowerFactor = await _device!.查询功率因数(Channel, Ct());
|
string rawPF = await _device!.查询功率因数(Channel, Ct());
|
||||||
|
|
||||||
|
// 转换科学计数法格式,如果失败会自动保持原样
|
||||||
|
MeasuredVoltage = FormatToDecimal(rawU, "F2"); // 电压两位小数
|
||||||
|
MeasuredCurrent = FormatToDecimal(rawI, "F3"); // 电流通常较小,建议保留3位小数
|
||||||
|
MeasuredPower = FormatToDecimal(rawP, "F2"); // 功率两位小数
|
||||||
|
MeasuredFrequency = FormatToDecimal(rawF, "F2"); // 频率两位小数
|
||||||
|
MeasuredPowerFactor = FormatToDecimal(rawPF, "F3"); // 功率因数保留3位小数
|
||||||
|
|
||||||
AppendLog($"CH{Channel} 测量 → U:{MeasuredVoltage}V I:{MeasuredCurrent}A P:{MeasuredPower}W F:{MeasuredFrequency}Hz PF:{MeasuredPowerFactor}");
|
AppendLog($"CH{Channel} 测量 → U:{MeasuredVoltage}V I:{MeasuredCurrent}A P:{MeasuredPower}W F:{MeasuredFrequency}Hz PF:{MeasuredPowerFactor}");
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -235,8 +242,8 @@ namespace DeviceEditModule.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_device = found;
|
_device = found;
|
||||||
DeviceName = foundName ?? "SPAW7000 (未找到)";
|
DeviceName = foundName ?? "SPAW7000 (未找到)";
|
||||||
IsConnected = _device?.IsConnected ?? false;
|
IsConnected = _device?.IsConnected ?? false;
|
||||||
|
|
||||||
AppendLog(found != null
|
AppendLog(found != null
|
||||||
@@ -256,6 +263,41 @@ namespace DeviceEditModule.ViewModels
|
|||||||
|
|
||||||
private CancellationToken Ct() => (_cts = new CancellationTokenSource(TimeSpan.FromSeconds(10))).Token;
|
private CancellationToken Ct() => (_cts = new CancellationTokenSource(TimeSpan.FromSeconds(10))).Token;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将科学计数法字符串转换为标准小数格式
|
||||||
|
/// </summary>
|
||||||
|
private string FormatToDecimal(string rawInput, string decimalFormat = "F2")
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(rawInput))
|
||||||
|
return "—";
|
||||||
|
|
||||||
|
// 去除可能夹杂的命令头,保留数据部分
|
||||||
|
string cleanInput = CleanResponseHeader(rawInput);
|
||||||
|
|
||||||
|
// 解析科学计数法(NumberStyles.Any 和 InvariantCulture 是关键)
|
||||||
|
if (double.TryParse(cleanInput, NumberStyles.Any, CultureInfo.InvariantCulture, out double value))
|
||||||
|
{
|
||||||
|
return value.ToString(decimalFormat, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleanInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 辅助清洗:剥离可能伴随吐回的命令头或双引号
|
||||||
|
/// </summary>
|
||||||
|
private string CleanResponseHeader(string response)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(response)) return string.Empty;
|
||||||
|
string result = response.Trim();
|
||||||
|
if (result.Contains(" "))
|
||||||
|
{
|
||||||
|
int lastSpaceIndex = result.LastIndexOf(' ');
|
||||||
|
result = result.Substring(lastSpaceIndex + 1).Trim();
|
||||||
|
}
|
||||||
|
return result.Replace("\"", "").Replace("'", "").Trim();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task Exec(Func<Task> action)
|
private async Task Exec(Func<Task> action)
|
||||||
{
|
{
|
||||||
if (_device == null)
|
if (_device == null)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
prism:ViewModelLocator.AutoWireViewModel="True"
|
||||||
Height="850" Width="900">
|
Height="900" Width="900">
|
||||||
|
|
||||||
<!-- 此窗口独立开启 ShowInTaskbar,以支持最小化后在任务栏恢复 -->
|
<!-- 此窗口独立开启 ShowInTaskbar,以支持最小化后在任务栏恢复 -->
|
||||||
<prism:Dialog.WindowStyle>
|
<prism:Dialog.WindowStyle>
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
<!-- 标题栏 -->
|
<!-- 标题栏 -->
|
||||||
<RowDefinition Height="38"/>
|
<RowDefinition Height="38"/>
|
||||||
<!-- Tab 标签条 -->
|
<!-- Tab 标签条 -->
|
||||||
<RowDefinition Height="40"/>
|
<RowDefinition Height="80"/>
|
||||||
<!-- 内容区 -->
|
<!-- 内容区 -->
|
||||||
<RowDefinition Height="*"/>
|
<RowDefinition Height="*"/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||||
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
prism:ViewModelLocator.AutoWireViewModel="False"
|
||||||
d:DesignHeight="760" d:DesignWidth="860">
|
d:DesignHeight="760" d:DesignWidth="860">
|
||||||
|
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||||
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
prism:ViewModelLocator.AutoWireViewModel="False"
|
||||||
d:DesignHeight="760" d:DesignWidth="860">
|
d:DesignHeight="760" d:DesignWidth="860">
|
||||||
|
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||||
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
prism:ViewModelLocator.AutoWireViewModel="False"
|
||||||
d:DesignHeight="760" d:DesignWidth="860">
|
d:DesignHeight="760" d:DesignWidth="860">
|
||||||
|
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||||
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
prism:ViewModelLocator.AutoWireViewModel="False"
|
||||||
d:DesignHeight="760" d:DesignWidth="860">
|
d:DesignHeight="760" d:DesignWidth="860">
|
||||||
|
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
|
|||||||
@@ -4,10 +4,11 @@
|
|||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:prism="http://prismlibrary.com/"
|
xmlns:prism="http://prismlibrary.com/"
|
||||||
|
xmlns:sys="clr-namespace:System;assembly=mscorlib"
|
||||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||||
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
prism:ViewModelLocator.AutoWireViewModel="False"
|
||||||
d:DesignHeight="760" d:DesignWidth="860">
|
d:DesignHeight="760" d:DesignWidth="860">
|
||||||
|
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
@@ -92,10 +93,10 @@
|
|||||||
<ComboBox Width="80" Height="32" Margin="4,0"
|
<ComboBox Width="80" Height="32" Margin="4,0"
|
||||||
SelectedItem="{Binding Channel}"
|
SelectedItem="{Binding Channel}"
|
||||||
VerticalContentAlignment="Center" FontSize="12">
|
VerticalContentAlignment="Center" FontSize="12">
|
||||||
<ComboBoxItem Content="1"/>
|
<sys:Int32>1</sys:Int32>
|
||||||
<ComboBoxItem Content="2"/>
|
<sys:Int32>2</sys:Int32>
|
||||||
<ComboBoxItem Content="3"/>
|
<sys:Int32>3</sys:Int32>
|
||||||
<ComboBoxItem Content="4"/>
|
<sys:Int32>4</sys:Int32>
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
<Button Content="开启" Command="{Binding SetChannelOnCommand}"
|
<Button Content="开启" Command="{Binding SetChannelOnCommand}"
|
||||||
Style="{StaticResource CmdBtn}"/>
|
Style="{StaticResource CmdBtn}"/>
|
||||||
@@ -137,7 +138,7 @@
|
|||||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
<TextBlock Text="V/div (V)" Style="{StaticResource ParamLabel}"/>
|
<TextBlock Text="V/div (V)" Style="{StaticResource ParamLabel}"/>
|
||||||
<TextBox Style="{StaticResource NumInput}"
|
<TextBox Style="{StaticResource NumInput}"
|
||||||
Text="{Binding VoltsPerDiv, UpdateSourceTrigger=PropertyChanged}"/>
|
Text="{Binding VoltsPerDiv}"/>
|
||||||
<Button Content="设置" Command="{Binding SetVoltsDivCommand}"
|
<Button Content="设置" Command="{Binding SetVoltsDivCommand}"
|
||||||
Style="{StaticResource CmdBtn}"/>
|
Style="{StaticResource CmdBtn}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -165,7 +166,7 @@
|
|||||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
<TextBlock Text="时基 (s/div)" Style="{StaticResource ParamLabel}"/>
|
<TextBlock Text="时基 (s/div)" Style="{StaticResource ParamLabel}"/>
|
||||||
<TextBox Style="{StaticResource NumInput}"
|
<TextBox Style="{StaticResource NumInput}"
|
||||||
Text="{Binding TimeBase, UpdateSourceTrigger=PropertyChanged}"/>
|
Text="{Binding TimeBase}"/>
|
||||||
<Button Content="设置" Command="{Binding SetTimeBaseCommand}"
|
<Button Content="设置" Command="{Binding SetTimeBaseCommand}"
|
||||||
Style="{StaticResource CmdBtn}"/>
|
Style="{StaticResource CmdBtn}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
@@ -5,9 +5,10 @@
|
|||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:prism="http://prismlibrary.com/"
|
xmlns:prism="http://prismlibrary.com/"
|
||||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||||
|
xmlns:sys="clr-namespace:System;assembly=mscorlib"
|
||||||
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
prism:ViewModelLocator.AutoWireViewModel="False"
|
||||||
d:DesignHeight="760" d:DesignWidth="860">
|
d:DesignHeight="760" d:DesignWidth="860">
|
||||||
|
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
@@ -92,38 +93,44 @@
|
|||||||
<ComboBox Width="80" Height="32" Margin="4,0"
|
<ComboBox Width="80" Height="32" Margin="4,0"
|
||||||
SelectedItem="{Binding Channel}"
|
SelectedItem="{Binding Channel}"
|
||||||
VerticalContentAlignment="Center" FontSize="12">
|
VerticalContentAlignment="Center" FontSize="12">
|
||||||
<ComboBoxItem Content="1"/>
|
<sys:Int32>1</sys:Int32>
|
||||||
<ComboBoxItem Content="2"/>
|
<sys:Int32>2</sys:Int32>
|
||||||
<ComboBoxItem Content="3"/>
|
<sys:Int32>3</sys:Int32>
|
||||||
<ComboBoxItem Content="4"/>
|
<sys:Int32>4</sys:Int32>
|
||||||
|
<sys:Int32>5</sys:Int32>
|
||||||
|
<sys:Int32>6</sys:Int32>
|
||||||
|
<sys:Int32>7</sys:Int32>
|
||||||
|
<sys:Int32>8</sys:Int32>
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
<TextBlock Text="电压量程 (V)" Style="{StaticResource ParamLabel}"/>
|
<TextBlock Text="电压量程 (V)" Style="{StaticResource ParamLabel}"/>
|
||||||
<TextBox Style="{StaticResource NumInput}"
|
<ComboBox Width="100" Height="32" Margin="4,0"
|
||||||
Text="{Binding VoltageRange, UpdateSourceTrigger=PropertyChanged}"/>
|
ItemsSource="{Binding VoltageRangeOptions}"
|
||||||
|
SelectedItem="{Binding VoltageRange}"
|
||||||
|
VerticalContentAlignment="Center" FontSize="12"/>
|
||||||
<Button Content="设置" Command="{Binding SetVoltageRangeCommand}"
|
<Button Content="设置" Command="{Binding SetVoltageRangeCommand}"
|
||||||
Style="{StaticResource CmdBtn}"/>
|
Style="{StaticResource CmdBtn}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
<TextBlock Text="电流量程 (A)" Style="{StaticResource ParamLabel}"/>
|
<TextBlock Text="电流量程 (A)" Style="{StaticResource ParamLabel}"/>
|
||||||
<TextBox Style="{StaticResource NumInput}"
|
<ComboBox Width="100" Height="32" Margin="4,0"
|
||||||
Text="{Binding CurrentRange, UpdateSourceTrigger=PropertyChanged}"/>
|
ItemsSource="{Binding CurrentRangeOptions}"
|
||||||
|
SelectedItem="{Binding CurrentRange}"
|
||||||
|
VerticalContentAlignment="Center" FontSize="12"/>
|
||||||
<Button Content="设置" Command="{Binding SetCurrentRangeCommand}"
|
<Button Content="设置" Command="{Binding SetCurrentRangeCommand}"
|
||||||
Style="{StaticResource CmdBtn}"/>
|
Style="{StaticResource CmdBtn}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
<!--<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
<TextBlock Text="耦合模式" Style="{StaticResource ParamLabel}"/>
|
<TextBlock Text="耦合模式" Style="{StaticResource ParamLabel}"/>
|
||||||
<ComboBox Width="90" Height="32" Margin="4,0"
|
<ComboBox Width="90" Height="32" Margin="4,0"
|
||||||
SelectedItem="{Binding CouplingMode}"
|
SelectedItem="{Binding CouplingMode}"
|
||||||
|
ItemsSource="{Binding InputMeasureModeOptions}"
|
||||||
VerticalContentAlignment="Center" FontSize="12">
|
VerticalContentAlignment="Center" FontSize="12">
|
||||||
<ComboBoxItem Content="AC"/>
|
|
||||||
<ComboBoxItem Content="DC"/>
|
|
||||||
<ComboBoxItem Content="ACDC"/>
|
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
<Button Content="设置" Command="{Binding SetCouplingModeCommand}"
|
<Button Content="设置" Command="{Binding SetCouplingModeCommand}"
|
||||||
Style="{StaticResource CmdBtn}"/>
|
Style="{StaticResource CmdBtn}"/>
|
||||||
</StackPanel>
|
</StackPanel>-->
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</GroupBox>
|
</GroupBox>
|
||||||
|
|
||||||
@@ -136,8 +143,8 @@
|
|||||||
<ComboBox Width="80" Height="32" Margin="4,0"
|
<ComboBox Width="80" Height="32" Margin="4,0"
|
||||||
SelectedItem="{Binding Resolution}"
|
SelectedItem="{Binding Resolution}"
|
||||||
VerticalContentAlignment="Center" FontSize="12">
|
VerticalContentAlignment="Center" FontSize="12">
|
||||||
<ComboBoxItem Content="5"/>
|
<sys:Int32>5</sys:Int32>
|
||||||
<ComboBoxItem Content="6"/>
|
<sys:Int32>6</sys:Int32>
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
<Button Content="设置" Command="{Binding SetResolutionCommand}"
|
<Button Content="设置" Command="{Binding SetResolutionCommand}"
|
||||||
Style="{StaticResource CmdBtn}"/>
|
Style="{StaticResource CmdBtn}"/>
|
||||||
@@ -179,12 +186,12 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
<TextBlock Text="型号" Style="{StaticResource ParamLabel}"/>
|
<TextBlock Text="型号" Style="{StaticResource ParamLabel}"/>
|
||||||
<TextBox Style="{StaticResource MeasureBox}" Width="120"
|
<TextBox Style="{StaticResource MeasureBox}" Width="281"
|
||||||
Text="{Binding DeviceModel, Mode=OneWay}"/>
|
Text="{Binding DeviceModel, Mode=OneWay}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||||
<TextBlock Text="序列号" Style="{StaticResource ParamLabel}"/>
|
<TextBlock Text="序列号" Style="{StaticResource ParamLabel}"/>
|
||||||
<TextBox Style="{StaticResource MeasureBox}" Width="160"
|
<TextBox Style="{StaticResource MeasureBox}" Width="281"
|
||||||
Text="{Binding DeviceSerial, Mode=OneWay}"/>
|
Text="{Binding DeviceSerial, Mode=OneWay}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
|
using DeviceCommand.Device;
|
||||||
using Logger;
|
using Logger;
|
||||||
using Prism.Ioc;
|
using Prism.Ioc;
|
||||||
using TestingModule.ViewModels;
|
using TestingModule.ViewModels;
|
||||||
@@ -78,8 +79,26 @@ namespace MainModule.ViewModels
|
|||||||
RefreshCommand = new DelegateCommand(OnRefresh);
|
RefreshCommand = new DelegateCommand(OnRefresh);
|
||||||
BackToProtocolCommand = new DelegateCommand(OnBackToProtocol);
|
BackToProtocolCommand = new DelegateCommand(OnBackToProtocol);
|
||||||
LoadedCommand = new AsyncDelegateCommand(OnLoad);
|
LoadedCommand = new AsyncDelegateCommand(OnLoad);
|
||||||
|
_eventAggregator.GetEvent<SilenceBuzzerEvent>().Subscribe(async (scope) => await SilenceBuzzer(scope));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task SilenceBuzzer(string scope)
|
||||||
|
{
|
||||||
|
if (_deviceManager.DeviceMap.TryGetValue("IO板卡(蜂鸣器)", out var lazyIoBoard))
|
||||||
|
{
|
||||||
|
if (lazyIoBoard is IOBoard io)
|
||||||
|
{
|
||||||
|
if (scope == "default")
|
||||||
|
await io.批量写输出开关(1, 0, [false, false, false, false, false, false, false, false]);
|
||||||
|
else if (scope != TestStatus) return;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int index = _systemConfig.SharedParameterList.Where(x => x.ParameterName == "蜂鸣器").FirstOrDefault().Value;
|
||||||
|
await io.写输出开关(1, (ushort)index, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
@@ -92,6 +111,7 @@ namespace MainModule.ViewModels
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
_eventAggregator.GetEvent<SilenceBuzzerEvent>().Unsubscribe(async (scope) => await SilenceBuzzer(scope));
|
||||||
// 2. 显式释放硬件资源(防止端口占用/死锁)
|
// 2. 显式释放硬件资源(防止端口占用/死锁)
|
||||||
if (_deviceManager is IDisposable disposableDevice)
|
if (_deviceManager is IDisposable disposableDevice)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -111,26 +111,36 @@ namespace MonitorModule.ViewModels.Dialogs
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. 发现设备方法信号并加入 ValueLimitList / DeviceSingleList
|
// 2. 发现设备方法信号并加入 ValueLimitList / DeviceSingleList
|
||||||
foreach (var signal in DiscoverDeviceSignals())
|
foreach (var (displayName, fingerprint, methodName) in DiscoverDeviceSignals())
|
||||||
{
|
|
||||||
//DeviceSingleList.Add(signal);
|
|
||||||
EnsureValueLimit(signal);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 发现 CAN 信号并加入 ValueLimitList / DeviceSingleList
|
|
||||||
foreach (var (displayName, fingerprint, methodName) in DiscoverCanSignals())
|
|
||||||
{
|
{
|
||||||
//DeviceSingleList.Add(displayName);
|
//DeviceSingleList.Add(displayName);
|
||||||
_canSignalMap[displayName] = (fingerprint, methodName);
|
EnsureValueLimit(fingerprint, displayName, displayName, methodName);
|
||||||
EnsureValueLimit(displayName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. 发现 CAN 信号并加入 ValueLimitList,同时收集有效信号名
|
||||||
|
var validCanSignalNames = new HashSet<string>();
|
||||||
|
foreach (var (displayName, fingerprint, methodName) in DiscoverCanSignals())
|
||||||
|
{
|
||||||
|
_canSignalMap[displayName] = (fingerprint, methodName);
|
||||||
|
EnsureValueLimit(fingerprint, displayName, displayName, methodName);
|
||||||
|
validCanSignalNames.Add(displayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 清理已不存在于 DBC 中的 CAN 信号(Fingerprint 以 "CAN" 开头且不在有效集合中)
|
||||||
|
var staleCanSignals = ValueLimitList
|
||||||
|
.Where(x => !string.IsNullOrEmpty(x.Fingerprint) &&
|
||||||
|
x.Fingerprint.StartsWith("CAN") &&
|
||||||
|
!validCanSignalNames.Contains(x.SignalName))
|
||||||
|
.ToList();
|
||||||
|
foreach (var item in staleCanSignals)
|
||||||
|
ValueLimitList.Remove(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 发现当前作用域所有可监测的设备方法信号(与 MonitorViewModel 逻辑一致)。
|
/// 发现当前作用域所有可监测的设备方法信号(与 MonitorViewModel 逻辑一致)。
|
||||||
/// 返回 DisplayName 列表。
|
/// 返回 (DisplayName, Fingerprint, MethodName) 列表。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private IEnumerable<string> DiscoverDeviceSignals()
|
private IEnumerable<(string DisplayName, string Fingerprint, string MethodName)> DiscoverDeviceSignals()
|
||||||
{
|
{
|
||||||
if (_deviceManager?.DeviceMap == null) yield break;
|
if (_deviceManager?.DeviceMap == null) yield break;
|
||||||
|
|
||||||
@@ -159,7 +169,7 @@ namespace MonitorModule.ViewModels.Dialogs
|
|||||||
string displayName = !string.IsNullOrEmpty(attr?.Description)
|
string displayName = !string.IsNullOrEmpty(attr?.Description)
|
||||||
? $"{deviceName}.{attr.Description}"
|
? $"{deviceName}.{attr.Description}"
|
||||||
: $"{deviceName}.{method.Name}";
|
: $"{deviceName}.{method.Name}";
|
||||||
yield return displayName;
|
yield return (displayName, fingerprint, method.Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,12 +201,13 @@ namespace MonitorModule.ViewModels.Dialogs
|
|||||||
if (_systemConfig?.ConfigurationList == null) yield break;
|
if (_systemConfig?.ConfigurationList == null) yield break;
|
||||||
|
|
||||||
var msgDb = _deviceManager.CANFD.DBCParser.MsgDatabase;
|
var msgDb = _deviceManager.CANFD.DBCParser.MsgDatabase;
|
||||||
|
string canDeviceFingerprint = _deviceManager.GetCanDeviceFingerprint();
|
||||||
foreach (var cfg in _systemConfig.ConfigurationList)
|
foreach (var cfg in _systemConfig.ConfigurationList)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(cfg.SignalName)) continue;
|
if (string.IsNullOrEmpty(cfg.SignalName)) continue;
|
||||||
if (cfg.Channel < 0 || cfg.Channel >= msgDb.Count) continue;
|
if (cfg.Channel < 0 || cfg.Channel >= msgDb.Count) continue;
|
||||||
|
|
||||||
string fingerprint = CANSignalBroadcaster.BuildFingerprint((uint)cfg.Channel);
|
string fingerprint = CANSignalBroadcaster.BuildFingerprint(canDeviceFingerprint, (uint)cfg.Channel);
|
||||||
string methodName = CANSignalBroadcaster.BuildMethodName((uint)cfg.MessageID, cfg.SignalName);
|
string methodName = CANSignalBroadcaster.BuildMethodName((uint)cfg.MessageID, cfg.SignalName);
|
||||||
string displayName = CANSignalBroadcaster.BuildDisplayName(cfg.MessageName, cfg.SignalName);
|
string displayName = CANSignalBroadcaster.BuildDisplayName(cfg.MessageName, cfg.SignalName);
|
||||||
|
|
||||||
@@ -224,13 +235,16 @@ namespace MonitorModule.ViewModels.Dialogs
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 确保 ValueLimitList 中存在指定信号;不存在则添加默认值。
|
/// 确保 ValueLimitList 中存在指定信号;不存在则添加默认值。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void EnsureValueLimit(string signalName)
|
private void EnsureValueLimit(string fingerprint, string displayName, string signalName, string methodName = "")
|
||||||
{
|
{
|
||||||
if (ValueLimitList.Any(x => x.SignalName == signalName)) return;
|
if (ValueLimitList.Any(x => x.SignalName == signalName)) return;
|
||||||
|
|
||||||
ValueLimitList.Add(new ValueLimitVM
|
ValueLimitList.Add(new ValueLimitVM
|
||||||
{
|
{
|
||||||
|
DisplayName = displayName,
|
||||||
|
Fingerprint = fingerprint,
|
||||||
SignalName = signalName,
|
SignalName = signalName,
|
||||||
|
MethodName = methodName,
|
||||||
Upper = 9999,
|
Upper = 9999,
|
||||||
Lower = -9999,
|
Lower = -9999,
|
||||||
UpperExtreme = 9999,
|
UpperExtreme = 9999,
|
||||||
@@ -323,7 +337,8 @@ namespace MonitorModule.ViewModels.Dialogs
|
|||||||
var msgDb = _deviceManager.CANFD.DBCParser.MsgDatabase;
|
var msgDb = _deviceManager.CANFD.DBCParser.MsgDatabase;
|
||||||
if (channel < 0 || channel >= msgDb.Count) return;
|
if (channel < 0 || channel >= msgDb.Count) return;
|
||||||
|
|
||||||
string fingerprint = CANSignalBroadcaster.BuildFingerprint(args.Channel);
|
string canDeviceFingerprint = _deviceManager.GetCanDeviceFingerprint();
|
||||||
|
string fingerprint = CANSignalBroadcaster.BuildFingerprint(canDeviceFingerprint, args.Channel);
|
||||||
|
|
||||||
// 1. 移除该通道中已失效的 CAN 信号限制项
|
// 1. 移除该通道中已失效的 CAN 信号限制项
|
||||||
var staleSignals = _canSignalMap
|
var staleSignals = _canSignalMap
|
||||||
@@ -353,7 +368,7 @@ namespace MonitorModule.ViewModels.Dialogs
|
|||||||
_canSignalMap[displayName] = (fingerprint, methodName);
|
_canSignalMap[displayName] = (fingerprint, methodName);
|
||||||
//if (!DeviceSingleList.Contains(displayName))
|
//if (!DeviceSingleList.Contains(displayName))
|
||||||
// DeviceSingleList.Add(displayName);
|
// DeviceSingleList.Add(displayName);
|
||||||
EnsureValueLimit(displayName);
|
EnsureValueLimit(fingerprint, displayName, displayName, methodName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -366,7 +381,8 @@ namespace MonitorModule.ViewModels.Dialogs
|
|||||||
{
|
{
|
||||||
if (args.Scope != _systemConfig?.Title) return;
|
if (args.Scope != _systemConfig?.Title) return;
|
||||||
|
|
||||||
string fingerprint = CANSignalBroadcaster.BuildFingerprint(args.Channel);
|
string canDeviceFingerprint = _deviceManager.GetCanDeviceFingerprint();
|
||||||
|
string fingerprint = CANSignalBroadcaster.BuildFingerprint(canDeviceFingerprint, args.Channel);
|
||||||
var toRemove = _canSignalMap
|
var toRemove = _canSignalMap
|
||||||
.Where(kvp => kvp.Value.Fingerprint == fingerprint)
|
.Where(kvp => kvp.Value.Fingerprint == fingerprint)
|
||||||
.Select(kvp => kvp.Key)
|
.Select(kvp => kvp.Key)
|
||||||
|
|||||||
@@ -222,10 +222,8 @@ namespace MonitorModule.ViewModels
|
|||||||
|
|
||||||
if (channel == null) return;
|
if (channel == null) return;
|
||||||
|
|
||||||
double time = _stopwatch.Elapsed.TotalSeconds;
|
|
||||||
|
|
||||||
// 记录数据点(线程安全:Record 内部 ConcurrentQueue + Series 操作)
|
// 记录数据点(线程安全:Record 内部 ConcurrentQueue + Series 操作)
|
||||||
channel.Record(time, args.Value);
|
channel.Record(args.Time, args.Value);
|
||||||
|
|
||||||
// 入队数据库批量写入
|
// 入队数据库批量写入
|
||||||
// CreateTime 使用 args.Time(采样触发时刻),而不是 DateTime.Now(设备响应到达时刻),
|
// CreateTime 使用 args.Time(采样触发时刻),而不是 DateTime.Now(设备响应到达时刻),
|
||||||
@@ -252,13 +250,12 @@ namespace MonitorModule.ViewModels
|
|||||||
{
|
{
|
||||||
if (!_stopwatch.IsRunning) return;
|
if (!_stopwatch.IsRunning) return;
|
||||||
|
|
||||||
double elapsed = _stopwatch.Elapsed.TotalSeconds;
|
var now = DateTime.Now;
|
||||||
var xAxis = Plot.Axes.FirstOrDefault(a => a.Position == AxisPosition.Bottom);
|
var xAxis = Plot.Axes.FirstOrDefault(a => a.Position == AxisPosition.Bottom);
|
||||||
if (xAxis != null)
|
if (xAxis != null)
|
||||||
{
|
{
|
||||||
double window = 10000 * 0.1; // 20s 视窗
|
xAxis.Minimum = DateTimeAxis.ToDouble(now.AddSeconds(-20));
|
||||||
xAxis.Minimum = Math.Max(0, elapsed - window);
|
xAxis.Maximum = DateTimeAxis.ToDouble(now.AddSeconds(0.5));
|
||||||
xAxis.Maximum = elapsed + 0.5;
|
|
||||||
}
|
}
|
||||||
Plot.InvalidatePlot(true);
|
Plot.InvalidatePlot(true);
|
||||||
}
|
}
|
||||||
@@ -370,10 +367,11 @@ namespace MonitorModule.ViewModels
|
|||||||
PlotAreaBorderColor = OxyColors.LightGray,
|
PlotAreaBorderColor = OxyColors.LightGray,
|
||||||
Background = OxyColors.White
|
Background = OxyColors.White
|
||||||
};
|
};
|
||||||
pm.Axes.Add(new LinearAxis
|
pm.Axes.Add(new DateTimeAxis
|
||||||
{
|
{
|
||||||
Position = AxisPosition.Bottom,
|
Position = AxisPosition.Bottom,
|
||||||
Title = "时间 (s)",
|
Title = "时间",
|
||||||
|
StringFormat = "HH:mm:ss",
|
||||||
MajorGridlineStyle = LineStyle.Dot,
|
MajorGridlineStyle = LineStyle.Dot,
|
||||||
MinorGridlineStyle = LineStyle.None
|
MinorGridlineStyle = LineStyle.None
|
||||||
});
|
});
|
||||||
@@ -556,7 +554,8 @@ namespace MonitorModule.ViewModels
|
|||||||
{
|
{
|
||||||
if (_systemConfig?.ConfigurationList == null) return;
|
if (_systemConfig?.ConfigurationList == null) return;
|
||||||
|
|
||||||
string fingerprint = CANSignalBroadcaster.BuildFingerprint(channel);
|
string canDeviceFingerprint = _deviceManager?.GetCanDeviceFingerprint() ?? string.Empty;
|
||||||
|
string fingerprint = CANSignalBroadcaster.BuildFingerprint(canDeviceFingerprint, channel);
|
||||||
var configs = _systemConfig.ConfigurationList.Where(c => c.Channel == (int)channel).ToList();
|
var configs = _systemConfig.ConfigurationList.Where(c => c.Channel == (int)channel).ToList();
|
||||||
bool changed = false;
|
bool changed = false;
|
||||||
|
|
||||||
@@ -650,7 +649,8 @@ namespace MonitorModule.ViewModels
|
|||||||
{
|
{
|
||||||
if (args.Scope != TestStatus) return;
|
if (args.Scope != TestStatus) return;
|
||||||
|
|
||||||
string fingerprint = CANSignalBroadcaster.BuildFingerprint(args.Channel);
|
string canDeviceFingerprint = _deviceManager?.GetCanDeviceFingerprint() ?? string.Empty;
|
||||||
|
string fingerprint = CANSignalBroadcaster.BuildFingerprint(canDeviceFingerprint, args.Channel);
|
||||||
|
|
||||||
// 从 AvailableMethods 中移除
|
// 从 AvailableMethods 中移除
|
||||||
var toRemoveMethods = AvailableMethods.Where(m => m.Fingerprint == fingerprint).ToList();
|
var toRemoveMethods = AvailableMethods.Where(m => m.Fingerprint == fingerprint).ToList();
|
||||||
@@ -820,7 +820,7 @@ namespace MonitorModule.ViewModels
|
|||||||
var recent = channel.DataPoints.ToArray();
|
var recent = channel.DataPoints.ToArray();
|
||||||
var startIdx = Math.Max(0, recent.Length - 10000);
|
var startIdx = Math.Max(0, recent.Length - 10000);
|
||||||
for (int i = startIdx; i < recent.Length; i++)
|
for (int i = startIdx; i < recent.Length; i++)
|
||||||
channel.Series.Points.Add(new DataPoint(recent[i].Time, recent[i].DisplayValue));
|
channel.Series.Points.Add(new DataPoint(recent[i].Time.ToOADate(), recent[i].DisplayValue));
|
||||||
|
|
||||||
Plot.Series.Add(channel.Series);
|
Plot.Series.Add(channel.Series);
|
||||||
Plot.InvalidatePlot(true);
|
Plot.InvalidatePlot(true);
|
||||||
@@ -844,7 +844,7 @@ namespace MonitorModule.ViewModels
|
|||||||
channel.Series.Points.Clear();
|
channel.Series.Points.Clear();
|
||||||
var startIdx = Math.Max(0, points.Length - 10000);
|
var startIdx = Math.Max(0, points.Length - 10000);
|
||||||
for (int i = startIdx; i < points.Length; i++)
|
for (int i = startIdx; i < points.Length; i++)
|
||||||
channel.Series.Points.Add(new DataPoint(points[i].Time, points[i].DisplayValue));
|
channel.Series.Points.Add(new DataPoint(points[i].Time.ToOADate(), points[i].DisplayValue));
|
||||||
Plot.InvalidatePlot(true);
|
Plot.InvalidatePlot(true);
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -127,26 +127,37 @@ namespace TestingModule.ViewModels
|
|||||||
if (SelectedDevice == null) return;
|
if (SelectedDevice == null) return;
|
||||||
var type = SelectedDevice.DeviceType.Split('.').Last();
|
var type = SelectedDevice.DeviceType.Split('.').Last();
|
||||||
var viewName = type + "View";
|
var viewName = type + "View";
|
||||||
|
var vmName = type + "ViewModel"; // 按照命名规范拼接出对应的 VM 类型名
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 1. 先确保弹窗管理器窗口已打开(首次 Show,后续只追加 Tab)
|
// 1. 先确保弹窗管理器窗口已打开
|
||||||
if (_dialogWindow == null || !_dialogWindow.IsVisible)
|
if (_dialogWindow == null || !_dialogWindow.IsVisible)
|
||||||
{
|
{
|
||||||
_dialogService.Show("DialogMangerView");
|
_dialogService.Show("DialogMangerView");
|
||||||
|
|
||||||
// 找到刚刚被 DialogService 打开的窗口(按 DataContext 类型名匹配)
|
|
||||||
_dialogWindow = System.Windows.Application.Current.Windows
|
_dialogWindow = System.Windows.Application.Current.Windows
|
||||||
.OfType<System.Windows.Window>()
|
.OfType<System.Windows.Window>()
|
||||||
.FirstOrDefault(w => w.DataContext?.GetType().Name == "DialogMangerViewModel");
|
.FirstOrDefault(w => w.DataContext?.GetType().Name == "DialogMangerViewModel");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 从容器按注册名解析设备编辑 View
|
// 2. 从当前专属容器解析设备编辑 View 实例
|
||||||
var view = _containerProvider.Resolve<object>(viewName) as System.Windows.FrameworkElement;
|
var view = _containerProvider.Resolve<object>(viewName) as System.Windows.FrameworkElement;
|
||||||
if (view == null) return;
|
if (view == null) return;
|
||||||
|
|
||||||
// 3. 通过反射调用 ViewModel 上的 Initialize(deviceName) 方法,
|
// 3. 关键:从同一个台架的专属容器中解析出该设备对应的 ViewModel 实例
|
||||||
// 避免 TestingModule 直接引用 DeviceEditModule 的类型
|
// 从而实现:不同的台架(不同的专属 _containerProvider)解析出各自独立的 VM 实例
|
||||||
|
var vmType = Assembly.Load("DeviceEditModule").GetType($"DeviceEditModule.ViewModels.{vmName}");
|
||||||
|
if (vmType != null)
|
||||||
|
{
|
||||||
|
var scopedViewModel = _containerProvider.Resolve(vmType);
|
||||||
|
if (scopedViewModel != null)
|
||||||
|
{
|
||||||
|
// 手动绑定 DataContext,将其锁死在当前作用域内
|
||||||
|
view.DataContext = scopedViewModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 调用 ViewModel 上的 Initialize 方法
|
||||||
var vm = view.DataContext;
|
var vm = view.DataContext;
|
||||||
if (vm != null)
|
if (vm != null)
|
||||||
{
|
{
|
||||||
@@ -154,18 +165,16 @@ namespace TestingModule.ViewModels
|
|||||||
initMethod?.Invoke(vm, new object[] { SelectedDevice.DeviceName });
|
initMethod?.Invoke(vm, new object[] { SelectedDevice.DeviceName });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 发布事件 → DialogMangerViewModel 接收后将此 View 添加为 Tab
|
// 5. 发布事件,将绑定好独立 VM 的 View 实例作为 Tab 载入
|
||||||
// 标题格式:{当前作用域} - {设备名称}
|
|
||||||
// 去重依据:设备硬件指纹(同一物理设备不重复打开)
|
|
||||||
var fingerprint = DeviceManager.ExtractHardwareFingerprint(SelectedDevice);
|
var fingerprint = DeviceManager.ExtractHardwareFingerprint(SelectedDevice);
|
||||||
_eventAggregator.GetEvent<AddDialogTabEvent>().Publish(new DialogTabInfo
|
_eventAggregator.GetEvent<AddDialogTabEvent>().Publish(new DialogTabInfo
|
||||||
{
|
{
|
||||||
Title = $"{_globalInfo.CurrentScope} - {SelectedDevice.DeviceName}",
|
Title = $"{_globalInfo.CurrentScope} - {SelectedDevice.DeviceName}",
|
||||||
Fingerprint = fingerprint,
|
Fingerprint = fingerprint,
|
||||||
Content = view
|
Content = view
|
||||||
});
|
});
|
||||||
|
|
||||||
// 5. 将窗口置顶(确保用户看到新增的 Tab)
|
// 6. 窗口置顶
|
||||||
if (_dialogWindow != null && _dialogWindow.IsVisible)
|
if (_dialogWindow != null && _dialogWindow.IsVisible)
|
||||||
{
|
{
|
||||||
if (_dialogWindow.WindowState == System.Windows.WindowState.Minimized)
|
if (_dialogWindow.WindowState == System.Windows.WindowState.Minimized)
|
||||||
@@ -187,7 +196,7 @@ namespace TestingModule.ViewModels
|
|||||||
|
|
||||||
private void ParameterEdit()
|
private void ParameterEdit()
|
||||||
{
|
{
|
||||||
if (!_globalInfo.IsAdmin) return;
|
if (!_globalInfo.IsAdmin||!SelectedParameter.IsEditable) return;
|
||||||
var param = new DialogParameters
|
var param = new DialogParameters
|
||||||
{
|
{
|
||||||
{ "Mode",SelectedParameter==null?"ADD":"Edit" },
|
{ "Mode",SelectedParameter==null?"ADD":"Edit" },
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ using System;
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.ComponentModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -79,6 +81,10 @@ namespace TestingModule.ViewModels
|
|||||||
private readonly GlobalInfo _globalInfo;
|
private readonly GlobalInfo _globalInfo;
|
||||||
private List<StepVM> tmpCopyList = new List<StepVM>();
|
private List<StepVM> tmpCopyList = new List<StepVM>();
|
||||||
|
|
||||||
|
// 追踪当前订阅的集合实例,用于在集合被整体替换时重新订阅
|
||||||
|
private ObservableCollection<StepVM> _trackedStepCollection;
|
||||||
|
private ObservableCollection<StepVM> _trackedErrorStepCollection;
|
||||||
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
#region 命令
|
#region 命令
|
||||||
@@ -101,13 +107,9 @@ namespace TestingModule.ViewModels
|
|||||||
DeleteStepCommand = new DelegateCommand(DeleteStep);
|
DeleteStepCommand = new DelegateCommand(DeleteStep);
|
||||||
TabSelectionChangedCommand = new DelegateCommand<string>(TabSelectionChanged);
|
TabSelectionChangedCommand = new DelegateCommand<string>(TabSelectionChanged);
|
||||||
SelectionChangedCommand = new DelegateCommand<object>(SelectionChanged);
|
SelectionChangedCommand = new DelegateCommand<object>(SelectionChanged);
|
||||||
Program.StepCollection.CollectionChanged += StepCollection_CollectionChanged;
|
SubscribeStepCollections();
|
||||||
Program.ErrorStepCollection.CollectionChanged += StepCollection_CollectionChanged;
|
Program.PropertyChanged += Program_PropertyChanged;
|
||||||
Admin = _globalInfo.IsAdmin;
|
Admin = _globalInfo.IsAdmin;
|
||||||
_eventAggregator.GetEvent<AlarmEvent>().Subscribe(() =>
|
|
||||||
{
|
|
||||||
SelectedTabIndex = 1;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SelectionChanged(object parameter)
|
private void SelectionChanged(object parameter)
|
||||||
@@ -206,13 +208,67 @@ namespace TestingModule.ViewModels
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region 辅助方法
|
#region 辅助方法
|
||||||
private void StepCollection_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订阅当前 Program 的 StepCollection 和 ErrorStepCollection 的 CollectionChanged 事件。
|
||||||
|
/// </summary>
|
||||||
|
private void SubscribeStepCollections()
|
||||||
|
{
|
||||||
|
_trackedStepCollection = Program.StepCollection;
|
||||||
|
_trackedErrorStepCollection = Program.ErrorStepCollection;
|
||||||
|
|
||||||
|
if (_trackedStepCollection != null)
|
||||||
|
_trackedStepCollection.CollectionChanged += StepCollection_CollectionChanged;
|
||||||
|
if (_trackedErrorStepCollection != null)
|
||||||
|
_trackedErrorStepCollection.CollectionChanged += StepCollection_CollectionChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 取消订阅当前追踪的集合事件。
|
||||||
|
/// </summary>
|
||||||
|
private void UnsubscribeStepCollections()
|
||||||
|
{
|
||||||
|
if (_trackedStepCollection != null)
|
||||||
|
{
|
||||||
|
_trackedStepCollection.CollectionChanged -= StepCollection_CollectionChanged;
|
||||||
|
_trackedStepCollection = null;
|
||||||
|
}
|
||||||
|
if (_trackedErrorStepCollection != null)
|
||||||
|
{
|
||||||
|
_trackedErrorStepCollection.CollectionChanged -= StepCollection_CollectionChanged;
|
||||||
|
_trackedErrorStepCollection = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当 Program 的 StepCollection / ErrorStepCollection 被整体替换时,自动重新订阅并刷新序号。
|
||||||
|
/// </summary>
|
||||||
|
private void Program_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(ProgramVM.StepCollection) ||
|
||||||
|
e.PropertyName == nameof(ProgramVM.ErrorStepCollection))
|
||||||
|
{
|
||||||
|
UnsubscribeStepCollections();
|
||||||
|
SubscribeStepCollections();
|
||||||
|
|
||||||
|
// 集合被整体替换后,对新集合重新编号
|
||||||
|
Application.Current?.Dispatcher.BeginInvoke(new Action(() =>
|
||||||
|
{
|
||||||
|
for (int i = 0; i < Program.StepCollection.Count; i++)
|
||||||
|
Program.StepCollection[i].Index = i + 1;
|
||||||
|
for (int i = 0; i < Program.ErrorStepCollection.Count; i++)
|
||||||
|
Program.ErrorStepCollection[i].Index = i + 1;
|
||||||
|
}), System.Windows.Threading.DispatcherPriority.Background);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StepCollection_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
var collection = sender as ObservableCollection<StepVM>;
|
var collection = sender as ObservableCollection<StepVM>;
|
||||||
// Add/Move/Remove 都会触发,这里判断具体情形
|
// Add/Move/Remove 都会触发,这里判断具体情形
|
||||||
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add ||
|
if (e.Action == NotifyCollectionChangedAction.Add ||
|
||||||
e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Move||
|
e.Action == NotifyCollectionChangedAction.Move||
|
||||||
e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove)
|
e.Action == NotifyCollectionChangedAction.Remove)
|
||||||
{
|
{
|
||||||
// 如果需要等 UI 更新完再处理,可也用 Dispatcher 延迟一小段时间
|
// 如果需要等 UI 更新完再处理,可也用 Dispatcher 延迟一小段时间
|
||||||
Application.Current?.Dispatcher.BeginInvoke(new Action(() =>
|
Application.Current?.Dispatcher.BeginInvoke(new Action(() =>
|
||||||
@@ -238,31 +294,27 @@ namespace TestingModule.ViewModels
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 1. 【核心修复】必须取消订阅 CollectionChanged 事件,否则 VM 永远无法被释放
|
// 1. 取消订阅 CollectionChanged 事件(从追踪的实例上取消,而非可能已被替换的当前实例)
|
||||||
|
UnsubscribeStepCollections();
|
||||||
|
|
||||||
|
// 2. 取消订阅 Program.PropertyChanged
|
||||||
if (Program != null)
|
if (Program != null)
|
||||||
{
|
{
|
||||||
if (Program.StepCollection != null)
|
Program.PropertyChanged -= Program_PropertyChanged;
|
||||||
{
|
|
||||||
Program.StepCollection.CollectionChanged -= StepCollection_CollectionChanged;
|
|
||||||
}
|
|
||||||
if (Program.ErrorStepCollection != null)
|
|
||||||
{
|
|
||||||
Program.ErrorStepCollection.CollectionChanged -= StepCollection_CollectionChanged;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 【核心修复】必须显式退订 Prism 全局事件(AlarmEvent)
|
// 3. 【核心修复】必须显式退订 Prism 全局事件(AlarmEvent)
|
||||||
// 注意:因为订阅时使用的是匿名 Lambda,最安全稳妥的退订方式是把整个事件上的当前 VM 订阅者全部注销
|
// 注意:因为订阅时使用的是匿名 Lambda,最安全稳妥的退订方式是把整个事件上的当前 VM 订阅者全部注销
|
||||||
_eventAggregator?.GetEvent<AlarmEvent>()?.Unsubscribe(null);
|
_eventAggregator?.GetEvent<AlarmEvent>()?.Unsubscribe(null);
|
||||||
|
|
||||||
// 3. 清空临时缓存集合与 UI 绑定列表,避免悬挂指针
|
// 4. 清空临时缓存集合与 UI 绑定列表,避免悬挂指针
|
||||||
tmpCopyList?.Clear();
|
tmpCopyList?.Clear();
|
||||||
tmpCopyList = null!;
|
tmpCopyList = null!;
|
||||||
|
|
||||||
SelectedItems?.Clear();
|
SelectedItems?.Clear();
|
||||||
SelectedItems = null!;
|
SelectedItems = null!;
|
||||||
|
|
||||||
// 4. 清除选中项状态引用
|
// 5. 清除选中项状态引用
|
||||||
SelectedStep = null;
|
SelectedStep = null;
|
||||||
if (_ScopedContext != null)
|
if (_ScopedContext != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ namespace UIShare.GlobalVariable
|
|||||||
{
|
{
|
||||||
if (_disposed) return;
|
if (_disposed) return;
|
||||||
|
|
||||||
string signalFingerprint = BuildFingerprint(channel);
|
string signalFingerprint = BuildFingerprint(canFingerprint, channel);
|
||||||
var now = DateTime.Now;
|
var now = DateTime.Now;
|
||||||
|
|
||||||
// 获取引用该 CAN 设备的所有作用域
|
// 获取引用该 CAN 设备的所有作用域
|
||||||
@@ -96,7 +96,11 @@ namespace UIShare.GlobalVariable
|
|||||||
Time = now
|
Time = now
|
||||||
});
|
});
|
||||||
|
|
||||||
ValueLimitAlarmHelper.CheckAlarm(scope, signalFingerprint, methodName, physicalValue, _globalInfo);
|
string MonitorSatus = ValueLimitAlarmHelper.CheckAlarm(scope, signalFingerprint, methodName, physicalValue, _globalInfo);
|
||||||
|
if(MonitorSatus!=""|| MonitorSatus != "未报警")
|
||||||
|
{
|
||||||
|
//_eventAggregator.GetEvent<AlarmEvent>().Publish(canFingerprint, MonitorSatus);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,11 +133,12 @@ namespace UIShare.GlobalVariable
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 生成 CAN 信号的 Fingerprint 格式:"CAN:{Channel}"
|
/// 生成 CAN 信号的 Fingerprint 格式:"{canDeviceFingerprint}:{channel}"
|
||||||
|
/// canDeviceFingerprint 来自 DeviceManager.ExtractHardwareFingerprint,如 "CAN:0"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string BuildFingerprint(uint channel)
|
public static string BuildFingerprint(string canDeviceFingerprint, uint channel)
|
||||||
{
|
{
|
||||||
return $"CAN:{channel}";
|
return $"{canDeviceFingerprint}:{channel}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|||||||
@@ -71,6 +71,20 @@ namespace UIShare.GlobalVariable
|
|||||||
|
|
||||||
return string.Empty;
|
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()
|
private void InitDevices()
|
||||||
{
|
{
|
||||||
DeviceMap = new Dictionary<string, IBaseInterface>(StringComparer.OrdinalIgnoreCase);
|
DeviceMap = new Dictionary<string, IBaseInterface>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|||||||
@@ -181,7 +181,11 @@ namespace UIShare.GlobalVariable
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 同步检查该作用域 ValueLimitList 是否超限
|
// 同步检查该作用域 ValueLimitList 是否超限
|
||||||
ValueLimitAlarmHelper.CheckAlarm(scope, entry.Fingerprint, entry.MethodName, value, _globalInfo);
|
string MonitorSatus= ValueLimitAlarmHelper.CheckAlarm(scope, entry.Fingerprint, entry.MethodName, value, _globalInfo);
|
||||||
|
if (MonitorSatus != "" || MonitorSatus != "未报警")
|
||||||
|
{
|
||||||
|
// _eventAggregator.GetEvent<AlarmEvent>().Publish(entry.Fingerprint, MonitorSatus);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|||||||
@@ -47,15 +47,40 @@ namespace UIShare.GlobalVariable
|
|||||||
{
|
{
|
||||||
Category = ParameterCategory.Input,
|
Category = ParameterCategory.Input,
|
||||||
Type = typeof(int),
|
Type = typeof(int),
|
||||||
Name = "继电器占位1",
|
Name = "单相充电",
|
||||||
Value = 0,
|
Value = 0,
|
||||||
|
IsEditable=false
|
||||||
},
|
},
|
||||||
new ParameterVM
|
new ParameterVM
|
||||||
{
|
{
|
||||||
Category = ParameterCategory.Input,
|
Category = ParameterCategory.Input,
|
||||||
Type = typeof(int),
|
Type = typeof(int),
|
||||||
Name = "继电器占位2",
|
Name = "抛负载",
|
||||||
Value = 1,
|
Value = 1,
|
||||||
|
IsEditable=false
|
||||||
|
},
|
||||||
|
new ParameterVM
|
||||||
|
{
|
||||||
|
Category = ParameterCategory.Input,
|
||||||
|
Type = typeof(int),
|
||||||
|
Name = "短路测试",
|
||||||
|
Value = 0,
|
||||||
|
IsEditable=false
|
||||||
|
}, new ParameterVM
|
||||||
|
{
|
||||||
|
Category = ParameterCategory.Input,
|
||||||
|
Type = typeof(int),
|
||||||
|
Name = "初始化供电",
|
||||||
|
Value = 0,
|
||||||
|
IsEditable=false
|
||||||
|
},
|
||||||
|
new ParameterVM
|
||||||
|
{
|
||||||
|
Category = ParameterCategory.Input,
|
||||||
|
Type = typeof(int),
|
||||||
|
Name = "蜂鸣器",
|
||||||
|
Value = 1,
|
||||||
|
IsEditable=false
|
||||||
},
|
},
|
||||||
new ParameterVM
|
new ParameterVM
|
||||||
{
|
{
|
||||||
@@ -63,6 +88,7 @@ namespace UIShare.GlobalVariable
|
|||||||
Type = typeof(int),
|
Type = typeof(int),
|
||||||
Name = "CAN通道",
|
Name = "CAN通道",
|
||||||
Value = 0,
|
Value = 0,
|
||||||
|
IsEditable=false
|
||||||
},
|
},
|
||||||
new ParameterVM
|
new ParameterVM
|
||||||
{
|
{
|
||||||
@@ -70,6 +96,7 @@ namespace UIShare.GlobalVariable
|
|||||||
Type = typeof(int),
|
Type = typeof(int),
|
||||||
Name = "示波器通道",
|
Name = "示波器通道",
|
||||||
Value = 0,
|
Value = 0,
|
||||||
|
IsEditable=false
|
||||||
},
|
},
|
||||||
new ParameterVM
|
new ParameterVM
|
||||||
{
|
{
|
||||||
@@ -77,6 +104,7 @@ namespace UIShare.GlobalVariable
|
|||||||
Type = typeof(int),
|
Type = typeof(int),
|
||||||
Name = "功率分析仪通道1",
|
Name = "功率分析仪通道1",
|
||||||
Value = 0,
|
Value = 0,
|
||||||
|
IsEditable=false
|
||||||
},
|
},
|
||||||
new ParameterVM
|
new ParameterVM
|
||||||
{
|
{
|
||||||
@@ -84,6 +112,7 @@ namespace UIShare.GlobalVariable
|
|||||||
Type = typeof(int),
|
Type = typeof(int),
|
||||||
Name = "功率分析仪通道2",
|
Name = "功率分析仪通道2",
|
||||||
Value = 0,
|
Value = 0,
|
||||||
|
IsEditable=false
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
// public ObservableCollection<DeviceInfoVM> DeviceList { get; set; } = new()
|
// public ObservableCollection<DeviceInfoVM> DeviceList { get; set; } = new()
|
||||||
|
|||||||
@@ -16,26 +16,16 @@ namespace UIShare.GlobalVariable
|
|||||||
/// <param name="methodName">方法名/信号标识</param>
|
/// <param name="methodName">方法名/信号标识</param>
|
||||||
/// <param name="value">当前采样值</param>
|
/// <param name="value">当前采样值</param>
|
||||||
/// <param name="globalInfo">全局信息</param>
|
/// <param name="globalInfo">全局信息</param>
|
||||||
public static void CheckAlarm(string scope, string fingerprint, string methodName, double value, GlobalInfo globalInfo)
|
public static string CheckAlarm(string scope, string fingerprint, string methodName, double value, GlobalInfo globalInfo)
|
||||||
{
|
{
|
||||||
if (globalInfo?.ConfigDic == null) return;
|
if (globalInfo?.ConfigDic == null) return "";
|
||||||
if (!globalInfo.ConfigDic.TryGetValue(scope, out var systemConfig)) return;
|
if (!globalInfo.ConfigDic.TryGetValue(scope, out var systemConfig)) return "";
|
||||||
if (systemConfig.ValueLimitList == null) return;
|
if (systemConfig.ValueLimitList == null) return "";
|
||||||
|
|
||||||
var limit = systemConfig.ValueLimitList.FirstOrDefault(x =>
|
var limit = systemConfig.ValueLimitList.FirstOrDefault(x =>
|
||||||
x.Fingerprint == fingerprint && x.MethodName == methodName);
|
x.Fingerprint == fingerprint && x.MethodName == methodName);
|
||||||
if (limit == null) return;
|
if (limit == null) return "";
|
||||||
if (value > limit.Upper)
|
if (value > limit.UpperExtreme)
|
||||||
{
|
|
||||||
limit.IsAlarm = true;
|
|
||||||
limit.AlarmSatus = AlarmStatus.超上限;
|
|
||||||
}
|
|
||||||
else if (value < limit.Lower)
|
|
||||||
{
|
|
||||||
limit.IsAlarm = true;
|
|
||||||
limit.AlarmSatus = AlarmStatus.超下限;
|
|
||||||
}
|
|
||||||
else if(value > limit.UpperExtreme)
|
|
||||||
{
|
{
|
||||||
limit.IsAlarm = true;
|
limit.IsAlarm = true;
|
||||||
limit.AlarmSatus = AlarmStatus.超上极限;
|
limit.AlarmSatus = AlarmStatus.超上极限;
|
||||||
@@ -45,12 +35,22 @@ namespace UIShare.GlobalVariable
|
|||||||
limit.IsAlarm = true;
|
limit.IsAlarm = true;
|
||||||
limit.AlarmSatus = AlarmStatus.超下极限;
|
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
|
else
|
||||||
{
|
{
|
||||||
limit.IsAlarm = false;
|
limit.IsAlarm = false;
|
||||||
limit.AlarmSatus = AlarmStatus.未报警;
|
limit.AlarmSatus = AlarmStatus.未报警;
|
||||||
}
|
}
|
||||||
|
return limit.AlarmSatus.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace UIShare.PubEvent
|
namespace UIShare.PubEvent
|
||||||
{
|
{
|
||||||
public class AlarmEvent :PubSubEvent
|
public class AlarmEvent :PubSubEvent<Tuple<string,string>>
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,5 +10,6 @@ namespace UIShare.UIViewModel
|
|||||||
public string SignalName { get; set; }
|
public string SignalName { get; set; }
|
||||||
public int CollectionInterval { get; set; }
|
public int CollectionInterval { get; set; }
|
||||||
public Guid CollectionID { get; set; }
|
public Guid CollectionID { get; set; }
|
||||||
|
public string MethodName {get;set;}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using OxyPlot.Series;
|
using OxyPlot.Series;
|
||||||
using OxyPlot;
|
using OxyPlot;
|
||||||
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using Prism.Mvvm;
|
using Prism.Mvvm;
|
||||||
using NCalc;
|
using NCalc;
|
||||||
@@ -52,7 +53,7 @@ namespace UIShare.UIViewModel
|
|||||||
|
|
||||||
// ===== 数据存储 =====
|
// ===== 数据存储 =====
|
||||||
/// <summary>所有采样数据点(线程安全),与 OxyPlot 无关</summary>
|
/// <summary>所有采样数据点(线程安全),与 OxyPlot 无关</summary>
|
||||||
public ConcurrentQueue<(double Time, double RawValue, double DisplayValue)> DataPoints { get; } = new();
|
public ConcurrentQueue<(DateTime Time, double RawValue, double DisplayValue)> DataPoints { get; } = new();
|
||||||
|
|
||||||
/// <summary>最大缓冲数据点数,超出则丢弃最旧的</summary>
|
/// <summary>最大缓冲数据点数,超出则丢弃最旧的</summary>
|
||||||
public int MaxBuffer { get; set; } = 5000;
|
public int MaxBuffer { get; set; } = 5000;
|
||||||
@@ -67,7 +68,7 @@ namespace UIShare.UIViewModel
|
|||||||
|
|
||||||
// ===== 辅助方法 =====
|
// ===== 辅助方法 =====
|
||||||
/// <summary>记录一个数据点:RawValue 经数学变换后得到 DisplayValue,一并入队</summary>
|
/// <summary>记录一个数据点:RawValue 经数学变换后得到 DisplayValue,一并入队</summary>
|
||||||
public void Record(double time, double rawValue)
|
public void Record(DateTime time, double rawValue)
|
||||||
{
|
{
|
||||||
double displayValue = rawValue;
|
double displayValue = rawValue;
|
||||||
|
|
||||||
@@ -82,8 +83,8 @@ namespace UIShare.UIViewModel
|
|||||||
// 如果正在显示,同步到 Series
|
// 如果正在显示,同步到 Series
|
||||||
if (_series != null)
|
if (_series != null)
|
||||||
{
|
{
|
||||||
_series.Points.Add(new DataPoint(time, displayValue));
|
_series.Points.Add(new DataPoint(time.ToOADate(), displayValue));
|
||||||
while (_series.Points.Count > 200)
|
while (_series.Points.Count > 10000)
|
||||||
{
|
{
|
||||||
_series.Points.RemoveAt(0);
|
_series.Points.RemoveAt(0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ namespace UIShare.UIViewModel
|
|||||||
get => _isVisible;
|
get => _isVisible;
|
||||||
set => SetProperty(ref _isVisible, value);
|
set => SetProperty(ref _isVisible, value);
|
||||||
}
|
}
|
||||||
|
private bool _isEditable = true;
|
||||||
|
public bool IsEditable
|
||||||
|
{
|
||||||
|
get => _isEditable;
|
||||||
|
set => SetProperty(ref _isEditable, value);
|
||||||
|
}
|
||||||
|
|
||||||
private string _name;
|
private string _name;
|
||||||
public string Name
|
public string Name
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ namespace UIShare.UIViewModel
|
|||||||
private string _signalName = string.Empty;
|
private string _signalName = string.Empty;
|
||||||
private string _fingerprint = string.Empty;
|
private string _fingerprint = string.Empty;
|
||||||
private string _methodName = string.Empty;
|
private string _methodName = string.Empty;
|
||||||
|
private string _displayName = string.Empty;
|
||||||
private double _upper;
|
private double _upper;
|
||||||
private double _lower;
|
private double _lower;
|
||||||
private double _upperExtreme;
|
private double _upperExtreme;
|
||||||
@@ -23,6 +24,14 @@ namespace UIShare.UIViewModel
|
|||||||
get => _signalName;
|
get => _signalName;
|
||||||
set => SetProperty(ref _signalName, value);
|
set => SetProperty(ref _signalName, value);
|
||||||
}
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// 信号名(显示用)
|
||||||
|
/// </summary>
|
||||||
|
public string DisplayName
|
||||||
|
{
|
||||||
|
get => _displayName;
|
||||||
|
set => SetProperty(ref _displayName, value);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 硬件指纹(物理设备唯一标识,用于超限匹配)
|
/// 硬件指纹(物理设备唯一标识,用于超限匹配)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Common.Attributes;
|
using Common.Attributes;
|
||||||
|
using Logger;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -24,10 +25,15 @@ namespace ZLGUSBCANFD
|
|||||||
|
|
||||||
// 异步高性能接收线程控制
|
// 异步高性能接收线程控制
|
||||||
private volatile bool _isRunning = false;
|
private volatile bool _isRunning = false;
|
||||||
|
private volatile bool _isClosing = false; // 关闭流程标志:通知循环发送任务尽快退出
|
||||||
private readonly List<Thread> _receiveThreads = new List<Thread>();
|
private readonly List<Thread> _receiveThreads = new List<Thread>();
|
||||||
|
|
||||||
// DBC 循环发送任务管理:Key = (通道号, 帧ID)
|
// DBC 循环发送任务管理:Key = (通道号, 帧ID),Value = (CTS, Task)
|
||||||
private readonly ConcurrentDictionary<(uint 通道号, uint 帧ID), CancellationTokenSource> _cyclicSenders = new ConcurrentDictionary<(uint, uint), CancellationTokenSource>();
|
private readonly ConcurrentDictionary<(uint 通道号, uint 帧ID), (CancellationTokenSource Cts, Task SendTask)> _cyclicSenders = new ConcurrentDictionary<(uint, uint), (CancellationTokenSource, Task)>();
|
||||||
|
|
||||||
|
// 信号值持久化覆盖表:Key = (通道号, 帧ID),Value = 信号名 → 物理值
|
||||||
|
// 每次 设置报文 会累积写入此表,发送时以 DBC 初始值为底再叠加此表覆盖值
|
||||||
|
private readonly ConcurrentDictionary<(uint 通道号, uint 帧ID), Dictionary<string, double>> _signalOverrides = new ConcurrentDictionary<(uint, uint), Dictionary<string, double>>();
|
||||||
|
|
||||||
// 动态硬件参数
|
// 动态硬件参数
|
||||||
private readonly uint _deviceType; // 76: USBCANFD-400U
|
private readonly uint _deviceType; // 76: USBCANFD-400U
|
||||||
@@ -93,15 +99,17 @@ namespace ZLGUSBCANFD
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual bool 初始化并启动通道(uint 通道号)
|
public virtual bool 初始化并启动通道(uint 通道号)
|
||||||
{
|
{
|
||||||
|
_isClosing = false; // 重置关闭标志,允许循环发送
|
||||||
if (_deviceHandle == IntPtr.Zero) throw new InvalidOperationException("请先调用 '打开设备()' 才能初始化通道。");
|
if (_deviceHandle == IntPtr.Zero) throw new InvalidOperationException("请先调用 '打开设备()' 才能初始化通道。");
|
||||||
if (通道号 >= _maxChannels) return false;
|
if (通道号 >= _maxChannels) return false;
|
||||||
|
|
||||||
lock (_channelLocks[通道号])
|
lock (_channelLocks[通道号])
|
||||||
{
|
{
|
||||||
// 如果已经启动过,先复位
|
// 如果已经启动过,直接退出
|
||||||
if (_channelHandles[通道号] != IntPtr.Zero)
|
if (_channelHandles[通道号] != IntPtr.Zero)
|
||||||
{
|
{
|
||||||
ZLGCAN.ZCAN_ResetCAN(_channelHandles[通道号]);
|
return true;
|
||||||
|
//ZLGCAN.ZCAN_ResetCAN(_channelHandles[通道号]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. 设置该通道专属的仲裁域与数据域波特率
|
// 1. 设置该通道专属的仲裁域与数据域波特率
|
||||||
@@ -145,19 +153,44 @@ namespace ZLGUSBCANFD
|
|||||||
|
|
||||||
public virtual void 关闭CAN卡设备()
|
public virtual void 关闭CAN卡设备()
|
||||||
{
|
{
|
||||||
_isRunning = false;
|
_isClosing = true; // 通知循环发送任务尽快退出
|
||||||
停止所有循环发送();
|
_isRunning = false; // 通知接收轮询线程退出
|
||||||
Thread.Sleep(50); // 确保轮询线程安全退出
|
停止所有循环发送(); // 内部会等待所有循环发送 Task 退出
|
||||||
|
|
||||||
|
// 等待所有接收轮询线程真正退出(每个最多 1 秒)
|
||||||
|
foreach (var thread in _receiveThreads)
|
||||||
|
{
|
||||||
|
if (thread.IsAlive)
|
||||||
|
thread.Join(1000);
|
||||||
|
}
|
||||||
_receiveThreads.Clear();
|
_receiveThreads.Clear();
|
||||||
|
|
||||||
// 动态复位所有通道并释放DBC
|
// 动态复位所有通道并释放 DBC(使用 TryEnter 防止死锁)
|
||||||
for (uint i = 0; i < _maxChannels; i++)
|
for (uint i = 0; i < _maxChannels; i++)
|
||||||
{
|
{
|
||||||
lock (_channelLocks[i])
|
if (Monitor.TryEnter(_channelLocks[i], TimeSpan.FromSeconds(2)))
|
||||||
{
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_channelHandles[i] != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
ZLGCAN.ZCAN_ResetCAN(_channelHandles[i]);
|
||||||
|
_channelHandles[i] = IntPtr.Zero;
|
||||||
|
}
|
||||||
|
释放通道DBC(i);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Monitor.Exit(_channelLocks[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 锁超时:循环发送 Task 可能仍阻塞在原生 API 调用中,强制清理
|
||||||
|
LoggerHelper.Info($"[ZLGCANFD] 关闭通道 {i} 时获取锁超时,强制清理");
|
||||||
if (_channelHandles[i] != IntPtr.Zero)
|
if (_channelHandles[i] != IntPtr.Zero)
|
||||||
{
|
{
|
||||||
ZLGCAN.ZCAN_ResetCAN(_channelHandles[i]);
|
try { ZLGCAN.ZCAN_ResetCAN(_channelHandles[i]); } catch { }
|
||||||
_channelHandles[i] = IntPtr.Zero;
|
_channelHandles[i] = IntPtr.Zero;
|
||||||
}
|
}
|
||||||
释放通道DBC(i);
|
释放通道DBC(i);
|
||||||
@@ -170,6 +203,8 @@ namespace ZLGUSBCANFD
|
|||||||
ZLGCAN.ZCAN_CloseDevice(_deviceHandle);
|
ZLGCAN.ZCAN_CloseDevice(_deviceHandle);
|
||||||
_deviceHandle = IntPtr.Zero;
|
_deviceHandle = IntPtr.Zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_isClosing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -349,17 +384,16 @@ namespace ZLGUSBCANFD
|
|||||||
停止循环发送(通道号, 帧ID);
|
停止循环发送(通道号, 帧ID);
|
||||||
|
|
||||||
var cts = new CancellationTokenSource();
|
var cts = new CancellationTokenSource();
|
||||||
_cyclicSenders[(通道号, 帧ID)] = cts;
|
var sendTask = Task.Run(async () =>
|
||||||
|
|
||||||
Task.Run(async () =>
|
|
||||||
{
|
{
|
||||||
while (!cts.Token.IsCancellationRequested)
|
while (!cts.Token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
|
if (_isClosing) break; // 关闭流程中立即退出
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
lock (_channelLocks[通道号])
|
lock (_channelLocks[通道号])
|
||||||
{
|
{
|
||||||
if (_channelHandles[通道号] == IntPtr.Zero) break;
|
if (_channelHandles[通道号] == IntPtr.Zero || _isClosing) break;
|
||||||
发送DBC定义报文单次(通道号, 帧ID, 信号物理值字典, 是否使用CANFD);
|
发送DBC定义报文单次(通道号, 帧ID, 信号物理值字典, 是否使用CANFD);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,16 +405,21 @@ namespace ZLGUSBCANFD
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// 循环发送出错时退出,避免刷屏
|
Logger.LoggerHelper.Error($"[ZLGCANFD] 循环发送 DBC 报文失败: {ex.Message}");
|
||||||
Console.WriteLine($"[ZLGCANFD] 循环发送 DBC 报文失败: {ex.Message}");
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_cyclicSenders.TryRemove((通道号, 帧ID), out var removedCts);
|
// 清理:仅当字典中存的仍然是本任务创建的 CTS 时才移除,避免误删新任务的 CTS
|
||||||
removedCts?.Dispose();
|
if (_cyclicSenders.TryGetValue((通道号, 帧ID), out var current) && ReferenceEquals(current.Cts, cts))
|
||||||
|
{
|
||||||
|
_cyclicSenders.TryRemove((通道号, 帧ID), out _);
|
||||||
|
}
|
||||||
|
cts.Dispose();
|
||||||
}, cts.Token);
|
}, cts.Token);
|
||||||
|
|
||||||
|
_cyclicSenders[(通道号, 帧ID)] = (cts, sendTask);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,10 +428,10 @@ namespace ZLGUSBCANFD
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual void 停止循环发送(uint 通道号, uint 帧ID)
|
public virtual void 停止循环发送(uint 通道号, uint 帧ID)
|
||||||
{
|
{
|
||||||
if (_cyclicSenders.TryRemove((通道号, 帧ID), out var cts))
|
if (_cyclicSenders.TryRemove((通道号, 帧ID), out var entry))
|
||||||
{
|
{
|
||||||
cts.Cancel();
|
entry.Cts.Cancel();
|
||||||
cts.Dispose();
|
// 不在这里 Dispose,由 Task 的清理代码负责释放,避免 Task 仍在使用已释放的 Token
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,14 +440,58 @@ namespace ZLGUSBCANFD
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual void 停止所有循环发送()
|
public virtual void 停止所有循环发送()
|
||||||
{
|
{
|
||||||
foreach (var kvp in _cyclicSenders)
|
var entries = _cyclicSenders.ToArray();
|
||||||
|
|
||||||
|
// 1. 取消所有 CTS(不 Dispose,由 Task 清理代码负责)
|
||||||
|
foreach (var kvp in entries)
|
||||||
{
|
{
|
||||||
kvp.Value.Cancel();
|
kvp.Value.Cts.Cancel();
|
||||||
kvp.Value.Dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. 等待所有循环发送 Task 退出(最多 3 秒)
|
||||||
|
var tasks = new List<Task>();
|
||||||
|
foreach (var kvp in entries)
|
||||||
|
{
|
||||||
|
if (!kvp.Value.SendTask.IsCompleted)
|
||||||
|
tasks.Add(kvp.Value.SendTask);
|
||||||
|
}
|
||||||
|
if (tasks.Count > 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Task.WaitAll(tasks.ToArray(), TimeSpan.FromSeconds(3));
|
||||||
|
}
|
||||||
|
catch { /* 忽略等待异常 */ }
|
||||||
|
}
|
||||||
|
|
||||||
_cyclicSenders.Clear();
|
_cyclicSenders.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 清除指定通道、指定帧 ID 的信号覆盖值,恢复为 DBC 初始值。
|
||||||
|
/// </summary>
|
||||||
|
public virtual void 清除信号覆盖(uint 通道号, uint 帧ID)
|
||||||
|
{
|
||||||
|
if (_signalOverrides.TryRemove((通道号, 帧ID), out var dict))
|
||||||
|
{
|
||||||
|
lock (dict) dict.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 清除指定通道所有帧的信号覆盖值。
|
||||||
|
/// </summary>
|
||||||
|
public virtual void 清除通道信号覆盖(uint 通道号)
|
||||||
|
{
|
||||||
|
foreach (var key in _signalOverrides.Keys)
|
||||||
|
{
|
||||||
|
if (key.通道号 == 通道号 && _signalOverrides.TryRemove(key, out var dict))
|
||||||
|
{
|
||||||
|
lock (dict) dict.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -536,7 +619,7 @@ namespace ZLGUSBCANFD
|
|||||||
/// <param name="物理值">要写入的实际物理数值</param>
|
/// <param name="物理值">要写入的实际物理数值</param>
|
||||||
/// <param name="循环发送间隔毫秒">0 = 单次发送;>0 = 周期循环发送(单位毫秒)</param>
|
/// <param name="循环发送间隔毫秒">0 = 单次发送;>0 = 周期循环发送(单位毫秒)</param>
|
||||||
/// <returns>操作是否成功</returns>
|
/// <returns>操作是否成功</returns>
|
||||||
public virtual bool 设置报文(uint 通道号, string 帧IDStr, string 信号名称, double 物理值, int 循环发送间隔毫秒 = 0)
|
public virtual bool 设置报文(uint 通道号, string 帧IDStr, string 信号名称, double 物理值, int 循环发送间隔毫秒 = 0, bool 直接发送=false)
|
||||||
{
|
{
|
||||||
if (!TryParseFrameId(帧IDStr, out uint 帧ID))
|
if (!TryParseFrameId(帧IDStr, out uint 帧ID))
|
||||||
{
|
{
|
||||||
@@ -548,12 +631,18 @@ namespace ZLGUSBCANFD
|
|||||||
if (!_isDbcLoadedArray[通道号]) return false;
|
if (!_isDbcLoadedArray[通道号]) return false;
|
||||||
if (string.IsNullOrWhiteSpace(信号名称)) return false;
|
if (string.IsNullOrWhiteSpace(信号名称)) return false;
|
||||||
|
|
||||||
var merged = 构建报文发送字典(通道号, 帧ID, new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase)
|
// 将本次设置的信号值写入持久化覆盖表(累积,不会丢失之前设置的值)
|
||||||
|
var overrides = _signalOverrides.GetOrAdd((通道号, 帧ID), _ => new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase));
|
||||||
|
lock (overrides)
|
||||||
{
|
{
|
||||||
{ 信号名称, 物理值 }
|
overrides[信号名称] = 物理值;
|
||||||
});
|
}
|
||||||
|
|
||||||
return 发送DBC定义报文(通道号, 帧ID, merged, 循环发送间隔毫秒);
|
// 构建发送字典:DBC 初始值 + 持久化覆盖表(已包含本次设置)
|
||||||
|
var merged = 构建报文发送字典(通道号, 帧ID);
|
||||||
|
|
||||||
|
if (直接发送) return 发送DBC定义报文(通道号, 帧ID, merged, 循环发送间隔毫秒);
|
||||||
|
else return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -660,6 +749,81 @@ namespace ZLGUSBCANFD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 直接发送 8 字节原始报文(经典 CAN 或 CANFD 均可)。
|
||||||
|
/// 无需加载 DBC,适合手动构造简单报文。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="通道号">通道号(0 ~ MaxChannels-1)</param>
|
||||||
|
/// <param name="帧ID">帧 ID(标准 11 位,扩展帧自动置位 0x80000000)</param>
|
||||||
|
/// <param name="data">原始数据;超过 8 字节会自动截断,不足 8 字节自动补零</param>
|
||||||
|
/// <param name="是否是扩展帧">是否为 29 位扩展帧</param>
|
||||||
|
/// <param name="是否是CANFD">true = CANFD 格式;false = 经典 CAN 格式</param>
|
||||||
|
/// <param name="开启波特率加速BRS">CANFD 是否开启波特率加速(经典 CAN 忽略此参数)</param>
|
||||||
|
/// <returns>是否发送成功</returns>
|
||||||
|
public virtual bool 发送原始报文(
|
||||||
|
uint 通道号,
|
||||||
|
uint 帧ID,
|
||||||
|
byte[] data,
|
||||||
|
bool 是否是扩展帧 = false,
|
||||||
|
bool 是否是CANFD = true,
|
||||||
|
bool 开启波特率加速BRS = true)
|
||||||
|
{
|
||||||
|
if (通道号 >= _maxChannels || _channelHandles[通道号] == IntPtr.Zero) return false;
|
||||||
|
|
||||||
|
byte[] buffer = new byte[8];
|
||||||
|
if (data != null)
|
||||||
|
{
|
||||||
|
Array.Copy(data, 0, buffer, 0, Math.Min(data.Length, 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
uint canId = 帧ID & 0x7FFFFFFF;
|
||||||
|
if (是否是扩展帧) canId |= 0x80000000;
|
||||||
|
|
||||||
|
lock (_channelLocks[通道号])
|
||||||
|
{
|
||||||
|
if (是否是CANFD)
|
||||||
|
{
|
||||||
|
ZLGCAN.canfd_frame fdFrame = new ZLGCAN.canfd_frame
|
||||||
|
{
|
||||||
|
can_id = canId,
|
||||||
|
len = 8,
|
||||||
|
flags = (byte)((是否是扩展帧 ? 0x01 : 0x00) | (开启波特率加速BRS ? 0x02 : 0x00) | 0x20),
|
||||||
|
data = new byte[64]
|
||||||
|
};
|
||||||
|
Array.Copy(buffer, 0, fdFrame.data, 0, 8);
|
||||||
|
|
||||||
|
ZLGCAN.ZCAN_TransmitFD_Data txFdData = new ZLGCAN.ZCAN_TransmitFD_Data { frame = fdFrame, transmit_type = 2 };
|
||||||
|
IntPtr pTxFd = Marshal.AllocHGlobal(Marshal.SizeOf(txFdData));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Marshal.StructureToPtr(txFdData, pTxFd, true);
|
||||||
|
return ZLGCAN.ZCAN_TransmitFD(_channelHandles[通道号], pTxFd, 1) == 1;
|
||||||
|
}
|
||||||
|
finally { Marshal.FreeHGlobal(pTxFd); }
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ZLGCAN.can_frame standardFrame = new ZLGCAN.can_frame
|
||||||
|
{
|
||||||
|
can_id = canId,
|
||||||
|
can_dlc = 8,
|
||||||
|
__pad = 0x20,
|
||||||
|
data = new byte[8]
|
||||||
|
};
|
||||||
|
Array.Copy(buffer, 0, standardFrame.data, 0, 8);
|
||||||
|
|
||||||
|
ZLGCAN.ZCAN_Transmit_Data txData = new ZLGCAN.ZCAN_Transmit_Data { frame = standardFrame, transmit_type = 2 };
|
||||||
|
IntPtr pTx = Marshal.AllocHGlobal(Marshal.SizeOf(txData));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Marshal.StructureToPtr(txData, pTx, true);
|
||||||
|
return ZLGCAN.ZCAN_Transmit(_channelHandles[通道号], pTx, 1) == 1;
|
||||||
|
}
|
||||||
|
finally { Marshal.FreeHGlobal(pTx); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 构建发送用信号字典:DBC 初始值 + 额外覆盖。
|
/// 构建发送用信号字典:DBC 初始值 + 额外覆盖。
|
||||||
/// 调用方需自行保证通道已初始化且 DBC 已加载。
|
/// 调用方需自行保证通道已初始化且 DBC 已加载。
|
||||||
@@ -678,6 +842,7 @@ namespace ZLGUSBCANFD
|
|||||||
var msg = (ZDBC.DBCMessage)Marshal.PtrToStructure(ptrDbcMsg, typeof(ZDBC.DBCMessage));
|
var msg = (ZDBC.DBCMessage)Marshal.PtrToStructure(ptrDbcMsg, typeof(ZDBC.DBCMessage));
|
||||||
var dict = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase);
|
var dict = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
// 1. DBC 初始值打底
|
||||||
for (int i = 0; i < msg.nSignalCount; i++)
|
for (int i = 0; i < msg.nSignalCount; i++)
|
||||||
{
|
{
|
||||||
var signal = msg.vSignals[i];
|
var signal = msg.vSignals[i];
|
||||||
@@ -688,6 +853,17 @@ namespace ZLGUSBCANFD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. 叠加持久化覆盖表(之前 设置报文 累积的值)
|
||||||
|
if (_signalOverrides.TryGetValue((通道号, 帧ID), out var persisted))
|
||||||
|
{
|
||||||
|
lock (persisted)
|
||||||
|
{
|
||||||
|
foreach (var kvp in persisted)
|
||||||
|
dict[kvp.Key] = kvp.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 叠加本次调用传入的额外覆盖(优先级最高)
|
||||||
if (额外覆盖 != null)
|
if (额外覆盖 != null)
|
||||||
{
|
{
|
||||||
foreach (var kvp in 额外覆盖)
|
foreach (var kvp in 额外覆盖)
|
||||||
@@ -701,6 +877,7 @@ namespace ZLGUSBCANFD
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
||||||
Marshal.FreeHGlobal(ptrDbcMsg);
|
Marshal.FreeHGlobal(ptrDbcMsg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Common\Common.csproj" />
|
<ProjectReference Include="..\Common\Common.csproj" />
|
||||||
|
<ProjectReference Include="..\Logger\Logger.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
Reference in New Issue
Block a user