Files
tbf/TBF/Rig/RegisterReaders/AllyReader/Communication/AllyCommandService.cs
T
michal c71fe75446 Ally iPerl initial - reader,test method, unit test
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.
2026-08-18 10:01:02 +02:00

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);
}
}
}
}