Compare commits

...

11 Commits

Author SHA1 Message Date
hsc
f83c93fcf9 合并冲突 2026-06-25 17:13:33 +08:00
hsc
1b4d8ea0de 压力测试优化1 2026-06-25 17:00:17 +08:00
hsc
45df76b06d 普通优化 2026-06-25 16:09:58 +08:00
hsc
72f8b8c5b3 超时bug修改 2026-06-25 09:13:44 +08:00
hsc
4deb785135 log日志bug修改 2026-06-24 14:07:17 +08:00
hsc
c4470af013 设备销毁处理 2026-06-24 10:08:38 +08:00
hsc
cd3cbe7c35 共有变量添加 2026-06-24 09:23:12 +08:00
hsc
5b721d2557 弹窗优化 2026-06-23 10:50:43 +08:00
hsc
1e21d5f9f2 命令运行bug修复 2026-06-23 10:29:09 +08:00
hsc
1faae12822 设备数量不对等适配 2026-06-22 17:22:21 +08:00
hsc
cba9af2892 设备编辑管理界面 2026-06-22 14:29:38 +08:00
54 changed files with 3194 additions and 247 deletions

View File

@@ -8,6 +8,12 @@
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<None Remove="Images\error.png" />
<None Remove="Images\info.png" />
<None Remove="Images\warning.png" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoMapper" Version="16.1.1" /> <PackageReference Include="AutoMapper" Version="16.1.1" />
</ItemGroup> </ItemGroup>
@@ -27,4 +33,16 @@
<ProjectReference Include="..\UpdateInfoMoudle\UpdateInfoMoudle.csproj" /> <ProjectReference Include="..\UpdateInfoMoudle\UpdateInfoMoudle.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Resource Include="Images\error.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource>
<Resource Include="Images\info.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource>
<Resource Include="Images\warning.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource>
</ItemGroup>
</Project> </Project>

View File

@@ -59,6 +59,10 @@ namespace ADP
} }
protected override void OnInitialized() protected override void OnInitialized()
{ {
// 配置全局日志分发器:按 CurrentScope 路由到对应 LogArea
var globalInfo = Container.Resolve<GlobalInfo>();
LoggerHelper.Progress = new ScopeLogDispatcher(globalInfo);
//初始化数据库 //初始化数据库
//DatabaseConfig.SetTenant(10001); //DatabaseConfig.SetTenant(10001);
//DatabaseConfig.InitMySql("127.0.0.1",3306,"ADP","root","123456"); //DatabaseConfig.InitMySql("127.0.0.1",3306,"ADP","root","123456");

BIN
ADP/Images/error.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

BIN
ADP/Images/info.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
ADP/Images/warning.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -1,10 +1,12 @@
using Logger; using Logger;
using MaterialDesignThemes.Wpf; using MaterialDesignThemes.Wpf;
using Model.Models;
using Notifications.Wpf.Core; using Notifications.Wpf.Core;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
@@ -225,7 +227,7 @@ namespace ADP.ViewModels
if (targetContext.RunState == "运行") if (targetContext.RunState == "运行")
{ {
targetContext.SingleStep = false; targetContext.SingleStep = false;
LoggerHelper.InfoWithNotify($"{_globalInfo.UserName} 执行工位 [{runningScope}] 运行命令"); LoggerHelper.InfoWithNotify(runningScope, $"{_globalInfo.UserName} 执行工位 [{runningScope}] 运行命令");
targetContext.RunState = "暂停"; targetContext.RunState = "暂停";
targetContext.RunIcon = PackIconKind.Pause; targetContext.RunIcon = PackIconKind.Pause;
@@ -251,7 +253,7 @@ namespace ADP.ViewModels
} }
catch (Exception ex) catch (Exception ex)
{ {
LoggerHelper.ErrorWithNotify($"工位 [{runningScope}] 异常中止: {ex.Message}"); LoggerHelper.ErrorWithNotify(runningScope, $"工位 [{runningScope}] 异常中止: {ex.Message}");
} }
// 💡 测试正常完毕或取消:停止当前工位的 SW并将最终时间固化到 RunningTime 字段中 // 💡 测试正常完毕或取消:停止当前工位的 SW并将最终时间固化到 RunningTime 字段中
@@ -284,7 +286,7 @@ namespace ADP.ViewModels
} }
else // 用户点击了暂停 else // 用户点击了暂停
{ {
LoggerHelper.InfoWithNotify($"{_globalInfo.UserName} 点击工位 [{runningScope}] 暂停命令"); LoggerHelper.InfoWithNotify(runningScope, $"{_globalInfo.UserName} 点击工位 [{runningScope}] 暂停命令");
targetContext.SingleStep = true; targetContext.SingleStep = true;
targetContext.IsStop = true; targetContext.IsStop = true;
targetContext.RunState = "运行"; targetContext.RunState = "运行";
@@ -307,7 +309,7 @@ namespace ADP.ViewModels
if (targetContext.RunState == "运行") if (targetContext.RunState == "运行")
{ {
targetContext.SingleStep = true; targetContext.SingleStep = true;
LoggerHelper.InfoWithNotify($"{_globalInfo.UserName} 执行工位 [{runningScope}] 单步执行命令"); LoggerHelper.InfoWithNotify(runningScope, $"{_globalInfo.UserName} 执行工位 [{runningScope}] 单步执行命令");
targetContext.RunState = "暂停"; targetContext.RunState = "暂停";
targetContext.RunIcon = PackIconKind.Pause; targetContext.RunIcon = PackIconKind.Pause;
@@ -333,7 +335,7 @@ namespace ADP.ViewModels
} }
catch (Exception ex) catch (Exception ex)
{ {
LoggerHelper.ErrorWithNotify($"工位 [{runningScope}] 单步执行异常: {ex.Message}"); LoggerHelper.ErrorWithNotify(runningScope, $"工位 [{runningScope}] 单步执行异常: {ex.Message}");
} }
// 💡 单步单步完成后会被 StepRunning 挂起,在此暂时停止 SW // 💡 单步单步完成后会被 StepRunning 挂起,在此暂时停止 SW
@@ -377,7 +379,7 @@ namespace ADP.ViewModels
return; return;
} }
LoggerHelper.InfoWithNotify($"{_globalInfo.UserName} 执行工位 [{runningScope}] 复位命令"); LoggerHelper.InfoWithNotify(runningScope, $"{_globalInfo.UserName} 执行工位 [{runningScope}] 复位命令");
_executionTasks.TryGetValue(runningScope, out var currentTask); _executionTasks.TryGetValue(runningScope, out var currentTask);
_errorExecutionTasks.TryGetValue(runningScope, out var errorTask); _errorExecutionTasks.TryGetValue(runningScope, out var errorTask);
@@ -427,7 +429,7 @@ namespace ADP.ViewModels
return; return;
} }
LoggerHelper.InfoWithNotify($"{_globalInfo.UserName} 执行工位 [{runningScope}] 异常流程命令"); LoggerHelper.InfoWithNotify(runningScope, $"{_globalInfo.UserName} 执行工位 [{runningScope}] 异常流程命令");
// 💡 异常测试启动计时 // 💡 异常测试启动计时
targetContext.SW.Start(); targetContext.SW.Start();
@@ -444,7 +446,7 @@ namespace ADP.ViewModels
} }
catch (Exception ex) catch (Exception ex)
{ {
LoggerHelper.ErrorWithNotify($"工位 [{runningScope}] 异常流程执行出错: {ex.Message}"); LoggerHelper.ErrorWithNotify(runningScope, $"工位 [{runningScope}] 异常流程执行出错: {ex.Message}");
} }
// 💡 异常测试结束停止计时 // 💡 异常测试结束停止计时
@@ -478,7 +480,7 @@ namespace ADP.ViewModels
{ {
CurrentConfig.DefaultProgramFilePath = targetContext.CurrentFilePath; CurrentConfig.DefaultProgramFilePath = targetContext.CurrentFilePath;
ConfigService.Save(CurrentConfig); ConfigService.Save(CurrentConfig);
LoggerHelper.SuccessWithNotify($"工位 [{runningScope}] 已成功将当前程序设为默认启动程序"); LoggerHelper.SuccessWithNotify(runningScope, $"工位 [{runningScope}] 已成功将当前程序设为默认启动程序");
} }
} }
@@ -495,8 +497,16 @@ namespace ADP.ViewModels
targetContext.Program.Parameters.Clear(); targetContext.Program.Parameters.Clear();
targetContext.Program.StepCollection.Clear(); targetContext.Program.StepCollection.Clear();
targetContext.Program.ErrorStepCollection.Clear(); targetContext.Program.ErrorStepCollection.Clear();
foreach (var item in CurrentConfig.ParameterList)
LoggerHelper.InfoWithNotify($"工位 [{runningScope}] 创建了空程序文件"); {
var param = CurrentConfig.SharedParameterList.FirstOrDefault(x => x.ParameterName == item.Name);
if (param != null)
{
item.Value = param.Value;
}
targetContext.Program.Parameters.Add(item);
}
LoggerHelper.InfoWithNotify(runningScope, $"工位 [{runningScope}] 创建了空程序文件");
// 如果当前正看着该工位,刷新 UI 显示 // 如果当前正看着该工位,刷新 UI 显示
if (_globalInfo.CurrentScope == runningScope) RefreshAllContextProperties(); if (_globalInfo.CurrentScope == runningScope) RefreshAllContextProperties();
@@ -530,7 +540,7 @@ namespace ADP.ViewModels
// 确认文件存在 // 确认文件存在
if (!File.Exists(filePath)) if (!File.Exists(filePath))
{ {
LoggerHelper.ErrorWithNotify($"文件不存在: {filePath}"); LoggerHelper.ErrorWithNotify(runningScope, $"文件不存在: {filePath}");
return; return;
} }
@@ -542,7 +552,7 @@ namespace ADP.ViewModels
if (program == null) if (program == null)
{ {
LoggerHelper.WarnWithNotify($"文件格式不正确或为空: {filePath}"); LoggerHelper.WarnWithNotify(runningScope, $"文件格式不正确或为空: {filePath}");
return; return;
} }
@@ -552,7 +562,17 @@ namespace ADP.ViewModels
targetContext.Program.ErrorStepCollection = program.ErrorStepCollection; targetContext.Program.ErrorStepCollection = program.ErrorStepCollection;
targetContext.CurrentFilePath = filePath; targetContext.CurrentFilePath = filePath;
LoggerHelper.SuccessWithNotify($"工位 [{runningScope}] 成功打开文件: {filePath}"); foreach (var item in CurrentConfig.SharedParameterList)
{
var parameter = targetContext?.Program?.Parameters?.FirstOrDefault(x => x.Name == item.ParameterName);
if (parameter != null)
{
parameter.Value = item.Value;
}
}
LoggerHelper.SuccessWithNotify(runningScope, $"工位 [{runningScope}] 成功打开文件: {filePath}");
// 💡 3. 安全调用异步复位,确保重置的是对应工位的数据 // 💡 3. 安全调用异步复位,确保重置的是对应工位的数据
// 注意:由于在 OnRestoration 内部第一行也做了快照拦截, // 注意:由于在 OnRestoration 内部第一行也做了快照拦截,
@@ -561,7 +581,7 @@ namespace ADP.ViewModels
} }
catch (Exception ex) catch (Exception ex)
{ {
LoggerHelper.ErrorWithNotify($"工位 [{runningScope}] 打开文件失败: {ex.Message}"); LoggerHelper.ErrorWithNotify(runningScope, $"工位 [{runningScope}] 打开文件失败: {ex.Message}");
} }
finally finally
{ {
@@ -600,7 +620,7 @@ namespace ADP.ViewModels
// 💡 3. 调用你的底层文件保存逻辑,传入快照工位的 Program 模型 // 💡 3. 调用你的底层文件保存逻辑,传入快照工位的 Program 模型
SaveProgramToFile(targetContext.CurrentFilePath, targetContext.Program); SaveProgramToFile(targetContext.CurrentFilePath, targetContext.Program);
LoggerHelper.InfoWithNotify($"{_globalInfo.UserName} 另存为文件成功: {saveFileDialog.FileName}"); LoggerHelper.InfoWithNotify(runningScope, $"{_globalInfo.UserName} 另存为文件成功: {saveFileDialog.FileName}");
if (_globalInfo.CurrentScope == runningScope) RefreshAllContextProperties(); if (_globalInfo.CurrentScope == runningScope) RefreshAllContextProperties();
} }
@@ -625,7 +645,7 @@ namespace ADP.ViewModels
// 💡 3. 持久化当前快照工位的 Program // 💡 3. 持久化当前快照工位的 Program
SaveProgramToFile(targetContext.CurrentFilePath, targetContext.Program); SaveProgramToFile(targetContext.CurrentFilePath, targetContext.Program);
LoggerHelper.SuccessWithNotify($"工位 [{runningScope}] 程序保存成功!"); LoggerHelper.SuccessWithNotify(_globalInfo.CurrentScope,$"工位 [{runningScope}] 程序保存成功!");
} }
// 💡 辅助方法:建议将你的通用序列化落盘代码调整为接收 ProgramVM 参数,提高复用性 // 💡 辅助方法:建议将你的通用序列化落盘代码调整为接收 ProgramVM 参数,提高复用性
@@ -638,7 +658,7 @@ namespace ADP.ViewModels
} }
catch (Exception ex) catch (Exception ex)
{ {
LoggerHelper.ErrorWithNotify($"写入程序文件失败: {ex.Message}"); LoggerHelper.ErrorWithNotify(_globalInfo.CurrentScope,$"写入程序文件失败: {ex.Message}");
} }
} }

View File

@@ -1,5 +1,6 @@
using Model.Models; using Model.Models;
using System; using System;
using System.IO;
using System.IO.Ports; using System.IO.Ports;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
@@ -7,7 +8,7 @@ using System.Threading.Tasks;
namespace DeviceCommand.Base namespace DeviceCommand.Base
{ {
public class Serial_Port : ISerialPort public class Serial_Port : ISerialPort, IDisposable
{ {
public string PortName { get; set; } = "COM1"; public string PortName { get; set; } = "COM1";
public int BaudRate { get; set; } = 9600; public int BaudRate { get; set; } = 9600;
@@ -58,8 +59,10 @@ namespace DeviceCommand.Base
_serialPort.DataBits = DataBits; _serialPort.DataBits = DataBits;
_serialPort.StopBits = StopBits; _serialPort.StopBits = StopBits;
_serialPort.Parity = Parity; _serialPort.Parity = Parity;
_serialPort.ReadTimeout = ReadTimeout;
_serialPort.WriteTimeout = WriteTimeout; // 允许底层保留默认设置,控制权交给外层 WaitAsync
_serialPort.ReadTimeout = SerialPort.InfiniteTimeout;
_serialPort.WriteTimeout = SerialPort.InfiniteTimeout;
_serialPort.Open(); _serialPort.Open();
return true; return true;
@@ -72,19 +75,33 @@ namespace DeviceCommand.Base
public virtual void Close() public virtual void Close()
{ {
if (_serialPort.IsOpen) _serialPort.Close(); if (_serialPort?.IsOpen == true) _serialPort.Close();
} }
// 内部无锁发送方法,供原子组合操作调用
private async Task LoglessSendAsync(string data, CancellationToken ct) private async Task LoglessSendAsync(string data, CancellationToken ct)
{ {
if (!_serialPort.IsOpen) throw new InvalidOperationException("串口未打开。"); if (!_serialPort.IsOpen) throw new InvalidOperationException("串口未打开。");
byte[] bytes = Encoding.UTF8.GetBytes(data); byte[] bytes = Encoding.UTF8.GetBytes(data);
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
if (WriteTimeout > 0) cts.CancelAfter(WriteTimeout);
await _serialPort.BaseStream.WriteAsync(bytes, 0, bytes.Length, cts.Token); try
{
if (WriteTimeout > 0)
{
// 核心修改:使用 WaitAsync 精准控制写入超时
await _serialPort.BaseStream.WriteAsync(bytes, 0, bytes.Length, ct)
.WaitAsync(TimeSpan.FromMilliseconds(WriteTimeout), ct)
.ConfigureAwait(false);
}
else
{
await _serialPort.BaseStream.WriteAsync(bytes, 0, bytes.Length, ct).ConfigureAwait(false);
}
}
catch (TimeoutException ex)
{
throw new TimeoutException($"串口写入数据超时({WriteTimeout} ms", ex);
}
} }
public async Task SendAsync(string data, CancellationToken ct = default) public async Task SendAsync(string data, CancellationToken ct = default)
@@ -100,7 +117,6 @@ namespace DeviceCommand.Base
} }
} }
// 内部无锁读取方法,利用 BaseStream 挂起线程,高性能不吃 CPU
private async Task<string> LoglessReadAsync(string delimiter, CancellationToken ct) private async Task<string> LoglessReadAsync(string delimiter, CancellationToken ct)
{ {
if (!_serialPort.IsOpen) throw new InvalidOperationException("串口未打开。"); if (!_serialPort.IsOpen) throw new InvalidOperationException("串口未打开。");
@@ -109,24 +125,44 @@ namespace DeviceCommand.Base
var sb = new StringBuilder(); var sb = new StringBuilder();
byte[] buffer = new byte[1024]; byte[] buffer = new byte[1024];
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); try
if (ReadTimeout > 0) cts.CancelAfter(ReadTimeout);
while (!cts.Token.IsCancellationRequested)
{ {
// 核心优化:利用流异步挂起,替代原先的 BytesToRead 循环延时 while (true)
int bytesRead = await _serialPort.BaseStream.ReadAsync(buffer, 0, buffer.Length, cts.Token);
if (bytesRead == 0) continue;
sb.Append(Encoding.UTF8.GetString(buffer, 0, bytesRead));
int index = sb.ToString().IndexOf(delimiter, StringComparison.Ordinal);
if (index >= 0)
{ {
return sb.ToString(0, index).Trim(); ct.ThrowIfCancellationRequested();
int bytesRead;
if (ReadTimeout > 0)
{
// 核心修改:使用 WaitAsync 精准控制单次异步读取超时
bytesRead = await _serialPort.BaseStream.ReadAsync(buffer, 0, buffer.Length, ct)
.WaitAsync(TimeSpan.FromMilliseconds(ReadTimeout), ct)
.ConfigureAwait(false);
}
else
{
bytesRead = await _serialPort.BaseStream.ReadAsync(buffer, 0, buffer.Length, ct).ConfigureAwait(false);
}
if (bytesRead == 0) continue;
sb.Append(Encoding.UTF8.GetString(buffer, 0, bytesRead));
string currentText = sb.ToString();
int index = currentText.IndexOf(delimiter, StringComparison.Ordinal);
if (index >= 0)
{
return currentText.Substring(0, index).Trim();
}
} }
} }
throw new TimeoutException("读取数据超时"); catch (TimeoutException ex)
{
// 关键防污染策略串口发生命令错、超时不响应时WaitAsync 会立功捕获异常
// 我们直接清空串口驱动内部的输入缓冲区,把残余垃圾数据物理抹除,确保下一条指令安全、不掉线
ClearHardwareBuffer();
throw new TimeoutException($"串口读取超时(未收到结束符 '{delimiter}'),等待时间:{ReadTimeout} ms", ex);
}
} }
public async Task<string> ReadAsync(string delimiter = "\n", CancellationToken ct = default) public async Task<string> ReadAsync(string delimiter = "\n", CancellationToken ct = default)
@@ -142,12 +178,14 @@ namespace DeviceCommand.Base
} }
} }
// 核心优化:保证多线程环境下发送和等待回包是一个原子过程
public async Task<string> WriteReadAsync(string command, string delimiter = "\n", CancellationToken ct = default) public async Task<string> WriteReadAsync(string command, string delimiter = "\n", CancellationToken ct = default)
{ {
await commLock.WaitAsync(ct); await commLock.WaitAsync(ct);
try try
{ {
// 发送新命令前先冲洗一下缓冲区,双重保险
ClearHardwareBuffer();
await LoglessSendAsync(command, ct); await LoglessSendAsync(command, ct);
return await LoglessReadAsync(delimiter, ct); return await LoglessReadAsync(delimiter, ct);
} }
@@ -157,9 +195,32 @@ namespace DeviceCommand.Base
} }
} }
/// <summary>
/// 物理强制清空串口驱动层及软件层的缓冲区
/// </summary>
private void ClearHardwareBuffer()
{
try
{
if (_serialPort?.IsOpen == true)
{
_serialPort.DiscardInBuffer(); // 清空硬件接收缓冲区
_serialPort.DiscardOutBuffer(); // 清空硬件发送缓冲区
}
}
catch
{
// 忽略清理异常
}
}
public void Dispose() public void Dispose()
{ {
_serialPort?.Dispose(); if (_serialPort != null)
{
if (_serialPort.IsOpen) _serialPort.Close();
_serialPort.Dispose();
}
commLock?.Dispose(); commLock?.Dispose();
} }
} }

View File

