tbf/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReader.cs

738 lines
24 KiB
C#

///
/// Copyright (c) 2013-2020 Sensus Slovensko a.s.
///
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Ports;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Forms;
using log4net;
using Common;
using NHibernate.Util;
using SharedDatabase.Entities;
using TBF.Boxes;
using TBF.Rig.Generic;
using TBF.Rig.GenericDevices;
using TBF.Rig.RegisterReaders.SerialStream;
using TBF.Rig.RegisterReaders.StandingStartStop;
using TBF.Rig.Uni.SharedDialogs.SmartMetersCommunication.common;
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;
readonly ControlBoard.IControlBoard controlBoard;
public int ComPortNr => registerReaderCfg?.ComPortNr ?? -1;
public PoseidonCfg RegPoseidonCfg => registerReaderCfg;
private bool activeHandlerSessioEnabled = false;
private CliRunner _cliRunner;
private CliRunner CliRunner
{
get
{
return (_cliRunner != null ?
_cliRunner :
(_cliRunner = new CliRunner(GetCliLogging())));
}
}
public enum CurrentPoseidonOp
{
None,
SendStartDataStream,
SendStartDataStream_Runing,
SendStartDataStream_Done,
ReadDataStream_Start,
ReadDataStream_End,
ReadDatastream_Running,
ReadDatastream_Done,
Done,
Error,
}
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 ;
}
public bool GetCliLogging()
{
return _isCliLogging.HasValue ? _isCliLogging.Value : true;
}
public void SetCliLogging(bool isCliLogging)
{
_isCliLogging = isCliLogging;
}
public int Position
{
get
{
int firstDigitPos = Name.IndexOfAny(new char[] { '1', '2', '3', '4', '5', '6', '7', '8', '9', '0' });
int position;
return (firstDigitPos < 0) ? 0 : (int.TryParse(Name.Substring(firstDigitPos), out position) ? position : 0);
}
}
public RegisterReaderType RegisterReaderType { get { return RegisterReaderType.DataStream; } }
public double PulsesPerLtr {
get { return 1000.0;}
set { PulsesPerLtr = value; }
}
public double LtrsPerPulse { get { return 1 / PulsesPerLtr; } }
public string QuantityUnits { get; set; }
///
/// Wrappers for procedure parameters
///
public StreamType StreamType { get { return (registerReaderCfg.ProcParams != null) ?registerReaderCfg.ProcParams.StreamType : StreamType.None; } }
public int FrameLength { get { return (registerReaderCfg.ProcParams != null) ?registerReaderCfg.ProcParams.FrameLength : 0; } }
public double FrameFrequency { get { return (registerReaderCfg.ProcParams != null) ?registerReaderCfg.ProcParams.FrameFrequency : 0; } }
public Common.Unit VolumeUnits { get { return (registerReaderCfg.ProcParams != null) ? registerReaderCfg.ProcParams.VolumeUnits : Common.Unit.l; } }
public double VolumeScaleFactor { get { return (registerReaderCfg.ProcParams != null) ? registerReaderCfg.ProcParams.VolumeScaleFactor : 1; } }
public int VolumeFieldStart { get { return (registerReaderCfg.ProcParams != null) ? registerReaderCfg.ProcParams.VolumeFieldStart : 0; } }
public int VolumeFieldEnd { get { return (registerReaderCfg.ProcParams != null) ? registerReaderCfg.ProcParams.VolumeFieldEnd : 0; } }
public FieldFormat VolumeFieldFormat { get { return (registerReaderCfg.ProcParams != null) ? registerReaderCfg.ProcParams.VolumeFieldFormat : FieldFormat.None; } }
public Common.Unit TimeUnits { get { return (registerReaderCfg.ProcParams != null) ? registerReaderCfg.ProcParams.TimeUnits : Common.Unit.s; } }
public double TimeScaleFactor { get { return (registerReaderCfg.ProcParams != null) ? registerReaderCfg.ProcParams.TimeScaleFactor : 1; } }
public int TimeFieldStart { get { return (registerReaderCfg.ProcParams != null) ? registerReaderCfg.ProcParams.TimeFieldStart : 0; } }
public int TimeFieldEnd { get { return (registerReaderCfg.ProcParams != null) ? registerReaderCfg.ProcParams.TimeFieldEnd : 0; } }
public FieldFormat TimeFieldFormat { get { return (registerReaderCfg.ProcParams != null) ? registerReaderCfg.ProcParams.TimeFieldFormat : FieldFormat.None; } }
public bool CommFailed
{
get { return commFailed; }
set { commFailed = value; }
}
bool commFailed;
/// <summary>
/// Passed to OptoTelegramRaw.UpdateFromString(...)
/// </summary>
Int64 volumeRawExtLast;
Int64 timestampExtLast;
///
/// Required for IRegisterReader interface
///
public int WMPulses { get { return wmPulses; } }
public int WMRefPulses { get { return wmRefPulses; } }
public double WMVolume { get { return wmVolume; } }
public double BeginWMState { get { return beginWMState; } set { beginWMState = value;} }
public double EndWMState { get { return endWMState; } set { endWMState = value;} }
public double WMTestTime { get { return wmTestTime; } }
public string SerialNr { get => wmSerialNr; set => wmSerialNr = value; }
double beginWMState;
double endWMState;
double wmVolume;
int wmPulses;
int wmRefPulses;
double wmTestTime;
private string wmSerialNr;
int timeFromStart; /// [s] Time from test start to determine when the test start sample should be taken
public IOperation ReadDatastreamOp()
{
return this;
}
/// <summary>
/// Store/update values to be used as a part of the opto-data log file name.
/// Stop data stream processing and saving, if it is enabled.
/// </summary>
/// <param name="test">Currently executed test</param>
/// <param name="repetitionNr">Currently executed repetition number</param>
public void TestIsGoingToStartSoon(Config.Entities.Test test, int repetitionNr)
{
/// Store/update values to be used as a part of the opto-data log file name
testName = test.Name;
testRepeats = test.Repeats;
this.repetitionNr = repetitionNr;
}
///
string testName;
int testRepeats;
int repetitionNr;
///
/// Volume of water from the opto telegram
///
private Int64 lastVolumeRaw; /// Last read raw volume
private double volumeLtr; ///
private double volumeLtr0;
public double VolumeLtrStart { get { return volumeLtrStart; } } /// Test start volume for metrology
public double VolumeLtrEnd { get { return volumeLtrEnd; } } /// Test end volume for metrology
double volumeLtrStart; /// Test start volume for metrology
double volumeLtrEnd; /// Test end volume for metrology
double volumeLtrEnd1; /// auxiliary buffer1 to keep the end volume before test stops
double volumeLtrEnd2; /// auxiliary buffer2 to keep the end volume before test stops
double volumeLtrEnd3; /// auxiliary buffer3 to keep the end volume before test stops
///
/// Timestamp from the opto telegram
///
bool lastTimestampValid;
private Int64 lastTimestamp;
private double timestampSec;
private double timestampSec0;
public bool NoSamples { get { return (timestampSecEnd - timestampSecStart) < float.Epsilon; } }
public double TimestampSecStart { get { return timestampSecStart; } }
public double TimestampSecEnd { get { return timestampSecEnd; } }
double timestampSecStart;
double timestampSecEnd;
double timestampSecEnd1;
double timestampSecEnd2;
double timestampSecEnd3;
private int frameIx;
public int TestStartFrameIx;
public int TestEndFrameIx;
DatastreamFrame[] datastreamFrames;
const int MaxDatastreamFramesCount = 80000; /// almost 3h at 8 Hz
int datastreamFramesCount;
string datastreamLogFileName;
DatastreamFrame toBeFlushed;
int flushedFramesCount;
public int FlushedFramesCount
{
get { return flushedFramesCount; }
set { flushedFramesCount = lastFlushedFramesCount = lastFlushedFramesCount_1 = value; }
}
int lastFlushedFramesCount_1;
int lastFlushedFramesCount;
public int FlushedFramesDelta
{
get
{
int retval = Math.Max(flushedFramesCount - lastFlushedFramesCount, lastFlushedFramesCount - lastFlushedFramesCount_1);
lastFlushedFramesCount_1 = lastFlushedFramesCount;
lastFlushedFramesCount = flushedFramesCount;
return retval;
}
}
///
/// Opto serial port and worker thread related private variables
///
private SerialPortData serialPort; /// Used in DebugMode.Normal
private TextReader textReader; /// Used instead of serialPort in DebugMode.Simulate
public PoseidonReader() { }
public PoseidonReader(Generic.IComponentCfg cfg, IList<Generic.IComponent> components)
: base(cfg)
{
registerReaderCfg = cfg as PoseidonCfg;
/// Control board is used to read reference flowmeter pulses
// controlBoard = TbfComponents.FindComponent(cfg.ParentName, components) as ControlBoard.IControlBoard;
// if (controlBoard == null) throw new Exception("Cannot find " + Name + " parent");
//
// log.Warn(this.ToString());
}
public override void Initialize()
{
Clear();
//log.WarnFormat("Initialize() ... FrameFormat set to {0}");
/// Allocate memory for opto-data from iPerl
datastreamFrames = new DatastreamFrame[MaxDatastreamFramesCount];
for (int i = 0; i < MaxDatastreamFramesCount; i++) datastreamFrames[i] = new DatastreamFrame();
toBeFlushed = new DatastreamFrame();
flushedFramesCount = 0;
activeHandlerSessioEnabled = false;
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),
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
//serialPort.Open();
//we can check file program if exists
if (!File.Exists(serialPort.SerialPortCmdClientPath))
{
log.FatalFormat("Serial port CLI {0} does not exist!", serialPort.SerialPortCmdClientPath);
return;
}
log.FatalFormat("{0} initialized: {1}", Name, this);
}
// else if (DebugLevel == DebugMode.Simulate)
// {
// }
else
{
serialPort = null;
log.FatalFormat("{0} in other mode: {1}", Name, this);
}
}
internal static string GetCliFileNameForMode(DebugMode debugMode, string configuredCliFileName)
{
return debugMode == DebugMode.Simulate
? SimulatedCliFileName
: configuredCliFileName;
}
public void Clear()
{
log.DebugFormat("{0}:Clear()", Name);
beginWMState = 0;
endWMState = 0;
wmRefPulses = 0;
}
private void ReadPulses()
{
wmRefPulses = controlBoard?.RefPulses ?? 0;
}
/// <summary>Start this operation</summary>
public void Start()
{
}
/// <summary>
/// for debug purposes what time will consume answer
/// </summary>
private long startTimeInMilis = -1, fullTimeInMilis = -1;
/// 30 seconds
private static long SafetyTimeOut = 30 * 1000;
private long incommingTime = -1;
private bool _lastOpTimedOut;
public long DeltaTime { get{return fullTimeInMilis;}}
public long IncommingTime { get{return incommingTime;}}
/// <summary>Run this operation</summary>
/// <returns>eventDone</returns>
public Event Run()
{
lock (this)
{
timeFromStart += StateMachine.Period;
ReadPulses();
}
if (_currentOp == CurrentPoseidonOp.SendStartDataStream)
{
CliRunner.Clear();
_lastOpTimedOut = false;
CliRunner.AddSendAsync(serialPort, SerialPortData.EMeterArg.AllParams);
_currentOp = CurrentPoseidonOp.SendStartDataStream_Runing;
return Event.Busy;
}
else if (_currentOp == CurrentPoseidonOp.SendStartDataStream_Runing)
{
if (CliRunner.AreTasksDone())
{
_currentOp = CurrentPoseidonOp.SendStartDataStream_Done;
}
else if (CliRunner.TimeOutReceived(SafetyTimeOut))
{
_lastOpTimedOut = true;
CliRunner.CancelUndoneTasksAsTimedOut();
_currentOp = CurrentPoseidonOp.SendStartDataStream_Done;
}
return Event.Busy;
}
else if (_currentOp == CurrentPoseidonOp.SendStartDataStream_Done)
{
var firstTaskInfo = CliRunner.TaskPool
.FindLast(t => t.Task is Task<string> && t.UseResult);
if (firstTaskInfo != null)
{
var task = (Task<string>)firstTaskInfo.Task;
string data = task.Result;
if (!string.IsNullOrEmpty(data))
{
TryGetDeviceId(data, out wmSerialNr);
}
}
else if (_lastOpTimedOut)
{
log.Warn(
$"PoseidonReader {Name}: SendStartDataStream timed out, no completed result will be used.");
}
CliRunner.Clear();
_currentOp = _lastOpTimedOut ? CurrentPoseidonOp.Error : CurrentPoseidonOp.Done;
return Event.Done;
}
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;
return Event.Busy;
}
else if (_currentOp == CurrentPoseidonOp.ReadDatastream_Running)
{
if (CliRunner.AreTasksDone())
{
_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))
{
_lastOpTimedOut = true;
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;
}
else if (_currentOp == CurrentPoseidonOp.ReadDatastream_Done)
{
fullTimeInMilis = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - startTimeInMilis;
log.Debug($"PoseidonReader.Run() Name= {Cfg.Name} fullTimeInMilis = {fullTimeInMilis}");
JsonDataFromPoseidon data = null;
var firstTaskInfo = CliRunner.TaskPool
.FindLast(t => t.Task is Task<JsonDataFromPoseidon> && t.UseResult);
if (firstTaskInfo != null)
{
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.");
}
else
{
log.Warn("No completed JsonDataFromPoseidon task available.");
}
}
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
{
wmSerialNr = data.DeviceId ?? wmSerialNr;
}
catch (Exception e)
{
log.Error("Serial Nr - parse error!", e);
}
}
double volumeLi;
string dialogValueFailureReason;
if (TryGetDialogValue(data, out volumeLi, out dialogValueFailureReason))
{
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);
}
}
}
CliRunner.Clear();
_currentOp = _lastOpTimedOut ? CurrentPoseidonOp.Error : CurrentPoseidonOp.Done;
return Event.Done;
}
return Event.None;
}
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()
{
//TODO BUMI stop CLI serial port
}
public void RunDeviceBefore()
{
//Start CLI communication and set CLI inputs and get CLI data
//ValidateCliFileExistence(true);
}
private void ValidateCliFileExistence(bool showErrorIfMissing)
{
if (DebugLevel == DebugMode.Normal && serialPort != null)
{
if (!serialPort.CliExists)
{
log.ErrorFormat("CLI file {0} does not exist!", serialPort.SerialPortCmdClientPath);
if (showErrorIfMissing)
MessageBox.Show($"CLI file {serialPort.SerialPortCmdClientPath} does not exist!", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
log.DebugFormat("CLI file {0} found", serialPort.SerialPortCmdClientPath);
}
}
public void RunDeviceAfter()
{
}
public void StopDevice()
{
try
{
if (DebugLevel == DebugMode.Normal && serialPort != null)
{
}
}
catch
{
}
}
public void StopDevice2()
{
}
public void StartSession()
{
ValidateCliFileExistence(false);
//this is no place to get device id
}
// private void RetrieveDeviceIdFromResponse()
// {
// if (DebugLevel == DebugMode.Simulate)
// {
// wmSerialNr = "777321";
// }
// else
// {
// //TODO BUMI start session - CMD send data to Poseidon
//
// CliRunner.AddSendAsync(serialPort, SerialPortData.EMeterArg.DeviceId);
// CliRunner.WaitAll();
// foreach (Task task in CliRunner.TaskPool)
// {
// if (task is Task<string>)
// {
// string result = (task as Task<string>).Result;
// if (TryGetDeviceId(result, out wmSerialNr))
// {
// break;
// }
// }
// }
//
// CliRunner.Clear();
// }
// }
private static readonly Regex DeviceIdRegex = new Regex(
@"(?im)(?:" +
@"^\s*Device\s*Id\s*:\s*([^\r\n]+)\s*$" + // old format
@"|" +
@"""DeviceId""\s*:\s*""?([0-9]+)""?" + // JSON format
@")",
RegexOptions.Compiled);
public bool TryGetDeviceId(string result, out string s)
{
s = null;
if (string.IsNullOrEmpty(result))
return false;
var m = DeviceIdRegex.Match(result);
if (!m.Success)
return false;
// one of the groups will be filled
s = !string.IsNullOrEmpty(m.Groups[1].Value)
? m.Groups[1].Value.Trim()
: m.Groups[2].Value.Trim();
return true;
}
public void SaveMark(object mark)
{
throw new NotImplementedException();
}
public void EndSession()
{
if (_cliRunner != null)
{
_cliRunner.Clear();
}
activeHandlerSessioEnabled = false;
}
}
}