2026-06-30 12:10:29 +08:00
|
|
|
using System.Net;
|
|
|
|
|
using System.Net.Sockets;
|
|
|
|
|
using System.Text;
|
|
|
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
|
|
|
using Microsoft.Extensions.Options;
|
|
|
|
|
using SSPCTester.Devices.Config;
|
|
|
|
|
using SSPCTester.Devices.Drivers.Real;
|
2026-07-12 12:07:41 +08:00
|
|
|
using SSPCTester.Devices.Interfaces;
|
2026-06-30 12:10:29 +08:00
|
|
|
|
|
|
|
|
namespace SSPCTester.Tests;
|
|
|
|
|
|
|
|
|
|
public sealed class It87xxLoadTests
|
|
|
|
|
{
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task CombinedQuery_ReadsVoltageCurrentAndPower()
|
|
|
|
|
{
|
2026-07-12 12:07:41 +08:00
|
|
|
var sync = new object();
|
2026-06-30 12:10:29 +08:00
|
|
|
var commands = new List<string>();
|
|
|
|
|
await using var server = await FakeScpiServer.StartAsync(command =>
|
|
|
|
|
{
|
2026-07-12 12:07:41 +08:00
|
|
|
lock (sync)
|
|
|
|
|
commands.Add(command);
|
2026-06-30 12:10:29 +08:00
|
|
|
return command switch
|
|
|
|
|
{
|
|
|
|
|
"*IDN?" => "ITECH,IT8702P,SN,1.0",
|
|
|
|
|
"SYST:REM" => FakeScpiServer.NoResponse,
|
|
|
|
|
"CHAN 1" => FakeScpiServer.NoResponse,
|
|
|
|
|
"FETC:VOLT?;:FETC:CURR?;:FETC:POW?" => "2.971700E+00;1.040000E+00;3.090000E+00",
|
|
|
|
|
"SYST:LOC" => FakeScpiServer.NoResponse,
|
|
|
|
|
_ => "0"
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
var driver = CreateDriver(server.Port, LoadQueryMode.Combined);
|
|
|
|
|
|
|
|
|
|
Assert.True(await driver.ConnectAsync());
|
|
|
|
|
var reading = await driver.ReadPowerAsync();
|
|
|
|
|
await driver.DisconnectAsync();
|
|
|
|
|
|
|
|
|
|
Assert.Equal(2.9717, reading.Volts, 6);
|
|
|
|
|
Assert.Equal(1.04, reading.Amps, 6);
|
|
|
|
|
Assert.Equal(3.09, reading.Watts, 6);
|
|
|
|
|
Assert.Contains("*IDN?", commands);
|
|
|
|
|
Assert.Contains("SYST:REM", commands);
|
|
|
|
|
Assert.Contains("CHAN 1", commands);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task SeparateQuery_ReadsVoltageCurrentAndPower()
|
|
|
|
|
{
|
|
|
|
|
await using var server = await FakeScpiServer.StartAsync(command => command switch
|
|
|
|
|
{
|
|
|
|
|
"*IDN?" => "ITECH,IT8702P,SN,1.0",
|
|
|
|
|
"SYST:REM" => FakeScpiServer.NoResponse,
|
|
|
|
|
"CHAN 1" => FakeScpiServer.NoResponse,
|
|
|
|
|
"FETC:VOLT?" => "2.5",
|
|
|
|
|
"FETC:CURR?" => "1.2",
|
|
|
|
|
"FETC:POW?" => "3.0",
|
|
|
|
|
"SYST:LOC" => FakeScpiServer.NoResponse,
|
|
|
|
|
_ => "0"
|
|
|
|
|
});
|
|
|
|
|
var driver = CreateDriver(server.Port, LoadQueryMode.Separate);
|
|
|
|
|
|
|
|
|
|
Assert.True(await driver.ConnectAsync());
|
|
|
|
|
var reading = await driver.ReadPowerAsync();
|
|
|
|
|
|
|
|
|
|
Assert.Equal(2.5, reading.Volts, 6);
|
|
|
|
|
Assert.Equal(1.2, reading.Amps, 6);
|
|
|
|
|
Assert.Equal(3.0, reading.Watts, 6);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 12:07:41 +08:00
|
|
|
[Fact]
|
|
|
|
|
public async Task ControlCommands_SetCurrentAndReset()
|
|
|
|
|
{
|
|
|
|
|
var sync = new object();
|
|
|
|
|
var commands = new List<string>();
|
|
|
|
|
await using var server = await FakeScpiServer.StartAsync(command =>
|
|
|
|
|
{
|
|
|
|
|
lock (sync)
|
|
|
|
|
commands.Add(command);
|
|
|
|
|
return command switch
|
|
|
|
|
{
|
|
|
|
|
"*IDN?" => "ITECH,IT8702P,SN,1.0",
|
|
|
|
|
"SYST:REM" => FakeScpiServer.NoResponse,
|
|
|
|
|
"CHAN 1" => FakeScpiServer.NoResponse,
|
|
|
|
|
"CURR 1.25" => FakeScpiServer.NoResponse,
|
|
|
|
|
"INP?" => "0",
|
|
|
|
|
"*RST" => FakeScpiServer.NoResponse,
|
|
|
|
|
"SYST:LOC" => FakeScpiServer.NoResponse,
|
|
|
|
|
_ => "0"
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
var driver = CreateDriver(server.Port, LoadQueryMode.Combined);
|
|
|
|
|
|
|
|
|
|
Assert.True(await driver.ConnectAsync());
|
|
|
|
|
await driver.SetModeAsync(LoadMode.CC);
|
|
|
|
|
await driver.SetValueAsync(1.25);
|
|
|
|
|
Assert.False(await driver.GetInputStateAsync());
|
|
|
|
|
await driver.ResetAsync();
|
|
|
|
|
await driver.DisconnectAsync();
|
|
|
|
|
await WaitForCommandAsync("SYST:LOC");
|
|
|
|
|
|
|
|
|
|
string[] sentCommands;
|
|
|
|
|
lock (sync)
|
|
|
|
|
sentCommands = commands.ToArray();
|
|
|
|
|
|
|
|
|
|
Assert.Contains("CURR 1.25", sentCommands);
|
|
|
|
|
Assert.Contains("INP?", sentCommands);
|
|
|
|
|
Assert.Contains("*RST", sentCommands);
|
|
|
|
|
Assert.Equal(2, sentCommands.Count(x => x == "SYST:REM"));
|
|
|
|
|
Assert.Equal(2, sentCommands.Count(x => x == "CHAN 1"));
|
|
|
|
|
Assert.DoesNotContain("MODE CC", sentCommands);
|
|
|
|
|
|
|
|
|
|
async Task WaitForCommandAsync(string expected)
|
|
|
|
|
{
|
|
|
|
|
for (int i = 0; i < 20; i++)
|
|
|
|
|
{
|
|
|
|
|
lock (sync)
|
|
|
|
|
{
|
|
|
|
|
if (commands.Contains(expected))
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await Task.Delay(10);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory]
|
|
|
|
|
[InlineData("ON", true)]
|
|
|
|
|
[InlineData("1", true)]
|
|
|
|
|
[InlineData("OFF", false)]
|
|
|
|
|
[InlineData("0", false)]
|
|
|
|
|
public void ParseSwitchState_AcceptsScpiValues(string response, bool expected) =>
|
|
|
|
|
Assert.Equal(expected, It87xxLoad.ParseSwitchState(response));
|
|
|
|
|
|
2026-06-30 12:10:29 +08:00
|
|
|
[Fact]
|
|
|
|
|
public async Task QueryFailure_MarksDisconnected()
|
|
|
|
|
{
|
|
|
|
|
await using var server = await FakeScpiServer.StartAsync(command => command switch
|
|
|
|
|
{
|
|
|
|
|
"*IDN?" => "ITECH,IT8702P,SN,1.0",
|
|
|
|
|
"SYST:REM" => FakeScpiServer.NoResponse,
|
|
|
|
|
"CHAN 1" => FakeScpiServer.NoResponse,
|
|
|
|
|
"FETC:VOLT?;:FETC:CURR?;:FETC:POW?" => FakeScpiServer.CloseConnection,
|
|
|
|
|
_ => "0"
|
|
|
|
|
});
|
|
|
|
|
var driver = CreateDriver(server.Port, LoadQueryMode.Combined);
|
|
|
|
|
|
|
|
|
|
Assert.True(await driver.ConnectAsync());
|
|
|
|
|
await Assert.ThrowsAsync<InvalidOperationException>(() => driver.ReadPowerAsync());
|
|
|
|
|
Assert.False(driver.IsConnected);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static It87xxLoad CreateDriver(int port, LoadQueryMode queryMode)
|
|
|
|
|
{
|
|
|
|
|
var options = Options.Create(new DeviceOptions
|
|
|
|
|
{
|
|
|
|
|
Load = new LoadOptions
|
|
|
|
|
{
|
|
|
|
|
Host = IPAddress.Loopback.ToString(),
|
|
|
|
|
Port = port,
|
|
|
|
|
TimeoutSeconds = 1,
|
|
|
|
|
Channel = 1,
|
|
|
|
|
UseFetch = true,
|
|
|
|
|
QueryMode = queryMode
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return new It87xxLoad(options, NullLogger<It87xxLoad>.Instance);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private sealed class FakeScpiServer : IAsyncDisposable
|
|
|
|
|
{
|
|
|
|
|
public const string NoResponse = "__NO_RESPONSE__";
|
|
|
|
|
public const string CloseConnection = "__CLOSE_CONNECTION__";
|
|
|
|
|
private readonly TcpListener _listener;
|
|
|
|
|
private readonly Func<string, string> _handler;
|
|
|
|
|
private readonly CancellationTokenSource _cts = new();
|
|
|
|
|
private readonly Task _loop;
|
|
|
|
|
|
|
|
|
|
private FakeScpiServer(TcpListener listener, Func<string, string> handler)
|
|
|
|
|
{
|
|
|
|
|
_listener = listener;
|
|
|
|
|
_handler = handler;
|
|
|
|
|
Port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
|
|
|
|
_loop = Task.Run(AcceptLoopAsync);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public int Port { get; }
|
|
|
|
|
|
|
|
|
|
public static Task<FakeScpiServer> StartAsync(Func<string, string> handler)
|
|
|
|
|
{
|
|
|
|
|
var listener = new TcpListener(IPAddress.Loopback, 0);
|
|
|
|
|
listener.Start();
|
|
|
|
|
return Task.FromResult(new FakeScpiServer(listener, handler));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async ValueTask DisposeAsync()
|
|
|
|
|
{
|
|
|
|
|
_cts.Cancel();
|
|
|
|
|
_listener.Stop();
|
|
|
|
|
try { await _loop; } catch { }
|
|
|
|
|
_cts.Dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task AcceptLoopAsync()
|
|
|
|
|
{
|
|
|
|
|
while (!_cts.IsCancellationRequested)
|
|
|
|
|
{
|
|
|
|
|
using var client = await _listener.AcceptTcpClientAsync(_cts.Token);
|
|
|
|
|
await using var stream = client.GetStream();
|
|
|
|
|
var buffer = new byte[1024];
|
2026-07-12 12:07:41 +08:00
|
|
|
var pending = "";
|
2026-06-30 12:10:29 +08:00
|
|
|
while (!_cts.IsCancellationRequested)
|
|
|
|
|
{
|
|
|
|
|
int read = await stream.ReadAsync(buffer, _cts.Token);
|
|
|
|
|
if (read <= 0) break;
|
|
|
|
|
|
2026-07-12 12:07:41 +08:00
|
|
|
pending += Encoding.ASCII.GetString(buffer, 0, read);
|
|
|
|
|
while (true)
|
|
|
|
|
{
|
|
|
|
|
int newline = pending.IndexOf('\n');
|
|
|
|
|
if (newline < 0)
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
string command = pending[..newline].Trim();
|
|
|
|
|
pending = pending[(newline + 1)..];
|
|
|
|
|
if (string.IsNullOrEmpty(command))
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
string response = _handler(command);
|
|
|
|
|
if (response == CloseConnection) return;
|
|
|
|
|
if (response == NoResponse) continue;
|
|
|
|
|
|
|
|
|
|
byte[] payload = Encoding.ASCII.GetBytes(response + "\n");
|
|
|
|
|
await stream.WriteAsync(payload, _cts.Token);
|
|
|
|
|
await stream.FlushAsync(_cts.Token);
|
|
|
|
|
}
|
2026-06-30 12:10:29 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|