@@ -8,7 +8,7 @@ using System.Threading.Tasks;
namespace DeviceCommand.Base namespace DeviceCommand.Base
{ {
public class Tcp : ITcp public class Tcp : ITcp, IDisposable
{ {
public string IPAddress { get; set; } = "127.0.0.1"; public string IPAddress { get; set; } = "127.0.0.1";
public int Port { get; set; } = 502; public int Port { get; set; } = 502;
@@ -46,15 +46,7 @@ namespace DeviceCommand.Base
await _commLock.WaitAsync(ct); await _commLock.WaitAsync(ct);
try try
{ {
if (_tcpClient.Connected) return true; return await ResetConnectionAsync(ct);
// 修复:释放并彻底清空旧的连接实例,否则复用引发异常
_tcpClient.Close();
_tcpClient.Dispose();
_tcpClient = new TcpClient();
await _tcpClient.ConnectAsync(IPAddress, Port, ct);
return true;
} }
finally finally
{ {
@@ -62,9 +54,25 @@ namespace DeviceCommand.Base
} }
} }
/// <summary>
/// 核心内部方法:无锁状态下安全重置并重建连接
/// </summary>
private async Task<bool> ResetConnectionAsync(CancellationToken ct)
{
if (_tcpClient != null)
{
_tcpClient.Close();
_tcpClient.Dispose();
}
_tcpClient = new TcpClient();
await _tcpClient.ConnectAsync(IPAddress, Port, ct).ConfigureAwait(false);
return true;
}
public virtual void Close() public virtual void Close()
{ {
if (_tcpClient.Connected) _tcpClient.Close(); if (_tcpClient?.Connected == true) _tcpClient.Close();
} }
private async Task LoglessSendAsync(byte[] buffer, CancellationToken ct) private async Task LoglessSendAsync(byte[] buffer, CancellationToken ct)
@@ -72,10 +80,26 @@ namespace DeviceCommand.Base
if (!IsConnected) throw new InvalidOperationException("TCP未连接。"); if (!IsConnected) throw new InvalidOperationException("TCP未连接。");
NetworkStream stream = _tcpClient.GetStream(); NetworkStream stream = _tcpClient.GetStream();
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); try
if (SendTimeout > 0) cts.CancelAfter(SendTimeout); {
if (SendTimeout > 0)
await stream.WriteAsync(buffer, 0, buffer.Length, cts.Token); {
// 使用 WaitAsync 控制发送超时
await stream.WriteAsync(buffer, 0, buffer.Length, ct)
.WaitAsync(TimeSpan.FromMilliseconds(SendTimeout), ct)
.ConfigureAwait(false);
}
else
{
await stream.WriteAsync(buffer, 0, buffer.Length, ct).ConfigureAwait(false);
}
}
catch (TimeoutException ex)
{
// 发送超时,安全起见直接断开重建
await ResetConnectionAsync(ct);
throw new TimeoutException($"TCP 发送数据超时({SendTimeout} ms连接已重置以确保安全。", ex);
}
} }
public async Task SendAsync(byte[] buffer, CancellationToken ct = default) public async Task SendAsync(byte[] buffer, CancellationToken ct = default)
@@ -107,17 +131,30 @@ namespace DeviceCommand.Base
byte[] buffer = new byte[length]; byte[] buffer = new byte[length];
int offset = 0; int offset = 0;
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); try
if (ReceiveTimeout > 0) cts.CancelAfter(ReceiveTimeout);
while (offset < length)
{ {
int read = await stream.ReadAsync(buffer, offset, length - offset, cts.Token); while (offset < length)
if (read == 0) break; {
offset += read; ct.ThrowIfCancellationRequested();
}
return offset == 0 ? Array.Empty<byte>() : buffer[..offset]; // 计算剩余超时时间(这里简单使用配置值,若要求极精准可加入不长计算)
int read = ReceiveTimeout > 0
? await stream.ReadAsync(buffer, offset, length - offset, ct)
.WaitAsync(TimeSpan.FromMilliseconds(ReceiveTimeout), ct)
.ConfigureAwait(false)
: await stream.ReadAsync(buffer, offset, length - offset, ct).ConfigureAwait(false);
if (read == 0) throw new IOException("远程主机已关闭连接");
offset += read;
}
return offset == 0 ? Array.Empty<byte>() : buffer[..offset];
}
catch (TimeoutException ex)
{
// 关键安全重置WaitAsync 超时后抛弃老连接,防止未完成的 Read 污染后续 Buffer
await ResetConnectionAsync(ct);
throw new TimeoutException($"TCP 读取定长数据超时({ReceiveTimeout} ms连接已重置避免数据错乱。", ex);
}
} }
finally finally
{ {
@@ -134,22 +171,37 @@ namespace DeviceCommand.Base
byte[] buffer = new byte[1024]; byte[] buffer = new byte[1024];
NetworkStream stream = _tcpClient.GetStream(); NetworkStream stream = _tcpClient.GetStream();
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); try
if (ReceiveTimeout > 0) cts.CancelAfter(ReceiveTimeout);
while (!cts.Token.IsCancellationRequested)
{ {
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cts.Token); while (true)
if (bytesRead == 0) throw new IOException("远程主机已关闭连接"); {
ct.ThrowIfCancellationRequested();
sb.Append(Encoding.UTF8.GetString(buffer, 0, bytesRead)); int bytesRead = 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);
int index = sb.ToString().IndexOf(delimiter, StringComparison.Ordinal); if (bytesRead == 0) throw new IOException("远程主机已关闭连接");
if (index >= 0)
return sb.ToString(0, index).Trim(); sb.Append(Encoding.UTF8.GetString(buffer, 0, bytesRead));
string currentText = sb.ToString();
int index = currentText.IndexOf(delimiter, StringComparison.Ordinal);
if (index >= 0)
{
return currentText.Substring(0, index).Trim();
}
}
}
catch (TimeoutException ex)
{
// 当 SCPI 仪表由于命令发错不响应时WaitAsync 会立功并抛出 TimeoutException
// 此时我们在内部秒速“重置连接”,上层业务只会收到一个干净的超时报错,但由于锁和自动重连,下一次通信直接恢复!
await ResetConnectionAsync(ct);
throw new TimeoutException($"SCPI 读取超时(未收到结束符 '{delimiter}',等待:{ReceiveTimeout} ms通信链路已自动刷新。", ex);
} }
throw new TimeoutException("读取数据超时");
} }
public async Task<string> ReadAsync(string delimiter = "\n", CancellationToken ct = default) public async Task<string> ReadAsync(string delimiter = "\n", CancellationToken ct = default)
@@ -165,7 +217,6 @@ namespace DeviceCommand.Base
} }
} }
// 核心优化:确保发送与读取在同一组锁生命周期内
public async Task<string> WriteReadAsync(string command, string delimiter = "\n", CancellationToken ct = default) public async Task<string> WriteReadAsync(string command, string delimiter = "\n", CancellationToken ct = default)
{ {
await _commLock.WaitAsync(ct); await _commLock.WaitAsync(ct);

View File

@@ -13,7 +13,7 @@ namespace DeviceCommand.Device
[ADPCommand] [ADPCommand]
public class IOBoard : ModbusTcp public class IOBoard : ModbusTcp
{ {
//只有两个,八台产品公用两两分组各用一个
public IOBoard(string Ip地址, int , int , int ) public IOBoard(string Ip地址, int , int , int )
{ {
ConfigureDevice(Ip地址, , , ); ConfigureDevice(Ip地址, , , );

View File

@@ -11,6 +11,7 @@ namespace DeviceCommand.Device
[ADPCommand] [ADPCommand]
public class IT7800E : Tcp public class IT7800E : Tcp
{ {
//只有一个,八台产品一起用一个
// 根据通用 SCPI 指令规范,使用换行符 (ASCII 字符 LF即 \n) 作为标准结束符 // 根据通用 SCPI 指令规范,使用换行符 (ASCII 字符 LF即 \n) 作为标准结束符
private const string ScpiDelimiter = "\n"; private const string ScpiDelimiter = "\n";

View File

@@ -11,6 +11,7 @@ namespace DeviceCommand.Device
[ADPCommand] [ADPCommand]
public class N36200 : Tcp public class N36200 : Tcp
{ {
//只有一个,八台产品一起用一个
// 手册第 9 页 2.2.4 明确规定:命令结束符为换行符 (ASCII 字符 LF即 \n) // 手册第 9 页 2.2.4 明确规定:命令结束符为换行符 (ASCII 字符 LF即 \n)
private const string ScpiDelimiter = "\n"; private const string ScpiDelimiter = "\n";

View File

@@ -29,7 +29,7 @@ namespace DeviceCommand.Device
/// </summary> /// </summary>
public virtual async Task (CancellationToken ct = default) public virtual async Task (CancellationToken ct = default)
{ {
await SendAsync($"*CLS{ScpiDelimiter}", ct); // await SendAsync($"*CLS{ScpiDelimiter}", ct);
} }
/// <summary> /// <summary>
@@ -37,7 +37,7 @@ namespace DeviceCommand.Device
/// </summary> /// </summary>
public virtual async Task<string> (CancellationToken ct = default) public virtual async Task<string> (CancellationToken ct = default)
{ {
return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct); // return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct);
} }
/// <summary> /// <summary>
@@ -45,17 +45,29 @@ namespace DeviceCommand.Device
/// </summary> /// </summary>
public virtual async Task (CancellationToken ct = default) public virtual async Task (CancellationToken ct = default)
{ {
await SendAsync($"*RST{ScpiDelimiter}", ct); // await SendAsync($"*RST{ScpiDelimiter}", ct);
} }
/// <summary>
/// 【补充】查询先前操作是否完成。
/// 在重置设备(*RST)或切换大物理量程后调用,返回 "1" 代表示波器继电器切换就绪,防止后续指令引发阻塞。
/// </summary>
public virtual async Task<bool> _OPC(CancellationToken ct = default)
{
string res = await WriteReadAsync($"*OPC?{ScpiDelimiter}", ScpiDelimiter, ct);
return res.Trim() == "1";
}
#endregion #endregion
#region 2. (Run / Stop / Single)
#region 2. (Run / Stop / Single)
/// <summary> /// <summary>
/// 控制示波器开始捕获波形 (等同于按下前端面板的 Run 键) /// 控制示波器开始捕获波形 (等同于按下前端面板的 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($"RUN{ScpiDelimiter}", ct);
} }
/// <summary> /// <summary>
@@ -63,15 +75,15 @@ namespace DeviceCommand.Device
/// </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($"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($"SINGle{ScpiDelimiter}", ct);
} }
/// <summary> /// <summary>
@@ -79,7 +91,19 @@ namespace DeviceCommand.Device
/// </summary> /// </summary>
public virtual async Task (CancellationToken ct = default) public virtual async Task (CancellationToken ct = default)
{ {
await SendAsync($"*TRG{ScpiDelimiter}", ct); // await SendAsync($"*TRG{ScpiDelimiter}", ct);
}
/// <summary>
/// 【补充】设置触发模式 (AUTO, NORM, SINGLE)
/// </summary>
public virtual async Task (string mode, CancellationToken ct = default)
{
string modeUpper = mode.ToUpper();
if (modeUpper != "AUTO" && modeUpper != "NORM" && modeUpper != "SINGLE")
throw new ArgumentException("触发模式只能为 AUTO, NORM, 或 SINGLE");
await SendAsync($"TRMD {modeUpper}{ScpiDelimiter}", ct);
} }
#endregion #endregion
@@ -87,12 +111,13 @@ namespace DeviceCommand.Device
#region 3. Channel (C1 ~ C4) #region 3. Channel (C1 ~ C4)
/// <summary> /// <summary>
/// 开启或关闭指定的模拟通道 (例如: channel=1 代表 C1) /// 开启或关闭指定的模拟通道
/// </summary> /// </summary>
public virtual async Task (int channel, bool enable, CancellationToken ct = default) public virtual async Task (int channel, bool enable, CancellationToken ct = default)
{ {
string state = enable ? "ON" : "OFF"; // 修正:根据手册规范,使用 1/0 比 ON/OFF 在高低版本固件中兼容性更稳定
await SendAsync($"C{channel}:TRAce {state}{ScpiDelimiter}", ct); // string state = enable ? "1" : "0";
await SendAsync($"C{channel}:TRAce {state}{ScpiDelimiter}", ct);
} }
/// <summary> /// <summary>
@@ -100,27 +125,35 @@ namespace DeviceCommand.Device
/// </summary> /// </summary>
public virtual async Task (int channel, double volts, CancellationToken ct = default) public virtual async Task (int channel, double volts, CancellationToken ct = default)
{ {
string cmd = string.Format(CultureInfo.InvariantCulture, "C{0}:VDIV {1:F4}{2}", channel, volts, ScpiDelimiter); // string cmd = string.Format(CultureInfo.InvariantCulture, "C{0}:VDIV {1:F4}{2}", channel, volts, ScpiDelimiter);
await SendAsync(cmd, ct); await SendAsync(cmd, ct);
} }
/// <summary>
/// 【补充验证】查询指定通道当前的电压档位 (用于 Setup-Verify 闭环验证逻辑)
/// </summary>
public virtual async Task<string> (int channel, CancellationToken ct = default)
{
return await WriteReadAsync($"C{channel}:VDIV?{ScpiDelimiter}", ScpiDelimiter, ct);
}
/// <summary> /// <summary>
/// 设置指定通道的垂直偏移量 (Offset单位: V) /// 设置指定通道的垂直偏移量 (Offset单位: V)
/// </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>
/// 设置通道的输入阻抗 (1MΩ 或 50Ω) /// 设置通道的输入阻抗与耦合模式
/// </summary> /// </summary>
/// <param name="is50Ohm">True: 50欧姆, False: 1M欧姆</param> /// <param name="coupling">合法参数A1M (交流1M), D1M (直流1M), D50 (直流50欧)</param>
public virtual async Task (int channel, bool is50Ohm, CancellationToken ct = default) public virtual async Task (int channel, string coupling, CancellationToken ct = default)
{ {
string value = is50Ohm ? "50" : "1M"; string coupUpper = coupling.ToUpper();
await SendAsync($"C{channel}:COUPling {value}{ScpiDelimiter}", ct); // await SendAsync($"C{channel}:COUPling {coupUpper}{ScpiDelimiter}", ct);
} }
#endregion #endregion
@@ -132,7 +165,8 @@ namespace DeviceCommand.Device
/// </summary> /// </summary>
public virtual async Task (double scale, CancellationToken ct = default) public virtual async Task (double scale, CancellationToken ct = default)
{ {
string cmd = string.Format(CultureInfo.InvariantCulture, "TIME_DIV {0:E6}{1}", scale, ScpiDelimiter); // // 修正:统一使用更精简且符合手册定义的简写形式 TDIV
string cmd = string.Format(CultureInfo.InvariantCulture, "TDIV {0:E6}{1}", scale, ScpiDelimiter);
await SendAsync(cmd, ct); await SendAsync(cmd, ct);
} }
@@ -141,7 +175,8 @@ namespace DeviceCommand.Device
/// </summary> /// </summary>
public virtual async Task (double delay, CancellationToken ct = default) public virtual async Task (double delay, CancellationToken ct = default)
{ {
string cmd = string.Format(CultureInfo.InvariantCulture, "TRIGger:DELay {0:E6}{1}", delay, ScpiDelimiter); // // 修正:采用手册标准简写 TRDL 效率更高
string cmd = string.Format(CultureInfo.InvariantCulture, "TRDL {0:E6}{1}", delay, ScpiDelimiter);
await SendAsync(cmd, ct); await SendAsync(cmd, ct);
} }
@@ -154,7 +189,8 @@ namespace DeviceCommand.Device
/// </summary> /// </summary>
public virtual async Task (double level, CancellationToken ct = default) public virtual async Task (double level, CancellationToken ct = default)
{ {
string cmd = string.Format(CultureInfo.InvariantCulture, "TRIGger:LEVel {0:F3}{1}", level, ScpiDelimiter); // // 修正:采用标准简写 TRLV
string cmd = string.Format(CultureInfo.InvariantCulture, "TRLV {0:F3}{1}", level, ScpiDelimiter);
await SendAsync(cmd, ct); await SendAsync(cmd, ct);
} }
@@ -163,7 +199,8 @@ namespace DeviceCommand.Device
/// </summary> /// </summary>
public virtual async Task (string source, CancellationToken ct = default) public virtual async Task (string source, CancellationToken ct = default)
{ {
await SendAsync($"TRIGger:SOURce {source.ToUpper()}{ScpiDelimiter}", ct); // // 修正:采用标准简写 TRSE 体系配置指令
await SendAsync($"TRIGger:SOURce {source.ToUpper()}{ScpiDelimiter}", ct);
} }
#endregion #endregion
@@ -177,12 +214,11 @@ namespace DeviceCommand.Device
/// <param name="paramName">参数名称助记符: /// <param name="paramName">参数名称助记符:
/// PKPK(峰峰值), MAX(最大值), MIN(最小值), AMPL(振幅值), /// PKPK(峰峰值), MAX(最大值), MIN(最小值), AMPL(振幅值),
/// FREQ(频率), PER(周期), MEAN(平均值), RMS(均方根) 等</param> /// FREQ(频率), PER(周期), MEAN(平均值), RMS(均方根) 等</param>
/// <returns>设备返回的科学计数法或自定义字符串数值</returns>
public virtual async Task<string> (int channel, string paramName, CancellationToken ct = default) public virtual async Task<string> (int channel, string paramName, CancellationToken ct = default)
{ {
// 语法格式示例C1:PAVA? FREQ // 优化:采用更兼容的测量读取指令语法格式
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>
@@ -190,7 +226,7 @@ namespace DeviceCommand.Device
/// </summary> /// </summary>
public virtual async Task<string> (int channel, CancellationToken ct = default) public virtual async Task<string> (int channel, CancellationToken ct = default)
{ {
return await (channel, "PKPK", ct); // return await (channel, "PKPK", ct);
} }
/// <summary> /// <summary>
@@ -198,7 +234,7 @@ namespace DeviceCommand.Device
/// </summary> /// </summary>
public virtual async Task<string> (int channel, CancellationToken ct = default) public virtual async Task<string> (int channel, CancellationToken ct = default)
{ {
return await (channel, "FREQ", ct); // return await (channel, "FREQ", ct);
} }
/// <summary> /// <summary>
@@ -206,7 +242,7 @@ namespace DeviceCommand.Device
/// </summary> /// </summary>
public virtual async Task<string> (int channel, CancellationToken ct = default) public virtual async Task<string> (int channel, CancellationToken ct = default)
{ {
return await (channel, "RMS", ct); // return await (channel, "RMS", ct);
} }
#endregion #endregion

View File

@@ -11,6 +11,7 @@ namespace DeviceCommand.Device
[ADPCommand] [ADPCommand]
public class SPAW7000 : Tcp public class SPAW7000 : Tcp
{ {
//只有两个,八台产品公用两两分组各用一个
// 根据通用 SCPI 与远宽指令规范,使用换行符 (ASCII 字符 LF即 \n) 作为标准结束符 // 根据通用 SCPI 与远宽指令规范,使用换行符 (ASCII 字符 LF即 \n) 作为标准结束符
private const string ScpiDelimiter = "\n"; private const string ScpiDelimiter = "\n";

View File

@@ -17,6 +17,10 @@ namespace DeviceEditModule
// 设备编辑 View 注册为 Navigation被动态解析后作为 Tab 内容嵌入 DialogMangerView // 设备编辑 View 注册为 Navigation被动态解析后作为 Tab 内容嵌入 DialogMangerView
containerRegistry.RegisterForNavigation<IT7800EView>("IT7800EView"); containerRegistry.RegisterForNavigation<IT7800EView>("IT7800EView");
containerRegistry.RegisterForNavigation<N36200View>("N36200View"); containerRegistry.RegisterForNavigation<N36200View>("N36200View");
containerRegistry.RegisterForNavigation<N36600View>("N36600View");
containerRegistry.RegisterForNavigation<N69200View>("N69200View");
containerRegistry.RegisterForNavigation<SDS2000X_HDView>("SDS2000X_HDView");
containerRegistry.RegisterForNavigation<SPAW7000View>("SPAW7000View");
} }
} }
} }

View File

@@ -1,6 +1,7 @@
using Prism.Commands; using Prism.Commands;
using Prism.Ioc; using Prism.Ioc;
using System; using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Windows.Input; using System.Windows.Input;
using UIShare.PubEvent; using UIShare.PubEvent;
@@ -26,6 +27,9 @@ namespace DeviceEditModule.ViewModels
/// <summary>所有 Tab 项集合,绑定到标签条的 ItemsControl。</summary> /// <summary>所有 Tab 项集合,绑定到标签条的 ItemsControl。</summary>
public ObservableCollection<DialogTabItemVM> TagItems { get; } = new(); public ObservableCollection<DialogTabItemVM> TagItems { get; } = new();
/// <summary>以设备指纹为 key 追踪已打开的 Tab防止同一物理设备重复打开。</summary>
private readonly Dictionary<string, DialogTabItemVM> _tabDictionary = new();
private DialogTabItemVM? _selectedTag; private DialogTabItemVM? _selectedTag;
/// <summary>当前激活的 Tab其 Content 显示在内容区。</summary> /// <summary>当前激活的 Tab其 Content 显示在内容区。</summary>
public DialogTabItemVM? SelectedTag public DialogTabItemVM? SelectedTag
@@ -72,7 +76,8 @@ namespace DeviceEditModule.ViewModels
private void OnClose() private void OnClose()
{ {
// 关闭前清空所有 Tab释放内容引用 // 关闭前清空所有 Tab释放内容引用,并清除去重字典
_tabDictionary.Clear();
TagItems.Clear(); TagItems.Clear();
SelectedTag = null; SelectedTag = null;
RequestClose.Invoke(); RequestClose.Invoke();
@@ -82,15 +87,26 @@ namespace DeviceEditModule.ViewModels
#region Tab #region Tab
/// <summary>收到事件:创建 Tab 并追加到集合,自动切换选中。</summary> /// <summary>收到事件:若指纹已存在则激活,否则创建并追加到集合。</summary>
private void OnAddTab(DialogTabInfo info) private void OnAddTab(DialogTabInfo info)
{ {
// 已有相同指纹的 Tab → 直接激活,不重复创建
if (!string.IsNullOrEmpty(info.Fingerprint) && _tabDictionary.TryGetValue(info.Fingerprint, out var existing))
{
ActivateTab(existing);
return;
}
var tab = new DialogTabItemVM(OnSelectTab, OnCloseTab) var tab = new DialogTabItemVM(OnSelectTab, OnCloseTab)
{ {
Title = info.Title, Title = info.Title,
Content = info.Content Fingerprint = info.Fingerprint,
Content = info.Content
}; };
if (!string.IsNullOrEmpty(info.Fingerprint))
_tabDictionary[info.Fingerprint] = tab;
TagItems.Add(tab); TagItems.Add(tab);
RaisePropertyChanged(nameof(HasNoTabs)); RaisePropertyChanged(nameof(HasNoTabs));
ActivateTab(tab); ActivateTab(tab);
@@ -103,6 +119,8 @@ namespace DeviceEditModule.ViewModels
private void OnCloseTab(DialogTabItemVM tab) private void OnCloseTab(DialogTabItemVM tab)
{ {
int idx = TagItems.IndexOf(tab); int idx = TagItems.IndexOf(tab);
if (!string.IsNullOrEmpty(tab.Fingerprint))
_tabDictionary.Remove(tab.Fingerprint);
TagItems.Remove(tab); TagItems.Remove(tab);
RaisePropertyChanged(nameof(HasNoTabs)); RaisePropertyChanged(nameof(HasNoTabs));

View File

@@ -0,0 +1,280 @@
using DeviceCommand.Device;
using Prism.Commands;
using Prism.Ioc;
using System;
using System.Threading;
using System.Windows.Input;
using UIShare.GlobalVariable;
using UIShare.ViewModelBase;
namespace DeviceEditModule.ViewModels
{
/// <summary>
/// N36600 便携式宽范围可编程直流电源控制面板 ViewModel。
/// </summary>
public class N36600ViewModel : NavigateViewModelBase, IDisposable
{
#region
private readonly DeviceManager _deviceManager;
private N36600? _device;
private CancellationTokenSource? _cts;
#endregion
#region
private string _deviceName = "N36600";
public string DeviceName
{
get => _deviceName;
set => SetProperty(ref _deviceName, value);
}
private bool _isConnected;
public bool IsConnected
{
get => _isConnected;
set => SetProperty(ref _isConnected, value);
}
private bool _isBusy;
public bool IsBusy
{
get => _isBusy;
set => SetProperty(ref _isBusy, value);
}
#endregion
#region
private double _voltage = 12.0;
/// <summary>待设置的输出电压值V。</summary>
public double Voltage
{
get => _voltage;
set => SetProperty(ref _voltage, value);
}
private double _currentLimit = 5.0;
/// <summary>待设置的输出电流值A。</summary>
public double CurrentLimit
{
get => _currentLimit;
set => SetProperty(ref _currentLimit, value);
}
private double _power = 60.0;
/// <summary>待设置的输出功率值W。</summary>
public double Power
{
get => _power;
set => SetProperty(ref _power, value);
}
private double _ovpValue = 15.0;
/// <summary>过压保护值V。</summary>
public double OvpValue
{
get => _ovpValue;
set => SetProperty(ref _ovpValue, value);
}
private double _ocpValue = 6.0;
/// <summary>过流保护值A。</summary>
public double OcpValue
{
get => _ocpValue;
set => SetProperty(ref _ocpValue, value);
}
#endregion
#region
private string _measuredVoltage = "—";
public string MeasuredVoltage
{
get => _measuredVoltage;
set => SetProperty(ref _measuredVoltage, value);
}
private string _measuredCurrent = "—";
public string MeasuredCurrent
{
get => _measuredCurrent;
set => SetProperty(ref _measuredCurrent, value);
}
private string _measuredPower = "—";
public string MeasuredPower
{
get => _measuredPower;
set => SetProperty(ref _measuredPower, value);
}
private string _outputState = "—";
/// <summary>当前 DC 输出状态OUTPut? 查询结果)。</summary>
public string OutputState
{
get => _outputState;
set => SetProperty(ref _outputState, value);
}
private string _responseLog = string.Empty;
/// <summary>命令响应日志(最新消息在顶部)。</summary>
public string ResponseLog
{
get => _responseLog;
set => SetProperty(ref _responseLog, value);
}
#endregion
#region
public ICommand QueryIdentityCommand { get; }
public ICommand OutputOnCommand { get; }
public ICommand OutputOffCommand { get; }
public ICommand SetVoltageCommand { get; }
public ICommand SetCurrentCommand { get; }
public ICommand SetPowerCommand { get; }
public ICommand SetOvpCommand { get; }
public ICommand SetOcpCommand { get; }
public ICommand QueryAllMeasureCommand { get; }
public ICommand QueryOutputStateCommand { get; }
public ICommand SetRemoteModeCommand { get; }
public ICommand SetLocalModeCommand { get; }
public ICommand ClearVoltageProtectionCommand { get; }
public ICommand ClearCurrentProtectionCommand { get; }
#endregion
public N36600ViewModel(IContainerProvider containerProvider) : base(containerProvider)
{
_deviceManager = containerProvider.Resolve<DeviceManager>();
QueryIdentityCommand = new DelegateCommand(async () => await Exec(async () => AppendLog("IDN: " + await _device!.(Ct()))));
OutputOnCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.DC输出(true, Ct()); AppendLog("输出已开启"); }));
OutputOffCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.DC输出(false, Ct()); AppendLog("输出已关闭"); }));
SetVoltageCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(Voltage, Ct()); AppendLog($"电压已设为 {Voltage} V"); }));
SetCurrentCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(CurrentLimit, Ct()); AppendLog($"电流已设为 {CurrentLimit} A"); }));
SetPowerCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(Power, Ct()); AppendLog($"功率已设为 {Power} W"); }));
SetOvpCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(OvpValue, Ct()); AppendLog($"OVP已设为 {OvpValue} V"); }));
SetOcpCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(OcpValue, Ct()); AppendLog($"OCP已设为 {OcpValue} A"); }));
SetRemoteModeCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(Ct()); AppendLog("已切换到远程控制模式"); }));
SetLocalModeCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(Ct()); AppendLog("已切换到本地控制模式"); }));
ClearVoltageProtectionCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(Ct()); AppendLog("电压保护状态已清除"); }));
ClearCurrentProtectionCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(Ct()); AppendLog("电流保护状态已清除"); }));
QueryOutputStateCommand = new DelegateCommand(async () => await Exec(async () =>
{
OutputState = await _device!.DC输出状态(Ct());
AppendLog($"输出状态: {OutputState}");
}));
QueryAllMeasureCommand = new DelegateCommand(async () => await Exec(async () =>
{
MeasuredVoltage = await _device!.(Ct());
MeasuredCurrent = await _device!.(Ct());
MeasuredPower = await _device!.(Ct());
AppendLog($"测量 → 电压:{MeasuredVoltage}V 电流:{MeasuredCurrent}A 功率:{MeasuredPower}W");
}));
Initialize();
}
#region / Navigation
public void Initialize(string? deviceName = null)
{
N36600? found = null;
string? foundName = null;
if (deviceName != null &&
_deviceManager.DeviceMap.TryGetValue(deviceName, out var d) &&
d is N36600 n)
{
found = n;
foundName = deviceName;
}
else
{
foreach (var kv in _deviceManager.DeviceMap)
{
if (kv.Value is N36600 n36)
{
found = n36;
foundName = kv.Key;
break;
}
}
}
_device = found;
DeviceName = foundName ?? "N36600 (未找到)";
IsConnected = _device?.IsConnected ?? false;
AppendLog(found != null
? $"已关联设备 [{DeviceName}],连接状态:{(IsConnected ? "" : "")}"
: "未在 DeviceManager 中找到 N36600 设备,请先初始化设备配置。");
}
public override void OnNavigatedTo(NavigationContext context)
{
var name = context.Parameters.GetValue<string?>("DeviceName");
Initialize(name);
}
#endregion
#region
private CancellationToken Ct() => (_cts = new CancellationTokenSource(TimeSpan.FromSeconds(10))).Token;
private async Task Exec(Func<Task> action)
{
if (_device == null)
{
AppendLog("错误:未关联到设备实例,请检查设备配置。");
return;
}
if (IsBusy) return;
IsBusy = true;
try
{
await action();
IsConnected = _device.IsConnected;
}
catch (OperationCanceledException)
{
AppendLog("命令超时或已取消。");
}
catch (Exception ex)
{
AppendLog($"错误:{ex.Message}");
}
finally
{
IsBusy = false;
}
}
private void AppendLog(string message)
{
var line = $"[{DateTime.Now:HH:mm:ss}] {message}";
ResponseLog = ResponseLog.Length > 4000
? line + "\n" + ResponseLog[..3000]
: line + "\n" + ResponseLog;
}
#endregion
public void Dispose()
{
_cts?.Cancel();
_cts?.Dispose();
}
}
}

