tbf/TBF/Rig/RegisterReaders/GenesisRegReader/communication/RadioService.cs
Michal Buzik 52bd0d01ce Restore Genesis communication and Q3 calibration from special branches
A – Registration and execution
- Register GenesisCommunication Factory in the component list.
- Separate the Genesis form and sequence from iPerl communication.

B – Communication activities
- Restore initialization, connection, PCB reading, password and login.
- Include grouped login, mode switching and disconnection.
- Support processing up to 10 slots.

C1 – Input calibration factors
- Read three factors from Water meters / Text1–Text3.
- Validate integer values in the range 1–65535.
- Preserve the default of 15625 when all three fields are empty.

D – Q3 calibration
- Connect the Prepare Q3 → measurement → Write Q3 workflow.
- Add channel processing to FlyingStart and FlyingStartMassCollection.
- Reset previous measurement data and validate calculated factors.
- Mark factors as stored only after StoreCalibration succeeds.

E – Results and database
- Store calibration factors separately for each meter and channel.
- Add result entities, mappings and Q3 data.
- Extend DB.cs / EnsureSchema to create and update the schema.
- Preserve compatibility with the existing binary format.

Validation:
- Debug build and 18 tests passed.
- Simulated communication runs follow the same activity sequence.
- The complete Q3 workflow has not yet been verified on hardware.

Known limitation:
- An inherited mismatch in simulated responses and error propagation
  can produce an incorrect OK result; this change does not fix it.
2026-09-09 15:04:49 +02:00

717 lines
26 KiB
C#

