Compare commits

..

11 Commits

Author SHA1 Message Date
hsc
c0a24af4cb 台架驱动 2026-07-17 17:36:08 +08:00
hsc
c4ec348217 图表x轴显示优化 2026-07-17 09:51:11 +08:00
hsc
74d544c288 canbug修复 2026-07-17 09:42:50 +08:00
hsc
2dfc819bcc SDS2000X示波器设备调试 2026-07-16 16:53:37 +08:00
hsc
0ed080f63c N36600修改 2026-07-16 10:35:32 +08:00
hsc
8e44be4894 CAN优化 2026-07-15 15:47:42 +08:00
hsc
aebbd88e23 CAN适配兼容 2026-07-14 16:50:57 +08:00
hsc
2fe307c142 spaw7000设备命令修改 2026-07-14 14:13:39 +08:00
hsc
3318c720b7 IO板卡系统优化 2026-07-13 15:59:02 +08:00
hsc
b6b767cf0a 解决stepsmanager bug 2026-07-13 14:06:37 +08:00
hsc
9171e0b0c1 自定义报文发送 2026-07-13 13:23:27 +08:00
41 changed files with 1285 additions and 367 deletions

View File

@@ -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

View File

@@ -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"

View 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
}
}

View 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);
}
}
}

View 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);
}
}
}

View File

@@ -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,

View File

@@ -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;

View File

@@ -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();

View File

@@ -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();

View File

@@ -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地址, , , );
} }

View File

@@ -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>

View File

@@ -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
} }
} }

View File

@@ -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>

View File

@@ -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>();
} }
} }
} }

View File

@@ -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()

View File

@@ -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)

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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)
{ {

View File

@@ -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)

View File

@@ -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

View File

@@ -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" },

View File

@@ -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)
{ {

View File

@@ -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()

View File

@@ -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);

View File

@@ -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

View File

@@ -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()

View File

@@ -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();
} }
} }
} }

View File

@@ -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>>
{ {
} }
} }

View 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>
{
}
}

View File

@@ -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;}
} }
} }

View File

@@ -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);
} }

View File

@@ -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

View File

@@ -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>
/// 硬件指纹(物理设备唯一标识,用于超限匹配) /// 硬件指纹(物理设备唯一标识,用于超限匹配)

View File

@@ -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);
} }
} }

View File

@@ -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>