View File

@@ -0,0 +1,316 @@
using DeviceCommand.Device;
using Prism.Commands;
using Prism.Ioc;
using System;
using System.Threading;
using System.Windows.Input;
using UIShare.GlobalVariable;
using UIShare.ViewModelBase;
namespace DeviceEditModule.ViewModels
{
/// <summary>
/// N69200 可编程直流电子负载控制面板 ViewModel。
/// </summary>
public class N69200ViewModel : NavigateViewModelBase, IDisposable
{
#region
private readonly DeviceManager _deviceManager;
private N69200? _device;
private CancellationTokenSource? _cts;
#endregion
#region
private string _deviceName = "N69200";
public string DeviceName
{
get => _deviceName;
set => SetProperty(ref _deviceName, value);
}
private bool _isConnected;
public bool IsConnected
{
get => _isConnected;
set => SetProperty(ref _isConnected, value);
}
private bool _isBusy;
public bool IsBusy
{
get => _isBusy;
set => SetProperty(ref _isBusy, value);
}
#endregion
#region
private string _selectedMode = "CC";
/// <summary>工作模式CC / CV / CP / CR。</summary>
public string SelectedMode
{
get => _selectedMode;
set => SetProperty(ref _selectedMode, value);
}
private double _loadValue = 1.0;
/// <summary>设定值(根据 SelectedMode 代表 A / V / W / Ω)。</summary>
public double LoadValue
{
get => _loadValue;
set => SetProperty(ref _loadValue, value);
}
private double _ovpValue = 60.0;
/// <summary>过压保护值V。</summary>
public double OvpValue
{
get => _ovpValue;
set => SetProperty(ref _ovpValue, value);
}
private double _ocpValue = 10.0;
/// <summary>过流保护值A。</summary>
public double OcpValue
{
get => _ocpValue;
set => SetProperty(ref _ocpValue, value);
}
private double _oppValue = 200.0;
/// <summary>过功率保护值W。</summary>
public double OppValue
{
get => _oppValue;
set => SetProperty(ref _oppValue, value);
}
#endregion
#region
private string _measuredVoltage = "—";
public string MeasuredVoltage
{
get => _measuredVoltage;
set => SetProperty(ref _measuredVoltage, value);
}
private string _measuredCurrent = "—";
public string MeasuredCurrent
{
get => _measuredCurrent;
set => SetProperty(ref _measuredCurrent, value);
}
private string _measuredPower = "—";
public string MeasuredPower
{
get => _measuredPower;
set => SetProperty(ref _measuredPower, value);
}
private string _currentMode = "—";
/// <summary>当前负载模式MODE? 查询结果)。</summary>
public string CurrentMode
{
get => _currentMode;
set => SetProperty(ref _currentMode, value);
}
private string _statusByte = "—";
/// <summary>状态字节(*STB? 查询结果)。</summary>
public string StatusByte
{
get => _statusByte;
set => SetProperty(ref _statusByte, value);
}
private string _responseLog = string.Empty;
/// <summary>命令响应日志(最新消息在顶部)。</summary>
public string ResponseLog
{
get => _responseLog;
set => SetProperty(ref _responseLog, value);
}
#endregion
#region
public ICommand QueryIdentityCommand { get; }
public ICommand ResetDeviceCommand { get; }
public ICommand InputOnCommand { get; }
public ICommand InputOffCommand { get; }
public ICommand SetModeCommand { get; }
public ICommand QueryModeCommand { get; }
public ICommand SetLoadValueCommand { get; }
public ICommand QueryAllMeasureCommand { get; }
public ICommand QueryStatusCommand { get; }
public ICommand SetOvpCommand { get; }
public ICommand SetOcpCommand { get; }
public ICommand SetOppCommand { get; }
public ICommand ClearAlarmCommand { get; }
public ICommand SetRemoteModeCommand { get; }
public ICommand SetLocalModeCommand { get; }
#endregion
public N69200ViewModel(IContainerProvider containerProvider) : base(containerProvider)
{
_deviceManager = containerProvider.Resolve<DeviceManager>();
QueryIdentityCommand = new DelegateCommand(async () => await Exec(async () => AppendLog("IDN: " + await _device!.(Ct()))));
ResetDeviceCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(Ct()); AppendLog("设备已重置"); }));
InputOnCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.DC输入(true, Ct()); AppendLog("输入已开启"); }));
InputOffCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.DC输入(false, Ct()); AppendLog("输入已关闭"); }));
SetModeCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(SelectedMode, Ct()); AppendLog($"模式已设为 {SelectedMode}"); }));
QueryModeCommand = new DelegateCommand(async () => await Exec(async () => { CurrentMode = await _device!.(Ct()); AppendLog($"当前模式: {CurrentMode}"); }));
SetOvpCommand = new DelegateCommand(async () => await Exec(async () => { await _device!._OVP(OvpValue, Ct()); AppendLog($"OVP已设为 {OvpValue} V"); }));
SetOcpCommand = new DelegateCommand(async () => await Exec(async () => { await _device!._OCP(OcpValue, Ct()); AppendLog($"OCP已设为 {OcpValue} A"); }));
SetOppCommand = new DelegateCommand(async () => await Exec(async () => { await _device!._OPP(OppValue, Ct()); AppendLog($"OPP已设为 {OppValue} W"); }));
ClearAlarmCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(Ct()); AppendLog("保护告警已清除"); }));
SetRemoteModeCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(Ct()); AppendLog("已切换到远程控制模式"); }));
SetLocalModeCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(Ct()); AppendLog("已切换到本地控制模式"); }));
SetLoadValueCommand = new DelegateCommand(async () => await Exec(async () =>
{
var mode = SelectedMode.ToUpperInvariant();
switch (mode)
{
case "CC":
await _device!.CC(LoadValue, Ct());
AppendLog($"恒流 CC 已设为 {LoadValue} A");
break;
case "CV":
await _device!.CV(LoadValue, Ct());
AppendLog($"恒压 CV 已设为 {LoadValue} V");
break;
case "CP":
await _device!.CP(LoadValue, Ct());
AppendLog($"恒功率 CP 已设为 {LoadValue} W");
break;
case "CR":
await _device!.CR(LoadValue, Ct());
AppendLog($"恒电阻 CR 已设为 {LoadValue} Ω");
break;
default:
AppendLog($"未知模式 {SelectedMode},无法设置设定值");
break;
}
}));
QueryStatusCommand = new DelegateCommand(async () => await Exec(async () =>
{
StatusByte = await _device!.(Ct());
AppendLog($"状态字节: {StatusByte}");
}));
QueryAllMeasureCommand = new DelegateCommand(async () => await Exec(async () =>
{
MeasuredVoltage = await _device!.(Ct());
MeasuredCurrent = await _device!.(Ct());
MeasuredPower = await _device!.(Ct());
AppendLog($"测量 → 电压:{MeasuredVoltage}V 电流:{MeasuredCurrent}A 功率:{MeasuredPower}W");
}));
Initialize();
}
#region / Navigation
public void Initialize(string? deviceName = null)
{
N69200? found = null;
string? foundName = null;
if (deviceName != null &&
_deviceManager.DeviceMap.TryGetValue(deviceName, out var d) &&
d is N69200 n)
{
found = n;
foundName = deviceName;
}
else
{
foreach (var kv in _deviceManager.DeviceMap)
{
if (kv.Value is N69200 n69)
{
found = n69;
foundName = kv.Key;
break;
}
}
}
_device = found;
DeviceName = foundName ?? "N69200 (未找到)";
IsConnected = _device?.IsConnected ?? false;
AppendLog(found != null
? $"已关联设备 [{DeviceName}],连接状态:{(IsConnected ? "" : "")}"
: "未在 DeviceManager 中找到 N69200 设备,请先初始化设备配置。");
}
public override void OnNavigatedTo(NavigationContext context)
{
var name = context.Parameters.GetValue<string?>("DeviceName");
Initialize(name);
}
#endregion
#region
private CancellationToken Ct() => (_cts = new CancellationTokenSource(TimeSpan.FromSeconds(10))).Token;
private async Task Exec(Func<Task> action)
{
if (_device == null)
{
AppendLog("错误:未关联到设备实例,请检查设备配置。");
return;
}
if (IsBusy) return;
IsBusy = true;
try
{
await action();
IsConnected = _device.IsConnected;
}
catch (OperationCanceledException)
{
AppendLog("命令超时或已取消。");
}
catch (Exception ex)
{
AppendLog($"错误:{ex.Message}");
}
finally
{
IsBusy = false;
}
}
private void AppendLog(string message)
{
var line = $"[{DateTime.Now:HH:mm:ss}] {message}";
ResponseLog = ResponseLog.Length > 4000
? line + "\n" + ResponseLog[..3000]
: line + "\n" + ResponseLog;
}
#endregion
public void Dispose()
{
_cts?.Cancel();
_cts?.Dispose();
}
}
}

View File

@@ -0,0 +1,292 @@
using DeviceCommand.Device;
using Prism.Commands;
using Prism.Ioc;
using System;
using System.Threading;
using System.Windows.Input;
using UIShare.GlobalVariable;
using UIShare.ViewModelBase;
namespace DeviceEditModule.ViewModels
{
/// <summary>
/// SDS2000X_HD 数字存储示波器控制面板 ViewModel。
/// </summary>
public class SDS2000X_HDViewModel : NavigateViewModelBase, IDisposable
{
#region
private readonly DeviceManager _deviceManager;
private SDS2000X_HD? _device;
private CancellationTokenSource? _cts;
#endregion
#region
private string _deviceName = "SDS2000X_HD";
public string DeviceName
{
get => _deviceName;
set => SetProperty(ref _deviceName, value);
}
private bool _isConnected;
public bool IsConnected
{
get => _isConnected;
set => SetProperty(ref _isConnected, value);
}
private bool _isBusy;
public bool IsBusy
{
get => _isBusy;
set => SetProperty(ref _isBusy, value);
}
#endregion
#region
private int _channel = 1;
/// <summary>当前操作通道1~4。</summary>
public int Channel
{
get => _channel;
set => SetProperty(ref _channel, value);
}
private double _voltsPerDiv = 1.0;
/// <summary>垂直电压档位V/div。</summary>
public double VoltsPerDiv
{
get => _voltsPerDiv;
set => SetProperty(ref _voltsPerDiv, value);
}
private double _offset = 0.0;
/// <summary>垂直偏移V。</summary>
public double Offset
{
get => _offset;
set => SetProperty(ref _offset, value);
}
private bool _is50Ohm;
/// <summary>是否使用 50Ω 输入阻抗false 为 1MΩ。</summary>
public bool Is50Ohm
{
get => _is50Ohm;
set => SetProperty(ref _is50Ohm, value);
}
private double _timeBase = 0.001;
/// <summary>水平时基档位s/div。</summary>
public double TimeBase
{
get => _timeBase;
set => SetProperty(ref _timeBase, value);
}
private double _triggerLevel = 0.0;
/// <summary>触发电平V。</summary>
public double TriggerLevel
{
get => _triggerLevel;
set => SetProperty(ref _triggerLevel, value);
}
private string _triggerSource = "C1";
/// <summary>触发源C1/C2/C3/C4/EX/LINE。</summary>
public string TriggerSource
{
get => _triggerSource;
set => SetProperty(ref _triggerSource, value);
}
#endregion
#region
private string _measuredVpp = "—";
public string MeasuredVpp
{
get => _measuredVpp;
set => SetProperty(ref _measuredVpp, value);
}
private string _measuredFrequency = "—";
public string MeasuredFrequency
{
get => _measuredFrequency;
set => SetProperty(ref _measuredFrequency, value);
}
private string _measuredRms = "—";
public string MeasuredRms
{
get => _measuredRms;
set => SetProperty(ref _measuredRms, value);
}
private string _responseLog = string.Empty;
/// <summary>命令响应日志(最新消息在顶部)。</summary>
public string ResponseLog
{
get => _responseLog;
set => SetProperty(ref _responseLog, value);
}
#endregion
#region
public ICommand QueryIdentityCommand { get; }
public ICommand ResetDeviceCommand { get; }
public ICommand RunCommand { get; }
public ICommand StopCommand { get; }
public ICommand SingleCommand { get; }
public ICommand ForceTriggerCommand { get; }
public ICommand SetChannelOnCommand { get; }
public ICommand SetChannelOffCommand { get; }
public ICommand SetVoltsDivCommand { get; }
public ICommand SetOffsetCommand { get; }
public ICommand SetTimeBaseCommand { get; }
public ICommand SetTriggerLevelCommand { get; }
public ICommand SetTriggerSourceCommand { get; }
public ICommand QueryMeasurementsCommand{ get; }
public ICommand QueryVppCommand { get; }
public ICommand QueryFrequencyCommand { get; }
public ICommand QueryRmsCommand { get; }
#endregion
public SDS2000X_HDViewModel(IContainerProvider containerProvider) : base(containerProvider)
{
_deviceManager = containerProvider.Resolve<DeviceManager>();
QueryIdentityCommand = new DelegateCommand(async () => await Exec(async () => AppendLog("IDN: " + await _device!.(Ct()))));
ResetDeviceCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(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("已停止捕获"); }));
SingleCommand = new DelegateCommand(async () => await Exec(async () => { await _device!._SINGLE(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} 已开启"); }));
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"); }));
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"); }));
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}"); }));
QueryMeasurementsCommand = new DelegateCommand(async () => await Exec(async () =>
{
MeasuredVpp = await _device!.(Channel, Ct());
MeasuredFrequency = await _device!.(Channel, Ct());
MeasuredRms = await _device!.(Channel, Ct());
AppendLog($"C{Channel} 测量 → Vpp:{MeasuredVpp} Freq:{MeasuredFrequency}Hz RMS:{MeasuredRms}V");
}));
QueryVppCommand = new DelegateCommand(async () => await Exec(async () => { MeasuredVpp = await _device!.(Channel, Ct()); AppendLog($"C{Channel} Vpp: {MeasuredVpp}"); }));
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}"); }));
Initialize();
}
#region / Navigation
public void Initialize(string? deviceName = null)
{
SDS2000X_HD? found = null;
string? foundName = null;
if (deviceName != null &&
_deviceManager.DeviceMap.TryGetValue(deviceName, out var d) &&
d is SDS2000X_HD s)
{
found = s;
foundName = deviceName;
}
else
{
foreach (var kv in _deviceManager.DeviceMap)
{
if (kv.Value is SDS2000X_HD sds)
{
found = sds;
foundName = kv.Key;
break;
}
}
}
_device = found;
DeviceName = foundName ?? "SDS2000X_HD (未找到)";
IsConnected = _device?.IsConnected ?? false;
AppendLog(found != null
? $"已关联设备 [{DeviceName}],连接状态:{(IsConnected ? "" : "")}"
: "未在 DeviceManager 中找到 SDS2000X_HD 设备,请先初始化设备配置。");
}
public override void OnNavigatedTo(NavigationContext context)
{
var name = context.Parameters.GetValue<string?>("DeviceName");
Initialize(name);
}
#endregion
#region
private CancellationToken Ct() => (_cts = new CancellationTokenSource(TimeSpan.FromSeconds(10))).Token;
private async Task Exec(Func<Task> action)
{
if (_device == null)
{
AppendLog("错误:未关联到设备实例,请检查设备配置。");
return;
}
if (IsBusy) return;
IsBusy = true;
try
{
await action();
IsConnected = _device.IsConnected;
}
catch (OperationCanceledException)
{
AppendLog("命令超时或已取消。");
}
catch (Exception ex)
{
AppendLog($"错误:{ex.Message}");
}
finally
{
IsBusy = false;
}
}
private void AppendLog(string message)
{
var line = $"[{DateTime.Now:HH:mm:ss}] {message}";
ResponseLog = ResponseLog.Length > 4000
? line + "\n" + ResponseLog[..3000]
: line + "\n" + ResponseLog;
}
#endregion
public void Dispose()
{
_cts?.Cancel();
_cts?.Dispose();
}
}
}