using System;
using System.Threading;
using System.Threading.Tasks;
using log4net;
using TBF.Rig.BridgeComponents.GciBridge;
using TBF.Rig.RegisterReaders.GenesisRegReader.common;
using TBF.Rig.RegisterReaders.GenesisRegReader.communication.common;
using TBF.Rig.RegisterReaders.GenesisRegReader.implementations;
using PublicModels = TBF.Rig.BridgeComponents.GciBridge.Interfaces.PublicModels;
namespace TBF.Rig.RegisterReaders.GenesisRegReader.communication
{
public class ReadPcbResult
{
public bool IsConnected { get; set; }
public bool IsLoggedOn { get; set; }
public bool IsValidPcb { get; set; }
public string PcbId { get; set; }
public string Message { get; set; }
}
public class RadioService
{
private static readonly ILog log = LogManager.GetLogger(typeof(RadioService));
static string okResponse = "Command complete, no errors";
static string errorResponse = "Unable to execute";
private bool bConnected = false;
private GciBridge _bridge;
public RadioService(GciBridge genesisHeadCommInterfaceBridgeComponent)
{
this._bridge = genesisHeadCommInterfaceBridgeComponent;
log.Debug("RadioService created with GciBridge= " + genesisHeadCommInterfaceBridgeComponent + "");
}
private async Task<ReadPcbResult> EnsureConnectedAsync(
GenesisSmartReader head,
CancellationToken token = default)
{
log.Debug("EnsureConnectedAsync called for iHead: " + head);
if (head?.CommInterfaceBridge == null)
{
return new ReadPcbResult
{
IsConnected = false,
IsLoggedOn = false,
IsValidPcb = false,
Message = "CommInterfaceBridge is null."
};
}
var slotInfo = await _bridge.GetSlotAsync(head.GetSlotNr);
if (slotInfo == null || !slotInfo.Success)
{
//just no definet yet ?
log.Debug("EnsureConnectedAsync() - SlotInfo is null.");
}
else if (slotInfo.IsConnected)
{
log.Debug("EnsureConnectedAsync() - Slot: " + slotInfo);
return new ReadPcbResult
{
IsConnected = true,
IsLoggedOn = slotInfo.IsLoggedOn,
IsValidPcb = false,
PcbId = slotInfo.PcbId,
Message = "Already connected."
};
}
log.Debug("EnsureConnectedAsync() - Connecting...");
var connectTask = head.CommInterfaceBridge.ConnectAsync(head.GetSlotNr, token);
var timeoutTask = Task.Delay(TimeSpan.FromMinutes(1), token);
var completedTask = await Task.WhenAny(connectTask, timeoutTask);
if (completedTask != connectTask)
{
return new ReadPcbResult
{
IsConnected = false,
IsLoggedOn = false,
IsValidPcb = false,
Message = "Connect timeout."
};
}
var result = await connectTask;
if (result == null || !result.Success)
{
return new ReadPcbResult
{
IsConnected = false,
IsLoggedOn = false,
IsValidPcb = false,
Message = result == null ? "Connect result is null." : "Connect failed."
};
}
return new ReadPcbResult
{
IsConnected = result.IsConnected,
IsLoggedOn = false,
IsValidPcb = false,
Message = "Connected OK."
};
}
public async Task<PublicModels.UdsPasswordResult> FindKeyStone(
string txtPCBId,
CancellationToken token = default)
{
string pcbId = txtPCBId?.Trim();
if (string.IsNullOrWhiteSpace(pcbId))
throw new Exception("PCB ID is empty.");
token.ThrowIfCancellationRequested();
PublicModels.UdsPasswordResult result = await _bridge.GetPasswordAsync(pcbId, token);
log.Debug("GetPasswordAsync PCB=" + pcbId + " Result: " + result);
return result;
}
public async Task<GenesisCordonelInterface.API.PublicModels.GciLoginResult> LoginByPasswordAsync(
int slotId, string txtPassword,
CancellationToken token = default)
{
if (string.IsNullOrWhiteSpace(txtPassword))
throw new Exception("LoginByPasswordAsync() - PASSWORD is empty.");
token.ThrowIfCancellationRequested();
log.Debug($"LoginByPasswordAsync( Slot: {slotId}) - set password to: " + txtPassword.Substring(0, 4) + "************");
var gciSetPasswordResult = await _bridge.SetPasswordAsync(slotId,txtPassword, token);
log.Debug($"LoginByPasswordAsync( Slot: {slotId}) - SetPasswordAsync Result: " + gciSetPasswordResult);
// LOGIN
log.Debug($"LoginByPasswordAsync( Slot: {slotId}) - START LOGIN");
var gciSlotInfo = await _bridge.GetSlotAsync(slotId);
log.Debug($"LoginByPasswordAsync( Checked before Login() Slot: {slotId}) - START LOGIN Slot: {gciSlotInfo}");
GenesisCordonelInterface.API.PublicModels.GciLoginResult result = await _bridge.LoginAsync(slotId, token);
log.Debug($"LoginByPasswordAsync( Slot: {slotId}) Result: " + result);
log.Debug($"LoginByPasswordAsync( Slot: {slotId}) - END LOGIN, Success: {result.Success}");
// ~ LOGIN
return result;
}
public async Task<ReadPcbResult> ReadRequest_PCBAsync( GenesisSmartReader head, bool bReload = false)
{
log.Debug("ReadRequest_PCB called for iHead: " + head);
if (head?.CommInterfaceBridge == null)
{
return new ReadPcbResult
{
IsConnected = false,
IsValidPcb = false,
Message = "CommInterfaceBridge is null."
};
}
try
{
// CONNECT ONLY IF NEEDED
var connectResult = await EnsureConnectedAsync(head);
if (!connectResult.IsConnected)
{
return connectResult;
}
log.Debug($"ReadRequest_PCB() connect - {connectResult.Message}");
//Check if exist PCB
if (!bReload)
{
var gciSlotInfo = await head.CommInterfaceBridge.GetSlotAsync(head.GetSlotNr);
if (gciSlotInfo == null && gciSlotInfo.Success && string.IsNullOrEmpty(gciSlotInfo.PcbId))
{
log.Debug(
$"BuildConnection() - SlotNr: {head.GetSlotNr} already exist PCB: {gciSlotInfo.PcbId}");
return new ReadPcbResult
{
IsConnected = gciSlotInfo.IsConnected,
IsValidPcb = true,
PcbId = gciSlotInfo.PcbId,
Message = "PCB already exist."
};
}
}
// PCB READ LOOP
string validPcbId = null;
int maxAttempts = 5;
DateTime startTime = DateTime.UtcNow;
TimeSpan maxDuration = TimeSpan.FromSeconds(30);
//LOOP
for (int attempt = 1; attempt <= maxAttempts; attempt++)
{
if (DateTime.UtcNow - startTime > maxDuration)
{
log.Debug($"ReadRequest_PCB({head.GetSlotNr}) - PCB max duration exceeded");
break;
}
log.Debug($"ReadRequest_PCB({head.GetSlotNr}) - PCB attempt {attempt}/{maxAttempts}");
var pcbTask = head.CommInterfaceBridge.GetPcbIdAsync(head.GetSlotNr);
var timeoutTaskPcb = Task.Delay(TimeSpan.FromSeconds(5));
var completedTaskPcb = await Task.WhenAny(pcbTask, timeoutTaskPcb);
if (completedTaskPcb != pcbTask)
{
log.Debug($"ReadRequest_PCB() PCB attempt {attempt} - Timeout");
continue;
}
var resultPCB = await pcbTask;
// VALIDATION BLOCK
{
if (resultPCB == null)
{
log.Debug($"ReadRequest_PCB({head.GetSlotNr}) PCB attempt {attempt} - result is null");
continue;
}
if (!resultPCB.Success)
{
log.Debug($"ReadRequest_PCB({head.GetSlotNr}) PCB attempt {attempt} - Success=false");
continue;
}
string pcbId = resultPCB.PcbId;
if (string.IsNullOrWhiteSpace(pcbId))
{
log.Debug($"ReadRequest_PCB({head.GetSlotNr}) PCB attempt {attempt} - PCB empty");
continue;
}
pcbId = pcbId.Trim();
if (pcbId.Length != 9)
{
log.Debug( $"ReadRequest_PCB({head.GetSlotNr}) PCB attempt {attempt} - Invalid PCB length: '{pcbId}', len={pcbId.Length}");
continue;
}
validPcbId = pcbId;
log.Debug($"ReadRequest_PCB({head.GetSlotNr}) PCB valid: {validPcbId}");
break;
}
}
if (string.IsNullOrEmpty(validPcbId))
{
return new ReadPcbResult
{
IsConnected = connectResult.IsConnected,
IsValidPcb = false,
PcbId = null,
Message = "Valid PCB not found."
};
}
if (head.ConfigStruct != null)
{
head.ConfigStruct.PCBNumberString = validPcbId;
}
return new ReadPcbResult
{
IsConnected = connectResult.IsConnected,
IsValidPcb = true,
PcbId = validPcbId,
Message = "PCB OK"
};
}
catch (Exception ex)
{
log.Error("ReadRequest_PCBAsync() failed", ex);
return new ReadPcbResult
{
IsConnected = false,
IsValidPcb = false,
PcbId = null,
Message = ex.Message
};
}
}
public async Task<bool> PrepareLoginAdnConnect_Async(
GenesisSmartReader iHead,
bool isConnected,
CancellationToken token = default)
{
log.Debug("PrepareLoginAdnConnect_Async called for iHead: " + iHead + " isConnected: " + isConnected);
if (iHead?.CommInterfaceBridge == null)
return false;
try
{
token.ThrowIfCancellationRequested();
var connectResult = await EnsureConnectedAsync(iHead, token);
if (!connectResult.IsConnected)
return false;
log.Debug($"PrepareLoginAdnConnect_Async() connect - {connectResult.Message}");
if (connectResult.IsLoggedOn)
return true;
//get stored PCB - Keystone
log.Debug("Have we PCB stored?");
string pcb = iHead.ConfigStruct?.PCBNumberString;
if (string.IsNullOrWhiteSpace(pcb))
{
pcb = connectResult.PcbId;
if (string.IsNullOrWhiteSpace(pcb))
{
pcb = iHead.SerialNr;
}
//GET PCB FROM Meter
if (string.IsNullOrWhiteSpace(pcb))
{
log.Debug("PrepareLoginAdnConnect_Async() No PCB stored. Trying to get PCB from Meter");
var pcbResult = await ReadRequest_PCBAsync(iHead);
if (pcbResult.IsValidPcb)
{
pcb = pcbResult.PcbId;
iHead.SerialNr = pcb;
if (iHead.ConfigStruct != null)
{
iHead.ConfigStruct.PCBNumberString = pcb;
}
}
}
}
if (string.IsNullOrWhiteSpace(pcb))
{
log.Debug("PrepareLoginAdnConnect_Async() No PCB stored. MISSING PCB!!!!!");
return false;
}
log.Debug( $"PrepareLoginAdnConnect_Async() Start Find Keystone Slot: {iHead.GetSlotNr} PCB:{iHead.SerialNr} Calib PCB:{pcb} - find keystone");
var keyStoneResult = await FindKeyStone(pcb, token);
if (keyStoneResult == null || !keyStoneResult.Success)
return false;
//LOGIN
log.Debug( $"PrepareLoginAdnConnect_Async() Start Login Slot: {iHead.GetSlotNr} PCB:{iHead.SerialNr} Calib PCB:{pcb} - login");
GenesisCordonelInterface.API.PublicModels.GciLoginResult loginByPasswordAsync =
await LoginByPasswordAsync(iHead.GetSlotNr, keyStoneResult.Password, token);
if (loginByPasswordAsync == null || !loginByPasswordAsync.Success)
{
return false;
}
log.Debug($"PrepareLoginAdnConnect_Async() Login OK");
return true;
}
catch (OperationCanceledException)
{
log.Debug("PrepareLoginAdnConnect_Async() canceled.");
return false;
}
}
public static readonly String LedMode = "GENESISFLOW_LedMode";
public static readonly String SampleRate = "GENESISFLOW_SampleRate";
public static readonly String CalFactor1 = "GENESISFLOW_CalFactor1";
public static readonly String CalFactor2 = "GENESISFLOW_CalFactor2";
public static readonly String CalFactor3 = "GENESISFLOW_CalFactor3";
public static readonly String ResetAccumulators = "GENESISFLOW_ResetAccumulators";
public static readonly String ForwardArrow = "GENESISFLOW_ForwardArrow";
public static readonly String StoreCalibration = "GENESISFLOW_StoreCalibration";
public static readonly String TriggerIdle = "GENESISFLOW_TriggerIdle";
public static readonly String MeterSize = "GENESISFLOW_MeterSize";
public async Task<LedState> GetActivityLedStatusMode_Async(
GenesisSmartReader iHead,
bool isConnected,
CancellationToken token = default)
{
log.Debug("GetActivityLedStatusMode_Async called for iHead: " + iHead + " isConnected: " + isConnected);
if (iHead?.CommInterfaceBridge == null)
return LedState.Unknown;
try
{
var loginAdnConnectAsync = await PrepareLoginAdnConnect_Async(iHead, isConnected, token);
if (!loginAdnConnectAsync)
return LedState.Unknown;
//Get Activity Status
var registerReadResult =
await _bridge.ReadRegisterWithRetryAsync(iHead.GetSlotNr,LedMode, token);
if (registerReadResult == null || !registerReadResult.Success)
{
return LedState.Unknown;
}
log.Debug($"GetActivityStatusMode() Read Register OK Response: {registerReadResult}");
try
{
byte[] bytes = HexFormatter.HexStringToByteArray(registerReadResult.Result.RawHex);
//Convert byte array to int
if (bytes == null || bytes.Length < 4)
{
log.Debug("Invalid byte array length");
return LedState.Unknown;
}
int value = BitConverter.ToInt32(bytes, 0);
// continue your real status logic here...
return value == 6 ? LedState.active : LedState.inactive;
}catch(Exception ex)
{
log.Error("GetActivityStatusMode() failed", ex);
return LedState.Unknown;
}
}
catch (OperationCanceledException)
{
log.Debug("GetActivityLedStatusMode_Async() canceled.");
return LedState.Unknown;
}
catch (Exception ex)
{
log.Error("GetActivityLedStatusMode_Async() failed", ex);
return LedState.Unknown;
}
}
public async Task<bool> SetLedMode_Async(GenesisSmartReader iHead, LedState ledMode, bool isConnected,
CancellationToken token = default)
{
log.Debug("SetLedMode_Async called for iHead: " + iHead + " isConnected: " + isConnected);
if (iHead?.CommInterfaceBridge == null)
return false;
try
{
var loginAdnConnectAsync = await PrepareLoginAdnConnect_Async(iHead, isConnected, token);
if (!loginAdnConnectAsync)
return false;
//Get Activity Status
byte valueLed = (ledMode == LedState.active) ? (byte)6 : (byte)0;
log.Debug($"SetLedMode_Async() Set Led Mode: {valueLed}");
var registerWriteResult =
await _bridge.WriteRegisterAsync(iHead.GetSlotNr, LedMode, valueLed, false, false, token);
if (registerWriteResult == null || !registerWriteResult.Success)
{
return false;
}
log.Debug($"SetLedMode_Async() Write Register OK Response: {registerWriteResult}");
return true;
}
catch (OperationCanceledException)
{
log.Debug("GetActivityLedStatusMode_Async() canceled.");
return false;
}
catch (Exception ex)
{
log.Error("GetActivityLedStatusMode_Async() failed", ex);
return false;
}
finally
{
log.Debug("SetLedMode_Async() - End - DO DISCONNECT");
var gciDisconnectResult = await _bridge.DisconnectAsync(iHead.GetSlotNr);
log.Debug($"SetLedMode_Async() Disconnect Result: {gciDisconnectResult}");
}
}
public async Task<bool> Disconnect_Async(GenesisSmartReader iHead)
{
try
{
log.Debug("Disconnect_Async() - Start");
var gciSlotInfo = await _bridge.GetSlotAsync(iHead.GetSlotNr);
log.Debug($"Disconnect_Async() - Slot: {iHead.GetSlotNr} - SlotInfo: {gciSlotInfo}");
log.Debug("Disconnect_Async() - DO DISCONNECT");
var gciDisconnectResult = await _bridge.DisconnectAsync(iHead.GetSlotNr);
log.Debug($"Disconnect_Async() Disconnect Result: {gciDisconnectResult}");
return gciDisconnectResult.Success;
}catch(Exception ex)
{
log.Error("Disconnect_Async() failed", ex);
return false;
}
}
private static ushort SafeIntToUShort(int value)
{
if (value < ushort.MinValue || value > ushort.MaxValue)
return 0xFD; // your error code
return (ushort)value;
}
public string GetVersion(GenesisSmartReader iHead)
{
if (iHead?.CommInterfaceBridge == null)
return string.Empty;
var connectResult = iHead.CommInterfaceBridge
.ConnectAsync(iHead.GetSlotNr)
.GetAwaiter()
.GetResult();
if (connectResult == null || !connectResult.Success || !connectResult.IsConnected)
return string.Empty;
// if (!string.IsNullOrEmpty(version))
// {
// if (iHead.ConfigStruct != null) // store mechanism
// {
// iHead.ConfigStruct.Version = version;
// }
// return version;
// }
return string.Empty;
}
public async Task<bool> SetActivityMode_Active(GenesisSmartReader iHead,
bool isConnected, CancellationToken token = default)
{
log.Debug("SetActivityMode_Active called for iHead: " + iHead + " isConnected: " + isConnected);
if (iHead?.CommInterfaceBridge == null)
return false;
try
{
token.ThrowIfCancellationRequested();
var connectResult = await EnsureConnectedAsync(iHead, token);
if (!connectResult.IsConnected)
return false;
isConnected = connectResult.IsConnected;
log.Debug($"SetActivityMode_Active() connect - {connectResult.Message}");
//Set LED to state 4
// string version = _bridge?.GciExternalInterface?.GetPcbId(iHead.GetSlotNr);
// if (!string.IsNullOrEmpty(version))
// {
// if (iHead.ConfigStruct != null) // store mechanism
// {
// iHead.ConfigStruct.Version = version;
// }
// return true;
// }
}catch(OperationCanceledException)
{
return false;
}
return false;
}
public bool SetActivityMode_Idle(GenesisSmartReader iHead)
{
if (iHead?.CommInterfaceBridge == null)
return false;
var connectResult = iHead.CommInterfaceBridge
.ConnectAsync(iHead.GetSlotNr)
.GetAwaiter()
.GetResult();
if (connectResult == null || !connectResult.Success || !connectResult.IsConnected)
return false;
//Set Activity State Idle
// string version = _bridge?.GciExternalInterface?.GetPcbId(iHead.GetSlotNr);
// if (!string.IsNullOrEmpty(version))
// {
// if (iHead.ConfigStruct != null) // store mechanism
// {
// iHead.ConfigStruct.Version = version;
// }
// return true;
// }
return false;
}
public bool SetOptTestMode(GenesisSmartReader iHead)
{
if (iHead?.CommInterfaceBridge == null)
return false;
var connectResult = iHead.CommInterfaceBridge
.ConnectAsync(iHead.GetSlotNr)
.GetAwaiter()
.GetResult();
if (connectResult == null || !connectResult.Success || !connectResult.IsConnected)
return false;
//Set LED to state 4
// string version = _bridge?.GciExternalInterface?.GetPcbId(iHead.GetSlotNr);
// if (!string.IsNullOrEmpty(version))
// {
// if (iHead.ConfigStruct != null) // store mechanism
// {
// iHead.ConfigStruct.Version = version;
// }
// return true;
// }
return false;
}
/// <summary>
/// stop data streaming by LED
/// </summary>
/// <param name="iHead"></param>
/// <returns></returns>
public bool SetOptActiveMode(GenesisSmartReader iHead)
{
if (iHead?.CommInterfaceBridge == null)
return false;
var connectResult = iHead.CommInterfaceBridge
.ConnectAsync(iHead.GetSlotNr)
.GetAwaiter()
.GetResult();
if (connectResult == null || !connectResult.Success || !connectResult.IsConnected)
return false;
//Set LED to state 1
// string version = _bridge?.GciExternalInterface?.GetPcbId(iHead.GetSlotNr);
// if (!string.IsNullOrEmpty(version))
// {
// if (iHead.ConfigStruct != null) // store mechanism
// {
// iHead.ConfigStruct.Version = version;
// }
// return true;
// }
return false;
}
public string GetUnit(GenesisSmartReader iperlHead)
{
if (iperlHead?.CommInterfaceBridge == null)
return string.Empty;
var connectResult = iperlHead.CommInterfaceBridge
.ConnectAsync(iperlHead.GetSlotNr)
.GetAwaiter()
.GetResult();
if (connectResult == null || !connectResult.Success || !connectResult.IsConnected)
return string.Empty;
// string version = _bridge?.GciExternalInterface?.GetPcbId(iperlHead.GetSlotNr);
// if (!string.IsNullOrEmpty(version))
// {
// if (iperlHead.ConfigStruct != null) // store mechanism
// {
// iperlHead.ConfigStruct.Version = version;
// }
// return version;
// }
return string.Empty;
}
}
}