2026-05-20 13:49:45 +08:00
|
|
|
|
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;
|
|
|
|
|
|
|
2026-05-20 19:46:52 +08:00
|
|
|
|
public int ReceiveBufferSize { get; set; } = 2 * 1024 * 1024;
|
2026-05-20 13:49:45 +08:00
|
|
|
|
public int ConnectTimeoutMs { get; set; } = 3000;
|
|
|
|
|
|
public int ReadWriteTimeoutMs { get; set; } = 5000;
|
|
|
|
|
|
|
2026-05-24 10:36:57 +08:00
|
|
|
|
// 默认电压量程(伏特),可根据实际信号修改,例如 0.1, 1, 10, 100, 1000
|
2026-05-28 16:38:40 +08:00
|
|
|
|
public double DefaultVoltageRange { get; set; } = 10.0;
|
2026-05-24 10:36:57 +08:00
|
|
|
|
|
2026-05-20 13:49:45 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-24 10:36:57 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 发送命令并等待完整响应(支持多行/大数据量)
|
|
|
|
|
|
/// </summary>
|
2026-05-20 13:49:45 +08:00
|
|
|
|
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();
|
2026-05-24 10:36:57 +08:00
|
|
|
|
byte[] buffer = new byte[65536];
|
2026-05-20 13:49:45 +08:00
|
|
|
|
int bytesRead;
|
2026-05-24 10:36:57 +08:00
|
|
|
|
bool endReached = false;
|
|
|
|
|
|
|
|
|
|
|
|
while (!endReached)
|
2026-05-20 13:49:45 +08:00
|
|
|
|
{
|
|
|
|
|
|
bytesRead = await _stream.ReadAsync(buffer, 0, buffer.Length);
|
|
|
|
|
|
if (bytesRead == 0) break;
|
|
|
|
|
|
string chunk = Encoding.ASCII.GetString(buffer, 0, bytesRead);
|
|
|
|
|
|
responseBuilder.Append(chunk);
|
2026-05-24 10:36:57 +08:00
|
|
|
|
// 如果收到换行符,再等50ms确认没有更多数据(TCP分包情况)
|
|
|
|
|
|
if (chunk.Contains("\n"))
|
|
|
|
|
|
{
|
|
|
|
|
|
await Task.Delay(50);
|
|
|
|
|
|
if (!_stream.DataAvailable)
|
|
|
|
|
|
endReached = true;
|
|
|
|
|
|
}
|
2026-05-20 13:49:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
return responseBuilder.ToString().Trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-24 10:36:57 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 发送命令,不等待响应
|
|
|
|
|
|
/// </summary>
|
2026-05-20 13:49:45 +08:00
|
|
|
|
public async Task SendCommandAsync(string command)
|
|
|
|
|
|
{
|
|
|
|
|
|
EnsureConnected();
|
|
|
|
|
|
byte[] cmdBytes = Encoding.ASCII.GetBytes(command + "\n");
|
|
|
|
|
|
await _stream.WriteAsync(cmdBytes, 0, cmdBytes.Length);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-05-24 10:36:57 +08:00
|
|
|
|
/// 配置为高速直流电压测量(BUS触发模式,固定量程,0.02PLC,关闭自归零)
|
2026-05-20 13:49:45 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
public async Task ConfigureForHighSpeedDcvAsync()
|
|
|
|
|
|
{
|
2026-05-24 10:36:57 +08:00
|
|
|
|
// 1. 重置到默认状态(可选)
|
|
|
|
|
|
await SendCommandAsync("*RST");
|
|
|
|
|
|
await Task.Delay(100);
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 固定量程(避免自动量程降低速度)
|
|
|
|
|
|
await SendCommandAsync($"VOLT:DC:RANG {DefaultVoltageRange}");
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 设置积分时间 0.02PLC(最快速度)
|
2026-06-03 19:52:57 +08:00
|
|
|
|
await SendCommandAsync("VOLT:DC:NPLC 0.1");
|
2026-05-24 10:36:57 +08:00
|
|
|
|
|
|
|
|
|
|
// 4. 关闭自动归零(提高速度)
|
2026-05-20 19:46:52 +08:00
|
|
|
|
await SendCommandAsync("VOLT:DC:ZERO:AUTO OFF");
|
2026-05-24 10:36:57 +08:00
|
|
|
|
|
|
|
|
|
|
// 5. 触发源设为 BUS(软件触发)
|
|
|
|
|
|
await SendCommandAsync("TRIG:SOUR BUS");
|
|
|
|
|
|
|
|
|
|
|
|
// 6. 关闭自动延迟,延迟设为0
|
2026-05-20 13:49:45 +08:00
|
|
|
|
await SendCommandAsync("TRIG:DEL:AUTO OFF");
|
|
|
|
|
|
await SendCommandAsync("TRIG:DEL 0");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-05-24 10:36:57 +08:00
|
|
|
|
/// 预置采样点数并进入等待触发状态(发送 INIT)
|
2026-05-20 19:46:52 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
public async Task PrepareBatchAsync(int sampleCount)
|
|
|
|
|
|
{
|
|
|
|
|
|
await SendCommandAsync($"SAMP:COUN {sampleCount}");
|
2026-05-24 10:36:57 +08:00
|
|
|
|
await SendCommandAsync("INIT"); // 进入等待触发状态
|
2026-05-20 19:46:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-05-24 10:36:57 +08:00
|
|
|
|
/// 发送软件触发信号(*TRG),开始采集
|
2026-05-20 19:46:52 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
public async Task TriggerAsync()
|
|
|
|
|
|
{
|
2026-05-24 10:36:57 +08:00
|
|
|
|
await SendCommandAsync("*TRG");
|
2026-05-20 19:46:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-05-24 10:36:57 +08:00
|
|
|
|
/// 获取批量采集结果(FETCh?)
|
2026-05-20 13:49:45 +08:00
|
|
|
|
/// </summary>
|
2026-05-24 10:36:57 +08:00
|
|
|
|
public async Task<double[]> FetchBatchAsync()
|
2026-05-20 13:49:45 +08:00
|
|
|
|
{
|
2026-05-20 19:46:52 +08:00
|
|
|
|
string response = await QueryAsync("FETCh?");
|
2026-05-24 10:36:57 +08:00
|
|
|
|
return ParseResponse(response);
|
2026-05-20 19:46:52 +08:00
|
|
|
|
}
|
2026-05-20 13:49:45 +08:00
|
|
|
|
|
2026-05-20 19:46:52 +08:00
|
|
|
|
/// <summary>
|
2026-05-24 10:36:57 +08:00
|
|
|
|
/// 一次性批量采集(简化接口,内部自动完成 SAMP:COUN + READ?)
|
|
|
|
|
|
/// 注意:此方法会阻塞直到采集完成,适合不需要分离触发时序的场景
|
2026-05-20 19:46:52 +08:00
|
|
|
|
/// </summary>
|
2026-05-24 10:36:57 +08:00
|
|
|
|
public async Task<double[]> ReadBatchAsync(int sampleCount)
|
2026-05-20 19:46:52 +08:00
|
|
|
|
{
|
2026-05-24 10:36:57 +08:00
|
|
|
|
await SendCommandAsync($"SAMP:COUN {sampleCount}");
|
|
|
|
|
|
string response = await QueryAsync("READ?");
|
|
|
|
|
|
return ParseResponse(response);
|
2026-05-20 19:46:52 +08:00
|
|
|
|
}
|
2026-05-20 13:49:45 +08:00
|
|
|
|
|
2026-05-24 10:36:57 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 解析逗号分隔的电压值数组
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private double[] ParseResponse(string response)
|
2026-05-20 19:46:52 +08:00
|
|
|
|
{
|
2026-05-24 10:36:57 +08:00
|
|
|
|
if (string.IsNullOrWhiteSpace(response))
|
|
|
|
|
|
throw new Exception("返回数据为空");
|
|
|
|
|
|
|
2026-05-20 19:46:52 +08:00
|
|
|
|
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]))
|
2026-05-24 10:36:57 +08:00
|
|
|
|
throw new Exception($"解析电压值失败: {parts[i]}");
|
2026-05-20 19:46:52 +08:00
|
|
|
|
}
|
2026-05-20 13:49:45 +08:00
|
|
|
|
return values;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-24 10:36:57 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 单次读取电压(保留,但不推荐用于高速采集)
|
|
|
|
|
|
/// </summary>
|
2026-05-20 13:49:45 +08:00
|
|
|
|
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}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task ResetAsync() => await SendCommandAsync("*RST");
|
|
|
|
|
|
public async Task<string> IdentifyAsync() => await QueryAsync("*IDN?");
|
|
|
|
|
|
|
|
|
|
|
|
private void EnsureConnected()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_tcpClient == null || !_tcpClient.Connected)
|
|
|
|
|
|
throw new InvalidOperationException("未连接到 TH1963,请先调用 ConnectAsync");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void Dispose()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!_disposed)
|
|
|
|
|
|
{
|
|
|
|
|
|
_stream?.Close();
|
|
|
|
|
|
_tcpClient?.Close();
|
|
|
|
|
|
_stream?.Dispose();
|
|
|
|
|
|
_tcpClient?.Dispose();
|
|
|
|
|
|
_disposed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|