View File

@@ -0,0 +1,303 @@
using DeviceCommand.Device;
using Prism.Commands;
using Prism.Ioc;
using System;
using System.Threading;
using System.Windows.Input;
using UIShare.GlobalVariable;
using UIShare.ViewModelBase;
namespace DeviceEditModule.ViewModels
{
/// <summary>
/// SPAW7000 功率分析记录仪控制面板 ViewModel。
/// </summary>
public class SPAW7000ViewModel : NavigateViewModelBase, IDisposable
{
#region
private readonly DeviceManager _deviceManager;
private SPAW7000? _device;
private CancellationTokenSource? _cts;
#endregion
#region
private string _deviceName = "SPAW7000";
public string DeviceName
{
get => _deviceName;
set => SetProperty(ref _deviceName, value);
}
private bool _isConnected;
public bool IsConnected
{
get => _isConnected;
set => SetProperty(ref _isConnected, value);
}
private bool _isBusy;
public bool IsBusy
{
get => _isBusy;
set => SetProperty(ref _isBusy, value);
}
#endregion
#region
private int _channel = 1;
/// <summary>当前操作通道。</summary>
public int Channel
{
get => _channel;
set => SetProperty(ref _channel, value);
}
private double _voltageRange = 300.0;
/// <summary>电压量程V。</summary>
public double VoltageRange
{
get => _voltageRange;
set => SetProperty(ref _voltageRange, value);
}
private double _currentRange = 5.0;
/// <summary>电流量程A。</summary>
public double CurrentRange
{
get => _currentRange;
set => SetProperty(ref _currentRange, value);
}
private string _couplingMode = "DC";
/// <summary>耦合模式AC / DC / ACDC。</summary>
public string CouplingMode
{
get => _couplingMode;
set => SetProperty(ref _couplingMode, value);
}
private int _resolution = 6;
/// <summary>显示分辨率5 或 6。</summary>
public int Resolution
{
get => _resolution;
set => SetProperty(ref _resolution, value);
}
private int _brightness = 5;
/// <summary>屏幕亮度1~10。</summary>
public int Brightness
{
get => _brightness;
set => SetProperty(ref _brightness, value);
}
#endregion
#region
private string _measuredVoltage = "—";
public string MeasuredVoltage
{
get => _measuredVoltage;
set => SetProperty(ref _measuredVoltage, value);
}
private string _measuredCurrent = "—";
public string MeasuredCurrent
{
get => _measuredCurrent;
set => SetProperty(ref _measuredCurrent, value);
}
private string _measuredPower = "—";
public string MeasuredPower
{
get => _measuredPower;
set => SetProperty(ref _measuredPower, value);
}
private string _measuredFrequency = "—";
public string MeasuredFrequency
{
get => _measuredFrequency;
set => SetProperty(ref _measuredFrequency, value);
}
private string _measuredPowerFactor = "—";
public string MeasuredPowerFactor
{
get => _measuredPowerFactor;
set => SetProperty(ref _measuredPowerFactor, value);
}
private string _deviceModel = "—";
public string DeviceModel
{
get => _deviceModel;
set => SetProperty(ref _deviceModel, value);
}
private string _deviceSerial = "—";
public string DeviceSerial
{
get => _deviceSerial;
set => SetProperty(ref _deviceSerial, value);
}
private string _responseLog = string.Empty;
/// <summary>命令响应日志(最新消息在顶部)。</summary>
public string ResponseLog
{
get => _responseLog;
set => SetProperty(ref _responseLog, value);
}
#endregion
#region
public ICommand QueryIdentityCommand { get; }
public ICommand ResetDeviceCommand { get; }
public ICommand QueryAllMeasureCommand { get; }
public ICommand SetVoltageRangeCommand { get; }
public ICommand SetCurrentRangeCommand { get; }
public ICommand SetCouplingModeCommand { get; }
public ICommand SetResolutionCommand { get; }
public ICommand SetBrightnessCommand { get; }
public ICommand SetTouchLockOnCommand { get; }
public ICommand SetTouchLockOffCommand { get; }
public ICommand QueryModelCommand { get; }
public ICommand QuerySerialCommand { get; }
public ICommand QueryStatusByteCommand { get; }
#endregion
public SPAW7000ViewModel(IContainerProvider containerProvider) : base(containerProvider)
{
_deviceManager = containerProvider.Resolve<DeviceManager>();
QueryIdentityCommand = new DelegateCommand(async () => await Exec(async () => AppendLog("IDN: " + await _device!.(Ct()))));
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"); }));
SetCurrentRangeCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(Channel, CurrentRange, Ct()); AppendLog($"通道 {Channel} 电流量程已设为 {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} 位"); }));
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("屏幕触摸已锁定"); }));
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}"); }));
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()))));
QueryAllMeasureCommand = new DelegateCommand(async () => await Exec(async () =>
{
MeasuredVoltage = await _device!.(Channel, Ct());
MeasuredCurrent = await _device!.(Channel, Ct());
MeasuredPower = await _device!.(Channel, Ct());
MeasuredFrequency = await _device!.(Channel, Ct());
MeasuredPowerFactor = await _device!.(Channel, Ct());
AppendLog($"CH{Channel} 测量 → U:{MeasuredVoltage}V I:{MeasuredCurrent}A P:{MeasuredPower}W F:{MeasuredFrequency}Hz PF:{MeasuredPowerFactor}");
}));
Initialize();
}
#region / Navigation
public void Initialize(string? deviceName = null)
{
SPAW7000? found = null;
string? foundName = null;
if (deviceName != null &&
_deviceManager.DeviceMap.TryGetValue(deviceName, out var d) &&
d is SPAW7000 s)
{
found = s;
foundName = deviceName;
}
else
{
foreach (var kv in _deviceManager.DeviceMap)
{
if (kv.Value is SPAW7000 spaw)
{
found = spaw;
foundName = kv.Key;
break;
}
}
}
_device = found;
DeviceName = foundName ?? "SPAW7000 (未找到)";
IsConnected = _device?.IsConnected ?? false;
AppendLog(found != null
? $"已关联设备 [{DeviceName}],连接状态:{(IsConnected ? "" : "")}"
: "未在 DeviceManager 中找到 SPAW7000 设备,请先初始化设备配置。");
}
public override void OnNavigatedTo(NavigationContext context)
{
var name = context.Parameters.GetValue<string?>("DeviceName");
Initialize(name);
}
#endregion
#region
private CancellationToken Ct() => (_cts = new CancellationTokenSource(TimeSpan.FromSeconds(10))).Token;
private async Task Exec(Func<Task> action)
{
if (_device == null)
{
AppendLog("错误:未关联到设备实例,请检查设备配置。");
return;
}
if (IsBusy) return;
IsBusy = true;
try
{
await action();
IsConnected = _device.IsConnected;
}
catch (OperationCanceledException)
{
AppendLog("命令超时或已取消。");
}
catch (Exception ex)
{
AppendLog($"错误:{ex.Message}");
}
finally
{
IsBusy = false;
}
}
private void AppendLog(string message)
{
var line = $"[{DateTime.Now:HH:mm:ss}] {message}";
ResponseLog = ResponseLog.Length > 4000
? line + "\n" + ResponseLog[..3000]
: line + "\n" + ResponseLog;
}
#endregion
public void Dispose()
{
_cts?.Cancel();
_cts?.Dispose();
}
}
}

View File

@@ -0,0 +1,249 @@
<UserControl x:Class="DeviceEditModule.Views.N36600View"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:prism="http://prismlibrary.com/"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
mc:Ignorable="d"
prism:ViewModelLocator.AutoWireViewModel="True"
d:DesignHeight="760" d:DesignWidth="860">
<UserControl.Resources>
<converters:BooleanToVisibilityConverter x:Key="BoolToVis"/>
</UserControl.Resources>
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<StackPanel Margin="12">
<!-- ═══ 设备信息头 ═══ -->
<materialDesign:Card Margin="0,0,0,8" Padding="12,8">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<materialDesign:PackIcon Kind="BatteryCharging" Width="22" Height="22"
Foreground="#2E7D32" Margin="0,0,8,0"
VerticalAlignment="Center"/>
<TextBlock Text="N36600 便携式宽范围可编程直流电源"
FontSize="15" FontWeight="Bold"
VerticalAlignment="Center"/>
<TextBlock Text="{Binding DeviceName, StringFormat=' [{0}]'}"
FontSize="13" Foreground="#757575"
VerticalAlignment="Center" Margin="4,0,0,0"/>
</StackPanel>
<!-- 连接状态指示 -->
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
<Border Width="10" Height="10" CornerRadius="5" Margin="0,0,6,0">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="#F44336"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsConnected}" Value="True">
<Setter Property="Background" Value="#4CAF50"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
<TextBlock VerticalAlignment="Center" FontSize="12">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Text" Value="未连接"/>
<Setter Property="Foreground" Value="#F44336"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsConnected}" Value="True">
<Setter Property="Text" Value="已连接"/>
<Setter Property="Foreground" Value="#4CAF50"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<ProgressBar IsIndeterminate="True" Width="80" Height="4"
Margin="12,0,0,0"
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVis}}"/>
</StackPanel>
</Grid>
</materialDesign:Card>
<!-- ═══ 主体 2 列 ═══ -->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- ─── 左列 ─── -->
<StackPanel Grid.Column="0" Margin="0,0,4,0">
<!-- 输出控制 -->
<GroupBox Header="输出控制" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<StackPanel Margin="4,4,4,4">
<!-- 开关输出 -->
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="DC 输出" Style="{StaticResource ParamLabel}"/>
<Button Content="开启输出" Command="{Binding OutputOnCommand}"
Style="{StaticResource MaterialDesignRaisedButton}"
Background="#388E3C" Foreground="White"
Height="32" Padding="12,0" FontSize="12" Margin="4,0"/>
<Button Content="关闭输出" Command="{Binding OutputOffCommand}"
Style="{StaticResource WarnBtn}"/>
</StackPanel>
<!-- 远程/本地 -->
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="控制模式" Style="{StaticResource ParamLabel}"/>
<Button Content="远程控制" Command="{Binding SetRemoteModeCommand}"
Style="{StaticResource CmdBtn}"/>
<Button Content="本地控制" Command="{Binding SetLocalModeCommand}"
Style="{StaticResource CmdBtn}"/>
</StackPanel>
<!-- 清除保护 -->
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="清除保护" Style="{StaticResource ParamLabel}"/>
<Button Content="清除OVP" Command="{Binding ClearVoltageProtectionCommand}"
Style="{StaticResource WarnBtn}"/>
<Button Content="清除OCP" Command="{Binding ClearCurrentProtectionCommand}"
Style="{StaticResource WarnBtn}"/>
</StackPanel>
</StackPanel>
</GroupBox>
<!-- 参数设置 -->
<GroupBox Header="参数设置" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<StackPanel Margin="4,4,4,4">
<!-- 电压 -->
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="电压 (V)" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource NumInput}"
materialDesign:HintAssist.Hint=""
Text="{Binding Voltage, UpdateSourceTrigger=PropertyChanged}"/>
<Button Content="设置" Command="{Binding SetVoltageCommand}"
Style="{StaticResource CmdBtn}"/>
</StackPanel>
<!-- 电流 -->
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="电流 (A)" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource NumInput}"
materialDesign:HintAssist.Hint=""
Text="{Binding CurrentLimit, UpdateSourceTrigger=PropertyChanged}"/>
<Button Content="设置" Command="{Binding SetCurrentCommand}"
Style="{StaticResource CmdBtn}"/>
</StackPanel>
<!-- 功率 -->
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="功率 (W)" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource NumInput}"
materialDesign:HintAssist.Hint=""
Text="{Binding Power, UpdateSourceTrigger=PropertyChanged}"/>
<Button Content="设置" Command="{Binding SetPowerCommand}"
Style="{StaticResource CmdBtn}"/>
</StackPanel>
</StackPanel>
</GroupBox>
<!-- 保护设置 -->
<GroupBox Header="保护设置" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<StackPanel Margin="4,4,4,4">
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="OVP (V)" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource NumInput}"
materialDesign:HintAssist.Hint=""
Text="{Binding OvpValue, UpdateSourceTrigger=PropertyChanged}"/>
<Button Content="设置 OVP" Command="{Binding SetOvpCommand}"
Style="{StaticResource CmdBtn}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="OCP (A)" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource NumInput}"
materialDesign:HintAssist.Hint=""
Text="{Binding OcpValue, UpdateSourceTrigger=PropertyChanged}"/>
<Button Content="设置 OCP" Command="{Binding SetOcpCommand}"
Style="{StaticResource CmdBtn}"/>
</StackPanel>
</StackPanel>
</GroupBox>
</StackPanel>
<!-- ─── 右列 ─── -->
<StackPanel Grid.Column="1" Margin="4,0,0,0">
<!-- 实时测量 -->
<GroupBox Header="实时测量" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<StackPanel Margin="4,4,4,4">
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="实际电压" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource MeasureBox}"
materialDesign:HintAssist.Hint=""
Text="{Binding MeasuredVoltage, Mode=OneWay}"/>
<TextBlock Text="V" VerticalAlignment="Center" Margin="2,0,8,0"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="实际电流" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource MeasureBox}"
materialDesign:HintAssist.Hint=""
Text="{Binding MeasuredCurrent, Mode=OneWay}"/>
<TextBlock Text="A" VerticalAlignment="Center" Margin="2,0,8,0"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="实际功率" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource MeasureBox}"
materialDesign:HintAssist.Hint=""
Text="{Binding MeasuredPower, Mode=OneWay}"/>
<TextBlock Text="W" VerticalAlignment="Center" Margin="2,0,8,0"/>
</StackPanel>
<Button Content="刷新全部测量" Command="{Binding QueryAllMeasureCommand}"
Style="{StaticResource CmdBtn}"
HorizontalAlignment="Left" Margin="0,4,0,0"/>
</StackPanel>
</GroupBox>
<!-- 设备信息与状态 -->
<GroupBox Header="设备信息与状态" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<StackPanel Margin="4,4,4,4">
<StackPanel Orientation="Horizontal" Margin="0,4">
<Button Content="查询 IDN" Command="{Binding QueryIdentityCommand}"
Style="{StaticResource CmdBtn}"/>
<Button Content="查询输出状态" Command="{Binding QueryOutputStateCommand}"
Style="{StaticResource CmdBtn}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="输出状态" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource MeasureBox}" Width="160"
materialDesign:HintAssist.Hint=""
Text="{Binding OutputState, Mode=OneWay}"/>
</StackPanel>
</StackPanel>
</GroupBox>
<!-- 响应日志 -->
<GroupBox Header="响应日志" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<ScrollViewer Height="300" VerticalScrollBarVisibility="Auto">
<TextBox Text="{Binding ResponseLog, Mode=OneWay}"
materialDesign:HintAssist.Hint=""
IsReadOnly="True"
TextWrapping="Wrap"
FontSize="11"
FontFamily="Consolas"
Background="#FAFAFA"
BorderThickness="0"
VerticalAlignment="Top"/>
</ScrollViewer>
</GroupBox>
</StackPanel>
</Grid>
</StackPanel>
</ScrollViewer>
</UserControl>

View File

