2026-06-30 12:10:29 +08:00
|
|
|
|
using Microsoft.Extensions.Options;
|
|
|
|
|
|
using SSPCTester.Devices.Config;
|
|
|
|
|
|
using SSPCTester.Devices.Interfaces;
|
|
|
|
|
|
using SSPCTester.Logic.Models;
|
|
|
|
|
|
using SSPCTester.Logic.Testing;
|
|
|
|
|
|
|
|
|
|
|
|
namespace SSPCTester.Tests;
|
|
|
|
|
|
|
|
|
|
|
|
#pragma warning disable CS0067
|
|
|
|
|
|
|
|
|
|
|
|
public sealed class InterfacePowerTests
|
|
|
|
|
|
{
|
|
|
|
|
|
[Fact]
|
|
|
|
|
|
public async Task PowerTest_UsesUdpInputAndIt8702pOutputPower()
|
|
|
|
|
|
{
|
|
|
|
|
|
var project = new Project();
|
|
|
|
|
|
var sspc = new MeasuringSspc();
|
|
|
|
|
|
var forbiddenPower = new ForbiddenPowerSupply();
|
|
|
|
|
|
var load = new MeasuringLoad();
|
|
|
|
|
|
var daq = new MeasuringDaq();
|
|
|
|
|
|
var options = Options.Create(new DeviceOptions
|
|
|
|
|
|
{
|
|
|
|
|
|
Daq = new DaqOptions
|
|
|
|
|
|
{
|
|
|
|
|
|
ChannelCount = 1,
|
|
|
|
|
|
Channels = new List<DaqChannelOptions>
|
|
|
|
|
|
{
|
|
|
|
|
|
new() { PhysicalChannel = 0, Role = nameof(DaqChannelRole.OutputVoltage), IsCalibrated = true },
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-07-03 14:16:15 +08:00
|
|
|
|
var power = new FixedPowerSupply();
|
|
|
|
|
|
var test = new PowerTest(power) { SamplesPerChannel = 1, IntervalMs = 0 };
|
2026-06-30 12:10:29 +08:00
|
|
|
|
var context = new TestContext(project, sspc, forbiddenPower, load, daq);
|
|
|
|
|
|
|
|
|
|
|
|
await test.RunAsync(context, new Progress<TestProgress>(), CancellationToken.None);
|
|
|
|
|
|
|
|
|
|
|
|
Assert.Equal(24, project.PowerResults.Count);
|
|
|
|
|
|
Assert.All(project.PowerResults, row =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Assert.Equal(TestStatus.Measured, row.Status);
|
|
|
|
|
|
Assert.Equal(30.0, row.Vin, 6); // 输入端电压来自高速采集卡
|
|
|
|
|
|
Assert.Equal(1.0, row.Iin, 6); // 通流电流来自 Modbus(Iin = Iout)
|
|
|
|
|
|
Assert.Equal(28.0, row.Vout, 6); // 输出端电压来自 Modbus
|
|
|
|
|
|
Assert.Equal(1.0, row.Iout, 6);
|
|
|
|
|
|
Assert.Equal(27.5, row.MeasuredPowerOut.GetValueOrDefault(), 6);
|
2026-07-02 01:50:02 +08:00
|
|
|
|
Assert.Equal(2.0, row.PowerLossW, 6);
|
|
|
|
|
|
Assert.Equal(93.333333, row.EfficiencyPct, 5);
|
2026-06-30 12:10:29 +08:00
|
|
|
|
});
|
2026-07-01 20:04:27 +08:00
|
|
|
|
Assert.Equal(0, sspc.TurnOffCount);
|
2026-06-30 12:10:29 +08:00
|
|
|
|
Assert.Equal(0, sspc.ReadMeasurementCount);
|
2026-07-03 14:16:15 +08:00
|
|
|
|
Assert.Equal(1, power.OutputOnCount);
|
|
|
|
|
|
Assert.Equal(1, load.InputOnCount);
|
2026-06-30 12:10:29 +08:00
|
|
|
|
Assert.Equal(24, load.ReadPowerCount);
|
|
|
|
|
|
Assert.Equal(0, daq.ReadInstantCount);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
|
public void PowerLossRow_FallsBackToCalculatedOutputPower()
|
|
|
|
|
|
{
|
|
|
|
|
|
var row = new PowerLossRow { Vin = 30, Iin = 1, Vout = 28, Iout = 1 };
|
|
|
|
|
|
|
|
|
|
|
|
Assert.Equal(28, row.PowerOut, 6);
|
|
|
|
|
|
Assert.Equal(2, row.PowerLossW, 6);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private sealed class MeasuringSspc : ISspc
|
|
|
|
|
|
{
|
|
|
|
|
|
public int TurnOffCount { get; private set; }
|
|
|
|
|
|
public int ReadMeasurementCount { get; private set; }
|
|
|
|
|
|
public int ChannelCount => 24;
|
|
|
|
|
|
public bool IsConnected => true;
|
|
|
|
|
|
public string DisplayName => "test";
|
|
|
|
|
|
public event EventHandler<bool>? ConnectionChanged;
|
|
|
|
|
|
public Task<bool> ConnectAsync(CancellationToken ct = default) => Task.FromResult(true);
|
|
|
|
|
|
public Task DisconnectAsync() => Task.CompletedTask;
|
|
|
|
|
|
public Task TurnOnAsync(int physicalSlot, CancellationToken ct = default) => Task.CompletedTask;
|
|
|
|
|
|
public Task TurnOffAsync(int physicalSlot, CancellationToken ct = default) { TurnOffCount++; return Task.CompletedTask; }
|
|
|
|
|
|
public Task FlashAsync(int physicalSlot, int milliseconds, CancellationToken ct = default) => Task.CompletedTask;
|
|
|
|
|
|
public Task<bool> GetChannelStateAsync(int physicalSlot, CancellationToken ct = default) => Task.FromResult(true);
|
|
|
|
|
|
public Task<ChannelMeasurement> ReadMeasurementAsync(int physicalSlot, CancellationToken ct = default)
|
|
|
|
|
|
{
|
|
|
|
|
|
ReadMeasurementCount++;
|
|
|
|
|
|
return Task.FromResult(new ChannelMeasurement { PhysicalSlot = physicalSlot, Voltage = 28.0, Current = 1.0 });
|
|
|
|
|
|
}
|
|
|
|
|
|
public Task<IReadOnlyList<ChannelMeasurement>> ReadMeasurementsAsync(CancellationToken ct = default) =>
|
|
|
|
|
|
Task.FromResult<IReadOnlyList<ChannelMeasurement>>(Array.Empty<ChannelMeasurement>());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private sealed class MeasuringDaq : IDaq
|
|
|
|
|
|
{
|
|
|
|
|
|
public int ReadInstantCount { get; private set; }
|
|
|
|
|
|
public bool IsConnected { get; private set; }
|
|
|
|
|
|
public string DisplayName => "daq";
|
|
|
|
|
|
public event EventHandler<bool>? ConnectionChanged;
|
|
|
|
|
|
public event EventHandler<CurveSample>? SampleReceived;
|
|
|
|
|
|
|
|
|
|
|
|
public Task<bool> ConnectAsync(CancellationToken ct = default)
|
|
|
|
|
|
{
|
|
|
|
|
|
IsConnected = true;
|
|
|
|
|
|
ConnectionChanged?.Invoke(this, true);
|
|
|
|
|
|
return Task.FromResult(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public Task DisconnectAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
IsConnected = false;
|
|
|
|
|
|
ConnectionChanged?.Invoke(this, false);
|
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public Task<IReadOnlyList<DaqDeviceDescriptor>> EnumerateDevicesAsync(CancellationToken ct = default) =>
|
|
|
|
|
|
Task.FromResult<IReadOnlyList<DaqDeviceDescriptor>>(Array.Empty<DaqDeviceDescriptor>());
|
|
|
|
|
|
|
|
|
|
|
|
public Task<CurveData> CaptureAsync(int[] channels, int sampleRateHz, double durationSec, CancellationToken ct = default) =>
|
|
|
|
|
|
throw new InvalidOperationException();
|
|
|
|
|
|
|
|
|
|
|
|
public Task<double[]> ReadInstantAsync(int[] channels, CancellationToken ct = default)
|
|
|
|
|
|
{
|
|
|
|
|
|
ReadInstantCount++;
|
|
|
|
|
|
// 采集卡输入端电压固定返回 30V。
|
|
|
|
|
|
var values = channels.Select(ch => ch == 0 ? 30.0 : double.NaN).ToArray();
|
|
|
|
|
|
return Task.FromResult(values);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public Task<CurveData> CaptureAroundActionAsync(DaqActionCaptureRequest request, Func<CancellationToken, Task> action, CancellationToken ct = default) =>
|
|
|
|
|
|
throw new InvalidOperationException();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private sealed class MeasuringLoad : ILoad
|
|
|
|
|
|
{
|
|
|
|
|
|
public int ReadPowerCount { get; private set; }
|
2026-07-03 14:16:15 +08:00
|
|
|
|
public int InputOnCount { get; private set; }
|
2026-06-30 12:10:29 +08:00
|
|
|
|
public bool IsConnected => true;
|
|
|
|
|
|
public string DisplayName => "it8702p";
|
|
|
|
|
|
public event EventHandler<bool>? ConnectionChanged;
|
|
|
|
|
|
public Task<bool> ConnectAsync(CancellationToken ct = default) => Task.FromResult(true);
|
|
|
|
|
|
public Task DisconnectAsync() => Task.CompletedTask;
|
|
|
|
|
|
public Task SetModeAsync(LoadMode mode, CancellationToken ct = default) => throw new InvalidOperationException();
|
|
|
|
|
|
public Task SetValueAsync(double value, CancellationToken ct = default) => throw new InvalidOperationException();
|
2026-07-12 12:07:41 +08:00
|
|
|
|
public Task ResetAsync(CancellationToken ct = default) => throw new InvalidOperationException();
|
2026-07-03 14:16:15 +08:00
|
|
|
|
public Task InputAsync(bool on, CancellationToken ct = default)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (on) InputOnCount++;
|
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
|
}
|
2026-07-12 12:07:41 +08:00
|
|
|
|
public Task<bool> GetInputStateAsync(CancellationToken ct = default) => Task.FromResult(true);
|
2026-06-30 12:10:29 +08:00
|
|
|
|
public Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default) => Task.FromResult((28.0, 1.0));
|
|
|
|
|
|
public Task<PowerReading> ReadPowerAsync(CancellationToken ct = default)
|
|
|
|
|
|
{
|
|
|
|
|
|
ReadPowerCount++;
|
|
|
|
|
|
return Task.FromResult(new PowerReading(28.0, 1.0, 27.5));
|
|
|
|
|
|
}
|
|
|
|
|
|
public Task<string> QueryIdentityAsync(CancellationToken ct = default) => Task.FromResult("it8702p");
|
2026-07-12 12:07:41 +08:00
|
|
|
|
public Task<string> QueryErrorAsync(CancellationToken ct = default) => Task.FromResult("0,\"No error\"");
|
2026-06-30 12:10:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private sealed class FixedPowerSupply : IPowerSupply
|
|
|
|
|
|
{
|
2026-07-03 14:16:15 +08:00
|
|
|
|
public int OutputOnCount { get; private set; }
|
2026-06-30 12:10:29 +08:00
|
|
|
|
public bool IsConnected => true;
|
|
|
|
|
|
public string DisplayName => "fixed power";
|
|
|
|
|
|
public event EventHandler<bool>? ConnectionChanged;
|
|
|
|
|
|
public Task<bool> ConnectAsync(CancellationToken ct = default) => Task.FromResult(true);
|
|
|
|
|
|
public Task DisconnectAsync() => Task.CompletedTask;
|
|
|
|
|
|
public Task SetVoltageAsync(double volts, CancellationToken ct = default) => throw new InvalidOperationException();
|
|
|
|
|
|
public Task SetCurrentLimitAsync(double amps, CancellationToken ct = default) => throw new InvalidOperationException();
|
2026-07-03 14:16:15 +08:00
|
|
|
|
public Task OutputAsync(bool on, CancellationToken ct = default)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (on) OutputOnCount++;
|
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
|
}
|
2026-07-12 12:07:41 +08:00
|
|
|
|
public Task<bool> GetOutputStateAsync(CancellationToken ct = default) => Task.FromResult(true);
|
2026-06-30 12:10:29 +08:00
|
|
|
|
public Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default) => Task.FromResult((30.0, 1.0));
|
|
|
|
|
|
public Task<string> QueryIdentityAsync(CancellationToken ct = default) => Task.FromResult("fixed");
|
2026-07-12 12:07:41 +08:00
|
|
|
|
public Task<string> QueryErrorAsync(CancellationToken ct = default) => Task.FromResult("0,\"No error\"");
|
2026-06-30 12:10:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private sealed class ForbiddenPowerSupply : IPowerSupply
|
|
|
|
|
|
{
|
|
|
|
|
|
public bool IsConnected => false;
|
|
|
|
|
|
public string DisplayName => "forbidden";
|
|
|
|
|
|
public event EventHandler<bool>? ConnectionChanged;
|
|
|
|
|
|
public Task<bool> ConnectAsync(CancellationToken ct = default) => throw new InvalidOperationException();
|
|
|
|
|
|
public Task DisconnectAsync() => Task.CompletedTask;
|
|
|
|
|
|
public Task SetVoltageAsync(double volts, CancellationToken ct = default) => throw new InvalidOperationException();
|
|
|
|
|
|
public Task SetCurrentLimitAsync(double amps, CancellationToken ct = default) => throw new InvalidOperationException();
|
|
|
|
|
|
public Task OutputAsync(bool on, CancellationToken ct = default) => throw new InvalidOperationException();
|
2026-07-12 12:07:41 +08:00
|
|
|
|
public Task<bool> GetOutputStateAsync(CancellationToken ct = default) => throw new InvalidOperationException();
|
2026-06-30 12:10:29 +08:00
|
|
|
|
public Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default) => throw new InvalidOperationException();
|
|
|
|
|
|
public Task<string> QueryIdentityAsync(CancellationToken ct = default) => throw new InvalidOperationException();
|
2026-07-12 12:07:41 +08:00
|
|
|
|
public Task<string> QueryErrorAsync(CancellationToken ct = default) => throw new InvalidOperationException();
|
2026-06-30 12:10:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private sealed class ForbiddenLoad : ILoad
|
|
|
|
|
|
{
|
|
|
|
|
|
public bool IsConnected => false;
|
|
|
|
|
|
public string DisplayName => "forbidden";
|
|
|
|
|
|
public event EventHandler<bool>? ConnectionChanged;
|
|
|
|
|
|
public Task<bool> ConnectAsync(CancellationToken ct = default) => throw new InvalidOperationException();
|
|
|
|
|
|
public Task DisconnectAsync() => Task.CompletedTask;
|
|
|
|
|
|
public Task SetModeAsync(LoadMode mode, CancellationToken ct = default) => throw new InvalidOperationException();
|
|
|
|
|
|
public Task SetValueAsync(double value, CancellationToken ct = default) => throw new InvalidOperationException();
|
2026-07-12 12:07:41 +08:00
|
|
|
|
public Task ResetAsync(CancellationToken ct = default) => throw new InvalidOperationException();
|
2026-06-30 12:10:29 +08:00
|
|
|
|
public Task InputAsync(bool on, CancellationToken ct = default) => throw new InvalidOperationException();
|
2026-07-12 12:07:41 +08:00
|
|
|
|
public Task<bool> GetInputStateAsync(CancellationToken ct = default) => throw new InvalidOperationException();
|
2026-06-30 12:10:29 +08:00
|
|
|
|
public Task<(double volts, double amps)> ReadAsync(CancellationToken ct = default) => throw new InvalidOperationException();
|
|
|
|
|
|
public Task<PowerReading> ReadPowerAsync(CancellationToken ct = default) => throw new InvalidOperationException();
|
|
|
|
|
|
public Task<string> QueryIdentityAsync(CancellationToken ct = default) => throw new InvalidOperationException();
|
2026-07-12 12:07:41 +08:00
|
|
|
|
public Task<string> QueryErrorAsync(CancellationToken ct = default) => throw new InvalidOperationException();
|
2026-06-30 12:10:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
#pragma warning restore CS0067
|