Compare commits

...
85 changed files with 5770 additions and 707 deletions
@@ -1,6 +1,7 @@
using System;
using MySql.Data.MySqlClient;
using Common;
using log4net;
namespace Results.Entities.helpers
{
@@ -10,6 +11,8 @@ namespace Results.Entities.helpers
/// </summary>
public static class DatabaseMigrationHelper
{
private static readonly ILog log = LogManager.GetLogger(typeof(DatabaseMigrationHelper));
public static void EnsureSchema(DBType dbType, string connectionString)
{
switch (dbType)
@@ -36,9 +39,31 @@ namespace Results.Entities.helpers
// EnsureColumnMySql(conn, "WaterMeter", "Q3Channel", "INT NOT NULL DEFAULT 0");
// EnsureColumnMySql(conn, "MeterTestRslt", "Q3Channel", "INT NOT NULL DEFAULT 0");
EnsureColumnMySql(conn, "MeterTestRslt", "FlipMode", "INT NULL");
}
}
EnsureColumnMySql(conn, "MeterTestRslt", "PulsesPerKilogram", "DOUBLE NOT NULL DEFAULT 0");
EnsureMeterTestResultMassColumnsMySql(conn);
EnsureColumnMySql(conn, "MeterTestRslt", "FlipMode", "INT NULL");
EnsureColumnMySql(conn, "MeterTestRslt", "ExtraDataPath", "VARCHAR(255) NULL");
EnsureMeterTestResultExtraColumnsMySql(conn);
}
}
private static void EnsureMeterTestResultExtraColumnsMySql(MySqlConnection conn)
{
for (int index = 1; index <= 9; index++)
EnsureColumnMySql(conn, "MeterTestRslt", "X" + index, "FLOAT NOT NULL DEFAULT 0");
}
// These properties were added to the MeterTestRslt NHibernate mapping
// after older customer databases had already been created. Keep them in
// one migration group so a batch insert does not fail one column at a time.
private static void EnsureMeterTestResultMassColumnsMySql(MySqlConnection conn)
{
EnsureColumnMySql(conn, "MeterTestRslt", "MassMeter", "DOUBLE NOT NULL DEFAULT 0");
EnsureColumnMySql(conn, "MeterTestRslt", "MassRef", "DOUBLE NOT NULL DEFAULT 0");
EnsureColumnMySql(conn, "MeterTestRslt", "ErrorMass", "DOUBLE NOT NULL DEFAULT 0");
EnsureColumnMySql(conn, "MeterTestRslt", "PassedMass", "TINYINT(1) NOT NULL DEFAULT 0");
EnsureColumnMySql(conn, "MeterTestRslt", "QuantityUnits", "VARCHAR(32) NULL");
}
private static void EnsureColumnMySql(
MySqlConnection conn,
@@ -74,7 +99,9 @@ namespace Results.Entities.helpers
alter.Transaction = transaction;
alter.CommandText = "ALTER TABLE `" + tableName + "` ADD COLUMN `" +
columnName + "` " + columnDefinition;
alter.ExecuteNonQuery();
alter.ExecuteNonQuery();
log.WarnFormat("Results DB migration: added {0}.{1} ({2}).",
tableName, columnName, columnDefinition);
}
}
@@ -100,9 +127,28 @@ namespace Results.Entities.helpers
// EnsureColumnSQLite(conn, "WaterMeter", "Q3Channel", "INTEGER NOT NULL DEFAULT 0");
// EnsureColumnSQLite(conn, "MeterTestRslt", "Q3Channel", "INTEGER NOT NULL DEFAULT 0");
EnsureColumnSQLite(conn, "MeterTestRslt", "FlipMode", "INTEGER NULL");
}
}
EnsureColumnSQLite(conn, "MeterTestRslt", "PulsesPerKilogram", "REAL NOT NULL DEFAULT 0");
EnsureMeterTestResultMassColumnsSQLite(conn);
EnsureColumnSQLite(conn, "MeterTestRslt", "FlipMode", "INTEGER NULL");
EnsureColumnSQLite(conn, "MeterTestRslt", "ExtraDataPath", "TEXT NULL");
EnsureMeterTestResultExtraColumnsSQLite(conn);
}
}
private static void EnsureMeterTestResultExtraColumnsSQLite(System.Data.SQLite.SQLiteConnection conn)
{
for (int index = 1; index <= 9; index++)
EnsureColumnSQLite(conn, "MeterTestRslt", "X" + index, "REAL NOT NULL DEFAULT 0");
}
private static void EnsureMeterTestResultMassColumnsSQLite(System.Data.SQLite.SQLiteConnection conn)
{
EnsureColumnSQLite(conn, "MeterTestRslt", "MassMeter", "REAL NOT NULL DEFAULT 0");
EnsureColumnSQLite(conn, "MeterTestRslt", "MassRef", "REAL NOT NULL DEFAULT 0");
EnsureColumnSQLite(conn, "MeterTestRslt", "ErrorMass", "REAL NOT NULL DEFAULT 0");
EnsureColumnSQLite(conn, "MeterTestRslt", "PassedMass", "INTEGER NOT NULL DEFAULT 0");
EnsureColumnSQLite(conn, "MeterTestRslt", "QuantityUnits", "TEXT NULL");
}
private static void EnsureColumnSQLite(
System.Data.SQLite.SQLiteConnection conn,
@@ -136,7 +182,9 @@ namespace Results.Entities.helpers
{
alter.CommandText = "ALTER TABLE " + tableName + " ADD COLUMN " +
columnName + " " + columnDefinition;
alter.ExecuteNonQuery();
alter.ExecuteNonQuery();
log.WarnFormat("Results DB migration: added {0}.{1} ({2}).",
tableName, columnName, columnDefinition);
}
}
}
+6
View File
@@ -227,6 +227,12 @@ namespace TBF
///
public long OptoHeadsEnabled; /// Bit field with opto-head enabled states, used by iPerlCommunicationForm and S640CommForm
/// <summary>
/// Smart-meter reader selection used by SmartCommunicationForm and DataEntry.UNI.
/// OptoHeadsEnabled remains for the legacy iPerl and S640 dialogs.
/// </summary>
public SmartReaderSelectionSettings SmartReaderSelections;
/// Serial numbers
public string[] LastSNTexts;
[XmlIgnore]
+11
View File
@@ -3,3 +3,14 @@
| Version | Source of change | Target Environment | Title | Description |
|------------|----------------------|--------------------------------------------------------------|-----------------------------------------------------|------------------------------------|
| 3.9.2149.0 | Michal databse error | HeatMeters, Heat meter sensors, Procedure Dilog, Tab Process | Excanged columns value 'Sensor' and 'Heat meter sensor' | Fix in code ProcedureDlg, row 1851 |
| 3.9.2149.1 | Main TBF | General release | Version iteration | Assembly and file version increment. |
| 3.9.2149.2 | Main TBF | Poseidon register reader | Poseidon pulse and timing handling | Added reference-pulse reading in `Run()` and improved Poseidon timing/task tracking. |
| 3.9.2149.4 | Main TBF | Mass collection / Poseidon start-stop | Mass collection update | Refactored standing-start mass collection, extended Poseidon start/end data-entry configuration and improved dialog/task handling. |
| 3.9.2200.1 | Main TBF | Test infrastructure | TBF test assembly access | Added `InternalsVisibleTo` support for `TBFTests`. |
| 3.9.2201.1 | Main TBF | Poseidon CLI | Poseidon CLI configuration | Added macro descriptions, refined serial-port/CLI argument configuration and extended CLI test coverage. |
| 3.9.2202.1 | Main TBF | Poseidon CLI | CLI release iteration | Assembly and file version increment for the Poseidon CLI workstream. |
| 3.9.2203.1 | Main TBF | Poseidon CLI / smart-meter sequence | CLI test and configuration update | Updated CLI executable test setup, serial-port default responses and smart-meter component-name handling. |
| 3.9.2204.1 | Morrisville | Morrisville / Poseidon CLI | Morrisville CLI diagnostics | Restored lost 2204 versioning, added detailed CLI command/response logging and used a fixed CLI directory for deployment. |
| 3.9.2205.1 | Morrisville | Morrisville / Poseidon CLI / Results DB | Poseidon read diagnostics and results DB migration | Improved Poseidon CLI execution and diagnostics: fixed CLI working directory, exit code/stdout/stderr capture, JSON/NFC/reading validation and culture-independent decimal parsing. Added reader-cycle and fake-CLI coverage. Results DB now creates missing compatibility columns (`FlipMode`, `ExtraDataPath`, `X1`-`X9`) automatically for MySQL and SQLite. |
| 3.9.2206.0 | Morrisville / Main TBF | Poseidon CLI / Results DB | Poseidon start/end rearm and simulation isolation | Re-armed a completed START reader once for END, preventing reused START values or skipped END CLI calls. Added CLI response, dialog prefill and confirmed-value diagnostics. Simulation always runs `C:\TBF\Cli\cmdSleepTest.exe` instead of the configured physical Hat CLI. Added customer-response, decimal separator, non-zero, dialog-transfer, reader-cycle and simulation-selection regression tests. Added compatibility migration for `PulsesPerKilogram`, `MassMeter`, `MassRef`, `ErrorMass`, `PassedMass` and `QuantityUnits`. |
| 3.9.3143.0 | Ally port | Ally / Poseidon CLI / Results DB | Morrisville Poseidon fixes transferred | Ported the applicable Morrisville Poseidon read, simulation, logging, regression-test and results-schema migration fixes to the Ally source branch while retaining its compatible legacy reader configuration and non-blocking UI flow. |
+223 -90
View File
@@ -41,6 +41,8 @@ namespace TBF.Rig.DataEntry.PoseidonCmd
System.Windows.Forms.Form modelessDlg;
bool modelessDialogOpening;
bool modelessDialogWaitLogged;
public bool Completed { get { return (modelessDlg is IHasCompleted) ? (modelessDlg as IHasCompleted).Completed : true; } }
double refVolume;
@@ -70,6 +72,15 @@ namespace TBF.Rig.DataEntry.PoseidonCmd
}
ReadDataOp readDataOp;
// Keep the CLI phase between state-machine ticks. Blocking here prevents
// the state machine from processing a UI STOP request.
PoseidonReadPhaseRunner poseidonPhaseRunner;
List<IPoseidonReadOperation> poseidonPhaseReaders;
DateTime poseidonPhaseStartedAt;
DateTime poseidonNextProgressLogAt;
bool sendStartPhaseInitialized;
int sendStartPhaseIterations;
public EntryForm() { }
@@ -126,6 +137,8 @@ namespace TBF.Rig.DataEntry.PoseidonCmd
//entryFormCfg.Direction = Direction.S640;
//currentOp = CurrentOp.EnterTestStartStates;
this.regReaders = regReaders;
log.InfoFormat("Poseidon data-entry requested START; operation={0}, batchWMs={1}, readers=[{2}]",
currentOp, waterMeters == null ? -1 : waterMeters.Count, DescribeReaders(regReaders));
int iterator = 0;
foreach (IRegReader reader in regReaders)
{
@@ -167,35 +180,105 @@ namespace TBF.Rig.DataEntry.PoseidonCmd
this.refVolume = refVolume;
this.errLimLo = errLimLo;
this.errLimHi = errLimHi;
log.InfoFormat("Poseidon data-entry requested END; operation={0}, batchWMs={1}, refVolume={2}, errLow={3}, errHigh={4}, readers=[{5}]",
currentOp, waterMeters == null ? -1 : waterMeters.Count, refVolume, errLimLo, errLimHi,
DescribeReaders(regReaders));
return this;
}
delegate void EntryFormDlgt(EntryForm myRef);
string DescribeReaders(IRegReader[] readers)
{
if (readers == null) return "<null>";
var descriptions = new List<string>();
for (int index = 0; index < readers.Length; index++)
{
IRegReader reader = readers[index];
if (reader == null)
{
descriptions.Add(index + ":<null>");
continue;
}
PoseidonReader poseidonReader = reader as PoseidonReader;
descriptions.Add(poseidonReader == null
? index + ":" + reader.GetType().Name
: string.Format("{0}:{1}(COM{2},state={3})", index, poseidonReader.Name,
poseidonReader.ComPortNr, poseidonReader.CurrentOp));
}
return string.Join("; ", descriptions);
}
int EnabledWaterMetersCount()
{
if (disabled == null) return TBF.Data.WMsCount;
int count = 0;
for (int index = 0; index < disabled.Length; index++)
if (!disabled[index]) count++;
return count;
}
///
void OpenBeginningDlg(EntryForm myRef)
{
if (ShowForm == 0)
return;
log.DebugFormat("Poseidon data-entry: creating CycleBeginningForm; configuredWMs={0}, batchWMs={1}",
TBF.Data.WMsCount, waterMeters == null ? -1 : waterMeters.Count);
myRef.modelessDlg = new CycleBeginningForm(TBF.Data.WMsCount, myRef.entryFormCfg,
ProcessData.SelectedProcedure.OrderInfo != null ? ProcessData.SelectedProcedure.OrderInfo.POName : string.Empty);
(myRef.modelessDlg as CycleBeginningForm)?.AutoClickOkAfterDelay();
modelessDlg.Show();
}
// The state machine runs on a worker thread. A synchronous Invoke can
// deadlock it while the UI is waiting for the next state-machine tick.
void BeginOpenDialog(string dialogName, EntryFormDlgt openDialog)
{
modelessDialogOpening = true;
modelessDialogWaitLogged = false;
log.InfoFormat("Poseidon data-entry: queueing dialog={0}, operation={1}, showForm={2}, readers=[{3}]",
dialogName, currentOp, ShowForm, DescribeReaders(regReaders));
Program.MainWnd.BeginInvoke(new Action(() =>
{
try
{
log.DebugFormat("Poseidon data-entry: opening dialog={0}, operation={1}", dialogName, currentOp);
openDialog(this);
log.InfoFormat("Poseidon data-entry: dialog opened={0}, operation={1}, formType={2}",
dialogName, currentOp, modelessDlg == null ? "<null>" : modelessDlg.GetType().Name);
}
finally
{
modelessDialogOpening = false;
}
}));
}
///
void OpenTestStartStatesDlg(EntryForm myRef)
{
if (ShowForm == 0)
return;
log.DebugFormat("Poseidon data-entry: creating TestStartEndForm START; configuredWMs={0}, enabledWMs={1}, batchWMs={2}, readers=[{3}]",
TBF.Data.WMsCount, EnabledWaterMetersCount(), waterMeters == null ? -1 : waterMeters.Count, DescribeReaders(regReaders));
myRef.modelessDlg = new TestStartEndForm(myRef.waterMeters.Count, myRef.regReaders, disabled);
modelessDlg.Show();
log.DebugFormat("Poseidon data-entry: TestStartEndForm START visible={0}, handleCreated={1}",
modelessDlg.Visible, modelessDlg.IsHandleCreated);
}
///
void OpenTestEndStatesDlg(EntryForm myRef)
{
if (ShowForm == 0)
return;
log.DebugFormat("Poseidon data-entry: creating TestStartEndForm END; configuredWMs={0}, enabledWMs={1}, batchWMs={2}, refVolume={3}, errLow={4}, errHigh={5}, readers=[{6}]",
TBF.Data.WMsCount, EnabledWaterMetersCount(), waterMeters == null ? -1 : waterMeters.Count,
refVolume, errLimLo, errLimHi, DescribeReaders(regReaders));
myRef.modelessDlg = new TestStartEndForm(TBF.Data.WMsCount, myRef.regReaders, wmStartStateStr, disabled, refVolume, errLimLo, errLimHi);
modelessDlg.Show();
log.DebugFormat("Poseidon data-entry: TestStartEndForm END visible={0}, handleCreated={1}",
modelessDlg.Visible, modelessDlg.IsHandleCreated);
}
/// <summary>Start this operation</summary>
@@ -204,6 +287,12 @@ namespace TBF.Rig.DataEntry.PoseidonCmd
readDataOp = ReadDataOp.None;
readAndSetDataToMeters = false;
filedDataToMeters = false;
poseidonPhaseRunner = null;
poseidonPhaseReaders = null;
sendStartPhaseInitialized = false;
sendStartPhaseIterations = 0;
modelessDialogOpening = false;
modelessDialogWaitLogged = false;
if (ShowForm == 0)
return ;
@@ -215,7 +304,7 @@ namespace TBF.Rig.DataEntry.PoseidonCmd
//ReadAndSetDataToMeters();
if (ProcessData.SelectedProcedure.OrderInfo == null)
{
Program.MainWnd.Invoke(new EntryFormDlgt(OpenBeginningDlg), this);
BeginOpenDialog("CycleBeginningForm", OpenBeginningDlg);
}
else
{
@@ -225,10 +314,10 @@ namespace TBF.Rig.DataEntry.PoseidonCmd
}
break;
case CurrentOp.ReadDatastream_StartStates:
Program.MainWnd.Invoke(new EntryFormDlgt(OpenTestStartStatesDlg), this);
BeginOpenDialog("TestStartEndForm.START", OpenTestStartStatesDlg);
break;
case CurrentOp.ReadDatastream_EndStates:
Program.MainWnd.Invoke(new EntryFormDlgt(OpenTestEndStatesDlg), this);
BeginOpenDialog("TestStartEndForm.END", OpenTestEndStatesDlg);
break;
}
}
@@ -240,6 +329,17 @@ namespace TBF.Rig.DataEntry.PoseidonCmd
/// <returns>Event.ResultsPrinted</returns>
public Event Run()
{
if (modelessDialogOpening)
{
if (!modelessDialogWaitLogged)
{
log.WarnFormat("Poseidon data-entry: state machine waiting for dialog open; operation={0}, readers=[{1}]",
currentOp, DescribeReaders(regReaders));
modelessDialogWaitLogged = true;
}
return Event.ModelessFormIsOpen;
}
if (!readAndSetDataToMeters) // run until not finished
readAndSetDataToMeters = ReadAndSetDataToMeters();
@@ -268,10 +368,16 @@ namespace TBF.Rig.DataEntry.PoseidonCmd
{
dlg.WMStartState[item] = poseidonReader.BeginWMState;
dlg.WMStartStateStr[item] = poseidonReader.BeginWMState.ToString();
log.InfoFormat("Poseidon dialog prefill: phase=Start, WM{0}, reader={1}, value={2}",
item + 1, poseidonReader.Name, dlg.WMStartState[item]);
}
if (currentOp == CurrentOp.ReadDatastream_EndStates)
{
dlg.WMEndState[item] = poseidonReader.EndWMState;
log.InfoFormat("Poseidon dialog prefill: phase=End, WM{0}, reader={1}, value={2}",
item + 1, poseidonReader.Name, dlg.WMEndState[item]);
}
}
}
@@ -326,38 +432,44 @@ namespace TBF.Rig.DataEntry.PoseidonCmd
}
}
}
else if (modelessDlg is TestStartEndForm && currentOp == CurrentOp.ReadDatastream_StartStates)
else if (modelessDlg is TestStartEndForm)
{
/// Fixed start test - start
TestStartEndForm dlg = (modelessDlg as TestStartEndForm);
if (dlg != null)
{
for (int i = 0; i < dlg.WaterMetersCount; i++)
{
wmStartState[i] = dlg.WMStartState[i];
wmStartStateStr[i] = dlg.WMStartStateStr[i];
}
}
}
else if (modelessDlg is TestStartEndForm && currentOp == CurrentOp.ReadDatastream_EndStates)
{
/// Fixed start test - end
TestStartEndForm dlg = (modelessDlg as TestStartEndForm);
if (dlg != null)
{
for (int i = 0; i < dlg.WaterMetersCount; i++)
{
wmEndState[i] = dlg.WMEndState[i];
}
}
}
StoreAcceptedDialogValues((TestStartEndForm)modelessDlg);
}
resultSaved = true;
modelessDlg = null;
modelessDlg = null;
modelessDialogOpening = false;
}
return Event.ModelessFormClosed; /// Form closed
}
private void StoreAcceptedDialogValues(TestStartEndForm dlg)
{
if (dlg == null)
return;
if (currentOp == CurrentOp.ReadDatastream_StartStates)
{
for (int i = 0; i < dlg.WaterMetersCount; i++)
{
wmStartState[i] = dlg.WMStartState[i];
wmStartStateStr[i] = dlg.WMStartStateStr[i];
log.InfoFormat("Poseidon dialog accepted: phase=Start, WM{0}, text='{1}', value={2}",
i + 1, wmStartStateStr[i], wmStartState[i]);
}
}
else if (currentOp == CurrentOp.ReadDatastream_EndStates)
{
for (int i = 0; i < dlg.WaterMetersCount; i++)
{
wmEndState[i] = dlg.WMEndState[i];
log.InfoFormat("Poseidon dialog accepted: phase=End, WM{0}, value={1}",
i + 1, wmEndState[i]);
}
}
}
/// <summary>Stop this operation</summary>
public void Stop()
{
@@ -369,6 +481,7 @@ namespace TBF.Rig.DataEntry.PoseidonCmd
currentOp = CurrentOp.None;
readAndSetDataToMeters = false;
filedDataToMeters = false;
sendStartPhaseInitialized = false;
}
public int ShowForm
@@ -389,88 +502,108 @@ namespace TBF.Rig.DataEntry.PoseidonCmd
bool bOperationSuccess = false;
if (currentOp == CurrentOp.SendStartDataStream)
{
bool finishedReading = regReaders == null; // we can work only with register readers
while (!finishedReading) //TODO BUMI lock - fuck ?
if (!sendStartPhaseInitialized)
{
bool bAllReadersFinished = true;
foreach (var iRegReader in regReaders )
sendStartPhaseInitialized = true;
sendStartPhaseIterations = 0;
poseidonPhaseStartedAt = DateTime.UtcNow;
poseidonNextProgressLogAt = poseidonPhaseStartedAt.AddSeconds(5);
log.InfoFormat("Poseidon send-start phase started; readers=[{0}]", DescribeReaders(regReaders));
if (regReaders != null)
{
if (iRegReader is PoseidonReader)
foreach (IRegReader regReader in regReaders)
{
PoseidonReader poseidonReader = (iRegReader as PoseidonReader);
PoseidonReader poseidonReader = regReader as PoseidonReader;
if (poseidonReader == null) continue;
poseidonReader.SetCliLogging(CliLogging);
if (poseidonReader.CurrentOp == PoseidonReader.CurrentPoseidonOp.SendStartDataStream_Done
|| poseidonReader.CurrentOp == PoseidonReader.CurrentPoseidonOp.SendStartDataStream_Runing)
{
poseidonReader.SetCurrentOp(PoseidonReader.CurrentPoseidonOp.SendStartDataStream);
}
/// Send start data stream
poseidonReader.Run();
readDataOp = ReadDataOp.Start;
if (!(poseidonReader.CurrentOp == PoseidonReader.CurrentPoseidonOp.Done
|| poseidonReader.CurrentOp == PoseidonReader.CurrentPoseidonOp.Error))
{
bAllReadersFinished = false;
}
poseidonReader.SetCurrentOp(PoseidonReader.CurrentPoseidonOp.SendStartDataStream);
log.DebugFormat("Poseidon send-start: armed reader={0}, COM={1}, state={2}",
poseidonReader.Name, poseidonReader.ComPortNr, poseidonReader.CurrentOp);
}
}
/// Wait for all readers to finish
if (bAllReadersFinished)
{
finishedReading = true;
readDataOp = ReadDataOp.Done;
}
else
{
System.Threading.Thread.Sleep(10);
}
}
bOperationSuccess = true;
sendStartPhaseIterations++;
bool allReadersFinished = true;
if (regReaders != null)
{
foreach (IRegReader regReader in regReaders)
{
PoseidonReader poseidonReader = regReader as PoseidonReader;
if (poseidonReader == null) continue;
poseidonReader.Run();
readDataOp = ReadDataOp.Start;
if (poseidonReader.CurrentOp != PoseidonReader.CurrentPoseidonOp.Done
&& poseidonReader.CurrentOp != PoseidonReader.CurrentPoseidonOp.Error)
allReadersFinished = false;
}
}
if (allReadersFinished)
{
readDataOp = ReadDataOp.Done;
log.InfoFormat("Poseidon send-start phase completed; iterations={0}, elapsedMs={1}",
sendStartPhaseIterations, (long)(DateTime.UtcNow - poseidonPhaseStartedAt).TotalMilliseconds);
sendStartPhaseInitialized = false;
bOperationSuccess = true;
}
else if (DateTime.UtcNow >= poseidonNextProgressLogAt)
{
log.WarnFormat("Poseidon send-start phase waiting; iterations={0}, elapsedMs={1}, readers=[{2}]",
sendStartPhaseIterations, (long)(DateTime.UtcNow - poseidonPhaseStartedAt).TotalMilliseconds,
DescribeReaders(regReaders));
poseidonNextProgressLogAt = DateTime.UtcNow.AddSeconds(5);
}
}
else if ((currentOp == CurrentOp.ReadDatastream_StartStates)
|| (currentOp == CurrentOp.ReadDatastream_EndStates))
{
bool finishedReading = regReaders == null; // we can work only with register readers
while (!finishedReading) //TODO BUMI lock - fuck ?
if (poseidonPhaseRunner == null)
{
bool bAllReadersFinished = true;
foreach (var iRegReader in regReaders )
poseidonPhaseReaders = new List<IPoseidonReadOperation>();
if (regReaders != null)
{
if (iRegReader is PoseidonReader)
foreach (IRegReader regReader in regReaders)
{
PoseidonReader poseidonReader = (iRegReader as PoseidonReader);
if(poseidonReader == null)
continue;
PoseidonReader poseidonReader = regReader as PoseidonReader;
if (poseidonReader == null) continue;
poseidonReader.SetCliLogging(CliLogging);
if (!(poseidonReader.CurrentOp == PoseidonReader.CurrentPoseidonOp.ReadDatastream_Done
|| poseidonReader.CurrentOp == PoseidonReader.CurrentPoseidonOp.ReadDatastream_Running))
{
poseidonReader.SetCurrentOp((currentOp == CurrentOp.ReadDatastream_StartStates)?
PoseidonReader.CurrentPoseidonOp.ReadDataStream_Start :
PoseidonReader.CurrentPoseidonOp.ReadDataStream_End);
}
/// Send start data stream
poseidonReader.Run();
if (!(poseidonReader.CurrentOp == PoseidonReader.CurrentPoseidonOp.Done
|| poseidonReader.CurrentOp == PoseidonReader.CurrentPoseidonOp.Error))
{
bAllReadersFinished = false;
}
poseidonPhaseReaders.Add(new PoseidonReaderOperation(poseidonReader));
}
}
if (bAllReadersFinished)
{
finishedReading = true;
}
else
{
System.Threading.Thread.Sleep(10);
}
poseidonPhaseRunner = new PoseidonReadPhaseRunner(
currentOp == CurrentOp.ReadDatastream_StartStates);
poseidonPhaseStartedAt = DateTime.UtcNow;
poseidonNextProgressLogAt = poseidonPhaseStartedAt.AddSeconds(5);
log.InfoFormat("Poseidon phase started: phase={0}, readers={1}, detail=[{2}]",
currentOp, poseidonPhaseReaders.Count, DescribeReaders(regReaders));
}
bOperationSuccess = true;
bool allReadersFinished = poseidonPhaseRunner.RunIteration(poseidonPhaseReaders);
foreach (IPoseidonReadOperation poseidonReader in poseidonPhaseReaders)
if (poseidonReader.HasError)
log.ErrorFormat("Poseidon read: {0} completed with Error during {1}", poseidonReader.Name, currentOp);
if (allReadersFinished)
{
log.InfoFormat("Poseidon phase completed: phase={0}, iterations={1}, elapsedMs={2}",
currentOp, poseidonPhaseRunner.IterationCount,
(long)(DateTime.UtcNow - poseidonPhaseStartedAt).TotalMilliseconds);
poseidonPhaseRunner = null;
poseidonPhaseReaders = null;
bOperationSuccess = true;
}
else if (DateTime.UtcNow >= poseidonNextProgressLogAt)
{
var pendingReaders = new List<string>();
foreach (IPoseidonReadOperation poseidonReader in poseidonPhaseReaders)
if (!poseidonReader.IsFinished) pendingReaders.Add(poseidonReader.Name);
log.WarnFormat("Poseidon phase waiting: phase={0}, iterations={1}, elapsedMs={2}, pending={3}",
currentOp, poseidonPhaseRunner.IterationCount,
(long)(DateTime.UtcNow - poseidonPhaseStartedAt).TotalMilliseconds,
string.Join(",", pendingReaders));
poseidonNextProgressLogAt = DateTime.UtcNow.AddSeconds(5);
}
}
return bOperationSuccess;
@@ -147,6 +147,8 @@ namespace TBF.Rig.DataEntry.PoseidonCmd
private void SetUiBusy(bool busy, long deltaTime = -1)
{
log.DebugFormat("Poseidon UI: SetUiBusy busy={0}, deltaTime={1}, enabledTextBoxes={2}",
busy, deltaTime, enabledTextBoxes.Count);
// show wait cursor for form and children
this.UseWaitCursor = busy;
@@ -296,6 +298,8 @@ namespace TBF.Rig.DataEntry.PoseidonCmd
BeginInvoke(new Action(() => UpdateValues(stratValue, enableEdit)));
return;
}
log.DebugFormat("Poseidon UI: UpdateValues on UI thread startValue={0}, enableEdit={1}, deltaTime={2}",
stratValue, enableEdit, deltaTime);
for (int i = 0; i < TextBoxesCount; i++)
{
@@ -320,6 +324,7 @@ namespace TBF.Rig.DataEntry.PoseidonCmd
if (enableEdit)
{
SetUiBusy(false, deltaTime);
log.Debug("Poseidon UI: values applied and dialog released from Loading...");
}
// if you also need to enable/disable editing, do it here,
// it's now safely on the UI thread.
@@ -348,6 +353,8 @@ namespace TBF.Rig.DataEntry.PoseidonCmd
if (endTextBoxes[i].Visible && endTextBoxes[i].Enabled)
{
WMEndState[i] = Utils.ParseUDouble(endTextBoxes[i].Text);
log.InfoFormat("Poseidon dialog OK: phase=End, WM{0}, enteredText='{1}', parsedValue={2}",
i + 1, endTextBoxes[i].Text, WMEndState[i]);
}
}
}
@@ -359,6 +366,8 @@ namespace TBF.Rig.DataEntry.PoseidonCmd
{
WMStartStateStr[i] = startTextBoxes[i].Text;
WMStartState[i] = Utils.ParseUDouble(startTextBoxes[i].Text);
log.InfoFormat("Poseidon dialog OK: phase=Start, WM{0}, enteredText='{1}', parsedValue={2}",
i + 1, startTextBoxes[i].Text, WMStartState[i]);
}
}
}
@@ -4,6 +4,8 @@ using System.Linq;
using log4net;
using Results.Entities;
using TBF.Rig.GenericDevices;
using TBF.Rig.Sequences;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication;
namespace TBF.Rig.DataEntry.Uni
{
@@ -23,21 +25,22 @@ namespace TBF.Rig.DataEntry.Uni
? new IRegReader[0]
: readers.ToArray();
long enabledHeads = Program.LocalSettings.OptoHeadsEnabled;
SmartReaderSelection.EnsureConfiguredReaders(Program.LocalSettings, ProcessData.SmartHeadsUni);
IRegReader[] selected = allSlots
.Where(reader => IsSelectedForDataEntry(reader, waterMeters, enabledHeads))
.Where(reader => IsSelectedForDataEntry(reader, waterMeters, Program.LocalSettings, enabledHeads))
.ToArray();
string selectedPositions = string.Join(",", selected.Select(reader => reader.Position));
string skippedPositions = string.Join(",", allSlots
.Where(reader => reader != null &&
!IsSelectedForDataEntry(reader, waterMeters, enabledHeads))
!IsSelectedForDataEntry(reader, waterMeters, Program.LocalSettings, enabledHeads))
.Select(reader => reader.Position));
log.DebugFormat(
"DATA_ENTRY_READER_FILTER Operation={0}, OptoHeadsEnabled=0x{1:X12}, " +
"DATA_ENTRY_READER_FILTER Operation={0}, LegacyOptoHeadsEnabled=0x{1:X12}, " +
"Slots={2}, Readers={3}, Selected={4}, SelectedPositions=[{5}], " +
"SkippedPositions=[{6}], SelectionRule=SmartMeterMaskOrPrecheckedWaterMeter",
"SkippedPositions=[{6}], SelectionRule=SmartReaderFamilySelectionOrPrecheckedWaterMeter",
operation,
enabledHeads,
allSlots.Length,
@@ -56,8 +59,8 @@ namespace TBF.Rig.DataEntry.Uni
reader.GetType().Name,
reader.Position,
reader.DebugLevel,
IsSelectedForDataEntry(reader, waterMeters, enabledHeads),
GetSelectionDetails(reader, waterMeters, enabledHeads));
IsSelectedForDataEntry(reader, waterMeters, Program.LocalSettings, enabledHeads),
GetSelectionDetails(reader, waterMeters, Program.LocalSettings, enabledHeads));
}
return selected;
@@ -67,6 +70,15 @@ namespace TBF.Rig.DataEntry.Uni
IRegReader reader,
IList<WaterMeter> waterMeters,
long enabledHeads)
{
return IsSelectedForDataEntry(reader, waterMeters, null, enabledHeads);
}
internal static bool IsSelectedForDataEntry(
IRegReader reader,
IList<WaterMeter> waterMeters,
LocalSettings settings,
long enabledHeads)
{
if (reader == null)
return false;
@@ -87,6 +99,9 @@ namespace TBF.Rig.DataEntry.Uni
if (reader is ISmartMeterReader)
{
if (settings != null && settings.SmartReaderSelections != null)
return SmartReaderSelection.IsEnabled(settings, reader);
int position0 = reader.Position - 1;
return position0 >= 0 && position0 < 63 &&
(enabledHeads & (1L << position0)) != 0;
@@ -115,6 +130,7 @@ namespace TBF.Rig.DataEntry.Uni
private static string GetSelectionDetails(
IRegReader reader,
IList<WaterMeter> waterMeters,
LocalSettings settings,
long enabledHeads)
{
#if IPERL
@@ -136,6 +152,15 @@ namespace TBF.Rig.DataEntry.Uni
#endif
if (reader is ISmartMeterReader)
{
if (settings != null && settings.SmartReaderSelections != null)
{
return string.Format(
", SelectionSource=SmartReaderFamily, Family={0}, ReaderKey={1}, Enabled={2}",
SmartReaderSelection.GetFamilyKey(reader),
SmartReaderSelection.GetReaderKey(reader),
SmartReaderSelection.IsEnabled(settings, reader));
}
int position0 = reader.Position - 1;
bool maskBit = position0 >= 0 && position0 < 63 &&
(enabledHeads & (1L << position0)) != 0;
@@ -26,10 +26,8 @@ namespace TBF.Rig.RegisterReaders.AllyReader
private const int DataEntryCommandTimeoutMs = 5000;
private const int DefaultDataEntryOpticalTimeoutMs = 3000;
private const int MaxStoredSamples = 40000;
private const long RawVolumeModulo = 0x1000000L;
private const long RawVolumeModulo = 0x100000000L;
private const long RawVolumeHalfRange = RawVolumeModulo / 2;
private const long RawTimestampModulo = 0x100000000L;
private const long RawTimestampHalfRange = RawTimestampModulo / 2;
private static readonly ILog log = LogManager.GetLogger(typeof(AllyMeterReader));
private readonly object commandSync = new object();
@@ -42,14 +40,13 @@ namespace TBF.Rig.RegisterReaders.AllyReader
private AllyCommandService commandService;
private SerialPort opticalPort;
private bool streamEnabled;
private bool opticalVerificationOutputActive;
private bool operationActive;
private bool hasPreviousRawVolume;
private bool hasPreviousRawTimestamp;
private bool hasTestStartSample;
private uint previousRawVolume;
private uint previousRawTimestamp;
private long extendedRawVolume;
private long extendedRawTimestamp;
private DateTime? firstSampleReceivedAtUtc;
private double beginWMState;
private double endWMState;
private double timestampSecStart;
@@ -94,6 +91,15 @@ namespace TBF.Rig.RegisterReaders.AllyReader
get { return allyCfg == null ? AllyMeterSize.AutoDetect : allyCfg.ConfiguredMeterSize; }
}
public bool IsOpticalVolumeConversionConfigured
{
// The ALLY C6 accumulator is always expressed in quarter millilitres.
// Unlike calibration-factor limits, decoding the optical accumulator does
// not depend on the nominal meter size. Keeping this true also allows a
// bench configured with AutoDetect to persist start/end states.
get { return true; }
}
public IReadOnlyList<AllyOpticalSample> OpticalSamples
{
get
@@ -132,12 +138,25 @@ namespace TBF.Rig.RegisterReaders.AllyReader
}
if (!string.IsNullOrEmpty(text))
{
log.DebugFormat(
"ALLY_OPTO RX COM{0}: bytes={1}, ASCII='{2}', HEX={3}",
allyCfg.OptoComPortNr,
text.Length,
ToLogText(text),
ToHex(text));
ProcessOpticalText(text);
}
}
catch (Exception ex)
{
CommFailed = true;
log.Error("ALLY optical stream read failed.", ex);
log.ErrorFormat(
"ALLY_OPTO read failed on COM{0} (open={1}, streaming={2}). {3}",
allyCfg == null ? 0 : allyCfg.OptoComPortNr,
opticalPort != null && opticalPort.IsOpen,
streamEnabled,
ex);
}
}
@@ -225,30 +244,66 @@ namespace TBF.Rig.RegisterReaders.AllyReader
public void StartDataStreamProcessing()
{
GetOpticalVolumeLitersPerRawUnit();
StartDataStreamProcessing(true);
}
lock (opticalSync)
private void StartDataStreamProcessing(bool requireVolumeConversion)
{
log.InfoFormat(
"ALLY_OPTO start requested: COM{0}, {1} Bd, 8N1, meter size={2}, debug={3}",
allyCfg == null ? 0 : allyCfg.OptoComPortNr,
allyCfg == null ? 0 : allyCfg.OptoBaudRate,
ConfiguredMeterSize,
DebugLevel);
try
{
if (streamEnabled)
return;
if (requireVolumeConversion)
GetOpticalVolumeLitersPerRawUnit();
opticalSamples.Clear();
opticalBuffer.Clear();
lastOpticalLine = string.Empty;
ResetVolumeState();
if (DebugLevel == DebugMode.Normal)
lock (opticalSync)
{
opticalPort = new SerialPort(
"COM" + allyCfg.OptoComPortNr,
allyCfg.OptoBaudRate,
Parity.None,
8,
StopBits.One);
opticalPort.Open();
opticalPort.DiscardInBuffer();
}
if (streamEnabled)
{
log.Debug("ALLY_OPTO start ignored: stream is already active.");
return;
}
streamEnabled = true;
opticalSamples.Clear();
opticalBuffer.Clear();
lastOpticalLine = string.Empty;
ResetVolumeState();
if (DebugLevel == DebugMode.Normal)
{
opticalPort = new SerialPort(
"COM" + allyCfg.OptoComPortNr,
allyCfg.OptoBaudRate,
Parity.None,
8,
StopBits.One);
opticalPort.Open();
opticalPort.DiscardInBuffer();
log.InfoFormat(
"ALLY_OPTO opened {0}: baud={1}, dataBits={2}, parity={3}, stopBits={4}",
opticalPort.PortName,
opticalPort.BaudRate,
opticalPort.DataBits,
opticalPort.Parity,
opticalPort.StopBits);
}
else
{
log.Info("ALLY_OPTO simulation mode: physical optical COM port was not opened.");
}
streamEnabled = true;
}
}
catch (Exception ex)
{
CommFailed = true;
log.Error("ALLY_OPTO start failed.", ex);
throw;
}
}
@@ -258,12 +313,18 @@ namespace TBF.Rig.RegisterReaders.AllyReader
{
streamEnabled = false;
if (opticalPort == null)
{
log.Debug("ALLY_OPTO stopped: no optical COM port was open.");
return;
}
try
{
if (opticalPort.IsOpen)
{
log.InfoFormat("ALLY_OPTO closing {0}.", opticalPort.PortName);
opticalPort.Close();
}
}
finally
{
@@ -363,6 +424,46 @@ namespace TBF.Rig.RegisterReaders.AllyReader
ExecuteCommand(service => service.SetDiagnosticLed(mode, timeoutMs));
}
public bool IsFactorySealed(int timeoutMs)
{
if (DebugLevel != DebugMode.Normal)
return false;
return ExecuteCommand(service => service.IsFactorySealed(timeoutMs));
}
public void UnsealFactory(int timeoutMs)
{
ExecuteCommand(service => service.UnsealFactory(timeoutMs));
}
public AllyFactoryUnsealData ReadFactoryUnsealData(int timeoutMs)
{
if (DebugLevel != DebugMode.Normal)
{
// Keep the complete test-method workflow executable without a
// physical meter. The simulated register is already unsealed.
return new AllyFactoryUnsealData(
false,
"SIMULATED-ALLY",
"SIMULATED",
"00000000",
0U);
}
return ExecuteCommand(service => service.ReadFactoryUnsealData(timeoutMs));
}
public void UnsealFactory(AllyFactoryUnsealData data, int timeoutMs)
{
ExecuteCommand(service => service.UnsealFactory(data, timeoutMs));
}
public void SealFactory(int timeoutMs)
{
ExecuteCommand(service => service.SealFactory(timeoutMs));
}
public double ResetCalibrationFactor(int timeoutMs)
{
double factor = GetResetCalibrationFactorPercent();
@@ -615,6 +716,18 @@ namespace TBF.Rig.RegisterReaders.AllyReader
}
private void ProcessOpticalText(string text)
{
ProcessOpticalText(text, DateTime.UtcNow);
}
// Kept internal for deterministic MSTest coverage of the host-receipt time
// used by ALLY C6 telegrams. C6 has no device timestamp to roll over.
internal void ProcessOpticalTextForTest(string text, DateTime receivedAtUtc)
{
ProcessOpticalText(text, receivedAtUtc);
}
private void ProcessOpticalText(string text, DateTime receivedAtUtc)
{
lock (opticalSync)
{
@@ -632,32 +745,174 @@ namespace TBF.Rig.RegisterReaders.AllyReader
lastOpticalLine = line;
AllyOpticalSample sample;
if (!AllyOpticalSample.TryParse(line, DateTime.UtcNow, out sample))
if (!AllyOpticalSample.TryParse(line, receivedAtUtc, out sample))
{
if (AllyOpticalSample.IsMetrologyPacket(line))
{
log.WarnFormat(
"ALLY_OPTO rejected C6 metrology telegram on COM{0}: bytes={1}, ASCII='{2}', HEX={3}",
allyCfg.OptoComPortNr,
line.Length,
ToLogText(line),
ToHex(line));
}
else
{
log.DebugFormat(
"ALLY_OPTO ignored non-metrology telegram on COM{0}: bytes={1}, ASCII='{2}', HEX={3}",
allyCfg.OptoComPortNr,
line.Length,
ToLogText(line),
ToHex(line));
}
continue;
}
ExtendRawVolume(sample.RawVolume);
ExtendRawTimestamp(sample.RawTimestamp);
sample.ExtendedVolumeLiters =
extendedRawVolume * GetOpticalVolumeLitersPerRawUnit();
sample.ElapsedSeconds = extendedRawTimestamp / 8192D;
if (!firstSampleReceivedAtUtc.HasValue)
firstSampleReceivedAtUtc = sample.ReceivedAtUtc;
sample.ElapsedSeconds = (sample.ReceivedAtUtc - firstSampleReceivedAtUtc.Value).TotalSeconds;
if (opticalSamples.Count == MaxStoredSamples)
opticalSamples.RemoveAt(0);
opticalSamples.Add(sample);
endWMState = sample.ExtendedVolumeLiters;
if (IsOpticalVolumeConversionConfigured)
{
sample.ExtendedVolumeLiters =
extendedRawVolume * GetOpticalVolumeLitersPerRawUnit();
endWMState = sample.ExtendedVolumeLiters;
}
else
{
sample.ExtendedVolumeLiters = Double.NaN;
}
timestampSecEnd = sample.ElapsedSeconds;
if (operationActive && !hasTestStartSample)
if (operationActive && !hasTestStartSample && IsOpticalVolumeConversionConfigured)
{
hasTestStartSample = true;
beginWMState = sample.ExtendedVolumeLiters;
timestampSecStart = sample.ElapsedSeconds;
}
log.DebugFormat(
"ALLY_OPTO parsed COM{0}: sequence=0x{1:X2}, rawVolume=0x{2:X8}, flow={3}, volume={4:F6} l, elapsed={5:F3} s, emptyPipe={6}, fastHptc={7}",
allyCfg.OptoComPortNr,
sample.Sequence,
sample.RawVolume,
sample.RawFlow,
sample.ExtendedVolumeLiters,
sample.ElapsedSeconds,
sample.IsEmptyPipe,
sample.IsFastHptc);
}
}
}
/// <summary>
/// Enables the meter optical output and then starts COM optical capture.
/// C6 volume is decoded in quarter millilitres and is independent of the
/// configured nominal meter size.
/// </summary>
public void StartOpticalVerificationStream(int timeoutMs)
{
if (IsFactorySealed(timeoutMs))
{
throw new InvalidOperationException(
"ALLY meter is factory sealed. Unseal the meter before starting the optical stream.");
}
try
{
ExecuteCommand(service => service.StartOpticalVerificationOutput(timeoutMs));
opticalVerificationOutputActive = true;
StartDataStreamProcessing(IsOpticalVolumeConversionConfigured);
}
catch
{
if (opticalVerificationOutputActive)
{
try
{
StopOpticalVerificationStream(timeoutMs);
}
catch (Exception cleanupException)
{
log.Error("ALLY optical start cleanup failed.", cleanupException);
}
}
throw;
}
}
/// <summary>
/// Production-bench setup operation. It reads the meter-specific factory
/// data, opens the factory seal only when needed, and then enables the
/// complete optical verification stream.
/// </summary>
public void UnsealAndStartOpticalVerificationStream(int timeoutMs)
{
AllyFactoryUnsealData unsealData = ReadFactoryUnsealData(timeoutMs);
log.InfoFormat("ALLY_OPTO_SETUP_SEAL_STATE: sealed={0}, factoryId='{1}', programmableText='{2}'",
unsealData.IsSealed,
unsealData.FactoryId,
unsealData.ProgrammableText);
if (unsealData.IsSealed)
{
UnsealFactory(unsealData, timeoutMs);
if (IsFactorySealed(timeoutMs))
throw new InvalidOperationException("ALLY factory seal remained active after the unseal command.");
log.Info("ALLY_OPTO_SETUP_UNSEALED: factory seal was removed for optical verification.");
}
else
{
log.Info("ALLY_OPTO_SETUP_UNSEAL_SKIPPED: meter is already unsealed.");
}
StartOpticalVerificationStream(timeoutMs);
log.Info("ALLY_OPTO_SETUP_STARTED: optical verification stream is active.");
}
/// <summary>
/// Stops COM optical capture and restores LED, meter mode and spread
/// spectrum even when the dialog is closed unexpectedly.
/// </summary>
public void StopOpticalVerificationStream(int timeoutMs)
{
try
{
lock (opticalSync)
{
operationActive = false;
}
if (opticalVerificationOutputActive)
ExecuteCommand(service => service.StopOpticalVerificationOutput(timeoutMs));
}
finally
{
opticalVerificationOutputActive = false;
StopDataStreamProcessing();
}
}
private static string ToHex(string text)
{
return BitConverter.ToString(Encoding.ASCII.GetBytes(text ?? string.Empty)).Replace("-", " ");
}
private static string ToLogText(string text)
{
return (text ?? string.Empty)
.Replace("\r", "\\r")
.Replace("\n", "\\n")
.Replace("\t", "\\t");
}
private void ExtendRawVolume(uint rawVolume)
{
if (!hasPreviousRawVolume)
@@ -678,51 +933,19 @@ namespace TBF.Rig.RegisterReaders.AllyReader
previousRawVolume = rawVolume;
}
private void ExtendRawTimestamp(uint rawTimestamp)
{
if (!hasPreviousRawTimestamp)
{
hasPreviousRawTimestamp = true;
previousRawTimestamp = rawTimestamp;
extendedRawTimestamp = rawTimestamp;
return;
}
long delta = (long)rawTimestamp - previousRawTimestamp;
if (delta < -RawTimestampHalfRange)
delta += RawTimestampModulo;
else if (delta > RawTimestampHalfRange)
delta -= RawTimestampModulo;
extendedRawTimestamp += delta;
previousRawTimestamp = rawTimestamp;
}
private double GetOpticalVolumeLitersPerRawUnit()
{
// The optical format scales volume by flow-tube size: raw / 16000
// for 5/8", 2 * raw / 16000 for 3/4", and 4 * raw / 16000 for 1".
switch (ConfiguredMeterSize)
{
case AllyMeterSize.FiveEighths: return 1D / 16000D;
case AllyMeterSize.ThreeQuarterShort:
case AllyMeterSize.ThreeQuarterLong: return 2D / 16000D;
case AllyMeterSize.OneInch: return 4D / 16000D;
default:
throw new InvalidOperationException(
"ALLY meter size is AutoDetect. UI-2031 serial-number parsing is required before decoding optical volume.");
}
// C6 accumulator is expressed in quarter millilitres, independent of tube size.
return 1D / 4000D;
}
private void ResetVolumeState()
{
hasPreviousRawVolume = false;
hasPreviousRawTimestamp = false;
hasTestStartSample = false;
previousRawVolume = 0;
previousRawTimestamp = 0;
extendedRawVolume = 0;
extendedRawTimestamp = 0;
firstSampleReceivedAtUtc = null;
beginWMState = 0;
endWMState = 0;
timestampSecStart = 0;
@@ -4,19 +4,51 @@ using System.Globalization;
namespace TBF.Rig.RegisterReaders.AllyReader
{
/// <summary>
/// Validated common portion of the 42-byte optical telegram used by the
/// register-reader pattern referenced by UI-2093. ALLY calibration-only
/// fields are intentionally not inferred without UI-1204/UI-1236.
/// ALLY optical metrology sample. ALLY emits a tab-separated envelope:
/// sequence, message type, Base64 binary payload and a four-hex checksum.
/// Type C6 contains the 24-byte C2 water-metrology layout.
/// </summary>
public sealed class AllyOpticalSample
{
private const int TelegramLength = 42;
private const byte MetrologyPacketType = 0xC6;
private const int MetrologyPayloadLength = 24;
public string RawLine { get; private set; }
public DateTime ReceivedAtUtc { get; private set; }
public byte Sequence { get; private set; }
public byte PacketType { get; private set; }
public ushort PacketChecksum { get; private set; }
public int RawAdc { get; private set; }
public short LastField { get; private set; }
public short RawFlow { get; private set; }
public uint RawVolume { get; private set; }
public uint RawTimestamp { get; private set; }
public ushort FlipPeriod { get; private set; }
public ushort VinfStart { get; private set; }
public ushort VinfEnd { get; private set; }
public short ElectrodeDelta { get; private set; }
public ushort Impedance { get; private set; }
public byte FieldDriveTime { get; private set; }
public byte Flags { get; private set; }
public byte[] ExtensionBytes { get; private set; }
// C6 has no legacy 8192 Hz meter timestamp. Time is based on receipt.
public uint RawTimestamp { get { return 0; } }
public bool IsLowFlow { get { return (Flags & 0x01) != 0; } }
public bool IsEmptyPipe { get { return (Flags & 0x02) != 0; } }
public bool IsFastHptc { get { return (Flags & 0x04) != 0; } }
public bool FieldPolarity { get { return (Flags & 0x08) != 0; } }
public bool ImpedancePolarity { get { return (Flags & 0x10) != 0; } }
/// <summary>
/// Flow decoded from the C6 payload. The payload stores quarter millilitres per second.
/// </summary>
public double FlowMillilitersPerSecond { get { return RawFlow / 4D; } }
/// <summary>
/// Accumulated volume decoded from the C6 payload. The payload stores quarter millilitres.
/// </summary>
public double VolumeLiters { get { return RawVolume / 4000D; } }
public double ExtendedVolumeLiters { get; internal set; }
public double ElapsedSeconds { get; internal set; }
@@ -33,49 +65,77 @@ namespace TBF.Rig.RegisterReaders.AllyReader
if (string.IsNullOrWhiteSpace(line))
return false;
if (line.Length < TelegramLength)
string telegram = line.TrimEnd('\r', '\n');
string[] fields = telegram.Split('\t');
if (fields.Length != 4)
return false;
string telegram = line.Substring(line.Length - TelegramLength, TelegramLength);
if (telegram[6] != '\t' || telegram[11] != '\t' || telegram[16] != '\t' ||
telegram[23] != '\t' || telegram[28] != '\t' || telegram[37] != '\t' ||
telegram[40] != '\r' || telegram[41] != '\n')
byte sequence;
byte packetType;
ushort checksum;
if (!byte.TryParse(fields[0], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out sequence) ||
!byte.TryParse(fields[1], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out packetType) ||
!ushort.TryParse(fields[3], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out checksum) ||
packetType != MetrologyPacketType)
return false;
byte[] payload;
try
{
payload = Convert.FromBase64String(fields[2]);
}
catch (FormatException)
{
return false;
}
ushort rawFlowUnsigned;
uint rawVolume;
uint rawTimestamp;
byte checksum;
if (!ushort.TryParse(telegram.Substring(12, 4), NumberStyles.HexNumber,
CultureInfo.InvariantCulture, out rawFlowUnsigned) ||
!uint.TryParse(telegram.Substring(17, 6), NumberStyles.HexNumber,
CultureInfo.InvariantCulture, out rawVolume) ||
!uint.TryParse(telegram.Substring(29, 8), NumberStyles.HexNumber,
CultureInfo.InvariantCulture, out rawTimestamp) ||
!byte.TryParse(telegram.Substring(38, 2), NumberStyles.HexNumber,
CultureInfo.InvariantCulture, out checksum))
{
return false;
}
byte calculatedChecksum = 0;
for (int i = 0; i < TelegramLength - 4; i++)
calculatedChecksum += (byte)telegram[i];
if (calculatedChecksum != checksum || rawVolume > 0xFFFFFF)
if (payload.Length < MetrologyPayloadLength)
return false;
sample = new AllyOpticalSample
{
RawLine = telegram,
RawLine = line,
ReceivedAtUtc = receivedAtUtc,
RawFlow = unchecked((short)rawFlowUnsigned),
RawVolume = rawVolume,
RawTimestamp = rawTimestamp
Sequence = sequence,
PacketType = packetType,
PacketChecksum = checksum,
RawAdc = BitConverter.ToInt32(payload, 0),
LastField = BitConverter.ToInt16(payload, 4),
RawFlow = BitConverter.ToInt16(payload, 6),
RawVolume = BitConverter.ToUInt32(payload, 8),
FlipPeriod = BitConverter.ToUInt16(payload, 12),
VinfStart = BitConverter.ToUInt16(payload, 14),
VinfEnd = BitConverter.ToUInt16(payload, 16),
ElectrodeDelta = BitConverter.ToInt16(payload, 18),
Impedance = BitConverter.ToUInt16(payload, 20),
FieldDriveTime = payload[22],
Flags = payload[23],
ExtensionBytes = CopyExtension(payload)
};
return true;
}
public static bool IsMetrologyPacket(string line)
{
if (string.IsNullOrWhiteSpace(line))
return false;
string[] fields = line.TrimEnd('\r', '\n').Split('\t');
byte packetType;
return fields.Length == 4 &&
byte.TryParse(fields[1], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out packetType) &&
packetType == MetrologyPacketType;
}
private static byte[] CopyExtension(byte[] payload)
{
int length = payload.Length - MetrologyPayloadLength;
if (length <= 0)
return new byte[0];
byte[] extension = new byte[length];
Buffer.BlockCopy(payload, MetrologyPayloadLength, extension, 0, length);
return extension;
}
}
}
@@ -37,7 +37,7 @@ namespace TBF.Rig.RegisterReaders.AllyReader
public IComponentCfgCtrl GetControl(IList<Config.Entities.Component> cmpntEntities)
{
return new Configs.ParamsProvider.ComponentCfgCtrl(this, null);
return new AllyReaderCfgCtrl();
}
public string ComponentName { get { return Name; } }
@@ -0,0 +1,51 @@
namespace TBF.Rig.RegisterReaders.AllyReader
{
partial class AllyReaderCfgCtrl
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing) { if (disposing && components != null) components.Dispose(); base.Dispose(disposing); }
private void InitializeComponent()
{
tabControl1 = new System.Windows.Forms.TabControl(); tabPage1 = new System.Windows.Forms.TabPage(); tabPage2 = new System.Windows.Forms.TabPage();
classNameLabel = new System.Windows.Forms.Label(); nameLabel = new System.Windows.Forms.Label(); nameTextBox = new System.Windows.Forms.TextBox();
muxBoardNrLabel = new System.Windows.Forms.Label(); muxBoardNrTextBox = new System.Windows.Forms.TextBox(); label3 = new System.Windows.Forms.Label();
groupLabel = new System.Windows.Forms.Label(); groupTextBox = new System.Windows.Forms.TextBox(); label4 = new System.Windows.Forms.Label();
optoDataGroupBox = new System.Windows.Forms.GroupBox(); radioButton1 = new System.Windows.Forms.RadioButton(); radioButton2 = new System.Windows.Forms.RadioButton();
ipAddressLabel = new System.Windows.Forms.Label(); ipAddressTextBox = new System.Windows.Forms.TextBox(); tcpipPortLabel = new System.Windows.Forms.Label(); tcpipPortTextBox = new System.Windows.Forms.TextBox(); optoSerialPortLabel = new System.Windows.Forms.Label(); optoSerialPortTextBox = new System.Windows.Forms.TextBox();
groupBox1 = new System.Windows.Forms.GroupBox(); label1 = new System.Windows.Forms.Label(); comboBoxCommunicationInterface = new System.Windows.Forms.ComboBox(); rfidSerialPortNrLabel = new System.Windows.Forms.Label(); rfidPortNrTextBox = new System.Windows.Forms.TextBox();
groupBox2 = new System.Windows.Forms.GroupBox(); label2 = new System.Windows.Forms.Label(); headPortNrTextBox = new System.Windows.Forms.TextBox();
tabControl1.SuspendLayout(); tabPage1.SuspendLayout(); optoDataGroupBox.SuspendLayout(); groupBox1.SuspendLayout(); groupBox2.SuspendLayout(); SuspendLayout();
tabControl1.Controls.Add(tabPage1); tabControl1.Controls.Add(tabPage2); tabControl1.Location = new System.Drawing.Point(3,3); tabControl1.Name="tabControl1"; tabControl1.SelectedIndex=0; tabControl1.Size=new System.Drawing.Size(611,432);
tabPage1.Controls.Add(groupBox2); tabPage1.Controls.Add(label4); tabPage1.Controls.Add(label3); tabPage1.Controls.Add(groupBox1); tabPage1.Controls.Add(optoDataGroupBox); tabPage1.Controls.Add(groupTextBox); tabPage1.Controls.Add(groupLabel); tabPage1.Controls.Add(muxBoardNrTextBox); tabPage1.Controls.Add(muxBoardNrLabel); tabPage1.Controls.Add(nameTextBox); tabPage1.Controls.Add(nameLabel); tabPage1.Controls.Add(classNameLabel); tabPage1.Location=new System.Drawing.Point(4,25); tabPage1.Name="tabPage1"; tabPage1.Padding=new System.Windows.Forms.Padding(3); tabPage1.Size=new System.Drawing.Size(603,403); tabPage1.Text="Config"; tabPage1.UseVisualStyleBackColor=true;
classNameLabel.AutoSize=true; classNameLabel.Location=new System.Drawing.Point(149,11); classNameLabel.Name="classNameLabel"; classNameLabel.Text="ClassName";
nameLabel.AutoSize=true; nameLabel.Location=new System.Drawing.Point(6,44); nameLabel.Name="nameLabel"; nameLabel.Text="Name";
nameTextBox.Enabled=false; nameTextBox.Location=new System.Drawing.Point(153,40); nameTextBox.Name="nameTextBox"; nameTextBox.Size=new System.Drawing.Size(160,22);
muxBoardNrLabel.AutoSize=true; muxBoardNrLabel.Location=new System.Drawing.Point(6,72); muxBoardNrLabel.Name="muxBoardNrLabel"; muxBoardNrLabel.Text="Group 1 (mux. board)";
muxBoardNrTextBox.Enabled=false; muxBoardNrTextBox.Location=new System.Drawing.Point(153,69); muxBoardNrTextBox.Name="muxBoardNrTextBox"; muxBoardNrTextBox.Size=new System.Drawing.Size(44,22);
label3.AutoSize=true; label3.Location=new System.Drawing.Point(208,72); label3.Name="label3"; label3.Text="1 .. 4";
groupLabel.AutoSize=true; groupLabel.Location=new System.Drawing.Point(6,101); groupLabel.Name="groupLabel"; groupLabel.Text="Group 2";
groupTextBox.Enabled=false; groupTextBox.Location=new System.Drawing.Point(153,97); groupTextBox.Name="groupTextBox"; groupTextBox.Size=new System.Drawing.Size(44,22);
label4.AutoSize=true; label4.Location=new System.Drawing.Point(208,101); label4.Name="label4"; label4.Text="1 .. 10";
optoDataGroupBox.Controls.Add(tcpipPortLabel); optoDataGroupBox.Controls.Add(tcpipPortTextBox); optoDataGroupBox.Controls.Add(ipAddressLabel); optoDataGroupBox.Controls.Add(ipAddressTextBox); optoDataGroupBox.Controls.Add(radioButton1); optoDataGroupBox.Controls.Add(radioButton2); optoDataGroupBox.Controls.Add(optoSerialPortLabel); optoDataGroupBox.Controls.Add(optoSerialPortTextBox); optoDataGroupBox.Location=new System.Drawing.Point(10,131); optoDataGroupBox.Name="optoDataGroupBox"; optoDataGroupBox.Size=new System.Drawing.Size(552,119); optoDataGroupBox.Text="Opto-data";
radioButton1.AutoSize=true; radioButton1.Checked=true; radioButton1.Enabled=false; radioButton1.Location=new System.Drawing.Point(29,23); radioButton1.Name="radioButton1"; radioButton1.Text="Use TCP/IP";
radioButton2.AutoSize=true; radioButton2.Enabled=false; radioButton2.Location=new System.Drawing.Point(312,23); radioButton2.Name="radioButton2"; radioButton2.Text="Use serial port";
ipAddressLabel.AutoSize=true; ipAddressLabel.Location=new System.Drawing.Point(41,59); ipAddressLabel.Name="ipAddressLabel"; ipAddressLabel.Text="IP address. :";
ipAddressTextBox.Enabled=false; ipAddressTextBox.Location=new System.Drawing.Point(143,55); ipAddressTextBox.Name="ipAddressTextBox"; ipAddressTextBox.Size=new System.Drawing.Size(129,22);
tcpipPortLabel.AutoSize=true; tcpipPortLabel.Location=new System.Drawing.Point(41,87); tcpipPortLabel.Name="tcpipPortLabel"; tcpipPortLabel.Text="Port nr.:";
tcpipPortTextBox.Enabled=false; tcpipPortTextBox.Location=new System.Drawing.Point(143,84); tcpipPortTextBox.Name="tcpipPortTextBox"; tcpipPortTextBox.Size=new System.Drawing.Size(51,22);
optoSerialPortLabel.AutoSize=true; optoSerialPortLabel.Location=new System.Drawing.Point(321,55); optoSerialPortLabel.Name="optoSerialPortLabel"; optoSerialPortLabel.Text="Serial port nr.:";
optoSerialPortTextBox.Enabled=false; optoSerialPortTextBox.Location=new System.Drawing.Point(439,52); optoSerialPortTextBox.Name="optoSerialPortTextBox"; optoSerialPortTextBox.Size=new System.Drawing.Size(44,22);
groupBox1.Controls.Add(comboBoxCommunicationInterface); groupBox1.Controls.Add(label1); groupBox1.Controls.Add(rfidPortNrTextBox); groupBox1.Controls.Add(rfidSerialPortNrLabel); groupBox1.Location=new System.Drawing.Point(10,259); groupBox1.Name="groupBox1"; groupBox1.Size=new System.Drawing.Size(552,68); groupBox1.Text="RFID / NFC communication (in case mux. board is not used)";
label1.AutoSize=true; label1.Location=new System.Drawing.Point(41,30); label1.Name="label1"; label1.Text="Communication Interface";
comboBoxCommunicationInterface.Enabled=false; comboBoxCommunicationInterface.FormattingEnabled=true; comboBoxCommunicationInterface.Location=new System.Drawing.Point(201,27); comboBoxCommunicationInterface.Name="comboBoxCommunicationInterface"; comboBoxCommunicationInterface.Size=new System.Drawing.Size(71,24);
rfidSerialPortNrLabel.AutoSize=true; rfidSerialPortNrLabel.Location=new System.Drawing.Point(321,30); rfidSerialPortNrLabel.Name="rfidSerialPortNrLabel"; rfidSerialPortNrLabel.Text="Serial port nr.:";
rfidPortNrTextBox.Enabled=false; rfidPortNrTextBox.Location=new System.Drawing.Point(439,26); rfidPortNrTextBox.Name="rfidPortNrTextBox"; rfidPortNrTextBox.Size=new System.Drawing.Size(44,22);
groupBox2.Controls.Add(headPortNrTextBox); groupBox2.Controls.Add(label2); groupBox2.Location=new System.Drawing.Point(10,335); groupBox2.Name="groupBox2"; groupBox2.Size=new System.Drawing.Size(552,50); groupBox2.Text="Head Communication";
label2.AutoSize=true; label2.Location=new System.Drawing.Point(321,18); label2.Name="label2"; label2.Text="Serial port nr.:";
headPortNrTextBox.Enabled=false; headPortNrTextBox.Location=new System.Drawing.Point(439,15); headPortNrTextBox.Name="headPortNrTextBox"; headPortNrTextBox.Size=new System.Drawing.Size(44,22);
tabPage2.Location=new System.Drawing.Point(4,25); tabPage2.Name="tabPage2"; tabPage2.Padding=new System.Windows.Forms.Padding(3); tabPage2.Size=new System.Drawing.Size(603,403); tabPage2.Text="Test"; tabPage2.UseVisualStyleBackColor=true;
AutoScaleDimensions=new System.Drawing.SizeF(8F,16F); AutoScaleMode=System.Windows.Forms.AutoScaleMode.Font; Controls.Add(tabControl1); Margin=new System.Windows.Forms.Padding(4); Name="AllyReaderCfgCtrl"; Size=new System.Drawing.Size(617,438); tabControl1.ResumeLayout(false); tabPage1.ResumeLayout(false); tabPage1.PerformLayout(); optoDataGroupBox.ResumeLayout(false); optoDataGroupBox.PerformLayout(); groupBox1.ResumeLayout(false); groupBox1.PerformLayout(); groupBox2.ResumeLayout(false); groupBox2.PerformLayout(); ResumeLayout(false);
}
private System.Windows.Forms.TabControl tabControl1; private System.Windows.Forms.TabPage tabPage1; private System.Windows.Forms.TabPage tabPage2; private System.Windows.Forms.Label classNameLabel,nameLabel,muxBoardNrLabel,groupLabel,label1,label2,label3,label4,ipAddressLabel,tcpipPortLabel,optoSerialPortLabel,rfidSerialPortNrLabel; private System.Windows.Forms.TextBox nameTextBox,muxBoardNrTextBox,groupTextBox,ipAddressTextBox,tcpipPortTextBox,optoSerialPortTextBox,rfidPortNrTextBox,headPortNrTextBox; private System.Windows.Forms.GroupBox optoDataGroupBox,groupBox1,groupBox2; private System.Windows.Forms.RadioButton radioButton1,radioButton2; private System.Windows.Forms.ComboBox comboBoxCommunicationInterface;
}
}
@@ -0,0 +1,74 @@
using System;
using System.Globalization;
using System.Windows.Forms;
using Common;
using TBF.Rig.Generic;
namespace TBF.Rig.RegisterReaders.AllyReader
{
public partial class AllyReaderCfgCtrl : UserControl, IComponentCfgCtrl
{
private AllyReaderCfg config;
private readonly AllyReaderManualTestCtrl manualTestControl;
public AllyReaderCfgCtrl()
{
InitializeComponent();
manualTestControl = new AllyReaderManualTestCtrl { Dock = DockStyle.Fill };
tabPage2.Controls.Add(manualTestControl);
tabPage1.Text = "Settings";
tabPage2.Text = "Manual test";
groupBox1.Text = "Touch-Read communication";
groupBox2.Text = "Touch-Read settings";
muxBoardNrLabel.Text = "Configured meter size";
groupLabel.Text = "Meter pulses/liter";
label3.Text = label4.Text = string.Empty;
ipAddressLabel.Text = "TCP/IP";
ipAddressTextBox.Text = "Not supported by ALLY";
tcpipPortLabel.Text = "Opto baud rate:";
label2.Text = "Baud rate:";
comboBoxCommunicationInterface.Items.Clear();
comboBoxCommunicationInterface.Items.Add("Touch-Read");
comboBoxCommunicationInterface.SelectedIndex = 0;
radioButton1.Checked = false;
radioButton2.Checked = true;
}
public IComponentCfg Config { get { return config; } set { config = value as AllyReaderCfg; Redraw(); manualTestControl.Config = config; } }
public bool ShowMore { get { return false; } }
public void Unlock() { nameTextBox.Enabled = muxBoardNrTextBox.Enabled = groupTextBox.Enabled = optoSerialPortTextBox.Enabled = tcpipPortTextBox.Enabled = rfidPortNrTextBox.Enabled = headPortNrTextBox.Enabled = true; }
public CfgUpdateFlags VerifyCfg(ref string message)
{
int n; double pulses; AllyMeterSize size;
if (config == null || string.IsNullOrWhiteSpace(nameTextBox.Text)) return Invalid(ref message, "Name");
if (!Enum.TryParse(muxBoardNrTextBox.Text, out size)) return Invalid(ref message, "Configured meter size");
if (!double.TryParse(groupTextBox.Text, NumberStyles.Float, CultureInfo.InvariantCulture, out pulses) || pulses <= 0) return Invalid(ref message, "Meter pulses/liter");
if (!int.TryParse(optoSerialPortTextBox.Text, out n) || n <= 0) return Invalid(ref message, "Optical serial port nr.");
if (!int.TryParse(tcpipPortTextBox.Text, out n) || n <= 0) return Invalid(ref message, "Optical baud rate");
if (!int.TryParse(rfidPortNrTextBox.Text, out n) || n <= 0) return Invalid(ref message, "Touch-Read serial port nr.");
if (!int.TryParse(headPortNrTextBox.Text, out n) || n <= 0) return Invalid(ref message, "Touch-Read baud rate");
return CfgUpdateFlags.None;
}
public CfgUpdateFlags UpdateCfg()
{
if (config == null) return CfgUpdateFlags.Error;
config.Name = nameTextBox.Text;
config.ConfiguredMeterSize = (AllyMeterSize)Enum.Parse(typeof(AllyMeterSize), muxBoardNrTextBox.Text);
config.MeterPulsesPerLiter = double.Parse(groupTextBox.Text, CultureInfo.InvariantCulture);
config.OptoComPortNr = int.Parse(optoSerialPortTextBox.Text); config.OptoBaudRate = int.Parse(tcpipPortTextBox.Text);
config.CommandComPortNr = int.Parse(rfidPortNrTextBox.Text); config.CommandBaudRate = int.Parse(headPortNrTextBox.Text);
return CfgUpdateFlags.RestartRqrd;
}
public void Closing() { manualTestControl.StopOpticalStream(); }
private void Redraw()
{
if (config == null) return;
classNameLabel.Text = config.Factory.ClassName; nameTextBox.Text = config.Name;
muxBoardNrTextBox.Text = config.ConfiguredMeterSize.ToString(); groupTextBox.Text = config.MeterPulsesPerLiter.ToString(CultureInfo.InvariantCulture);
optoSerialPortTextBox.Text = config.OptoComPortNr.ToString(); tcpipPortTextBox.Text = config.OptoBaudRate.ToString();
rfidPortNrTextBox.Text = config.CommandComPortNr.ToString(); headPortNrTextBox.Text = config.CommandBaudRate.ToString();
}
private static CfgUpdateFlags Invalid(ref string message, string text) { message += Environment.NewLine + "'" + text + "' is not valid"; return CfgUpdateFlags.Error; }
}
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<resheader name="resmimetype"><value>text/microsoft-resx</value></resheader>
<resheader name="version"><value>2.0</value></resheader>
<resheader name="reader"><value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></resheader>
<resheader name="writer"><value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></resheader>
</root>
@@ -0,0 +1,22 @@
namespace TBF.Rig.RegisterReaders.AllyReader
{
partial class AllyReaderManualTestCtrl
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing) { if(disposing) { StopOpticalStream(); if(components != null) components.Dispose(); } base.Dispose(disposing); }
private void InitializeComponent()
{
optoTestGroupBox=new System.Windows.Forms.GroupBox(); optoListBox=new System.Windows.Forms.ListBox(); RfidTestGroupBox=new System.Windows.Forms.GroupBox(); rfidOutputListBox=new System.Windows.Forms.ListBox(); label2=new System.Windows.Forms.Label(); rfidCommandComboBox=new System.Windows.Forms.ComboBox(); commandTestButton=new System.Windows.Forms.Button();
optoTestGroupBox.SuspendLayout(); RfidTestGroupBox.SuspendLayout(); SuspendLayout();
optoTestGroupBox.Controls.Add(optoListBox); optoTestGroupBox.Location=new System.Drawing.Point(5,4); optoTestGroupBox.Name="optoTestGroupBox"; optoTestGroupBox.Size=new System.Drawing.Size(591,161); optoTestGroupBox.Text="Opto-data";
optoListBox.FormattingEnabled=true; optoListBox.ItemHeight=16; optoListBox.Location=new System.Drawing.Point(7,22); optoListBox.Name="optoListBox"; optoListBox.Size=new System.Drawing.Size(573,132);
RfidTestGroupBox.Controls.Add(rfidOutputListBox); RfidTestGroupBox.Controls.Add(label2); RfidTestGroupBox.Controls.Add(rfidCommandComboBox); RfidTestGroupBox.Controls.Add(commandTestButton); RfidTestGroupBox.Location=new System.Drawing.Point(5,171); RfidTestGroupBox.Name="RfidTestGroupBox"; RfidTestGroupBox.Size=new System.Drawing.Size(591,224); RfidTestGroupBox.Text="RFID / NFC data";
rfidOutputListBox.FormattingEnabled=true; rfidOutputListBox.ItemHeight=16; rfidOutputListBox.Location=new System.Drawing.Point(5,54); rfidOutputListBox.Name="rfidOutputListBox"; rfidOutputListBox.SelectionMode=System.Windows.Forms.SelectionMode.None; rfidOutputListBox.Size=new System.Drawing.Size(575,164);
label2.AutoSize=true; label2.Location=new System.Drawing.Point(2,25); label2.Name="label2"; label2.Text="Command";
rfidCommandComboBox.FormattingEnabled=true; rfidCommandComboBox.Location=new System.Drawing.Point(86,19); rfidCommandComboBox.Name="rfidCommandComboBox"; rfidCommandComboBox.Size=new System.Drawing.Size(341,24);
commandTestButton.Location=new System.Drawing.Point(449,19); commandTestButton.Name="commandTestButton"; commandTestButton.Size=new System.Drawing.Size(126,24); commandTestButton.Text="Send command"; commandTestButton.UseVisualStyleBackColor=true; commandTestButton.MouseClick += new System.Windows.Forms.MouseEventHandler(CommandTestButtonClick);
AutoScaleDimensions=new System.Drawing.SizeF(8F,16F); AutoScaleMode=System.Windows.Forms.AutoScaleMode.Font; Controls.Add(optoTestGroupBox); Controls.Add(RfidTestGroupBox); Name="AllyReaderManualTestCtrl"; Size=new System.Drawing.Size(611,432); optoTestGroupBox.ResumeLayout(false); RfidTestGroupBox.ResumeLayout(false); RfidTestGroupBox.PerformLayout(); ResumeLayout(false);
}
private System.Windows.Forms.GroupBox optoTestGroupBox,RfidTestGroupBox; private System.Windows.Forms.ListBox optoListBox,rfidOutputListBox; private System.Windows.Forms.Label label2; private System.Windows.Forms.ComboBox rfidCommandComboBox; private System.Windows.Forms.Button commandTestButton;
}
}
@@ -0,0 +1,113 @@
using System;
using System.Linq;
using System.Windows.Forms;
using TBF.Rig.RegisterReaders.AllyReader.Communication;
using TBF.Rig.Sequences;
namespace TBF.Rig.RegisterReaders.AllyReader
{
public partial class AllyReaderManualTestCtrl : UserControl
{
private const int CommandTimeoutMs = 5000;
private readonly Timer opticalPollTimer;
private AllyReaderCfg config;
public AllyReaderManualTestCtrl()
{
InitializeComponent();
ShowNormalCommands();
opticalPollTimer = new Timer { Interval = 250 };
opticalPollTimer.Tick += OpticalPollTimer_Tick;
}
public AllyReaderCfg Config { set { config = value; } }
public void StopOpticalStream()
{
opticalPollTimer.Stop();
AllyMeterReader reader = FindReader();
if (reader != null)
{
try { reader.StopOpticalVerificationStream(CommandTimeoutMs); }
catch (Exception exception) { AddOutput("Stop error: " + exception.Message); }
}
ShowNormalCommands();
}
private void CommandTestButtonClick(object sender, MouseEventArgs e)
{
AllyMeterReader r = FindReader(); string cmd = rfidCommandComboBox.SelectedItem as string;
if (r == null) { AddOutput("Configured ALLY reader is not initialized on this bench."); return; }
try {
if (cmd == "Start optical stream")
{
r.StartOpticalVerificationStream(CommandTimeoutMs);
opticalPollTimer.Start();
ShowStopOnlyCommand();
AddOutput("Optical stream started: valve open, spread spectrum disabled, mode 0x09, LED 0xC2, COM optical capture active.");
}
else if (cmd == "Stop optical stream") { StopOpticalStream(); AddOutput("Stopped"); }
else if (cmd == "Read optical data") { r.RunDeviceBefore(); AddOpto(r.ReadOptoData()); }
else if (cmd == "Read serial number") AddOutput(r.ReadSerialNumber(CommandTimeoutMs));
else if (cmd == "Read version and type") AddOutput(r.ReadVersionAndType(CommandTimeoutMs).ToString());
else if (cmd == "View factory seal") AddOutput(r.IsFactorySealed(CommandTimeoutMs) ? "Factory seal: sealed" : "Factory seal: unsealed");
else if (cmd == "Unseal meter") UnsealMeter(r);
else if (cmd == "Seal meter") SealMeter(r);
else if (cmd == "Set RFID mode") { r.SetRfidInterface(); AddOutput("OK"); }
else if (cmd == "Set NFC mode") { r.SetNfcInterface(); AddOutput("OK"); }
} catch(Exception x) { AddOutput("Error: " + x.Message); }
}
private void UnsealMeter(AllyMeterReader reader)
{
if (MessageBox.Show("Unseal the ALLY meter? This changes its factory-seal state.", "Unseal meter", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes)
{
AddOutput("Unseal cancelled.");
return;
}
AllyFactoryUnsealData data = reader.ReadFactoryUnsealData(CommandTimeoutMs);
AddOutput("Factory seal before unseal: " + (data.IsSealed ? "sealed" : "unsealed"));
AddOutput("Factory ID: " + data.FactoryId);
AddOutput("Programmable text: " + data.ProgrammableText);
AddOutput("Reading preset: " + data.ReadingPreset);
AddOutput("Seconds active: " + data.SecondsActive);
AddOutput("Calculated unseal hash: " + data.CredentialHex);
if (!data.IsSealed)
{
AddOutput("Unseal skipped: meter is already unsealed.");
return;
}
reader.UnsealFactory(data, CommandTimeoutMs);
AddOutput("Unseal response: 0x01 COMPLETE_NO_ERRORS");
AddOutput(reader.IsFactorySealed(CommandTimeoutMs) ? "Factory seal is still sealed." : "Factory seal: unsealed");
}
private void SealMeter(AllyMeterReader reader)
{
if (MessageBox.Show("Seal the ALLY meter? This protects factory commands and optical-output configuration.", "Seal meter", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes)
{
AddOutput("Seal cancelled.");
return;
}
reader.SealFactory(CommandTimeoutMs);
AddOutput("Seal response: 0x01 COMPLETE_NO_ERRORS");
AddOutput(reader.IsFactorySealed(CommandTimeoutMs) ? "Factory seal: sealed" : "Factory seal was not applied.");
}
private void OpticalPollTimer_Tick(object sender, EventArgs e) { AllyMeterReader r=FindReader(); if(r==null) { opticalPollTimer.Stop(); return; } try { r.RunDeviceBefore(); AddOpto(r.ReadOptoData()); } catch(Exception x) { AddOpto("Error: " + x.Message); } }
private AllyMeterReader FindReader() { return config == null || ProcessData.SmartHeadsUni == null ? null : ProcessData.SmartHeadsUni.OfType<AllyMeterReader>().FirstOrDefault(x => x.Name == config.Name); }
private void AddOpto(string text) { if(!string.IsNullOrEmpty(text)) optoListBox.Items.Insert(0,text); }
private void AddOutput(string text) { rfidOutputListBox.Items.Insert(0,text ?? string.Empty); }
private void ShowNormalCommands()
{
rfidCommandComboBox.Items.Clear();
rfidCommandComboBox.Items.AddRange(new object[] { "Read serial number", "Read version and type", "View factory seal", "Unseal meter", "Seal meter", "Set RFID mode", "Set NFC mode", "Read optical data", "Start optical stream", "Stop optical stream" });
rfidCommandComboBox.SelectedIndex = 0;
}
private void ShowStopOnlyCommand()
{
rfidCommandComboBox.Items.Clear();
rfidCommandComboBox.Items.Add("Stop optical stream");
rfidCommandComboBox.SelectedIndex = 0;
}
}
}
@@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8"?><root><resheader name="resmimetype"><value>text/microsoft-resx</value></resheader><resheader name="version"><value>2.0</value></resheader><resheader name="reader"><value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></resheader><resheader name="writer"><value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></resheader></root>
@@ -93,6 +93,96 @@ namespace TBF.Rig.RegisterReaders.AllyReader.Communication
timeoutMs);
}
/// <summary>
/// Enables the documented ALLY optical-verification output. Factory
/// sealed registers must be explicitly unsealed before this sequence.
/// </summary>
public void StartOpticalVerificationOutput(int timeoutMs)
{
if (IsFactorySealed(timeoutMs))
{
throw new InvalidOperationException(
"ALLY meter is factory sealed. Unseal the meter before starting the optical stream.");
}
OpenValve(timeoutMs);
SetSpreadSpectrum(false, timeoutMs);
SetMeterMode(0x09, timeoutMs);
SetDiagnosticLed(0xC2, timeoutMs);
}
/// <summary>
/// Restores the documented normal ALLY operating state after optical
/// verification output has stopped.
/// </summary>
public void StopOpticalVerificationOutput(int timeoutMs)
{
SetDiagnosticLed(0x00, timeoutMs);
SetMeterMode(0x02, timeoutMs);
SetSpreadSpectrum(true, timeoutMs);
}
public bool IsFactorySealed(int timeoutMs)
{
AllyResponse response = Send(
new AllyFrameBuilder().WithDeviceCommand(AllyDeviceCommand.ViewFactorySeal).BuildBytes(),
timeoutMs);
return response.GetByte() != 0x00;
}
public AllyFactoryUnsealData ReadFactoryUnsealData(int timeoutMs)
{
bool isSealed = IsFactorySealed(timeoutMs);
string programmableText = Send(
new AllyFrameBuilder().WithCommand(AllyCommand.ViewProgrammableText).BuildBytes(),
timeoutMs).GetNullTerminatedAscii();
string factoryId = Send(
new AllyFrameBuilder().WithCommand(AllyCommand.ViewFactoryId).BuildBytes(),
timeoutMs).GetNullTerminatedAscii();
string readingPreset = Send(
new AllyFrameBuilder().WithCommand(AllyCommand.ViewPresetTotal).BuildBytes(),
timeoutMs).GetNullTerminatedAscii();
uint secondsActive = Send(
new AllyFrameBuilder().WithDeviceCommand(AllyDeviceCommand.ViewSecondsActive).BuildBytes(),
timeoutMs).GetUInt32LittleEndian();
return new AllyFactoryUnsealData(
isSealed,
factoryId,
programmableText,
readingPreset,
secondsActive);
}
public void UnsealFactory(int timeoutMs)
{
UnsealFactory(ReadFactoryUnsealData(timeoutMs), timeoutMs);
}
public void UnsealFactory(AllyFactoryUnsealData data, int timeoutMs)
{
if (data == null)
throw new ArgumentNullException(nameof(data));
Send(
new AllyFrameBuilder()
.WithDeviceCommand(AllyDeviceCommand.SetFactorySeal)
.WithByte(0x00)
.WithBytes(data.Credential)
.BuildBytes(),
timeoutMs);
}
public void SealFactory(int timeoutMs)
{
Send(
new AllyFrameBuilder()
.WithDeviceCommand(AllyDeviceCommand.SetFactorySeal)
.WithByte(0x01)
.BuildBytes(),
timeoutMs);
}
public double ReadCalibrationFactorPercent(int timeoutMs)
{
AllyResponse response = Send(
@@ -0,0 +1,130 @@
using System;
using System.Text;
namespace TBF.Rig.RegisterReaders.AllyReader.Communication
{
/// <summary>
/// Values read immediately before breaking an ALLY factory seal and the
/// corresponding, meter-specific eight-byte unseal credential.
/// </summary>
public sealed class AllyFactoryUnsealData
{
private const string FactoryFallback = "Factory";
internal AllyFactoryUnsealData(
bool isSealed,
string factoryId,
string programmableText,
string readingPreset,
uint secondsActive)
{
IsSealed = isSealed;
FactoryId = factoryId ?? string.Empty;
ProgrammableText = programmableText ?? string.Empty;
ReadingPreset = readingPreset ?? string.Empty;
SecondsActive = secondsActive;
Credential = BuildCredential();
}
public bool IsSealed { get; private set; }
public string FactoryId { get; private set; }
public string ProgrammableText { get; private set; }
public string ReadingPreset { get; private set; }
public uint SecondsActive { get; private set; }
public byte[] Credential { get; private set; }
public string CredentialHex
{
get { return BitConverter.ToString(Credential).Replace("-", " "); }
}
private byte[] BuildCredential()
{
byte[] result = new byte[8];
ushort secondsPart = (ushort)((SecondsActive >> 8) & 0xFFFF);
ushort factoryIdPart = CalculateIperlCrc16(NormalizeFactoryId(FactoryId));
ushort customerTextPart = CalculateCustomerTextPart(ProgrammableText);
ushort presetPart = CalculatePresetPart(ReadingPreset);
WriteUInt16LittleEndian(result, 0, secondsPart);
WriteUInt16LittleEndian(result, 2, factoryIdPart);
WriteUInt16LittleEndian(result, 4, customerTextPart);
WriteUInt16LittleEndian(result, 6, presetPart);
return result;
}
private static ushort CalculateIperlCrc16(string value)
{
ushort crc = 0;
byte[] data = Encoding.ASCII.GetBytes(value);
foreach (byte valueByte in data)
{
ushort temp = crc;
crc = (ushort)(temp >> 8);
crc += (ushort)(temp << 8);
crc ^= valueByte;
temp = (ushort)((crc >> 4) & 0x000F);
crc ^= temp;
temp = (ushort)((crc & 0x000F) << 12);
crc ^= temp;
temp = (ushort)((crc & 0x00FF) << 5);
crc ^= temp;
}
return crc;
}
private static ushort CalculateCustomerTextPart(string value)
{
byte[] data = Encoding.ASCII.GetBytes(OverlayFactory(value));
ushort result = 0;
for (int index = 0; index < 4; index++)
{
byte current = data[index];
byte setBits = 0;
for (int bit = 0; bit < 8; bit++)
setBits += (byte)((current >> bit) & 0x01);
result |= (ushort)(setBits << (index * 4));
}
return result;
}
private static ushort CalculatePresetPart(string value)
{
byte[] data = Encoding.ASCII.GetBytes(OverlayFactory(value));
uint selectedCharacters = ((uint)data[2] << 24) |
((uint)data[3] << 16) |
((uint)data[4] << 8) |
data[5];
return (ushort)(selectedCharacters % 11);
}
private static string OverlayFactory(string value)
{
char[] result = FactoryFallback.ToCharArray();
if (!string.IsNullOrWhiteSpace(value))
{
string trimmed = value.Trim();
int count = Math.Min(result.Length, trimmed.Length);
for (int index = 0; index < count; index++)
result[index] = trimmed[index];
}
return new string(result);
}
private static string NormalizeFactoryId(string value)
{
return string.IsNullOrWhiteSpace(value) ? FactoryFallback : value.Trim();
}
private static void WriteUInt16LittleEndian(byte[] target, int offset, ushort value)
{
target[offset] = (byte)(value & 0xFF);
target[offset + 1] = (byte)(value >> 8);
}
}
}
@@ -16,6 +16,8 @@ namespace TBF.Rig.RegisterReaders.AllyReader.Communication
{
ViewFactoryId = 0x01,
ViewVersionAndType = 0x05,
ViewProgrammableText = 0x07,
ViewPresetTotal = 0x13,
SetPresetTotal = 0x14,
SetMeterMode = 0x1A,
SetValvePosition = 0x1E
@@ -29,6 +31,9 @@ namespace TBF.Rig.RegisterReaders.AllyReader.Communication
SetCalibration = 0x54,
ViewRebootCount = 0x55,
SetDiagnosticLed = 0x60,
ViewSecondsActive = 0x3D,
ViewFactorySeal = 0x63,
SetFactorySeal = 0x64,
SetLcdTimeout = 0x8C,
StartOffsetLearning = 0xD1
}
@@ -1,6 +1,7 @@
using System;
using System.Diagnostics;
using System.IO.Ports;
using log4net;
namespace TBF.Rig.RegisterReaders.AllyReader.Communication
{
@@ -62,6 +63,7 @@ namespace TBF.Rig.RegisterReaders.AllyReader.Communication
public sealed class AllySerialTransport : IAllyTransport
{
private static readonly ILog log = LogManager.GetLogger(typeof(AllySerialTransport));
private readonly object sync = new object();
private readonly string portName;
private readonly int baudRate;
@@ -113,33 +115,62 @@ namespace TBF.Rig.RegisterReaders.AllyReader.Communication
serialPort.DiscardInBuffer();
serialPort.WriteTimeout = timeoutMs;
serialPort.Write(request, 0, request.Length);
log.DebugFormat("ALLY_CMD TX {0}: {1}", portName, FormatRequestForLog(request));
Stopwatch stopwatch = Stopwatch.StartNew();
int first;
do
try
{
first = ReadByte(stopwatch, timeoutMs);
serialPort.Write(request, 0, request.Length);
Stopwatch stopwatch = Stopwatch.StartNew();
int first;
do
{
first = ReadByte(stopwatch, timeoutMs);
}
while (first != AllyProtocol.Start);
int direction = ReadByte(stopwatch, timeoutMs);
int length = ReadByte(stopwatch, timeoutMs);
if (length < 5)
throw new FormatException("ALLY response length is invalid.");
byte[] response = new byte[length];
response[0] = (byte)first;
response[1] = (byte)direction;
response[2] = (byte)length;
for (int i = 3; i < response.Length; i++)
response[i] = (byte)ReadByte(stopwatch, timeoutMs);
log.DebugFormat("ALLY_CMD RX {0}: {1}", portName, ToHex(response));
return response;
}
catch (Exception exception)
{
log.Error("ALLY_CMD failed on " + portName + "; request=" + FormatRequestForLog(request), exception);
throw;
}
while (first != AllyProtocol.Start);
int direction = ReadByte(stopwatch, timeoutMs);
int length = ReadByte(stopwatch, timeoutMs);
if (length < 5)
throw new FormatException("ALLY response length is invalid.");
byte[] response = new byte[length];
response[0] = (byte)first;
response[1] = (byte)direction;
response[2] = (byte)length;
for (int i = 3; i < response.Length; i++)
response[i] = (byte)ReadByte(stopwatch, timeoutMs);
return response;
}
}
private static string FormatRequestForLog(byte[] request)
{
// Factory-unseal credentials must not be persisted in the shared TBF log.
if (request != null && request.Length == 15 &&
request[0] == 0x53 && request[1] == 0x57 && request[2] == 0x0F &&
request[3] == 0xFD && request[4] == 0x64 && request[5] == 0x00 && request[14] == 0x0D)
{
return "53 57 0F FD 64 00 <factory-unseal credential redacted; 8 bytes> 0D";
}
return ToHex(request);
}
private static string ToHex(byte[] data)
{
return data == null ? "<null>" : BitConverter.ToString(data).Replace("-", " ");
}
private int ReadByte(Stopwatch stopwatch, int timeoutMs)
{
int remaining = timeoutMs - (int)stopwatch.ElapsedMilliseconds;
@@ -0,0 +1,55 @@
using System;
using TBF.Rig.Generic;
using TBF.Rig.TestMethods.iPerlCommunication.iPerlHead;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.common;
namespace TBF.Rig.RegisterReaders.AsicReader
{
/// <summary>
/// ASIC register reader.
///
/// The communication implementation deliberately derives from the proven
/// iPerl head implementation. This keeps the wire protocol and persisted
/// iPerl configurations compatible while the public component family is
/// migrated to RegisterReaders.AsicReader.
/// </summary>
public class AsicReader : IperlHead, ISmartReader
{
public AsicReader()
{
}
public AsicReader(IComponentCfg cfg)
: base(cfg)
{
}
// The original IperlHead API keeps the communication interface strongly
// typed. SmartCommunicationForm presents all reader families through a
// string-based contract.
string ISmartReader.CommInterface { get { return CommInterface.ToString(); } }
bool ISmartReader.CommFailed
{
get { return CommFailed; }
set { CommFailed = value; }
}
bool ISmartReader.Disabled
{
get { return Disabled; }
set { Disabled = value; }
}
void ISmartReader.ResetNfcInterface(bool? nfcOn) { ResetNfcInterface(nfcOn); }
void ISmartReader.SetNfcInterface() { SetNfcInterface(); }
void ISmartReader.SetRfidInterface() { SetRfidInterface(); }
void ISmartReader.SetCommunicationInterface(string commInterface)
{
CommunicationInterface parsedInterface;
if (!Enum.TryParse(commInterface, true, out parsedInterface))
throw new ArgumentException("Unknown ASIC communication interface: " + commInterface, nameof(commInterface));
SetCommunicationInterface(parsedInterface);
}
}
}
@@ -0,0 +1,37 @@
using System.Xml.Serialization;
using TBF.Rig.Generic;
using TBF.Rig.TestMethods.iPerlCommunication.iPerlHead;
namespace TBF.Rig.RegisterReaders.AsicReader
{
/// <summary>
/// Configuration identity for newly created ASIC readers.
///
/// It intentionally inherits the proven iPerl transport fields: the ASIC
/// hardware uses the same C4/optical/NFC/RFID wiring. The separate XML root
/// prevents new ASIC components from being persisted as generic iPerl heads,
/// while Factory still accepts legacy IperlHeadCfg XML.
/// </summary>
[XmlRoot("AsicReaderCfg")]
public class AsicReaderCfg : IperlHeadCfg
{
public static readonly XmlSerializer Serializer =
XmlSerializer.FromTypes(new[] { typeof(AsicReaderCfg) })[0];
public AsicReaderCfg()
: this(null)
{
}
public AsicReaderCfg(IComponentFactory factory)
: base(factory)
{
Name = "ASIC";
}
public override XmlSerializer GetSerializer()
{
return Serializer;
}
}
}
@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using TBF.Rig.Generic;
using TBF.Rig.TestMethods.iPerlCommunication.iPerlHead;
namespace TBF.Rig.RegisterReaders.AsicReader
{
/// <summary>
/// Factory for newly configured ASIC readers. Existing database rows with
/// class name "RegisterReader for iPerl" continue to resolve through the
/// legacy iPerl factory.
/// </summary>
public class Factory : IComponentFactory
{
public string ClassName { get { return GetType().Namespace.Substring(8); } }
public override string ToString()
{
return ClassName;
}
public IComponent DummyComponent()
{
return new AsicReader();
}
public IComponent GetComponent(IComponentCfg cfg, IList<IComponent> components)
{
return new AsicReader(cfg);
}
// New ASIC components are stored under their own XML root. AsicReaderCfg
// inherits IperlHeadCfg, so the proven transport implementation receives
// exactly the same wire/port settings as before.
public IComponentCfg DefaultConfig()
{
return new AsicReaderCfg(this);
}
public IComponentCfg CmpntCfgFromCmpntEntity(Config.Entities.Component component)
{
try
{
return ComponentCfgBase.CreateFromDbEntity(AsicReaderCfg.Serializer, component, this);
}
catch (InvalidOperationException)
{
// A partially migrated bench can already contain the former
// IperlHeadCfg XML. Keep it readable rather than requiring a
// database/configuration migration before an ASIC test can run.
return ComponentCfgBase.CreateFromDbEntity(IperlHeadCfg.Serializer, component, this);
}
}
}
}
@@ -222,6 +222,7 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
{
FileName = fileName,
Arguments = args,
WorkingDirectory = System.IO.Path.GetDirectoryName(System.IO.Path.GetFullPath(fileName)),
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
@@ -258,9 +259,18 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
incommingTimeMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
string allOutput = (stdOutTask.Result ?? "") + (stdErrTask.Result ?? "");
info.ExitCode = process.ExitCode;
info.StandardOutput = stdOutTask.Result ?? "";
info.StandardError = stdErrTask.Result ?? "";
string allOutput = info.StandardOutput + info.StandardError;
log?.Debug(allOutput);
if (info.ExitCode != 0)
{
info.FailureReason = $"CLI exited with exit code {info.ExitCode}.";
log?.Error($"{info.Name}: {info.FailureReason} stderr='{info.StandardError}'");
}
info.State = CliTaskState.Completed;
return allOutput;
}
@@ -290,6 +300,7 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
public void AddRunAndCaptureJsonAsync<T>(string fileName, string args) where T : new()
{
log?.Debug($"CLI queued: file='{fileName}', args='{args}'");
ResetStartTime();
var cts = new CancellationTokenSource();
@@ -311,6 +322,7 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
{
FileName = fileName,
Arguments = args,
WorkingDirectory = System.IO.Path.GetDirectoryName(System.IO.Path.GetFullPath(fileName)),
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
@@ -323,6 +335,7 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
try
{
log?.Debug($"CLI starting: file='{psi.FileName}', args='{psi.Arguments}', workingDirectory='{psi.WorkingDirectory}', exists={System.IO.File.Exists(psi.FileName)}");
process.Start();
var stdoutTask = process.StandardOutput.ReadToEndAsync();
@@ -347,8 +360,20 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
incommingTimeMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
string allOutput = (stdoutTask.Result ?? "") + (stderrTask.Result ?? "");
info.ExitCode = process.ExitCode;
info.StandardOutput = stdoutTask.Result ?? "";
info.StandardError = stderrTask.Result ?? "";
string allOutput = info.StandardOutput + info.StandardError;
log?.Debug(allOutput);
log?.Debug($"CLI completed: name='{info.Name}', exitCode={info.ExitCode}, stdoutLength={info.StandardOutput.Length}, stderrLength={info.StandardError.Length}");
if (info.ExitCode != 0)
{
info.FailureReason = $"CLI exited with exit code {info.ExitCode}.";
log?.Error($"{info.Name}: {info.FailureReason} stderr='{info.StandardError}'");
info.State = CliTaskState.Completed;
return default(T);
}
string json = ExtractJson(allOutput);
T result;
@@ -358,6 +383,8 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
return result;
}
info.FailureReason = "CLI completed without a valid JSON response.";
log?.Error($"{info.Name}: {info.FailureReason} stdout='{info.StandardOutput}' stderr='{info.StandardError}'");
info.State = CliTaskState.Completed;
return default(T);
}
@@ -380,18 +407,17 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
public bool TryJsonStringDeserialize<T>(string json, out T value) where T : new()
{
if (json != null)
if (!string.IsNullOrWhiteSpace(json))
{
try
{
value = JsonConvert.DeserializeObject<T>(json);
return true;
return value != null;
}
catch (Exception ex)
{
log?.Debug(ex.Message);
value = TryConvert<T>(json);
return true;
return TryConvert(json, out value);
}
}
@@ -399,7 +425,7 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
return false;
}
private static T TryConvert<T>(string json) where T : new()
private static bool TryConvert<T>(string json, out T value) where T : new()
{
T obj = new T();
@@ -417,21 +443,24 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
{
try
{
object value = token.ToObject(prop.PropertyType);
prop.SetValue(obj, value);
object propertyValue = token.ToObject(prop.PropertyType);
prop.SetValue(obj, propertyValue);
}
catch
{
}
}
}
value = obj;
return true;
}
catch (Exception ex)
{
Console.WriteLine($"TryConvert failed: {ex.Message}");
value = default(T);
return false;
}
return obj;
}
public string ExtractJson(string text)
@@ -462,4 +491,4 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
});
}
}
}
}
@@ -11,6 +11,10 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
public Process Process { get; set; }
public CliTaskState State { get; set; } = CliTaskState.Running;
public string Name { get; set; }
public int? ExitCode { get; set; }
public string StandardOutput { get; set; }
public string StandardError { get; set; }
public string FailureReason { get; set; }
public bool UseResult
{
@@ -22,4 +26,4 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
}
}
}
}
}
@@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using TBF.Rig;
namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
{
public interface IPoseidonReadOperation
{
string Name { get; }
bool IsNotStarted { get; }
bool IsFinished { get; }
bool HasError { get; }
void Start(bool readStart);
Event Run();
}
public sealed class PoseidonReaderOperation : IPoseidonReadOperation
{
private readonly PoseidonReader reader;
public PoseidonReaderOperation(PoseidonReader reader)
{
if (reader == null) throw new ArgumentNullException(nameof(reader));
this.reader = reader;
}
public string Name { get { return reader.Name; } }
public bool IsNotStarted { get { return reader.CurrentOp == PoseidonReader.CurrentPoseidonOp.None; } }
public bool IsFinished { get { return reader.CurrentOp == PoseidonReader.CurrentPoseidonOp.Done || reader.CurrentOp == PoseidonReader.CurrentPoseidonOp.Error; } }
public bool HasError { get { return reader.CurrentOp == PoseidonReader.CurrentPoseidonOp.Error; } }
public void Start(bool readStart) { reader.SetCurrentOp(readStart ? PoseidonReader.CurrentPoseidonOp.ReadDataStream_Start : PoseidonReader.CurrentPoseidonOp.ReadDataStream_End); }
public Event Run() { return reader.Run(); }
}
public static class PoseidonReadCycle
{
public static bool RunIteration(IEnumerable<IPoseidonReadOperation> readers, bool readStart)
{
if (readers == null) return true;
bool allReadersFinished = true;
foreach (IPoseidonReadOperation reader in readers)
{
if (reader == null) continue;
if (reader.IsNotStarted) reader.Start(readStart);
reader.Run();
if (!reader.IsFinished) allReadersFinished = false;
}
return allReadersFinished;
}
}
/// <summary>
/// Owns one dialog phase (START or STOP). A reader is armed once per phase,
/// independently of its terminal state from a preceding phase.
/// </summary>
public sealed class PoseidonReadPhaseRunner
{
private readonly bool readStart;
private readonly HashSet<IPoseidonReadOperation> startedReaders =
new HashSet<IPoseidonReadOperation>();
public int IterationCount { get; private set; }
public PoseidonReadPhaseRunner(bool readStart)
{
this.readStart = readStart;
}
public bool RunIteration(IEnumerable<IPoseidonReadOperation> readers)
{
if (readers == null) return true;
IterationCount++;
bool allReadersFinished = true;
foreach (IPoseidonReadOperation reader in readers)
{
if (reader == null) continue;
if (startedReaders.Add(reader)) reader.Start(readStart);
reader.Run();
if (!reader.IsFinished) allReadersFinished = false;
}
return allReadersFinished;
}
}
}
@@ -3,6 +3,7 @@
///
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Ports;
using System.Text.RegularExpressions;
@@ -24,6 +25,8 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
public class PoseidonReader : ComponentBase, IDevice, IRegReaderDatastream, ISessionDataMngmnt, IOperation, ICommonRegReader
{
private static readonly ILog log = LogManager.GetLogger(typeof(PoseidonReader));
// Simulation must never call the CLI configured for the physical Hat.
internal const string SimulatedCliFileName = "cmdSleepTest.exe";
public override string ToString() { return string.Format("{0}({1})", ClassName, Cfg.ToString(-1)); }
readonly PoseidonCfg registerReaderCfg;
@@ -62,12 +65,19 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
CurrentPoseidonOp _currentOp;
private bool _isReadingStart = true;
private bool? _isCliLogging;
private bool lastCliReadingParsed;
private string lastCliReadFailureReason;
private bool lastCliReadSucceeded;
public CurrentPoseidonOp CurrentOp
{
get { return _currentOp; }
}
public bool LastCliReadingParsed { get { return lastCliReadingParsed; } }
public string LastCliReadFailureReason { get { return lastCliReadFailureReason; } }
public bool LastCliReadSucceeded { get { return lastCliReadSucceeded; } }
public void SetCurrentOp(CurrentPoseidonOp operation = CurrentPoseidonOp.None)
{
_currentOp = operation ;
@@ -286,9 +296,25 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
if ((DebugLevel == DebugMode.Normal)||(DebugLevel == DebugMode.Simulate))
{
/// Prepare serial port
if (registerReaderCfg.MeterType <= 0)
{
log.WarnFormat(
"{0}: configured Poseidon MeterType={1}; using CLI default MeterType={2}.",
Name,
registerReaderCfg.MeterType,
SerialPortData.DefaultPoseidonMeterType);
}
string cliFileName = GetCliFileNameForMode(DebugLevel, registerReaderCfg.CliFileName);
serialPort = new SerialPortData(string.Format("COM{0}", registerReaderCfg.ComPortNr),
registerReaderCfg.CliFileName,
registerReaderCfg.MeterType);
cliFileName,
registerReaderCfg.MeterType);
if (DebugLevel == DebugMode.Simulate)
{
log.WarnFormat(
"{0}: simulation mode enabled; overriding configured CLI '{1}' with '{2}'.",
Name, registerReaderCfg.CliFileName, serialPort.SerialPortCmdClientPath);
}
//TODO BUMI prepare serial port - for us do nothing
@@ -311,6 +337,13 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
}
}
internal static string GetCliFileNameForMode(DebugMode debugMode, string configuredCliFileName)
{
return debugMode == DebugMode.Simulate
? SimulatedCliFileName
: configuredCliFileName;
}
public void Clear()
{
log.DebugFormat("{0}:Clear()", Name);
@@ -407,12 +440,18 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
else if (_currentOp == CurrentPoseidonOp.ReadDataStream_Start
|| _currentOp == CurrentPoseidonOp.ReadDataStream_End)
{
lastCliReadingParsed = false;
lastCliReadFailureReason = null;
lastCliReadSucceeded = false;
startTimeInMilis = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
incommingTime = -1;
_isReadingStart = (_currentOp == CurrentPoseidonOp.ReadDataStream_Start);
CliRunner.Clear();
_lastOpTimedOut = false;
log.DebugFormat("{0}: starting CLI read, direction={1}, path='{2}', args='{3}'",
Name, _isReadingStart ? "start" : "end", serialPort.SerialPortCmdClientPath,
serialPort.DefaultArgSettings(SerialPortData.EMeterArg.AllParams));
CliRunner.AddRunAndCaptureJsonAsync<JsonDataFromPoseidon>(serialPort,
SerialPortData.EMeterArg.AllParams);
_currentOp = CurrentPoseidonOp.ReadDatastream_Running;
@@ -424,6 +463,8 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
{
_currentOp = CurrentPoseidonOp.ReadDatastream_Done;
incommingTime = CliRunner.IncommingTime;
log.DebugFormat("{0}: CLI task completed for {1}; incomingTime={2}", Name,
_isReadingStart ? "START" : "STOP", incommingTime);
}
else if (CliRunner.TimeOutReceived(SafetyTimeOut))
{
@@ -431,6 +472,8 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
CliRunner.CancelUndoneTasksAsTimedOut();
_currentOp = CurrentPoseidonOp.ReadDatastream_Done;
incommingTime = CliRunner.IncommingTime;
log.ErrorFormat("{0}: CLI timeout for {1}; timeoutMs={2}", Name,
_isReadingStart ? "START" : "STOP", SafetyTimeOut);
}
return Event.Busy;
@@ -449,9 +492,16 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
{
var task = (Task<JsonDataFromPoseidon>)firstTaskInfo.Task;
data = task.Result;
if (data == null)
lastCliReadFailureReason = firstTaskInfo.FailureReason ?? "CLI returned no Poseidon JSON data.";
}
else
{
lastCliReadFailureReason = _lastOpTimedOut
? "CLI read timed out."
: "No completed JsonDataFromPoseidon task was available.";
log.ErrorFormat("{0}: Poseidon {1} read failed: {2}", Name,
_isReadingStart ? "START" : "STOP", lastCliReadFailureReason);
if (_lastOpTimedOut)
{
log.Warn($"PoseidonReader {Name}: ReadDatastream timed out, no completed result available.");
@@ -464,6 +514,15 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
if (data != null)
{
string validationError;
if (!TryValidateCliReadResponse(data, out validationError))
{
lastCliReadFailureReason = validationError;
log.ErrorFormat("{0}: Poseidon {1} read rejected. {2}", Name,
_isReadingStart ? "Begin" : "End", validationError);
}
else
{
if (string.IsNullOrEmpty(wmSerialNr))
{
try
@@ -476,15 +535,28 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
}
}
double volume;
if (Double.TryParse(data.Reading, out volume))
double volumeLi;
string dialogValueFailureReason;
if (TryGetDialogValue(data, out volumeLi, out dialogValueFailureReason))
{
double volumeLi = Units.ConvertFrom(Unit.USgal, volume);
lastCliReadingParsed = true;
lastCliReadSucceeded = true;
double volume;
TryParseCliReading(data.Reading, out volume);
if (_isReadingStart)
beginWMState = volumeLi;
else
endWMState = volumeLi;
log.InfoFormat("{0}: Poseidon {1} value stored. deviceId={2}, rawReading='{3}', gallons={4}, litres={5}, Begin={6}, End={7}",
Name, _isReadingStart ? "Begin" : "End", data.DeviceId, data.Reading, volume, volumeLi, beginWMState, endWMState);
}
else
{
lastCliReadFailureReason = dialogValueFailureReason;
log.ErrorFormat("{0}: cannot parse CLI reading '{1}' using invariant or current culture.",
Name, data.Reading);
}
}
}
@@ -497,6 +569,47 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
}
public static bool TryParseCliReading(string reading, out double value)
{
value = 0;
if (String.IsNullOrWhiteSpace(reading)) return false;
return Double.TryParse(reading.Trim().Replace(',', '.'), NumberStyles.Float,
CultureInfo.InvariantCulture, out value);
}
/// <summary>
/// Validates a CLI response and converts its US-gallon reading to the
/// litre value assigned to the START/END dialog.
/// </summary>
public static bool TryGetDialogValue(JsonDataFromPoseidon data, out double valueLitres,
out string failureReason)
{
valueLitres = 0;
if (!TryValidateCliReadResponse(data, out failureReason))
return false;
double valueGallons;
if (!TryParseCliReading(data.Reading, out valueGallons))
{
failureReason = "Reading could not be parsed: '" + data.Reading + "'.";
return false;
}
valueLitres = Units.ConvertFrom(Unit.USgal, valueGallons);
failureReason = null;
return true;
}
public static bool TryValidateCliReadResponse(JsonDataFromPoseidon data, out string failureReason)
{
if (data == null) { failureReason = "CLI returned no JSON data."; return false; }
if (data.NfcTagDetected != true) { failureReason = "NfcTagDetected is false or missing."; return false; }
if (data.ReadingComplete != true) { failureReason = "ReadingComplete is false or missing."; return false; }
if (String.IsNullOrWhiteSpace(data.Reading)) { failureReason = "Reading is empty."; return false; }
failureReason = null;
return true;
}
/// <summary>Stop this operation</summary>
public void Stop()
@@ -5,6 +5,11 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
{
public class SerialPortData
{
public const string CliDirectory = @"C:\TBF\Cli";
// HatCliDemo identifies Poseidon with the numeric meter type 74.
// Older persisted configurations can contain the uninitialized value 0.
public const int DefaultPoseidonMeterType = 74;
private Boolean? _cliExists;
public bool CliExists { get {
if (_cliExists == null || !_cliExists.HasValue)
@@ -13,17 +18,26 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
}
return _cliExists.Value;
} }
public string SerialPortCmdClientPath {
#if DEBUG
get { return Path.Combine("C:\\","TBF","Cli", CmdClientName);}
#else
get { return Path.Combine("..","Cli", CmdClientName);}
#endif
}
public string SerialPortCmdClientPath
{
get
{
string cliFileName = Path.GetFileName(CmdClientName);
if (String.IsNullOrWhiteSpace(cliFileName))
cliFileName = "HalCli.exe";
return Path.Combine(CliDirectory, cliFileName);
}
}
public string CmdClientName { get; set; } = "HalCli.exe";
public string PortName { get; set; }
public int MeterType { get; set; }
public static int NormalizeMeterType(int meterType)
{
return meterType > 0 ? meterType : DefaultPoseidonMeterType;
}
public enum EMeterArg {
Calibration = 0,
AllParams = 2,
@@ -53,7 +67,7 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
{
PortName = portName;
CmdClientName = cmdClientName;
MeterType = meterType;
MeterType = NormalizeMeterType(meterType);
}
}
}
}
+36 -2
View File
@@ -29,8 +29,42 @@ namespace TBF.Rig.Sequences
///
void OpenIPerlCommForm(iPerlCommunicationSeq myRef, SmartComponentBase method, Test test, iPerlCommunicationParams testParams)
{
myRef.modelessDlg = new SmartCommunicationForm(method, test, testParams);
myRef.modelessDlg.Show();
// This delegate runs on the WinForms thread. Without this boundary a
// constructor exception is reported by Control.Invoke only, losing the
// useful SmartCommunicationForm stack frame in the application log.
log.InfoFormat(
"SMART_COMM_FORM_OPEN_START: methodType='{0}', methodName='{1}', test='{2}', activity='{3}', uiThread={4}",
method == null ? "<null>" : method.GetType().FullName,
method == null ? "<null>" : method.Name,
test == null ? "<null>" : test.Name,
testParams == null ? "<null>" : testParams.Activity,
System.Threading.Thread.CurrentThread.ManagedThreadId);
try
{
myRef.modelessDlg = new SmartCommunicationForm(method, test, testParams);
log.InfoFormat(
"SMART_COMM_FORM_OPEN_CONSTRUCTED: formType='{0}', disposed={1}, handleCreated={2}",
myRef.modelessDlg.GetType().FullName,
myRef.modelessDlg.IsDisposed,
myRef.modelessDlg.IsHandleCreated);
myRef.modelessDlg.Show();
log.InfoFormat(
"SMART_COMM_FORM_OPEN_SHOWN: visible={0}, handleCreated={1}",
myRef.modelessDlg.Visible,
myRef.modelessDlg.IsHandleCreated);
}
catch (Exception exception)
{
log.ErrorFormat(
"SMART_COMM_FORM_OPEN_FAILED: methodType='{0}', test='{1}', activity='{2}', exception={3}",
method == null ? "<null>" : method.GetType().FullName,
test == null ? "<null>" : test.Name,
testParams == null ? "<null>" : testParams.Activity,
exception);
throw;
}
}
+6
View File
@@ -15,6 +15,7 @@ using TBF.Rig.Sequences;
using Dirichlet.Numerics;
using TBF.Resources;
using TBF.Rig.RegisterReaders.iPerlReaderUNI;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.common;
namespace TBF.Rig
@@ -313,6 +314,11 @@ namespace TBF.Rig
cmpnt.StartChangeHandler(); /// Start handling parameter change events
}
// Keep the persisted manual/DataEntry selection aligned with the
// actual smart readers configured on this bench. This does not
// create placeholder positions for other reader families.
SmartReaderSelection.EnsureConfiguredReaders(Program.LocalSettings, ProcessData.SmartHeadsUni);
/// Pre-initialize the control board (= buffer the arguments ctrlBrdComponent, tankCapacities)
if (ControlBoardMain == null)
{
+4
View File
@@ -144,6 +144,8 @@ namespace TBF.Rig
new RegisterReaders.PoseidonReader.Factory(),
new RegisterReaders.IPerlReader.Factory(), /// the same functionality as TestMethods.iPerlCommunication.iPerlHead.Factory(),
new RegisterReaders.iPerlASICReader.Factory(), /// ASIC IPerl, C4 communication
new RegisterReaders.AsicReader.Factory(), /// ASIC reader extracted from the proven iPerl implementation
new RegisterReaders.AllyReader.Factory(), // ALLY IPerl, communication
new RegisterReaders.GenesisRegReader.Factory(), /// Genesis RegisterReader - dirrect communication with the head
new RegisterReaders.PulsesFromUniCB.Factory(), /// 'RegisterReader'
new RegisterReaders.StandingStartStop.Factory(), /// 'RegisterReader for standing start/stop'
@@ -201,6 +203,8 @@ namespace TBF.Rig
//new TestMethods.GenesisCommunication.GenesisHead.Factory(),
new TestMethods.GrabImage.Factory(),
new TestMethods.iPerlCommunication.TestMethodFactory(), /// iPerlCommunication
new TestMethods.AsicTest.Factory(), /// Explicit ASIC communication test
new TestMethods.AllyCalibration.Factory(), // Ally meter test
new TestMethods.LeakTest.Factory(),
new TestMethods.LiveStream.Factory(),
new TestMethods.ManualEntry.Factory(),
@@ -0,0 +1,80 @@
# ALLY optical-stream procedure
## Recommended procedure layout
Use the combined activities at the boundaries of measurement:
1. `Read serial number` (optional, recommended for traceability)
2. `Unseal meter and start optical stream`
3. One or more optical measurement activities
4. `Stop optical stream`
The optical measurement must run after the start activity completes and before the stop activity begins.
## Unseal meter and start optical stream
This is a production-bench setup operation. It leaves the factory seal **unsealed** and leaves the optical stream **running**.
| Order | Action | Protocol effect |
| --- | --- | --- |
| 1 | Read factory unseal data | Reads factory seal state, factory ID, programmable text, reading preset and seconds active. These values produce the meter-specific unseal credential. |
| 2 | Unseal only if necessary | Sends Factory Unseal with the calculated credential when the meter is sealed. An already unsealed meter is not changed. |
| 3 | Verify factory seal | Confirms that the seal is no longer active. The setup stops on failure. |
| 4 | Open ALLY valve | Sends valve state `Open`. |
| 5 | Disable Spread Spectrum | Required before enabling the diagnostic optical output. |
| 6 | Set initial meter mode | Sets meter mode `0x09`. |
| 7 | Turn diagnostic LED on | Sets LED output `0xC2`, enabling the optical verification output. |
| 8 | Start optical COM capture | Opens the configured ALLY optical port and starts parsing optical telegrams. |
### Equivalent manual composition
The procedure activity is intentionally atomic because the factory unseal credential is meter-specific. The UI manual command **Unseal meter** performs steps 13. The rest can be composed in the procedure as:
1. `Open ALLY valve`
2. `Disable spread spectrum`
3. `Set initial meter mode`
4. `Turn diagnostic LED on`
These individual activities configure the meter output, but do **not** open the TBF optical COM capture. Use `Unseal meter and start optical stream` when TBF must receive and parse optical data.
## Stop optical stream
Use this as the final cleanup activity after all optical measurements. It is idempotent with respect to COM capture: it always closes the local stream even if a previous command failed.
| Order | Action | Protocol effect |
| --- | --- | --- |
| 1 | Stop optical processing | Stops the reader's active optical processing loop. |
| 2 | Turn diagnostic LED off | Sets LED output `0x00`. |
| 3 | Set active meter mode | Sets meter mode `0x02`. |
| 4 | Enable Spread Spectrum | Restores normal Spread Spectrum operation. |
| 5 | Close optical COM capture | Stops data-stream processing and closes the configured optical serial port, also when an earlier cleanup command failed. |
The operation does **not** reseal the meter and does not close the valve. Resealing is a separate intentional factory operation.
### Equivalent manual composition
The closest procedure-only composition is:
1. `Turn diagnostic LED off`
2. `Set active meter mode`
3. `Enable spread spectrum`
This restores meter settings but it does **not** close TBF optical COM capture. Use `Stop optical stream` whenever the stream was started by `Unseal meter and start optical stream`.
## Individual activities
| Activity | What it changes | What it does not do |
| --- | --- | --- |
| `Open ALLY valve` | Opens the valve. | Does not alter optical output. |
| `Disable spread spectrum` | Disables Spread Spectrum. | Does not select mode or LED output. |
| `Set initial meter mode` | Sets mode `0x09`. | Does not enable the diagnostic LED or COM capture. |
| `Turn diagnostic LED on` | Sets LED `0xC2`. | Does not check/unseal factory seal or open COM capture. |
| `Turn diagnostic LED off` | Sets LED `0x00`. | Does not restore mode, spread spectrum or COM capture. |
| `Set active meter mode` | Sets mode `0x02`. | Does not turn off LED, enable Spread Spectrum or close COM capture. |
| `Enable spread spectrum` | Restores Spread Spectrum. | Does not stop LED or COM capture. |
| `Unseal meter and start optical stream` | Complete setup and COM capture. | Does not reseal automatically. |
| `Stop optical stream` | Complete optical cleanup and COM close. | Does not reseal or close the valve. |
## Factory-seal warning
`Unseal meter and start optical stream` changes factory-seal state when the meter is sealed. Add it only to procedures intended for authorized verification or calibration benches. A normal accuracy test should use a meter already prepared for optical verification.
@@ -20,7 +20,9 @@ namespace TBF.Rig.TestMethods.AllyCalibration
WriteCalibrationFactor,
WriteDisplayVolume,
SetLcdTimeout,
StartOffsetLearning
StartOffsetLearning,
UnsealAndStartOpticalStream,
StopOpticalStream
}
internal static class AllyCalibrationActivityNames
@@ -44,6 +46,8 @@ namespace TBF.Rig.TestMethods.AllyCalibration
public const string WriteDisplayVolume = "Write display volume";
public const string SetLcdTimeout = "Set LCD timeout";
public const string StartOffsetLearning = "Start offset learning";
public const string UnsealAndStartOpticalStream = "Unseal meter and start optical stream";
public const string StopOpticalStream = "Stop optical stream";
public static readonly string[] All =
{
@@ -65,7 +69,9 @@ namespace TBF.Rig.TestMethods.AllyCalibration
WriteCalibrationFactor,
WriteDisplayVolume,
SetLcdTimeout,
StartOffsetLearning
StartOffsetLearning,
UnsealAndStartOpticalStream,
StopOpticalStream
};
public static bool TryParse(string value, out AllyCalibrationActivity activity)
@@ -3,7 +3,9 @@ using System.Collections.Generic;
using System.Threading;
using Common;
using Config.Entities;
using log4net;
using Results.Entities;
using TBF.Rig.GenericDevices;
using TBF.Rig.RegisterReaders.AllyReader;
using TBF.Rig.RegisterReaders.AllyReader.Communication;
using TBF.Rig.Sequences;
@@ -13,6 +15,7 @@ namespace TBF.Rig.TestMethods.AllyCalibration
{
public class AllyCalibrationSeq : SequenceBase
{
private static readonly ILog log = LogManager.GetLogger(typeof(AllyCalibrationSeq));
private const byte InitialMeterMode = 0x09;
private const byte ActiveMeterMode = 0x02;
private const byte DiagnosticLedCalibrationMode = 0xC2;
@@ -28,6 +31,13 @@ namespace TBF.Rig.TestMethods.AllyCalibration
TestMethodCfg cfg,
TestMethodParams parameters)
{
log.InfoFormat(
"ALLY_CALIBRATION_SEQUENCE_START: test='{0}', metersPath='{1}', activity='{2}', configuredPositions={3}, registerReaders={4}",
test == null ? "<null>" : test.Name,
test == null ? "<null>" : test.MetersPath,
parameters == null ? "<null>" : parameters.Activity,
BatchRslts == null ? 0 : BatchRslts.WMPositionsCount,
sensPath == null || sensPath.RegisterReaders == null ? 0 : sensPath.RegisterReaders.Length);
Bridge.OnTestProgress(this, new TestProgressEventArgs(test, Progress.JustStarted));
TestRslt testResult = BatchRslts.GetTestRslt(test.Name, 0);
@@ -40,7 +50,14 @@ namespace TBF.Rig.TestMethods.AllyCalibration
{
Results.Entities.WaterMeter waterMeter = BatchRslts.Batch.WaterMeters[position];
if (waterMeter == null || waterMeter.Disabled)
{
log.InfoFormat(
"ALLY_CALIBRATION_POSITION_SKIPPED: test='{0}', position={1}, reason={2}",
test.Name,
position + 1,
waterMeter == null ? "water meter is missing" : "water meter is disabled");
continue;
}
MeterTestRslt meterResult = BatchRslts.GetMeterTestRslt(
test.Name,
@@ -59,11 +76,34 @@ namespace TBF.Rig.TestMethods.AllyCalibration
{
passed = false;
resultMessage = "ALLY" + (position + 1) + ": ALLY reader is not configured.";
IRegReader configuredReader = sensPath != null &&
sensPath.RegisterReaders != null &&
position < sensPath.RegisterReaders.Length
? sensPath.RegisterReaders[position]
: null;
log.WarnFormat(
"ALLY_CALIBRATION_READER_MISSING: test='{0}', position={1}; configuredReaderName='{2}', configuredReaderType='{3}', expectedType='AllyMeterReader'.",
test.Name,
position + 1,
configuredReader == null ? "<null>" : configuredReader.Name,
configuredReader == null ? "<null>" : configuredReader.GetType().FullName);
}
else
{
log.InfoFormat(
"ALLY_CALIBRATION_POSITION_START: test='{0}', position={1}, reader='{2}', activity='{3}'",
test.Name,
position + 1,
reader.Name,
parameters.Activity);
passed = ExecuteWithRetries(reader, cfg, parameters, out resultMessage);
resultMessage = reader.Name + ": " + resultMessage;
log.InfoFormat(
"ALLY_CALIBRATION_POSITION_COMPLETED: test='{0}', position={1}, passed={2}, result='{3}'",
test.Name,
position + 1,
passed,
resultMessage);
}
messages.Add(resultMessage);
@@ -90,7 +130,7 @@ namespace TBF.Rig.TestMethods.AllyCalibration
return new List<Event> { Event.Done };
}
private static bool ExecuteWithRetries(
internal static bool ExecuteWithRetries(
AllyMeterReader reader,
TestMethodCfg cfg,
TestMethodParams parameters,
@@ -103,6 +143,13 @@ namespace TBF.Rig.TestMethods.AllyCalibration
{
try
{
log.InfoFormat(
"ALLY_CALIBRATION_COMMAND_START: reader='{0}', activity='{1}', attempt={2}/{3}, timeoutMs={4}",
reader.Name,
parameters.Activity,
attempt,
attempts,
cfg.CommandTimeoutMs);
bool passed = ExecuteOnce(reader, cfg, parameters, out resultMessage);
if (!passed)
return false;
@@ -113,6 +160,13 @@ namespace TBF.Rig.TestMethods.AllyCalibration
catch (Exception ex)
{
lastException = ex;
log.WarnFormat(
"ALLY_CALIBRATION_COMMAND_FAILED: reader='{0}', activity='{1}', attempt={2}/{3}, error='{4}'",
reader.Name,
parameters.Activity,
attempt,
attempts,
ex.Message);
if (attempt < attempts && cfg.DelayBetweenAttemptsMs > 0)
Thread.Sleep(cfg.DelayBetweenAttemptsMs);
}
@@ -251,6 +305,16 @@ namespace TBF.Rig.TestMethods.AllyCalibration
resultMessage = "OffsetLearning=1000 samples, 1s delay";
return true;
case AllyCalibrationActivity.UnsealAndStartOpticalStream:
reader.UnsealAndStartOpticalVerificationStream(timeout);
resultMessage = "FactorySeal=unsealed; OpticalStream=active (valve=open, spread spectrum=disabled, mode=0x09, LED=0xC2)";
return true;
case AllyCalibrationActivity.StopOpticalStream:
reader.StopOpticalVerificationStream(timeout);
resultMessage = "OpticalStream=stopped (LED=off, mode=active 0x02, spread spectrum=enabled, COM capture=stopped)";
return true;
default:
throw new InvalidOperationException("Unsupported ALLY calibration activity.");
}
@@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Windows.Forms;
using Common;
using Config.Entities;
using log4net;
using TBF.Rig.Generic;
using TBF.Rig.GenericDevices;
using TBF.Rig.Sequences;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication;
using TBF.UiBridge;
namespace TBF.Rig.TestMethods.AllyCalibration
{
/// <summary>
/// ALLY equivalent of iPerlCommunicationSeq. Command execution remains in
/// AllyTestCorrections; this sequence owns only the modeless dialog lifecycle.
/// </summary>
public sealed class AllyCommunicationSeq : SequenceBase
{
private static readonly ILog log = LogManager.GetLogger(typeof(AllyCommunicationSeq));
private Form modelessDialog;
private volatile bool dialogClosed;
private delegate void OpenFormDelegate(AllyCommunicationSeq sequence, SmartComponentBase method, Test test, ITestParams parameters);
private void OpenForm(AllyCommunicationSeq sequence, SmartComponentBase method, Test test, ITestParams parameters)
{
sequence.modelessDialog = new SmartCommunicationForm(method, test, parameters);
sequence.modelessDialog.FormClosed += delegate
{
sequence.dialogClosed = true;
log.Info("ALLY_SMART_COMM_FORM_CLOSED_SIGNAL: FormClosed event received.");
};
sequence.modelessDialog.Show();
}
public IList<Event> Execute(Test test, int repetitionNr, SmartComponentBase method, ITestParams parameters)
{
checkUiOp = new Operations.CheckUIOp(true);
modelessDialog = null;
dialogClosed = false;
Program.MainWnd.Invoke(new OpenFormDelegate(OpenForm), new object[] { this, method, test, parameters });
Bridge.OnActivity(this, TBF.Resources.Strings.Meter_Communication_in_progress);
bool stopPressed;
bool completed;
IList<Event> events;
State.Create("AllyCommunicationSeq : Wait until the SmartCommunicationForm is closed")
.AddOperation(checkUiOp)
.EnterState();
do
{
events = StateMachine.WaitRunDevsRunOps();
stopPressed = TestAndLogUiCmdStop(test, events);
completed = dialogClosed ||
(modelessDialog is IHasCompleted && ((IHasCompleted)modelessDialog).Completed);
} while (!stopPressed && !completed);
if (stopPressed)
{
Bridge.OnCloseModelessForm(this, null);
return new List<Event> { Event.UiCmdStop };
}
Bridge.OnTestProgress(this, new TestProgressEventArgs(test.Name, Progress.Completed));
log.InfoFormat("ALLY_SMART_COMM_SEQUENCE_COMPLETED: test='{0}', formClosed={1}",
test == null ? "<null>" : test.Name, dialogClosed);
modelessDialog = null;
return new List<Event> { Event.Done };
}
}
}
@@ -6,14 +6,16 @@ using log4net;
using TBF.Rig.Generic;
using TBF.Rig.GenericDevices;
using TBF.Rig.Sequences;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication;
using TBF.UiBridge;
namespace TBF.Rig.TestMethods.AllyCalibration
{
public class TestMethod : ComponentBase, ISimultTestMethod, ITestMethodSmart
public class TestMethod : SmartComponentBase, ISimultTestMethod, ITestMethodSmart
{
private static readonly ILog log = LogManager.GetLogger(typeof(TestMethod));
private readonly TestMethodCfg allyCfg;
private readonly bool[] meterCommunicationMilestones = new bool[32];
public TestMethod()
{
@@ -72,15 +74,45 @@ namespace TBF.Rig.TestMethods.AllyCalibration
log.FatalFormat("{0} initialized: {1}", Name, this);
}
public override void MeterCommMilestone(int iItem, bool value)
{
if (iItem >= 0 && iItem < meterCommunicationMilestones.Length)
meterCommunicationMilestones[iItem] = value;
}
public override bool IsMeterCommMilestone(int iItem)
{
return iItem >= 0 && iItem < meterCommunicationMilestones.Length && meterCommunicationMilestones[iItem];
}
public IList<Event> Execute(Test test, int repetitionNr, bool isLastRepetition)
{
TestMethodParams parameters = allyCfg.TestParams as TestMethodParams;
log.InfoFormat(
"ALLY_TEST_EXECUTE: test='{0}', repetition={1}, last={2}, debugMode={3}, activity='{4}', parametersLoaded={5}",
test == null ? "<null>" : test.Name,
repetitionNr,
isLastRepetition,
DebugLevel,
parameters == null ? "<null>" : parameters.Activity,
parameters != null);
if (parameters == null)
{
log.ErrorFormat(
"ALLY_TEST_NOT_STARTED: test='{0}' cannot execute because the runtime TestParams provider is null. " +
"No ALLY command, correction adapter, or SmartCommunicationForm can be started.",
test == null ? "<null>" : test.Name);
throw new InvalidOperationException("ALLY calibration test parameters are missing.");
}
if (DebugLevel == DebugMode.Normal)
return new AllyCalibrationSeq().Execute(test, repetitionNr, this, allyCfg, parameters);
{
log.Info("ALLY_TEST_ROUTE: opening SmartCommunicationForm through AllyCommunicationSeq and AllyTestCorrections.");
return new AllyCommunicationSeq().Execute(test, repetitionNr, this, parameters);
}
log.Info("ALLY_TEST_ROUTE: simulation branch selected; no physical ALLY command will be sent and no Optical heads dialog will be opened.");
new AllyCalibrationSeq().MakeSimulatedTrivial(test, repetitionNr, test.Part);
Bridge.OnTestProgress(this, new TestProgressEventArgs(test, 1, Progress.Completed));
Bridge.OnTestCompleted(
@@ -1,13 +1,18 @@
using System.Collections.Generic;
using System.IO;
using System.IO.Ports;
using System.Xml.Serialization;
using Common;
using Config.Entities;
using TBF.Rig.Generic;
using TBF.Rig.RegisterReaders.CommonRR.IPerl;
using SmartTestParams = TBF.Rig.Generic.ITestParams;
namespace TBF.Rig.TestMethods.AllyCalibration
{
public class TestMethodCfg : ComponentCfgBase, IComponentCfg, IParamsProvider
// Implement the common smart-meter configuration contract so the ALLY test can
// use SmartCommunicationForm in exactly the same way as the ASIC test method.
public class TestMethodCfg : ComponentCfgBase, ITestMethodCfg, IParamsProvider
{
public static readonly XmlSerializer Serializer =
XmlSerializer.FromTypes(new[] { typeof(TestMethodCfg) })[0];
@@ -18,11 +23,31 @@ namespace TBF.Rig.TestMethods.AllyCalibration
public string ExpectedDeviceType;
public string ExpectedFirmwareVersion;
// The generic SmartCommunicationForm consumes these values through
// ITestMethodCfg. ALLY uses one serialized command worker; the fields are
// kept separate from the ALLY-specific command timeout above.
public int DelayBetweenRetries { get; set; }
public int MaxCommRetries { get; set; }
public int WaitTimeAfterFailure { get; set; }
public int PassThroughWaitTime { get; set; }
public int NrThreads { get; set; }
public int CommTimeout { get; set; }
public int MciTimeoutMs { get; set; }
public int BaudRate { get; set; }
public int DataBits { get; set; }
public Parity ParityBit { get; set; }
public StopBits StopBits { get; set; }
public bool UseWebService { get; set; }
public string BaseUrl { get; set; }
public string RelativeUrl { get; set; }
[XmlIgnore]
public ITestParams TestParams { get; set; }
public SmartTestParams TestParams { get; set; }
private TestMethodCfg()
{
InitializeAll();
TestParams = new TestMethodParams(true);
}
public TestMethodCfg(IComponentFactory factory)
@@ -68,6 +93,20 @@ namespace TBF.Rig.TestMethods.AllyCalibration
DelayBetweenAttemptsMs = 250;
ExpectedDeviceType = "SWM003";
ExpectedFirmwareVersion = string.Empty;
DelayBetweenRetries = 250;
MaxCommRetries = 4;
WaitTimeAfterFailure = 0;
PassThroughWaitTime = 0;
NrThreads = 1;
CommTimeout = CommandTimeoutMs;
MciTimeoutMs = 5000;
BaudRate = 2400;
DataBits = 8;
ParityBit = Parity.None;
StopBits = StopBits.One;
UseWebService = false;
BaseUrl = string.Empty;
RelativeUrl = string.Empty;
}
public int ParamsCount()
@@ -179,7 +218,7 @@ namespace TBF.Rig.TestMethods.AllyCalibration
DelayBetweenAttemptsMs = DelayBetweenAttemptsMs,
ExpectedDeviceType = ExpectedDeviceType,
ExpectedFirmwareVersion = ExpectedFirmwareVersion,
TestParams = TestParams == null ? null : TestParams.Clone() as ITestParams
TestParams = TestParams == null ? null : TestParams.Clone() as SmartTestParams
};
}
}
@@ -9,7 +9,9 @@ using TBF.Resources;
namespace TBF.Rig.TestMethods.AllyCalibration
{
public class TestMethodParams : TestParamsBase, IParamsProvider, ITestParams
// Use the same parameter contract as SmartCommunicationForm explicitly.
// Config.Entities also exposes an ITestParams name in some builds.
public class TestMethodParams : TestParamsBase, IParamsProvider, TBF.Rig.Generic.ITestParams
{
public static readonly XmlSerializer Serializer =
XmlSerializer.FromTypes(new[] { typeof(TestMethodParams) })[0];
+35
View File
@@ -0,0 +1,35 @@
using System.Collections.Generic;
using TBF.Rig.Generic;
using LegacyTestMethodCfg = TBF.Rig.TestMethods.iPerlCommunication.TestMethodCfg;
namespace TBF.Rig.TestMethods.AsicTest
{
/// <summary>
/// Factory for the explicit ASIC test method. It retains the legacy XML
/// settings schema so existing communication parameters stay compatible.
/// </summary>
public class Factory : IComponentFactory
{
public string ClassName { get { return GetType().Namespace.Substring(8); } }
public IComponent DummyComponent()
{
return new TestMethod();
}
public IComponent GetComponent(IComponentCfg cfg, IList<IComponent> components)
{
return new TestMethod(cfg);
}
public IComponentCfg DefaultConfig()
{
return new LegacyTestMethodCfg(this);
}
public IComponentCfg CmpntCfgFromCmpntEntity(Config.Entities.Component component)
{
return ComponentCfgBase.CreateFromDbEntity(LegacyTestMethodCfg.Serializer, component, this);
}
}
}
@@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using Common;
using Config.Entities;
using log4net;
using TBF.Rig.Generic;
using TBF.Rig.Sequences;
using TBF.UiBridge;
using AsicCommunicationSequence = TBF.Rig.Sequences.iPerlCommunicationSeq;
using LegacyTestMethodCfg = TBF.Rig.TestMethods.iPerlCommunication.TestMethodCfg;
using LegacyTestParams = TBF.Rig.TestMethods.iPerlCommunication.iPerlCommunicationParams;
using SmartTestParams = TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.iPerlCommunicationParams;
namespace TBF.Rig.TestMethods.AsicTest
{
/// <summary>
/// ASIC-facing test method. It retains the proven iPerl protocol settings,
/// but executes through the shared smart-reader workflow.
/// </summary>
public class TestMethod : iPerlCommunication.TestMethod
{
private static readonly ILog log = LogManager.GetLogger(typeof(TestMethod));
public TestMethod()
{
}
public TestMethod(IComponentCfg cfg)
: base(cfg)
{
}
/// <summary>
/// Uses the multi-reader sequence, not the legacy iPerl dialog. The
/// sequence opens SmartCommunicationForm, which selects AsicCorrections
/// from the register readers in the active Meters Path.
/// </summary>
public override IList<Event> Execute(Test test, int repetitionNr, bool isLastRepetition)
{
LegacyTestMethodCfg config = Cfg as LegacyTestMethodCfg;
LegacyTestParams parameters = config == null ? null : config.TestParams as LegacyTestParams;
if (parameters == null)
{
throw new InvalidOperationException(
"ASIC test parameters are missing. Configure the ASIC test activity before starting the procedure.");
}
log.InfoFormat(
"ASIC_TEST_ROUTE: test='{0}', repetition={1}, last={2}, debugMode={3}, activity='{4}', route=SmartCommunicationForm/AsicCorrections",
test == null ? "<null>" : test.Name,
repetitionNr,
isLastRepetition,
DebugLevel,
parameters.Activity);
if (DebugLevel == DebugMode.Normal)
{
return new AsicCommunicationSequence().Execute(test, repetitionNr, this,
ToSmartCommunicationParams(parameters));
}
new AsicCommunicationSequence().MakeSimulatedTrivial(test, repetitionNr, test.Part);
Bridge.OnTestProgress(this, new TestProgressEventArgs(test, 1, Progress.Completed));
Bridge.OnTestCompleted(this,
new TestCompletedEventArgs(test.Name,
ProcessData.BatchRslts.GetTestRslt(Common.Utils.GetTestName(test.Name, 1, 1), 0)));
return new List<Event> { Event.Done };
}
private static SmartTestParams ToSmartCommunicationParams(LegacyTestParams parameters)
{
// The persisted AsicTest configuration intentionally retains the
// legacy iPerl parameter type. SmartCommunicationForm uses its own
// equivalent type, so copy the three persisted values at the route
// boundary instead of changing existing procedure rows.
return new SmartTestParams
{
Activity = parameters.Activity,
SimultWithPrevious = parameters.SimultWithPrevious,
SimultWithNext = parameters.SimultWithNext
};
}
}
}
@@ -107,7 +107,7 @@ namespace TBF.Rig.TestMethods.iPerlCommunication
}
}
public IList<Event> Execute(Test test, int repetNr, bool isLastRepetition)
public virtual IList<Event> Execute(Test test, int repetNr, bool isLastRepetition)
{
if (DebugLevel == DebugMode.Normal)
{
@@ -190,8 +190,16 @@ namespace TBF.Rig.TestMethods.iPerlCommunication.iPerlHead
///
public int WMPulses { get { return wmPulses; } }
public int WMRefPulses { get { return wmRefPulses; } }
public double BeginWMState { get { return ResolveNaNDouble(beginWMState); } }
public double EndWMState { get { return ResolveNaNDouble(endWMState); } }
public double BeginWMState
{
get { return ResolveNaNDouble(beginWMState); }
set { beginWMState = value; }
}
public double EndWMState
{
get { return ResolveNaNDouble(endWMState); }
set { endWMState = value; }
}
public double WMVolume { get { return ResolveNaNDouble(wmVolume); } }
public double WMTestTime { get { return ResolveNaNDouble(wmTestTime); } }
@@ -35,7 +35,7 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
ThreadId,
WMNr0,
(Ihead != null) ? Ihead.Name : "null",
Wm.WMPosition,
Wm == null ? "null" : Wm.WMPosition.ToString(),
(CommMessage != null) ? CommMessage : "null",
CommErr);
}
@@ -17,6 +17,9 @@ using TBF.Rig.Generic;
using TBF.Rig.RegisterReaders.CommonRR.IPerl;
using TBF.Rig.RegisterReaders.iPerlReaderUNI;
using TBF.Rig.RegisterReaders.PoseidonReader;
using AllyMeterReader = TBF.Rig.RegisterReaders.AllyReader.AllyMeterReader;
using AsicReader = TBF.Rig.RegisterReaders.AsicReader.AsicReader;
using IPerlASICSmartReader = TBF.Rig.RegisterReaders.iPerlASICReader.implementations.SmartReader;
using TBF.Rig.Sequences;
using TBF.Rig.TestMethods.iPerlCommunication.iPerlHead;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.common;
@@ -67,6 +70,13 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
public bool Completed { get { return formCompleted; } }
bool formCompleted;
// A communication worker can finish before this modeless form receives
// its first paint message (most noticeably with one meter). Keep the
// result visible briefly before closing the form automatically.
private bool closeAfterResultRequested;
private System.Windows.Forms.Timer closeAfterResultTimer;
private EventHandler closeAfterResultShownHandler;
bool forcedClose; /// Set in the forced close handler
@@ -98,15 +108,22 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
public CheckBoxImage[] CheckBoxes { get => this.checkBoxes; }
public int[] CkbIndex { get => SmartCommunicationForm.ckbIndex; }
public bool[] CkbState { get => SmartCommunicationForm.ckbState; }
public IList<int> WaterMeterPositions0 { get => SmartCommunicationForm.waterMeterPositions0; }
///
/// RFID multiplexer PCB / RFID serial port and worker thread related variables
///
private static List<ICorrections> _corrections;
// Setting the initial combobox item raises SelectedIndexChanged before the
// constructor has finished preparing the form and its local settings.
private bool initializingMeterTypeItems;
private static ICorrections Correction
{
get
{
if (_corrections == null || _corrections.Count == 0)
return null;
if (string.IsNullOrEmpty(SelectedTypeReader) && _corrections.Count > 0)
{
return _corrections.First();
@@ -139,10 +156,22 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
{
List<ICorrections> correctionsList = new List<ICorrections>();
foreach (var smartHead in ProcessData.SmartHeadsUni)
// In a test use the reader family from the selected Meters Path. The
// global list can contain iPerl, ASIC and ALLY readers at the same time.
IEnumerable<ISmartReader> smartHeads = ProcessData.RegisterReaders == null
? ProcessData.SmartHeadsUni
: ProcessData.RegisterReaders.OfType<ISmartReader>();
foreach (var smartHead in smartHeads)
{
try{
if (smartHead is IperlHead iperlHead)
if (smartHead is AsicReader)
{
if (correctionsList.Any(x => x is AsicCorrections))
continue;
correctionsList.Add(new AsicCorrections(this, componentBase, cfg, tests, multiTestParams));
continue;
}
if (smartHead is IperlHead iperlHead)
{
if (correctionsList.Any(x => x is IPerlCorrections))
continue;
@@ -150,15 +179,31 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
continue;
}
if (smartHead is SmartReader smartReader)
{
if (correctionsList.Any(x => x is SmartReader))
continue;
correctionsList.Add(new PoseidonCorrections(this,log, rfidDataLogger, componentBase, cfg, tests, multiTestParams));
continue;
}
if (smartHead is IPerlASICSmartReader)
{
if (correctionsList.Any(x => x is IPerlASICCorrections))
continue;
correctionsList.Add(new IPerlASICCorrections(this, componentBase, cfg, tests, multiTestParams));
continue;
}
throw new Exception("Unknown smart head type");
if (smartHead is AllyMeterReader)
{
if (correctionsList.Any(x => x is AllyTestCorrections))
continue;
correctionsList.Add(new AllyTestCorrections(this, componentBase, cfg, tests, multiTestParams));
continue;
}
if (smartHead is SmartReader smartReader)
{
if (correctionsList.Any(x => x is PoseidonCorrections))
continue;
correctionsList.Add(new PoseidonCorrections(this,log, rfidDataLogger, componentBase, cfg, tests, multiTestParams));
continue;
}
throw new Exception("Unknown smart head type");
}
catch (Exception e)
{
@@ -169,68 +214,39 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
}
private static List<ICorrections> GetNewCorrectionList(SmartCommunicationForm form)
{
List<ICorrections> correctionsList = new List<ICorrections>();
// Manual communication is extended by registering a correction adapter here.
// An adapter is included only when at least one configured reader belongs to it.
// This keeps unsupported protocols out of the type selector and context menu.
List<ICorrections> candidates = new List<ICorrections>
{
new AsicCorrections(form),
new IPerlCorrections(form),
new IPerlASICCorrections(form),
new GenesisCorrections(form),
new AllyCorrections(form),
new PoseidonCorrections(form)
};
foreach (var smartHead in ProcessData.SmartHeadsUni)
{
try{
if (smartHead is IperlHead iperlHead)
{
if (correctionsList.Any(x => x is IPerlCorrections))
continue;
correctionsList.Add(new IPerlCorrections(form));
continue;
}
if (ProcessData.SmartHeadsUni == null)
return new List<ICorrections>();
if (smartHead is SmartReader smartReader)
{
if (correctionsList.Any(x => x is SmartReader))
continue;
correctionsList.Add(new PoseidonCorrections(form));
continue;
}
throw new Exception("Unknown smart head type");
}
catch (Exception e)
{
log.Error("IdentifyReaderTypes()", e);
}
}
return correctionsList;
return candidates
.Where(correction => ProcessData.SmartHeadsUni.Any(correction.IsFamilyOfSmartReader))
.ToList();
}
public static List<string> IdentifyReaderTypes()
{
List<string> typeReaders = new List<string>();
foreach (var smartHead in ProcessData.SmartHeadsUni)
{
try
{
if (typeReaders.Contains(smartHead.ClassName))
continue;
if (_corrections == null || ProcessData.SmartHeadsUni == null)
return typeReaders;
if (smartHead is IperlHead iperlHead)
{
typeReaders.Add(iperlHead.ClassName);
continue;
}
if (smartHead is SmartReader smartReader)
{
typeReaders.Add(smartReader.ClassName);
continue;
}
throw new Exception("Unknown smart head type");
}
catch (Exception e)
{
log.Error("IdentifyReaderTypes()", e);
}
}
foreach (ICorrections correction in _corrections)
{
if (ProcessData.SmartHeadsUni.Any(correction.IsFamilyOfSmartReader))
typeReaders.Add(correction.TypeIdentificatorName());
}
return typeReaders;
}
@@ -276,9 +292,42 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
/// <summary> Parameterless constructor (without watermeters, threads) </summary>
public SmartCommunicationForm()
{
InitializeComponent();
InitializeComponent();
this.Icon = Properties.Resources.TBF_icon;
log.Info("SMART_COMM_FORM_CREATED");
Shown += (sender, args) =>
{
log.Info("SMART_COMM_FORM_OPENED");
LogFormState("SHOWN");
};
FormClosed += (sender, args) =>
{
log.InfoFormat("SMART_COMM_FORM_CLOSED: forced={0}, completed={1}, delayedClose={2}",
forcedClose, formCompleted, closeAfterResultRequested);
LogFormState("CLOSED_STATE");
};
}
private void LogFormState(string stage)
{
string correctionNames = _corrections == null
? "<null>"
: string.Join(",", _corrections.Select(correction => correction.TypeIdentificatorName()));
string positions = waterMeterPositions0 == null
? "<null>"
: string.Join(",", waterMeterPositions0.Select(position => (position + 1).ToString()));
log.InfoFormat(
"SMART_COMM_FORM_{0}: editMode={1}, visible={2}, activity='{3}', selectedType='{4}', corrections=[{5}], heads={6}, positions=[{7}]",
stage,
checkBoxesEditMode,
Visible,
activityLabel == null ? "<uninitialized>" : activityLabel.Text,
SelectedTypeReader ?? "<null>",
correctionNames,
iperlHeads == null ? 0 : iperlHeads.Count,
positions);
}
/// <summary>
@@ -323,28 +372,40 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
/// </summary>
/// <param name="waterMetersCount">Number of text boxes for serial numbers</param>
public SmartCommunicationForm(Generic.IComponent componentBase, IList<Test> tests, IList<ITestParams> multiTestParams)
: this()
: this()
{
checkBoxesEditMode = false;
ITestMethodCfg cfg = (componentBase as ITestMethodCfg);
ISmartTestMethod smartTestMethod = componentBase as ISmartTestMethod;
// Resolve the Meters Path before selecting a correction adapter. This is
// essential on mixed benches: the ALLY test must not select an iPerl
// adapter merely because an iPerl reader exists elsewhere on the bench.
if (tests != null && tests.Count > 0)
{
ProcessData.RegisterReaders = StateMachine.GetMetersPath(tests[0]).RegisterReaders;
}
//TODO get corrections based on defined meter
_corrections = GetNewCorrectionList(smartTestMethod, smartTestMethod.TestMethodCfg, tests, multiTestParams);
InitializeMeterTypeItems();
UpdateHeads();
LogFormState("CONSTRUCTOR_HEADS_READY");
//_corrections = new PoseidonCorrections(this, log, rfidDataLogger, componentBase as ISmartTestMethod,componentBase.Cfg as TestMethodCfg,tests,multiTestParams);
if (multiTestParams.Count > 0)
{
ProcessData.RegisterReaders = StateMachine.GetMetersPath(tests[0]).RegisterReaders;
activityLabel.Text = multiTestParams[0].Activity;
activityLabel.Text = multiTestParams[0] == null
? string.Empty
: multiTestParams[0].Activity ?? string.Empty;
foreach (var p in multiTestParams)
{
if (p.Activity.ToLower() == iPerlCommunicationConstants.GetDefaultQ2CorrectionsStr.ToLower())
string activity = p == null ? string.Empty : p.Activity ?? string.Empty;
if (string.Equals(activity, iPerlCommunicationConstants.GetDefaultQ2CorrectionsStr,
StringComparison.OrdinalIgnoreCase))
{
/// Reset IsQ2PreCorrectionCalculated that is used for synchronization/to prevent double REST call
ProcessData.IsQ2PreCorrectionCalculated = false;
@@ -376,15 +437,25 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
else DoOnAllCompleted(sender, args);
};
formCompleted = false;
log.Info("SMART_COMM_FORM_CONSTRUCTOR_FORCE_CLOSE_HANDLER_START");
StartForceCloseHandler();
log.Info("SMART_COMM_FORM_CONSTRUCTOR_FORCE_CLOSE_HANDLER_READY");
log.Info("SMART_COMM_FORM_CONSTRUCTOR_WATER_METER_DATA_START");
InitializeWaterMeterData();
LogFormState("CONSTRUCTOR_WATER_METER_DATA_READY");
}
private void UpdateHeads()
{
log.InfoFormat(
"SMART_COMM_FORM_UPDATE_HEADS_START: selectedType='{0}', configuredSmartHeads={1}, corrections={2}",
SelectedTypeReader ?? "<null>",
ProcessData.SmartHeadsUni == null ? 0 : ProcessData.SmartHeadsUni.Count,
_corrections == null ? 0 : _corrections.Count);
if (!(waterMeterPositions0 == null || waterMeterPositions0.Count <= 0)
&& labels != null && counters != null && messages != null && checkBoxes != null)
{
@@ -411,33 +482,27 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
waterMeterPositions0?.Clear();
if (waterMeterPositions0 == null) waterMeterPositions0 = new List<int>();
//iperlHeads = ProcessData.SmartHeadsUni;
string comparedTypeReader = SelectedTypeReader;
ICorrections corre = null;
if (string.IsNullOrEmpty(SelectedTypeReader))
{
comparedTypeReader = ProcessData.SmartHeadsUni?.First()?.GetType().Name;
ISmartReader firstReader = ProcessData.SmartHeadsUni?.FirstOrDefault();
corre = _corrections.FirstOrDefault(correction => correction.IsFamilyOfSmartReader(firstReader));
}
ICorrections corre = null;
foreach (ICorrections correction in _corrections)
else
{
if (correction.TypeIdentificatorName() == SelectedTypeReader)
{
corre = correction;
break;
}
corre = _corrections.FirstOrDefault(
correction => correction.TypeIdentificatorName() == SelectedTypeReader);
}
if (corre != null)
{
int wmPos = 0;
foreach (var smartHead in ProcessData.SmartHeadsUni)
for (int wmPos = 0; wmPos < ProcessData.SmartHeadsUni.Count; wmPos++)
{
ISmartReader smartHead = ProcessData.SmartHeadsUni[wmPos];
if (corre.IsFamilyOfSmartReader(smartHead))
{
iperlHeads.Add(smartHead);
waterMeterPositions0.Add(wmPos);
wmPos++;
if (wmPos >= ProcessData.WMsCount) break;
}
}
@@ -448,45 +513,70 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
WaterMetersCount = iperlHeads.Count;
ShuffleTextBoxes(WaterMetersCount, ProcessData.LineSize);
this.ContextMenu = Correction.GetContextMenu();
Correction.PrepareForTestsActivities(WaterMetersCount);
this.ContextMenu = corre.GetContextMenu();
corre.PrepareForTestsActivities(WaterMetersCount);
}
else
{
this.ContextMenu = null;
}
}
log.InfoFormat(
"SMART_COMM_FORM_UPDATE_HEADS_COMPLETED: selectedType='{0}', correction='{1}', displayedHeads={2}, positions=[{3}]",
SelectedTypeReader ?? "<null>",
corre == null ? "<none>" : corre.TypeIdentificatorName(),
iperlHeads.Count,
string.Join(",", waterMeterPositions0.Select(position => (position + 1).ToString())));
}
private void InitializeMeterTypeItems()
{
if (_corrections.Count >= 0)
{
meterTypeComboBox.Items.Clear();
int iItem = 0;
foreach (ICorrections correction in _corrections)
{
string name = correction?.TypeIdentificatorName();
if (string.IsNullOrEmpty(name))
{
name = iItem.ToString();
}
meterTypeComboBox.Items.Add(name);
if (iItem == 0)
SelectedTypeReader = name;
iItem++;
}
meterTypeComboBox.Visible = true;
meterTypeComboBox.Enabled = true;
meterTypeComboBox.SelectedIndex = 0;
}
}
private void InitializeMeterTypeItems()
{
if (_corrections != null && _corrections.Count > 0)
{
initializingMeterTypeItems = true;
try
{
meterTypeComboBox.Items.Clear();
int iItem = 0;
foreach (ICorrections correction in _corrections)
{
string name = correction?.TypeIdentificatorName();
if (string.IsNullOrEmpty(name))
{
name = iItem.ToString();
}
meterTypeComboBox.Items.Add(name);
if (iItem == 0)
SelectedTypeReader = name;
iItem++;
}
meterTypeComboBox.Visible = true;
meterTypeComboBox.Enabled = true;
meterTypeComboBox.SelectedIndex = 0;
}
finally
{
initializingMeterTypeItems = false;
}
}
}
private void InitializeWaterMeterData()
{
InitializeSmartReaderLists();
if (iperlHeads.Count <= 0)
if (checkBoxesEditMode)
{
// In manual mode the selected family must determine the displayed heads.
// Do not preload all smart readers and overwrite the combobox selection.
UpdateHeads();
}
else
{
InitializeSmartReaderLists();
if (iperlHeads.Count <= 0)
UpdateHeads();
}
WaterMetersCount = iperlHeads.Count;
ShuffleTextBoxes(WaterMetersCount, ProcessData.LineSize);
@@ -530,7 +620,9 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
private void DoOnCommCompleted(object sender, CommCompletedEventArgs data)
{
log.InfoFormat("SMART_COMM_FORM_COMM_COMPLETED: {0}", data == null ? "<null>" : data.ToString());
Correction.DoOnCommCompleted(sender, data, waterMeterPositions0);
LogFormState("COMM_TEXT_UPDATED");
}
@@ -662,6 +754,7 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
private void SmartCommunicationForm_Load(object sender, EventArgs e)
{
Localize();
LogFormState("LOAD_START");
if (checkBoxesEditMode)
{
@@ -674,19 +767,47 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
samplePictureBox3.Visible = false;
}
if (_corrections == null || _corrections.Count == 0 || Correction == null)
{
ContextMenu = null;
log.Warn("SmartCommunicationForm opened without a registered manual communication adapter.");
return;
}
if (_corrections.Count > 0)//enable combo for choose SmartMeter
{
meterTypeComboBox.Visible = true;
}
Correction.Load(labels,counters,messages,checkBoxes,ckbIndex,ckbState, iperlHeads,textBoxesCount,checkBoxesEditMode );
LogFormState("CONTROLS_LOADED");
///
/// Set location and checkbox states to values stored in local settings
///
TBF.LocalSettings ls = Program.LocalSettings;
Left = (ls.iPerlCommunicationsFormLeft != 0) ? ls.iPerlCommunicationsFormLeft : 150;
Top = (ls.iPerlCommunicationsFormTop != 0) ? ls.iPerlCommunicationsFormTop : 150;
SetCheckBoxStates(ls.OptoHeadsEnabled);
if (ls == null)
{
log.Warn("SmartCommunicationForm loaded before LocalSettings were initialized; using default window and checkbox state.");
Left = 150;
Top = 150;
SetCurrentFamilySelectionMask(SmartReaderSelection.GetMask(null, iperlHeads));
}
else
{
Left = (ls.iPerlCommunicationsFormLeft != 0) ? ls.iPerlCommunicationsFormLeft : 150;
Top = (ls.iPerlCommunicationsFormTop != 0) ? ls.iPerlCommunicationsFormTop : 150;
SmartReaderSelection.EnsureConfiguredReaders(ls, ProcessData.SmartHeadsUni);
long selectionMask = SmartReaderSelection.GetMask(ls, iperlHeads);
SetCurrentFamilySelectionMask(selectionMask);
LogFamilySelection("LOADED", selectionMask);
}
int selectedHeads = checkBoxes == null ? 0 : checkBoxes.Take(Math.Min(textBoxesCount, iperlHeads.Count)).Count(checkBox => checkBox.Checked);
log.InfoFormat(
"SMART_COMM_FORM_SELECTION_APPLIED: selectedHeads={0}/{1}, activity='{2}'",
selectedHeads,
iperlHeads.Count,
activityLabel.Text);
if (!checkBoxesEditMode)
{
@@ -725,6 +846,8 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
void OnForceClose(object sender, EventArgs args)
{
LogFormState("FORCE_CLOSE_REQUESTED");
CancelCloseAfterResult();
CommCompletedHandler = null;
AllCompletedHandler = null;
@@ -769,6 +892,7 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
void DoOnAllCompleted(object sender, AllCompletedEventArgs data)
{
Text = data.CommMessage;
log.InfoFormat("SMART_COMM_FORM_ALL_COMPLETED: message='{0}'", data == null ? "<null>" : data.CommMessage);
}
@@ -872,14 +996,78 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
}
}
/// <summary>
/// Applies a persisted family-local selection mask to the controls that
/// currently represent that family. GetMask/SetMask use ordinal reader
/// indices; ckbIndex is a layout/physical-position mapping and must not
/// be used as the persisted bit position when switching reader families.
/// </summary>
private void SetCurrentFamilySelectionMask(long state)
{
int count = Math.Min(textBoxesCount, iperlHeads == null ? 0 : iperlHeads.Count);
for (int localIndex = 0; localIndex < count; localIndex++)
{
bool enabled = localIndex < 63 && (state & (1L << localIndex)) != 0L;
checkBoxes[localIndex].Checked = enabled;
int wmNr0 = ckbIndex[localIndex];
if (wmNr0 >= 0 && wmNr0 < ckbState.Length)
ckbState[wmNr0] = enabled;
}
}
private long GetCurrentFamilySelectionMask()
{
int count = Math.Min(textBoxesCount, iperlHeads == null ? 0 : iperlHeads.Count);
long result = 0L;
for (int localIndex = 0; localIndex < count && localIndex < 63; localIndex++)
{
if (checkBoxes[localIndex].Checked)
result |= 1L << localIndex;
}
return result;
}
private void LogFamilySelection(string phase, long selectionMask)
{
if (iperlHeads == null)
return;
string family = iperlHeads.Count == 0 ? "<none>" : SmartReaderSelection.GetFamilyKey(iperlHeads[0]);
string readers = string.Join(", ", iperlHeads.Select((reader, index) =>
string.Format("{0}:{1}={2}", index + 1, reader == null ? "<null>" : reader.Name,
index < 63 && (selectionMask & (1L << index)) != 0L ? "enabled" : "disabled")));
log.InfoFormat("SMART_COMM_FORM_SELECTION_{0}: family='{1}', mask=0x{2:X}, readers=[{3}]",
phase, family, selectionMask, readers);
}
private void saveButton_Click(object sender, EventArgs e)
{
Program.LocalSettings.OptoHeadsEnabled = GetCheckBoxStates();
Program.LocalSettings.Save();
SaveCurrentFamilySelection();
DialogResult = DialogResult.OK;
Close();
}
private void SaveCurrentFamilySelection()
{
if (Program.LocalSettings == null)
return;
long checkboxStates = GetCurrentFamilySelectionMask();
SmartReaderSelection.SetMask(Program.LocalSettings, iperlHeads, checkboxStates);
LogFamilySelection("SAVING", checkboxStates);
// The dedicated legacy iPerl/S640 dialogs still consume this
// bitmask. Do not overwrite it while editing another family.
if (iperlHeads != null && iperlHeads.Count > 0 &&
SmartReaderSelection.GetFamilyKey(iperlHeads[0]) == "iperl")
{
Program.LocalSettings.OptoHeadsEnabled = checkboxStates;
}
Program.LocalSettings.Save();
}
#endregion
private void SmartCommunicationForm_FormClosing(object sender, FormClosingEventArgs e)
@@ -887,40 +1075,128 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
if (!forcedClose && !formCompleted && !checkBoxesEditMode)
{
e.Cancel = true;
log.Info("SMART_COMM_FORM_CLOSE_BLOCKED: communication is still in progress.");
return;
}
log.InfoFormat("SMART_COMM_FORM_CLOSE_ALLOWED: forced={0}, completed={1}, editMode={2}",
forcedClose, formCompleted, checkBoxesEditMode);
}
public void NormalClose()
{
LogFormState("NORMAL_CLOSE_REQUESTED");
CancelCloseAfterResult();
CommCompletedHandler = null;
AllCompletedHandler = null;
Correction?.NormalClose(waterMeterPositions0);
long checkboxStates = GetCheckBoxStates();
long checkboxStates = GetCurrentFamilySelectionMask();
if (Program.LocalSettings.iPerlCommunicationsFormLeft != Location.X ||
Program.LocalSettings.iPerlCommunicationsFormTop != Location.Y ||
Program.LocalSettings.OptoHeadsEnabled != checkboxStates)
Program.LocalSettings.iPerlCommunicationsFormTop != Location.Y)
{
/// Update local settings
Program.LocalSettings.iPerlCommunicationsFormLeft = Location.X;
Program.LocalSettings.iPerlCommunicationsFormTop = Location.Y;
Program.LocalSettings.OptoHeadsEnabled = checkboxStates;
Program.LocalSettings.Save();
}
SmartReaderSelection.SetMask(Program.LocalSettings, iperlHeads, checkboxStates);
LogFamilySelection("CLOSING", checkboxStates);
if (iperlHeads != null && iperlHeads.Count > 0 &&
SmartReaderSelection.GetFamilyKey(iperlHeads[0]) == "iperl")
{
Program.LocalSettings.OptoHeadsEnabled = checkboxStates;
}
Program.LocalSettings.Save();
formCompleted = true;
DialogResult = DialogResult.OK;
Close();
}
/// <summary>
/// Closes the modeless communication form after the result has been
/// visible for <paramref name="delayMs"/>. If a worker completes while
/// the form is still being constructed, the delay begins after Shown.
/// </summary>
public void CloseAfterResult(int delayMs)
{
if (formCompleted || IsDisposed || closeAfterResultRequested)
{
log.InfoFormat("SMART_COMM_FORM_CLOSE_DELAY_IGNORED: completed={0}, disposed={1}, alreadyRequested={2}",
formCompleted, IsDisposed, closeAfterResultRequested);
return;
}
closeAfterResultRequested = true;
delayMs = Math.Max(1, delayMs);
log.InfoFormat("SMART_COMM_FORM_CLOSE_DELAY_REQUESTED: delayMs={0}, visible={1}", delayMs, Visible);
if (!Visible)
{
log.InfoFormat("SMART_COMM_FORM_CLOSE_DELAY_WAITING_FOR_SHOW: delayMs={0}", delayMs);
closeAfterResultShownHandler = null;
closeAfterResultShownHandler = delegate
{
if (closeAfterResultShownHandler != null)
Shown -= closeAfterResultShownHandler;
closeAfterResultShownHandler = null;
StartCloseAfterResultTimer(delayMs);
};
Shown += closeAfterResultShownHandler;
return;
}
StartCloseAfterResultTimer(delayMs);
}
private void StartCloseAfterResultTimer(int delayMs)
{
if (formCompleted || IsDisposed)
return;
closeAfterResultTimer = new System.Windows.Forms.Timer { Interval = Math.Max(1, delayMs) };
closeAfterResultTimer.Tick += delegate
{
CancelCloseAfterResultTimer();
log.Info("SMART_COMM_FORM_CLOSE_DELAY_ELAPSED");
NormalClose();
};
log.InfoFormat("SMART_COMM_FORM_CLOSE_DELAY_SCHEDULED: delayMs={0}, visible={1}", delayMs, Visible);
closeAfterResultTimer.Start();
}
private void CancelCloseAfterResult()
{
if (closeAfterResultShownHandler != null)
{
Shown -= closeAfterResultShownHandler;
closeAfterResultShownHandler = null;
}
CancelCloseAfterResultTimer();
}
private void CancelCloseAfterResultTimer()
{
if (closeAfterResultTimer == null)
return;
log.Info("SMART_COMM_FORM_CLOSE_DELAY_CANCELLED");
closeAfterResultTimer.Stop();
closeAfterResultTimer.Dispose();
closeAfterResultTimer = null;
}
private void meterTypeComboBox_SelectedIndexChanged(object sender, EventArgs e)
{
ComboBox senderCombo = sender as ComboBox;
if (initializingMeterTypeItems) return;
ComboBox senderCombo = sender as ComboBox;
if (senderCombo == null) return;
SaveCurrentFamilySelection();
SelectedTypeReader = senderCombo.SelectedItem?.ToString();
UpdateHeads();
SmartCommunicationForm_Load(this, EventArgs.Empty);
@@ -0,0 +1,183 @@
using System;
using System.Collections.Generic;
using System.Linq;
using TBF.Rig.GenericDevices;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.common;
using AsicReader = TBF.Rig.RegisterReaders.AsicReader.AsicReader;
namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
{
/// <summary>
/// Resolves persisted smart-reader selections. The visual dialog may have a
/// fixed number of controls, but the persisted model contains only configured
/// readers in each family.
/// </summary>
public static class SmartReaderSelection
{
private const string LegacyIPerlFamilyKey = "iperl";
public static void EnsureConfiguredReaders(LocalSettings settings, IEnumerable<ISmartReader> readers)
{
if (settings == null || readers == null)
return;
List<ISmartReader> configuredReaders = readers.Where(reader => reader != null).ToList();
if (configuredReaders.Count == 0)
return;
SmartReaderSelectionSettings selections = GetOrCreateSettings(settings);
bool migrateLegacyMask = !selections.LegacyOptoHeadsEnabledMigrated;
string legacyFamily = configuredReaders.Any(reader => GetFamilyKey(reader) == LegacyIPerlFamilyKey)
? LegacyIPerlFamilyKey
: GetFamilyKey(configuredReaders[0]);
foreach (IGrouping<string, ISmartReader> family in configuredReaders.GroupBy(GetFamilyKey))
{
SmartReaderFamilySelection storedFamily = GetOrCreateFamily(selections, family.Key);
int ordinal = 0;
foreach (ISmartReader reader in family)
{
SmartReaderSelectionItem item = storedFamily.Readers
.FirstOrDefault(candidate => candidate.ReaderKey == GetReaderKey(reader));
if (item == null)
{
item = new SmartReaderSelectionItem
{
ReaderKey = GetReaderKey(reader),
Enabled = !migrateLegacyMask || family.Key != legacyFamily ||
IsLegacyMaskEnabled(settings.OptoHeadsEnabled, reader, ordinal)
};
storedFamily.Readers.Add(item);
}
ordinal++;
}
}
if (migrateLegacyMask)
selections.LegacyOptoHeadsEnabledMigrated = true;
}
public static long GetMask(LocalSettings settings, IEnumerable<ISmartReader> readers)
{
List<ISmartReader> familyReaders = ToReaderList(readers);
if (familyReaders.Count == 0)
return 0L;
EnsureConfiguredReaders(settings, familyReaders);
long result = 0L;
for (int index = 0; index < familyReaders.Count && index < 63; index++)
{
if (IsEnabled(settings, familyReaders[index]))
result |= 1L << index;
}
return result;
}
public static void SetMask(LocalSettings settings, IEnumerable<ISmartReader> readers, long state)
{
List<ISmartReader> familyReaders = ToReaderList(readers);
if (settings == null || familyReaders.Count == 0)
return;
EnsureConfiguredReaders(settings, familyReaders);
for (int index = 0; index < familyReaders.Count; index++)
SetEnabled(settings, familyReaders[index], index < 63 && (state & (1L << index)) != 0L);
}
public static bool IsEnabled(LocalSettings settings, IRegReader reader)
{
if (settings == null || reader == null)
return true;
SmartReaderSelectionSettings selections = settings.SmartReaderSelections;
if (selections == null)
return true;
SmartReaderFamilySelection family = selections.Families == null
? null
: selections.Families.FirstOrDefault(item => item.FamilyKey == GetFamilyKey(reader));
SmartReaderSelectionItem item = family == null || family.Readers == null
? null
: family.Readers.FirstOrDefault(candidate => candidate.ReaderKey == GetReaderKey(reader));
// A newly configured reader is enabled until the user explicitly
// changes it in SmartCommunicationForm.
return item == null || item.Enabled;
}
public static string GetFamilyKey(IRegReader reader)
{
// Prefer the concrete extracted reader type. The string fallback
// below is retained for persisted/proxy-backed legacy readers.
if (reader is AsicReader)
return "asic";
string typeName = reader == null ? string.Empty : reader.GetType().FullName ?? string.Empty;
if (typeName.IndexOf(".AllyReader.", StringComparison.OrdinalIgnoreCase) >= 0)
return "ally";
if (typeName.IndexOf(".GenesisRegReader.", StringComparison.OrdinalIgnoreCase) >= 0)
return "genesis";
if (typeName.IndexOf(".AsicReader.", StringComparison.OrdinalIgnoreCase) >= 0)
return "asic";
if (typeName.IndexOf(".iPerlASICReader.", StringComparison.OrdinalIgnoreCase) >= 0)
return "iperl-asic";
if (typeName.IndexOf(".Poseidon", StringComparison.OrdinalIgnoreCase) >= 0)
return "poseidon";
return LegacyIPerlFamilyKey;
}
public static string GetReaderKey(IRegReader reader)
{
if (reader == null)
return string.Empty;
if (!string.IsNullOrEmpty(reader.Name))
return reader.Name;
return string.Format("{0}#{1}", reader.GetType().FullName, reader.Position);
}
private static void SetEnabled(LocalSettings settings, IRegReader reader, bool enabled)
{
SmartReaderFamilySelection family = GetOrCreateFamily(GetOrCreateSettings(settings), GetFamilyKey(reader));
SmartReaderSelectionItem item = family.Readers.FirstOrDefault(candidate => candidate.ReaderKey == GetReaderKey(reader));
if (item == null)
{
item = new SmartReaderSelectionItem { ReaderKey = GetReaderKey(reader) };
family.Readers.Add(item);
}
item.Enabled = enabled;
}
private static SmartReaderSelectionSettings GetOrCreateSettings(LocalSettings settings)
{
if (settings.SmartReaderSelections == null)
settings.SmartReaderSelections = new SmartReaderSelectionSettings();
if (settings.SmartReaderSelections.Families == null)
settings.SmartReaderSelections.Families = new List<SmartReaderFamilySelection>();
return settings.SmartReaderSelections;
}
private static SmartReaderFamilySelection GetOrCreateFamily(SmartReaderSelectionSettings settings, string familyKey)
{
SmartReaderFamilySelection family = settings.Families.FirstOrDefault(item => item.FamilyKey == familyKey);
if (family == null)
{
family = new SmartReaderFamilySelection { FamilyKey = familyKey };
settings.Families.Add(family);
}
if (family.Readers == null)
family.Readers = new List<SmartReaderSelectionItem>();
return family;
}
private static bool IsLegacyMaskEnabled(long legacyMask, IRegReader reader, int ordinal)
{
int index = reader.Position > 0 && reader.Position <= 63 ? reader.Position - 1 : ordinal;
return index >= 0 && index < 63 && (legacyMask & (1L << index)) != 0L;
}
private static List<ISmartReader> ToReaderList(IEnumerable<ISmartReader> readers)
{
return readers == null ? new List<ISmartReader>() : readers.Where(reader => reader != null).ToList();
}
}
}
@@ -17,7 +17,17 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication
public static XmlSerializer Serializer = XmlSerializer.FromTypes(new[] { typeof(iPerlCommunicationParams) })[0];
public override XmlSerializer GetSerializer() { return Serializer; }
public string Activity; /// Communication activity
/// <summary>
/// Communication activity. Do not use a field here: callers commonly
/// receive this object through <see cref="ITestParams"/>, which reads the
/// inherited TestParamsBase.Activity property. A field would hide that
/// property and make the activity appear empty in SmartCommunicationForm.
/// </summary>
public string Activity
{
get { return base.Activity; }
set { base.Activity = value; }
}
public bool SimultWithPrevious;
public bool SimultWithNext;
@@ -0,0 +1,37 @@
using System.Collections.Generic;
using System.Windows.Forms;
using TBF.Rig.RegisterReaders.AllyReader;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.common;
namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
{
internal sealed class AllyCorrections : ManualSmartCorrectionsBase<AllyMeterReader>
{
private const int CommandTimeoutMs = 5000;
public AllyCorrections(SmartCommunicationForm parent) : base(parent) { }
public override string TypeIdentificatorName() => "ALLY";
public override bool IsFamilyOfSmartReader(ISmartReader smartHead) => smartHead is AllyMeterReader;
protected override IEnumerable<MenuItem> CreateMenuItems()
{
yield return new MenuItem("Read Serial Number") { Tag = "ReadSerialNumber" };
yield return new MenuItem("Read Version and Type") { Tag = "ReadVersion" };
yield return new MenuItem("Set RFID mode") { Tag = "SetRfid" };
yield return new MenuItem("Set NFC mode") { Tag = "SetNfc" };
}
protected override string ExecuteManualCommand(AllyMeterReader reader, string command)
{
switch (command)
{
case "ReadSerialNumber": return reader.ReadSerialNumber(CommandTimeoutMs);
case "ReadVersion": return reader.ReadVersionAndType(CommandTimeoutMs).ToString();
case "SetRfid": reader.SetRfidInterface(); return "OK";
case "SetNfc": reader.SetNfcInterface(); return "OK";
default: return "Unsupported ALLY operation";
}
}
}
}
@@ -0,0 +1,339 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Windows.Forms;
using Common;
using Config.Entities;
using log4net;
using Results.Entities;
using TBF.Rig.Generic;
using TBF.Rig.RegisterReaders.AllyReader;
using TBF.Rig.RegisterReaders.CommonRR.IPerl;
using TBF.Rig.Sequences;
using TBF.Rig.TestMethods.AllyCalibration;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.common;
using CheckBoxImage = TBF.Boxes.CheckBoxImage;
namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
{
/// <summary>
/// Test-time ALLY adapter for SmartCommunicationForm.
///
/// AllyCorrections intentionally remains the manual-command adapter. This class
/// runs configured ALLY test activities and reports every completed reader back
/// to the shared dialog, just like the iPerl ASIC correction adapter does.
/// </summary>
internal sealed class AllyTestCorrections : ICorrections
{
private ILog log { get { return ParentFrom == null ? null : ParentFrom.Log; } }
// Keep the result visible long enough to be verified by the operator.
// This is deliberately identical to the ASIC correction behaviour.
private const int ResultVisibilityDelayMs = 1500;
private readonly ISmartTestMethod testMethod;
private readonly ITestMethodCfg cfg;
private readonly IList<Test> tests;
private readonly IList<ITestParams> multiTestParams;
private readonly IList<Thread> workerThreads = new List<Thread>();
private volatile bool stopWorkerThreads;
private int completedActivities;
private int expectedActivities;
public AllyTestCorrections(SmartCommunicationForm parent, ISmartTestMethod testMethod,
ITestMethodCfg cfg, IList<Test> tests, IList<ITestParams> multiTestParams)
{
ParentFrom = parent;
this.testMethod = testMethod;
this.cfg = cfg;
this.tests = tests ?? new List<Test>();
this.multiTestParams = multiTestParams ?? new List<ITestParams>();
}
public string TypeIdentificatorName() { return "ALLY"; }
public DateTime StartTime { get; set; }
public int StartTimeSec { get; set; }
public SmartCommunicationForm ParentFrom { get; set; }
public ITestMethodCfg Cfg { get { return cfg; } set { } }
public IList<ITestParams> MultiTestParams { get { return multiTestParams; } }
public IList<Test> Tests { get { return tests; } set { } }
public IList<ISmartReader> iperlHeads { get { return ParentFrom == null ? null : ParentFrom.Heads; } }
public void PrepareForTestsActivities(int waterMetersCount)
{
StartTime = DateTime.Now;
StartTimeSec = StateMachine.Time;
stopWorkerThreads = false;
completedActivities = 0;
expectedActivities = Math.Max(0, waterMetersCount) * Math.Max(1, multiTestParams.Count);
workerThreads.Clear();
// ALLY command communication is serialized. A single command port must
// not be accessed concurrently even when more than one meter is shown.
workerThreads.Add(new Thread(Worker) { IsBackground = true, Name = "ALLY SmartCommunication worker" });
ParentFrom.Log.InfoFormat(
"ALLY_TEST_CORRECTIONS_PREPARE: readers={0}, activities={1}, expectedCallbacks={2}, workers=1",
waterMetersCount, multiTestParams.Count, expectedActivities);
}
public void Worker(object threadData)
{
int threadId = (threadData as TBF.Boxes.IntBox) == null ? 0 : (threadData as TBF.Boxes.IntBox).Val;
ParentFrom.Log.InfoFormat("ALLY_TEST_CORRECTIONS_WORKER_START: thread={0}, readers={1}, activities={2}",
threadId, iperlHeads == null ? 0 : iperlHeads.Count, multiTestParams.Count);
for (int activityIndex = 0; activityIndex < multiTestParams.Count && !stopWorkerThreads; activityIndex++)
{
Test currentTest = activityIndex < tests.Count ? tests[activityIndex] : null;
TestMethodParams parameters = multiTestParams[activityIndex] as TestMethodParams;
bool allPassed = true;
int processedReaders = 0;
if (parameters == null)
{
ParentFrom.Log.ErrorFormat("ALLY_TEST_CORRECTIONS_INVALID_PARAMS: activityIndex={0}", activityIndex);
continue;
}
TBF.UiBridge.Bridge.OnTestProgress(this,
new TBF.UiBridge.TestProgressEventArgs(currentTest, Progress.JustStarted));
for (int localIndex = 0; localIndex < iperlHeads.Count && !stopWorkerThreads; localIndex++)
{
AllyMeterReader reader = iperlHeads[localIndex] as AllyMeterReader;
int originalIndex = GetOriginalIndex(localIndex);
WaterMeter waterMeter = GetWaterMeter(originalIndex);
CommErr error = CommErr.None;
string message;
bool passed = false;
if (reader == null)
{
error = CommErr.WrongIPerlType;
message = "ALLY reader is not configured.";
}
else if (IsDisabled(localIndex, reader, waterMeter))
{
error = CommErr.HeadDisabledByUser;
message = "Disabled by user";
ParentFrom.Log.InfoFormat(
"ALLY_TEST_CORRECTIONS_SKIPPED_DISABLED: position={0}, selected={1}, readerDisabled={2}, waterMeterDisabled={3}",
originalIndex + 1,
IsSelected(localIndex),
reader.Disabled,
waterMeter != null && waterMeter.Disabled);
}
else
{
try
{
ParentFrom.Log.InfoFormat(
"ALLY_TEST_CORRECTIONS_COMMAND_START: test='{0}', position={1}, reader='{2}', activity='{3}'",
currentTest == null ? "<null>" : currentTest.Name,
originalIndex + 1,
reader.Name,
parameters.Activity);
passed = AllyCalibrationSeq.ExecuteWithRetries(reader, cfg as TestMethodCfg, parameters, out message);
message = reader.Name + ": " + message;
if (!passed)
error = CommErr.CommFailed;
}
catch (Exception exception)
{
error = CommErr.CommFailed;
message = reader.Name + ": " + exception.Message;
ParentFrom.Log.Error("ALLY_TEST_CORRECTIONS_COMMAND_FAILED", exception);
}
}
UpdateResult(currentTest, originalIndex, waterMeter, reader, parameters.Activity, passed);
processedReaders++;
allPassed &= passed;
ParentFrom.Log.InfoFormat(
"ALLY_TEST_CORRECTIONS_COMMAND_COMPLETED: test='{0}', position={1}, passed={2}, error={3}, result='{4}'",
currentTest == null ? "<null>" : currentTest.Name,
originalIndex + 1,
passed,
error,
message);
SmartCommunicationForm.OnCommCompleted(this,
new CommCompletedEventArgs(threadId, localIndex, reader, waterMeter, message, error));
}
CompleteTest(currentTest, processedReaders, allPassed);
}
ParentFrom.Log.InfoFormat("ALLY_TEST_CORRECTIONS_WORKER_COMPLETED: thread={0}", threadId);
}
public bool WorkerActivity(string currentActivity, ISmartReader iHead, WaterMeter wm, Test currentTest,
int wmNr0, ref CommErr error, ref string resultStr, bool[] ckbState, int threadID, int currentActivityStep)
{
AllyMeterReader reader = iHead as AllyMeterReader;
TestMethodParams parameters = currentActivityStep < multiTestParams.Count
? multiTestParams[currentActivityStep] as TestMethodParams
: null;
if (reader == null || parameters == null)
{
error = CommErr.WrongArguments;
resultStr = "ALLY reader or activity is missing.";
return false;
}
bool passed = AllyCalibrationSeq.ExecuteWithRetries(reader, cfg as TestMethodCfg, parameters, out resultStr);
error = passed ? CommErr.None : CommErr.CommFailed;
return passed;
}
public void ProcessResultOfWorkerActivity(int iMultiTestParamsItem, string currentActivity, int currentGroup,
ISmartReader iHead, WaterMeter wm, int wmNr0, CommErr error, string resultStr, bool[] ckbState, int threadID)
{
SmartCommunicationForm.OnCommCompleted(this,
new CommCompletedEventArgs(threadID, wmNr0, iHead, wm, resultStr, error));
}
public void StopWorkerThreads(bool stopAllThreads) { stopWorkerThreads = stopAllThreads; }
public bool GetStopWorkerThreads() { return stopWorkerThreads; }
public IList<Thread> GetAllThreads() { return workerThreads; }
public ICorrections GetNewCorrection() { return this; }
public void GetGroup() { }
public ContextMenu GetContextMenu() { return new ContextMenu(); }
public void Load(Label[] labels, PictureBox[] counters, TextBox[] messages, CheckBoxImage[] checkBoxes, int[] ckbIndex,
bool[] ckbState, IList<ISmartReader> heads, int textBoxesCount, bool checkBoxesEditMode)
{
for (int index = 0; index < textBoxesCount; index++)
{
bool hasHead = heads != null && index < heads.Count && heads[index] is AllyMeterReader;
labels[index].Visible = counters[index].Visible = messages[index].Visible = checkBoxes[index].Visible = hasHead;
if (!hasHead)
continue;
bool disabled = heads[index].Disabled;
checkBoxes[index].Enabled = checkBoxes[index].Checked = ckbState[index] = !disabled;
messages[index].Text = disabled ? "Disabled by user" : "---";
counters[index].BackColor = disabled
? iPerlCommunicationConstants.DisabledColor
: iPerlCommunicationConstants.OptoNokColor;
}
}
public int GetHeadsCount() { return iperlHeads == null ? 0 : iperlHeads.Count; }
public void StartDataStreamProcessingForActiveMeters(int iMultiTestParamsItem) { }
public void DoOnCommCompleted(object sender, CommCompletedEventArgs data, IList<int> waterMeterPositions0)
{
if (data == null || data.WMNr0 < 0 || data.WMNr0 >= ParentFrom.Messages.Length)
return;
ParentFrom.Messages[data.WMNr0].Text = data.CommMessage;
ParentFrom.Counters[data.WMNr0].BackColor = data.CommErr == CommErr.None
? iPerlCommunicationConstants.OptoAndDirOKColor
: iPerlCommunicationConstants.OptoNokColor;
if (++completedActivities < expectedActivities || ParentFrom.IsDisposed)
return;
ParentFrom.Log.InfoFormat("ALLY_TEST_CORRECTIONS_ALL_COMPLETED: callbacks={0}", completedActivities);
ParentFrom.CloseAfterResult(ResultVisibilityDelayMs);
}
public void NormalClose(IList<int> waterMeterPositions0) { StopWorkerThreads(true); }
public bool IsFamilyOfSmartReader(ISmartReader smartHead) { return smartHead is AllyMeterReader; }
private bool IsDisabled(int localIndex, AllyMeterReader reader, WaterMeter waterMeter)
{
return !IsSelected(localIndex) ||
reader.Disabled ||
(waterMeter != null && waterMeter.Disabled);
}
private bool IsSelected(int localIndex)
{
if (ParentFrom.CheckBoxes != null && localIndex >= 0 && localIndex < ParentFrom.CheckBoxes.Length)
return ParentFrom.CheckBoxes[localIndex].Checked;
return ParentFrom.CkbState == null || localIndex < 0 || localIndex >= ParentFrom.CkbState.Length ||
ParentFrom.CkbState[localIndex];
}
private int GetOriginalIndex(int localIndex)
{
IList<int> positions = ParentFrom.WaterMeterPositions0;
return positions != null && localIndex < positions.Count ? positions[localIndex] : localIndex;
}
private static WaterMeter GetWaterMeter(int originalIndex)
{
return ProcessData.BatchRslts != null && ProcessData.BatchRslts.Batch != null &&
ProcessData.BatchRslts.Batch.WaterMeters != null &&
originalIndex >= 0 && originalIndex < ProcessData.BatchRslts.Batch.WaterMeters.Count
? ProcessData.BatchRslts.Batch.WaterMeters[originalIndex]
: null;
}
private void UpdateResult(Test test, int originalIndex, WaterMeter waterMeter, AllyMeterReader reader,
string activity, bool passed)
{
if (test == null || ProcessData.BatchRslts == null)
return;
MeterTestRslt result = ProcessData.BatchRslts.GetMeterTestRslt(test.Name, originalIndex, CompoundMeterId.Single);
if (result == null)
return;
result.RegReaderType = (int)RegisterReaderType.DataStream;
result.TestDone = true;
result.Passed = passed;
// Follow the established iPerl/ASIC behavior: the physical reader owns
// the received value, while the displayed and persisted s/n belongs to
// the WaterMeter associated with this MeterTestRslt.
AllyCalibrationActivity parsedActivity;
if (passed && reader != null && waterMeter != null &&
AllyCalibrationActivityNames.TryParse(activity, out parsedActivity) &&
parsedActivity == AllyCalibrationActivity.ReadSerialNumber &&
!string.IsNullOrWhiteSpace(reader.SerialNr))
{
// The MeterTestRslt inverse reference can be null until NHibernate
// flushes it. The batch WaterMeter is the authoritative object used
// by the Results grid, therefore write to it directly.
waterMeter.SerialNr = reader.SerialNr;
if (result.WaterMeter != null)
result.WaterMeter.SerialNr = reader.SerialNr;
log.InfoFormat(
"ALLY_TEST_SERIAL_NUMBER_MAPPED: test='{0}', position={1}, serialNumber='{2}'",
test.Name,
originalIndex + 1,
reader.SerialNr);
}
log.InfoFormat("ALLY_TEST_RESULT_UPDATED: test='{0}', position={1}, passed={2}, testDone={3}",
test.Name, originalIndex + 1, result.Passed, result.TestDone);
}
private void CompleteTest(Test test, int processedReaders, bool allPassed)
{
if (test == null || ProcessData.BatchRslts == null)
return;
TestRslt testResult = ProcessData.BatchRslts.GetTestRslt(test.Name, 0);
if (testResult == null)
return;
if (testResult.StartTime == DateTime.MinValue)
testResult.StartTime = DateTime.Now;
testResult.EndTime = DateTime.Now;
testResult.TestDone = true;
testResult.Remark = processedReaders == 0
? "No ALLY reader is configured for this Meters Path."
: allPassed ? "ALLY communication completed." : "ALLY communication failed.";
log.InfoFormat("ALLY_TEST_RESULT_COMPLETED: test='{0}', readers={1}, resultDone={2}, remark='{3}'",
test.Name, processedReaders, testResult.TestDone, testResult.Remark);
TBF.UiBridge.Bridge.OnTestProgress(this,
new TBF.UiBridge.TestProgressEventArgs(test, Progress.Completed));
TBF.UiBridge.Bridge.OnTestCompleted(this,
new TBF.UiBridge.TestCompletedEventArgs(test.Name, testResult));
}
}
}
@@ -0,0 +1,480 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Windows.Forms;
using Common;
using Config.Entities;
using Results.Entities;
using TBF.Rig.Generic;
using TBF.Rig.RegisterReaders.AsicReader;
using TBF.Rig.RegisterReaders.CommonRR.IPerl;
using TBF.Rig.Sequences;
using TBF.Rig.TestMethods.iPerlCommunication;
using TBF.Rig.TestMethods.iPerlCommunication.communication.C4.protocolCommons;
using TBF.Rig.TestMethods.iPerlCommunication.communication.C4.diagnosticLed;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.common;
using CheckBoxImage = TBF.Boxes.CheckBoxImage;
namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
{
/// <summary>
/// SmartCommunicationForm adapter for the extracted ASIC reader family.
/// It calls the proven IperlHead OptoHeadTest implementation; it does not
/// route ASIC readers through the unrelated legacy IPerlCorrections class.
/// </summary>
public sealed class AsicCorrections : ICorrections
{
// Keep the final state/result readable for a single-meter operation.
// The timer itself belongs to the form and starts only after it is shown.
private const int ResultVisibilityDelayMs = 1500;
private readonly ISmartTestMethod testMethod;
private readonly ITestMethodCfg cfg;
private readonly IList<Test> tests;
private readonly IList<ITestParams> multiTestParams;
private readonly IList<Thread> workerThreads = new List<Thread>();
private volatile bool stopWorkerThreads;
private int completedCallbacks;
private int expectedCallbacks;
public AsicCorrections(SmartCommunicationForm parent)
: this(parent, null, null, new List<Test>(), new List<ITestParams>())
{
}
public AsicCorrections(SmartCommunicationForm parent, ISmartTestMethod testMethod,
ITestMethodCfg cfg, IList<Test> tests, IList<ITestParams> multiTestParams)
{
ParentFrom = parent;
this.testMethod = testMethod;
this.cfg = cfg;
this.tests = tests ?? new List<Test>();
this.multiTestParams = multiTestParams ?? new List<ITestParams>();
}
public string TypeIdentificatorName() { return "ASIC"; }
public DateTime StartTime { get; set; }
public int StartTimeSec { get; set; }
public SmartCommunicationForm ParentFrom { get; set; }
public ITestMethodCfg Cfg { get { return cfg; } set { } }
public IList<ITestParams> MultiTestParams { get { return multiTestParams; } }
public IList<Test> Tests { get { return tests; } set { } }
public IList<ISmartReader> iperlHeads { get { return ParentFrom == null ? null : ParentFrom.Heads; } }
public void PrepareForTestsActivities(int waterMeterPositions0)
{
StartTime = DateTime.Now;
StartTimeSec = StateMachine.Time;
stopWorkerThreads = false;
completedCallbacks = 0;
expectedCallbacks = Math.Max(0, waterMeterPositions0) * Math.Max(1, multiTestParams.Count);
workerThreads.Clear();
workerThreads.Add(new Thread(Worker) { IsBackground = true, Name = "ASIC SmartCommunication worker" });
LogInfo("ASIC_CORRECTIONS_PREPARE: readers={0}, activities={1}, expectedCallbacks={2}",
waterMeterPositions0, multiTestParams.Count, expectedCallbacks);
}
public void Worker(object threadData)
{
int threadId = threadData is TBF.Boxes.IntBox ? ((TBF.Boxes.IntBox)threadData).Val : 0;
IList<ISmartReader> readers = iperlHeads ?? new List<ISmartReader>();
for (int activityIndex = 0; activityIndex < multiTestParams.Count && !stopWorkerThreads; activityIndex++)
{
Test test = activityIndex < tests.Count ? tests[activityIndex] : null;
string activity = multiTestParams[activityIndex] == null ? string.Empty : multiTestParams[activityIndex].Activity;
bool allPassed = true;
int processed = 0;
// Match the established iPerl workflow: the optical stream is
// opened after entering test mode, before the next configured
// activity. It remains open for the following measurement.
StartDataStreamProcessingForActiveMeters(activityIndex);
for (int localIndex = 0; localIndex < readers.Count && !stopWorkerThreads; localIndex++)
{
AsicReader reader = readers[localIndex] as AsicReader;
int originalIndex = GetOriginalIndex(localIndex);
WaterMeter waterMeter = GetWaterMeter(originalIndex);
CommErr error = CommErr.None;
string result = string.Empty;
bool passed = WorkerActivity(activity, reader, waterMeter, test, localIndex, ref error, ref result,
ParentFrom == null ? null : ParentFrom.CkbState, threadId, activityIndex);
UpdateResult(test, originalIndex, waterMeter, reader, activity, passed);
processed++;
allPassed &= passed;
LogInfo("ASIC_CORRECTIONS_ACTIVITY: activity='{0}', position={1}, passed={2}, error={3}, result='{4}'",
activity, originalIndex + 1, passed, error, result);
SmartCommunicationForm.OnCommCompleted(this,
new CommCompletedEventArgs(threadId, localIndex, reader, waterMeter, result, error));
}
CompleteTest(test, processed, allPassed);
}
}
public bool WorkerActivity(string currentActivity, ISmartReader iHead, WaterMeter wm, Test currentTest,
int wmNr0, ref CommErr error, ref string resultStr, bool[] ckbState, int threadID, int currentActivityStep)
{
AsicReader reader = iHead as AsicReader;
if (reader == null)
{
error = CommErr.WrongIPerlType;
resultStr = "ASIC reader is not configured.";
return false;
}
if (IsDisabled(wmNr0, reader, wm, ckbState))
{
error = CommErr.HeadDisabledByUser;
resultStr = "Disabled by user";
return false;
}
try
{
string activity = currentActivity ?? string.Empty;
string normalized = activity.ToLowerInvariant();
bool succeeded;
CommErr failureError = CommErr.CommFailed;
if (normalized.Contains(iPerlCommunicationForm.ReadSerialNrStr.ToLowerInvariant()) ||
normalized.Contains("read pcb"))
{
succeeded = reader.OptoHeadTest.ReadSerialNr();
resultStr = succeeded ? "Serial No: " + reader.OptoHeadTest.ReadRequest_PCB() : "Failed Read Serial No";
}
else if (normalized.Contains(iPerlCommunicationForm.ReadConfigurationStr.ToLowerInvariant()))
{
succeeded = reader.OptoHeadTest.ReadConfiguration(DiagnosticLedState.State7);
resultStr = succeeded && reader.ConfigStruct != null ? reader.ConfigStruct.ToString(1) : "Failed to read configuration";
}
else if (normalized.Contains(iPerlCommunicationConstants.ReadAdditionalCommonParametersStr.ToLowerInvariant()))
{
// Preserve the legacy iPerl workflow: this command fills
// ConfigStruct and copies the relevant values to the WM.
if (wm == null)
{
error = CommErr.CommFailed;
resultStr = "Read additional common parameters: WaterMeter is null.";
return false;
}
succeeded = reader.OptoHeadTest.ReadAdditionalCommonParameters(out resultStr, false);
failureError = CommErr.Read;
if (succeeded)
CopyAdditionalCommonParametersToWaterMeter(reader, wm);
else if (string.IsNullOrWhiteSpace(resultStr))
resultStr = "Failed to read additional common parameters.";
}
else if (normalized.Contains(iPerlCommunicationConstants.SetFlipModeConstantStr.ToLowerInvariant()))
{
succeeded = reader.OptoHeadTest.SetFlipMode(FlipMode.Constant);
failureError = CommErr.Write;
resultStr = succeeded
? iPerlCommunicationConstants.SetFlipModeConstantStr + ": OK (0x00)"
: iPerlCommunicationConstants.SetFlipModeConstantStr + ": FAILED";
}
else if (normalized.Contains(iPerlCommunicationConstants.SetFlipModeRandomizedStr.ToLowerInvariant()))
{
succeeded = reader.OptoHeadTest.SetFlipMode(FlipMode.Randomized);
failureError = CommErr.Write;
resultStr = succeeded
? iPerlCommunicationConstants.SetFlipModeRandomizedStr + ": OK (0x01)"
: iPerlCommunicationConstants.SetFlipModeRandomizedStr + ": FAILED";
}
else if (normalized.Contains(iPerlCommunicationForm.SetTestModeStr.ToLowerInvariant()) ||
normalized.Contains(iPerlCommunicationForm.SetTestModeOpto7Str.ToLowerInvariant()))
{
succeeded = reader.OptoHeadTest.SetTestMode();
resultStr = succeeded ? "Test mode enabled" : "Failed Set Test Mode";
}
else if (normalized.Equals(iPerlCommunicationForm.SetActiveModeStr.ToLowerInvariant()))
{
succeeded = reader.OptoHeadTest.SetActiveMode();
resultStr = succeeded ? "Active mode enabled" : "Failed Set Active Mode";
if (succeeded)
reader.StopDataStreamProcessing();
}
else if (normalized.Equals(iPerlCommunicationForm.SetIdleModeStr.ToLowerInvariant()))
{
succeeded = reader.OptoHeadTest.SetIdleMode();
resultStr = succeeded ? "Idle mode enabled" : "Failed Set Idle Mode";
}
else
{
error = CommErr.WrongArguments;
resultStr = "ASIC activity is not supported by AsicCorrections: " + activity;
return false;
}
reader.CommFailed = !succeeded;
error = succeeded ? CommErr.None : failureError;
return succeeded;
}
catch (Exception exception)
{
reader.CommFailed = true;
error = CommErr.CommFailed;
resultStr = "ASIC communication failed: " + exception.Message;
ParentFrom?.Log.Error("ASIC_CORRECTIONS_ACTIVITY_FAILED", exception);
return false;
}
}
public void ProcessResultOfWorkerActivity(int iMultiTestParamsItem, string currentActivity, int currentGroup,
ISmartReader iHead, WaterMeter wm, int wmNr0, CommErr error, string resultStr, bool[] ckbState, int threadID)
{
SmartCommunicationForm.OnCommCompleted(this,
new CommCompletedEventArgs(threadID, wmNr0, iHead, wm, resultStr, error));
}
public void StopWorkerThreads(bool stopAllThreads) { stopWorkerThreads = stopAllThreads; }
public bool GetStopWorkerThreads() { return stopWorkerThreads; }
public IList<Thread> GetAllThreads() { return workerThreads; }
public ICorrections GetNewCorrection() { return this; }
public void GetGroup() { }
public ContextMenu GetContextMenu()
{
ContextMenu menu = new ContextMenu();
menu.MenuItems.Add(NewMenuItem("Read PCB number", iPerlCommunicationForm.ReadSerialNrStr));
menu.MenuItems.Add(NewMenuItem("Read configuration", iPerlCommunicationForm.ReadConfigurationStr));
menu.MenuItems.Add(NewMenuItem(iPerlCommunicationConstants.ReadAdditionalCommonParametersStr,
iPerlCommunicationConstants.ReadAdditionalCommonParametersStr));
menu.MenuItems.Add(NewMenuItem(iPerlCommunicationConstants.SetFlipModeConstantStr,
iPerlCommunicationConstants.SetFlipModeConstantStr));
menu.MenuItems.Add(NewMenuItem(iPerlCommunicationConstants.SetFlipModeRandomizedStr,
iPerlCommunicationConstants.SetFlipModeRandomizedStr));
menu.MenuItems.Add(NewMenuItem("Set test mode", iPerlCommunicationForm.SetTestModeStr));
menu.MenuItems.Add(NewMenuItem("Set active mode", iPerlCommunicationForm.SetActiveModeStr));
menu.MenuItems.Add(NewMenuItem("Start optical stream", "__start_stream"));
menu.MenuItems.Add(NewMenuItem("Stop optical stream", "__stop_stream"));
return menu;
}
public void Load(Label[] labels, PictureBox[] counters, TextBox[] messages, CheckBoxImage[] checkBoxes, int[] ckbIndex,
bool[] ckbState, IList<ISmartReader> heads, int textBoxesCount, bool checkBoxesEditMode)
{
for (int index = 0; index < textBoxesCount; index++)
{
bool hasReader = heads != null && index < heads.Count && heads[index] is AsicReader;
labels[index].Visible = counters[index].Visible = messages[index].Visible = checkBoxes[index].Visible = hasReader;
if (!hasReader)
continue;
bool disabled = heads[index].Disabled;
checkBoxes[index].Enabled = checkBoxes[index].Checked = ckbState[index] = !disabled;
messages[index].Text = disabled ? "Disabled by user" : "---";
counters[index].BackColor = disabled ? iPerlCommunicationConstants.DisabledColor : iPerlCommunicationConstants.OptoNokColor;
}
}
public int GetHeadsCount() { return iperlHeads == null ? 0 : iperlHeads.Count; }
public void StartDataStreamProcessingForActiveMeters(int iMultiTestParamsItem)
{
if (iperlHeads == null || iMultiTestParamsItem <= 0 || multiTestParams == null ||
iMultiTestParamsItem - 1 >= multiTestParams.Count ||
!ShouldStartDataStreamForActivity(multiTestParams[iMultiTestParamsItem - 1] == null
? null
: multiTestParams[iMultiTestParamsItem - 1].Activity))
return;
int started = 0;
for (int localIndex = 0; localIndex < iperlHeads.Count; localIndex++)
{
AsicReader reader = iperlHeads[localIndex] as AsicReader;
WaterMeter waterMeter = GetWaterMeter(GetOriginalIndex(localIndex));
if (reader == null || reader.Disabled || (waterMeter != null && waterMeter.Disabled))
continue;
lock (reader)
{
reader.StartDataStreamProcessing();
started++;
}
}
LogInfo("ASIC_CORRECTIONS_STREAM_STARTED: previousActivity='{0}', readers={1}",
multiTestParams[iMultiTestParamsItem - 1].Activity, started);
}
public void DoOnCommCompleted(object sender, CommCompletedEventArgs data, IList<int> waterMeterPositions0)
{
if (data == null || ParentFrom == null || data.WMNr0 < 0 || data.WMNr0 >= ParentFrom.Messages.Length)
return;
ParentFrom.Messages[data.WMNr0].Text = data.CommMessage;
ParentFrom.Counters[data.WMNr0].BackColor = data.CommErr == CommErr.None
? iPerlCommunicationConstants.OptoAndDirOKColor
: iPerlCommunicationConstants.OptoNokColor;
if (++completedCallbacks >= expectedCallbacks && expectedCallbacks > 0 && !ParentFrom.IsDisposed)
ParentFrom.CloseAfterResult(ResultVisibilityDelayMs);
}
public void NormalClose(IList<int> waterMeterPositions0)
{
// Do not stop the stream here. The stream must survive closing this
// setup dialog so that the subsequent measurement can consume it.
// The explicit "Stop optical stream" command or Set Active mode is
// responsible for closing the physical port.
StopWorkerThreads(true);
}
public bool IsFamilyOfSmartReader(ISmartReader smartHead)
{
// Use the central family resolver rather than a concrete CLR type.
// Readers can be supplied through a proxy/legacy configuration while
// still belonging to the extracted ASIC family.
return string.Equals(SmartReaderSelection.GetFamilyKey(smartHead), "asic", StringComparison.OrdinalIgnoreCase);
}
private MenuItem NewMenuItem(string text, string command)
{
MenuItem item = new MenuItem { Text = text, Tag = command };
item.Click += OnManualCommand;
return item;
}
private void OnManualCommand(object sender, EventArgs e)
{
MenuItem item = sender as MenuItem;
if (item == null || ParentFrom == null)
return;
string command = item.Tag as string ?? string.Empty;
ParentFrom.ActivityLabel.Text = item.Text;
for (int index = 0; index < iperlHeads.Count; index++)
{
AsicReader reader = iperlHeads[index] as AsicReader;
if (reader == null || (ParentFrom.CkbState != null && index < ParentFrom.CkbState.Length && !ParentFrom.CkbState[index]))
continue;
string result;
CommErr error = CommErr.None;
if (command == "__start_stream")
{
reader.StartDataStreamProcessing();
result = "Optical stream started";
}
else if (command == "__stop_stream")
{
reader.StopDataStreamProcessing();
result = "Optical stream stopped";
}
else
{
result = string.Empty;
WorkerActivity(command, reader, null, null, index, ref error, ref result,
ParentFrom.CkbState, 0, 0);
}
if (index < ParentFrom.Messages.Length)
ParentFrom.Messages[index].Text = result;
if (index < ParentFrom.Counters.Length)
ParentFrom.Counters[index].BackColor = error == CommErr.None
? iPerlCommunicationConstants.OptoAndDirOKColor
: iPerlCommunicationConstants.OptoNokColor;
}
}
private bool IsDisabled(int localIndex, AsicReader reader, WaterMeter waterMeter, bool[] ckbState)
{
return (ckbState != null && localIndex < ckbState.Length && !ckbState[localIndex]) || reader.Disabled ||
(waterMeter != null && waterMeter.Disabled);
}
public static bool ShouldStartDataStreamForActivity(string previousActivity)
{
if (string.IsNullOrWhiteSpace(previousActivity))
return false;
return previousActivity.IndexOf(iPerlCommunicationForm.SetTestModeStr, StringComparison.OrdinalIgnoreCase) >= 0 &&
previousActivity.IndexOf("80", StringComparison.Ordinal) < 0;
}
private int GetOriginalIndex(int localIndex)
{
IList<int> positions = ParentFrom == null ? null : ParentFrom.WaterMeterPositions0;
return positions != null && localIndex < positions.Count ? positions[localIndex] : localIndex;
}
private static WaterMeter GetWaterMeter(int originalIndex)
{
return ProcessData.BatchRslts != null && ProcessData.BatchRslts.Batch != null &&
ProcessData.BatchRslts.Batch.WaterMeters != null && originalIndex >= 0 &&
originalIndex < ProcessData.BatchRslts.Batch.WaterMeters.Count
? ProcessData.BatchRslts.Batch.WaterMeters[originalIndex]
: null;
}
private void CopyAdditionalCommonParametersToWaterMeter(AsicReader reader, WaterMeter waterMeter)
{
if (reader == null || reader.ConfigStruct == null || waterMeter == null)
return;
if (reader.ConfigStruct.VersionType != null)
{
waterMeter.FWVersion = string.Format("{0} {1} {2}",
reader.ConfigStruct.VersionType.TouchReadVersion,
reader.ConfigStruct.VersionType.MeterDeviceType,
reader.ConfigStruct.VersionType.MeterFirmwareVersion);
}
if (reader.ConfigStruct.ReadingUnits.HasValue)
waterMeter.SerialNrAux = reader.ConfigStruct.ReadingUnits.Value.ToString();
if (reader.ConfigStruct.Calibration != null)
waterMeter.CalibFactor = reader.ConfigStruct.Calibration.RawValue;
LogInfo("ASIC_CORRECTIONS_ADDITIONAL_COMMON_PARAMETERS: reader='{0}', FWVersion='{1}', ReadingUnits='{2}', FlipMode='{3}', CalibFactor={4}",
reader.Name,
waterMeter.FWVersion,
waterMeter.SerialNrAux,
reader.ConfigStruct.FlipMode.HasValue ? reader.ConfigStruct.FlipMode.Value.ToString() : "<not read>",
waterMeter.CalibFactor);
}
private void UpdateResult(Test test, int originalIndex, WaterMeter waterMeter, AsicReader reader, string activity, bool passed)
{
if (test == null || ProcessData.BatchRslts == null)
return;
MeterTestRslt result = ProcessData.BatchRslts.GetMeterTestRslt(test.Name, originalIndex, CompoundMeterId.Single);
if (result == null)
return;
result.RegReaderType = (int)RegisterReaderType.DataStream;
result.TestDone = true;
result.Passed = passed;
if (passed && activity != null && activity.ToLowerInvariant().Contains(iPerlCommunicationForm.ReadSerialNrStr.ToLowerInvariant()) &&
reader != null && waterMeter != null && !string.IsNullOrWhiteSpace(reader.SerialNr))
{
waterMeter.SerialNr = reader.SerialNr;
if (result.WaterMeter != null)
result.WaterMeter.SerialNr = reader.SerialNr;
}
}
private void CompleteTest(Test test, int processedReaders, bool allPassed)
{
if (test == null || ProcessData.BatchRslts == null)
return;
TestRslt testResult = ProcessData.BatchRslts.GetTestRslt(test.Name, 0);
if (testResult == null)
return;
if (testResult.StartTime == DateTime.MinValue)
testResult.StartTime = DateTime.Now;
testResult.EndTime = DateTime.Now;
testResult.TestDone = true;
testResult.Remark = processedReaders == 0 ? "No ASIC reader is configured for this Meters Path."
: allPassed ? "ASIC communication completed." : "ASIC communication failed.";
}
private void LogInfo(string format, params object[] values)
{
ParentFrom?.Log.InfoFormat(format, values);
}
}
}
@@ -0,0 +1,33 @@
using System.Collections.Generic;
using System.Windows.Forms;
using TBF.Rig.RegisterReaders.GenesisRegReader.implementations;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.common;
namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
{
internal sealed class GenesisCorrections : ManualSmartCorrectionsBase<GenesisSmartReader>
{
public GenesisCorrections(SmartCommunicationForm parent) : base(parent) { }
public override string TypeIdentificatorName() => "Genesis";
public override bool IsFamilyOfSmartReader(ISmartReader smartHead) => smartHead is GenesisSmartReader;
protected override IEnumerable<MenuItem> CreateMenuItems()
{
yield return new MenuItem("Read PCB Number") { Tag = "ReadPcb" };
yield return new MenuItem("Set RFID mode") { Tag = "SetRfid" };
yield return new MenuItem("Set NFC mode") { Tag = "SetNfc" };
}
protected override string ExecuteManualCommand(GenesisSmartReader reader, string command)
{
switch (command)
{
case "ReadPcb": return reader.OptoHeadTest.ReadRequest_PCB();
case "SetRfid": reader.SetRfidInterface(); return "OK";
case "SetNfc": reader.SetNfcInterface(); return "OK";
default: return "Unsupported Genesis operation";
}
}
}
}
@@ -13,10 +13,9 @@ using Sensus.iPerl.NfcHandler;
using TBF.Resources;
using TBF.Rig.Generic;
using TBF.Rig.RegisterReaders.CommonRR.IPerl;
using TBF.Rig.RegisterReaders.IPerlReader.implementations;
using TBF.Rig.RegisterReaders.iPerlASICReader.implementations;
using TBF.Rig.Sequences;
using TBF.Rig.TestMethods.iPerlCommunication;
using TBF.Rig.TestMethods.iPerlCommunication.iPerlHead;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.common;
using CalibrationStruct = TBF.Rig.RegisterReaders.CommonRR.IPerl.communication.CalibrationStruct;
using CalibrationStructV4 = TBF.Rig.RegisterReaders.CommonRR.IPerl.communication.CalibrationStructV4;
@@ -24,7 +23,7 @@ using CheckBoxImage = TBF.Boxes.CheckBoxImage;
using Command = TBF.Rig.RegisterReaders.CommonRR.Command;
using CommunicationInterface = TBF.Rig.RegisterReaders.CommonRR.CommunicationInterface;
using ConfigStruct = TBF.Rig.RegisterReaders.CommonRR.IPerl.communication.ConfigStruct;
using Factory = TBF.Rig.RegisterReaders.iPerlReaderUNI.Factory;
using Factory = TBF.Rig.RegisterReaders.iPerlASICReader.Factory;
using MessageID = TBF.Rig.RegisterReaders.CommonRR.MessageID;
using MeterState = TBF.Rig.RegisterReaders.CommonRR.MeterState;
using MeterType = TBF.Rig.RegisterReaders.CommonRR.MeterType;
@@ -228,7 +227,7 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
for (int wmNr0 = 0; wmNr0 < iperlHeads.Count; wmNr0++)
{
IperlHead ihead = iperlHeads[wmNr0] as IperlHead;
SmartReader ihead = iperlHeads[wmNr0] as SmartReader;
if ((ihead.Group == group) && (threadIx < muxBrdOrGroup14Nrs.Count) &&
(ihead.MuxBoardNrOrGroup14 == muxBrdOrGroup14Nrs[threadIx]))
{
@@ -306,14 +305,13 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
activityLabel.Text = menuItem.Text;
List<Task> tasks = new List<Task>();
foreach (var iSmartReader in ProcessData.SmartHeadsUni)
for (int position = 0; position < iperlHeads.Count; position++)
{
if (!(iSmartReader is IperlHead iHead))
if (!(iperlHeads[position] is SmartReader iHead))
{
continue;//ignore different types of heads
}
int position = iHead.Position - 1;
if (position < 0 || position >= checkBoxes.Length || position >= messages.Length)
{
continue; // Skip this head if position is out of range
@@ -342,37 +340,39 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
await Task.WhenAll(tasks);
}
private async Task<string> ProcessTask(IperlHead head, object tag)
private async Task<string> ProcessTask(SmartReader head, object tag)
{
string txt = "";
switch (tag)
{
case "ReadPCB":
txt = OpticalHeadTest.ReadRequest_PCB(head);
txt = IPerlASICOpticalHeadTest.ReadPcb(head);
break;
case "WriteRequestPort_u8_Customer_Text":
txt = OpticalHeadTest.WriteRequestPort_u8_Customer_Text(head);
txt = "Unsupported ASIC operation";
break;
case "OpenSealing":
txt = OpticalHeadTest.OpenSealing(head);
txt = "Unsupported ASIC operation";
break;
case "StartTestMode":
txt = OpticalHeadTest.SetTestMode(head);
txt = IPerlASICOpticalHeadTest.SetTestMode(head);
break;
case "TurnOffTestMode":
txt = OpticalHeadTest.SetActiveMode(head);
txt = IPerlASICOpticalHeadTest.SetActiveMode(head);
break;
case "TurnOffRadio":
txt = OpticalHeadTest.TurnOffRadio(head);
txt = IPerlASICOpticalHeadTest.TurnOffRadio(head);
break;
case "SetProductionMode":
txt = OpticalHeadTest.SetProductionMode(head);
txt = IPerlASICOpticalHeadTest.SetProductionMode(head);
break;
case "SetRFID":
txt = OpticalHeadTest.SetRfidMode(head);
head.SetRfidInterface();
txt = "OK";
break;
case "SetNFC":
txt = OpticalHeadTest.SetNfcMode(head);
head.SetNfcInterface();
txt = "OK";
break;
}
@@ -399,10 +399,12 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
///
for (int i = 0; i < textBoxesCount; i++)
{
labels[i].Visible = counters[i].Visible = messages[i].Visible = checkBoxes[i].Visible = true;
bool hasHead = i < iperlHeads.Count;
labels[i].Visible = counters[i].Visible = messages[i].Visible = checkBoxes[i].Visible = hasHead;
if (!hasHead) continue;
IperlHead iperlHead = iperlHeads[i] as IperlHead;
SmartReader iperlHead = iperlHeads[i] as SmartReader;
if (!checkBoxesEditMode && (iperlHead == null || iperlHead.Disabled))
{
/// iPerl position i+1 is disabled
@@ -441,7 +443,7 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
{
try
{
var iPerl = iSmartReader as IperlHead;
var iPerl = iSmartReader as SmartReader;
if (iPerl != null && iPerl.Group > lastGroup) lastGroup = iPerl.Group;
}
catch (Exception E)
@@ -468,7 +470,7 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
{
foreach (ISmartReader iPerl in iperlHeads)
{
if (iPerl is IperlHead ihead)
if (iPerl is SmartReader ihead)
{
if (!muxBrdOrGroup14Nrs.Contains(ihead.MuxBoardNrOrGroup14))
muxBrdOrGroup14Nrs.Add(ihead.MuxBoardNrOrGroup14);
@@ -2591,8 +2593,8 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
private static readonly List<Type> SupportedReaders = new List<Type>()
{
typeof(IperlHead),
typeof(TestMethods.iPerlCommunication.iPerlHead.Factory)
typeof(SmartReader),
typeof(TBF.Rig.RegisterReaders.iPerlASICReader.Factory)
};
public bool IsFamilyOfSmartReader(ISmartReader smartHead)
{
@@ -2814,7 +2816,7 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
}
else
{
IperlHead iperlHead = (iperlHeads[i] as IperlHead);
SmartReader iperlHead = (iperlHeads[i] as SmartReader);
OptoHeadState checkFlowDirection =
((iperlHead == null) ? OptoHeadState.Disabled : iperlHead.CheckFlowDirection());
@@ -2875,4 +2877,4 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
}
}
}
@@ -0,0 +1,105 @@
using System;
using Config.Resources;
using log4net;
using Sensus.iPerl.RfidCom.Helper;
using TBF.Rig.RegisterReaders.CommonRR;
using TBF.Rig.RegisterReaders.CommonRR.IPerl;
using TBF.Rig.RegisterReaders.iPerlASICReader.implementations;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.common;
using static Sensus.iPerl.NfcHandler.MCI_Protocol;
namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
{
/// <summary>
/// Manual commands for ASIC readers. They intentionally use the ASIC correction
/// transport path and never fall back to the old Sensus iPerl implementation.
/// </summary>
internal static class IPerlASICOpticalHeadTest
{
private static readonly ILog RfidDataLogger = LogManager.GetLogger("RfidData");
internal static string ReadPcb(SmartReader reader)
{
try
{
byte[] pcb;
int result = IPerlASICCorrections.ReadRequestPort(
SmartCommunicationForm.TestMethodCfg,
reader,
MessageID.Configuration,
StructName.Configuration,
16,
5,
out pcb);
if (result == 0 && pcb != null)
{
return RfidHelper.HexLiteral2Unsigned(
RfidHelper.SwapHexcode(BitConverter.ToString(pcb).Replace("-", string.Empty))).ToString();
}
RfidDataLogger.ErrorFormat("COM{0}: ASIC Read PCB failed ({1}).", reader.RfidComPortNr, result);
return "Error";
}
catch (Exception ex)
{
return ex.Message;
}
}
internal static string SetActiveMode(SmartReader reader)
{
return WriteCommand(reader, Command.SetActiveMode, "Error Set Active Mode");
}
internal static string SetTestMode(SmartReader reader)
{
return WriteCommand(reader, Command.SetTestMode, "Error Set Test Mode");
}
internal static string TurnOffRadio(SmartReader reader)
{
return WriteRadioValue(reader, MessageID.RadioPassthrough, StructName.RadioParams, 0x1898, 3);
}
internal static string SetProductionMode(SmartReader reader)
{
return WriteRadioValue(reader, MessageID.RadioPassthrough, StructName.RadioInfo, 0x1804, 1);
}
private static string WriteCommand(SmartReader reader, Command command, string errorText)
{
int result = IPerlASICCorrections.WriteRequestPort(
SmartCommunicationForm.TestMethodCfg,
reader,
MessageID.Command,
StructName.Command,
0,
1,
new[] { (byte)command });
return result == 0 ? "OK" : errorText;
}
private static string WriteRadioValue(SmartReader reader, MessageID messageId,
StructName structName, int offset, byte value)
{
try
{
int result = IPerlASICCorrections.WriteRequestPort(
SmartCommunicationForm.TestMethodCfg,
reader,
messageId,
structName,
offset,
1,
new[] { value });
return result == 0 ? "OK" : "Error";
}
catch (Exception ex)
{
return ex.Message;
}
}
}
}
@@ -14,9 +14,9 @@ using TBF.Resources;
using TBF.Rig.Generic;
using TBF.Rig.RegisterReaders.CommonRR.IPerl;
using TBF.Rig.RegisterReaders.IPerlReader.implementations;
using LegacyOpticalHeadTest = TBF.Rig.RegisterReaders.iPerlReaderUNI.test.OpticalHeadTest;
using TBF.Rig.Sequences;
using TBF.Rig.TestMethods.iPerlCommunication;
using TBF.Rig.TestMethods.iPerlCommunication.iPerlHead;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.common;
using CalibrationStruct = TBF.Rig.RegisterReaders.CommonRR.IPerl.communication.CalibrationStruct;
using CalibrationStructV4 = TBF.Rig.RegisterReaders.CommonRR.IPerl.communication.CalibrationStructV4;
@@ -24,7 +24,7 @@ using CheckBoxImage = TBF.Boxes.CheckBoxImage;
using Command = TBF.Rig.RegisterReaders.CommonRR.Command;
using CommunicationInterface = TBF.Rig.RegisterReaders.CommonRR.CommunicationInterface;
using ConfigStruct = TBF.Rig.RegisterReaders.CommonRR.IPerl.communication.ConfigStruct;
using Factory = TBF.Rig.RegisterReaders.iPerlReaderUNI.Factory;
using Factory = TBF.Rig.RegisterReaders.IPerlReader.Factory;
using MessageID = TBF.Rig.RegisterReaders.CommonRR.MessageID;
using MeterState = TBF.Rig.RegisterReaders.CommonRR.MeterState;
using MeterType = TBF.Rig.RegisterReaders.CommonRR.MeterType;
@@ -139,7 +139,7 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
public IPerlCorrections(SmartCommunicationForm parent)
{
this.ParentFrom = parent;
TBF.Rig.RegisterReaders.iPerlReaderUNI.Factory factory = new Factory();
Factory factory = new Factory();
TestMethod method = new TestMethod(factory.DefaultConfig());
this.testMethod = method;
this.Cfg = method.testMethodCfg;
@@ -228,7 +228,7 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
for (int wmNr0 = 0; wmNr0 < iperlHeads.Count; wmNr0++)
{
IperlHead ihead = iperlHeads[wmNr0] as IperlHead;
SmartReader ihead = iperlHeads[wmNr0] as SmartReader;
if ((ihead.Group == group) && (threadIx < muxBrdOrGroup14Nrs.Count) &&
(ihead.MuxBoardNrOrGroup14 == muxBrdOrGroup14Nrs[threadIx]))
{
@@ -306,14 +306,13 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
activityLabel.Text = menuItem.Text;
List<Task> tasks = new List<Task>();
foreach (var iSmartReader in ProcessData.SmartHeadsUni)
for (int position = 0; position < iperlHeads.Count; position++)
{
if (!(iSmartReader is IperlHead iHead))
if (!(iperlHeads[position] is SmartReader iHead))
{
continue;//ignore different types of heads
}
int position = iHead.Position - 1;
if (position < 0 || position >= checkBoxes.Length || position >= messages.Length)
{
continue; // Skip this head if position is out of range
@@ -342,37 +341,39 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
await Task.WhenAll(tasks);
}
private async Task<string> ProcessTask(IperlHead head, object tag)
private async Task<string> ProcessTask(SmartReader head, object tag)
{
string txt = "";
switch (tag)
{
case "ReadPCB":
txt = OpticalHeadTest.ReadRequest_PCB(head);
txt = LegacyOpticalHeadTest.ReadRequest_PCB(head);
break;
case "WriteRequestPort_u8_Customer_Text":
txt = OpticalHeadTest.WriteRequestPort_u8_Customer_Text(head);
txt = "Unsupported old iPerl operation";
break;
case "OpenSealing":
txt = OpticalHeadTest.OpenSealing(head);
txt = LegacyOpticalHeadTest.OpenSealing(head);
break;
case "StartTestMode":
txt = OpticalHeadTest.SetTestMode(head);
txt = LegacyOpticalHeadTest.SetTestMode(head);
break;
case "TurnOffTestMode":
txt = OpticalHeadTest.SetActiveMode(head);
txt = LegacyOpticalHeadTest.SetActiveMode(head);
break;
case "TurnOffRadio":
txt = OpticalHeadTest.TurnOffRadio(head);
txt = LegacyOpticalHeadTest.TurnOffRadio(head);
break;
case "SetProductionMode":
txt = OpticalHeadTest.SetProductionMode(head);
txt = LegacyOpticalHeadTest.SetProductionMode(head);
break;
case "SetRFID":
txt = OpticalHeadTest.SetRfidMode(head);
head.SetRfidInterface();
txt = "OK";
break;
case "SetNFC":
txt = OpticalHeadTest.SetNfcMode(head);
head.SetNfcInterface();
txt = "OK";
break;
}
@@ -399,10 +400,12 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
///
for (int i = 0; i < textBoxesCount; i++)
{
labels[i].Visible = counters[i].Visible = messages[i].Visible = checkBoxes[i].Visible = true;
bool hasHead = i < iperlHeads.Count;
labels[i].Visible = counters[i].Visible = messages[i].Visible = checkBoxes[i].Visible = hasHead;
if (!hasHead) continue;
IperlHead iperlHead = iperlHeads[i] as IperlHead;
SmartReader iperlHead = iperlHeads[i] as SmartReader;
if (!checkBoxesEditMode && (iperlHead == null || iperlHead.Disabled))
{
/// iPerl position i+1 is disabled
@@ -441,7 +444,7 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
{
try
{
var iPerl = iSmartReader as IperlHead;
var iPerl = iSmartReader as SmartReader;
if (iPerl != null && iPerl.Group > lastGroup) lastGroup = iPerl.Group;
}
catch (Exception E)
@@ -468,7 +471,7 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
{
foreach (ISmartReader iPerl in iperlHeads)
{
if (iPerl is IperlHead ihead)
if (iPerl is SmartReader ihead)
{
if (!muxBrdOrGroup14Nrs.Contains(ihead.MuxBoardNrOrGroup14))
muxBrdOrGroup14Nrs.Add(ihead.MuxBoardNrOrGroup14);
@@ -2591,8 +2594,8 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
private static readonly List<Type> SupportedReaders = new List<Type>()
{
typeof(IperlHead),
typeof(TestMethods.iPerlCommunication.iPerlHead.Factory)
typeof(SmartReader),
typeof(TBF.Rig.RegisterReaders.IPerlReader.Factory)
};
public bool IsFamilyOfSmartReader(ISmartReader smartHead)
{
@@ -2814,7 +2817,7 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
}
else
{
IperlHead iperlHead = (iperlHeads[i] as IperlHead);
SmartReader iperlHead = (iperlHeads[i] as SmartReader);
OptoHeadState checkFlowDirection =
((iperlHead == null) ? OptoHeadState.Disabled : iperlHead.CheckFlowDirection());
@@ -2875,4 +2878,4 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
}
}
}
@@ -0,0 +1,141 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using Config.Entities;
using Results.Entities;
using TBF.Rig.Generic;
using TBF.Rig.RegisterReaders.CommonRR.IPerl;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.common;
using CheckBoxImage = TBF.Boxes.CheckBoxImage;
namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
{
/// <summary>
/// Common manual-only adapter for smart-meter reader families.
/// Test execution is deliberately not implemented here; each protocol must provide
/// its own test adapter before it is enabled for SmartCommunication test activities.
/// </summary>
internal abstract class ManualSmartCorrectionsBase<TReader> : ICorrections
where TReader : class, ISmartReader
{
protected ManualSmartCorrectionsBase(SmartCommunicationForm parent)
{
ParentFrom = parent;
}
public abstract string TypeIdentificatorName();
public abstract bool IsFamilyOfSmartReader(ISmartReader smartHead);
protected abstract IEnumerable<MenuItem> CreateMenuItems();
protected abstract string ExecuteManualCommand(TReader reader, string command);
public DateTime StartTime { get; set; }
public int StartTimeSec { get; set; }
public SmartCommunicationForm ParentFrom { get; set; }
public ITestMethodCfg Cfg { get; set; }
public IList<ITestParams> MultiTestParams { get; private set; }
public IList<Test> Tests { get; set; }
public IList<ISmartReader> iperlHeads => ParentFrom.Heads;
public ContextMenu GetContextMenu()
{
ContextMenu menu = new ContextMenu();
foreach (MenuItem item in CreateMenuItems())
{
item.Click += OnManualCommand;
menu.MenuItems.Add(item);
}
return menu;
}
private async void OnManualCommand(object sender, EventArgs e)
{
MenuItem item = sender as MenuItem;
if (item == null || item.Tag == null)
return;
ParentFrom.ActivityLabel.Text = item.Text;
string command = item.Tag.ToString();
IList<ISmartReader> heads = ParentFrom.Heads;
ParentFrom.Log.InfoFormat(
"SMART_COMM_MANUAL_COMMAND_START: family='{0}', command='{1}', caption='{2}', heads={3}",
TypeIdentificatorName(),
command,
item.Text,
heads == null ? 0 : heads.Count);
List<Task<string>> operations = heads
.Select(head => head as TReader)
.Select(reader => reader == null
? Task.FromResult("Unsupported reader")
: Task.Run(() => ExecuteSafely(reader, command)))
.ToList();
string[] results = await Task.WhenAll(operations);
for (int index = 0; index < results.Length && index < ParentFrom.Messages.Length; index++)
{
ParentFrom.Messages[index].Text = results[index];
ParentFrom.Log.InfoFormat(
"SMART_COMM_MANUAL_COMMAND_RESULT: family='{0}', command='{1}', displayedRow={2}, result='{3}'",
TypeIdentificatorName(),
command,
index + 1,
results[index]);
}
}
private string ExecuteSafely(TReader reader, string command)
{
try
{
return ExecuteManualCommand(reader, command) ?? "OK";
}
catch (Exception exception)
{
ParentFrom.Log.Error($"{TypeIdentificatorName()} manual command '{command}' failed for {reader.Name}.", exception);
return $"Error: {exception.Message}";
}
}
public void Load(Label[] labels, PictureBox[] counters, TextBox[] messages, CheckBoxImage[] checkBoxes,
int[] ckbIndex, bool[] ckbState, IList<ISmartReader> heads, int textBoxesCount, bool checkBoxesEditMode)
{
for (int index = 0; index < textBoxesCount; index++)
{
bool visible = heads != null && index < heads.Count;
labels[index].Visible = counters[index].Visible = messages[index].Visible = checkBoxes[index].Visible = visible;
if (!visible)
continue;
ISmartReader reader = heads[index];
bool enabled = reader != null && (!reader.Disabled || checkBoxesEditMode);
checkBoxes[index].Enabled = checkBoxes[index].Checked = ckbState[index] = enabled;
counters[index].BackColor = enabled ? SystemColors.Control : iPerlCommunicationConstants.DisabledColor;
messages[index].Text = enabled ? "---" : "Disabled by user";
}
}
public int GetHeadsCount() => ParentFrom?.Heads?.Count ?? 0;
public void PrepareForTestsActivities(int waterMeterPositions0) { }
public void StartDataStreamProcessingForActiveMeters(int iMultiTestParamsItem) { }
public void StopWorkerThreads(bool bStopAllThreads) { }
public bool GetStopWorkerThreads() => false;
public IList<Thread> GetAllThreads() => new List<Thread>();
public ICorrections GetNewCorrection() => throw new NotSupportedException("Manual adapter cannot create a test adapter.");
public void GetGroup() { }
public void Worker(object threadData) => throw new NotSupportedException($"{TypeIdentificatorName()} test communication is not implemented.");
public bool WorkerActivity(string currentActivity, ISmartReader iHead, WaterMeter wm, Test currentTest,
int wmNr0, ref CommErr error, ref string resultStr, bool[] ckbState, int threadID, int currentActivityStep)
{
error = CommErr.WrongIPerlType;
resultStr = $"{TypeIdentificatorName()} test communication is not implemented.";
return false;
}
public void ProcessResultOfWorkerActivity(int iMultiTestParamsItem, string currentActivity, int currentGroup,
ISmartReader iHead, WaterMeter wm, int wmNr0, CommErr error, string resultStr, bool[] ckbState, int threadID) { }
public void DoOnCommCompleted(object sender, CommCompletedEventArgs data, IList<int> waterMeterPositions0) { }
public void NormalClose(IList<int> waterMeterPositions0) { }
}
}
@@ -294,7 +294,9 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
///
for (int i = 0; i < textBoxesCount; i++)
{
labels[i].Visible = counters[i].Visible = messages[i].Visible = checkBoxes[i].Visible = true;
bool hasHead = i < iperlHeads.Count;
labels[i].Visible = counters[i].Visible = messages[i].Visible = checkBoxes[i].Visible = hasHead;
if (!hasHead) continue;
ISmartReader iperlHead = iperlHeads[i];
@@ -516,14 +518,13 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
activityLabel.Text = menuItem.Text;
List<Task> tasks = new List<Task>();
foreach (var iSmartReader in ProcessData.SmartHeadsUni)
for (int position = 0; position < iperlHeads.Count; position++)
{
if (!(iSmartReader is SmartReader iHead))
if (!(iperlHeads[position] is SmartReader iHead))
{
continue;//ignore different types of heads
}
int position = iHead.Position;
if (position < 0 || position >= checkBoxes.Length || position >= messages.Length)
{
continue; // Skip this head if position is out of range
@@ -617,4 +618,4 @@ namespace TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
}
}
}
+28
View File
@@ -0,0 +1,28 @@
using System.Collections.Generic;
namespace TBF
{
/// <summary>
/// User selection of smart register readers. The selection is deliberately
/// kept per reader family because reader positions are only meaningful inside
/// one configured family.
/// </summary>
public sealed class SmartReaderSelectionSettings
{
public bool LegacyOptoHeadsEnabledMigrated;
public List<SmartReaderFamilySelection> Families = new List<SmartReaderFamilySelection>();
}
public sealed class SmartReaderFamilySelection
{
public string FamilyKey;
public List<SmartReaderSelectionItem> Readers = new List<SmartReaderSelectionItem>();
}
public sealed class SmartReaderSelectionItem
{
/// <summary>Stable configured component name; not a visual row index.</summary>
public string ReaderKey;
public bool Enabled;
}
}
+35
View File
@@ -1633,7 +1633,20 @@
<Compile Include="Rig\RegisterReaders\AllyReader\AllyMeterSize.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\AllyOpticalSample.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\AllyReaderCfg.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\AllyReaderCfgCtrl.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Include="Rig\RegisterReaders\AllyReader\AllyReaderCfgCtrl.Designer.cs">
<DependentUpon>AllyReaderCfgCtrl.cs</DependentUpon>
</Compile>
<Compile Include="Rig\RegisterReaders\AllyReader\AllyReaderManualTestCtrl.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Include="Rig\RegisterReaders\AllyReader\AllyReaderManualTestCtrl.Designer.cs">
<DependentUpon>AllyReaderManualTestCtrl.cs</DependentUpon>
</Compile>
<Compile Include="Rig\RegisterReaders\AllyReader\Communication\AllyCommandService.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\Communication\AllyFactoryUnsealData.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\Communication\AllyFrame.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\Communication\AllyFrameBuilder.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\Communication\AllyFrameParser.cs" />
@@ -1643,6 +1656,9 @@
<Compile Include="Rig\RegisterReaders\AllyReader\Communication\AllyVersionInfo.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\Communication\IAllyTransport.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\Factory.cs" />
<Compile Include="Rig\RegisterReaders\AsicReader\AsicReader.cs" />
<Compile Include="Rig\RegisterReaders\AsicReader\AsicReaderCfg.cs" />
<Compile Include="Rig\RegisterReaders\AsicReader\Factory.cs" />
<Compile Include="Rig\RegisterReaders\GenesisRegReader\GenesisCfgCtrl.designer.cs">
<DependentUpon>GenesisCfgCtrl.cs</DependentUpon>
</Compile>
@@ -1746,6 +1762,7 @@
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\JsonDataFromPoseidon.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\PoseidonCfg.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\PoseidonProcParams.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\PoseidonReadCycle.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\PoseidonReader.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\ProcessExtensions.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\ScopedLoggerFactory.cs" />
@@ -1969,10 +1986,13 @@
<Compile Include="Rig\TestMethods\FlyingStartMassCollection\Single\TestParams.cs" />
<Compile Include="Rig\TestMethods\AllyCalibration\AllyCalibrationActivity.cs" />
<Compile Include="Rig\TestMethods\AllyCalibration\AllyCalibrationSeq.cs" />
<Compile Include="Rig\TestMethods\AllyCalibration\AllyCommunicationSeq.cs" />
<Compile Include="Rig\TestMethods\AllyCalibration\Factory.cs" />
<Compile Include="Rig\TestMethods\AllyCalibration\TestMethod.cs" />
<Compile Include="Rig\TestMethods\AllyCalibration\TestMethodCfg.cs" />
<Compile Include="Rig\TestMethods\AllyCalibration\TestMethodParams.cs" />
<Compile Include="Rig\TestMethods\AsicTest\Factory.cs" />
<Compile Include="Rig\TestMethods\AsicTest\TestMethod.cs" />
<Compile Include="Rig\TestMethods\GenesisCommunication\Factory.cs" />
<Compile Include="Rig\TestMethods\GenesisCommunication\GenesisCommunicationSeq.cs" />
<Compile Include="Rig\TestMethods\GenesisCommunication\iPerlCommunicationParams.cs" />
@@ -2558,7 +2578,13 @@
<Compile Include="Rig\Uni\SharedDialogs\SmartMetersCommunication\common\ICorrections.cs" />
<Compile Include="Rig\Uni\SharedDialogs\SmartMetersCommunication\common\ISmartReader.cs" />
<Compile Include="Rig\Uni\SharedDialogs\SmartMetersCommunication\implementations\EnumExtensions.cs" />
<Compile Include="Rig\Uni\SharedDialogs\SmartMetersCommunication\implementations\ManualSmartCorrectionsBase.cs" />
<Compile Include="Rig\Uni\SharedDialogs\SmartMetersCommunication\implementations\GenesisCorrections.cs" />
<Compile Include="Rig\Uni\SharedDialogs\SmartMetersCommunication\implementations\AllyCorrections.cs" />
<Compile Include="Rig\Uni\SharedDialogs\SmartMetersCommunication\implementations\AllyTestCorrections.cs" />
<Compile Include="Rig\Uni\SharedDialogs\SmartMetersCommunication\implementations\AsicCorrections.cs" />
<Compile Include="Rig\Uni\SharedDialogs\SmartMetersCommunication\implementations\IPerlASICCorrections.cs" />
<Compile Include="Rig\Uni\SharedDialogs\SmartMetersCommunication\implementations\IPerlASICOpticalHeadTest.cs" />
<Compile Include="Rig\Uni\SharedDialogs\SmartMetersCommunication\implementations\IPerlCorrections.cs" />
<Compile Include="Rig\Uni\SharedDialogs\SmartMetersCommunication\implementations\PoseidonCorrections.cs" />
<Compile Include="Rig\Uni\SharedDialogs\SmartMetersCommunication\SmartComponentBase.cs" />
@@ -2567,6 +2593,7 @@
<Compile Include="Rig\Uni\SharedDialogs\SmartMetersCommunication\SmartCommunicationForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Rig\Uni\SharedDialogs\SmartMetersCommunication\SmartReaderSelection.cs" />
<Compile Include="Rig\Uni\SharedDialogs\SmartMetersCommunication\SmartCommunicationForm.designer.cs">
<DependentUpon>SmartCommunicationForm.cs</DependentUpon>
</Compile>
@@ -2817,6 +2844,7 @@
<Compile Include="Rig\State.cs" />
<Compile Include="Rig\Transition.cs" />
<Compile Include="LocalSettings.cs" />
<Compile Include="SmartReaderSelectionSettings.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Rig\Generic\IDevice.cs" />
@@ -3854,6 +3882,12 @@
</EmbeddedResource>
<EmbeddedResource Include="Rig\RegisterReaders\iPerlASICReader\IperlASICUniHeadTestCtrl.resx" />
<EmbeddedResource Include="Rig\RegisterReaders\iPerlASICReader\IPerlUniCfgCtrl.resx" />
<EmbeddedResource Include="Rig\RegisterReaders\AllyReader\AllyReaderCfgCtrl.resx">
<DependentUpon>AllyReaderCfgCtrl.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Rig\RegisterReaders\AllyReader\AllyReaderManualTestCtrl.resx">
<DependentUpon>AllyReaderManualTestCtrl.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Rig\RegisterReaders\iPerlReaderUNI\IPerlUniCfgCtrl.resx">
<DependentUpon>IPerlUniCfgCtrl.cs</DependentUpon>
</EmbeddedResource>
@@ -4523,6 +4557,7 @@
<Content Include="Rig\Modbus\TempControl\Pictures\TControl-XL-cooling.png" />
<Content Include="Rig\Modbus\TempControl\Pictures\TControl-XL-heating.png" />
<Content Include="Rig\Modbus\TempControl\Pictures\TControl-XL-idle.png" />
<Content Include="Rig\TestMethods\AllyCalibration\ALLY_OPTICAL_STREAM_SEQUENCE.md" />
<Content Include="SampleConfig\config.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
+1 -1
View File
@@ -1199,7 +1199,7 @@ namespace TBF.UI
private void optoHeadsToolStripMenuItem_Click(object sender, EventArgs e)
{
new iPerlCommunicationForm(true).ShowDialog();
new SmartCommunicationForm(true).ShowDialog();
}
private void statusStrip1_DoubleClick(object sender, EventArgs e)
@@ -2,8 +2,10 @@ using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Results.Entities;
using TBF;
using TBF.Rig.DataEntry.Uni;
using TBF.Rig.GenericDevices;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication;
namespace TBFTests.Rig.DataEntry.Uni
{
@@ -83,12 +85,42 @@ namespace TBFTests.Rig.DataEntry.Uni
Assert.IsFalse(selected);
}
private static Mock<IRegReader> CreateReader(int position, bool smart = false)
[TestMethod]
public void SmartReader_PerFamilySelection_IsUsedInsteadOfLegacyPositionMask()
{
var reader = CreateReader(1, true, "iPerl01");
var settings = new LocalSettings
{
SmartReaderSelections = new SmartReaderSelectionSettings
{
LegacyOptoHeadsEnabledMigrated = true,
Families = new List<SmartReaderFamilySelection>
{
new SmartReaderFamilySelection
{
FamilyKey = "iperl",
Readers = new List<SmartReaderSelectionItem>
{
new SmartReaderSelectionItem { ReaderKey = "iPerl01", Enabled = false }
}
}
}
}
};
bool selected = RegisterReaderSelection.IsSelectedForDataEntry(
reader.Object, null, settings, long.MaxValue);
Assert.IsFalse(selected);
}
private static Mock<IRegReader> CreateReader(int position, bool smart = false, string name = null)
{
var reader = new Mock<IRegReader>();
if (smart)
reader.As<ISmartMeterReader>();
reader.SetupGet(item => item.Position).Returns(position);
reader.SetupGet(item => item.Name).Returns(name);
return reader;
}
}
@@ -1,6 +1,5 @@
using System;
using System.IO;
using System.Reflection;
using Common;
using JetBrains.Annotations;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -97,24 +96,75 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader
}
[TestMethod]
public void ProcessOpticalText_VolumeAndTimestampRollover_ProducesContinuousMeasurement()
public void ProcessOpticalText_32BitVolumeRollover_ProducesContinuousMeasurementUsingHostElapsedTime()
{
AllyMeterReader reader = CreateReader(AllyMeterSize.FiveEighths);
reader.Initialize();
reader.Start();
string input =
AllyOpticalTelegramFactory.Create(10, 0xFFFFF0, 0xFFFFFF00) +
AllyOpticalTelegramFactory.Create(11, 0x000010, 0x00000100);
InvokeProcessOpticalText(reader, input);
AllyOpticalTelegramFactory.CreateC6(10, 0, 0, 10, 0xFFFFFFF0, 0, 0, 0, 0, 0, 0, 0) +
AllyOpticalTelegramFactory.CreateC6(11, 0, 0, 11, 0x00000010, 0, 0, 0, 0, 0, 0, 0);
reader.ProcessOpticalTextForTest(
input,
new DateTime(2026, 9, 16, 8, 0, 0, DateTimeKind.Utc));
reader.Stop();
Assert.AreEqual(2, reader.OpticalSamples.Count);
Assert.IsFalse(reader.NoSamples);
Assert.AreEqual(32D / 16000D, reader.WMVolume, 1E-9);
Assert.AreEqual(512D / 8192D,
reader.TimestampSecEnd - reader.TimestampSecStart, 1E-9);
Assert.AreEqual(2, reader.WMPulses);
Assert.AreEqual(32D / 4000D, reader.WMVolume, 1E-9);
// ALLY C6 does not carry an ASIC timestamp; elapsed time is based on the host receipt time.
Assert.IsTrue(reader.TimestampSecEnd >= reader.TimestampSecStart);
Assert.AreEqual(8, reader.WMPulses);
}
[TestMethod]
public void ProcessOpticalText_AutoDetect_TransfersDecodedStartAndEndVolumes()
{
AllyMeterReader reader = CreateReader(AllyMeterSize.AutoDetect);
reader.Initialize();
reader.Start();
DateTime start = new DateTime(2026, 9, 15, 10, 40, 0, DateTimeKind.Utc);
reader.ProcessOpticalTextForTest(
AllyOpticalTelegramFactory.CreateC6(1, 0, 0, 8, 100U, 0, 0, 0, 0, 0, 0, 0),
start);
reader.ProcessOpticalTextForTest(
AllyOpticalTelegramFactory.CreateC6(2, 0, 0, 8, 140U, 0, 0, 0, 0, 0, 0, 0),
start.AddSeconds(3));
reader.Stop();
Assert.IsTrue(reader.IsOpticalVolumeConversionConfigured);
Assert.IsFalse(reader.NoSamples);
Assert.AreEqual(100D / 4000D, reader.BeginWMState, 1E-12);
Assert.AreEqual(140D / 4000D, reader.EndWMState, 1E-12);
Assert.AreEqual(40D / 4000D, reader.WMVolume, 1E-12);
Assert.AreEqual(3D, reader.TimestampSecEnd - reader.TimestampSecStart, 1E-12);
}
[TestMethod]
public void ProcessOpticalText_VolumeAndSequenceRollover_KeepMeasurementAndHostTimeContinuous()
{
AllyMeterReader reader = CreateReader(AllyMeterSize.AutoDetect);
reader.Initialize();
reader.Start();
DateTime beforeMidnight = new DateTime(2026, 12, 31, 23, 59, 59, 900, DateTimeKind.Utc);
reader.ProcessOpticalTextForTest(
AllyOpticalTelegramFactory.CreateC6(0xFF, 0, 0, 4, 0xFFFFFFFEU, 0, 0, 0, 0, 0, 0, 0),
beforeMidnight);
reader.ProcessOpticalTextForTest(
AllyOpticalTelegramFactory.CreateC6(0x00, 0, 0, 4, 0x00000002U, 0, 0, 0, 0, 0, 0, 0),
beforeMidnight.AddMilliseconds(200));
reader.Stop();
Assert.AreEqual((byte)0xFF, reader.OpticalSamples[0].Sequence);
Assert.AreEqual((byte)0x00, reader.OpticalSamples[1].Sequence);
// The delta is calculated from two ~1,073,741 l double values after
// 32-bit rollover. Allow binary floating-point cancellation far below
// a single C6 quarter-millilitre unit (0.00025 l).
Assert.AreEqual(4D / 4000D, reader.WMVolume, 1E-9);
Assert.AreEqual(0.2D, reader.TimestampSecEnd - reader.TimestampSecStart, 1E-9);
}
[TestMethod]
@@ -172,14 +222,5 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader
};
}
private static void InvokeProcessOpticalText(AllyMeterReader reader, string text)
{
MethodInfo method = typeof(AllyMeterReader).GetMethod(
"ProcessOpticalText",
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.IsNotNull(method, "ProcessOpticalText method was not found.");
method.Invoke(reader, new object[] { text });
}
}
}
@@ -0,0 +1,161 @@
using System;
using System.Globalization;
using Common;
using JetBrains.Annotations;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.RegisterReaders.AllyReader;
namespace TBFTests.Rig.RegisterReaders.AllyReader
{
/// <summary>
/// Regression coverage for C6 telegrams captured from an ALLY optical sensor.
/// The test output deliberately documents the decoded values and their units.
/// </summary>
[TestClass]
[TestSubject(typeof(AllyOpticalSample))]
public class AllyOpticalRealExamplesTest
{
public TestContext TestContext { get; set; }
[TestMethod]
public void TryParse_RealC6Examples_ParsesValuesAndDocumentsUnits()
{
AllyOpticalSample first = Parse("17\tC6\taQcAABgQAABgEgAA8AAAALbo//8A6AMAAA==\t8513\r\n");
Assert.AreEqual((byte)0x17, first.Sequence);
Assert.AreEqual(1897, first.RawAdc);
Assert.AreEqual((short)4120, first.LastField);
Assert.AreEqual((short)0, first.RawFlow);
Assert.AreEqual(0D, first.FlowMillilitersPerSecond, 1E-12);
Assert.AreEqual(4704U, first.RawVolume);
Assert.AreEqual(1.176D, first.VolumeLiters, 1E-12);
Assert.AreEqual((ushort)240, first.FlipPeriod);
Assert.AreEqual((ushort)0, first.VinfStart);
Assert.AreEqual((ushort)59574, first.VinfEnd);
Assert.AreEqual((short)-1, first.ElectrodeDelta);
Assert.AreEqual((ushort)59392, first.Impedance);
Assert.AreEqual((byte)3, first.FieldDriveTime);
Assert.AreEqual((byte)0, first.Flags);
Assert.IsFalse(first.IsEmptyPipe);
Assert.IsFalse(first.IsFastHptc);
Assert.AreEqual(0x8513, first.PacketChecksum);
CollectionAssert.AreEqual(new byte[] { 0 }, first.ExtensionBytes);
AllyOpticalSample second = Parse("13\tC6\tSAoAAJ4OAABOEgAA8AAAAMby//8A6AMAAA==\t4A31\r\n");
Assert.AreEqual((byte)0x13, second.Sequence);
Assert.AreEqual(2632, second.RawAdc);
Assert.AreEqual((short)3742, second.LastField);
Assert.AreEqual((short)0, second.RawFlow);
Assert.AreEqual(0D, second.FlowMillilitersPerSecond, 1E-12);
Assert.AreEqual(4686U, second.RawVolume);
Assert.AreEqual(1.1715D, second.VolumeLiters, 1E-12);
Assert.AreEqual((ushort)240, second.FlipPeriod);
Assert.AreEqual((ushort)62150, second.VinfEnd);
Assert.AreEqual((short)-1, second.ElectrodeDelta);
Assert.AreEqual((ushort)59392, second.Impedance);
Assert.AreEqual((byte)3, second.FieldDriveTime);
Assert.AreEqual((byte)0, second.Flags);
Assert.AreEqual(0x4A31, second.PacketChecksum);
CollectionAssert.AreEqual(new byte[] { 0 }, second.ExtensionBytes);
WriteDecodedValues("example 1", first);
WriteDecodedValues("example 2", second);
}
[TestMethod]
public void Decode_TenC6Samples_AcrossSequenceTimeAndVolumeRollover_ReportsStartEndAndDelta()
{
AllyReaderCfg cfg = new AllyReaderCfg(new TBF.Rig.RegisterReaders.AllyReader.Factory())
{
DebugLevel = DebugMode.Simulate,
ConfiguredMeterSize = AllyMeterSize.AutoDetect,
Name = "AllyRolloverTest"
};
AllyMeterReader reader = new AllyMeterReader(cfg);
reader.Initialize();
reader.Start();
// ALLY C6 has no device timestamp. The time rollover therefore means
// the host receipt time crossing midnight, while the 32-bit C6 volume
// accumulator wraps from FFFFFFFF to 00000000.
DateTime firstReceivedAt = new DateTime(2026, 12, 31, 23, 59, 57, 750, DateTimeKind.Utc);
const uint firstRawVolume = 0xFFFFFFF0U;
const uint rawIncrement = 4U; // one millilitre per sample
for (int index = 0; index < 10; index++)
{
uint rawVolume = unchecked(firstRawVolume + rawIncrement * (uint)index);
byte sequence = unchecked((byte)(0xF8 + index));
DateTime receivedAt = firstReceivedAt.AddMilliseconds(500 * index);
string telegram = AllyOpticalTelegramFactory.CreateC6(
sequence, 1000 + index, 0, 4, rawVolume, 240, 0, 0, -1, 59392, 3, 0);
reader.ProcessOpticalTextForTest(telegram, receivedAt);
AllyOpticalSample decoded = reader.OpticalSamples[index];
WriteDecodedValues("rollover sample " + (index + 1), decoded);
}
reader.Stop();
Assert.AreEqual(10, reader.OpticalSamples.Count);
Assert.AreEqual((byte)0xF8, reader.OpticalSamples[0].Sequence);
Assert.AreEqual((byte)0x01, reader.OpticalSamples[9].Sequence);
Assert.AreEqual(firstRawVolume, reader.OpticalSamples[0].RawVolume);
Assert.AreEqual(0x00000014U, reader.OpticalSamples[9].RawVolume);
Assert.AreEqual(9D * rawIncrement / 4000D, reader.WMVolume, 1E-9);
Assert.AreEqual(4.5D, reader.TimestampSecEnd - reader.TimestampSecStart, 1E-9);
Assert.IsTrue(reader.OpticalSamples[5].ReceivedAtUtc.Date > firstReceivedAt.Date);
if (TestContext != null)
{
TestContext.WriteLine(
"ALLY rollover result | samples={0}; start={1:F9} l; end={2:F9} l; delta={3:F9} l; elapsed={4:F3} s",
reader.OpticalSamples.Count,
reader.BeginWMState,
reader.EndWMState,
reader.WMVolume,
reader.TimestampSecEnd - reader.TimestampSecStart);
}
}
private static AllyOpticalSample Parse(string telegram)
{
AllyOpticalSample sample;
bool parsed = AllyOpticalSample.TryParse(
telegram,
new DateTime(2026, 9, 10, 8, 55, 52, DateTimeKind.Utc),
out sample);
Assert.IsTrue(parsed, "The captured C6 optical telegram must parse.");
Assert.IsNotNull(sample);
return sample;
}
private void WriteDecodedValues(string name, AllyOpticalSample sample)
{
string output = string.Format(
CultureInfo.InvariantCulture,
"ALLY C6 {0} | sequence=0x{1:X2}; ADC={2} raw counts; field={3} raw counts; flow={4} quarter-mL/s ({5:F3} mL/s); accumulator={6} quarter-mL ({7:F6} l); flipPeriod={8} raw ticks; VinfStart={9} raw; VinfEnd={10} raw; electrodeDelta={11} mV; impedance={12} raw; fieldDriveTime={13} us; flags=0x{14:X2}; emptyPipe={15}; fastHptc={16}; checksum=0x{17:X4} (preserved, not CRC-validated); extension={18}",
name,
sample.Sequence,
sample.RawAdc,
sample.LastField,
sample.RawFlow,
sample.FlowMillilitersPerSecond,
sample.RawVolume,
sample.VolumeLiters,
sample.FlipPeriod,
sample.VinfStart,
sample.VinfEnd,
sample.ElectrodeDelta,
sample.Impedance,
sample.FieldDriveTime,
sample.Flags,
sample.IsEmptyPipe,
sample.IsFastHptc,
sample.PacketChecksum,
BitConverter.ToString(sample.ExtensionBytes));
if (TestContext != null)
TestContext.WriteLine(output);
}
}
}
@@ -10,56 +10,59 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader
public class AllyOpticalSampleTest
{
[TestMethod]
public void TryParse_ValidTelegram_ParsesFlowVolumeAndTimestamp()
public void TryParse_RealC6Telegram_ParsesAllyMetrologyFields()
{
string telegram = AllyOpticalTelegramFactory.Create(-2, 0x123456, 0x89ABCDEF);
DateTime receivedAt = new DateTime(2026, 8, 14, 10, 0, 0, DateTimeKind.Utc);
const string telegram = "17\tC6\taQcAABgQAABgEgAA8AAAALbo//8A6AMAAA==\t8513\r\n";
DateTime receivedAt = new DateTime(2026, 9, 10, 8, 55, 52, DateTimeKind.Utc);
AllyOpticalSample sample;
bool result = AllyOpticalSample.TryParse(telegram, receivedAt, out sample);
Assert.IsTrue(result);
Assert.IsNotNull(sample);
Assert.AreEqual((short)-2, sample.RawFlow);
Assert.AreEqual(0x123456U, sample.RawVolume);
Assert.AreEqual(0x89ABCDEFU, sample.RawTimestamp);
Assert.AreEqual((byte)0x17, sample.Sequence);
Assert.AreEqual((byte)0xC6, sample.PacketType);
Assert.AreEqual(0x8513, sample.PacketChecksum);
Assert.AreEqual(1897, sample.RawAdc);
Assert.AreEqual((short)0x1018, sample.LastField);
Assert.AreEqual(0x1260U, sample.RawVolume);
Assert.AreEqual((ushort)0x00F0, sample.FlipPeriod);
Assert.AreEqual((short)-1, sample.ElectrodeDelta);
Assert.AreEqual(1, sample.ExtensionBytes.Length);
Assert.AreEqual(receivedAt, sample.ReceivedAtUtc);
Assert.AreEqual(telegram, sample.RawLine);
}
[TestMethod]
public void TryParse_PrefixedValidTelegram_UsesLastCompleteTelegram()
public void TryParse_C6Telegram_ParsesFlags()
{
string telegram = AllyOpticalTelegramFactory.Create(1, 2, 3);
string telegram = AllyOpticalTelegramFactory.CreateC6(1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0x07);
AllyOpticalSample sample;
bool result = AllyOpticalSample.TryParse("noise" + telegram, DateTime.UtcNow, out sample);
bool result = AllyOpticalSample.TryParse(telegram, DateTime.UtcNow, out sample);
Assert.IsTrue(result);
Assert.AreEqual(2U, sample.RawVolume);
Assert.AreEqual(3U, sample.RawTimestamp);
Assert.AreEqual(telegram, sample.RawLine);
Assert.IsTrue(sample.IsLowFlow);
Assert.IsTrue(sample.IsEmptyPipe);
Assert.IsTrue(sample.IsFastHptc);
}
[TestMethod]
public void TryParse_CorruptChecksum_ReturnsFalse()
public void TryParse_NonMetrologyPacket_ReturnsFalse()
{
string telegram = AllyOpticalTelegramFactory.Create(1, 2, 3);
string corrupt = (telegram[0] == '0' ? "1" : "0") + telegram.Substring(1);
AllyOpticalSample sample;
bool result = AllyOpticalSample.TryParse(corrupt, DateTime.UtcNow, out sample);
bool result = AllyOpticalSample.TryParse("12\tC2\tOwIAAFAiAAAAAAAAACBYDgwOAACXylAS\t7947\r\n", DateTime.UtcNow, out sample);
Assert.IsFalse(result);
Assert.IsNull(sample);
}
[TestMethod]
public void TryParse_InvalidStructure_ReturnsFalse()
public void TryParse_InvalidBase64_ReturnsFalse()
{
AllyOpticalSample sample;
bool result = AllyOpticalSample.TryParse(
"000000 0000 0000 000001 0000 00000001 00\r\n",
"17\tC6\tinvalid\t8513\r\n",
DateTime.UtcNow,
out sample);
@@ -1,27 +1,27 @@
using System.Globalization;
using System;
namespace TBFTests.Rig.RegisterReaders.AllyReader
{
internal static class AllyOpticalTelegramFactory
{
public static string Create(short rawFlow, uint rawVolume, uint rawTimestamp)
public static string CreateC6(byte sequence, int rawAdc, short lastField, short rawFlow, uint rawVolume,
ushort flipPeriod, ushort vinfStart, ushort vinfEnd, short electrodeDelta, ushort impedance,
byte fieldDriveTime, byte flags, byte extension = 0)
{
string prefix = string.Join("\t", new[]
{
"000000",
"0000",
unchecked((ushort)rawFlow).ToString("X4", CultureInfo.InvariantCulture),
rawVolume.ToString("X6", CultureInfo.InvariantCulture),
"0000",
rawTimestamp.ToString("X8", CultureInfo.InvariantCulture),
string.Empty
});
byte checksum = 0;
for (int i = 0; i < prefix.Length; i++)
checksum += (byte)prefix[i];
return prefix + checksum.ToString("X2", CultureInfo.InvariantCulture) + "\r\n";
byte[] payload = new byte[25];
Buffer.BlockCopy(BitConverter.GetBytes(rawAdc), 0, payload, 0, 4);
Buffer.BlockCopy(BitConverter.GetBytes(lastField), 0, payload, 4, 2);
Buffer.BlockCopy(BitConverter.GetBytes(rawFlow), 0, payload, 6, 2);
Buffer.BlockCopy(BitConverter.GetBytes(rawVolume), 0, payload, 8, 4);
Buffer.BlockCopy(BitConverter.GetBytes(flipPeriod), 0, payload, 12, 2);
Buffer.BlockCopy(BitConverter.GetBytes(vinfStart), 0, payload, 14, 2);
Buffer.BlockCopy(BitConverter.GetBytes(vinfEnd), 0, payload, 16, 2);
Buffer.BlockCopy(BitConverter.GetBytes(electrodeDelta), 0, payload, 18, 2);
Buffer.BlockCopy(BitConverter.GetBytes(impedance), 0, payload, 20, 2);
payload[22] = fieldDriveTime;
payload[23] = flags;
payload[24] = extension;
return sequence.ToString("X2") + "\tC6\t" + Convert.ToBase64String(payload) + "\t0000\r\n";
}
}
}
@@ -81,6 +81,93 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.communication
AssertRequest(0x53, 0x57, 0x07, 0xFD, 0x60, 0xC2, 0x0D);
}
[TestMethod]
public void OpticalVerificationOutput_UnsealedMeter_UsesDocumentedStartAndStopSequence()
{
transport.QueueResponse(AllyResponseFactory.CreateSuccess(0x00));
service.StartOpticalVerificationOutput(TimeoutMs);
Assert.AreEqual(5, transport.Requests.Count);
CollectionAssert.AreEqual(new byte[] { 0x53, 0x57, 0x06, 0xFD, 0x63, 0x0D }, transport.Requests[0]);
CollectionAssert.AreEqual(new byte[] { 0x53, 0x57, 0x07, 0x1E, 0x00, 0x02, 0x0D }, transport.Requests[1]);
CollectionAssert.AreEqual(new byte[] { 0x53, 0x57, 0x08, 0xFD, 0x15, 0x06, 0x01, 0x0D }, transport.Requests[2]);
CollectionAssert.AreEqual(new byte[] { 0x53, 0x57, 0x06, 0x1A, 0x09, 0x0D }, transport.Requests[3]);
CollectionAssert.AreEqual(new byte[] { 0x53, 0x57, 0x07, 0xFD, 0x60, 0xC2, 0x0D }, transport.Requests[4]);
transport.Requests.Clear();
service.StopOpticalVerificationOutput(TimeoutMs);
CollectionAssert.AreEqual(new byte[] { 0x53, 0x57, 0x07, 0xFD, 0x60, 0x00, 0x0D }, transport.Requests[0]);
CollectionAssert.AreEqual(new byte[] { 0x53, 0x57, 0x06, 0x1A, 0x02, 0x0D }, transport.Requests[1]);
CollectionAssert.AreEqual(new byte[] { 0x53, 0x57, 0x08, 0xFD, 0x15, 0x06, 0x00, 0x0D }, transport.Requests[2]);
}
[TestMethod]
public void OpticalVerificationOutput_SealedMeter_StopsBeforeAnyStateChangingCommand()
{
transport.Response = AllyResponseFactory.CreateSuccess(0x01);
try
{
service.StartOpticalVerificationOutput(TimeoutMs);
Assert.Fail("Expected the factory-seal guard to reject optical output activation.");
}
catch (InvalidOperationException exception)
{
StringAssert.Contains(exception.Message, "factory sealed");
}
Assert.AreEqual(1, transport.Requests.Count);
CollectionAssert.AreEqual(new byte[] { 0x53, 0x57, 0x06, 0xFD, 0x63, 0x0D }, transport.Requests[0]);
}
[TestMethod]
public void FactorySealCommands_ReadMeterSpecificInputsAndBuildProvidedVerificationBenchFrame()
{
transport.QueueResponse(AllyResponseFactory.CreateSuccess(0x01));
transport.QueueResponse(AllyResponseFactory.CreateAsciiSuccess("Customer Text 123456"));
transport.QueueResponse(AllyResponseFactory.CreateAsciiSuccess("RB252601C144"));
transport.QueueResponse(AllyResponseFactory.CreateAsciiSuccess("00001000"));
transport.QueueResponse(AllyResponseFactory.CreateSuccess(0xD0, 0xC5, 0x5D, 0x00));
AllyFactoryUnsealData data = service.ReadFactoryUnsealData(TimeoutMs);
Assert.IsTrue(data.IsSealed);
Assert.AreEqual("RB252601C144", data.FactoryId);
Assert.AreEqual("Customer Text 123456", data.ProgrammableText);
Assert.AreEqual("00001000", data.ReadingPreset);
Assert.AreEqual((uint)6145488, data.SecondsActive);
CollectionAssert.AreEqual(
new byte[] { 0xC5, 0x5D, 0xF1, 0xF8, 0x53, 0x45, 0x09, 0x00 },
data.Credential);
CollectionAssert.AreEqual(
new byte[] { 0x53, 0x57, 0x06, 0xFD, 0x63, 0x0D },
transport.Requests[0]);
CollectionAssert.AreEqual(
new byte[] { 0x53, 0x57, 0x05, 0x07, 0x0D },
transport.Requests[1]);
CollectionAssert.AreEqual(
new byte[] { 0x53, 0x57, 0x05, 0x01, 0x0D },
transport.Requests[2]);
CollectionAssert.AreEqual(
new byte[] { 0x53, 0x57, 0x05, 0x13, 0x0D },
transport.Requests[3]);
CollectionAssert.AreEqual(
new byte[] { 0x53, 0x57, 0x06, 0xFD, 0x3D, 0x0D },
transport.Requests[4]);
service.UnsealFactory(data, TimeoutMs);
AssertRequest(
0x53, 0x57, 0x0F, 0xFD, 0x64,
0x00, 0xC5, 0x5D, 0xF1, 0xF8, 0x53, 0x45, 0x09, 0x00,
0x0D);
service.SealFactory(TimeoutMs);
AssertRequest(0x53, 0x57, 0x07, 0xFD, 0x64, 0x01, 0x0D);
}
[TestMethod]
public void CalibrationFactor_WriteAndRead_UsesFactorTimes40Point96Encoding()
{
@@ -12,25 +12,27 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.communication
{
/// <summary>
/// Direct hardware smoke test modelled after IperlHatIntegrationTests.
/// COM3 is the ALLY Touch-Read connection and COM4 is the optical source.
/// COM12 is the ALLY Touch-Read connection and COM13 is the optical source.
/// </summary>
[TestClass]
[TestCategory("HardwareIntegration")]
[DoNotParallelize]
public class AllyIntegrationTests
{
private const string CommandComPort = "COM3";
private const string OpticalComPort = "COM4";
private const string CommandComPort = "COM12";
private const string OpticalComPort = "COM13";
private const int CommandBaudRate = 2400;
private const int OpticalBaudRate = 9600;
private const int OpticalBaudRate = 38400;
private const int ReadTimeoutMs = 5000;
private const int OpticalReadSeconds = 10;
private const byte ActiveMeterMode = 0x02;
private const byte InitialMeterMode = 0x09;
private const byte DiagnosticLedCalibrationMode = 0xC2;
[TestMethod]
[TestCategory("Hardware")]
[TestCategory("Serial")]
public void Serial_ReadFactoryId_SetActive_ReadOptical_SetInitial()
public void Integration_Serial_ReadFactoryId_PrepareOptical_ReadOptical_SetActive()
{
Console.WriteLine(
"ALLY integration test: command={0}/{1}, optical={2}/{3}",
@@ -52,7 +54,9 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.communication
Console.WriteLine("OPEN command port " + CommandComPort);
commandPort.Open();
bool activeModeCommandAttempted = false;
bool initialModeCommandAttempted = false;
bool diagnosticLedCommandAttempted = false;
bool spreadSpectrumDisableAttempted = false;
Exception testFailure = null;
try
@@ -71,13 +75,38 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.communication
string.IsNullOrWhiteSpace(serialNumber),
"ViewFactoryId returned an empty manufacturing serial number.");
byte[] activeModeRequest = new AllyFrameBuilder()
SendRequest(
commandPort,
new AllyFrameBuilder()
.WithCommand(AllyCommand.SetValvePosition)
.WithBytes(0x00, 0x02)
.BuildBytes(),
"Open valve for calibration");
spreadSpectrumDisableAttempted = true;
SendRequest(
commandPort,
new AllyFrameBuilder()
.WithDeviceCommand(AllyDeviceCommand.Configuration)
.WithBytes(0x06, 0x01)
.BuildBytes(),
"Disable Spread Spectrum");
byte[] initialModeRequest = new AllyFrameBuilder()
.WithCommand(AllyCommand.SetMeterMode)
.WithByte(ActiveMeterMode)
.WithByte(InitialMeterMode)
.BuildBytes();
activeModeCommandAttempted = true;
SendRequest(commandPort, activeModeRequest, "SetMeterMode Active 0x02");
Console.WriteLine("STEP PASS Active meter mode 0x02 is acknowledged.");
initialModeCommandAttempted = true;
SendRequest(commandPort, initialModeRequest, "SetMeterMode Initial calibration 0x09");
diagnosticLedCommandAttempted = true;
SendRequest(
commandPort,
new AllyFrameBuilder()
.WithDeviceCommand(AllyDeviceCommand.SetDiagnosticLed)
.WithByte(DiagnosticLedCalibrationMode)
.BuildBytes(),
"Set diagnostic LED calibration 0xC2");
int parsedSamples = ReadOpticalSamples();
Assert.IsTrue(
@@ -93,22 +122,44 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.communication
}
finally
{
// Attempt the safe state even when the active command timed out: the
// meter may have accepted it while its response was lost.
if (activeModeCommandAttempted && commandPort.IsOpen)
if (commandPort.IsOpen)
{
try
{
byte[] initialModeRequest = new AllyFrameBuilder()
.WithCommand(AllyCommand.SetMeterMode)
.WithByte(InitialMeterMode)
.BuildBytes();
SendRequest(commandPort, initialModeRequest, "SetMeterMode Initial 0x09");
Console.WriteLine("STEP PASS Initial meter mode 0x09 is acknowledged.");
if (diagnosticLedCommandAttempted)
{
SendRequest(
commandPort,
new AllyFrameBuilder()
.WithDeviceCommand(AllyDeviceCommand.SetDiagnosticLed)
.WithByte(0x00)
.BuildBytes(),
"Set diagnostic LED off 0x00");
}
if (initialModeCommandAttempted)
{
SendRequest(
commandPort,
new AllyFrameBuilder()
.WithCommand(AllyCommand.SetMeterMode)
.WithByte(ActiveMeterMode)
.BuildBytes(),
"SetMeterMode Active 0x02");
}
if (spreadSpectrumDisableAttempted)
{
SendRequest(
commandPort,
new AllyFrameBuilder()
.WithDeviceCommand(AllyDeviceCommand.Configuration)
.WithBytes(0x06, 0x00)
.BuildBytes(),
"Enable Spread Spectrum");
}
}
catch (Exception restoreException)
{
Console.WriteLine("RESTORE FAIL Initial mode 0x09: " + restoreException);
Console.WriteLine("RESTORE FAIL calibration cleanup: " + restoreException);
if (testFailure == null)
throw;
}
@@ -238,11 +289,14 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.communication
parsedSamples++;
Console.WriteLine(
"OPTO PARSE PASS: sample={0}, flow={1}, rawVolume={2}, rawTimestamp={3}",
"OPTO PARSE PASS: sample={0}, type=0x{1:X2}, sequence=0x{2:X2}, flow={3}, rawVolume={4}, emptyPipe={5}, fastHptc={6}",
parsedSamples,
sample.PacketType,
sample.Sequence,
sample.RawFlow,
sample.RawVolume,
sample.RawTimestamp);
sample.IsEmptyPipe,
sample.IsFastHptc);
}
catch (TimeoutException)
{
@@ -1,4 +1,5 @@
using TBF.Rig.RegisterReaders.AllyReader.Communication;
using System.Collections.Generic;
namespace TBFTests.Rig.RegisterReaders.AllyReader.communication
{
@@ -11,6 +12,13 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.communication
public byte[] Response { get; set; }
public byte[] LastRequest { get; private set; }
public int LastTimeoutMs { get; private set; }
public IList<byte[]> Requests { get; private set; } = new List<byte[]>();
private readonly Queue<byte[]> responses = new Queue<byte[]>();
public void QueueResponse(byte[] response)
{
responses.Enqueue(response);
}
public void Open()
{
@@ -23,7 +31,8 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.communication
SendAndWaitCallCount++;
LastRequest = request;
LastTimeoutMs = timeoutMs;
return Response;
Requests.Add(request);
return responses.Count > 0 ? responses.Dequeue() : Response;
}
public void Dispose()
@@ -15,6 +15,7 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.Integration
public int OpticalCaptureSeconds { get; private set; }
public int ActiveModeSettleMs { get; private set; }
public AllyMeterSize MeterSize { get; private set; }
public bool UnsealMeterBeforeOpticalTest { get; private set; }
public string CommandPortName { get { return "COM" + CommandPortNumber; } }
public string OpticalPortName { get { return "COM" + OpticalPortNumber; } }
@@ -24,13 +25,14 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.Integration
return new AllyHardwareIntegrationSettings
{
Enabled = ParseEnabled(Environment.GetEnvironmentVariable("ALLY_HW_TESTS")),
CommandPortNumber = ParsePort("ALLY_COMMAND_PORT", "COM3"),
OpticalPortNumber = ParsePort("ALLY_OPTICAL_PORT", "COM4"),
CommandPortNumber = ParsePort("ALLY_COMMAND_PORT", "COM12"),
OpticalPortNumber = ParsePort("ALLY_OPTICAL_PORT", "COM13"),
CommandBaudRate = ParsePositiveInt("ALLY_COMMAND_BAUD", 2400),
OpticalBaudRate = ParsePositiveInt("ALLY_OPTICAL_BAUD", 9600),
OpticalBaudRate = ParsePositiveInt("ALLY_OPTICAL_BAUD", 38400),
CommandTimeoutMs = ParsePositiveInt("ALLY_COMMAND_TIMEOUT_MS", 5000),
OpticalCaptureSeconds = ParsePositiveInt("ALLY_OPTICAL_CAPTURE_SECONDS", 10),
ActiveModeSettleMs = ParsePositiveInt("ALLY_ACTIVE_SETTLE_MS", 1000),
UnsealMeterBeforeOpticalTest = ParseEnabled(Environment.GetEnvironmentVariable("ALLY_UNSEAL_METER")),
MeterSize = ParseMeterSize(Environment.GetEnvironmentVariable("ALLY_METER_SIZE"))
};
}
@@ -4,7 +4,6 @@ using System.Diagnostics;
using System.Globalization;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.RegisterReaders.AllyReader;
@@ -17,9 +16,6 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.Integration
[DoNotParallelize]
public class AllyHardwareIntegrationTest
{
private const byte ActiveMeterMode = 0x02;
private const byte InitialMeterMode = 0x09;
private AllyHardwareIntegrationSettings settings;
private AllyIntegrationTestLogger logger;
@@ -32,14 +28,15 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.Integration
logger = new AllyIntegrationTestLogger(TestContext);
logger.Log(string.Format(
CultureInfo.InvariantCulture,
"Configuration | command={0}/{1}, optical={2}/{3}, size={4}, timeout={5} ms, capture={6} s",
"Configuration | command={0}/{1}, optical={2}/{3}, size={4}, timeout={5} ms, capture={6} s, unseal={7}",
settings.CommandPortName,
settings.CommandBaudRate,
settings.OpticalPortName,
settings.OpticalBaudRate,
settings.MeterSize,
settings.CommandTimeoutMs,
settings.OpticalCaptureSeconds));
settings.OpticalCaptureSeconds,
settings.UnsealMeterBeforeOpticalTest));
if (!settings.Enabled)
{
@@ -78,7 +75,10 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.Integration
value.TouchReadVersion,
value.DeviceType,
value.FirmwareVersion));
Assert.AreEqual("SWM003", version.DeviceType, "The connected device is not an ALLY meter.");
Assert.AreEqual<string>(
"SWM003",
version.DeviceType,
"The connected device is not an ALLY meter.");
logger.Step("Read meter system time (UTC)", () => reader.ReadSystemTimeUtc(settings.CommandTimeoutMs),
value => value.ToString("O", CultureInfo.InvariantCulture) + "; parse=PASS");
@@ -92,46 +92,8 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.Integration
}
}
[TestMethod]
public void CommandPort_ActiveModeRoundTrip_AlwaysRestoresInitialMode()
{
RequirePorts(settings.CommandPortName);
AllyMeterReader reader = CreateReader();
reader.StartSession();
try
{
logger.Step(
"Set active meter mode 0x02",
() => reader.SetMeterMode(ActiveMeterMode, settings.CommandTimeoutMs));
logger.Log("WAIT | active-mode settling for " + settings.ActiveModeSettleMs + " ms");
Thread.Sleep(settings.ActiveModeSettleMs);
}
finally
{
try
{
logger.Step(
"Restore initial meter mode 0x09",
() => reader.SetMeterMode(InitialMeterMode, settings.CommandTimeoutMs));
}
finally
{
reader.EndSession();
logger.Log("SESSION | command port closed");
}
}
}
[TestMethod]
public void OpticalPort_CaptureAndParseTelegrams_LogsRawAndParseResult()
{
RequirePorts(settings.OpticalPortName);
IList<AllyOpticalSample> samples = CaptureOpticalTelegramsDirectly();
Assert.IsTrue(samples.Count > 0, "No valid ALLY optical telegram was parsed.");
}
[TestMethod]
public void Complex_ReadSerialActivateCaptureParseAndDeactivate_LogsWholeWorkflow()
{
RequirePorts(settings.CommandPortName, settings.OpticalPortName);
AllyMeterReader reader = CreateReader();
@@ -139,21 +101,50 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.Integration
reader.StartSession();
try
{
string serialNumber = logger.Step(
"Read manufacturing serial number",
() => reader.ReadSerialNumber(settings.CommandTimeoutMs));
Assert.IsFalse(string.IsNullOrWhiteSpace(serialNumber), "The manufacturing serial number is empty.");
logger.Step(
"Set active meter mode 0x02",
() => reader.SetMeterMode(ActiveMeterMode, settings.CommandTimeoutMs));
logger.Log("WAIT | active-mode settling for " + settings.ActiveModeSettleMs + " ms");
Thread.Sleep(settings.ActiveModeSettleMs);
logger.Step("Open optical stream and start measurement", reader.Start);
ReadSerialNumberAndFirmwareRevision(reader);
EnsureMeterIsUnsealed(reader);
logger.Step("Start ALLY optical verification stream and open " + settings.OpticalPortName,
() => reader.StartOpticalVerificationStream(settings.CommandTimeoutMs));
streamStarted = true;
CaptureThroughReader(reader);
IReadOnlyList<AllyOpticalSample> samples = reader.OpticalSamples;
Assert.IsTrue(samples.Count > 0, "No valid ALLY optical telegram was parsed.");
logger.Log("OPTO CAPTURE | PASS | parsed samples=" + samples.Count);
}
finally
{
try
{
if (streamStarted)
logger.Step("Stop ALLY optical verification stream and close " + settings.OpticalPortName,
() => reader.StopOpticalVerificationStream(settings.CommandTimeoutMs));
}
finally
{
reader.EndSession();
logger.Log("SESSION | optical port closed");
}
}
}
[TestMethod]
public void Complex_ReadSerialCaptureParseAndStop_LogsWholeWorkflow()
{
RequirePorts(settings.CommandPortName, settings.OpticalPortName);
AllyMeterReader reader = CreateReader();
bool streamStarted = false;
reader.StartSession();
try
{
string serialNumber = ReadSerialNumberAndFirmwareRevision(reader);
EnsureMeterIsUnsealed(reader);
logger.Step("Start ALLY optical verification stream and open " + settings.OpticalPortName,
() => reader.StartOpticalVerificationStream(settings.CommandTimeoutMs));
streamStarted = true;
logger.Step("Start optical measurement interval", reader.Start);
CaptureThroughReader(reader);
IReadOnlyList<AllyOpticalSample> samples = reader.OpticalSamples;
Assert.IsTrue(samples.Count >= 2, "At least two valid optical samples are required for a measurement.");
Assert.IsFalse(reader.NoSamples, "The reader did not establish a valid optical measurement interval.");
@@ -170,21 +161,13 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.Integration
try
{
if (streamStarted)
logger.Step("Stop optical measurement and close COM4", reader.Stop);
logger.Step("Stop ALLY optical verification stream and close " + settings.OpticalPortName,
() => reader.StopOpticalVerificationStream(settings.CommandTimeoutMs));
}
finally
{
try
{
logger.Step(
"Restore initial meter mode 0x09",
() => reader.SetMeterMode(InitialMeterMode, settings.CommandTimeoutMs));
}
finally
{
reader.EndSession();
logger.Log("SESSION | all ports closed");
}
reader.EndSession();
logger.Log("SESSION | all ports closed");
}
}
}
@@ -196,69 +179,62 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.Integration
return reader;
}
private IList<AllyOpticalSample> CaptureOpticalTelegramsDirectly()
private string ReadSerialNumberAndFirmwareRevision(AllyMeterReader reader)
{
List<AllyOpticalSample> samples = new List<AllyOpticalSample>();
StringBuilder buffer = new StringBuilder();
int parsedLines = 0;
int rejectedLines = 0;
string serialNumber = logger.Step(
"Read manufacturing serial number",
() => reader.ReadSerialNumber(settings.CommandTimeoutMs));
Assert.IsFalse(string.IsNullOrWhiteSpace(serialNumber), "The manufacturing serial number is empty.");
using (SerialPort port = new SerialPort(
settings.OpticalPortName,
settings.OpticalBaudRate,
Parity.None,
8,
StopBits.One))
logger.Step(
"Read firmware revision",
() => reader.ReadVersionAndType(settings.CommandTimeoutMs),
value => string.Format(
CultureInfo.InvariantCulture,
"raw={0}; type={1}; firmware={2}; parse=PASS",
value.RawValue,
value.DeviceType,
value.FirmwareVersion));
return serialNumber;
}
private void EnsureMeterIsUnsealed(AllyMeterReader reader)
{
bool isSealed = logger.Step("View factory seal", () => reader.IsFactorySealed(settings.CommandTimeoutMs),
value => value ? "sealed" : "unsealed");
if (!isSealed)
{
logger.Step("Open optical source " + settings.OpticalPortName, port.Open);
try
{
port.DiscardInBuffer();
Stopwatch stopwatch = Stopwatch.StartNew();
while (stopwatch.Elapsed < TimeSpan.FromSeconds(settings.OpticalCaptureSeconds))
{
string text = port.ReadExisting();
if (!string.IsNullOrEmpty(text))
buffer.Append(text);
string line;
while (TryTakeLine(buffer, out line))
{
AllyOpticalSample sample;
bool parsed = AllyOpticalSample.TryParse(line, DateTime.UtcNow, out sample);
logger.OpticalParse(line, parsed, sample);
if (parsed)
{
parsedLines++;
samples.Add(sample);
}
else
{
rejectedLines++;
}
}
Thread.Sleep(50);
}
}
finally
{
port.Close();
logger.Log("PORT | optical source closed");
}
logger.Log("FACTORY SEAL | meter is already unsealed.");
return;
}
if (buffer.Length > 0)
if (!settings.UnsealMeterBeforeOpticalTest)
{
logger.Log("OPTO PARTIAL | trailing incomplete data: " + buffer.ToString()
.Replace("\r", "\\r").Replace("\n", "\\n").Replace("\t", "\\t"));
const string message =
"ALLY meter is factory sealed. Set ALLY_UNSEAL_METER=1 to explicitly authorize " +
"the integration test to unseal this meter, then rerun the test.";
logger.Log("BLOCKED | " + message);
Assert.Inconclusive(message);
return;
}
logger.Log(string.Format(
CultureInfo.InvariantCulture,
"OPTO SUMMARY | valid={0}, rejected={1}, partialChars={2}",
parsedLines,
rejectedLines,
buffer.Length));
return samples;
AllyFactoryUnsealData data = logger.Step(
"Read ALLY factory-unseal inputs",
() => reader.ReadFactoryUnsealData(settings.CommandTimeoutMs),
value => string.Format(
CultureInfo.InvariantCulture,
"factoryId={0}; programmableText={1}; readingPreset={2}; secondsActive={3}; credential=<redacted>",
value.FactoryId,
value.ProgrammableText,
value.ReadingPreset,
value.SecondsActive));
logger.Step("Unseal ALLY meter", () => reader.UnsealFactory(data, settings.CommandTimeoutMs));
bool isSealedAfterUnseal = logger.Step("Verify factory seal after unseal",
() => reader.IsFactorySealed(settings.CommandTimeoutMs),
value => value ? "sealed" : "unsealed");
Assert.IsFalse(isSealedAfterUnseal, "ALLY unseal command completed but the meter still reports sealed.");
logger.Log("FACTORY SEAL | unseal completed and verified.");
}
private void CaptureThroughReader(AllyMeterReader reader)
@@ -275,7 +251,10 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.Integration
{
AllyOpticalSample parsedSample;
bool parsed = AllyOpticalSample.TryParse(rawLine, DateTime.UtcNow, out parsedSample);
logger.OpticalParse(rawLine, parsed, parsedSample);
if (parsed || AllyOpticalSample.IsMetrologyPacket(rawLine))
logger.OpticalParse(rawLine, parsed, parsedSample);
else
logger.Log("OPTO IGNORE | non-metrology telegram | " + rawLine.Replace("\r", "\\r").Replace("\n", "\\n").Replace("\t", "\\t"));
lastRawLine = rawLine;
}
@@ -285,13 +264,28 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.Integration
AllyOpticalSample sample = currentSamples[loggedSampleCount++];
logger.Log(string.Format(
CultureInfo.InvariantCulture,
"OPTO SAMPLE | index={0}, flow={1}, rawVolume={2}, rawTimestamp={3}, liters={4:R}, seconds={5:R}",
"OPTO SAMPLE | index={0}; type=0x{1:X2}; sequence=0x{2:X2}; ADC={3} raw counts; field={4} raw counts; flow={5} quarter-mL/s ({6:F3} mL/s); accumulator={7} quarter-mL ({8:F6} l); resultVolume={9:R} l; elapsed={10:F3} s; flipPeriod={11} raw ticks; VinfStart={12} raw; VinfEnd={13} raw; electrodeDelta={14} mV; impedance={15} raw; fieldDriveTime={16} us; flags=0x{17:X2}; emptyPipe={18}; fastHptc={19}; checksum=0x{20:X4} (not CRC-validated)",
loggedSampleCount,
sample.PacketType,
sample.Sequence,
sample.RawAdc,
sample.LastField,
sample.RawFlow,
sample.FlowMillilitersPerSecond,
sample.RawVolume,
sample.RawTimestamp,
sample.VolumeLiters,
sample.ExtendedVolumeLiters,
sample.ElapsedSeconds));
sample.ElapsedSeconds,
sample.FlipPeriod,
sample.VinfStart,
sample.VinfEnd,
sample.ElectrodeDelta,
sample.Impedance,
sample.FieldDriveTime,
sample.Flags,
sample.IsEmptyPipe,
sample.IsFastHptc,
sample.PacketChecksum));
}
Thread.Sleep(50);
}
@@ -313,19 +307,5 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.Integration
Assert.Inconclusive(message);
}
private static bool TryTakeLine(StringBuilder buffer, out string line)
{
string text = buffer.ToString();
int lineEnd = text.IndexOf('\n');
if (lineEnd < 0)
{
line = null;
return false;
}
line = text.Substring(0, lineEnd + 1);
buffer.Remove(0, lineEnd + 1);
return true;
}
}
}
@@ -64,10 +64,25 @@ namespace TBFTests.Rig.RegisterReaders.AllyReader.Integration
return;
}
Log(string.Format( CultureInfo.InvariantCulture, "OPTO PARSE | PASS | flow={0}, volume=0x{1:X6} ({1}), timestamp=0x{2:X8} ({2})",
Log(string.Format( CultureInfo.InvariantCulture, "OPTO PARSE | PASS | type=0x{0:X2}, sequence=0x{1:X2}; ADC={2} raw counts; field={3} raw counts; flow={4} quarter-mL/s ({5:F3} mL/s); accumulator={6} quarter-mL ({7:F6} l); flipPeriod={8} raw ticks; VinfStart={9} raw; VinfEnd={10} raw; electrodeDelta={11} mV; impedance={12} raw; fieldDriveTime={13} us; flags=0x{14:X2}; emptyPipe={15}; fastHptc={16}; checksum=0x{17:X4} (not CRC-validated)",
sample.PacketType,
sample.Sequence,
sample.RawAdc,
sample.LastField,
sample.RawFlow,
sample.FlowMillilitersPerSecond,
sample.RawVolume,
sample.RawTimestamp));
sample.VolumeLiters,
sample.FlipPeriod,
sample.VinfStart,
sample.VinfEnd,
sample.ElectrodeDelta,
sample.Impedance,
sample.FieldDriveTime,
sample.Flags,
sample.IsEmptyPipe,
sample.IsFastHptc,
sample.PacketChecksum));
}
public void Dispose()
@@ -0,0 +1,66 @@
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.RegisterReaders.AsicReader;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.common;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations;
namespace TBFTests.Rig.RegisterReaders.AsicReader
{
[TestClass]
public class AsicCorrectionsTest
{
[TestMethod]
public void Adapter_IdentifiesOnlyExtractedAsicReaderFamily()
{
AsicCorrections corrections = new AsicCorrections(null);
ISmartReader asicReader = new TBF.Rig.RegisterReaders.AsicReader.AsicReader(new Factory().DefaultConfig());
Assert.AreEqual("ASIC", corrections.TypeIdentificatorName());
Assert.IsTrue(corrections.IsFamilyOfSmartReader(asicReader));
Assert.AreEqual("asic", SmartReaderSelection.GetFamilyKey(asicReader));
}
[TestMethod]
public void UnsupportedActivity_FailsExplicitlyWithoutSendingAnyCommand()
{
AsicCorrections corrections = new AsicCorrections(null);
ISmartReader asicReader = new TBF.Rig.RegisterReaders.AsicReader.AsicReader(new Factory().DefaultConfig());
CommErr error = CommErr.None;
string result = null;
bool passed = corrections.WorkerActivity("Unsupported ASIC activity", asicReader, null, null, 0,
ref error, ref result, new[] { true }, 0, 0);
Assert.IsFalse(passed);
Assert.AreEqual(CommErr.WrongArguments, error);
StringAssert.Contains(result, "not supported");
}
[TestMethod]
public void ManualAdapter_ExposesAllSupportedAsicCommands()
{
AsicCorrections corrections = new AsicCorrections(null);
System.Windows.Forms.ContextMenu menu = corrections.GetContextMenu();
Assert.AreEqual(9, menu.MenuItems.Count);
Assert.AreEqual("Read PCB number", menu.MenuItems[0].Text);
Assert.AreEqual(iPerlCommunicationConstants.ReadAdditionalCommonParametersStr, menu.MenuItems[2].Text);
Assert.AreEqual(iPerlCommunicationConstants.SetFlipModeConstantStr, menu.MenuItems[3].Text);
Assert.AreEqual(iPerlCommunicationConstants.SetFlipModeRandomizedStr, menu.MenuItems[4].Text);
Assert.AreEqual("Stop optical stream", menu.MenuItems[8].Text);
Assert.AreEqual(0, corrections.GetAllThreads().Count);
Assert.IsFalse(corrections.GetStopWorkerThreads());
}
[TestMethod]
public void OpticalStreamLifecycle_StartsOnlyAfterSupportedTestMode()
{
Assert.IsTrue(AsicCorrections.ShouldStartDataStreamForActivity("Set Test mode"));
Assert.IsTrue(AsicCorrections.ShouldStartDataStreamForActivity("Set Test mode Optho 7"));
Assert.IsFalse(AsicCorrections.ShouldStartDataStreamForActivity("Set Test mode 80"));
Assert.IsFalse(AsicCorrections.ShouldStartDataStreamForActivity("Set Active mode"));
Assert.IsFalse(AsicCorrections.ShouldStartDataStreamForActivity(null));
}
}
}
@@ -0,0 +1,104 @@
using Config.Entities;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Reflection;
using TBF.Rig;
using TBF.Rig.Generic;
using TBF.Rig.RegisterReaders.AsicReader;
using TBF.Rig.TestMethods.iPerlCommunication.iPerlHead;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.common;
using AsicReaderComponent = TBF.Rig.RegisterReaders.AsicReader.AsicReader;
using Factory = TBF.Rig.RegisterReaders.AsicReader.Factory;
using AsicTestFactory = TBF.Rig.TestMethods.AsicTest.Factory;
using AsicTestMethod = TBF.Rig.TestMethods.AsicTest.TestMethod;
using LegacyTestMethod = TBF.Rig.TestMethods.iPerlCommunication.TestMethod;
using LegacyTestMethodCfg = TBF.Rig.TestMethods.iPerlCommunication.TestMethodCfg;
namespace TBFTests.Rig.RegisterReaders.AsicReader
{
[TestClass]
public class AsicReaderFactoryTest
{
[TestMethod]
public void Factory_CreatesAsicReaderWithLegacyIperlConfiguration()
{
Factory factory = new Factory();
IComponentCfg cfg = factory.DefaultConfig();
IComponent component = factory.GetComponent(cfg, null);
Assert.AreEqual("RegisterReaders.AsicReader", factory.ClassName);
Assert.IsInstanceOfType(cfg, typeof(AsicReaderCfg));
Assert.IsInstanceOfType(cfg, typeof(IperlHeadCfg));
Assert.IsInstanceOfType(component, typeof(AsicReaderComponent));
Assert.IsInstanceOfType(component, typeof(IperlHead));
Assert.IsInstanceOfType(component, typeof(ISmartReader));
}
[TestMethod]
public void Factory_ReadsBothAsicAndLegacyIperlConfigurationXml()
{
Factory factory = new Factory();
Component asicEntity = new AsicReaderCfg(factory).CreateDbEntity();
IComponentCfg asicCfg = factory.CmpntCfgFromCmpntEntity(asicEntity);
Component legacyEntity = new IperlHeadCfg(factory).CreateDbEntity();
IComponentCfg legacyCfg = factory.CmpntCfgFromCmpntEntity(legacyEntity);
Assert.IsInstanceOfType(asicCfg, typeof(AsicReaderCfg));
Assert.IsInstanceOfType(legacyCfg, typeof(IperlHeadCfg));
Assert.IsInstanceOfType(factory.GetComponent(asicCfg, null), typeof(AsicReaderComponent));
Assert.IsInstanceOfType(factory.GetComponent(legacyCfg, null), typeof(AsicReaderComponent));
}
[TestMethod]
public void ReaderStateAndFamilyMapping_PreserveAsicSpecificState()
{
AsicReaderComponent component = new AsicReaderComponent(new Factory().DefaultConfig());
component.BeginWMState = 12.345;
component.EndWMState = 67.89;
component.Disabled = true;
component.CommFailed = true;
Assert.AreEqual(12.345, component.BeginWMState, 0.000001);
Assert.AreEqual(67.89, component.EndWMState, 0.000001);
Assert.IsTrue(component.Disabled);
Assert.IsTrue(component.CommFailed);
Assert.AreEqual("asic", SmartReaderSelection.GetFamilyKey(component));
Assert.IsFalse(string.IsNullOrEmpty(component.CommInterface.ToString()));
}
[TestMethod]
public void AsicTestFactory_CreatesExplicitAsicTestWithLegacyConfiguration()
{
AsicTestFactory factory = new AsicTestFactory();
IComponentCfg cfg = factory.DefaultConfig();
IComponent component = factory.GetComponent(cfg, null);
Assert.AreEqual("TestMethods.AsicTest", factory.ClassName);
Assert.IsInstanceOfType(cfg, typeof(LegacyTestMethodCfg));
Assert.IsInstanceOfType(component, typeof(AsicTestMethod));
Assert.IsInstanceOfType(component, typeof(LegacyTestMethod));
}
[TestMethod]
public void AsicTest_OverridesLegacyExecuteToUseSmartCommunicationRoute()
{
MethodInfo execute = typeof(AsicTestMethod).GetMethod("Execute");
Assert.IsNotNull(execute);
Assert.AreEqual(typeof(AsicTestMethod), execute.DeclaringType);
Assert.IsTrue(execute.GetBaseDefinition().DeclaringType == typeof(LegacyTestMethod));
}
[TestMethod]
public void Factories_ResolveExplicitAsicReaderWithoutChangingLegacyIperlAlias()
{
Assert.IsInstanceOfType(TbfComponents.CmpntFactoryFromClassName("RegisterReaders.AsicReader"), typeof(Factory));
Assert.IsInstanceOfType(TbfComponents.CmpntFactoryFromClassName("TestMethods.AsicTest"), typeof(AsicTestFactory));
Assert.IsNotNull(TbfComponents.CmpntFactoryFromClassName("RegisterReader for iPerl"));
}
}
}
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Ports;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.TestMethods.iPerlCommunication.communication.C4.diagnosticLed;
@@ -14,13 +15,30 @@ using TBF.Rig.TestMethods.iPerlCommunication.communication.C4.protocolCommons;
namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.protocol
{
[TestClass]
public class CordonelProtocolTests
[TestCategory("HardwareIntegration")]
public class CordonelProtocolIntegrationTests
{
private const string ComPort = "COM3"; // CHANGE THIS
private const int BaudRate = 9600;
private const int BaudRateOpto = 38400;
private const int ReadTimeoutMs = 2000;
[TestInitialize]
public void RequireExplicitHardwareConfiguration()
{
if (!string.Equals(Environment.GetEnvironmentVariable("GENESIS_HW_TESTS"), "1",
StringComparison.Ordinal))
{
Assert.Inconclusive("Set GENESIS_HW_TESTS=1 to run Genesis serial hardware integration tests.");
}
if (!SerialPort.GetPortNames().Any(port =>
string.Equals(port, ComPort, StringComparison.OrdinalIgnoreCase)))
{
Assert.Inconclusive($"Genesis hardware integration requires configured port {ComPort}.");
}
}
//...MF
[TestMethod]
@@ -590,4 +608,4 @@ namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.protocol
}
}
}
}
@@ -245,5 +245,34 @@ namespace TBFTests.Rig.RegisterReaders.PoseidonCmdStartStop
"Invalid JSON should not produce a valid DeviceId.");
}
}
[TestMethod]
public void SerialPortData_ZeroMeterType_UsesPoseidonCliDefault()
{
var serialPort = new SerialPortData("COM3", "HatCliDemo.exe", 0);
Assert.AreEqual(SerialPortData.DefaultPoseidonMeterType, serialPort.MeterType);
Assert.AreEqual(
"-p COM3 -m 74 --operation readall",
serialPort.DefaultArgSettings(SerialPortData.EMeterArg.AllParams));
}
[TestMethod]
public async Task CliProcess_ExitErrorWithoutJson_RecordsDiagnosticsAndReturnsNoReading()
{
string cmdExe = Environment.GetEnvironmentVariable("ComSpec") ?? @"C:\Windows\System32\cmd.exe";
var cliRunner = new CliRunner(false);
var info = new CliTaskInfo { Name = "missing-dependency-simulation" };
JsonDataFromPoseidon result = await cliRunner.RunAndCaptureJsonAsync<JsonDataFromPoseidon>(
cmdExe,
"/d /c \"echo Could not load file or assembly 'log4net' 1>&2 & exit /b 17\"",
info);
Assert.IsNull(result);
Assert.AreEqual(17, info.ExitCode);
StringAssert.Contains(info.FailureReason, "exit code 17");
StringAssert.Contains(info.StandardError, "log4net");
}
}
}
}
@@ -0,0 +1,54 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.RegisterReaders.PoseidonCmdStartStop;
using CmdPoseidonReader = TBF.Rig.RegisterReaders.PoseidonCmdStartStop.PoseidonReader;
namespace TBFTests.Rig.RegisterReaders.PoseidonCmdStartStop
{
/// <summary>
/// Regression fixtures extracted from PT50 customer CliRunner responses.
/// </summary>
[TestClass]
public class PoseidonCustomerCliResponseTest
{
// Independently calculated fixtures can differ by insignificant
// IEEE-754 rounding during gallons-to-litres conversion.
private const double DialogValueToleranceLitres = 0.0001;
private const string Com43InitialResponse =
"{\"Reading\":\"002780.99\",\"DeviceId\":\"1000000219\",\"ProductType\":74,\"ReadingComplete\":true,\"NfcTagDetected\":true,\"CliVersion\":\"2.0.0.2\"}";
private const string Com45InitialResponse =
"{\"Reading\":\"002316.63\",\"DeviceId\":\"1000000279\",\"ProductType\":74,\"ReadingComplete\":true,\"NfcTagDetected\":true,\"CliVersion\":\"2.0.0.2\"}";
private const string Com43LaterResponse =
"{\"Reading\":\"002898.60\",\"DeviceId\":\"1000000219\",\"ProductType\":74,\"ReadingComplete\":true,\"NfcTagDetected\":true,\"CliVersion\":\"2.0.0.2\"}";
private const string Com45LaterResponse =
"{\"Reading\":\"002433.50\",\"DeviceId\":\"1000000279\",\"ProductType\":74,\"ReadingComplete\":true,\"NfcTagDetected\":true,\"CliVersion\":\"2.0.0.2\"}";
[TestMethod]
public void CustomerCliResponses_AreDeserializedAndConvertedToDialogValues()
{
AssertDialogValue(Com43InitialResponse, "1000000219", "002780.99", 10527.1923171862);
AssertDialogValue(Com45InitialResponse, "1000000279", "002316.63", 8769.39850116792);
AssertDialogValue(Com43LaterResponse, "1000000219", "002898.60", 10972.3945971024);
AssertDialogValue(Com45LaterResponse, "1000000279", "002433.50", 9211.799576364);
}
private static void AssertDialogValue(string json, string expectedDeviceId,
string expectedReading, double expectedLitres)
{
var cliRunner = new CliRunner(false);
JsonDataFromPoseidon response;
Assert.IsTrue(cliRunner.TryJsonStringDeserialize(json, out response));
Assert.IsNotNull(response);
Assert.AreEqual(expectedDeviceId, response.DeviceId);
Assert.AreEqual(expectedReading, response.Reading);
double dialogValue;
string failureReason;
Assert.IsTrue(CmdPoseidonReader.TryGetDialogValue(response, out dialogValue, out failureReason), failureReason);
Assert.AreEqual(expectedLitres, dialogValue, DialogValueToleranceLitres,
"The value assigned to the START/END dialog must come from the CLI response.");
Assert.AreNotEqual(0d, dialogValue);
}
}
}
@@ -0,0 +1,129 @@
using System;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Windows.Forms;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.DataEntry.PoseidonCmd;
using TBF.Rig.RegisterReaders.PoseidonCmdStartStop;
using CmdPoseidonReader = TBF.Rig.RegisterReaders.PoseidonCmdStartStop.PoseidonReader;
namespace TBFTests.Rig.RegisterReaders.PoseidonCmdStartStop
{
[TestClass]
public class PoseidonDialogTransferTest
{
private const string CustomerCliResponse =
"{\"Reading\":\"002433.50\",\"DeviceId\":\"1000000279\",\"ProductType\":74,\"ReadingComplete\":true,\"NfcTagDetected\":true,\"CliVersion\":\"2.0.0.2\"}";
[TestMethod]
public void CustomerCliValue_IsPrefilledConfirmedAndTransferred_ForStartAndEnd()
{
Exception failure = null;
var staThread = new Thread(() =>
{
try { RunDialogTransferScenario(); }
catch (Exception exception) { failure = exception; }
});
staThread.SetApartmentState(ApartmentState.STA);
staThread.Start();
staThread.Join(TimeSpan.FromSeconds(15));
Assert.IsFalse(staThread.IsAlive, "The Poseidon dialog test did not finish.");
if (failure != null) throw failure;
}
private static void RunDialogTransferScenario()
{
var cliRunner = new CliRunner(false);
JsonDataFromPoseidon response;
Assert.IsTrue(cliRunner.TryJsonStringDeserialize(CustomerCliResponse, out response));
double cliValueLitres;
string failureReason;
Assert.IsTrue(CmdPoseidonReader.TryGetDialogValue(response, out cliValueLitres, out failureReason), failureReason);
VerifyStartDialogTransfer(cliValueLitres);
VerifyEndDialogTransfer(cliValueLitres);
}
private static void VerifyStartDialogTransfer(double expectedValue)
{
using (var dialog = new TestStartEndForm(1, null, new bool[1]))
{
dialog.CreateControl();
dialog.WMStartState[0] = expectedValue;
dialog.WMStartStateStr[0] = expectedValue.ToString();
dialog.UpdateValues(true, true);
Assert.AreEqual(dialog.WMStartStateStr[0], FindTextBox(dialog, "startTextBox1").Text);
ConfirmDialog(dialog);
Assert.IsTrue(dialog.Completed);
Assert.AreEqual(expectedValue, dialog.WMStartState[0], 0.0001);
var entryForm = CreateEntryFormForTransfer(EntryForm.CurrentOp.ReadDatastream_StartStates);
TransferAcceptedDialogValues(entryForm, dialog);
Assert.AreEqual(expectedValue, entryForm.WMStartState(0), 0.0001);
}
}
private static void VerifyEndDialogTransfer(double expectedValue)
{
using (var dialog = new TestStartEndForm(1, null, new[] { expectedValue.ToString() }, new bool[1], 0, 0, 0))
{
dialog.CreateControl();
dialog.WMEndState[0] = expectedValue;
dialog.UpdateValues(false, true);
Assert.AreEqual(expectedValue.ToString(), FindTextBox(dialog, "endTextBox1").Text);
ConfirmDialog(dialog);
Assert.IsTrue(dialog.Completed);
Assert.AreEqual(expectedValue, dialog.WMEndState[0], 0.0001);
var entryForm = CreateEntryFormForTransfer(EntryForm.CurrentOp.ReadDatastream_EndStates);
TransferAcceptedDialogValues(entryForm, dialog);
Assert.AreEqual(expectedValue, entryForm.WMEndState(0), 0.0001);
}
}
private static TextBox FindTextBox(Control dialog, string name)
{
TextBox textBox = dialog.Controls.Find(name, true).OfType<TextBox>().FirstOrDefault();
Assert.IsNotNull(textBox, "Expected dialog control was not found: " + name);
return textBox;
}
private static void ConfirmDialog(TestStartEndForm dialog)
{
MethodInfo okHandler = typeof(TestStartEndForm).GetMethod("okButton_Click",
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.IsNotNull(okHandler);
okHandler.Invoke(dialog, new object[] { dialog, EventArgs.Empty });
}
private static EntryForm CreateEntryFormForTransfer(EntryForm.CurrentOp currentOp)
{
var entryForm = new EntryForm();
SetPrivateField(entryForm, "currentOp", currentOp);
SetPrivateField(entryForm, "wmStartState", new double[1]);
SetPrivateField(entryForm, "wmStartStateStr", new string[1]);
SetPrivateField(entryForm, "wmEndState", new double[1]);
return entryForm;
}
private static void TransferAcceptedDialogValues(EntryForm entryForm, TestStartEndForm dialog)
{
MethodInfo method = typeof(EntryForm).GetMethod("StoreAcceptedDialogValues",
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.IsNotNull(method);
method.Invoke(entryForm, new object[] { dialog });
}
private static void SetPrivateField(object instance, string name, object value)
{
FieldInfo field = instance.GetType().GetField(name,
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.IsNotNull(field, "Expected field was not found: " + name);
field.SetValue(instance, value);
}
}
}
@@ -0,0 +1,115 @@
using System.Collections.Generic;
using JetBrains.Annotations;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig;
using TBF.Rig.RegisterReaders.PoseidonCmdStartStop;
using CmdPoseidonReader = TBF.Rig.RegisterReaders.PoseidonCmdStartStop.PoseidonReader;
namespace TBFTests.Rig.RegisterReaders.PoseidonCmdStartStop
{
[TestClass]
[TestSubject(typeof(PoseidonReadCycle))]
public class PoseidonReadCycleTest
{
private sealed class FakeReader : IPoseidonReadOperation
{
private readonly int iterationsToComplete;
private int iterations;
public FakeReader(string name, int iterationsToComplete) { Name = name; this.iterationsToComplete = iterationsToComplete; }
public string Name { get; private set; }
public int StartCount { get; private set; }
public CmdPoseidonReader.CurrentPoseidonOp CurrentOp { get; private set; }
public bool IsNotStarted { get { return CurrentOp == CmdPoseidonReader.CurrentPoseidonOp.None; } }
public bool IsFinished { get { return CurrentOp == CmdPoseidonReader.CurrentPoseidonOp.Done || CurrentOp == CmdPoseidonReader.CurrentPoseidonOp.Error; } }
public bool HasError { get { return CurrentOp == CmdPoseidonReader.CurrentPoseidonOp.Error; } }
public void Start(bool readStart) { StartCount++; CurrentOp = readStart ? CmdPoseidonReader.CurrentPoseidonOp.ReadDataStream_Start : CmdPoseidonReader.CurrentPoseidonOp.ReadDataStream_End; }
public Event Run()
{
if (CurrentOp == CmdPoseidonReader.CurrentPoseidonOp.ReadDataStream_Start || CurrentOp == CmdPoseidonReader.CurrentPoseidonOp.ReadDataStream_End)
{ CurrentOp = CmdPoseidonReader.CurrentPoseidonOp.ReadDatastream_Running; return Event.Busy; }
if (CurrentOp == CmdPoseidonReader.CurrentPoseidonOp.ReadDatastream_Running && ++iterations >= iterationsToComplete)
{ CurrentOp = CmdPoseidonReader.CurrentPoseidonOp.Done; return Event.Done; }
return Event.Busy;
}
}
[TestMethod]
public void RunIteration_FullBench_ArmsEachReaderOnce()
{
var readers = new List<IPoseidonReadOperation>();
for (int i = 1; i <= 48; i++) readers.Add(new FakeReader("PoseidonPos" + i, 1 + i % 3));
for (int i = 0; i < 5; i++) PoseidonReadCycle.RunIteration(readers, true);
foreach (FakeReader reader in readers) Assert.AreEqual(1, reader.StartCount, reader.Name);
}
[TestMethod]
public void RunIteration_ReducedBench_DoesNotRestartCompletedReader()
{
var readers = new List<IPoseidonReadOperation> { new FakeReader("Pos1", 1), new FakeReader("Pos2", 3) };
for (int i = 0; i < 5; i++) PoseidonReadCycle.RunIteration(readers, false);
foreach (FakeReader reader in readers) Assert.AreEqual(1, reader.StartCount, reader.Name);
}
[TestMethod]
public void StartThenStop_PhasesArmTheSameReaderOncePerPhase()
{
var reader = new FakeReader("Pos1", 1);
var readers = new List<IPoseidonReadOperation> { reader };
var startPhase = new PoseidonReadPhaseRunner(true);
// START phase completes.
startPhase.RunIteration(readers);
startPhase.RunIteration(readers);
Assert.AreEqual(CmdPoseidonReader.CurrentPoseidonOp.Done, reader.CurrentOp);
// A new STOP phase re-arms the terminal reader exactly once.
var stopPhase = new PoseidonReadPhaseRunner(false);
stopPhase.RunIteration(readers);
stopPhase.RunIteration(readers);
Assert.AreEqual(2, reader.StartCount);
Assert.AreEqual(CmdPoseidonReader.CurrentPoseidonOp.Done, reader.CurrentOp);
}
[TestMethod]
public void InputJson_ZeroReading_WithDecimalCommaOrDot_IsValid()
{
foreach (string reading in new[] { "00000,0", "00000.0" })
{
double value;
Assert.IsTrue(CmdPoseidonReader.TryParseCliReading(reading, out value));
Assert.AreEqual(0d, value);
}
}
[TestMethod]
public void InputJson_NonZeroReading_WithDecimalCommaOrDot_IsParsedCorrectly()
{
foreach (string reading in new[] { "002433,50", "002433.50", "002898,60", "002898.60" })
{
double value;
Assert.IsTrue(CmdPoseidonReader.TryParseCliReading(reading, out value));
Assert.IsTrue(value > 0, "The non-zero CLI reading must remain non-zero: " + reading);
}
}
[TestMethod]
public void SimulationMode_AlwaysUsesCmdSleepTestInsteadOfConfiguredHatCli()
{
Assert.AreEqual("cmdSleepTest.exe", CmdPoseidonReader.GetCliFileNameForMode(
Common.DebugMode.Simulate, "HatCliDemo.exe"));
Assert.AreEqual("HatCliDemo.exe", CmdPoseidonReader.GetCliFileNameForMode(
Common.DebugMode.Normal, "HatCliDemo.exe"));
}
[TestMethod]
public void CliReadResponse_RequiresNfcAndCompletedReading()
{
string reason;
var data = new JsonDataFromPoseidon { NfcTagDetected = true, ReadingComplete = true, Reading = "00000.0" };
Assert.IsTrue(CmdPoseidonReader.TryValidateCliReadResponse(data, out reason));
data.ReadingComplete = false;
Assert.IsFalse(CmdPoseidonReader.TryValidateCliReadResponse(data, out reason));
}
}
}
@@ -37,5 +37,20 @@ namespace TBFTests.Rig.RegisterReaders.PoseidonCmdStartStop
Assert.IsTrue(tryGetDeviceId);
Assert.AreEqual("1000000267", strOut);
}
[TestMethod]
public void TryParseCliReading_ShouldAcceptZeroWithDotAndComma()
{
double dotValue;
double commaValue;
Assert.IsTrue(TBF.Rig.RegisterReaders.PoseidonCmdStartStop.PoseidonReader
.TryParseCliReading("00000.0", out dotValue));
Assert.IsTrue(TBF.Rig.RegisterReaders.PoseidonCmdStartStop.PoseidonReader
.TryParseCliReading("00000,0", out commaValue));
Assert.AreEqual(0d, dotValue);
Assert.AreEqual(0d, commaValue);
}
}
}
}
@@ -0,0 +1,51 @@
using System.IO;
using System.Threading;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.RegisterReaders.PoseidonCmdStartStop;
using CmdPoseidonReader = TBF.Rig.RegisterReaders.PoseidonCmdStartStop.PoseidonReader;
namespace TBFTests.Rig.RegisterReaders.PoseidonCmdStartStop
{
[TestClass]
[TestCategory("Integration")]
public class PoseidonSingleMeterIntegrationTest
{
// Set this only when a real Poseidon meter is connected to the selected hat.
private const int RealPoseidonComPort = 0;
private const string RealPoseidonCliFile = "HatCliDemo.exe";
[TestMethod]
[TestCategory("Manual")]
public void RealMeter_ReadStartValueThroughCli()
{
if (RealPoseidonComPort <= 0)
Assert.Inconclusive("Set RealPoseidonComPort before running this manual Poseidon integration test.");
string cliPath = Path.Combine(SerialPortData.CliDirectory, RealPoseidonCliFile);
if (!File.Exists(cliPath))
Assert.Inconclusive("Poseidon CLI was not found: " + cliPath);
var cfg = new PoseidonCfg("PoseidonIntegration", new Factory())
{
ComPortNr = RealPoseidonComPort,
CliFileName = RealPoseidonCliFile
};
var reader = new CmdPoseidonReader(cfg, null);
reader.DebugLevel = Common.DebugMode.Normal;
reader.Initialize();
reader.SetCurrentOp(CmdPoseidonReader.CurrentPoseidonOp.ReadDataStream_Start);
for (int iteration = 0; iteration < 3500 &&
reader.CurrentOp != CmdPoseidonReader.CurrentPoseidonOp.Done &&
reader.CurrentOp != CmdPoseidonReader.CurrentPoseidonOp.Error; iteration++)
{
reader.Run();
Thread.Sleep(10);
}
Assert.AreEqual(CmdPoseidonReader.CurrentPoseidonOp.Done, reader.CurrentOp);
Assert.IsTrue(reader.LastCliReadingParsed,
"CLI returned no parseable reading. Inspect the Poseidon CLI and TBF logs.");
}
}
}
@@ -119,6 +119,7 @@ namespace TBFTests.Rig.RegisterReaders.PoseidonReader
Name = "PoseidonCfg",
OptoComPortNr = 6,
RfidComPortNr = 5,
DebugLevel = Common.DebugMode.Simulate,
};
config = poseidonCfg;
@@ -156,7 +157,8 @@ namespace TBFTests.Rig.RegisterReaders.PoseidonReader
string allText = string.Join(", ", listBox.Items.Cast<object>().Select(i => i.ToString()));
Console.WriteLine(allText);
Assert.IsTrue(allText.Contains("ReadSerialNo: 1000000267"));
Assert.IsTrue(allText.Contains("ReadSerialNo: 1111"),
"The UI unit test must use the deterministic simulated serial number, not physical hardware.");
}
@@ -215,10 +217,6 @@ namespace TBFTests.Rig.RegisterReaders.PoseidonReader
string allText = string.Join(", ", listBox.Items.Cast<object>().Select(i => i.ToString()));
Console.WriteLine(allText);
Assert.IsTrue(allText.Contains("SetTestMode: OK"));
//some input from the meter
Thread.Sleep(50000);
//SET OPTOTEST MODE OFF
comboBox.SelectedIndex = (int)PoseidonImplHeadTestCtrl.Operations.SetTestModeOff;
uniHeadTestCtrl.CommandTestButtonClick(commandTestButton, eventArgs);
@@ -226,22 +224,6 @@ namespace TBFTests.Rig.RegisterReaders.PoseidonReader
allText = string.Join(", ", listBox.Items.Cast<object>().Select(i => i.ToString()));
Console.WriteLine(allText);
Assert.IsTrue(allText.Contains("SetActiveMode: OK"));
/////////////// results of OptoHead
// Get the private ListBox field via reflection
var fieldOpto = uniHeadTestCtrl.GetType()
.GetField("optoListBox", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
// Read its value from the control instance (not Text!)
var listBoxOpto = fieldOpto?.GetValue(uniHeadTestCtrl) as System.Windows.Forms.ListBox;
// Verify we found it
Assert.IsNotNull(listBoxOpto, "optoListBox not found in UniHeadTestCtrl");
//check if the test mode is set OFF
allText = string.Join(", ", listBoxOpto.Items.Cast<object>().Select(i => i.ToString()));
Console.WriteLine(allText);
Assert.IsTrue(allText.Contains("Status: OptoHeadC7"));
Assert.IsTrue(allText.Contains("C7Data: C7"));
}
}
}
}
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Ports;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.TestMethods.iPerlCommunication.communication.C4.diagnosticLed;
@@ -14,6 +15,7 @@ using TBF.Rig.TestMethods.iPerlCommunication.communication.C4.protocolCommons;
namespace TBFTests.Rig.RegisterReaders.iPerlASICReader.communication.C4.IperlHatProtocol
{
[TestClass]
[TestCategory("HardwareIntegration")]
public class IperlHatIntegrationTests
{
private const string ComPort = "COM12"; // CHANGE THIS
@@ -22,6 +24,23 @@ namespace TBFTests.Rig.RegisterReaders.iPerlASICReader.communication.C4.IperlHat
private const int BaudRateOpto = 38400;
private const int ReadTimeoutMs = 2000;
[TestInitialize]
public void RequireExplicitHardwareConfiguration()
{
if (!string.Equals(Environment.GetEnvironmentVariable("IPERL_ASIC_HW_TESTS"), "1",
StringComparison.Ordinal))
{
Assert.Inconclusive("Set IPERL_ASIC_HW_TESTS=1 to run iPerl ASIC serial hardware integration tests.");
}
string[] ports = SerialPort.GetPortNames();
if (!ports.Any(port => string.Equals(port, ComPort, StringComparison.OrdinalIgnoreCase)) ||
!ports.Any(port => string.Equals(port, ComPortOptho, StringComparison.OrdinalIgnoreCase)))
{
Assert.Inconclusive($"iPerl ASIC hardware integration requires configured ports {ComPort} and {ComPortOptho}.");
}
}
[TestMethod]
[TestCategory("Hardware")]
[TestCategory("Serial")]
@@ -454,7 +473,7 @@ namespace TBFTests.Rig.RegisterReaders.iPerlASICReader.communication.C4.IperlHat
/// </summary>
[TestMethod]
[TestCategory("Hardware")]
public void Serial_OptoRawSniff_Standalone()
public void Integration_Serial_OptoRawSniff_Standalone()
{
Serial_OptoRawSniff();
}
@@ -503,7 +522,7 @@ namespace TBFTests.Rig.RegisterReaders.iPerlASICReader.communication.C4.IperlHat
}
catch (TimeoutException)
{
Assert.Fail("Serial read timeout");
Assert.Inconclusive("No optical packet was received within 10 seconds. Enable optical test mode before running this sniff integration test.");
}
}
@@ -997,4 +1016,4 @@ namespace TBFTests.Rig.RegisterReaders.iPerlASICReader.communication.C4.IperlHat
return true;
}
}
}
}
@@ -0,0 +1,65 @@
using System;
using JetBrains.Annotations;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.RegisterReaders.AllyReader;
using TBF.Rig.TestMethods.AllyCalibration;
namespace TBFTests.Rig.TestMethods.AllyCalibration
{
/// <summary>
/// Component-level integration coverage of Read PCB, start optical output,
/// C6 parsing and cleanup without serial hardware.
/// </summary>
[TestClass]
[TestSubject(typeof(AllyCalibrationSeq))]
public class AllyOpticalWorkflowIntegrationTest
{
[TestMethod]
public void ConfiguredWorkflow_FakeAllyReader_ReadPcbStartReadOpticalAndStop_TransfersMeterStates()
{
AllyMeterReader reader = CreateFakeReader();
TestMethodCfg cfg = new TestMethodCfg(new TBF.Rig.TestMethods.AllyCalibration.Factory());
TestMethodParams parameters = new TestMethodParams(true);
string message;
parameters.Activity = AllyCalibrationActivityNames.ReadSerialNumber;
Assert.IsTrue(AllyCalibrationSeq.ExecuteWithRetries(reader, cfg, parameters, out message), message);
Assert.AreEqual("ALLY-SIMULATED", reader.SerialNr);
parameters.Activity = AllyCalibrationActivityNames.UnsealAndStartOpticalStream;
Assert.IsTrue(AllyCalibrationSeq.ExecuteWithRetries(reader, cfg, parameters, out message), message);
reader.Start();
DateTime start = new DateTime(2026, 9, 15, 10, 40, 0, DateTimeKind.Utc);
reader.ProcessOpticalTextForTest(
TBFTests.Rig.RegisterReaders.AllyReader.AllyOpticalTelegramFactory.CreateC6(0x10, 0, 0, 12, 8000U, 0, 0, 0, 0, 0, 0, 0),
start);
reader.ProcessOpticalTextForTest(
TBFTests.Rig.RegisterReaders.AllyReader.AllyOpticalTelegramFactory.CreateC6(0x12, 0, 0, 12, 8120U, 0, 0, 0, 0, 0, 0, 0),
start.AddSeconds(5));
reader.Stop();
parameters.Activity = AllyCalibrationActivityNames.StopOpticalStream;
Assert.IsTrue(AllyCalibrationSeq.ExecuteWithRetries(reader, cfg, parameters, out message), message);
Assert.IsFalse(reader.NoSamples);
Assert.AreEqual(2D, reader.BeginWMState, 1E-12);
Assert.AreEqual(2.03D, reader.EndWMState, 1E-12);
Assert.AreEqual(0.03D, reader.WMVolume, 1E-12);
Assert.AreEqual(5D, reader.TimestampSecEnd - reader.TimestampSecStart, 1E-12);
}
private static AllyMeterReader CreateFakeReader()
{
AllyReaderCfg cfg = new AllyReaderCfg(new TBF.Rig.RegisterReaders.AllyReader.Factory())
{
DebugLevel = Common.DebugMode.Simulate,
ConfiguredMeterSize = AllyMeterSize.AutoDetect,
Name = "FakeAlly1"
};
AllyMeterReader reader = new AllyMeterReader(cfg);
reader.Initialize();
return reader;
}
}
}
@@ -28,8 +28,10 @@ namespace TBFTests.Rig.TestMethods.AllyCalibration
TestMethodParams parameters = new TestMethodParams(true);
ICollection<string> activities = parameters.ParamValues(0);
Assert.AreEqual(19, activities.Count);
Assert.AreEqual(21, activities.Count);
CollectionAssert.Contains((System.Collections.ICollection)activities, "Read serial number");
CollectionAssert.Contains((System.Collections.ICollection)activities, "Unseal meter and start optical stream");
CollectionAssert.Contains((System.Collections.ICollection)activities, "Stop optical stream");
CollectionAssert.Contains((System.Collections.ICollection)activities, "Start offset learning");
CollectionAssert.DoesNotContain((System.Collections.ICollection)activities, "Q2 correction");
@@ -0,0 +1,241 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using TBF;
using TBF.Rig.RegisterReaders.AllyReader;
using TBF.Rig.RegisterReaders.GenesisRegReader.implementations;
using TBF.Rig.Sequences;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.common;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations;
namespace TBFTests.Rig.Uni.SharedDialogs.SmartMetersCommunication
{
[TestClass]
public class SmartCommunicationFormTests
{
private static readonly object ProcessDataLock = new object();
[TestMethod]
public void SharedCommunicationParams_ActivityIsRetainedThroughITestParams()
{
var parameters = new iPerlCommunicationParams
{
Activity = "Set Test mode Optho 7"
};
TBF.Rig.Generic.ITestParams asInterface = parameters;
Assert.AreEqual("Set Test mode Optho 7", asInterface.Activity);
}
[TestMethod]
public void ManualForm_PopulatesAvailableFamiliesFromRegisterReaders()
{
RunInSta(() =>
{
WithSmartReaders(new ISmartReader[] { CreateGenesisReader(), CreateAllyReader() }, () =>
{
using (var form = new SmartCommunicationForm(true))
{
CollectionAssert.AreEqual(
new[] { "Genesis", "ALLY" },
SmartCommunicationForm.IdentifyReaderTypes());
}
});
});
}
[TestMethod]
public void ManualForm_SelectedFamilyPopulatesOnlyMatchingReaderAndCorrection()
{
RunInSta(() =>
{
GenesisSmartReader genesis = CreateGenesisReader();
AllyMeterReader ally = CreateAllyReader();
WithSmartReaders(new ISmartReader[] { genesis, ally }, () =>
{
using (var form = new SmartCommunicationForm(true))
{
SelectFamily(form, "ALLY");
Assert.AreEqual(1, form.Heads.Count);
Assert.AreSame(ally, form.Heads[0]);
Assert.IsInstanceOfType(CurrentCorrection(), typeof(AllyCorrections));
SelectFamily(form, "Genesis");
Assert.AreEqual(1, form.Heads.Count);
Assert.AreSame(genesis, form.Heads[0]);
Assert.IsInstanceOfType(CurrentCorrection(), typeof(GenesisCorrections));
}
});
});
}
[TestMethod]
public void ManualForm_LoadsReaderRowsAndCheckboxTogglesSelectionState()
{
RunInSta(() =>
{
AllyMeterReader first = CreateAllyReader();
AllyMeterReader second = CreateAllyReader();
WithSmartReaders(new ISmartReader[] { first, second }, () =>
{
using (var form = new SmartCommunicationForm(true))
{
SelectFamily(form, "ALLY");
form.CkbIndex[0] = 0;
form.CkbIndex[1] = 1;
ICorrections correction = CurrentCorrection();
correction.Load(form.Labels, form.Counters, form.Messages, form.CheckBoxes,
form.CkbIndex, form.CkbState, form.Heads, form.CheckBoxes.Length, true);
Assert.AreEqual(2, form.Heads.Count);
Assert.AreSame(first, form.Heads[0]);
Assert.AreSame(second, form.Heads[1]);
Assert.IsTrue(form.CheckBoxes[0].Checked);
Assert.IsTrue(form.CheckBoxes[1].Checked);
Assert.AreEqual("---", form.Messages[0].Text);
Assert.AreEqual("---", form.Messages[1].Text);
form.CheckBoxes[0].Checked = false;
InvokePrivate(form, "checkBoxImage1_Click", form.CheckBoxes[0], EventArgs.Empty);
Assert.IsFalse(form.CkbState[0]);
form.CheckBoxes[0].Checked = true;
InvokePrivate(form, "checkBoxImage1_Click", form.CheckBoxes[0], EventArgs.Empty);
Assert.IsTrue(form.CkbState[0]);
Assert.IsTrue((form.GetCheckBoxStates() & 1L) != 0L);
}
});
});
}
[TestMethod]
public void SelectedCorrection_ProvidesCommandsForTheSelectedRegisterReaderFamily()
{
RunInSta(() =>
{
WithSmartReaders(new ISmartReader[] { CreateGenesisReader(), CreateAllyReader() }, () =>
{
using (var form = new SmartCommunicationForm(true))
{
SelectFamily(form, "ALLY");
CollectionAssert.Contains(
CurrentCorrection().GetContextMenu().MenuItems.Cast<System.Windows.Forms.MenuItem>()
.Select(item => item.Text).ToArray(),
"Read Serial Number");
SelectFamily(form, "Genesis");
CollectionAssert.Contains(
CurrentCorrection().GetContextMenu().MenuItems.Cast<System.Windows.Forms.MenuItem>()
.Select(item => item.Text).ToArray(),
"Read PCB Number");
}
});
});
}
[TestMethod]
public void SelectionSettings_MigratesLegacyMaskToConfiguredIPerlFamily()
{
var first = new Mock<ISmartReader>();
first.SetupGet(reader => reader.Name).Returns("iPerl01");
first.SetupGet(reader => reader.Position).Returns(1);
var second = new Mock<ISmartReader>();
second.SetupGet(reader => reader.Name).Returns("iPerl02");
second.SetupGet(reader => reader.Position).Returns(2);
var settings = new LocalSettings { OptoHeadsEnabled = 1L << 1 };
SmartReaderSelection.EnsureConfiguredReaders(
settings, new ISmartReader[] { first.Object, second.Object });
SmartReaderFamilySelection family = settings.SmartReaderSelections.Families.Single();
Assert.AreEqual("iperl", family.FamilyKey);
Assert.IsFalse(family.Readers.Single(item => item.ReaderKey == SmartReaderSelection.GetReaderKey(first.Object)).Enabled);
Assert.IsTrue(family.Readers.Single(item => item.ReaderKey == SmartReaderSelection.GetReaderKey(second.Object)).Enabled);
}
[TestMethod]
public void SelectionSettings_MigratesLegacyMaskToFirstFamilyWhenIPerlIsNotConfigured()
{
AllyMeterReader ally = CreateAllyReader();
var settings = new LocalSettings { OptoHeadsEnabled = 1L };
SmartReaderSelection.EnsureConfiguredReaders(settings, new ISmartReader[] { ally });
SmartReaderFamilySelection family = settings.SmartReaderSelections.Families.Single();
Assert.AreEqual("ally", family.FamilyKey);
Assert.IsTrue(family.Readers.Single().Enabled);
}
private static AllyMeterReader CreateAllyReader()
{
return new AllyMeterReader(new AllyReaderCfg(new TBF.Rig.RegisterReaders.AllyReader.Factory()));
}
private static GenesisSmartReader CreateGenesisReader()
{
return new GenesisSmartReader(new TBF.Rig.RegisterReaders.GenesisRegReader.GenesisCfg(
new TBF.Rig.RegisterReaders.GenesisRegReader.Factory()));
}
private static void SelectFamily(SmartCommunicationForm form, string family)
{
SmartCommunicationForm.SelectedTypeReader = family;
InvokePrivate(form, "UpdateHeads");
}
private static ICorrections CurrentCorrection()
{
PropertyInfo property = typeof(SmartCommunicationForm).GetProperty(
"Correction", BindingFlags.NonPublic | BindingFlags.Static);
return (ICorrections)property.GetValue(null, null);
}
private static void InvokePrivate(SmartCommunicationForm form, string methodName, params object[] parameters)
{
MethodInfo method = typeof(SmartCommunicationForm).GetMethod(
methodName, BindingFlags.NonPublic | BindingFlags.Instance);
method.Invoke(form, parameters);
}
private static void WithSmartReaders(IList<ISmartReader> readers, Action action)
{
lock (ProcessDataLock)
{
IList<ISmartReader> previous = ProcessData.SmartHeadsUni;
string previousSelectedType = SmartCommunicationForm.SelectedTypeReader;
try
{
ProcessData.SmartHeadsUni = readers;
SmartCommunicationForm.SelectedTypeReader = null;
action();
}
finally
{
ProcessData.SmartHeadsUni = previous;
SmartCommunicationForm.SelectedTypeReader = previousSelectedType;
}
}
}
private static void RunInSta(Action action)
{
Exception failure = null;
Thread thread = new Thread(() =>
{
try { action(); }
catch (Exception exception) { failure = exception; }
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
if (failure != null)
throw new AssertFailedException(failure.ToString());
}
}
}
@@ -0,0 +1,92 @@
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.RegisterReaders.AllyReader;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication;
using TBF.Rig.RegisterReaders.GenesisRegReader.implementations;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.common;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations;
using LegacyIPerlReader = TBF.Rig.RegisterReaders.IPerlReader.implementations.SmartReader;
using AsicIPerlReader = TBF.Rig.RegisterReaders.iPerlASICReader.implementations.SmartReader;
namespace TBFTests.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
{
/// <summary>Fast tests of the correction-adapter contract; no COM port is opened.</summary>
[TestClass]
public class CorrectionsContractTests
{
[TestMethod]
public void ManualAdapters_ExposeStableFamilyNames()
{
Assert.AreEqual("ALLY", new AllyCorrections(null).TypeIdentificatorName());
Assert.AreEqual("Genesis", new GenesisCorrections(null).TypeIdentificatorName());
Assert.AreEqual("iPerl", new IPerlCorrections(null).TypeIdentificatorName());
Assert.AreEqual("iPerl ASIC", new IPerlASICCorrections(null).TypeIdentificatorName());
Assert.AreEqual("Poseidon", new PoseidonCorrections(null).TypeIdentificatorName());
}
[TestMethod]
public void AllyManualAdapter_ExposesOnlySupportedSafeOperations()
{
var menu = new AllyCorrections(null).GetContextMenu();
CollectionAssert.AreEqual(
new[] { "Read Serial Number", "Read Version and Type", "Set RFID mode", "Set NFC mode" },
menu.MenuItems.Cast<System.Windows.Forms.MenuItem>().Select(item => item.Text).ToArray());
}
[TestMethod]
public void GenesisManualAdapter_ExposesOnlyImplementedOperations()
{
var menu = new GenesisCorrections(null).GetContextMenu();
CollectionAssert.AreEqual(
new[] { "Read PCB Number", "Set RFID mode", "Set NFC mode" },
menu.MenuItems.Cast<System.Windows.Forms.MenuItem>().Select(item => item.Text).ToArray());
}
[TestMethod]
public void ManualOnlyAdapters_RejectTestCommunicationExplicitly()
{
ICorrections correction = new AllyCorrections(null);
CommErr error = CommErr.None;
string result = string.Empty;
bool handled = correction.WorkerActivity("Read Serial Number", null, null, null, 0,
ref error, ref result, new bool[0], 0, 0);
Assert.IsFalse(handled);
Assert.AreEqual(CommErr.WrongIPerlType, error);
StringAssert.Contains(result, "test communication is not implemented");
}
[TestMethod]
public void IPerlCorrection_DisabledReaderStopsBusinessActivityBeforeCommunication()
{
ISmartReader reader = new LegacyIPerlReader(
new TBF.Rig.RegisterReaders.IPerlReader.Factory().DefaultConfig()) { Disabled = true };
CommErr error = CommErr.None;
string result = string.Empty;
bool handled = new IPerlCorrections(null).WorkerActivity("Unknown activity", reader, null, null, 0,
ref error, ref result, new[] { true }, 0, 0);
Assert.IsTrue(handled);
Assert.AreEqual(CommErr.HeadDisabledByUser, error);
}
[TestMethod]
public void IPerlAsicCorrection_DisabledReaderStopsBusinessActivityBeforeCommunication()
{
ISmartReader reader = new AsicIPerlReader(
new TBF.Rig.RegisterReaders.iPerlASICReader.Factory().DefaultConfig()) { Disabled = true };
CommErr error = CommErr.None;
string result = string.Empty;
bool handled = new IPerlASICCorrections(null).WorkerActivity("Unknown activity", reader, null, null, 0,
ref error, ref result, new[] { true }, 0, 0);
Assert.IsTrue(handled);
Assert.AreEqual(CommErr.HeadDisabledByUser, error);
}
}
}
@@ -0,0 +1,114 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.Generic;
using TBF.Rig.GenericDevices;
using TBF.Rig.RegisterReaders.AllyReader;
using TBF.Rig.RegisterReaders.GenesisRegReader.implementations;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.common;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations;
using IPerlSmartReader = TBF.Rig.RegisterReaders.IPerlReader.implementations.SmartReader;
using IPerlAsicSmartReader = TBF.Rig.RegisterReaders.iPerlASICReader.implementations.SmartReader;
using PoseidonSmartReader = TBF.Rig.RegisterReaders.PoseidonReader.SmartReader;
namespace TBFTests.Rig.Uni.SharedDialogs.SmartMetersCommunication.implementations
{
/// <summary>Reader-to-adapter wiring tests. They use real reader classes but no hardware transport.</summary>
[TestClass]
public class RegisterReaderCorrectionWiringTests
{
[TestMethod]
public void AllyReader_IsRecognizedOnlyByAllyCorrection()
{
AllyMeterReader reader = new AllyMeterReader(new AllyReaderCfg(new TBF.Rig.RegisterReaders.AllyReader.Factory()));
Assert.IsInstanceOfType(reader, typeof(ISmartReader));
Assert.IsInstanceOfType(reader, typeof(IRegReaderSmart));
Assert.IsTrue(new AllyCorrections(null).IsFamilyOfSmartReader(reader));
Assert.IsFalse(new GenesisCorrections(null).IsFamilyOfSmartReader(reader));
Assert.IsFalse(new IPerlCorrections(null).IsFamilyOfSmartReader(reader));
Assert.IsFalse(new IPerlASICCorrections(null).IsFamilyOfSmartReader(reader));
}
[TestMethod]
public void GenesisReader_IsRecognizedOnlyByGenesisCorrection()
{
GenesisSmartReader reader = new GenesisSmartReader(
new TBF.Rig.RegisterReaders.GenesisRegReader.GenesisCfg(new TBF.Rig.RegisterReaders.GenesisRegReader.Factory()));
Assert.IsInstanceOfType(reader, typeof(ISmartReader));
Assert.IsInstanceOfType(reader, typeof(IRegReaderSmart));
Assert.IsTrue(new GenesisCorrections(null).IsFamilyOfSmartReader(reader));
Assert.IsFalse(new AllyCorrections(null).IsFamilyOfSmartReader(reader));
Assert.IsFalse(new IPerlCorrections(null).IsFamilyOfSmartReader(reader));
Assert.IsFalse(new IPerlASICCorrections(null).IsFamilyOfSmartReader(reader));
}
[TestMethod]
public void ExistingSmartReaderFamilies_AreBoundToTheirOwnCorrections()
{
ISmartReader oldIperl = new IPerlSmartReader(
new TBF.Rig.RegisterReaders.IPerlReader.Factory().DefaultConfig());
ISmartReader asic = new IPerlAsicSmartReader(
new TBF.Rig.RegisterReaders.iPerlASICReader.Factory().DefaultConfig());
ISmartReader poseidon = new PoseidonSmartReader(
new TBF.Rig.RegisterReaders.PoseidonReader.Factory().DefaultConfig());
Assert.IsTrue(new IPerlCorrections(null).IsFamilyOfSmartReader(oldIperl));
Assert.IsTrue(new IPerlASICCorrections(null).IsFamilyOfSmartReader(asic));
Assert.IsTrue(new PoseidonCorrections(null).IsFamilyOfSmartReader(poseidon));
Assert.IsFalse(new IPerlCorrections(null).IsFamilyOfSmartReader(asic));
Assert.IsFalse(new IPerlASICCorrections(null).IsFamilyOfSmartReader(oldIperl));
Assert.IsFalse(new PoseidonCorrections(null).IsFamilyOfSmartReader(oldIperl));
}
[TestMethod]
public void NonAsicFamilies_AllyPoseidonAndGenesisCordonel_UseDistinctSelectionKeysAndCorrections()
{
// GenesisSmartReader is the RegisterReaders.GenesisRegReader implementation
// backed by the Cordonel protocol. Keep this check at the reader boundary so
// a later rename cannot silently route a Cordonel reader through another
// SmartCommunicationForm correction adapter.
ISmartReader ally = new AllyMeterReader(
new AllyReaderCfg(new TBF.Rig.RegisterReaders.AllyReader.Factory()));
ISmartReader genesisCordonel = new GenesisSmartReader(
new TBF.Rig.RegisterReaders.GenesisRegReader.GenesisCfg(
new TBF.Rig.RegisterReaders.GenesisRegReader.Factory()));
ISmartReader poseidon = new PoseidonSmartReader(
new TBF.Rig.RegisterReaders.PoseidonReader.Factory().DefaultConfig());
Assert.AreEqual("ally", SmartReaderSelection.GetFamilyKey(ally));
Assert.AreEqual("genesis", SmartReaderSelection.GetFamilyKey(genesisCordonel));
Assert.AreEqual("poseidon", SmartReaderSelection.GetFamilyKey(poseidon));
AssertOnlyMatchingCorrection(ally, new AllyCorrections(null),
new GenesisCorrections(null), new PoseidonCorrections(null));
AssertOnlyMatchingCorrection(genesisCordonel, new GenesisCorrections(null),
new AllyCorrections(null), new PoseidonCorrections(null));
AssertOnlyMatchingCorrection(poseidon, new PoseidonCorrections(null),
new AllyCorrections(null), new GenesisCorrections(null));
}
[TestMethod]
public void SmartReaderContract_PreservesManualSelectionStateWithoutTransport()
{
ISmartReader reader = new AllyMeterReader(new AllyReaderCfg(new TBF.Rig.RegisterReaders.AllyReader.Factory()));
reader.Disabled = true;
reader.SerialNr = "ALLY-TEST";
reader.SetCommunicationInterface("RFID");
Assert.IsTrue(reader.Disabled);
Assert.AreEqual("ALLY-TEST", reader.SerialNr);
Assert.AreEqual("Touch-Read", reader.CommInterface);
}
private static void AssertOnlyMatchingCorrection(ISmartReader reader, ICorrections expected,
params ICorrections[] unexpected)
{
Assert.IsTrue(expected.IsFamilyOfSmartReader(reader));
foreach (ICorrections correction in unexpected)
Assert.IsFalse(correction.IsFamilyOfSmartReader(reader));
}
}
}
+11
View File
@@ -135,11 +135,18 @@
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\CliRunnerTimeoutTest.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\CliRunnerTimeoutUnitTest.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\PoseidonReaderTest.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\PoseidonReadCycleTest.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\PoseidonCustomerCliResponseTest.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\PoseidonDialogTransferTest.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\PoseidonSingleMeterIntegrationTest.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonReader\UniHeadTestCtrlTest.cs" />
<Compile Include="Rig\Scales\MettlerToledo\ReadStableMassOpTest.cs" />
<Compile Include="Rig\TestMethods\GenesisCommunication\GenesisHead\GenesisHeadBatchIntegrationTest.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\AllyMeterReaderTest.cs" />
<Compile Include="Rig\RegisterReaders\AsicReader\AsicReaderFactoryTest.cs" />
<Compile Include="Rig\RegisterReaders\AsicReader\AsicCorrectionsTest.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\AllyOpticalSampleTest.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\AllyOpticalRealExamplesTest.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\AllyOpticalTelegramFactory.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\AllyReaderCfgTest.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\communication\AllyCommandServiceTest.cs" />
@@ -154,11 +161,15 @@
<Compile Include="Rig\RegisterReaders\AllyReader\integration\AllyIntegrationTestLogger.cs" />
<Compile Include="Rig\RegisterReaders\AllyReader\integration\AllyHardwareIntegrationTest.cs" />
<Compile Include="Rig\TestMethods\AllyCalibration\TestMethodConfigTest.cs" />
<Compile Include="Rig\TestMethods\AllyCalibration\AllyOpticalWorkflowIntegrationTest.cs" />
<Compile Include="Rig\TestMethods\iPerlCommunication\common\OptoTelegramRawTest.cs" />
<Compile Include="Rig\TestMethods\iPerlCommunication\communication\FakeSerialDriver.cs" />
<Compile Include="Rig\TestMethods\iPerlCommunication\communication\IperlResponseFactory.cs" />
<Compile Include="Rig\TestMethods\iPerlCommunication\communication\RadioServiceTest.cs" />
<Compile Include="Rig\Uni\SharedDialogs\SmartMetersCommunication\implementations\PoseidonCorrectionsTest.cs" />
<Compile Include="Rig\Uni\SharedDialogs\SmartMetersCommunication\SmartCommunicationFormTests.cs" />
<Compile Include="Rig\Uni\SharedDialogs\SmartMetersCommunication\implementations\CorrectionsContractTests.cs" />
<Compile Include="Rig\Uni\SharedDialogs\SmartMetersCommunication\implementations\RegisterReaderCorrectionWiringTests.cs" />
</ItemGroup>
<ItemGroup>
<None Include="app.config" />