@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace DeviceEditModule.Views
{
/// <summary>
/// N36600View.xaml 的交互逻辑
/// </summary>
public partial class N36600View : UserControl
{
public N36600View()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,262 @@
<UserControl x:Class="DeviceEditModule.Views.N69200View"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:prism="http://prismlibrary.com/"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
mc:Ignorable="d"
prism:ViewModelLocator.AutoWireViewModel="True"
d:DesignHeight="760" d:DesignWidth="860">
<UserControl.Resources>
<converters:BooleanToVisibilityConverter x:Key="BoolToVis"/>
</UserControl.Resources>
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<StackPanel Margin="12">
<!-- ═══ 设备信息头 ═══ -->
<materialDesign:Card Margin="0,0,0,8" Padding="12,8">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<materialDesign:PackIcon Kind="CurrentDc" Width="22" Height="22"
Foreground="#1565C0" Margin="0,0,8,0"
VerticalAlignment="Center"/>
<TextBlock Text="N69200 可编程直流电子负载"
FontSize="15" FontWeight="Bold"
VerticalAlignment="Center"/>
<TextBlock Text="{Binding DeviceName, StringFormat=' [{0}]'}"
FontSize="13" Foreground="#757575"
VerticalAlignment="Center" Margin="4,0,0,0"/>
</StackPanel>
<!-- 连接状态指示 -->
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
<Border Width="10" Height="10" CornerRadius="5" Margin="0,0,6,0">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="#F44336"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsConnected}" Value="True">
<Setter Property="Background" Value="#4CAF50"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
<TextBlock VerticalAlignment="Center" FontSize="12">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Text" Value="未连接"/>
<Setter Property="Foreground" Value="#F44336"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsConnected}" Value="True">
<Setter Property="Text" Value="已连接"/>
<Setter Property="Foreground" Value="#4CAF50"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<ProgressBar IsIndeterminate="True" Width="80" Height="4"
Margin="12,0,0,0"
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVis}}"/>
</StackPanel>
</Grid>
</materialDesign:Card>
<!-- ═══ 主体 2 列 ═══ -->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- ─── 左列 ─── -->
<StackPanel Grid.Column="0" Margin="0,0,4,0">
<!-- 输入控制 -->
<GroupBox Header="输入控制" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<StackPanel Margin="4,4,4,4">
<!-- 开关输入 -->
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="DC 输入" Style="{StaticResource ParamLabel}"/>
<Button Content="开启输入" Command="{Binding InputOnCommand}"
Style="{StaticResource MaterialDesignRaisedButton}"
Background="#388E3C" Foreground="White"
Height="32" Padding="12,0" FontSize="12" Margin="4,0"/>
<Button Content="关闭输入" Command="{Binding InputOffCommand}"
Style="{StaticResource WarnBtn}"/>
</StackPanel>
<!-- 工作模式 -->
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="工作模式" Style="{StaticResource ParamLabel}"/>
<ComboBox Width="90" Height="32" Margin="4,0"
materialDesign:HintAssist.Hint=""
SelectedItem="{Binding SelectedMode}"
VerticalContentAlignment="Center" FontSize="12">
<ComboBoxItem Content="CC"/>
<ComboBoxItem Content="CV"/>
<ComboBoxItem Content="CP"/>
<ComboBoxItem Content="CR"/>
</ComboBox>
<Button Content="设置" Command="{Binding SetModeCommand}"
Style="{StaticResource CmdBtn}"/>
<Button Content="查询" Command="{Binding QueryModeCommand}"
Style="{StaticResource CmdBtn}"/>
</StackPanel>
<!-- 远程/本地 -->
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="控制模式" Style="{StaticResource ParamLabel}"/>
<Button Content="远程控制" Command="{Binding SetRemoteModeCommand}"
Style="{StaticResource CmdBtn}"/>
<Button Content="本地控制" Command="{Binding SetLocalModeCommand}"
Style="{StaticResource CmdBtn}"/>
</StackPanel>
<!-- 系统操作 -->
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="系统操作" Style="{StaticResource ParamLabel}"/>
<Button Content="清除告警" Command="{Binding ClearAlarmCommand}"
Style="{StaticResource WarnBtn}"/>
<Button Content="重置设备" Command="{Binding ResetDeviceCommand}"
Style="{StaticResource WarnBtn}"/>
</StackPanel>
</StackPanel>
</GroupBox>
<!-- 参数设置 -->
<GroupBox Header="参数设置" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<StackPanel Margin="4,4,4,4">
<!-- 设定值 -->
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="设定值" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource NumInput}"
materialDesign:HintAssist.Hint=""
Text="{Binding LoadValue, UpdateSourceTrigger=PropertyChanged}"/>
<Button Content="设置" Command="{Binding SetLoadValueCommand}"
Style="{StaticResource CmdBtn}"/>
</StackPanel>
</StackPanel>
</GroupBox>
<!-- 保护设置 -->
<GroupBox Header="保护设置" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<StackPanel Margin="4,4,4,4">
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="OVP (V)" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource NumInput}"
materialDesign:HintAssist.Hint=""
Text="{Binding OvpValue, UpdateSourceTrigger=PropertyChanged}"/>
<Button Content="设置 OVP" Command="{Binding SetOvpCommand}"
Style="{StaticResource CmdBtn}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="OCP (A)" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource NumInput}"
materialDesign:HintAssist.Hint=""
Text="{Binding OcpValue, UpdateSourceTrigger=PropertyChanged}"/>
<Button Content="设置 OCP" Command="{Binding SetOcpCommand}"
Style="{StaticResource CmdBtn}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="OPP (W)" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource NumInput}"
materialDesign:HintAssist.Hint=""
Text="{Binding OppValue, UpdateSourceTrigger=PropertyChanged}"/>
<Button Content="设置 OPP" Command="{Binding SetOppCommand}"
Style="{StaticResource CmdBtn}"/>
</StackPanel>
</StackPanel>
</GroupBox>
</StackPanel>
<!-- ─── 右列 ─── -->
<StackPanel Grid.Column="1" Margin="4,0,0,0">
<!-- 实时测量 -->
<GroupBox Header="实时测量" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<StackPanel Margin="4,4,4,4">
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="实际电压" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource MeasureBox}"
materialDesign:HintAssist.Hint=""
Text="{Binding MeasuredVoltage, Mode=OneWay}"/>
<TextBlock Text="V" VerticalAlignment="Center" Margin="2,0,8,0"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="实际电流" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource MeasureBox}"
materialDesign:HintAssist.Hint=""
Text="{Binding MeasuredCurrent, Mode=OneWay}"/>
<TextBlock Text="A" VerticalAlignment="Center" Margin="2,0,8,0"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="实际功率" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource MeasureBox}"
materialDesign:HintAssist.Hint=""
Text="{Binding MeasuredPower, Mode=OneWay}"/>
<TextBlock Text="W" VerticalAlignment="Center" Margin="2,0,8,0"/>
</StackPanel>
<Button Content="刷新全部测量" Command="{Binding QueryAllMeasureCommand}"
Style="{StaticResource CmdBtn}"
HorizontalAlignment="Left" Margin="0,4,0,0"/>
</StackPanel>
</GroupBox>
<!-- 设备信息与状态 -->
<GroupBox Header="设备信息与状态" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<StackPanel Margin="4,4,4,4">
<StackPanel Orientation="Horizontal" Margin="0,4">
<Button Content="查询 IDN" Command="{Binding QueryIdentityCommand}"
Style="{StaticResource CmdBtn}"/>
<Button Content="查询状态字节" Command="{Binding QueryStatusCommand}"
Style="{StaticResource CmdBtn}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="当前模式" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource MeasureBox}" Width="120"
materialDesign:HintAssist.Hint=""
Text="{Binding CurrentMode, Mode=OneWay}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="状态字节" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource MeasureBox}" Width="120"
materialDesign:HintAssist.Hint=""
Text="{Binding StatusByte, Mode=OneWay}"/>
</StackPanel>
</StackPanel>
</GroupBox>
<!-- 响应日志 -->
<GroupBox Header="响应日志" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<ScrollViewer Height="260" VerticalScrollBarVisibility="Auto">
<TextBox Text="{Binding ResponseLog, Mode=OneWay}"
materialDesign:HintAssist.Hint=""
IsReadOnly="True"
TextWrapping="Wrap"
FontSize="11"
FontFamily="Consolas"
Background="#FAFAFA"
BorderThickness="0"
VerticalAlignment="Top"/>
</ScrollViewer>
</GroupBox>
</StackPanel>
</Grid>
</StackPanel>
</ScrollViewer>
</UserControl>

View File

@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace DeviceEditModule.Views
{
/// <summary>
/// N69200View.xaml 的交互逻辑
/// </summary>
public partial class N69200View : UserControl
{
public N69200View()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,246 @@
<UserControl x:Class="DeviceEditModule.Views.SDS2000X_HDView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:prism="http://prismlibrary.com/"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
mc:Ignorable="d"
prism:ViewModelLocator.AutoWireViewModel="True"
d:DesignHeight="760" d:DesignWidth="860">
<UserControl.Resources>
<converters:BooleanToVisibilityConverter x:Key="BoolToVis"/>
</UserControl.Resources>
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<StackPanel Margin="12">
<!-- ═══ 设备信息头 ═══ -->
<materialDesign:Card Margin="0,0,0,8" Padding="12,8">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<materialDesign:PackIcon Kind="Waveform" Width="22" Height="22"
Foreground="#1565C0" Margin="0,0,8,0"
VerticalAlignment="Center"/>
<TextBlock Text="SDS2000X_HD 数字存储示波器"
FontSize="15" FontWeight="Bold"
VerticalAlignment="Center"/>
<TextBlock Text="{Binding DeviceName, StringFormat=' [{0}]'}"
FontSize="13" Foreground="#757575"
VerticalAlignment="Center" Margin="4,0,0,0"/>
</StackPanel>
<!-- 连接状态指示 -->
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
<Border Width="10" Height="10" CornerRadius="5" Margin="0,0,6,0">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="#F44336"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsConnected}" Value="True">
<Setter Property="Background" Value="#4CAF50"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
<TextBlock VerticalAlignment="Center" FontSize="12">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Text" Value="未连接"/>
<Setter Property="Foreground" Value="#F44336"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsConnected}" Value="True">
<Setter Property="Text" Value="已连接"/>
<Setter Property="Foreground" Value="#4CAF50"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<ProgressBar IsIndeterminate="True" Width="80" Height="4"
Margin="12,0,0,0"
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVis}}"/>
</StackPanel>
</Grid>
</materialDesign:Card>
<!-- ═══ 主体 2 列 ═══ -->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- ─── 左列 ─── -->
<StackPanel Grid.Column="0" Margin="0,0,4,0">
<!-- 通道选择 -->
<GroupBox Header="通道选择" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<StackPanel Margin="4,4,4,4">
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="通道号" Style="{StaticResource ParamLabel}"/>
<ComboBox Width="80" Height="32" Margin="4,0"
SelectedItem="{Binding Channel}"
VerticalContentAlignment="Center" FontSize="12">
<ComboBoxItem Content="1"/>
<ComboBoxItem Content="2"/>
<ComboBoxItem Content="3"/>
<ComboBoxItem Content="4"/>
</ComboBox>
<Button Content="开启" Command="{Binding SetChannelOnCommand}"
Style="{StaticResource CmdBtn}"/>
<Button Content="关闭" Command="{Binding SetChannelOffCommand}"
Style="{StaticResource WarnBtn}"/>
</StackPanel>
</StackPanel>
</GroupBox>
<!-- 运行控制 -->
<GroupBox Header="运行控制" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<StackPanel Margin="4,4,4,4">
<StackPanel Orientation="Horizontal" Margin="0,4">
<Button Content="Run" Command="{Binding RunCommand}"
Style="{StaticResource MaterialDesignRaisedButton}"
Background="#388E3C" Foreground="White"
Height="32" Padding="16,0" FontSize="12" Margin="4,0"/>
<Button Content="Stop" Command="{Binding StopCommand}"
Style="{StaticResource WarnBtn}"/>
<Button Content="Single" Command="{Binding SingleCommand}"
Style="{StaticResource CmdBtn}"/>
<Button Content="Force Trig" Command="{Binding ForceTriggerCommand}"
Style="{StaticResource CmdBtn}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<Button Content="查询 IDN" Command="{Binding QueryIdentityCommand}"
Style="{StaticResource CmdBtn}"/>
<Button Content="重置设备" Command="{Binding ResetDeviceCommand}"
Style="{StaticResource WarnBtn}"/>
</StackPanel>
</StackPanel>
</GroupBox>
<!-- 垂直设置 -->
<GroupBox Header="垂直设置" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<StackPanel Margin="4,4,4,4">
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="V/div (V)" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource NumInput}"
Text="{Binding VoltsPerDiv, UpdateSourceTrigger=PropertyChanged}"/>
<Button Content="设置" Command="{Binding SetVoltsDivCommand}"
Style="{StaticResource CmdBtn}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="Offset (V)" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource NumInput}"
Text="{Binding Offset, UpdateSourceTrigger=PropertyChanged}"/>
<Button Content="设置" Command="{Binding SetOffsetCommand}"
Style="{StaticResource CmdBtn}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="输入阻抗" Style="{StaticResource ParamLabel}"/>
<Button Content="50Ω" Command="{Binding SetImpedance50Command}"
Style="{StaticResource CmdBtn}"/>
<Button Content="1MΩ" Command="{Binding SetImpedance1MCommand}"
Style="{StaticResource CmdBtn}"/>
</StackPanel>
</StackPanel>
</GroupBox>
<!-- 水平/触发设置 -->
<GroupBox Header="水平与触发" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<StackPanel Margin="4,4,4,4">
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="时基 (s/div)" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource NumInput}"
Text="{Binding TimeBase, UpdateSourceTrigger=PropertyChanged}"/>
<Button Content="设置" Command="{Binding SetTimeBaseCommand}"
Style="{StaticResource CmdBtn}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="触发电平 (V)" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource NumInput}"
Text="{Binding TriggerLevel, UpdateSourceTrigger=PropertyChanged}"/>
<Button Content="设置" Command="{Binding SetTriggerLevelCommand}"
Style="{StaticResource CmdBtn}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="触发源" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource NumInput}" Width="80"
Text="{Binding TriggerSource, UpdateSourceTrigger=PropertyChanged}"/>
<Button Content="设置" Command="{Binding SetTriggerSourceCommand}"
Style="{StaticResource CmdBtn}"/>
</StackPanel>
</StackPanel>
</GroupBox>
</StackPanel>
<!-- ─── 右列 ─── -->
<StackPanel Grid.Column="1" Margin="4,0,0,0">
<!-- 自动测量 -->
<GroupBox Header="自动测量" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<StackPanel Margin="4,4,4,4">
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="Vpp" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource MeasureBox}"
Text="{Binding MeasuredVpp, Mode=OneWay}"/>
<TextBlock Text="V" VerticalAlignment="Center" Margin="2,0,8,0"/>
<Button Content="刷新" Command="{Binding QueryVppCommand}"
Style="{StaticResource CmdBtn}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="频率" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource MeasureBox}"
Text="{Binding MeasuredFrequency, Mode=OneWay}"/>
<TextBlock Text="Hz" VerticalAlignment="Center" Margin="2,0,8,0"/>
<Button Content="刷新" Command="{Binding QueryFrequencyCommand}"
Style="{StaticResource CmdBtn}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="RMS" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource MeasureBox}"
Text="{Binding MeasuredRms, Mode=OneWay}"/>
<TextBlock Text="V" VerticalAlignment="Center" Margin="2,0,8,0"/>
<Button Content="刷新" Command="{Binding QueryRmsCommand}"
Style="{StaticResource CmdBtn}"/>
</StackPanel>
<Button Content="刷新全部测量" Command="{Binding QueryMeasurementsCommand}"
Style="{StaticResource CmdBtn}"
HorizontalAlignment="Left" Margin="0,4,0,0"/>
</StackPanel>
</GroupBox>
<!-- 响应日志 -->
<GroupBox Header="响应日志" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<ScrollViewer Height="460" VerticalScrollBarVisibility="Auto">
<TextBox Text="{Binding ResponseLog, Mode=OneWay}"
materialDesign:HintAssist.Hint=""
IsReadOnly="True"
TextWrapping="Wrap"
FontSize="11"
FontFamily="Consolas"
Background="#FAFAFA"
BorderThickness="0"
VerticalAlignment="Top"/>
</ScrollViewer>
</GroupBox>
</StackPanel>
</Grid>
</StackPanel>
</ScrollViewer>
</UserControl>

View File

@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace DeviceEditModule.Views
{
/// <summary>
/// SDS2000X_HDView.xaml 的交互逻辑
/// </summary>
public partial class SDS2000X_HDView : UserControl
{
public SDS2000X_HDView()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,255 @@
<UserControl x:Class="DeviceEditModule.Views.SPAW7000View"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:prism="http://prismlibrary.com/"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
mc:Ignorable="d"
prism:ViewModelLocator.AutoWireViewModel="True"
d:DesignHeight="760" d:DesignWidth="860">
<UserControl.Resources>
<converters:BooleanToVisibilityConverter x:Key="BoolToVis"/>
</UserControl.Resources>
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<StackPanel Margin="12">
<!-- ═══ 设备信息头 ═══ -->
<materialDesign:Card Margin="0,0,0,8" Padding="12,8">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<materialDesign:PackIcon Kind="ChartLine" Width="22" Height="22"
Foreground="#1565C0" Margin="0,0,8,0"
VerticalAlignment="Center"/>
<TextBlock Text="SPAW7000 功率分析记录仪"
FontSize="15" FontWeight="Bold"
VerticalAlignment="Center"/>
<TextBlock Text="{Binding DeviceName, StringFormat=' [{0}]'}"
FontSize="13" Foreground="#757575"
VerticalAlignment="Center" Margin="4,0,0,0"/>
</StackPanel>
<!-- 连接状态指示 -->
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
<Border Width="10" Height="10" CornerRadius="5" Margin="0,0,6,0">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="#F44336"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsConnected}" Value="True">
<Setter Property="Background" Value="#4CAF50"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
<TextBlock VerticalAlignment="Center" FontSize="12">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Text" Value="未连接"/>
<Setter Property="Foreground" Value="#F44336"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsConnected}" Value="True">
<Setter Property="Text" Value="已连接"/>
<Setter Property="Foreground" Value="#4CAF50"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<ProgressBar IsIndeterminate="True" Width="80" Height="4"
Margin="12,0,0,0"
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVis}}"/>
</StackPanel>
</Grid>
</materialDesign:Card>
<!-- ═══ 主体 2 列 ═══ -->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- ─── 左列 ─── -->
<StackPanel Grid.Column="0" Margin="0,0,4,0">
<!-- 通道与量程 -->
<GroupBox Header="通道与量程" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<StackPanel Margin="4,4,4,4">
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="通道号" Style="{StaticResource ParamLabel}"/>
<ComboBox Width="80" Height="32" Margin="4,0"
SelectedItem="{Binding Channel}"
VerticalContentAlignment="Center" FontSize="12">
<ComboBoxItem Content="1"/>
<ComboBoxItem Content="2"/>
<ComboBoxItem Content="3"/>
<ComboBoxItem Content="4"/>
</ComboBox>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="电压量程 (V)" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource NumInput}"
Text="{Binding VoltageRange, UpdateSourceTrigger=PropertyChanged}"/>
<Button Content="设置" Command="{Binding SetVoltageRangeCommand}"
Style="{StaticResource CmdBtn}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="电流量程 (A)" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource NumInput}"
Text="{Binding CurrentRange, UpdateSourceTrigger=PropertyChanged}"/>
<Button Content="设置" Command="{Binding SetCurrentRangeCommand}"
Style="{StaticResource CmdBtn}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="耦合模式" Style="{StaticResource ParamLabel}"/>
<ComboBox Width="90" Height="32" Margin="4,0"
SelectedItem="{Binding CouplingMode}"
VerticalContentAlignment="Center" FontSize="12">
<ComboBoxItem Content="AC"/>
<ComboBoxItem Content="DC"/>
<ComboBoxItem Content="ACDC"/>
</ComboBox>
<Button Content="设置" Command="{Binding SetCouplingModeCommand}"
Style="{StaticResource CmdBtn}"/>
</StackPanel>
</StackPanel>
</GroupBox>
<!-- 系统设置 -->
<GroupBox Header="系统设置" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<StackPanel Margin="4,4,4,4">
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="分辨率" Style="{StaticResource ParamLabel}"/>
<ComboBox Width="80" Height="32" Margin="4,0"
SelectedItem="{Binding Resolution}"
VerticalContentAlignment="Center" FontSize="12">
<ComboBoxItem Content="5"/>
<ComboBoxItem Content="6"/>
</ComboBox>
<Button Content="设置" Command="{Binding SetResolutionCommand}"
Style="{StaticResource CmdBtn}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="亮度 (1~10)" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource NumInput}" Width="60"
Text="{Binding Brightness, UpdateSourceTrigger=PropertyChanged}"/>
<Button Content="设置" Command="{Binding SetBrightnessCommand}"
Style="{StaticResource CmdBtn}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="触摸锁" Style="{StaticResource ParamLabel}"/>
<Button Content="锁定" Command="{Binding SetTouchLockOnCommand}"
Style="{StaticResource CmdBtn}"/>
<Button Content="解锁" Command="{Binding SetTouchLockOffCommand}"
Style="{StaticResource WarnBtn}"/>
</StackPanel>
</StackPanel>
</GroupBox>
<!-- 设备信息 -->
<GroupBox Header="设备信息" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<StackPanel Margin="4,4,4,4">
<StackPanel Orientation="Horizontal" Margin="0,4">
<Button Content="查询 IDN" Command="{Binding QueryIdentityCommand}"
Style="{StaticResource CmdBtn}"/>
<Button Content="重置设备" Command="{Binding ResetDeviceCommand}"
Style="{StaticResource WarnBtn}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<Button Content="查询型号" Command="{Binding QueryModelCommand}"
Style="{StaticResource CmdBtn}"/>
<Button Content="查询序列号" Command="{Binding QuerySerialCommand}"
Style="{StaticResource CmdBtn}"/>
<Button Content="查询 STB" Command="{Binding QueryStatusByteCommand}"
Style="{StaticResource CmdBtn}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="型号" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource MeasureBox}" Width="120"
Text="{Binding DeviceModel, Mode=OneWay}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="序列号" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource MeasureBox}" Width="160"
Text="{Binding DeviceSerial, Mode=OneWay}"/>
</StackPanel>
</StackPanel>
</GroupBox>
</StackPanel>
<!-- ─── 右列 ─── -->
<StackPanel Grid.Column="1" Margin="4,0,0,0">
<!-- 实时测量 -->
<GroupBox Header="实时测量" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<StackPanel Margin="4,4,4,4">
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="电压" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource MeasureBox}"
Text="{Binding MeasuredVoltage, Mode=OneWay}"/>
<TextBlock Text="V" VerticalAlignment="Center" Margin="2,0,8,0"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="电流" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource MeasureBox}"
Text="{Binding MeasuredCurrent, Mode=OneWay}"/>
<TextBlock Text="A" VerticalAlignment="Center" Margin="2,0,8,0"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="功率" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource MeasureBox}"
Text="{Binding MeasuredPower, Mode=OneWay}"/>
<TextBlock Text="W" VerticalAlignment="Center" Margin="2,0,8,0"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="频率" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource MeasureBox}"
Text="{Binding MeasuredFrequency, Mode=OneWay}"/>
<TextBlock Text="Hz" VerticalAlignment="Center" Margin="2,0,8,0"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="功率因数" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource MeasureBox}"
Text="{Binding MeasuredPowerFactor, Mode=OneWay}"/>
</StackPanel>
<Button Content="刷新全部测量" Command="{Binding QueryAllMeasureCommand}"
Style="{StaticResource CmdBtn}"
HorizontalAlignment="Left" Margin="0,4,0,0"/>
</StackPanel>
</GroupBox>
<!-- 响应日志 -->
<GroupBox Header="响应日志" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<ScrollViewer Height="360" VerticalScrollBarVisibility="Auto">
<TextBox Text="{Binding ResponseLog, Mode=OneWay}"
materialDesign:HintAssist.Hint=""
IsReadOnly="True"
TextWrapping="Wrap"
FontSize="11"
FontFamily="Consolas"
Background="#FAFAFA"
BorderThickness="0"
VerticalAlignment="Top"/>
</ScrollViewer>
</GroupBox>
</StackPanel>
</Grid>
</StackPanel>
</ScrollViewer>
</UserControl>

