From f613a9cdb4acd66b53091e8e9b7ba5a36c23f75a Mon Sep 17 00:00:00 2001 From: Thomas Wiedebusch Date: Wed, 28 Jan 2026 13:49:54 +0100 Subject: [PATCH] FM2014: - prepared for multiple FM2014s - 1.Step --- .../FM2014/FM2014Core/FM2014.cs | 1204 +++++++++-------- MiniPrf/Ui/FrmMainMiniPrf.cs | 23 +- 2 files changed, 657 insertions(+), 570 deletions(-) diff --git a/Common/Hardware/WaterMeter/MechanicalMeter/FM2014/FM2014Core/FM2014.cs b/Common/Hardware/WaterMeter/MechanicalMeter/FM2014/FM2014Core/FM2014.cs index 43c048ba..3e0f777b 100644 --- a/Common/Hardware/WaterMeter/MechanicalMeter/FM2014/FM2014Core/FM2014.cs +++ b/Common/Hardware/WaterMeter/MechanicalMeter/FM2014/FM2014Core/FM2014.cs @@ -38,6 +38,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO.Ports; +using System.Linq; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -46,7 +47,6 @@ using Xylem.Common.Metrology.Measurements.Consts; using Xylem.Common.Utils.ProcessExec.EventArguments; using static Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core.Consts.FM2014CmdDef; - namespace Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core { @@ -58,24 +58,66 @@ namespace Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core /// public class FM2014 { - #region events - /// - /// Received data feedback - /// - public virtual event EventHandler OnRawRecordReceived; - #endregion - - #region properties - /// - /// Shared cancellation token - /// - public static CancellationToken SharedCancellationToken { set; get; } - + #region ------------------------------------------- constants ------------------------------------------------- /// /// Resolution of FM2014 timer for measurement of pulse width in seconds based on 2994 Hz /// private const Double TMR_RESOLUTION_s = 334.001336e-6; + /// + /// This is the invalid marker which can be stored to the FM2014 with command + /// 'W' - 'REF pulses per volume'! The REF pulses/cm can then be restored using + /// 'Ref_pulses_per_cm = Dut_pulses_per_cm * RefToDutScale_norm' + /// + private const UInt32 RefPulsesPerVolumeRegulationSetupInvalidMarker = 1; + + /// + /// fm2014 is the minimum value which can be stored to the FM2014 with commands + /// 'V' - 'DUT pulses per volume' and 'W' - 'REF pulses per volume'! + /// + private const UInt32 PulsesPerVolumeRegulationSetupMin = 1; + + /// + /// This is the maximum value which can be stored to the FM2014 with command + /// 'V' - 'DUT pulses per volume'! + /// + private const UInt32 PulsesPerVolumeRegulationSetupMax = 9999; + + /// + /// This is the maximum input value for the REF which CANNOT be stored to FM2014 + /// but will be used to calculate the 'RefToDutScale_norm'. + /// + private const UInt32 RefPulsesPerCmRegulationInputLimitMax = 100000000; + + /// + /// Publish the FM2014 minimum REF to DUT scale for error display. + /// + public const Double RefToDutScaleMin = 0.0001; + /// + /// Publish the FM2014 maximum REF to DUT scale for error display. + /// + public const Double RefToDutScaleMax = 999.9; + + /// + /// The mantissa for the scale value has to be in the range from 1000 to 9999. + /// + private const UInt16 MantissaScaleMin = 1000; + + #endregion ---------------------------------------- constants ------------------------------------------------- + + #region ------------------------------------------- events ---------------------------------------------------- + /// + /// Received data feedback + /// + public virtual event EventHandler OnRawRecordReceived; + #endregion ---------------------------------------- events ---------------------------------------------------- + + #region ------------------------------------------- static properties ----------------------------------------- + /// + /// Shared cancellation token + /// + public static CancellationToken SharedCancellationToken { set; get; } + /// /// Shared receive task is active for all connected FM2014. /// Used to mark this event and to kill ongoing task if set inactive. @@ -85,7 +127,27 @@ namespace Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core /// /// Shared list of all connected FM2014 /// - public static List RegisteredFm2014s { get; private set; } + public static List RegisteredFm2014s { get; private set; } + + /// + /// Serial port shared for all connected FM2014 + /// + private static SerialPort SharedSerialPort { get; set; } + + /// + /// Marker if broadcast connection was last communication + /// + private static CmdType SharedCmdTypeReminderLastCmd { get; set; } + + + /// + /// Reminder of last individual address of FM2014 to initiate a new communication to another device. + /// + private static Int32 ReminderLastIndividualCommFm2014Address { get; set; } + + #endregion ---------------------------------------- static properties ----------------------------------------- + + #region ------------------------------------------- object properties ----------------------------------------- ///// ///// Logger for raw data output @@ -100,11 +162,6 @@ namespace Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core /// public String SerialNumber { get; private set; } - /// - /// Serial port shared for all connected FM2014 - /// - private static SerialPort SharedSerialPort { get; set; } - /// /// FM2014 identification string e.g. FM2014.112.1 /// @@ -124,11 +181,6 @@ namespace Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core } } - /// - /// Marker if broadcast connection was last communication - /// - private static CmdType SharedCmdTypeReminderLastCmd { get; set; } - /// /// FM2014 address string /// @@ -169,11 +221,7 @@ namespace Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core /// /// Use the damped tolerance output from the FM2014 to stabilize this value /// - public Boolean UseDampedTolerance - { - get; - set; - } + public Boolean UseDampedTolerance { get; set; } /// /// Publish the FM2014 minimum pulses for REF and DUT time measurement. @@ -184,32 +232,6 @@ namespace Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core /// public const UInt32 TimeMeasurementPulsesSetupMax = 0xFFFF; - - /// - /// This is the invalid marker which can be stored to the FM2014 with command - /// 'W' - 'REF pulses per volume'! The REF pulses/cm can then be restored using - /// 'Ref_pulses_per_cm = Dut_pulses_per_cm * RefToDutScale_norm' - /// - private const UInt32 RefPulsesPerVolumeRegulationSetupInvalidMarker = 1; - - /// - /// This is the minimum value which can be stored to the FM2014 with commands - /// 'V' - 'DUT pulses per volume' and 'W' - 'REF pulses per volume'! - /// - private const UInt32 PulsesPerVolumeRegulationSetupMin = 1; - - /// - /// This is the maximum value which can be stored to the FM2014 with command - /// 'V' - 'DUT pulses per volume'! - /// - private const UInt32 PulsesPerVolumeRegulationSetupMax = 9999; - - /// - /// This is the maximum input value for the REF which CANNOT be stored to FM2014 - /// but will be used to calculate the 'RefToDutScale_norm'. - /// - private const UInt32 RefPulsesPerCmRegulationInputLimitMax = 100000000; - private UInt32 _ref_pulse_per_cm; /// /// Reference pulses per cubic-meter @@ -253,20 +275,6 @@ namespace Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core } } - /// - /// Publish the FM2014 minimum REF to DUT scale for error display. - /// - public const Double RefToDutScaleMin = 0.0001; - /// - /// Publish the FM2014 maximum REF to DUT scale for error display. - /// - public const Double RefToDutScaleMax = 999.9; - - /// - /// The mantissa for the scale value has to be in the range from 1000 to 9999. - /// - private const UInt16 MantissaScaleMin = 1000; - /// /// As the scale needed to be sent to the FM2014 doesn't follow the standardized nomenclature /// for floating point values it has to be converted to the FM2014 requirement. @@ -381,63 +389,313 @@ namespace Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core _actualProcessProgress_percent = 100.0 * _actualProcessProgress / MaxActualProcessProgress; } } - #endregion + #endregion ---------------------------------------- object properties ----------------------------------------- - #region methods + #region ------------------------------------------- static methods -------------------------------------------- /// /// Executes all StoreConfiguration and StoreCalibration for each application. /// /// true if all configurations are stored - public Boolean StoreAllConfigurations() + public static Boolean StoreAllConfigurations() { return false; } /// - /// Common routine for reboot being able to override this routine which will be called in - /// to simulate a reboot. + /// Read FM2014: + /// - Accesses directly to the 'SerialPort.ReadTo', + /// - An exception has to be caught by the caller. /// + /// + /// + /// /// - public StatusReturn RebootFM2014() + /// + /// - Initial. + /// + /// + /// - Support for multiple FM2014s. + /// + private static Boolean Read(CmdName cmdName, FM2014 fm2014, out String responseStr) { - return StatusReturn.Failed; + responseStr = ""; + var info = $"{Resources.StrCommResponse}: {GetCmdInfo(cmdName)}"; + var statusReturn = StatusReturn.Failed; + + // Check for individual command without response + if (CmdType.INDIVIDUAL_UNRESPONSIVE == SharedCmdTypeReminderLastCmd && + CmdType.INDIVIDUAL_UNRESPONSIVE == GetCmdCmdType(cmdName)) + { + statusReturn = StatusReturn.Skipped; + } + else if (CmdType.INDIVIDUAL == SharedCmdTypeReminderLastCmd && + CmdType.INDIVIDUAL == GetCmdCmdType(cmdName)) + { + // Read or timeout with a TimeoutException + var answerStr = SharedSerialPort?.ReadTo(END_OF_STR); + if (string.IsNullOrEmpty(answerStr)) + { + statusReturn = StatusReturn.Failed; + } + else + { + statusReturn = StatusReturn.Okay; + responseStr = answerStr; + } + } + + var isErrorStr = StatusReturn.Failed == statusReturn ? Resources.StrError + ": " : ""; + var response = new CmdResponse(cmdName, $"{isErrorStr}{info} {responseStr}"); + fm2014?.OnRawRecordReceived?.Invoke(fm2014, new ProcessExecEventArgs("", specificInfoObj: response, + statusReturn: statusReturn)); + + return StatusReturn.Failed != statusReturn; } /// - /// Connect to FM2014 + /// Write command to FM2014: + /// - Accesses directly to the 'SerialPort.Write', + /// - Select broadcast or individual addressing based on the command table, + /// - Transmits optional data: + /// - If the dataObj is a string, this is going to be sent as is (e.g. "1000K2"), + /// - If the dataObj is an UInt16 or Byte this will be assembled with the command code (e.g. "3T"), + /// - Every 'Write' is a string delimited here with 'CR', + /// - An exception has to be caught by the caller. /// - /// - /// + /// + /// + /// /// - public Boolean Connect(String comPort, Int32? address = null) + /// + /// - Initial. + /// + /// + /// - Support for multiple FM2014s. + /// + private static Boolean Write(CmdName cmdName, FM2014 fm2014, Object dataObj = null) { + if (fm2014 == null) + return false; + if (SharedSerialPort == null) { - SharedSerialPort = new SerialPort(); - SharedSerialPort.BaudRate = 1200; - SharedSerialPort.DataBits = 7; - SharedSerialPort.Parity = Parity.Odd; - SharedSerialPort.PortName = comPort; - SharedSerialPort.ReadTimeout = 1000; - SharedSerialPort.WriteTimeout = 1000; + SharedCmdTypeReminderLastCmd = CmdType.NOT_INITIALIZED; + fm2014.IsLoggedOn = false; + SharedCyclicReceiveTaskLoopIsActive = false; + return false; } - if (RegisteredFm2014s == null) - RegisteredFm2014s = new List(); + // Initiate communication using the individual or broadcast address + var retVal = true; + if (CmdType.BROADCAST == GetCmdCmdType(cmdName)) + { + if (SharedCmdTypeReminderLastCmd != CmdType.BROADCAST) + { + retVal = InitiateBroadcastComm(fm2014); + } + } + else + { + if (ReminderLastIndividualCommFm2014Address != fm2014.Address || + SharedCmdTypeReminderLastCmd != CmdType.INDIVIDUAL || + SharedCmdTypeReminderLastCmd != CmdType.INDIVIDUAL) + { + retVal = InitiateIndividualComm(fm2014); + if (retVal) + ReminderLastIndividualCommFm2014Address = fm2014.Address; + } + } - if (address != null) - Address = (Int32)address; + if (!retVal) + { + return false; + } - RegisteredFm2014s.Add(AddressStr); + var cmdCodeStr = GetCmdStr(cmdName); + var info = GetCmdInfo(cmdName) + $": {cmdCodeStr}"; + CmdResponse response; + // Without data the command alone needs to be sent + if (dataObj == null) + { + SharedSerialPort.Write($"{cmdCodeStr}{END_OF_STR}"); + var additionalInfo = GetCmdCmdType(cmdName) == CmdType.BROADCAST + ? Resources.StrCommDirectionSetup + : Resources.StrCommDirectionRequest; + response = new CmdResponse(cmdName, $"{additionalInfo}: {info}"); + } + else + { + // A string has to be entire assembled, only the string delimiter will to be added here + if (dataObj is String) + { + SharedSerialPort.Write($"{dataObj}{END_OF_STR}"); + response = new CmdResponse(cmdName, $"{Resources.StrCommDirectionSetup}: {info}: {dataObj}"); + } - // Reset pulse ratios to force a preset with 'standalone' settings from - // FM2014 wit '%D@' - Read 'Default Measurement Setup' - _ref_pulse_per_cm = 0; - _dut_pulse_per_cm = 0; - SharedCmdTypeReminderLastCmd = CmdType.NOT_INITIALIZED; - return true; + // The data contains the preceding setup, the command code and string delimiter will be added here + else if (dataObj is UInt16 || dataObj is Byte) + { + SharedSerialPort.Write($"{dataObj}{cmdCodeStr}{END_OF_STR}"); + response = new CmdResponse(cmdName, + $"{Resources.StrCommDirectionSetup}: {info}: {dataObj}{cmdCodeStr}"); + + } + else + { + response = new CmdResponse(cmdName, + $"{Resources.StrError}: {Resources.StrCommDirectionSetup}: {info} - " + + $"{Resources.StrCmdRespErrorPramTypeNotSupported} {dataObj}"); + retVal = false; + } + } + + var statusReturn = retVal ? StatusReturn.Okay : StatusReturn.Failed; + fm2014.OnRawRecordReceived?.Invoke(fm2014, new ProcessExecEventArgs("", specificInfoObj: response, + statusReturn: statusReturn)); + + return retVal; } + /// + /// Initiate the broadcast communication: + /// - This 'InitiateBroadcastComm' needs to access the 'SerialPort.Write' directly as it is called from + /// the 'FM2014.Write' to check, based on the command table, if a broadcast or an individual communication + /// needs to be in initialized! + /// + /// + /// + /// + /// - Initial. + /// + /// + /// - Support for multiple FM2014s. + /// + private static Boolean InitiateBroadcastComm(FM2014 fm2014) + { + if (fm2014 == null) + return false; + + if (!SharedSerialPort.IsOpen) + { + SharedSerialPort.Open(); + if (!SharedSerialPort.IsOpen) + { + var response = new CmdResponse(CmdName.CMD_ADDR_PC, + $"{Resources.StrCmdRespErrorBroadcast}"); + fm2014.OnRawRecordReceived?.Invoke(fm2014, new ProcessExecEventArgs(Resources.StrError, + specificInfoObj: response, + statusReturn: StatusReturn.Failed)); + SharedCmdTypeReminderLastCmd = CmdType.NOT_INITIALIZED; + return false; + } + } + + // If broadcast communication is already active + if (SharedCmdTypeReminderLastCmd == CmdType.BROADCAST) + { + return true; + } + + try + { + // Establish connection to all FM2014s with broadcast address + // The direct write has to be used as this is being called from the 'Write' routine!!!! + SharedSerialPort.Write(BROADCAST_ADDRESS + END_OF_STR); + var cmdName = CmdName.CMD_CONNECT; + var info = GetCmdInfo(cmdName) + ": " + BROADCAST_ADDRESS; + var response = new CmdResponse(cmdName, info); + fm2014.OnRawRecordReceived?.Invoke(fm2014, new ProcessExecEventArgs("", specificInfoObj: response)); + SharedCmdTypeReminderLastCmd = CmdType.BROADCAST; + return true; + } + catch (Exception e) + { + var response = new CmdResponse(CmdName.CMD_ADDR_PC, + $"{Resources.StrCmdRespErrorBroadcast} - {e.Message}"); + fm2014.OnRawRecordReceived?.Invoke(fm2014, new ProcessExecEventArgs(Resources.StrError, + specificInfoObj: response, statusReturn: StatusReturn.Failed)); + SharedCmdTypeReminderLastCmd = CmdType.NOT_INITIALIZED; + return false; + } + } + + /// + /// Initiate communication to individual node + /// - This 'InitiateIndividualComm' needs to access the 'SerialPort.Write' and 'SerialPort.ReadTo' as this is + /// called from the 'FM2014.Write' and 'FM2014.Read' to check, based on the command table, if a broadcast or an + /// individual communication needs to be in initialized! + /// + /// + /// + /// + /// - Initial. + /// + /// + /// - Support for multiple FM2014s. + /// + private static Boolean InitiateIndividualComm(FM2014 fm2014) + { + if (fm2014 == null) + return false; + + CmdResponse response; + if (!SharedSerialPort.IsOpen) + { + SharedSerialPort.Open(); + if (!SharedSerialPort.IsOpen) + { + response = new CmdResponse(CmdName.CMD_ADDR_PC, $"{Resources.StrCmdRespErrorIndividual}"); + fm2014.OnRawRecordReceived?.Invoke(fm2014, new ProcessExecEventArgs(Resources.StrError, + specificInfoObj: response, + statusReturn: StatusReturn.Failed)); + SharedCmdTypeReminderLastCmd = CmdType.NOT_INITIALIZED; + fm2014.IsLoggedOn = false; + return false; + } + } + + try + { + + // Establish connection to one specific FM2014 + // The direct write has to be used as this is being called from the 'Write' routine!!!! + SharedSerialPort.Write(fm2014.AddressStr + END_OF_STR); + fm2014.ConnectResponse = SharedSerialPort.ReadTo(END_OF_STR); + + if (!string.IsNullOrEmpty(fm2014.ConnectResponse)) + { + var cmdName = CmdName.CMD_CONNECT; + var info = GetCmdInfo(cmdName) + ": " + fm2014.AddressStr; + response = new CmdResponse(cmdName, info); + fm2014.OnRawRecordReceived?.Invoke(fm2014, new ProcessExecEventArgs("", specificInfoObj: response)); + fm2014.ConnectResponse = fm2014.ConnectResponse.Replace(ADDR_RECORD_STR, ""); + var fields = fm2014.ConnectResponse.Split('.'); + fm2014.FwVersion = fields[1]; + SharedCmdTypeReminderLastCmd = GetCmdCmdType(cmdName); + return true; + } + } + catch (Exception e) + { + response = new CmdResponse(CmdName.CMD_CONNECT, $"{Resources.StrCmdRespErrorIndividual}" + + $": {fm2014.AddressStr}" + $" - {e.Message}"); + fm2014.OnRawRecordReceived?.Invoke(fm2014, new ProcessExecEventArgs(Resources.StrError, + specificInfoObj: response, statusReturn: StatusReturn.Failed)); + SharedCmdTypeReminderLastCmd = CmdType.NOT_INITIALIZED; + fm2014.IsLoggedOn = false; + return false; + } + + response = new CmdResponse(CmdName.CMD_CONNECT, $"{Resources.StrCmdRespErrorIndividual}"); + fm2014.OnRawRecordReceived?.Invoke(fm2014, new ProcessExecEventArgs(Resources.StrError, + specificInfoObj: response, statusReturn: StatusReturn.Failed)); + SharedCmdTypeReminderLastCmd = CmdType.NOT_INITIALIZED; + return false; + } + + #endregion ---------------------------------------- static methods -------------------------------------------- + + #region ------------------------------------------- object methods -------------------------------------------- /// /// Pulse counter measurement: /// - REF counter (default ON), @@ -454,7 +712,9 @@ namespace Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core /// /// request to count REF pulses /// request to count DUT pulses - /// + /// true if initialization successfully executed or + /// marks the already started cyclic requests + /// for all registered FM2014s /// /// - Initial. /// @@ -465,12 +725,17 @@ namespace Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core /// - 'u' read DUT backup pulses /// will be used. /// + /// + /// - Support for multiple FM2014s. + /// public Boolean PulseCounterMeasurement(Boolean refCtrOn = true, Boolean dutCtrOn = false) { - if (!refCtrOn && !dutCtrOn) - { + if (!refCtrOn && !dutCtrOn && IsLoggedOn) return false; - } + + // If the cyclic task has already been started everything is fine + if (SharedCyclicReceiveTaskLoopIsActive) + return true; var cmdName = CmdName.CMD_NA; String measurementInfoStr; @@ -495,7 +760,7 @@ namespace Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core } // Start the pulse counter measurement - if (!Write(cmdName)) + if (!Write(cmdName, this)) { return false; } @@ -531,52 +796,54 @@ namespace Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core do { - try + foreach (var fm2014 in RegisteredFm2014s.Where(fm2014 => fm2014.IsLoggedOn)) { - // Make a backup of the pulse counters to synchronize those and read out the backups - if (SharedCyclicReceiveTaskLoopIsActive && dutCtrOn && refCtrOn) + try { - // Synchronized backup of REF and DUT pulse counter - Write(CmdName.CMD_REF_DUT_BU_PLS_CTR); - } - - if (SharedCyclicReceiveTaskLoopIsActive) - { - if (refCtrOn && Write(cmdNameRef)) + // Make a backup of the pulse counters to synchronize those and read out the backups + if (SharedCyclicReceiveTaskLoopIsActive && dutCtrOn && refCtrOn) { - if (Read(cmdNameRef, out var responseStr) && - int.TryParse(responseStr, NumberStyles.HexNumber, new CultureInfo("en"), out var intValue)) - { - var response = new CmdResponse(cmdNameRef, infoRef, intValue); - OnRawRecordReceived?.Invoke(this, - new ProcessExecEventArgs("", actualProcessMessage: measurementInfoStr, - specificInfoObj: response)); - } + // Synchronized backup of REF and DUT pulse counter + Write(CmdName.CMD_REF_DUT_BU_PLS_CTR, fm2014); } - if (dutCtrOn && Write(cmdNameDut)) + if (SharedCyclicReceiveTaskLoopIsActive) { - if (Read(cmdNameDut, out var responseStr) && - int.TryParse(responseStr, NumberStyles.HexNumber, new CultureInfo("en"), out var intValue)) + if (refCtrOn && Write(cmdNameRef, fm2014)) { - var response = new CmdResponse(cmdNameDut, infoDut, intValue); - OnRawRecordReceived?.Invoke(this, - new ProcessExecEventArgs("", actualProcessMessage: measurementInfoStr, - specificInfoObj: response)); + if (Read(cmdNameRef, fm2014, out var responseStr) && + int.TryParse(responseStr, NumberStyles.HexNumber, new CultureInfo("en"), + out var intValue)) + { + var response = new CmdResponse(cmdNameRef, infoRef, intValue); + fm2014?.OnRawRecordReceived?.Invoke(fm2014, + new ProcessExecEventArgs("", actualProcessMessage: measurementInfoStr, + specificInfoObj: response)); + } + } + + if (dutCtrOn && Write(cmdNameDut, fm2014)) + { + if (Read(cmdNameDut, fm2014, out var responseStr) && + int.TryParse(responseStr, NumberStyles.HexNumber, new CultureInfo("en"), + out var intValue)) + { + var response = new CmdResponse(cmdNameDut, infoDut, intValue); + OnRawRecordReceived?.Invoke(fm2014, + new ProcessExecEventArgs("", actualProcessMessage: measurementInfoStr, + specificInfoObj: response)); + } } } } - } - catch (Exception e) - { - var response = new CmdResponse(cmdName, e.Message); - OnRawRecordReceived?.Invoke(this, new ProcessExecEventArgs(Resources.StrError, specificInfoObj: response, - statusReturn: StatusReturn.Failed)); - return false; + catch (Exception e) + { + var response = new CmdResponse(cmdName, e.Message); + fm2014?.OnRawRecordReceived?.Invoke(fm2014, new ProcessExecEventArgs(Resources.StrError, + specificInfoObj: response, statusReturn: StatusReturn.Failed)); + } } } while (SharedCyclicReceiveTaskLoopIsActive); - - return true; }, SharedCancellationToken); return true; @@ -605,14 +872,22 @@ namespace Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core /// /// - Initial. /// + /// + /// - Support for multiple FM2014s. + /// public Boolean RegulationMeasurement() { if (Ref_pulse_per_cm == 0 || Dut_pulse_per_cm == 0 || - RefToDutScale_norm < RefToDutScaleMin || RefToDutScale_norm > RefToDutScaleMax) + RefToDutScale_norm < RefToDutScaleMin || RefToDutScale_norm > RefToDutScaleMax || + !IsLoggedOn) { return false; } + // If the cyclic task has already been started everything is fine + if (SharedCyclicReceiveTaskLoopIsActive) + return true; + var cmdName = CmdName.CMD_NA; var measurementInfo = Resources.StrMeasMsgRegulation; @@ -620,17 +895,17 @@ namespace Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core try { cmdName = CmdName.CMD_SET_DBPL_UNLOCK; - if (!Write(cmdName)) + if (!Write(cmdName, this)) { return false; } cmdName = CmdName.CMD_MEAS_SET_ATTN; - if (!Write(cmdName, Attenuation)) + if (!Write(cmdName, this, Attenuation)) { return false; } cmdName = CmdName.CMD_REF_SET_SCALE; - if (!Write(cmdName, _refToDutScaleStr)) + if (!Write(cmdName, this, _refToDutScaleStr)) { return false; } @@ -651,152 +926,153 @@ namespace Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core SharedCyclicReceiveTaskLoopIsActive = true; do { - try + foreach (var fm2014 in RegisteredFm2014s.Where(fm2014 => fm2014.IsLoggedOn)) { - // Read out the damped/undamped tolerance and convert the raw value (0, +/-1..255 digits) to the - // tolerance in percent depending on the setup to 3 % (3.3) of 5 % (5.5) - String responseStr; - cmdName = UseDampedTolerance ? CmdName.CMD_GET_DTLC : CmdName.CMD_GET_UDTLC; - var info = GetCmdInfo(cmdName); - if (SharedCyclicReceiveTaskLoopIsActive && Write(cmdName)) + try { - if (Read(cmdName, out responseStr)) + // Read out the damped/undamped tolerance and convert the raw value (0, +/-1..255 digits) to the + // tolerance in percent depending on the setup to 3 % (3.3) of 5 % (5.5) + String responseStr; + cmdName = UseDampedTolerance ? CmdName.CMD_GET_DTLC : CmdName.CMD_GET_UDTLC; + var info = GetCmdInfo(cmdName); + if (SharedCyclicReceiveTaskLoopIsActive && Write(cmdName, fm2014)) { - // The response contains a sign! - var signMultiplier = responseStr.Contains("+") ? 1.0f : -1.0f; - var cleanedStr = responseStr.Replace("+", "").Replace("-", ""); - if (int.TryParse(cleanedStr, NumberStyles.HexNumber, new CultureInfo("en"), - out var measTolerance)) + if (Read(cmdName, fm2014, out responseStr)) { + // The response contains a sign! + var signMultiplier = responseStr.Contains("+") ? 1.0f : -1.0f; + var cleanedStr = responseStr.Replace("+", "").Replace("-", ""); + if (int.TryParse(cleanedStr, NumberStyles.HexNumber, new CultureInfo("en"), + out var measTolerance)) + { + var response = new CmdResponse(cmdName, info, + doubleValue: measTolerance * _toleranceRawToPercentScale * signMultiplier, + siUnit: SiUnits.GetInfo(SiUnits.SiUnitName.PERCENTAGE_NON_SI)); + fm2014?.OnRawRecordReceived?.Invoke(fm2014, + new ProcessExecEventArgs("", actualProcessMessage: measurementInfo, + specificInfoObj: response)); + } + } + } + + // Read the REF period and calculate the frequency [Hz] and flow rate [m³/h] based on this period + cmdName = CmdName.CMD_GET_REF_PERIOD; + info = GetCmdInfo(cmdName); + if (RequestDebugInformation && SharedCyclicReceiveTaskLoopIsActive && Write(cmdName, fm2014)) + { + if (Read(cmdName, fm2014, out responseStr) && + int.TryParse(responseStr, NumberStyles.HexNumber, new CultureInfo("en"), out var period)) + { + // Publish the period [ms] var response = new CmdResponse(cmdName, info, - doubleValue: measTolerance * _toleranceRawToPercentScale * signMultiplier, - siUnit: SiUnits.GetInfo(SiUnits.SiUnitName.PERCENTAGE_NON_SI)); - OnRawRecordReceived?.Invoke(this, + doubleValue: TMR_RESOLUTION_s * 1000.0 * period, + siUnit: "m" + SiUnits.GetInfo(SiUnits.SiUnitName.TIME)); + fm2014?.OnRawRecordReceived?.Invoke(fm2014, + new ProcessExecEventArgs("", actualProcessMessage: measurementInfo, + specificInfoObj: response)); + + // Publish the frequency in [Hz] + cmdName = CmdName.CMD_CAL_FREQU_REF_PERIOD; + info = GetCmdInfo(cmdName); + response = new CmdResponse(cmdName, info, + doubleValue: 1.0 / (TMR_RESOLUTION_s * period), + siUnit: SiUnits.GetInfo(SiUnits.SiUnitName.FREQUENCY)); + fm2014?.OnRawRecordReceived?.Invoke(fm2014, + new ProcessExecEventArgs("", actualProcessMessage: measurementInfo, + specificInfoObj: response)); + + // Convert to m³/h (3600 s/h) based on REF pulse rate + var flowRate_cm_per_h = 3600.0 / (period * TMR_RESOLUTION_s) / Ref_pulse_per_cm; + // Publish the calculated flow rate [m³/h] + cmdName = CmdName.CMD_CAL_FLOW_REF_PERIOD; + info = GetCmdInfo(cmdName); + response = new CmdResponse(cmdName, info, doubleValue: flowRate_cm_per_h, + siUnit: SiUnits.GetInfo(SiUnits.SiUnitName.FLOW_RATE_NON_SI)); + fm2014?.OnRawRecordReceived?.Invoke(fm2014, + new ProcessExecEventArgs("", actualProcessMessage: measurementInfo, + specificInfoObj: response)); + } + } + + + // Read the DUT period and calculate the frequency [Hz] and flow rate [m³/h] based on this period + cmdName = CmdName.CMD_GET_DUT_PERIOD; + info = GetCmdInfo(cmdName); + if (RequestDebugInformation && SharedCyclicReceiveTaskLoopIsActive && Write(cmdName, fm2014)) + { + if (Read(cmdName, fm2014, out responseStr) && + int.TryParse(responseStr, NumberStyles.HexNumber, new CultureInfo("en"), out var period)) + { + // Publish the period [ms] + var response = new CmdResponse(cmdName, info, + doubleValue: TMR_RESOLUTION_s * 1000.0 * period, + siUnit: "m" + SiUnits.GetInfo(SiUnits.SiUnitName.TIME)); + fm2014?.OnRawRecordReceived?.Invoke(fm2014, + new ProcessExecEventArgs("", actualProcessMessage: measurementInfo, + specificInfoObj: response)); + + // Publish the frequency in [Hz] + cmdName = CmdName.CMD_CAL_FREQU_DUT_PERIOD; + info = GetCmdInfo(cmdName); + response = new CmdResponse(cmdName, info, + doubleValue: 1.0 / (TMR_RESOLUTION_s * period), + siUnit: SiUnits.GetInfo(SiUnits.SiUnitName.FREQUENCY)); + fm2014?.OnRawRecordReceived?.Invoke(fm2014, + new ProcessExecEventArgs("", actualProcessMessage: measurementInfo, + specificInfoObj: response)); + + // Convert to m³/h (3600 s/h) based on DUT pulse rate + var flowRate_cm_per_h = 3600.0 / (period * TMR_RESOLUTION_s) / Dut_pulse_per_cm; + // Publish the calculated flow rate [m³/h] + cmdName = CmdName.CMD_CAL_FLOW_DUT_PERIOD; + info = GetCmdInfo(cmdName); + response = new CmdResponse(cmdName, info, doubleValue: flowRate_cm_per_h, + siUnit: SiUnits.GetInfo(SiUnits.SiUnitName.FLOW_RATE_NON_SI)); + fm2014?.OnRawRecordReceived?.Invoke(fm2014, + new ProcessExecEventArgs("", actualProcessMessage: measurementInfo, + specificInfoObj: response)); + } + } + + + // Read the REF frequency [Hz] directly and calculate the flow rate [m³/h] based on this frequency + cmdName = CmdName.CMD_GET_REF_FREQU; + info = GetCmdInfo(cmdName); + if (SharedCyclicReceiveTaskLoopIsActive && Write(cmdName, fm2014)) + { + if (Read(cmdName, fm2014, out responseStr) && + int.TryParse(responseStr, NumberStyles.HexNumber, new CultureInfo("en"), + out var refFrequency)) + { + // Publish the frequency in [Hz] + var response = new CmdResponse(cmdName, info, refFrequency, + siUnit: SiUnits.GetInfo(SiUnits.SiUnitName.FREQUENCY)); + fm2014?.OnRawRecordReceived?.Invoke(fm2014, new ProcessExecEventArgs("", + actualProcessMessage: measurementInfo, + specificInfoObj: response)); + + // Convert to m³/h (3600 s/h) and save the value + RefFlowRate_cm_per_h = 3600.0 * refFrequency / Ref_pulse_per_cm; + // Publish the calculated flow rate [m³/h] + cmdName = CmdName.CMD_CAL_FLOW_REF_FREQU; + info = GetCmdInfo(cmdName); + response = new CmdResponse(cmdName, info, doubleValue: RefFlowRate_cm_per_h, + siUnit: SiUnits.GetInfo(SiUnits.SiUnitName.FLOW_RATE_NON_SI)); + fm2014?.OnRawRecordReceived?.Invoke(fm2014, new ProcessExecEventArgs("", actualProcessMessage: measurementInfo, specificInfoObj: response)); } } } - - // Read the REF period and calculate the frequency [Hz] and flow rate [m³/h] based on this period - cmdName = CmdName.CMD_GET_REF_PERIOD; - info = GetCmdInfo(cmdName); - if (RequestDebugInformation && SharedCyclicReceiveTaskLoopIsActive && Write(cmdName)) + catch (Exception e) { - if (Read(cmdName, out responseStr) && - int.TryParse(responseStr, NumberStyles.HexNumber, new CultureInfo("en"), out var period)) - { - // Publish the period [ms] - var response = new CmdResponse(cmdName, info, - doubleValue: TMR_RESOLUTION_s * 1000.0 * period, - siUnit: "m" + SiUnits.GetInfo(SiUnits.SiUnitName.TIME)); - OnRawRecordReceived?.Invoke(this, - new ProcessExecEventArgs("", actualProcessMessage: measurementInfo, - specificInfoObj: response)); - - // Publish the frequency in [Hz] - cmdName = CmdName.CMD_CAL_FREQU_REF_PERIOD; - info = GetCmdInfo(cmdName); - response = new CmdResponse(cmdName, info, - doubleValue: 1.0 / (TMR_RESOLUTION_s * period), - siUnit: SiUnits.GetInfo(SiUnits.SiUnitName.FREQUENCY)); - OnRawRecordReceived?.Invoke(this, - new ProcessExecEventArgs("", actualProcessMessage: measurementInfo, - specificInfoObj: response)); - - // Convert to m³/h (3600 s/h) based on REF pulse rate - var flowRate_cm_per_h = 3600.0 / (period * TMR_RESOLUTION_s) / Ref_pulse_per_cm; - // Publish the calculated flow rate [m³/h] - cmdName = CmdName.CMD_CAL_FLOW_REF_PERIOD; - info = GetCmdInfo(cmdName); - response = new CmdResponse(cmdName, info, doubleValue: flowRate_cm_per_h, - siUnit: SiUnits.GetInfo(SiUnits.SiUnitName.FLOW_RATE_NON_SI)); - OnRawRecordReceived?.Invoke(this, - new ProcessExecEventArgs("", actualProcessMessage: measurementInfo, - specificInfoObj: response)); - } + var response = new CmdResponse(cmdName, e.Message); + fm2014?.OnRawRecordReceived?.Invoke(fm2014, new ProcessExecEventArgs(Resources.StrError, + specificInfoObj: response, statusReturn: StatusReturn.Failed)); } - - - // Read the DUT period and calculate the frequency [Hz] and flow rate [m³/h] based on this period - cmdName = CmdName.CMD_GET_DUT_PERIOD; - info = GetCmdInfo(cmdName); - if (RequestDebugInformation && SharedCyclicReceiveTaskLoopIsActive && Write(cmdName)) - { - if (Read(cmdName, out responseStr) && - int.TryParse(responseStr, NumberStyles.HexNumber, new CultureInfo("en"), out var period)) - { - // Publish the period [ms] - var response = new CmdResponse(cmdName, info, - doubleValue: TMR_RESOLUTION_s * 1000.0 * period, - siUnit: "m" + SiUnits.GetInfo(SiUnits.SiUnitName.TIME)); - OnRawRecordReceived?.Invoke(this, - new ProcessExecEventArgs("", actualProcessMessage: measurementInfo, - specificInfoObj: response)); - - // Publish the frequency in [Hz] - cmdName = CmdName.CMD_CAL_FREQU_DUT_PERIOD; - info = GetCmdInfo(cmdName); - response = new CmdResponse(cmdName, info, - doubleValue: 1.0 / (TMR_RESOLUTION_s * period), - siUnit: SiUnits.GetInfo(SiUnits.SiUnitName.FREQUENCY)); - OnRawRecordReceived?.Invoke(this, - new ProcessExecEventArgs("", actualProcessMessage: measurementInfo, - specificInfoObj: response)); - - // Convert to m³/h (3600 s/h) based on DUT pulse rate - var flowRate_cm_per_h = 3600.0 / (period * TMR_RESOLUTION_s) / Dut_pulse_per_cm; - // Publish the calculated flow rate [m³/h] - cmdName = CmdName.CMD_CAL_FLOW_DUT_PERIOD; - info = GetCmdInfo(cmdName); - response = new CmdResponse(cmdName, info, doubleValue: flowRate_cm_per_h, - siUnit: SiUnits.GetInfo(SiUnits.SiUnitName.FLOW_RATE_NON_SI)); - OnRawRecordReceived?.Invoke(this, - new ProcessExecEventArgs("", actualProcessMessage: measurementInfo, - specificInfoObj: response)); - } - } - - - // Read the REF frequency [Hz] directly and calculate the flow rate [m³/h] based on this frequency - cmdName = CmdName.CMD_GET_REF_FREQU; - info = GetCmdInfo(cmdName); - if (SharedCyclicReceiveTaskLoopIsActive && Write(cmdName)) - { - if (Read(cmdName, out responseStr) && - int.TryParse(responseStr, NumberStyles.HexNumber, new CultureInfo("en"), out var refFrequency)) - { - // Publish the frequency in [Hz] - var response = new CmdResponse(cmdName, info, refFrequency, - siUnit: SiUnits.GetInfo(SiUnits.SiUnitName.FREQUENCY)); - OnRawRecordReceived?.Invoke(this, new ProcessExecEventArgs("", actualProcessMessage: measurementInfo, - specificInfoObj: response)); - - // Convert to m³/h (3600 s/h) and save the value - RefFlowRate_cm_per_h = 3600.0 * refFrequency / Ref_pulse_per_cm; - // Publish the calculated flow rate [m³/h] - cmdName = CmdName.CMD_CAL_FLOW_REF_FREQU; - info = GetCmdInfo(cmdName); - response = new CmdResponse(cmdName, info, doubleValue: RefFlowRate_cm_per_h, - siUnit: SiUnits.GetInfo(SiUnits.SiUnitName.FLOW_RATE_NON_SI)); - OnRawRecordReceived?.Invoke(this, - new ProcessExecEventArgs("", actualProcessMessage: measurementInfo, - specificInfoObj: response)); - } - } - - Thread.Sleep(500); - } - catch (Exception e) - { - var response = new CmdResponse(cmdName, e.Message); - OnRawRecordReceived?.Invoke(this, new ProcessExecEventArgs(Resources.StrError, specificInfoObj: response, - statusReturn: StatusReturn.Failed)); - return false; } } while (SharedCyclicReceiveTaskLoopIsActive); - return true; }, SharedCancellationToken); return true; @@ -812,8 +1088,15 @@ namespace Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core /// /// - REF pulse rate can be 0 as 'invalid marker'. /// + /// + /// - Support for multiple FM2014s. + /// public Boolean SaveStandAloneMeasurement() { + // If the cyclic task has already been started everything is fine + if (SharedCyclicReceiveTaskLoopIsActive || !IsLoggedOn) + return false; + var cmdName = CmdName.CMD_REF_LPP_SCALE; try { @@ -821,32 +1104,32 @@ namespace Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core // which should be recovered by 'Ref_pulses_per_cm = Dut_pulses_per_cm * RefToDutScale_norm' var limitedRefPulsesPerCm = Ref_pulse_per_cm <= PulsesPerVolumeRegulationSetupMax ? Ref_pulse_per_cm : RefPulsesPerVolumeRegulationSetupInvalidMarker; - var retVal = Write(cmdName, (UInt16)limitedRefPulsesPerCm); + var retVal = Write(cmdName, this, (UInt16)limitedRefPulsesPerCm); if (retVal) { cmdName = CmdName.CMD_DUT_LPP_SCALE; - retVal = Write(cmdName, (UInt16)Dut_pulse_per_cm); + retVal = Write(cmdName, this, (UInt16)Dut_pulse_per_cm); } if (retVal) { cmdName = CmdName.CMD_REF_SET_SCALE; // During setup of the 'Ref_pulse_per_cm' the '_refToDutScaleStr' will be generated - retVal = Write(cmdName, _refToDutScaleStr); + retVal = Write(cmdName, this, _refToDutScaleStr); } if (retVal) { cmdName = CmdName.CMD_MEAS_SET_ATTN; - retVal = Write(cmdName, Attenuation); + retVal = Write(cmdName, this, Attenuation); } if (retVal) { // Save all settings from RAM to nonvolatile EEPROM cmdName = CmdName.CMD_SAVE_RAM_TO_EEP_MEAS_SETUP; - return Write(cmdName); + return Write(cmdName, this); } } catch (Exception e) @@ -873,8 +1156,15 @@ namespace Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core /// to avoid an overflow as the REF pulses per volume can store max 9999 pulses. As the Dut_pulse_per_cm /// is for the smallest meter 1000 Impulses/m³ /// + /// + /// - Support for multiple FM2014s. + /// private Boolean ReadAloneMeasurementControl(CmdName cmdName) { + // If the cyclic task has already been started everything is fine + if (SharedCyclicReceiveTaskLoopIsActive || !IsLoggedOn) + return false; + // Allow explicit standalone measurement commands if (cmdName != CmdName.CMD_READ_EEP_MEAS_SETUP && cmdName != CmdName.CMD_READ_RAM_MEAS_SETUP) @@ -885,7 +1175,7 @@ namespace Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core { var measurementInfoStr = Resources.StrMeasMsgStanaloneRegulation; var responseStrs = new List(); - if (Write(cmdName)) + if (Write(cmdName, this)) { if (cmdName == CmdName.CMD_READ_EEP_MEAS_SETUP) { @@ -893,7 +1183,7 @@ namespace Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core var loopCtr = 4; do { - if (Read(cmdName, out var responseStr)) + if (Read(cmdName, this, out var responseStr)) responseStrs.Add(responseStr); } while (loopCtr-- > 0); } @@ -903,7 +1193,7 @@ namespace Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core var loopCtr = 5; do { - if (Read(cmdName, out var responseStr)) + if (Read(cmdName, this, out var responseStr)) responseStrs.Add(responseStr); } while (loopCtr-- > 0); } @@ -999,20 +1289,28 @@ namespace Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core /// /// - Initial. /// + /// + /// - Support for multiple FM2014s. + /// public Boolean Login() { IsLoggedOn = false; + + // If the cyclic task has already been started everything is fine + if (SharedCyclicReceiveTaskLoopIsActive) + return false; + var cmdCode = GetCmdStr(CmdName.CMD_SERIAL); const CmdName cmdName = CmdName.CMD_SERIAL; try { // Read out the serial number of this FM2014 - if (!Write(cmdName)) + if (!Write(cmdName, this)) { return false; } - if (Read(cmdName, out var responseStr)) + if (Read(cmdName, this, out var responseStr)) SerialNumber = responseStr; if (!string.IsNullOrEmpty(SerialNumber)) @@ -1036,286 +1334,6 @@ namespace Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core return IsLoggedOn; } - /// - /// Initiate the broadcast communication: - /// - This 'InitiateBroadcastComm' needs to access the 'SerialPort.Write' directly as it is called from - /// the 'FM2014.Write' to check, based on the command table, if a broadcast or an individual communication - /// needs to be in initialized! - /// - /// - /// - Initial. - /// - private Boolean InitiateBroadcastComm() - { - if (SharedSerialPort == null) - { - var response = new CmdResponse(CmdName.CMD_ADDR_PC, - $"{Resources.StrCmdRespErrorBroadcast}"); - OnRawRecordReceived?.Invoke(this, new ProcessExecEventArgs(Resources.StrError, - specificInfoObj: response, - statusReturn: StatusReturn.Failed)); - SharedCmdTypeReminderLastCmd = CmdType.NOT_INITIALIZED; - return false; - } - - if (!SharedSerialPort.IsOpen) - { - SharedSerialPort.Open(); - if (!SharedSerialPort.IsOpen) - { - var response = new CmdResponse(CmdName.CMD_ADDR_PC, - $"{Resources.StrCmdRespErrorBroadcast}"); - OnRawRecordReceived?.Invoke(this, new ProcessExecEventArgs(Resources.StrError, - specificInfoObj: response, - statusReturn: StatusReturn.Failed)); - SharedCmdTypeReminderLastCmd = CmdType.NOT_INITIALIZED; - return false; - } - } - - // If broadcast communication is already active - if (SharedCmdTypeReminderLastCmd == CmdType.BROADCAST) - { - return true; - } - - try - { - // Establish connection to all FM2014s with broadcast address - // The direct write has to be used as this is being called from the 'Write' routine!!!! - SharedSerialPort.Write(BROADCAST_ADDRESS + END_OF_STR); - var cmdName = CmdName.CMD_CONNECT; - var info = GetCmdInfo(cmdName) + ": " + BROADCAST_ADDRESS; - var response = new CmdResponse(cmdName, info); - OnRawRecordReceived?.Invoke(this, new ProcessExecEventArgs("", specificInfoObj: response)); - SharedCmdTypeReminderLastCmd = CmdType.BROADCAST; - return true; - } - catch (Exception e) - { - var response = new CmdResponse(CmdName.CMD_ADDR_PC, - $"{Resources.StrCmdRespErrorBroadcast} - {e.Message}"); - OnRawRecordReceived?.Invoke(this, new ProcessExecEventArgs(Resources.StrError, specificInfoObj: response, - statusReturn: StatusReturn.Failed)); - SharedCmdTypeReminderLastCmd = CmdType.NOT_INITIALIZED; - return false; - } - } - - /// - /// Initiate communication to individual node - /// - This 'InitiateIndividualComm' needs to access the 'SerialPort.Write' and 'SerialPort.ReadTo' as this is - /// called from the 'FM2014.Write' and 'FM2014.Read' to check, based on the command table, if a broadcast or an - /// individual communication needs to be in initialized! - /// - /// - /// - /// - Initial. - /// - private Boolean InitiateIndividualComm() - { - CmdResponse response; - if (SharedSerialPort == null) - { - response = new CmdResponse(CmdName.CMD_ADDR_PC, $"{Resources.StrCmdRespErrorIndividual}"); - OnRawRecordReceived?.Invoke(this, new ProcessExecEventArgs(Resources.StrError, - specificInfoObj: response, - statusReturn: StatusReturn.Failed)); - SharedCmdTypeReminderLastCmd = CmdType.NOT_INITIALIZED; - return false; - } - - if (!SharedSerialPort.IsOpen) - { - SharedSerialPort.Open(); - if (!SharedSerialPort.IsOpen) - { - response = new CmdResponse(CmdName.CMD_ADDR_PC, $"{Resources.StrCmdRespErrorIndividual}"); - OnRawRecordReceived?.Invoke(this, new ProcessExecEventArgs(Resources.StrError, - specificInfoObj: response, - statusReturn: StatusReturn.Failed)); - SharedCmdTypeReminderLastCmd = CmdType.NOT_INITIALIZED; - return false; - } - } - - try - { - - // Establish connection to one specific FM2014 - // The direct write has to be used as this is being called from the 'Write' routine!!!! - SharedSerialPort.Write(AddressStr + END_OF_STR); - ConnectResponse = SharedSerialPort.ReadTo(END_OF_STR); - - if (!string.IsNullOrEmpty(ConnectResponse)) - { - var cmdName = CmdName.CMD_CONNECT; - var info = GetCmdInfo(cmdName) + ": " + AddressStr; - response = new CmdResponse(cmdName, info); - OnRawRecordReceived?.Invoke(this, new ProcessExecEventArgs("", specificInfoObj: response)); - ConnectResponse = ConnectResponse.Replace(ADDR_RECORD_STR, ""); - var fields = ConnectResponse.Split('.'); - FwVersion = fields[1]; - SharedCmdTypeReminderLastCmd = GetCmdCmdType(cmdName); - return true; - } - } - catch (Exception e) - { - response = new CmdResponse(CmdName.CMD_CONNECT, $"{Resources.StrCmdRespErrorIndividual} - {e.Message}"); - OnRawRecordReceived?.Invoke(this, new ProcessExecEventArgs(Resources.StrError, - specificInfoObj: response, statusReturn: StatusReturn.Failed)); - SharedCmdTypeReminderLastCmd = CmdType.NOT_INITIALIZED; - return false; - } - - response = new CmdResponse(CmdName.CMD_CONNECT, $"{Resources.StrCmdRespErrorIndividual}"); - OnRawRecordReceived?.Invoke(this, new ProcessExecEventArgs(Resources.StrError, - specificInfoObj: response, statusReturn: StatusReturn.Failed)); - SharedCmdTypeReminderLastCmd = CmdType.NOT_INITIALIZED; - return false; - } - - /// - /// Read FM2014: - /// - Accesses directly to the 'SerialPort.ReadTo', - /// - An exception has to be caught by the caller. - /// - /// - /// - /// - /// - /// - Initial. - /// - private Boolean Read(CmdName cmdName, out String responseStr) - { - responseStr = ""; - var info = $"{Resources.StrCommResponse}: {GetCmdInfo(cmdName)}"; - var statusReturn = StatusReturn.Failed; - - // Check for individual command without response - if (CmdType.INDIVIDUAL_UNRESPONSIVE == SharedCmdTypeReminderLastCmd && - CmdType.INDIVIDUAL_UNRESPONSIVE == GetCmdCmdType(cmdName)) - { - statusReturn = StatusReturn.Skipped; - } - else if (CmdType.INDIVIDUAL == SharedCmdTypeReminderLastCmd && - CmdType.INDIVIDUAL == GetCmdCmdType(cmdName)) - { - // Read or timeout with a TimeoutException - var answerStr = SharedSerialPort.ReadTo(END_OF_STR); - if (string.IsNullOrEmpty(answerStr)) - { - statusReturn = StatusReturn.Failed; - } - else - { - statusReturn = StatusReturn.Okay; - responseStr = answerStr; - } - } - - var isErrorStr = StatusReturn.Failed == statusReturn ? Resources.StrError + ": " : ""; - var response = new CmdResponse(cmdName, $"{isErrorStr}{info} {responseStr}"); - OnRawRecordReceived?.Invoke(this, new ProcessExecEventArgs("", specificInfoObj: response, - statusReturn: statusReturn)); - - return StatusReturn.Failed != statusReturn; - } - - /// - /// Write command to FM2014: - /// - Accesses directly to the 'SerialPort.Write', - /// - Select broadcast or individual addressing based on the command table, - /// - Transmits optional data: - /// - If the dataObj is a string, this is going to be sent as is (e.g. "1000K2"), - /// - If the dataObj is an UInt16 or Byte this will be assembled with the command code (e.g. "3T"), - /// - Every 'Write' is a string delimited here with 'CR', - /// - An exception has to be caught by the caller. - /// - /// - /// - /// - /// - /// - Initial. - /// - private Boolean Write(CmdName cmdName, Object dataObj = null) - { - if (SharedSerialPort == null) - { - SharedCmdTypeReminderLastCmd = CmdType.NOT_INITIALIZED; - IsLoggedOn = false; - SharedCyclicReceiveTaskLoopIsActive = false; - return false; - } - - // Initiate communication using the individual or broadcast address - var retVal = true; - if (CmdType.BROADCAST == GetCmdCmdType(cmdName)) - { - if (SharedCmdTypeReminderLastCmd != CmdType.BROADCAST) - { - retVal = InitiateBroadcastComm(); - } - } - else - { - if (SharedCmdTypeReminderLastCmd != CmdType.INDIVIDUAL || SharedCmdTypeReminderLastCmd != CmdType.INDIVIDUAL) - { - retVal = InitiateIndividualComm(); - } - } - - if (!retVal) - { - return false; - } - - var cmdCodeStr = GetCmdStr(cmdName); - var info = GetCmdInfo(cmdName) + $": {cmdCodeStr}"; - CmdResponse response; - // Without data the command alone needs to be sent - if (dataObj == null) - { - SharedSerialPort.Write($"{cmdCodeStr}{END_OF_STR}"); - var additionalInfo = GetCmdCmdType(cmdName) == CmdType.BROADCAST - ? Resources.StrCommDirectionSetup - : Resources.StrCommDirectionRequest; - response = new CmdResponse(cmdName, $"{additionalInfo}: {info}"); - } - else - { - // A string has to be entire assembled, only the string delimiter will to be added here - if (dataObj is String) - { - SharedSerialPort.Write($"{dataObj}{END_OF_STR}"); - response = new CmdResponse(cmdName, $"{Resources.StrCommDirectionSetup}: {info}: {dataObj}"); - } - - // The data contains the preceding setup, the command code and string delimiter will be added here - else if (dataObj is UInt16 || dataObj is Byte) - { - SharedSerialPort.Write($"{dataObj}{cmdCodeStr}{END_OF_STR}"); - response = new CmdResponse(cmdName, - $"{Resources.StrCommDirectionSetup}: {info}: {dataObj}{cmdCodeStr}"); - - } - else - { - response = new CmdResponse(cmdName, - $"{Resources.StrError}: {Resources.StrCommDirectionSetup}: {info} - " + - $"{Resources.StrCmdRespErrorPramTypeNotSupported} {dataObj}"); - retVal = false; - } - } - - var statusReturn = retVal ? StatusReturn.Okay : StatusReturn.Failed; - OnRawRecordReceived?.Invoke(this, new ProcessExecEventArgs("", specificInfoObj: response, - statusReturn: statusReturn)); - - return retVal; - } - /// /// Reset the measurement but only if receive task is active. /// @@ -1326,6 +1344,9 @@ namespace Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core /// /// - Delayed reset to allow other tasks to finish. /// + /// + /// - Support for multiple FM2014s. + /// public Boolean ResetMeasurement() { if (!SharedCyclicReceiveTaskLoopIsActive) @@ -1341,7 +1362,7 @@ namespace Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core { // Reset measurement var infoStr = GetCmdInfo(CmdName.CMD_RST_MEAS); - if (!Write(CmdName.CMD_RST_MEAS)) + if (!Write(CmdName.CMD_RST_MEAS, this)) return; // Wait until FM2014 calibration finished @@ -1367,10 +1388,60 @@ namespace Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core return true; } + /// + /// Common routine for reboot being able to override this routine which will be called in + /// to simulate a reboot. + /// + /// + /// + /// - Support for multiple FM2014s. + /// + public StatusReturn Reboot() + { + return StatusReturn.Failed; + } + + /// + /// Connect to FM2014 + /// + /// + /// + /// + /// - Support for multiple FM2014s. + /// + public Boolean Connect(String comPort) + { + if (SharedSerialPort == null) + { + SharedSerialPort = new SerialPort(); + SharedSerialPort.BaudRate = 1200; + SharedSerialPort.DataBits = 7; + SharedSerialPort.Parity = Parity.Odd; + SharedSerialPort.PortName = comPort; + SharedSerialPort.ReadTimeout = 1000; + SharedSerialPort.WriteTimeout = 1000; + } + + if (RegisteredFm2014s == null) + RegisteredFm2014s = new List(); + + RegisteredFm2014s.Add(this); + + // Reset pulse ratios to force a preset with 'standalone' settings from + // FM2014 wit '%D@' - Read 'Default Measurement Setup' + _ref_pulse_per_cm = 0; + _dut_pulse_per_cm = 0; + SharedCmdTypeReminderLastCmd = CmdType.NOT_INITIALIZED; + return true; + } + /// /// Logout from FM2014 /// /// + /// + /// - Support for multiple FM2014s. + /// public Boolean Logout() { // Close only if all FM2014 instances are removed @@ -1385,6 +1456,9 @@ namespace Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core /// /// Dispose FM2014 /// + /// + /// - Support for multiple FM2014s. + /// public void Dispose() { Logout(); @@ -1392,6 +1466,6 @@ namespace Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core RegisteredFm2014s = null; SharedSerialPort?.Dispose(); } - #endregion methods + #endregion ---------------------------------------- object methods -------------------------------------------- } } \ No newline at end of file diff --git a/MiniPrf/Ui/FrmMainMiniPrf.cs b/MiniPrf/Ui/FrmMainMiniPrf.cs index 020c9de2..ce34c734 100644 --- a/MiniPrf/Ui/FrmMainMiniPrf.cs +++ b/MiniPrf/Ui/FrmMainMiniPrf.cs @@ -66,11 +66,12 @@ namespace Sensus.MiniPrf.Ui /// /// The single FM2014 for this program /// - private FM2014 Fm2014 - { - get; - set; - } + private FM2014 Fm2014 { get; set; } + + /// + /// The single FM2014 for this program + /// + private FM2014 Fm2014_TEST { get; set; } /// /// The start time is going to be used for time measurements of processes. It has to be set to @@ -444,6 +445,12 @@ namespace Sensus.MiniPrf.Ui //assign new meter Fm2014 = new FM2014(); + + //TODO THW TEST FM2014 multiple objects + Fm2014_TEST = new FM2014(); + Fm2014_TEST.Address = 2; + Fm2014_TEST.OnRawRecordReceived += DataReceived_Handler; + Fm2014.OnRawRecordReceived += DataReceived_Handler; ResetCancellationToken(); FM2014.SharedCancellationToken = _cancellationToken; @@ -470,10 +477,16 @@ namespace Sensus.MiniPrf.Ui lblActualProcess.Text = Resources.StrMsgConnecting; Fm2014.Connect(cbxFM2014ComPort.SelectedItem.ToString()); + //TODO THW TEST FM2014 multiple objects + Fm2014_TEST.Connect(cbxFM2014ComPort.SelectedItem.ToString()); + Task.Factory.StartNew(() => { Fm2014.Login(); + //TODO THW TEST FM2014 multiple objects + Fm2014_TEST.Login(); + if (!Fm2014.IsLoggedOn) { LogErrorText(Resources.StrErrorMsgFM2014AccessDenied);