194 lines
6.3 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Exchanger.Hardware.Abstractions;
using Exchanger.Hardware.PeripheryController;
using Exchanger.Hardware.SerialPortHardware;
using NUnit.Framework;
using System.Collections.Concurrent;
using System.Diagnostics;
namespace Exchanger.HardwareTests;
internal sealed class BoardTestSession : IDisposable
{
private readonly BoardTestConfiguration _configuration;
private readonly Aes256EcbBlockCipher _cipher;
private readonly Stopwatch _clock = Stopwatch.StartNew();
private readonly ConcurrentQueue<ObservedBoardMessage> _messages = new();
private bool _disposed;
public BoardTestSession(
BoardTestConfiguration configuration,
PeripheryDispenseMode dispenseMode = PeripheryDispenseMode.Unsupported)
{
_configuration = configuration;
Port = new SerialPortHardware();
_cipher = new Aes256EcbBlockCipher(PeripheryControllerSecurity.Key.ToArray());
var authenticator = new ControllerAuthenticator(_cipher);
Controller = new PeripheryMachineController(
Port,
authenticator,
dispenseMode,
stockSensorInputIndex: configuration.StockSensorInputIndex,
stockLowWhenInputHigh: configuration.StockLowWhenInputHigh,
handshakeTimeoutMilliseconds: (int)configuration.AuthorizationTimeout.TotalMilliseconds,
serviceButtonInputIndex: PeripheryControllerInputs.ServiceButton,
serviceButtonActiveHigh: false,
hopper1DispenseSensorInputIndex: PeripheryControllerInputs.Hopper1DispensePulse,
hopper1DispenseSensorActiveHigh: false,
hopper2DispenseSensorInputIndex: PeripheryControllerInputs.Hopper2DispensePulse,
hopper2DispenseSensorActiveHigh: false,
inputPollMilliseconds: 100);
Controller.ConnectionStateChanged += OnConnectionStateChanged;
Port.OnDataReceived += OnDataReceived;
}
public SerialPortHardware Port { get; }
public PeripheryMachineController Controller { get; }
public List<MachineConnectionState> ConnectionStates { get; } = new();
public void ConnectAndAuthorize()
{
TestContext.Progress.WriteLine($"Открытие {_configuration.PortName} @ {_configuration.BaudRate} и запуск handshake.");
long openDeadline = _clock.ElapsedMilliseconds + 3_000;
do
{
Port.Connect(_configuration.PortName, _configuration.BaudRate);
if (Port.IsConnected)
{
break;
}
Thread.Sleep(100);
}
while (_clock.ElapsedMilliseconds < openDeadline);
Assert.That(Port.IsConnected, Is.True, $"Не удалось открыть {_configuration.PortName} за 3 секунды.");
WaitUntil(
() => Controller.IsReady,
_configuration.AuthorizationTimeout,
"Контроллер не перешёл в Ready после полного handshake.");
DrainAndClear(TimeSpan.FromMilliseconds(250));
}
public void DisconnectAndWait()
{
Port.Disconnect();
WaitUntil(
() => Controller.ConnectionState == MachineConnectionState.Disconnected,
TimeSpan.FromSeconds(3),
"Контроллер не опубликовал Disconnected после закрытия порта.");
}
public ObservedBoardMessage SendAndWaitForMessage(
string command,
Func<ObservedBoardMessage, bool> predicate,
string failureMessage)
{
DrainAndClear(TimeSpan.FromMilliseconds(150));
Port.SendCommand(command);
return WaitForMessage(predicate, _configuration.ResponseTimeout, failureMessage);
}
public ObservedBoardMessage WaitForMessage(
Func<ObservedBoardMessage, bool> predicate,
TimeSpan timeout,
string failureMessage)
{
ObservedBoardMessage matched = default;
WaitUntil(
() => TryTakeMessage(predicate, out matched),
timeout,
failureMessage);
return matched;
}
public void WaitUntil(Func<bool> condition, TimeSpan timeout, string failureMessage)
{
long deadline = _clock.ElapsedMilliseconds + (long)timeout.TotalMilliseconds;
while (_clock.ElapsedMilliseconds < deadline)
{
PumpOnce();
if (condition())
{
return;
}
Thread.Sleep(10);
}
PumpOnce();
Assert.That(condition(), Is.True, failureMessage);
}
public void DrainAndClear(TimeSpan duration)
{
long deadline = _clock.ElapsedMilliseconds + (long)duration.TotalMilliseconds;
while (_clock.ElapsedMilliseconds < deadline)
{
PumpOnce();
Thread.Sleep(10);
}
while (_messages.TryDequeue(out _))
{
}
}
public void PumpOnce()
{
Port.ProcessPendingEvents();
Controller.Update((ulong)_clock.ElapsedMilliseconds);
}
public void Dispose()
{
if (_disposed)
{
return;
}
Controller.ConnectionStateChanged -= OnConnectionStateChanged;
Port.OnDataReceived -= OnDataReceived;
Controller.Dispose();
Port.Disconnect();
Port.Dispose();
_cipher.Dispose();
_disposed = true;
}
private bool TryTakeMessage(
Func<ObservedBoardMessage, bool> predicate,
out ObservedBoardMessage matched)
{
while (_messages.TryDequeue(out ObservedBoardMessage candidate))
{
if (predicate(candidate))
{
matched = candidate;
return true;
}
}
matched = default;
return false;
}
private void OnDataReceived(string frame)
{
PeripheryMessage parsed = PeripheryProtocolParser.Parse(frame);
_messages.Enqueue(new ObservedBoardMessage(parsed.Kind, parsed.NumericValue, frame?.Length ?? 0));
}
private void OnConnectionStateChanged(MachineConnectionState state)
{
ConnectionStates.Add(state);
TestContext.Progress.WriteLine($"Сервисное состояние контроллера: {state}.");
}
}
internal readonly record struct ObservedBoardMessage(
PeripheryMessageKind Kind,
int NumericValue,
int FrameLength);