View File

@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace DeviceEditModule.Views
{
/// <summary>
/// SPAW7000View.xaml 的交互逻辑
/// </summary>
public partial class SPAW7000View : UserControl
{
public SPAW7000View()
{
InitializeComponent();
}
}
}

View File

@@ -10,4 +10,10 @@
<PackageReference Include="NLog" Version="6.1.3" /> <PackageReference Include="NLog" Version="6.1.3" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Update="Nlog.config">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project> </Project>

View File

@@ -12,23 +12,23 @@ namespace Logger
{ {
public static readonly ILogger Logger = LogManager.GetLogger("InfoLogger"); public static readonly ILogger Logger = LogManager.GetLogger("InfoLogger");
public static readonly ILogger sqlLogger = LogManager.GetLogger("SqlLogger"); public static readonly ILogger sqlLogger = LogManager.GetLogger("SqlLogger");
public static IProgress<(string message, string color, int depth)> Progress { get; set; } public static IProgress<(string scope, string message, string color, int depth)> Progress { get; set; }
static LoggerHelper() static LoggerHelper()
{ {
Progress = new Progress<(string message, string color, int depth)>(); Progress = new Progress<(string scope, string message, string color, int depth)>();
} }
public static void InfoWithNotify(string message, int depth = 0) public static void InfoWithNotify(string scope, string message, int depth = 0)
{
Logger.Info(message); // 写入 NLog
NotifyUI(message, "blue", depth); // 触发UI显示
}
public static void SuccessWithNotify(string message, int depth = 0)
{ {
Logger.Info(message); Logger.Info(message);
NotifyUI(message, "lightgreen", depth); NotifyUI(scope, message, "blue", depth);
} }
public static void WarnWithNotify(string message, string stackTrace = null, int depth = 0)
public static void SuccessWithNotify(string scope, string message, int depth = 0)
{
Logger.Info(message);
NotifyUI(scope, message, "lightgreen", depth);
}
public static void WarnWithNotify(string scope, string message, string stackTrace = null, int depth = 0)
{ {
if (!string.IsNullOrEmpty(stackTrace)) if (!string.IsNullOrEmpty(stackTrace))
{ {
@@ -37,10 +37,10 @@ namespace Logger
} }
Logger.Warn(message); Logger.Warn(message);
NotifyUI(message, "orange", depth); NotifyUI(scope, message, "orange", depth);
} }
public static void ErrorWithNotify(string message, string stackTrace = null, int depth = 0) public static void ErrorWithNotify(string scope, string message, string stackTrace = null, int depth = 0)
{ {
if (!string.IsNullOrEmpty(stackTrace)) if (!string.IsNullOrEmpty(stackTrace))
{ {
@@ -49,11 +49,11 @@ namespace Logger
} }
Logger.Error(message); Logger.Error(message);
NotifyUI(message, "red", depth); NotifyUI(scope, message, "red", depth);
} }
private static void NotifyUI(string message, string color, int depth) private static void NotifyUI(string scope, string message, string color, int depth)
{ {
Progress.Report((message, color, depth)); Progress.Report((scope, message, color, depth));
} }
public static void Info(string message, int depth = 0) public static void Info(string message, int depth = 0)
@@ -95,20 +95,18 @@ namespace Logger
foreach (var line in lines) foreach (var line in lines)
{ {
// 匹配你项目的命名空间路径
if (line.Contains("ADP")) if (line.Contains("ADP"))
{ {
// 提取 "in 文件路径:line 行号"
var match = Regex.Match(line, @"in (.+?):line (\d+)"); var match = Regex.Match(line, @"in (.+?):line (\d+)");
if (match.Success) if (match.Success)
{ {
return match.Value; // 返回类似 C:\...\MainViewModel.cs:line 37 return match.Value;
} }
return line.Trim(); return line.Trim();
} }
} }
return lines[0].Trim(); // 如果找不到就返回第一条 return lines[0].Trim();
} }
} }
} }

View File

@@ -3,7 +3,7 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<!-- 定义目标 --> <!-- 定义目标 -->
<targets> <targets async="true">
<!-- SQL 日志 --> <!-- SQL 日志 -->
<target name="sqlLog" xsi:type="File" <target name="sqlLog" xsi:type="File"
fileName="Logs/${shortdate}_sql.txt" fileName="Logs/${shortdate}_sql.txt"

View File

@@ -110,7 +110,7 @@ namespace MainModule.ViewModels
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LoggerHelper.ErrorWithNotify($"卸载机台 [{TestStatus}] 全局引用或资源失败: {ex.Message}"); Logger.LoggerHelper.ErrorWithNotify(TestStatus, $"卸载机台 [{TestStatus}] 全局引用或资源失败: {ex.Message}");
} }
finally finally
{ {
@@ -164,7 +164,7 @@ namespace MainModule.ViewModels
if (program == null) if (program == null)
{ {
LoggerHelper.WarnWithNotify($"文件格式不正确或为空: {filePath}"); LoggerHelper.WarnWithNotify(_globalInfo.CurrentScope, $"文件格式不正确或为空: {filePath}");
return; return;
} }
@@ -173,6 +173,15 @@ namespace MainModule.ViewModels
_scopedContext.Program.StepCollection = program.StepCollection; _scopedContext.Program.StepCollection = program.StepCollection;
_scopedContext.Program.ErrorStepCollection = program.ErrorStepCollection; _scopedContext.Program.ErrorStepCollection = program.ErrorStepCollection;
_scopedContext.CurrentFilePath = filePath; _scopedContext.CurrentFilePath = filePath;
foreach (var item in _systemConfig.SharedParameterList)
{
var parameter = _scopedContext?.Program?.Parameters?.FirstOrDefault(x => x.Name == item.ParameterName);
if (parameter != null)
{
parameter.Value = item.Value;
}
}
} }
} }

View File

@@ -24,7 +24,6 @@ namespace Model.Models
/// </summary> /// </summary>
public string? Category { get; set; } public string? Category { get; set; }
public bool IsGlobal { get; set; }
public object? Value { get; set; } public object? Value { get; set; }
@@ -36,7 +35,6 @@ namespace Model.Models
public bool IsUseVar { get; set; } public bool IsUseVar { get; set; }
public bool IsSave { get; set; }
public string? VariableName { get; set; } public string? VariableName { get; set; }

View File

@@ -217,7 +217,7 @@ namespace MonitorModule.ViewModels
} }
catch (Exception ex) catch (Exception ex)
{ {
LoggerHelper.ErrorWithNotify($"加载数据表失败:{ex.Message}"); LoggerHelper.ErrorWithNotify(_globalInfo.CurrentScope,$"加载数据表失败:{ex.Message}");
StatusMessage = $"加载失败:{ex.Message}"; StatusMessage = $"加载失败:{ex.Message}";
} }
} }
@@ -256,7 +256,7 @@ namespace MonitorModule.ViewModels
} }
catch (Exception ex) catch (Exception ex)
{ {
LoggerHelper.ErrorWithNotify($"查询失败:{ex.Message}"); LoggerHelper.ErrorWithNotify(TestStatus,$"查询失败:{ex.Message}");
StatusMessage = $"查询失败:{ex.Message}"; StatusMessage = $"查询失败:{ex.Message}";
ResultTable = new DataTable(); ResultTable = new DataTable();
TotalCount = 0; TotalCount = 0;
@@ -295,11 +295,11 @@ namespace MonitorModule.ViewModels
WriteCsv(dlg.FileName, dt); WriteCsv(dlg.FileName, dt);
StatusMessage = $"导出完成:{dlg.FileName}{dt.Rows.Count} 行)"; StatusMessage = $"导出完成:{dlg.FileName}{dt.Rows.Count} 行)";
LoggerHelper.InfoWithNotify($"工位 [{TestStatus}] 导出 [{SelectedTable}] 至 {dlg.FileName},共 {dt.Rows.Count} 行"); LoggerHelper.InfoWithNotify(TestStatus, $"工位 [{TestStatus}] 导出 [{SelectedTable}] 至 {dlg.FileName},共 {dt.Rows.Count} 行");
} }
catch (Exception ex) catch (Exception ex)
{ {
LoggerHelper.ErrorWithNotify($"导出失败:{ex.Message}"); LoggerHelper.ErrorWithNotify(TestStatus, $"导出失败:{ex.Message}");
StatusMessage = $"导出失败:{ex.Message}"; StatusMessage = $"导出失败:{ex.Message}";
} }
} }

View File

@@ -45,6 +45,11 @@ namespace SettingModule.ViewModels
get => _deviceList; get => _deviceList;
set => SetProperty(ref _deviceList, value); set => SetProperty(ref _deviceList, value);
} }
public ObservableCollection<SharedParameter> SharedParameterList
{
get => _sharedParameterList;
set => SetProperty(ref _sharedParameterList, value);
}
public string StatusMessage public string StatusMessage
{ {
@@ -72,6 +77,7 @@ namespace SettingModule.ViewModels
private string _testStatus = string.Empty; private string _testStatus = string.Empty;
private DeviceInfoVM? _selectedDevice; private DeviceInfoVM? _selectedDevice;
private ObservableCollection<DeviceInfoVM> _deviceList; private ObservableCollection<DeviceInfoVM> _deviceList;
private ObservableCollection<SharedParameter> _sharedParameterList;
private string _statusMessage = "请在左侧选择设备查看 / 编辑配置"; private string _statusMessage = "请在左侧选择设备查看 / 编辑配置";
#endregion #endregion
public SettingViewModel(IContainerExtension container) : base(container) public SettingViewModel(IContainerExtension container) : base(container)
@@ -95,7 +101,7 @@ namespace SettingModule.ViewModels
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LoggerHelper.ErrorWithNotify($"释放配置管理组件SettingViewModel资源失败: {ex.Message}"); Logger.LoggerHelper.ErrorWithNotify(TestStatus,$"释放配置管理组件SettingViewModel资源失败: {ex.Message}");
} }
} }
@@ -199,6 +205,20 @@ namespace SettingModule.ViewModels
SelectedDevice = DeviceList[0]; SelectedDevice = DeviceList[0];
} }
DeviceList = SystemConfig.DeviceList; DeviceList = SystemConfig.DeviceList;
SharedParameterList=SystemConfig.SharedParameterList;
if (SharedParameterList.Count == 0)
{
foreach (var device in SystemConfig.ParameterList)
{
var sharedParam = new SharedParameter
{
Id = device.ID.ToString(),
ParameterName = device.Name,
Value = device.Value is int intVal ? intVal : Convert.ToInt32(device.Value)
};
SharedParameterList.Add(sharedParam);
}
}
IsInitiated = true; IsInitiated = true;
} }
} }

View File

@@ -462,6 +462,34 @@
</StackPanel> </StackPanel>
</Border> </Border>
<Border Background="White"
BorderBrush="#E0E0E0" BorderThickness="1"
CornerRadius="4" Padding="14"
Margin="0,0,0,12">
<StackPanel>
<TextBlock Text="公共变量SharedParameterList" FontWeight="Bold" FontSize="14" Margin="0,0,0,10"/>
<TextBlock Foreground="#888" FontSize="12" TextWrapping="Wrap"
Text="每个台架可独立设置自己的通道号等整型变量;新建或打开程序时会用此处值覆盖程序中的同名变量。" Margin="0,0,0,10"/>
<ItemsControl ItemsSource="{Binding SharedParameterList}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Margin="0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="160"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding ParameterName}" VerticalAlignment="Center"/>
<TextBox Grid.Column="1"
VerticalAlignment="Center"
materialDesign:HintAssist.Hint=""
Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
<Border Background="White" <Border Background="White"
BorderBrush="#E0E0E0" BorderThickness="1" BorderBrush="#E0E0E0" BorderThickness="1"
CornerRadius="4" Padding="14" CornerRadius="4" Padding="14"

View File

@@ -189,7 +189,7 @@ namespace TestingModule.ViewModels
{ {
try try
{ {
var assembly = Assembly.LoadFile(dllPath); var assembly = Assembly.LoadFrom(dllPath);
Assemblies.Add(assembly); Assemblies.Add(assembly);
// 加载对应的XML注释文件 (项目没有用到) // 加载对应的XML注释文件 (项目没有用到)
@@ -210,7 +210,7 @@ namespace TestingModule.ViewModels
} }
catch (Exception ex) catch (Exception ex)
{ {
LoggerHelper.WarnWithNotify($"无法加载程序集 {Path.GetFileName(dllPath)}: {ex.Message}"); LoggerHelper.WarnWithNotify(_globalInfo.CurrentScope, $"无法加载程序集 {Path.GetFileName(dllPath)}: {ex.Message}");
} }
} }
} }
@@ -237,7 +237,7 @@ namespace TestingModule.ViewModels
} }
catch (Exception ex) catch (Exception ex)
{ {
LoggerHelper.WarnWithNotify($"加载子程序错误: {filePath} - {ex.Message}"); LoggerHelper.WarnWithNotify(_globalInfo.CurrentScope, $"加载子程序错误: {filePath} - {ex.Message}");
} }
} }
} }
@@ -317,7 +317,7 @@ namespace TestingModule.ViewModels
} }
catch (Exception ex) catch (Exception ex)
{ {
LoggerHelper.ErrorWithNotify($"加载类型错误: {assembly.FullName} - {ex.Message}"); LoggerHelper.ErrorWithNotify(_globalInfo.CurrentScope, $"加载类型错误: {assembly.FullName} - {ex.Message}");
} }
if (validTypes.Count > 0) if (validTypes.Count > 0)
@@ -504,7 +504,7 @@ namespace TestingModule.ViewModels
} }
catch (Exception ex) catch (Exception ex)
{ {
LoggerHelper.ErrorWithNotify($"添加方法失败: {method.Name} - {ex.Message}"); LoggerHelper.ErrorWithNotify(_globalInfo.CurrentScope, $"添加方法失败: {method.Name} - {ex.Message}");
} }
} }
@@ -537,7 +537,7 @@ namespace TestingModule.ViewModels
} }
catch (Exception ex) catch (Exception ex)
{ {
LoggerHelper.ErrorWithNotify($"添加子程序失败: {subProgram.Name} - {ex.Message}"); LoggerHelper.ErrorWithNotify(_globalInfo.CurrentScope, $"添加子程序失败: {subProgram.Name} - {ex.Message}"); ;
} }
} }

View File

@@ -1,9 +1,10 @@
using UIShare.GlobalVariable; using UIShare.GlobalVariable;
using Logger; using Logger;
using Prism.Ioc;
using Prism.Mvvm; using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Reflection;
using System.Windows;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Threading; using System.Windows.Threading;
@@ -21,26 +22,54 @@ namespace TestingModule.ViewModels
get => _logs; get => _logs;
set => SetProperty(ref _logs, value); set => SetProperty(ref _logs, value);
} }
public ICommand ClearLogCommand { get; set; } public ICommand ClearLogCommand { get; set; }
private readonly ScopedContext _scopedContext;
private readonly GlobalInfo _globalInfo;
/// <summary>UI 线程定时器,每 100ms 批量从 LogBuffer 出队并刷新</summary>
private readonly DispatcherTimer _flushTimer;
public LogAreaViewModel(IContainerProvider containerProvider) : base(containerProvider) public LogAreaViewModel(IContainerProvider containerProvider) : base(containerProvider)
{ {
ClearLogCommand = new DelegateCommand(ClearLog); ClearLogCommand = new DelegateCommand(ClearLog);
_scopedContext = containerProvider.Resolve<ScopedContext>();
_globalInfo = containerProvider.Resolve<GlobalInfo>();
// 2. 保持原有逻辑,但请确保在 Dispose 中对其进行清理 // UI 线程定时器:每 100ms 从 ScopedContext.LogBuffer 批量出队并 Add
LoggerHelper.Progress = new System.Progress<(string message, string color, int depth)>( _flushTimer = new DispatcherTimer(DispatcherPriority.Normal, System.Windows.Application.Current.Dispatcher)
log => {
{ Interval = TimeSpan.FromMilliseconds(100)
// 增加防御性代码:防止进入销毁流程时异步回调引发空引用异常 };
if (Logs == null) return; _flushTimer.Tick += FlushLogBuffer;
_flushTimer.Start();
}
var brush = (Brush)new BrushConverter().ConvertFromString(log.color); /// <summary>
Logs.Add(new LogItem(log.message, brush, log.depth)); /// 定时批量刷新日志:一次性出队并 AddWPF 对连续 Add 做合并渲染
}); /// </summary>
private void FlushLogBuffer(object? sender, EventArgs e)
{
if (_scopedContext.LogBuffer.IsEmpty || Logs == null) return;
var batch = new List<(string Message, Brush Color, int Depth)>();
while (_scopedContext.LogBuffer.TryDequeue(out var item))
{
batch.Add(item);
}
// 批量 AddWPF 会对连续 Add 做合并渲染,只触发一次 layout/render pass
foreach (var item in batch)
{
Logs.Add(new LogItem(item.Message, item.Color, item.Depth));
}
} }
private void ClearLog() private void ClearLog()
{ {
// 清空缓冲区,防止定时器下次还把旧数据刷出来
while (_scopedContext.LogBuffer.TryDequeue(out _)) { }
Logs?.Clear(); Logs?.Clear();
} }
@@ -51,7 +80,13 @@ namespace TestingModule.ViewModels
{ {
try try
{ {
LoggerHelper.Progress = null!; // 停止定时器
_flushTimer.Stop();
_flushTimer.Tick -= FlushLogBuffer;
// 最后一次刷新缓冲区中的剩余日志
FlushLogBuffer(null, EventArgs.Empty);
if (Logs != null) if (Logs != null)
{ {
Logs.Clear(); Logs.Clear();
@@ -60,7 +95,9 @@ namespace TestingModule.ViewModels
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LoggerHelper.ErrorWithNotify($"释放日志组件LogAreaViewModel资源失败: {ex.Message}"); Logger.LoggerHelper.ErrorWithNotify(
_scopedContext != null ? _globalInfo.CurrentScope : "",
$"释放日志组件LogAreaViewModel资源失败: {ex.Message}");
} }
} }
} }
@@ -71,6 +108,7 @@ namespace TestingModule.ViewModels
public string Message { get; set; } public string Message { get; set; }
public Brush Color { get; set; } = Brushes.Black; public Brush Color { get; set; } = Brushes.Black;
public int Depth { get; set; } public int Depth { get; set; }
public LogItem(string message, Brush color, int depth = 0) public LogItem(string message, Brush color, int depth = 0)
{ {
Message = new string(' ', depth * 20) + message; Message = new string(' ', depth * 20) + message;

View File

@@ -16,13 +16,6 @@ namespace TestingModule.ViewModels
public class ParametersManagerViewModel:NavigateViewModelBase, IDisposable public class ParametersManagerViewModel:NavigateViewModelBase, IDisposable
{ {
#region #region
//private ObservableCollection<DeviceConfigModel> _DeviceList;
//public ObservableCollection<DeviceConfigModel> DeviceList
//{
// get { return _DeviceList; }
// set { SetProperty(ref _DeviceList,value); }
//}
private ObservableCollection<DeviceInfoVM> _DeviceInfoModel; private ObservableCollection<DeviceInfoVM> _DeviceInfoModel;
public ObservableCollection<DeviceInfoVM> DeviceInfoVM public ObservableCollection<DeviceInfoVM> DeviceInfoVM
@@ -82,7 +75,6 @@ namespace TestingModule.ViewModels
public ParametersManagerViewModel(IContainerProvider containerProvider) : base(containerProvider) public ParametersManagerViewModel(IContainerProvider containerProvider) : base(containerProvider)
{ {
_ScopedContext = containerProvider.Resolve<ScopedContext>(); _ScopedContext = containerProvider.Resolve<ScopedContext>();
_systemConfig = containerProvider.Resolve<SystemConfig>(); _systemConfig = containerProvider.Resolve<SystemConfig>();
_deviceManager = containerProvider.Resolve<DeviceManager>(); _deviceManager = containerProvider.Resolve<DeviceManager>();
@@ -96,8 +88,24 @@ namespace TestingModule.ViewModels
ReConnectCommand = new AsyncDelegateCommand(OnReConnect); ReConnectCommand = new AsyncDelegateCommand(OnReConnect);
CloseCommand = new AsyncDelegateCommand(OnClose); CloseCommand = new AsyncDelegateCommand(OnClose);
LoadedCommand = new DelegateCommand(OnLoad); LoadedCommand = new DelegateCommand(OnLoad);
InitParameters();
} }
#region #region
private void InitParameters()
{
foreach(var item in _systemConfig.ParameterList)
{
var copy=new ParameterVM(item);
var param = _systemConfig.SharedParameterList.FirstOrDefault(x => x.ParameterName == copy.Name);
if (param != null)
{
copy.Value = param.Value;
}
Program.Parameters.Add(copy);
}
}
private void OnLoad() private void OnLoad()
{ {
DeviceInfoVM = _systemConfig.DeviceList; DeviceInfoVM = _systemConfig.DeviceList;
@@ -147,10 +155,14 @@ namespace TestingModule.ViewModels
} }
// 4. 发布事件 → DialogMangerViewModel 接收后将此 View 添加为 Tab // 4. 发布事件 → DialogMangerViewModel 接收后将此 View 添加为 Tab
// 标题格式:{当前作用域} - {设备名称}
// 去重依据:设备硬件指纹(同一物理设备不重复打开)
var fingerprint = DeviceManager.ExtractHardwareFingerprint(SelectedDevice);
_eventAggregator.GetEvent<AddDialogTabEvent>().Publish(new DialogTabInfo _eventAggregator.GetEvent<AddDialogTabEvent>().Publish(new DialogTabInfo
{ {
Title = $"{type} [{SelectedDevice.DeviceName}]", Title = $"{_globalInfo.CurrentScope} - {SelectedDevice.DeviceName}",
Content = view Fingerprint = fingerprint,
Content = view
}); });
// 5. 将窗口置顶(确保用户看到新增的 Tab // 5. 将窗口置顶(确保用户看到新增的 Tab
@@ -163,7 +175,7 @@ namespace TestingModule.ViewModels
} }
catch (Exception ex) catch (Exception ex)
{ {
LoggerHelper.ErrorWithNotify($"打开设备编辑窗口 [{type}] 失败:{ex.Message}"); LoggerHelper.ErrorWithNotify(_globalInfo.CurrentScope, $"打开设备编辑窗口 [{type}] 失败:{ex.Message}");
} }
} }

View File

@@ -47,6 +47,7 @@ namespace TestingModule.ViewModels
} }
#endregion #endregion
private ScopedContext _ScopedContext; private ScopedContext _ScopedContext;
private GlobalInfo _globalInfo;
#region #region
public ICommand CancelEditCommand { get; set; } public ICommand CancelEditCommand { get; set; }
public ICommand SaveStepCommand { get; set; } public ICommand SaveStepCommand { get; set; }
@@ -54,6 +55,7 @@ namespace TestingModule.ViewModels
public SingleStepEditViewModel(IContainerProvider containerProvider, ScopedContext scopedContext) : base(containerProvider) public SingleStepEditViewModel(IContainerProvider containerProvider, ScopedContext scopedContext) : base(containerProvider)
{ {
_ScopedContext = scopedContext; _ScopedContext = scopedContext;
_globalInfo=containerProvider.Resolve<GlobalInfo>();
_eventAggregator.GetEvent<EditSetpEvent>().Subscribe(EditSingleStep); _eventAggregator.GetEvent<EditSetpEvent>().Subscribe(EditSingleStep);
CancelEditCommand = new DelegateCommand(CancelEdit); CancelEditCommand = new DelegateCommand(CancelEdit);
SaveStepCommand = new DelegateCommand(SaveStep); SaveStepCommand = new DelegateCommand(SaveStep);
@@ -100,7 +102,7 @@ namespace TestingModule.ViewModels
} }
catch catch
{ {
LoggerHelper.ErrorWithNotify("循环指令参数设置错误:类型转换失败"); LoggerHelper.ErrorWithNotify(_globalInfo.CurrentScope,"循环指令参数设置错误:类型转换失败");
} }
} }
else else

