Add AllyReader and AllyCalibration support with tests: Introduce new register readers, calibration methods, and comprehensive unit tests for integration and functionality validation. Update project files accordingly.
209 lines
7.3 KiB
C#
209 lines
7.3 KiB
C#
using System;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
|
|
namespace TBF.Rig.RegisterReaders.AllyReader.Communication
|
|
{
|
|
/// <summary>
|
|
/// ALLY commands required by UI-2093 R9.0. The service deliberately omits
|
|
/// iPerl-only Q2, 2 Hz, flip-mode and RFID structure operations.
|
|
/// </summary>
|
|
public sealed class AllyCommandService
|
|
{
|
|
private static readonly DateTime SystemTimeEpochUtc =
|
|
new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
|
|
|
private readonly IAllyTransport transport;
|
|
private readonly AllyFrameParser parser = new AllyFrameParser();
|
|
|
|
public AllyCommandService(IAllyTransport transport)
|
|
{
|
|
this.transport = transport ?? throw new ArgumentNullException(nameof(transport));
|
|
}
|
|
|
|
public string ReadSerialNumber(int timeoutMs)
|
|
{
|
|
AllyResponse response = Send(
|
|
new AllyFrameBuilder().WithCommand(AllyCommand.ViewFactoryId).BuildBytes(),
|
|
timeoutMs);
|
|
return response.GetNullTerminatedAscii();
|
|
}
|
|
|
|
public AllyVersionInfo ReadVersionAndType(int timeoutMs)
|
|
{
|
|
AllyResponse response = Send(
|
|
new AllyFrameBuilder().WithCommand(AllyCommand.ViewVersionAndType).BuildBytes(),
|
|
timeoutMs);
|
|
return AllyVersionInfo.Parse(response.GetNullTerminatedAscii());
|
|
}
|
|
|
|
public DateTime ReadSystemTimeUtc(int timeoutMs)
|
|
{
|
|
AllyResponse response = Send(
|
|
new AllyFrameBuilder().WithDeviceCommand(AllyDeviceCommand.ViewSystemTime).BuildBytes(),
|
|
timeoutMs);
|
|
return SystemTimeEpochUtc.AddSeconds(response.GetUInt32LittleEndian());
|
|
}
|
|
|
|
public byte ReadRebootCount(int timeoutMs)
|
|
{
|
|
AllyResponse response = Send(
|
|
new AllyFrameBuilder().WithDeviceCommand(AllyDeviceCommand.ViewRebootCount).BuildBytes(),
|
|
timeoutMs);
|
|
return response.GetByte();
|
|
}
|
|
|
|
public void OpenValve(int timeoutMs)
|
|
{
|
|
Send(
|
|
new AllyFrameBuilder()
|
|
.WithCommand(AllyCommand.SetValvePosition)
|
|
.WithBytes(0x00, 0x02)
|
|
.BuildBytes(),
|
|
timeoutMs);
|
|
}
|
|
|
|
public void SetSpreadSpectrum(bool enabled, int timeoutMs)
|
|
{
|
|
Send(
|
|
new AllyFrameBuilder()
|
|
.WithDeviceCommand(AllyDeviceCommand.Configuration)
|
|
.WithBytes(0x06, enabled ? (byte)0x00 : (byte)0x01)
|
|
.BuildBytes(),
|
|
timeoutMs);
|
|
}
|
|
|
|
public void SetMeterMode(byte mode, int timeoutMs)
|
|
{
|
|
Send(
|
|
new AllyFrameBuilder()
|
|
.WithCommand(AllyCommand.SetMeterMode)
|
|
.WithByte(mode)
|
|
.BuildBytes(),
|
|
timeoutMs);
|
|
}
|
|
|
|
public void SetDiagnosticLed(byte mode, int timeoutMs)
|
|
{
|
|
Send(
|
|
new AllyFrameBuilder()
|
|
.WithDeviceCommand(AllyDeviceCommand.SetDiagnosticLed)
|
|
.WithByte(mode)
|
|
.BuildBytes(),
|
|
timeoutMs);
|
|
}
|
|
|
|
public double ReadCalibrationFactorPercent(int timeoutMs)
|
|
{
|
|
AllyResponse response = Send(
|
|
new AllyFrameBuilder().WithDeviceCommand(AllyDeviceCommand.ViewCalibration).BuildBytes(),
|
|
timeoutMs);
|
|
return response.GetUInt16LittleEndian() / 40.96D;
|
|
}
|
|
|
|
public void SetCalibrationFactorPercent(double factorPercent, int timeoutMs)
|
|
{
|
|
if (factorPercent <= 0 || factorPercent > ushort.MaxValue / 40.96D)
|
|
throw new ArgumentOutOfRangeException(nameof(factorPercent));
|
|
|
|
ushort rawValue = checked((ushort)Math.Round(
|
|
factorPercent * 40.96D,
|
|
MidpointRounding.AwayFromZero));
|
|
|
|
Send(
|
|
new AllyFrameBuilder()
|
|
.WithDeviceCommand(AllyDeviceCommand.SetCalibration)
|
|
.WithUInt16LittleEndian(rawValue)
|
|
.BuildBytes(),
|
|
timeoutMs);
|
|
}
|
|
|
|
public void SetMagneticTamperProfile(AllyMagneticTamperProfile profile, int timeoutMs)
|
|
{
|
|
AllyFrameBuilder builder = new AllyFrameBuilder()
|
|
.WithDeviceCommand(AllyDeviceCommand.Configuration);
|
|
|
|
// Exact UI-2093 frames. The leading byte selects the RCS profile
|
|
// (0x02) or the firmware-default profile (0x05).
|
|
if (profile == AllyMagneticTamperProfile.Calibration)
|
|
builder.WithBytes(0x02, 0x5A, 0x0C, 0x05, 0x3C, 0x50);
|
|
else
|
|
builder.WithBytes(0x05, 0x1E, 0x04, 0x05, 0x3C, 0x50);
|
|
|
|
Send(builder.BuildBytes(), timeoutMs);
|
|
}
|
|
|
|
public void SetDisplayVolume(string eightDigitVolume, int timeoutMs)
|
|
{
|
|
if (eightDigitVolume == null ||
|
|
eightDigitVolume.Length != 8 ||
|
|
!eightDigitVolume.All(char.IsDigit))
|
|
{
|
|
throw new ArgumentException(
|
|
"ALLY display volume must contain exactly eight digits.",
|
|
nameof(eightDigitVolume));
|
|
}
|
|
|
|
Send(
|
|
new AllyFrameBuilder()
|
|
.WithCommand(AllyCommand.SetPresetTotal)
|
|
.WithNullTerminatedAscii(eightDigitVolume)
|
|
.BuildBytes(),
|
|
timeoutMs);
|
|
}
|
|
|
|
public void SetLcdTimeout(byte seconds, int timeoutMs)
|
|
{
|
|
Send(
|
|
new AllyFrameBuilder()
|
|
.WithDeviceCommand(AllyDeviceCommand.SetLcdTimeout)
|
|
.WithByte(seconds)
|
|
.BuildBytes(),
|
|
timeoutMs);
|
|
}
|
|
|
|
public void StartOffsetLearning(ushort samples, ushort startDelaySeconds, int timeoutMs)
|
|
{
|
|
if (samples == 0)
|
|
throw new ArgumentOutOfRangeException(nameof(samples));
|
|
|
|
Send(
|
|
new AllyFrameBuilder()
|
|
.WithDeviceCommand(AllyDeviceCommand.StartOffsetLearning)
|
|
.WithUInt16LittleEndian(samples)
|
|
.WithUInt16LittleEndian(startDelaySeconds)
|
|
.BuildBytes(),
|
|
timeoutMs);
|
|
}
|
|
|
|
private AllyResponse Send(byte[] request, int timeoutMs)
|
|
{
|
|
try
|
|
{
|
|
if (!transport.IsOpen)
|
|
transport.Open();
|
|
|
|
byte[] rawResponse = transport.SendAndWait(request, timeoutMs);
|
|
AllyResponse response = parser.ParseResponse(rawResponse);
|
|
if (!response.IsSuccess)
|
|
{
|
|
throw new AllyCommunicationException(
|
|
"ALLY command failed with status 0x" +
|
|
response.Status.ToString("X2", CultureInfo.InvariantCulture) + ".",
|
|
response.Status);
|
|
}
|
|
|
|
return response;
|
|
}
|
|
catch (AllyCommunicationException)
|
|
{
|
|
throw;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
throw new AllyCommunicationException("ALLY communication failed.", ex);
|
|
}
|
|
}
|
|
}
|
|
}
|