This commit is contained in:
xyy
2026-05-20 13:49:45 +08:00
parent d85ee68e25
commit 47bb9f1538
8 changed files with 372 additions and 132 deletions

178
Services/LanScpiSocket.cs Normal file
View File

@@ -0,0 +1,178 @@
using System;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace ASTM_D7896_Tester.Services
{
/// <summary>
/// 基于 LAN (TCP/IP) 的 TH1963 数字多用表通信服务
/// 支持 SCPI 命令,实现高速批量采集
/// </summary>
public class Th1963LanService : IDisposable
{
private TcpClient _tcpClient;
private NetworkStream _stream;
private bool _disposed = false;
/// <summary>
/// 接收缓冲区大小(字节),建议至少 1MB 以支持大量数据
/// </summary>
public int ReceiveBufferSize { get; set; } = 2 * 1024 * 1024; // 2 MB
/// <summary>
/// 连接超时(毫秒)
/// </summary>
public int ConnectTimeoutMs { get; set; } = 3000;
/// <summary>
/// 读写超时(毫秒)
/// </summary>
public int ReadWriteTimeoutMs { get; set; } = 5000;
/// <summary>
/// 连接到 TH1963
/// </summary>
/// <param name="ipAddress">仪器 IP 地址,如 "192.168.1.241"</param>
/// <param name="port">端口号,默认 45454</param>
public async Task ConnectAsync(string ipAddress, int port = 45454)
{
if (_tcpClient != null && _tcpClient.Connected)
return;
_tcpClient = new TcpClient();
_tcpClient.ReceiveBufferSize = ReceiveBufferSize;
_tcpClient.SendBufferSize = ReceiveBufferSize;
var connectTask = _tcpClient.ConnectAsync(ipAddress, port);
if (await Task.WhenAny(connectTask, Task.Delay(ConnectTimeoutMs)) != connectTask)
throw new TimeoutException($"连接 TH1963 ({ipAddress}:{port}) 超时");
_stream = _tcpClient.GetStream();
_stream.ReadTimeout = ReadWriteTimeoutMs;
_stream.WriteTimeout = ReadWriteTimeoutMs;
}
/// <summary>
/// 发送 SCPI 命令并等待响应(适用于查询类命令)
/// </summary>
public async Task<string> QueryAsync(string command)
{
EnsureConnected();
byte[] cmdBytes = Encoding.ASCII.GetBytes(command + "\n");
await _stream.WriteAsync(cmdBytes, 0, cmdBytes.Length);
// 读取响应,直到换行符
var responseBuilder = new StringBuilder();
byte[] buffer = new byte[4096];
int bytesRead;
while (true)
{
bytesRead = await _stream.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0) break;
string chunk = Encoding.ASCII.GetString(buffer, 0, bytesRead);
responseBuilder.Append(chunk);
if (chunk.Contains("\n")) break;
}
return responseBuilder.ToString().Trim();
}
/// <summary>
/// 发送命令但不等待响应(适用于设置类命令)
/// </summary>
public async Task SendCommandAsync(string command)
{
EnsureConnected();
byte[] cmdBytes = Encoding.ASCII.GetBytes(command + "\n");
await _stream.WriteAsync(cmdBytes, 0, cmdBytes.Length);
}
/// <summary>
/// 配置为高速直流电压测量(用于批量采集)
/// 设置0.02 PLC≈400μs积分时间自动量程关闭自动归零
/// </summary>
public async Task ConfigureForHighSpeedDcvAsync()
{
await SendCommandAsync("CONF:VOLT:DC AUTO");
await SendCommandAsync("VOLT:DC:NPLC 0.02"); // 最快速度
await SendCommandAsync("VOLT:DC:ZERO:AUTO OFF"); // 提高速度
await SendCommandAsync("TRIG:SOUR EXT"); // 外部触发与PLC同步
await SendCommandAsync("TRIG:DEL:AUTO OFF");
await SendCommandAsync("TRIG:DEL 0");
}
/// <summary>
/// 批量采集指定数量的电压值(使用外部触发,需等待硬件触发信号)
/// 调用前必须已执行 ConfigureForHighSpeedDcvAsync 和 SAMP:COUN
/// </summary>
/// <param name="sampleCount">采样点数(最大 5000</param>
/// <returns>电压数组(单位:伏特)</returns>
public async Task<double[]> AcquireBatchAsync(int sampleCount)
{
// 预置采样点数
await SendCommandAsync($"SAMP:COUN {sampleCount}");
// 进入等待触发状态
await SendCommandAsync("INIT");
// 等待采集完成估算sampleCount / 1000 秒 + 稳定时间)
int waitMs = (int)(sampleCount / 1000.0 * 1000) + 200;
await Task.Delay(waitMs);
// 取回数据
string response = await QueryAsync("FETCh?");
string[] parts = response.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
double[] values = new double[parts.Length];
for (int i = 0; i < parts.Length; i++)
{
if (!double.TryParse(parts[i], System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture, out values[i]))
throw new Exception($"解析电压值失败: {parts[i]}");
}
if (values.Length != sampleCount)
throw new Exception($"期望 {sampleCount} 个点,实际收到 {values.Length}");
return values;
}
/// <summary>
/// 单次读取当前电压(非批量,用于实时监控)
/// </summary>
public async Task<double> ReadVoltageAsync()
{
string resp = await QueryAsync("MEAS:VOLT:DC?");
if (double.TryParse(resp, System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture, out double value))
return value;
throw new Exception($"电压读数无效: {resp}");
}
/// <summary>
/// 复位仪器
/// </summary>
public async Task ResetAsync() => await SendCommandAsync("*RST");
/// <summary>
/// 查询仪器标识
/// </summary>
public async Task<string> IdentifyAsync() => await QueryAsync("*IDN?");
private void EnsureConnected()
{
if (_tcpClient == null || !_tcpClient.Connected)
throw new InvalidOperationException("未连接到 TH1963请先调用 ConnectAsync");
}
// 实现 IDisposable
public void Dispose()
{
if (!_disposed)
{
_stream?.Close();
_tcpClient?.Close();
_stream?.Dispose();
_tcpClient?.Dispose();
_disposed = true;
}
}
}
}

View File

@@ -48,10 +48,12 @@ public class PlcService : IPlcService
// 现在可以直接使用扩展方法
await _tcpClient.ConnectAsync(_config.PlcConnection.IpAddress, _config.PlcConnection.Port).WithCancellation(cts.Token);
_master = ModbusIpMaster.CreateIp(_tcpClient);
await ConnectAsync();
return;
}
catch (Exception ex) when (i < retryCount - 1)
{
await DisconnectAsync();
System.Diagnostics.Debug.WriteLine($"连接失败,{500}ms 后重试... {ex.Message}");
await Task.Delay(500);
}