View File

@@ -110,10 +110,7 @@
ItemsSource="{Binding EnumValues}" ItemsSource="{Binding EnumValues}"
SelectedItem="{Binding Parameter.Value}" SelectedItem="{Binding Parameter.Value}"
Visibility="{Binding Parameter.Type, Converter={StaticResource IsEnumTypeConverter}}" />--> Visibility="{Binding Parameter.Type, Converter={StaticResource IsEnumTypeConverter}}" />-->
<CheckBox Margin="10,0"
VerticalAlignment="Bottom"
Content="保存数据"
IsChecked="{Binding Parameter.IsSave}" />
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>

View File

@@ -62,7 +62,7 @@ namespace UIShare.GlobalVariable
} }
catch (Exception ex) catch (Exception ex)
{ {
LoggerHelper.ErrorWithNotify($"格子 [{title}] 配置加载失败: {ex.Message}"); LoggerHelper.ErrorWithNotify(title, $"格子 [{title}] 配置加载失败: {ex.Message}");
return new SystemConfig { Title = title }; return new SystemConfig { Title = title };
} }
} }
@@ -90,11 +90,11 @@ namespace UIShare.GlobalVariable
}); });
File.WriteAllText(configPath, json); File.WriteAllText(configPath, json);
LoggerHelper.InfoWithNotify($"配置 [{config.Title}] 已保存。"); LoggerHelper.InfoWithNotify(config.Title, $"配置 [{config.Title}] 已保存。");
} }
catch (Exception ex) catch (Exception ex)
{ {
LoggerHelper.ErrorWithNotify($"配置 [{config.Title}] 保存失败: {ex.Message}"); LoggerHelper.ErrorWithNotify(config.Title, $"配置 [{config.Title}] 保存失败: {ex.Message}");
} }
} }
} }

View File

@@ -15,10 +15,12 @@ namespace UIShare.GlobalVariable
/// 设备管理器:根据 <see cref="SystemConfig.DeviceList"/> 反射实例化所有启用的设备, /// 设备管理器:根据 <see cref="SystemConfig.DeviceList"/> 反射实例化所有启用的设备,
/// 通过 <see cref="IBaseInterface"/> 多态统一管理,避免为每种设备单独硬编码字段。 /// 通过 <see cref="IBaseInterface"/> 多态统一管理,避免为每种设备单独硬编码字段。
/// </summary> /// </summary>
public class DeviceManager public class DeviceManager:IDisposable
{ {
private object _lockObj = new object(); private object _lockObj = new object();
public SystemConfig _systemConfig { get; set; } public SystemConfig _systemConfig { get; set; }
private readonly GlobalInfo _globalInfo;
private readonly string _scopeName;
/// <summary>按 DeviceName 索引的设备字典,便于业务层按名取实例。</summary> /// <summary>按 DeviceName 索引的设备字典,便于业务层按名取实例。</summary>
public IDictionary<string, IBaseInterface> DeviceMap { get; private set; } public IDictionary<string, IBaseInterface> DeviceMap { get; private set; }
@@ -27,11 +29,35 @@ namespace UIShare.GlobalVariable
/// <summary>类名 → Type 的反射缓存(仅扫描一次)。</summary> /// <summary>类名 → Type 的反射缓存(仅扫描一次)。</summary>
private static readonly IReadOnlyDictionary<string, Type> _deviceTypeMap = BuildDeviceTypeMap(); private static readonly IReadOnlyDictionary<string, Type> _deviceTypeMap = BuildDeviceTypeMap();
public DeviceManager(SystemConfig systemConfig) public DeviceManager(SystemConfig systemConfig, GlobalInfo globalInfo)
{ {
_systemConfig = systemConfig; _systemConfig = systemConfig;
_globalInfo = globalInfo;
// 用 SystemConfig.Title 作为作用域唯一标识,无需反查 ConfigDic
_scopeName = _systemConfig.Title;
InitDevices(); InitDevices();
} }
/// <summary>
/// 根据设备配置提取唯一的硬件指纹字符串。
/// <para>Tcp → "Tcp:IP:Port"Serial → "Serial:PortName";无法识别则返回空字符串。</para>
/// </summary>
public static string ExtractHardwareFingerprint(DeviceInfoVM config)
{
if (string.Equals(config.ConnectionType, "Tcp", StringComparison.OrdinalIgnoreCase)
&& config.TcpConfig != null)
{
return $"Tcp:{config.TcpConfig.IPAddress}:{config.TcpConfig.Port}";
}
if (string.Equals(config.ConnectionType, "Serial", StringComparison.OrdinalIgnoreCase)
&& config.SerialPortConfig != null)
{
return $"Serial:{config.SerialPortConfig.PortName}";
}
return string.Empty;
}
private void InitDevices() private void InitDevices()
{ {
DeviceMap = new Dictionary<string, IBaseInterface>(StringComparer.OrdinalIgnoreCase); DeviceMap = new Dictionary<string, IBaseInterface>(StringComparer.OrdinalIgnoreCase);
@@ -51,29 +77,58 @@ namespace UIShare.GlobalVariable
try try
{ {
IBaseInterface? instance = config.ConnectionType switch // 第一步:提取硬件指纹,为空则跳过
var fingerprint = ExtractHardwareFingerprint(config);
if (string.IsNullOrEmpty(fingerprint))
{ {
"Tcp" => CreateTcpDevice(deviceType, config.TcpConfig), LoggerHelper.Warn($"设备 [{config.DeviceName}] 无法提取硬件指纹(连接方式={config.ConnectionType}),已跳过。");
"Serial" => CreateSerialDevice(deviceType, config.SerialPortConfig), continue;
_ => null }
};
// 第二步:原子化获取或添加 Lazy 包装盒,确保同一指纹全局只创建一个实例
var lazy = _globalInfo.HardwarePool.GetOrAdd(fingerprint, key => new Lazy<IBaseInterface>(() =>
{
return config.ConnectionType switch
{
"Tcp" => CreateTcpDevice(deviceType, config.TcpConfig)!,
"Serial" => CreateSerialDevice(deviceType, config.SerialPortConfig)!,
_ => null!
};
}));
// 第三步安全拆盒Lazy 内部线程锁保证只实例化一次
var instance = lazy.Value;
if (instance == null) if (instance == null)
{ {
LoggerHelper.Warn($"设备 [{config.DeviceName}] 连接方式 [{config.ConnectionType}] 不支持,已跳过。"); LoggerHelper.Warn($"设备 [{config.DeviceName}] 连接方式 [{config.ConnectionType}] 不支持,已跳过。");
continue; continue;
} }
// 第四步:绑定逻辑名 → 同一物理设备可被多个台架名称映射
if (!string.IsNullOrWhiteSpace(config.DeviceName)) if (!string.IsNullOrWhiteSpace(config.DeviceName))
{ {
DeviceMap[config.DeviceName] = instance; DeviceMap[config.DeviceName] = instance;
} }
LoggerHelper.Info($"已加载设备 [{config.DeviceName} / {config.DeviceType} / {config.ConnectionType}]"); // 第五步:将当前作用域注册到指纹的作用域列表,用于引用计数与安全销毁
if (!string.IsNullOrEmpty(_scopeName))
{
var scopeList = _globalInfo.DeviceAndScopeDic.GetOrAdd(fingerprint,
_ => new Lazy<List<string>>(() => new List<string>())).Value;
lock (scopeList)
{
if (!scopeList.Contains(_scopeName))
scopeList.Add(_scopeName);
}
}
LoggerHelper.Info($"已加载设备 [{config.DeviceName} / {config.DeviceType} / {config.ConnectionType}] 指纹={fingerprint}");
} }
catch (Exception ex) catch (Exception ex)
{ {
var inner = ex.InnerException?.Message ?? ex.Message; var inner = ex.InnerException?.Message ?? ex.Message;
LoggerHelper.ErrorWithNotify($"设备 [{config.DeviceName}] 实例化失败:{inner}"); LoggerHelper.ErrorWithNotify(_scopeName, $"设备 [{config.DeviceName}] 实例化失败:{inner}");
} }
} }
} }
@@ -233,7 +288,7 @@ namespace UIShare.GlobalVariable
{ {
if (info != null) info.IsConnected = false; if (info != null) info.IsConnected = false;
var inner = ex.InnerException?.Message ?? ex.Message; var inner = ex.InnerException?.Message ?? ex.Message;
LoggerHelper.ErrorWithNotify($"设备 [{name}/{conn}] 连接异常:{inner}"); LoggerHelper.ErrorWithNotify(_scopeName, $"设备 [{name}/{conn}] 连接异常:{inner}");
} }
} }
@@ -298,6 +353,50 @@ namespace UIShare.GlobalVariable
}; };
return Activator.CreateInstance(type, cfg) as IBaseInterface; return Activator.CreateInstance(type, cfg) as IBaseInterface;
} }
public void Dispose()
{
if (string.IsNullOrEmpty(_scopeName)) return;
// 遍历当前作用域用到的所有指纹,逐一移除本作用域的引用
var fingerprintsToRemove = new List<string>();
foreach (var kvp in _globalInfo.DeviceAndScopeDic)
{
string fingerprint = kvp.Key;
var scopeList = kvp.Value.IsValueCreated ? kvp.Value.Value : null;
if (scopeList == null) continue;
lock (scopeList)
{
scopeList.Remove(_scopeName);
// 引用归零 → 标记为待清理
if (scopeList.Count == 0)
fingerprintsToRemove.Add(fingerprint);
}
}
// 对引用归零的指纹:销毁设备实例 + 从全局池中移除
foreach (var fingerprint in fingerprintsToRemove)
{
// 尝试从 HardwarePool 取出并销毁
if (_globalInfo.HardwarePool.TryRemove(fingerprint, out var lazy))
{
if (lazy.IsValueCreated && lazy.Value is IBaseInterface device)
{
try { device.Close(); }
catch { /* 销毁时忽略异常 */ }
LoggerHelper.Info($"指纹 [{fingerprint}] 无作用域引用,已销毁设备实例。");
}
}
// 同步清除 DeviceAndScopeDic 中的空条目
_globalInfo.DeviceAndScopeDic.TryRemove(fingerprint, out _);
}
DeviceMap.Clear();
}
#endregion #endregion
} }
} }

View File

@@ -1,4 +1,6 @@
using System; using DeviceCommand.Base;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@@ -13,6 +15,13 @@ namespace UIShare.GlobalVariable
public Dictionary<string,StepRunning> StepRunningDic { get; set; } public Dictionary<string,StepRunning> StepRunningDic { get; set; }
public Dictionary<string, SystemConfig> ConfigDic { get; set; } public Dictionary<string, SystemConfig> ConfigDic { get; set; }
public Dictionary<string, IScopedProvider> ScopeDic { get; set; } public Dictionary<string, IScopedProvider> ScopeDic { get; set; }
/// <summary>硬件指纹 → 设备实例的并发池,确保同一物理硬件全局只创建一个驱动实例。</summary>
public ConcurrentDictionary<string, Lazy<IBaseInterface>> HardwarePool { get; set; }
/// <summary>硬件指纹 → 正在使用该设备的作用域名称列表,用于引用计数与安全销毁。</summary>
public ConcurrentDictionary<string, Lazy<List<string>>> DeviceAndScopeDic { get; set; }
public String UserName { get; set; } = "Not Logged in"; public String UserName { get; set; } = "Not Logged in";
public bool IsAdmin { get; set; } = true; public bool IsAdmin { get; set; } = true;
public string CurrentOpeningScope; public string CurrentOpeningScope;
@@ -35,6 +44,8 @@ namespace UIShare.GlobalVariable
StepRunningDic = new(); StepRunningDic = new();
ConfigDic = new(); ConfigDic = new();
ScopeDic = new(); ScopeDic = new();
HardwarePool = new ConcurrentDictionary<string, Lazy<IBaseInterface>>(StringComparer.OrdinalIgnoreCase);
DeviceAndScopeDic = new ConcurrentDictionary<string, Lazy<List<string>>>(StringComparer.OrdinalIgnoreCase);
CurrentScope = "default"; CurrentScope = "default";
} }

View File

@@ -0,0 +1,48 @@
using Logger;
using System;
using System.Collections.Concurrent;
using System.Windows.Media;
namespace UIShare.GlobalVariable
{
/// <summary>
/// 作用域日志分发器:根据显式传入的 scope 参数把日志路由到对应
/// <see cref="ScopedContext.LogBuffer"/>,实现每个台架/作用域拥有独立 LogArea。
/// 直接写入 ConcurrentQueue不走 Progress&lt;T&gt; / SynchronizationContext避免 UI 线程压力。
/// </summary>
public class ScopeLogDispatcher : IProgress<(string scope, string message, string color, int depth)>
{
private readonly GlobalInfo _globalInfo;
/// <summary>Brush 缓存,避免每次都 new BrushConverter</summary>
private static readonly ConcurrentDictionary<string, Brush> _brushCache = new();
public ScopeLogDispatcher(GlobalInfo globalInfo)
{
_globalInfo = globalInfo ?? throw new ArgumentNullException(nameof(globalInfo));
}
public void Report((string scope, string message, string color, int depth) value)
{
var scope = value.scope;
if (string.IsNullOrEmpty(scope)) return;
if (!_globalInfo.ContextDic.TryGetValue(scope, out var context)) return;
Brush brush = _brushCache.GetOrAdd(value.color, color =>
{
try
{
return (Brush)new BrushConverter().ConvertFromString(color);
}
catch
{
return Brushes.Black;
}
});
// 直接入队到后台线程安全的 ConcurrentQueue不走 SynchronizationContext
context.LogBuffer.Enqueue((value.message, brush, value.depth));
}
}
}

View File

@@ -1,6 +1,7 @@
using DeviceCommand.Base; using DeviceCommand.Base;
using MaterialDesignThemes.Wpf; using MaterialDesignThemes.Wpf;
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics; using System.Diagnostics;
@@ -8,6 +9,7 @@ using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Media;
using UIShare.UIViewModel; using UIShare.UIViewModel;
namespace UIShare.GlobalVariable namespace UIShare.GlobalVariable
@@ -30,6 +32,14 @@ namespace UIShare.GlobalVariable
public ParameterVM SelectedParameter { get; set; } public ParameterVM SelectedParameter { get; set; }
public List<IBaseInterface> DeviceList { get; set; } = new(); public List<IBaseInterface> DeviceList { get; set; } = new();
/// <summary>
/// 日志缓冲队列后台线程ScopeLogDispatcher直接入队
/// UI 线程DispatcherTimer定时出队并刷新到 ObservableCollection。
/// 不走 Progress&lt;T&gt; / SynchronizationContext避免 Post 淹没 UI 消息队列。
/// </summary>
public ConcurrentQueue<(string Message, Brush Color, int Depth)> LogBuffer { get; } = new();
// 【新增测试属性】:每个实例被 new 出来时独一无二的随机身份 // 【新增测试属性】:每个实例被 new 出来时独一无二的随机身份
// 证 ID // 证 ID
public int DebugRandomId { get; private set; } public int DebugRandomId { get; private set; }

View File

@@ -88,7 +88,7 @@ namespace UIShare
} }
else else
{ {
LoggerHelper.ErrorWithNotify("程序循环指令未闭合,请检查后重试"); LoggerHelper.ErrorWithNotify(_systemConfig.Title, "程序循环指令未闭合,请检查后重试");
break; break;
} }
} }
@@ -108,7 +108,7 @@ namespace UIShare
}; };
loopStack.Push(context); loopStack.Push(context);
step.CurrentLoopCount = context.LoopCount; step.CurrentLoopCount = context.LoopCount;
LoggerHelper.InfoWithNotify($"循环开始,共{context.LoopCount}次", depth); LoggerHelper.InfoWithNotify(_systemConfig.Title, $"循环开始,共{context.LoopCount}次", depth);
index++; index++;
} }
@@ -117,7 +117,7 @@ namespace UIShare
{ {
if (loopStack.Count == 0) if (loopStack.Count == 0)
{ {
LoggerHelper.ErrorWithNotify("未匹配的循环结束指令", depth: depth); LoggerHelper.ErrorWithNotify(_systemConfig.Title, "未匹配的循环结束指令", depth: depth);
step.Result = 2; step.Result = 2;
index++; index++;
continue; continue;
@@ -133,7 +133,7 @@ namespace UIShare
{ {
// 继续循环:跳转到循环开始后的第一条指令 // 继续循环:跳转到循环开始后的第一条指令
index = context.StartIndex + 1; index = context.StartIndex + 1;
LoggerHelper.InfoWithNotify($"循环第{context.CurrentLoop}次结束,跳回开始,剩余{context.LoopCount - context.CurrentLoop}次", depth); LoggerHelper.InfoWithNotify(_systemConfig.Title, $"循环第{context.CurrentLoop}次结束,跳回开始,剩余{context.LoopCount - context.CurrentLoop}次", depth);
} }
else else
{ {
@@ -141,7 +141,7 @@ namespace UIShare
loopStack.Pop(); loopStack.Pop();
var loopStopwatch = loopStopwatchStack.Peek(); var loopStopwatch = loopStopwatchStack.Peek();
index++; index++;
LoggerHelper.InfoWithNotify($"循环结束,共执行{context.LoopCount}次", depth); LoggerHelper.InfoWithNotify(_systemConfig.Title, $"循环结束,共执行{context.LoopCount}次", depth);
if (depth == 0 && loopStopwatch.IsRunning) if (depth == 0 && loopStopwatch.IsRunning)
{ {
loopStopwatch.Stop(); loopStopwatch.Stop();
@@ -168,7 +168,7 @@ namespace UIShare
SubSingleStep = true; SubSingleStep = true;
_scopedContext.SingleStep = false; _scopedContext.SingleStep = false;
} }
LoggerHelper.InfoWithNotify($"开始执行子程序 [ {step.Index} ] [ {step.Name} ] ", depth); LoggerHelper.InfoWithNotify(_systemConfig.Title, $"开始执行子程序 [ {step.Index} ] [ {step.Name} ] ", depth);
stepSuccess = await ExecuteSteps(step.SubProgram, depth + 1, cancellationToken); stepSuccess = await ExecuteSteps(step.SubProgram, depth + 1, cancellationToken);
UpdateCurrentStepResult(step, true, stepSuccess, depth); UpdateCurrentStepResult(step, true, stepSuccess, depth);
if (SubSingleStep) if (SubSingleStep)
@@ -179,7 +179,7 @@ namespace UIShare
} }
else if (step.Method != null) else if (step.Method != null)
{ {
LoggerHelper.InfoWithNotify($"开始执行指令 [ {step.Index} ] [ {step.Method!.FullName}.{step.Method.Name} ] ", depth); LoggerHelper.InfoWithNotify(_systemConfig.Title, $"开始执行指令 [ {step.Index} ] [ {step.Method!.FullName}.{step.Method.Name} ] ", depth);
await ExecuteMethodStep(step, tmpParameters, depth, cancellationToken); await ExecuteMethodStep(step, tmpParameters, depth, cancellationToken);
stepSuccess = step.Result == 1; stepSuccess = step.Result == 1;
if (step.NGGotoStepID != null && !stepSuccess) if (step.NGGotoStepID != null && !stepSuccess)
@@ -188,7 +188,7 @@ namespace UIShare
if (tmp != null) if (tmp != null)
{ {
index = tmp.Index - 2; index = tmp.Index - 2;
LoggerHelper.InfoWithNotify($"指令跳转 [ {tmp.Index} ] [ {tmp.Name} ]", depth); LoggerHelper.InfoWithNotify(_systemConfig.Title, $"指令跳转 [ {tmp.Index} ] [ {tmp.Name} ]", depth);
} }
} }
if (step.OKGotoStepID != null && stepSuccess) if (step.OKGotoStepID != null && stepSuccess)
@@ -197,7 +197,7 @@ namespace UIShare
if (tmp != null) if (tmp != null)
{ {
index = tmp.Index - 2; index = tmp.Index - 2;
LoggerHelper.InfoWithNotify($"指令跳转 [ {tmp.Index} ] [ {tmp.Name} ]", depth); LoggerHelper.InfoWithNotify(_systemConfig.Title, $"指令跳转 [ {tmp.Index} ] [ {tmp.Name} ]", depth);
} }
} }
} }
@@ -257,7 +257,7 @@ namespace UIShare
} }
else else
{ {
LoggerHelper.ErrorWithNotify("程序循环指令未闭合,请检查后重试"); LoggerHelper.ErrorWithNotify(_systemConfig.Title, "程序循环指令未闭合,请检查后重试");
break; break;
} }
} }
@@ -277,7 +277,7 @@ namespace UIShare
}; };
loopStack.Push(context); loopStack.Push(context);
step.CurrentLoopCount = context.LoopCount; step.CurrentLoopCount = context.LoopCount;
LoggerHelper.InfoWithNotify($"循环开始({step.Name}),共{context.LoopCount}次", depth); LoggerHelper.InfoWithNotify(_systemConfig.Title, $"循环开始({step.Name}),共{context.LoopCount}次", depth);
index++; index++;
} }
@@ -286,7 +286,7 @@ namespace UIShare
{ {
if (loopStack.Count == 0) if (loopStack.Count == 0)
{ {
LoggerHelper.ErrorWithNotify("未匹配的循环结束指令", depth:depth); LoggerHelper.ErrorWithNotify(_systemConfig.Title, "未匹配的循环结束指令", depth:depth);
step.Result = 2; step.Result = 2;
index++; index++;
continue; continue;
@@ -302,7 +302,7 @@ namespace UIShare
{ {
// 继续循环:跳转到循环开始后的第一条指令 // 继续循环:跳转到循环开始后的第一条指令
index = context.StartIndex + 1; index = context.StartIndex + 1;
LoggerHelper.InfoWithNotify($"循环第{context.CurrentLoop}次结束,跳回开始,剩余{context.LoopCount - context.CurrentLoop}次", depth); LoggerHelper.InfoWithNotify(_systemConfig.Title, $"循环第{context.CurrentLoop}次结束,跳回开始,剩余{context.LoopCount - context.CurrentLoop}次", depth);
} }
else else
{ {
@@ -310,7 +310,7 @@ namespace UIShare
loopStack.Pop(); loopStack.Pop();
var loopStopwatch = loopStopwatchStack.Peek(); var loopStopwatch = loopStopwatchStack.Peek();
index++; index++;
LoggerHelper.InfoWithNotify($"循环结束,共执行{context.LoopCount}次", depth); LoggerHelper.InfoWithNotify(_systemConfig.Title, $"循环结束,共执行{context.LoopCount}次", depth);
if (depth == 0 && loopStopwatch.IsRunning) if (depth == 0 && loopStopwatch.IsRunning)
{ {
loopStopwatch.Stop(); loopStopwatch.Stop();
@@ -337,7 +337,7 @@ namespace UIShare
SubSingleStep = true; SubSingleStep = true;
_scopedContext.SingleStep = false; _scopedContext.SingleStep = false;
} }
LoggerHelper.InfoWithNotify($"开始执行子程序 [ {step.Index} ] [ {step.Name} ] ", depth); LoggerHelper.InfoWithNotify(_systemConfig.Title, $"开始执行子程序 [ {step.Index} ] [ {step.Name} ] ", depth);
stepSuccess = await ExecuteSteps(step.SubProgram, depth + 1, cancellationToken); stepSuccess = await ExecuteSteps(step.SubProgram, depth + 1, cancellationToken);
UpdateCurrentStepResult(step, true, stepSuccess, depth); UpdateCurrentStepResult(step, true, stepSuccess, depth);
if (SubSingleStep) if (SubSingleStep)
@@ -348,7 +348,7 @@ namespace UIShare
} }
else if (step.Method != null) else if (step.Method != null)
{ {
LoggerHelper.InfoWithNotify($"开始执行指令 [ {step.Index} ] [ {step.Method!.FullName}.{step.Method.Name} ] ", depth); LoggerHelper.InfoWithNotify(_systemConfig.Title, $"开始执行指令 [ {step.Index} ] [ {step.Method!.FullName}.{step.Method.Name} ] ", depth);
await ExecuteMethodStep(step, tmpParameters, depth, cancellationToken); await ExecuteMethodStep(step, tmpParameters, depth, cancellationToken);
stepSuccess = step.Result == 1; stepSuccess = step.Result == 1;
if (step.NGGotoStepID != null && !stepSuccess) if (step.NGGotoStepID != null && !stepSuccess)
@@ -357,7 +357,7 @@ namespace UIShare
if (tmp != null) if (tmp != null)
{ {
index = tmp.Index - 2; index = tmp.Index - 2;
LoggerHelper.InfoWithNotify($"指令跳转 [ {tmp.Index} ] [ {tmp.Name} ]", depth); LoggerHelper.InfoWithNotify(_systemConfig.Title, $"指令跳转 [ {tmp.Index} ] [ {tmp.Name} ]", depth);
} }
} }
if (step.OKGotoStepID != null && stepSuccess) if (step.OKGotoStepID != null && stepSuccess)
@@ -366,7 +366,7 @@ namespace UIShare
if (tmp != null) if (tmp != null)
{ {
index = tmp.Index - 2; index = tmp.Index - 2;
LoggerHelper.InfoWithNotify($"指令跳转 [ {tmp.Index} ] [ {tmp.Name} ]", depth); LoggerHelper.InfoWithNotify(_systemConfig.Title, $"指令跳转 [ {tmp.Index} ] [ {tmp.Name} ]", depth);
} }
} }
} }
@@ -408,7 +408,7 @@ namespace UIShare
} }
if (targetType == null) if (targetType == null)
{ {
LoggerHelper.ErrorWithNotify($"指令 [ {step.Index} ] 执行错误:未找到类型 {step.Method!.FullName}", depth: depth); LoggerHelper.ErrorWithNotify(_systemConfig.Title, $"指令 [ {step.Index} ] 执行错误:未找到类型 {step.Method!.FullName}", depth: depth);
step.Result = 2; step.Result = 2;
} }
@@ -522,7 +522,7 @@ namespace UIShare
} }
catch (Exception ex) catch (Exception ex)
{ {
LoggerHelper.WarnWithNotify($"指令 [ {step.Index} ] 执行错误:参数 {param.Name} 类型转换失败: {ex.Message}", depth: depth); LoggerHelper.WarnWithNotify(_systemConfig.Title, $"指令 [ {step.Index} ] 执行错误:参数 {param.Name} 类型转换失败: {ex.Message}", depth: depth);
} }
} }
} }
@@ -546,7 +546,7 @@ namespace UIShare
if (method == null) if (method == null)
{ {
LoggerHelper.ErrorWithNotify($"指令 [ {step.Index} ] 执行错误:未找到方法{step.Method.Name}", depth: depth); LoggerHelper.ErrorWithNotify(_systemConfig.Title, $"指令 [ {step.Index} ] 执行错误:未找到方法{step.Method.Name}", depth: depth);
step.Result = 2; step.Result = 2;
} }
@@ -558,11 +558,11 @@ namespace UIShare
{ {
try try
{ {
//instance = _devices.DeviceDic[targetType.Name]; instance = _deviceManager.DeviceMap[targetType.Name];
} }
catch (Exception ex) catch (Exception ex)
{ {
LoggerHelper.ErrorWithNotify($"指令 [ {step.Index} ] 执行错误:创建实例失败 - {ex.Message}", depth: depth); LoggerHelper.ErrorWithNotify(_systemConfig.Title, $"指令 [ {step.Index} ] 执行错误:创建实例失败 - {ex.Message}", depth: depth);
step.Result = 2; step.Result = 2;
} }
} }
@@ -600,7 +600,7 @@ namespace UIShare
} }
catch (Exception ex) catch (Exception ex)
{ {
LoggerHelper.ErrorWithNotify($"指令 [ {step.Index} ] 执行错误: {ex.InnerException?.Message ?? ex.Message}", depth: depth); LoggerHelper.ErrorWithNotify(_systemConfig.Title, $"指令 [ {step.Index} ] 执行错误: {ex.InnerException?.Message ?? ex.Message}", depth: depth);
step.Result = 2; step.Result = 2;
return; return;
} }
@@ -619,31 +619,28 @@ namespace UIShare
paraResult = tmp.Item1; paraResult = tmp.Item1;
if (tmp.Item2 != null) if (tmp.Item2 != null)
{ {
LoggerHelper.WarnWithNotify(tmp.Item2); LoggerHelper.WarnWithNotify(_systemConfig.Title, tmp.Item2);
} }
//if (currentPara.IsSave && _scopedContext.Program.Parameters.FirstOrDefault(x => x.ID == currentPara.ID) != null)
//{
// _ = SaveDataToDatabase(_scopedContext.Program.ID, currentPara);
//}
} }
var returnType = returnValue?.GetType(); var returnType = returnValue?.GetType();
if (returnType != null) if (returnType != null)
{ {
if (!returnType.IsArray) if (!returnType.IsArray)
{ {
LoggerHelper.SuccessWithNotify($"输出 [ {outputParam.Name} ] = {returnValue} ({returnType.Name})", depth); LoggerHelper.SuccessWithNotify(_systemConfig.Title, $"输出 [ {outputParam.Name} ] = {returnValue} ({returnType.Name})", depth);
} }
else else
{ {
if (returnValue is IEnumerable enumerable) if (returnValue is IEnumerable enumerable)
{ {
var elements = enumerable.Cast<object>().Select(item => item?.ToString() ?? "null"); var elements = enumerable.Cast<object>().Select(item => item?.ToString() ?? "null");
LoggerHelper.SuccessWithNotify($"输出 [ {outputParam.Name} ] = [ {string.Join(", ", elements)} ] ({returnType.Name})", depth); LoggerHelper.SuccessWithNotify(_systemConfig.Title, $"输出 [ {outputParam.Name} ] = [ {string.Join(", ", elements)} ] ({returnType.Name})", depth);
} }
} }
} }
} }
LoggerHelper.SuccessWithNotify($"指令 [ {step.Index} ] 执行成功", depth); LoggerHelper.SuccessWithNotify(_systemConfig.Title, $"指令 [ {step.Index} ] 执行成功", depth);
UpdateCurrentStepResult(step, paraResult: paraResult, depth: depth); UpdateCurrentStepResult(step, paraResult: paraResult, depth: depth);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
@@ -652,7 +649,7 @@ namespace UIShare
} }
catch (Exception ex) catch (Exception ex)
{ {
LoggerHelper.ErrorWithNotify($"指令 [ {step.Index} ] 执行错误: {ex.InnerException?.Message ?? ex.Message}", depth: depth); LoggerHelper.ErrorWithNotify(_systemConfig.Title, $"指令 [ {step.Index} ] 执行错误: {ex.InnerException?.Message ?? ex.Message}", depth: depth);
step.Result = 2; step.Result = 2;
return; return;
} }
@@ -700,7 +697,7 @@ namespace UIShare
step.Result = re ? 1 : 2; step.Result = re ? 1 : 2;
if (step.Result == 2) if (step.Result == 2)
{ {
LoggerHelper.WarnWithNotify($"指令 [ {step.Index} ] NG:条件表达式验证失败", depth: depth); LoggerHelper.WarnWithNotify(_systemConfig.Title, $"指令 [ {step.Index} ] NG:条件表达式验证失败", depth: depth);
} }
} }
} }
@@ -708,7 +705,7 @@ namespace UIShare
{ {
if (!paraResult) if (!paraResult)
{ {
LoggerHelper.WarnWithNotify("参数限值校验失败", depth: depth); LoggerHelper.WarnWithNotify(_systemConfig.Title, "参数限值校验失败", depth: depth);
} }
step.Result = 2; step.Result = 2;
} }

View File

@@ -9,6 +9,7 @@ using System.Reflection;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using UIShare.UIViewModel; using UIShare.UIViewModel;
using static UIShare.UIViewModel.ParameterVM;
namespace UIShare.GlobalVariable namespace UIShare.GlobalVariable
{ {
@@ -27,6 +28,53 @@ namespace UIShare.GlobalVariable
public string DefaultBLFFilePath { get; set; } = ""; public string DefaultBLFFilePath { get; set; } = "";
public string DefaultDBCFilePath { get; set; } = ""; public string DefaultDBCFilePath { get; set; } = "";
public ObservableCollection<DeviceInfoVM> DeviceList = new(); public ObservableCollection<DeviceInfoVM> DeviceList = new();
public ObservableCollection<SharedParameter> SharedParameterList = new();
[JsonIgnore]
public ObservableCollection<ParameterVM> ParameterList = new()
{
new ParameterVM
{
Category = ParameterCategory.Input,
Type = typeof(int),
Name = "继电器占位1",
Value = 0,
},
new ParameterVM
{
Category = ParameterCategory.Input,
Type = typeof(int),
Name = "继电器占位2",
Value = 1,
},
new ParameterVM
{
Category = ParameterCategory.Input,
Type = typeof(int),
Name = "CAN通道",
Value = 0,
},
new ParameterVM
{
Category = ParameterCategory.Input,
Type = typeof(int),
Name = "示波器通道",
Value = 0,
},
new ParameterVM
{
Category = ParameterCategory.Input,
Type = typeof(int),
Name = "功率分析仪通道1",
Value = 0,
},
new ParameterVM
{
Category = ParameterCategory.Input,
Type = typeof(int),
Name = "功率分析仪通道2",
Value = 0,
},
};
// public ObservableCollection<DeviceInfoVM> DeviceList { get; set; } = new() // public ObservableCollection<DeviceInfoVM> DeviceList { get; set; } = new()
//{ //{
// new DeviceInfoVM // new DeviceInfoVM

View File

@@ -17,5 +17,6 @@
<ResourceDictionary Source="pack://application:,,,/MaterialDesignColors;component/Themes/Recommended/Secondary/MaterialDesignColor.Lime.xaml" /> <ResourceDictionary Source="pack://application:,,,/MaterialDesignColors;component/Themes/Recommended/Secondary/MaterialDesignColor.Lime.xaml" />
<!--自定义style--> <!--自定义style-->
<ResourceDictionary Source="/UIShare;component/Styles/WindowStyle.xaml"></ResourceDictionary> <ResourceDictionary Source="/UIShare;component/Styles/WindowStyle.xaml"></ResourceDictionary>
<ResourceDictionary Source="/UIShare;component/Styles/DialogControlStyle.xaml"></ResourceDictionary>
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
</ResourceDictionary> </ResourceDictionary>

View File

@@ -0,0 +1,63 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Light.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesign3.Defaults.xaml" />
</ResourceDictionary.MergedDictionaries>
<Style x:Key="CmdBtn" TargetType="Button" BasedOn="{StaticResource MaterialDesignRaisedButton}">
<Setter Property="Height" Value="32"/>
<Setter Property="Padding" Value="12,0"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Margin" Value="4,0"/>
</Style>
<Style x:Key="WarnBtn" TargetType="Button" BasedOn="{StaticResource MaterialDesignRaisedButton}">
<Setter Property="Height" Value="32"/>
<Setter Property="Padding" Value="12,0"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Margin" Value="4,0"/>
<Setter Property="Background" Value="#EF6C00"/>
<Setter Property="Foreground" Value="White"/>
</Style>
<Style x:Key="NumInput" TargetType="TextBox" BasedOn="{StaticResource MaterialDesignOutlinedTextBox}">
<Setter Property="Width" Value="100"/>
<Setter Property="Height" Value="32"/>
<Setter Property="Padding" Value="5,0"/>
<Setter Property="materialDesign:TextFieldAssist.TextBoxViewMargin" Value="6,2,6,2"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Margin" Value="4,0"/>
<Setter Property="materialDesign:HintAssist.IsFloating" Value="False"/>
<Setter Property="materialDesign:HintAssist.Hint" Value=""/>
<Setter Property="AutomationProperties.Name" Value=""/>
</Style>
<Style x:Key="MeasureBox" TargetType="TextBox" BasedOn="{StaticResource MaterialDesignOutlinedTextBox}">
<Setter Property="Width" Value="110"/>
<Setter Property="Height" Value="32"/>
<Setter Property="Padding" Value="5,0"/>
<Setter Property="materialDesign:TextFieldAssist.TextBoxViewMargin" Value="6,2,6,2"/>
<Setter Property="IsReadOnly" Value="True"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Margin" Value="4,0"/>
<Setter Property="Background" Value="#F5F5F5"/>
<Setter Property="materialDesign:HintAssist.IsFloating" Value="False"/>
<Setter Property="materialDesign:HintAssist.Hint" Value=""/>
<Setter Property="AutomationProperties.Name" Value=""/>
</Style>
<Style x:Key="ParamLabel" TargetType="TextBlock">
<Setter Property="Width" Value="80"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Margin" Value="0,0,4,0"/>
</Style>
</ResourceDictionary>

View File

@@ -1,5 +1,5 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" >
<Style x:Key="DialogUserManageStyle" <Style x:Key="DialogUserManageStyle"
TargetType="Window"> TargetType="Window">
@@ -19,4 +19,5 @@
Value="WidthAndHeight" /> Value="WidthAndHeight" />
</Style> </Style>
</ResourceDictionary> </ResourceDictionary>

View File

@@ -10,6 +10,12 @@ namespace UIShare.UIViewModel
/// <summary>Tab 标题(显示在标签页上)。</summary> /// <summary>Tab 标题(显示在标签页上)。</summary>
public string Title { get; set; } = string.Empty; public string Title { get; set; } = string.Empty;
/// <summary>
/// 设备硬件指纹(如 "Tcp:192.168.1.100:502"),用于去重判断,
/// 防止同一物理设备被重复打开多个 Tab。
/// </summary>
public string Fingerprint { get; set; } = string.Empty;
/// <summary> /// <summary>
/// Tab 内容:传入一个已实例化的 <see cref="System.Windows.FrameworkElement"/>(通常是 UserControl /// Tab 内容:传入一个已实例化的 <see cref="System.Windows.FrameworkElement"/>(通常是 UserControl
/// 由 ContentControl 直接承载展示。 /// 由 ContentControl 直接承载展示。

View File

@@ -21,6 +21,14 @@ namespace DeviceEditModule.ViewModels
set => SetProperty(ref _title, value); set => SetProperty(ref _title, value);
} }
private string _fingerprint = string.Empty;
/// <summary>设备硬件指纹,用于去重字典的 Key。</summary>
public string Fingerprint
{
get => _fingerprint;
set => SetProperty(ref _fingerprint, value);
}
private object? _content; private object? _content;
/// <summary>Tab 内容区域(通常为 UserControl 实例)。</summary> /// <summary>Tab 内容区域(通常为 UserControl 实例)。</summary>
public object? Content public object? Content

View File

@@ -23,10 +23,8 @@ namespace UIShare.UIViewModel
Type = source.Type; Type = source.Type;
Category = source.Category; Category = source.Category;
IsUseVar = source.IsUseVar; IsUseVar = source.IsUseVar;
IsSave = source.IsSave;
VariableName = source.VariableName; VariableName = source.VariableName;
VariableID = source.VariableID; VariableID = source.VariableID;
IsGlobal = source.IsGlobal;
Value = source.Value; Value = source.Value;
LowerLimit = source.LowerLimit; LowerLimit = source.LowerLimit;
UpperLimit = source.UpperLimit; UpperLimit = source.UpperLimit;
@@ -69,12 +67,6 @@ namespace UIShare.UIViewModel
set => SetProperty(ref _category, value); set => SetProperty(ref _category, value);
} }
private bool _isGlobal;
public bool IsGlobal
{
get => _isGlobal;
set => SetProperty(ref _isGlobal, value);
}
private object? _value; private object? _value;
public object? Value public object? Value
@@ -111,12 +103,7 @@ namespace UIShare.UIViewModel
set => SetProperty(ref _isUseVar, value); set => SetProperty(ref _isUseVar, value);
} }
private bool _isSave;
public bool IsSave
{
get => _isSave;
set => SetProperty(ref _isSave, value);
}
private string? _variableName; private string? _variableName;
public string? VariableName public string? VariableName

View File

@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace UIShare.UIViewModel
{
public class SharedParameter:BindableBase
{
private string _id;
public string Id
{
get => _id;
set => SetProperty(ref _id, value);
}
private string _parameterName;
public string ParameterName
{
get => _parameterName;
set => SetProperty(ref _parameterName, value);
}
private int _value;
public int Value
{
get => _value;
set => SetProperty(ref _value, value);
}
}
}