laatzen/MiniPrf/Ui/FrmMainMiniPrf.cs
2026-02-18 17:24:41 +01:00

1398 lines
56 KiB
C#

/*********************************************************************************************************************/
/*!@file FrmMainMiniPrf.cs
* @brief Main form for 'MiniPrf' - Single FM2014 Manual Flow Test Station:
* - Used to regulate one mechanical flow meter - 'DUT - Device under test',
* - Can be used to calibrate the 'REF - Reference meter'
*
* @author Thomas Wiedebusch
* @date 2025-Nov-13
*
* @details <i>Implements the serial communication to the FM2014 .</i>
*
* @copyright © SENSUS GmbH 2025..2026. All rights reserved.
**********************************************************************************************************************/
using Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Config;
using Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core;
using Sensus.MiniPrf.Ui.Properties;
using System;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.IO.Ports;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core.Consts;
using Xylem.Common.CommonCore.Consts;
using Xylem.Common.Utils.ProcessExec.EventArguments;
namespace Sensus.MiniPrf.Ui
{
/// <summary>
/// Main form for 'MiniPrf'
/// </summary>
public partial class FrmMainMiniPrf : Form
{
#region Properties
private static readonly String _assemblyExecRootPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
// cancellation token
private CancellationTokenSource _cancellationTokenSource;
private CancellationToken _cancellationToken;
/// <summary>
/// The single FM2014 for this program
/// </summary>
private FM2014 Fm2014 { get; set; }
/// <summary>
/// The start time is going to be used for time measurements of processes. It has to be set to
/// the actual time if the measurement should be (re)started.
/// </summary>
private DateTimeOffset _startTime;
/// <summary>
/// The auto progress bar enabled is for infinite processes or if the process doesn't feed
/// the progress bar with information (e.g. Connect()).
/// </summary>
private Boolean _autoProgressBar;
// external update remarks form
private readonly FrmHistory _frmHistory = new FrmHistory();
private readonly Version _version;
// status information
private static readonly Color ColorDefault = Color.Black;
private static readonly Color ColorSuccess = Color.Green;
private static readonly Color ColorProcessFailed = Color.Red;
//private static readonly Color ColorOngoingProcess = Color.Blue;
//private static readonly Color ColorUnknownStatus = Color.Gray;
private static Color ColorStandardInputField;
private static Color ColorStandardDisplayField;
//private const String SuccessSign = @"✔";
//private const String FailedSign = @"✘";
private const String StrSeparator = "--------------------------------------------------" +
"--------------------------------------------------" +
"--------------------------------------------------";
//private readonly ILogger _logger = NLogHelper.CreateOrGetLogger("FM2014");
private readonly FM2014Config _fm2014Config = new FM2014Config();
// internal reminders of changed items to avoid write access on startup if items are preloaded
private Int32 _comPortIdx;
private Int32 _addressIdx;
private Int32 _currentOutputDisplayRangeIdx;
// Reminder of settings in standalone measurement to avoid repeated storage af already stored values
private UInt32 _initialRefPulsesPerCm;
private UInt16 _initialDutPulsesPerCm;
private Byte _initialCurrentOutputAttenuation;
/// <summary>
/// The regulation setup will be stored directly to the FM2014 being able to use it at startup
/// </summary>
private Boolean _fm2014RegulationSetupHasChanged;
/// <summary>
/// If the program configuration has changed, this shall be stored to the configuration file
/// </summary>
private Boolean _programConfigSetupHasChanged;
#endregion Properties
#region FormControls
/// <summary>
/// Ctor
/// </summary>
/// <remarks date="2025-Nov-13" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
public FrmMainMiniPrf()
{
InitializeComponent();
//var cultureInfo = new CultureInfo("en-GB");
//Thread.CurrentThread.CurrentUICulture = cultureInfo;
//Thread.CurrentThread.CurrentCulture = cultureInfo;
_version = Assembly.GetExecutingAssembly().GetName().Version;
ColorStandardDisplayField = lblFM2014SerialPort.BackColor;
ColorStandardInputField = cbxProgramComPort.BackColor;
_cancellationTokenSource = new CancellationTokenSource();
_cancellationToken = _cancellationTokenSource.Token;
helpToolStripMenuItem.Text = Resources.StrMenuHelp;
}
/// <summary>
/// Clear data table and set genesis to not connected
/// </summary>
/// <remarks date="2025-Nov-13" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void Init()
{
ActionControl(false);
_autoProgressBar = false;
_fm2014RegulationSetupHasChanged = false;
_programConfigSetupHasChanged = false;
gbxDutToRefRegulation.Visible = true;
gbxRegulationSetup.Visible = true;
if (_fm2014Config != null)
{
// Load the configuration from the local file stored in AppData\FM2014
_fm2014Config.ReadFM2014Config();
// Display actual com-port, if not assigned use the '?'. This will be placed on Items[0]
if (_fm2014Config.SharedSerialPort != null && !cbxProgramComPort.Items.Contains(_fm2014Config.SharedSerialPort))
{
cbxProgramComPort.Items.Add(_fm2014Config.SharedSerialPort);
}
else if (_fm2014Config.SharedSerialPort == null && !cbxProgramComPort.Items.Contains("?"))
{
cbxProgramComPort.Items.Add("?");
}
// Collect and add all com-ports from device manager to items
var comPorts = SerialPort.GetPortNames().ToList();
foreach (var comPort in comPorts.Where(comPort => !cbxProgramComPort.Items.Contains(comPort)))
{
cbxProgramComPort.Items.Add(comPort);
}
cbxProgramComPort.Text = cbxProgramComPort.Items[_comPortIdx].ToString();
// Select the last address
var itemContent = _fm2014Config.Address ?? 1;
for (var idx = 0; idx < cbxFM2014Address.Items.Count; idx++)
{
if (!cbxFM2014Address.Items[idx].ToString().Equals(itemContent.ToString())) continue;
_addressIdx = idx;
cbxFM2014Address.SelectedItem = cbxFM2014Address.Items[idx];
break;
}
// Select the current output range
itemContent = _fm2014Config.CommonCurrentOutputDisplayRangeNominal_percent;
for (var idx = 0; idx < cbxToleranceDisplayRange.Items.Count; idx++)
{
if (!cbxToleranceDisplayRange.Items[idx].ToString().Equals(itemContent.ToString())) continue;
_currentOutputDisplayRangeIdx = idx;
cbxToleranceDisplayRange.SelectedItem = cbxToleranceDisplayRange.Items[idx];
break;
}
// Restore baudrate
FM2014.Baudrate = _fm2014Config.Baudrate;
}
// FM2014 group box
lblConnectFM2014.Text = Resources.StrLblFM2014NotConnected;
lblConnectFM2014.ForeColor = ColorProcessFailed;
tbxFM2014FullId.Text = "";
tbxFM2014ApplicationFwVersion.Text = "";
lblFM2014SerialNumber.Text = Resources.StrLblFM2014SerialNumber;
tbxFM2014SerialNumber.Text = "";
lblFM2014SerialPort.Text = Resources.StrLblFM2014SerialPort;
lblFM2014Address.Text = Resources.StrLblFM2014Address;
lblFM2014Tolerance.Text = Resources.StrLblFM2014Tolerance;
btnFM2014Connect.Text = Resources.StrBtnConnect;
// Regulation Setup group box
gbxRegulationSetup.Text = Resources.StrGbxRegulationSetup;
lblRefPulsePerVolume.Text = Resources.StrLblRefPulsesPerCm;
tbxRefPulsesPerCm.Text = "";
lblDutPulsePerVolume.Text = Resources.StrLblDutPulsesPerCm;
tbxDutPulsesPerCm.Text = "";
lblScaleRefToDut.Text = Resources.StrLblScaleRefToDut;
tbxScaleRefToDut.Text = "";
tbxScaleRefToDut.BackColor = ColorStandardDisplayField;
lblToleranceDisplayAttenuation.Text = Resources.StrLblAttenuation;
btnSaveSetupToFm2014.Text = Resources.StrBtnSaveRegulationSetup;
// Manual REF Calibration group box
gbxManualRefCalibration.Text = Resources.StrGbxManualRefCalibration;
lblMeasuredRefPulses.Text = Resources.StrLblMeasuredRefPulses;
tbxRefPulsesMeasured.Text = "";
cbxMeasuredDutPulses.Text = Resources.StrLblMeasuredDutPulses;
tbxDutPulsesMeasured.Text = "";
lblVolumeMeasuredLiters.Text = Resources.StrLblMeasuredWeightScaleVolume;
tbxVolumeMeasuredLiters.Text = "";
tbxVolumeMeasuredLiters.BackColor = ColorStandardInputField;
lblPulseVolumeCmRelation.Text = Resources.StrLblRefPulsesPerCm;
tbxRefCalibrationResultPulsePerCm.Text = "";
tbxRefCalibrationResultPulsePerCm.BackColor = ColorStandardDisplayField;
btnManualRefCalibration.Text = Resources.StrBtnStartCalibration;
// DUT to REF Regulation group box
gbxDutToRefRegulation.Text = Resources.StrGbxDutToRefRegulation;
lblActualFlowRateCmPerHour.Text = Resources.StrLblActualMeasuredFlowRate;
tbxActualFlowRateCmPerHour.Text = "";
tbxActualFlowRateCmPerHour.BackColor = ColorStandardDisplayField;
lblActualMeasuredTolerance.Text = Resources.StrLblActualMeasuredToleranceDutToRef;
tbxActualMeasuredToleranceDutToRef.Text = "";
chkUseDampedTolerance.Text = Resources.StrChkUseDampedTolerance;
btnDutToRefRegulation.Text = Resources.StrBtnStartRegulation;
tbxRefFrequencyHz.Text = "";
tbxRefFrequencyHz.BackColor = ColorStandardDisplayField;
// Process status
lblActualProcess.Text = Resources.StrLblProcessStatus;
lblTimeText.Text = Resources.StrLblTime;
lblWaitingForMeterResponse.Text = Resources.StrLblWaitingForMeterResponse;
lblWaitingForMeterResponse.Visible = false;
CheckUpdateEnabled();
}
/// <summary>
/// Store the settings adjusted by the UI
/// </summary>
/// <remarks date="2025-Nov-13" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
/// <remarks date="2026-Jan-15..Feb-16" author="Thomas Wiedebusch">
/// - Update FM2014 properties.
/// </remarks>
private void StoreProgramConfiguration()
{
if (_fm2014Config == null)
return;
Invoke(new Action(() =>
{
_programConfigSetupHasChanged = false;
_fm2014Config.SharedSerialPort = cbxProgramComPort.Text;
if (int.TryParse(cbxFM2014Address.SelectedItem.ToString(), out var address))
{
_fm2014Config.Address = address;
}
if (int.TryParse(cbxToleranceDisplayRange.SelectedItem.ToString(), out var tolerancePercent)
&& (tolerancePercent == FM2014.CURRENT_OUTPUT_DISPLAY_RANGE_STANDARD_NOMINAL_percent ||
tolerancePercent == FM2014.CURRENT_OUTPUT_DISPLAY_RANGE_EXTENDED_NOMINAL_percent))
{
_fm2014Config.CommonCurrentOutputDisplayRangeNominal_percent = tolerancePercent;
}
_fm2014Config.Update();
}));
}
private void FrmMainMiniPrf_Load(Object sender, EventArgs e)
{
Init();
var xPosition = Location.X + Size.Width;
var yPosition = Location.Y;
if (_frmHistory != null)
{
_frmHistory.SetDesktopLocation(xPosition, yPosition);
_frmHistory.Show();
}
var version = $"Version: {_version.Major}.{_version.Minor}.{_version.Build}";
Text = $@"MiniPrf - {version}";
LogText($"MiniPrf: {version}");
LogText(StrSeparator);
btnFM2014Connect.Focus();
}
private void FrmMainMiniPrf_FormClosed(Object sender, FormClosedEventArgs e)
{
_cancellationTokenSource?.Cancel();
_frmHistory?.Close();
FM2014.Dispose();
Dispose();
}
#endregion FormControls
#region TimerControls
private void tmrProgressUpdate_Tick(Object sender, EventArgs e)
{
if (_autoProgressBar || barSingleProgressUpdate.Visible)
{
barSingleProgressUpdate.Value =
barSingleProgressUpdate.Value + 4 > 100 ? 0 : barSingleProgressUpdate.Value + 4;
}
SetTimeDisplay();
Update();
}
private void SetTimeDisplay()
{
var time = DateTimeOffset.UtcNow;
var timeSpan = time - _startTime;
lblUpdateTimeValue.Text = $@"{(UInt32)timeSpan.TotalMinutes}:{timeSpan.Seconds:00}";
}
#endregion TimerControls
#region ActivationControls
/// <summary>
/// Lock all buttons
/// </summary>
private void SetFM2014AccessLocked()
{
// Disable all buttons
btnSaveSetupToFm2014.Enabled = false;
btnManualRefCalibration.Enabled = false;
btnDutToRefRegulation.Enabled = false;
btnFM2014Connect.Enabled = false;
// Disable manual input
tbxVolumeMeasuredLiters.ReadOnly = true;
tbxRefPulsesPerCm.ReadOnly = true;
tbxDutPulsesPerCm.ReadOnly = true;
// Disable setting selections
cbxToleranceDisplayAttenuation.Enabled = false;
cbxFM2014Address.Enabled = false;
cbxProgramComPort.Enabled = false;
cbxToleranceDisplayRange.Enabled = false;
cbxMeasuredDutPulses.Enabled = false;
}
/// <summary>
/// Enable all buttons, FM2014 has to be connected
/// </summary>
private void SetFM2014AccessEnabled()
{
Invoke(new Action(() =>
{
// Enable save setup depending on changed properties
btnSaveSetupToFm2014.Enabled = _fm2014RegulationSetupHasChanged;
// Enable buttons and display correct information
btnManualRefCalibration.Enabled = true;
btnDutToRefRegulation.Enabled = true;
btnFM2014Connect.Enabled = true;
btnManualRefCalibration.Text = Resources.StrBtnStartCalibration;
btnDutToRefRegulation.Text = Resources.StrBtnStartRegulation;
// Enable manual input
tbxVolumeMeasuredLiters.ReadOnly = false;
tbxRefPulsesPerCm.ReadOnly = false;
tbxDutPulsesPerCm.ReadOnly = false;
// Enable setting selections
cbxToleranceDisplayAttenuation.Enabled = true;
cbxFM2014Address.Enabled = true;
cbxProgramComPort.Enabled = true;
cbxToleranceDisplayRange.Enabled = true;
cbxMeasuredDutPulses.Enabled = true;
}));
}
/// <summary>
/// Restore setting of buttons and timeout after operation with device
/// </summary>
private void CheckUpdateEnabled()
{
if (Fm2014 == null || !Fm2014.IsLoggedOn)
{
SetFM2014AccessLocked();
btnFM2014Connect.Enabled = true;
cbxFM2014Address.Enabled = true;
cbxProgramComPort.Enabled = true;
return;
}
SetFM2014AccessEnabled();
}
#endregion ActivationControls
#region BoardControls
/// <summary>
/// Establish connection
/// </summary>
/// <remarks date="2025-Aug-07" author="Thomas Wiedebusch">
/// - Compare the max supported FW versions of the configuration.json.
/// </remarks>
/// <remarks date="2025-Oct-02" author="Thomas Wiedebusch">
/// - Catch error message on unknown data type and kill meter.
/// </remarks>
private void Connect()
{
try
{
if (_programConfigSetupHasChanged)
StoreProgramConfiguration();
Init();
if (string.IsNullOrEmpty(cbxProgramComPort?.SelectedItem?.ToString()) ||
cbxProgramComPort.SelectedItem.ToString().Equals("?"))
{
var text = Resources.StrErrorMsgComPortNotAssigned;
MessageBox.Show(text, Resources.StrError, MessageBoxButtons.OK, MessageBoxIcon.Error);
LogText(text);
return;
}
ActionControl(true);
if (Fm2014 != null)
{
Fm2014.OnRawRecordReceived -= DataReceived_Handler;
FM2014.Dispose();
//dispose old meter
Fm2014 = null;
}
//assign new meter
Fm2014 = new FM2014();
Fm2014.OnRawRecordReceived += DataReceived_Handler;
ResetCancellationToken();
FM2014.SharedCancellationToken = _cancellationToken;
if (int.TryParse(cbxFM2014Address.SelectedItem.ToString(), out var address))
{
Fm2014.Address = address;
}
else
{
ActionControl(false);
LogErrorText(Resources.StrErrorMsgFM2014AddressInvalid);
}
if (int.TryParse(cbxToleranceDisplayRange.SelectedItem.ToString(), out var tolerancePercent)
&& (tolerancePercent == FM2014.CURRENT_OUTPUT_DISPLAY_RANGE_STANDARD_NOMINAL_percent ||
tolerancePercent == FM2014.CURRENT_OUTPUT_DISPLAY_RANGE_EXTENDED_NOMINAL_percent))
{
Fm2014.CurrentOutputDisplayRangeNominal_percent = tolerancePercent;
}
else
{
ActionControl(false);
LogErrorText(Resources.StrErrorMsgFM2014ToleranceInvalid);
}
lblActualProcess.Text = Resources.StrMsgConnecting;
Fm2014.Connect(cbxProgramComPort.SelectedItem.ToString(), true);
Task.Factory.StartNew(() =>
{
Fm2014.Login();
if (!Fm2014.IsLoggedOn)
{
LogErrorText(Resources.StrErrorMsgFM2014AccessDenied);
Fm2014.Logout();
ActionControl(false);
return;
}
if (Fm2014.IsLoggedOn)
{
Invoke(new Action(() =>
{
lblConnectFM2014.Text = Resources.StrLblFM2014Connected;
lblConnectFM2014.ForeColor = Color.Green;
tbxFM2014FullId.Text = Fm2014.ConnectResponse;
tbxFM2014FullId.Visible = true;
tbxFM2014ApplicationFwVersion.Text = Fm2014.FwVersion;
tbxFM2014SerialNumber.Text = Fm2014.SerialNumber;
// Logging to history window
LogText("FM2014 ID: " + Fm2014.ConnectResponse);
LogText("FM2014 Firmware Version: " + Fm2014.FwVersion);
LogText($"FM2014 {Resources.StrLblFM2014SerialNumber} " + Fm2014.SerialNumber);
LogText($"FW2014 {Resources.StrLblFM2014Address} " + Fm2014.Address);
var dt = DateTime.UtcNow;
var msg = $"{dt:yyyy-MM-dd HH:mm:ss} UTC";
LogText($"{Resources.StrMsgPcDateTime} {msg}");
LogText(StrSeparator);
CheckUpdateEnabled();
}));
}
}, _cancellationToken).ContinueWith(delegate
{
ActionControl(false);
}, _cancellationToken);
}
catch (Exception ex)
{
ActionControl(false);
MessageBox.Show(ex.Message, Resources.StrError, MessageBoxButtons.OK, MessageBoxIcon.Error);
LogErrorText(ex.Message);
FM2014.Dispose();
}
}
#endregion BoardControls
#region ProcessControls
/// <summary>
/// Common method to (de-)activate controls and timer.
/// </summary>
/// <remarks date="2026-Jan-14..20" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void ActionControl(Boolean isActive)
{
Invoke(new Action(() =>
{
if (isActive)
{
lblActualProcess.Visible = true;
barSingleProgressUpdate.Visible = true;
SetFM2014AccessLocked();
tmrProgressUpdate.Enabled = true;
}
else
{
lblActualProcess.Visible = false;
barSingleProgressUpdate.Visible = false;
tmrProgressUpdate.Enabled = false;
lblWaitingForMeterResponse.Visible = false;
lblActualProcess.Text = "";
CheckUpdateEnabled();
}
//common actions and settings
lblActualProcess.Update();
barSingleProgressUpdate.Value = 0;
barSingleProgressUpdate.Update();
Update();
}));
}
/// <summary>
/// Common method to prepare for changed setup.
/// </summary>
/// <remarks date="2026-Jan-14..20" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void DutToRefRegulationSetupHasChanged()
{
_fm2014RegulationSetupHasChanged = true;
Invoke(new Action(() =>
{
btnSaveSetupToFm2014.Enabled = true;
btnDutToRefRegulation.Enabled = true;
tbxRefFrequencyHz.BackColor = ColorStandardDisplayField;
tbxActualFlowRateCmPerHour.BackColor = ColorStandardDisplayField;
tbxActualMeasuredToleranceDutToRef.BackColor = ColorStandardDisplayField;
tbxRefPulsesPerCm.BackColor = ColorStandardInputField;
tbxDutPulsesPerCm.BackColor = ColorStandardInputField;
tbxScaleRefToDut.BackColor = ColorStandardDisplayField;
tbxVolumeMeasuredLiters.BackColor = ColorStandardInputField;
tbxRefCalibrationResultPulsePerCm.BackColor = ColorStandardDisplayField;
}));
}
/// <summary>
/// This method checks if any setting applied to the FM2014 memory has been changed.
/// Initially, those values will be read and preset during the connection procedure.
/// </summary>
/// <remarks date="2026-Feb-17" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void CheckForFM2014SetupChanged()
{
if (Fm2014 == null)
return;
// Avoid stress to the FM2014 EEPROM as the identical values mustn't be stored again
if (_initialCurrentOutputAttenuation == Fm2014.CurrentOutputAttenuation &&
_initialRefPulsesPerCm == Fm2014.RefPulses_per_cm &&
_initialDutPulsesPerCm == Fm2014.DutPulses_per_cm)
{
_fm2014RegulationSetupHasChanged = false;
btnSaveSetupToFm2014.Enabled = false;
return;
}
DutToRefRegulationSetupHasChanged();
}
/// <summary>
/// Common routine for REF to DUT scale check and update
/// </summary>
/// <returns></returns>
/// <remarks date="2026-Jan-14..21" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private Boolean UpdateRefToDutScale()
{
var retVal = true;
Invoke(new Action(() =>
{
tbxScaleRefToDut.Text = $@"{Fm2014.RefToDutScale_norm:F4}";
// Display error if scale doesn't fit
var tempRefToDutScale_norm = (Double)Fm2014.RefPulses_per_cm / Fm2014.DutPulses_per_cm;
if (tempRefToDutScale_norm < FM2014.REF_TO_DUT_SCALE_MIN ||
tempRefToDutScale_norm > FM2014.REF_TO_DUT_SCALE_MAX ||
Fm2014.RefToDutScale_norm < FM2014.REF_TO_DUT_SCALE_MIN ||
Fm2014.RefToDutScale_norm > FM2014.REF_TO_DUT_SCALE_MAX)
{
tbxScaleRefToDut.BackColor = ColorProcessFailed;
tbxScaleRefToDut.Text = Resources.StrError;
btnSaveSetupToFm2014.Enabled = false;
btnDutToRefRegulation.Enabled = false;
retVal = false;
}
}));
return retVal;
}
/// <summary>
/// Common routine to reset the cancellation token on restart or new meter
/// </summary>
/// <remarks date="2026-Jan-26" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
private void ResetCancellationToken()
{
// This state needs an external state change like user input to go ahead
// Remove cancellation token if idle reached for clean start
if (_cancellationToken.IsCancellationRequested)
{
// Reset the cancellation request
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = new CancellationTokenSource();
_cancellationToken = _cancellationTokenSource.Token;
}
}
#endregion ProcessControls
#region InfoWindow
/// <summary>
/// Clear history window.
/// </summary>
/// <remarks date="2021-Jan-05" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
private void ClearHistoryWindow()
{
if (_frmHistory != null && _frmHistory.rtbHistory.InvokeRequired)
{
_frmHistory.rtbHistory.Invoke(new Action(() => { _frmHistory.rtbHistory.Clear(); }));
}
else
{
_frmHistory?.rtbHistory.Clear();
}
}
/// <summary>
/// Output exclusively to user update remarks text window.
/// </summary>
/// <remarks date="2021-Apr-26" author="Thomas Wiedebusch">
/// - Color added.
/// </remarks>
/// <remarks date="2026-Aug-15" author="Thomas Wiedebusch">
/// - File output added.
/// </remarks>
private void LogText(String txtHistory, String filename = null)
{
InfoWindowColoredText(txtHistory, ColorDefault);
if (!string.IsNullOrEmpty(filename))
{
File.AppendAllLines(filename, new[] { txtHistory });
}
}
/// <summary>
/// Output exclusively to user update remarks text window.
/// </summary>
private void LogErrorText(String txtHistory)
{
InfoWindowColoredText(txtHistory, ColorProcessFailed);
}
/// <summary>
/// Output exclusively to user update remarks text window.
/// </summary>
private void LogSuccessText(String txtHistory)
{
InfoWindowColoredText(txtHistory, ColorSuccess);
}
/// <summary>
/// Output exclusively to user update remarks text window.
/// </summary>
private void InfoWindowColoredText(String txtHistory, Color color)
{
if (_frmHistory?.rtbHistory == null)
{
return;
}
Invoke(new Action(() =>
{
_frmHistory.rtbHistory.SuspendLayout();
_frmHistory.rtbHistory.SelectionStart = _frmHistory.rtbHistory.Text.Length;
_frmHistory.rtbHistory.SelectionLength = 0;
_frmHistory.rtbHistory.SelectionColor = color;
_frmHistory.rtbHistory.AppendText($"{txtHistory}{Environment.NewLine}");
_frmHistory.rtbHistory.SelectionColor = _frmHistory.rtbHistory.ForeColor;
_frmHistory.rtbHistory.ScrollToCaret();
_frmHistory.rtbHistory.ResumeLayout();
}));
}
#endregion HistoryWindow
#region Event handler
/// <summary>
/// Feedback from FM2014being parsed to GUI
/// </summary>
/// <remarks date="2026-Jan-04..Feb-16" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void DataReceived_Handler(Object sender, ProcessExecEventArgs e)
{
Invoke(new Action(() =>
{
// Process info
if (e.ActualProcessMessage != null)
lblActualProcess.Text = e.ActualProcessMessage;
lblActualProcess.Update();
SetTimeDisplay();
lblUpdateTimeValue.Update();
if (e.ActualProcessPercent != null && !_autoProgressBar && barSingleProgressUpdate.Visible)
barSingleProgressUpdate.Value = (Int32)e.ActualProcessPercent;
// Data dispatcher
var resp = (CmdResponse)e.SpecificInfoObj;
// Used as marker for error, null means measurement is in range or status or health is good
var statusColor = ColorStandardInputField;
// Color the output for the user display for quick check capability
if (e.StatusReturn == StatusReturn.MeasurementOutOfRange)
statusColor = ColorProcessFailed;
else if (e.StatusReturn == StatusReturn.MeasurementInRange)
statusColor = ColorSuccess;
if (e.StatusReturn == StatusReturn.Failed)
{
LogErrorText(resp.AnswerStr);
//ErrorHandler(resp.CmdName);
}
else if (resp.IntValue == null && resp.DoubleValue == null)
{
LogText(resp.AnswerStr);
}
else if (resp.IntValue != null)
{
LogText($"{resp.AnswerStr}: {resp.IntValue:D} {resp.Unit}");
switch (resp.CmdName)
{
case CmdName.CmdGetRefPulsesCounted:
case CmdName.CmdGetRefPulsesCountedBackup:
tbxRefPulsesMeasured.Text = $@"{resp.IntValue:D}";
break;
case CmdName.CmdGetDutPulsesCounted:
case CmdName.CmdGetDutPulsesCountedBackup:
tbxDutPulsesMeasured.Text = $@"{resp.IntValue:D}";
break;
case CmdName.CmdSetRefPulsesPerCm:
tbxRefPulsesPerCm.Text = $@"{resp.IntValue:D}";
_initialRefPulsesPerCm = (UInt32)resp.IntValue;
break;
case CmdName.CmdSetDutPulsesPerCm:
tbxDutPulsesPerCm.Text = $@"{resp.IntValue:D}";
_initialDutPulsesPerCm = (UInt16)resp.IntValue;
break;
case CmdName.CmdSetCurrentOutputAttenuation:
var idx = cbxToleranceDisplayAttenuation.FindString($@"{resp.IntValue}");
cbxToleranceDisplayAttenuation.SelectedIndex = idx;
_initialCurrentOutputAttenuation = (Byte)resp.IntValue;
break;
case CmdName.CmdGetRefFrequency:
tbxRefFrequencyHz.Text = $@"{resp.IntValue:D}";
tbxRefFrequencyHz.BackColor = statusColor;
tbxActualFlowRateCmPerHour.BackColor = statusColor;
break;
case CmdName.CmdResetMeasurement:
// Immediately lock all buttons and input fields until reset is finished
SetFM2014AccessLocked();
// Check countdown of reset method
if (resp.IntValue == 0)
ActionControl(false);
break;
}
}
else if (resp.DoubleValue != null)
{
LogText($"{resp.AnswerStr}: {resp.DoubleValue:F2} {resp.Unit}");
switch (resp.CmdName)
{
case CmdName.CmdGetDutToRefToleranceUndamped:
case CmdName.CmdGetDutToRefToleranceDamped:
tbxActualMeasuredToleranceDutToRef.Text = $@"{resp.DoubleValue:F2}";
tbxActualMeasuredToleranceDutToRef.BackColor = statusColor;
break;
case CmdName.CmdRefToDutScale:
tbxScaleRefToDut.Text = $@"{resp.DoubleValue:F4}";
break;
case CmdName.CmdCalculatedFlowRefFrequ:
tbxActualFlowRateCmPerHour.Text = $@"{resp.DoubleValue:F3}";
break;
}
}
// Catch the latest information
Update();
}));
}
#endregion Event Handler
#region Tools
/// <summary>
/// Set the actual process and log the text.
/// </summary>
/// <remarks date="2026-Jan-04" author="Thomas Wiedebusch">
/// - Color added.
/// </remarks>
private void SetActualProcessAndLog(String message, String logFileName = null)
{
Invoke(new Action(() => { lblActualProcess.Text = message; }));
LogText(message, logFileName);
}
#endregion
#region Buttons and Controls
/// <summary>
/// Establish connection to FM2014 with individual address and read out FM2014 info.
/// </summary>
/// <remarks date="2026-Jan-04" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void btnConnect_Click(Object sender, EventArgs e)
{
_startTime = DateTimeOffset.UtcNow;
Connect();
}
/// <summary>
/// Start and stop the DUT to REF regulation measurement.
/// </summary>
/// <remarks date="2026-Jan-14" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void btnDutToRefRegulation_Click(Object sender, EventArgs e)
{
_startTime = DateTimeOffset.UtcNow;
Invoke(new Action(() =>
{
if (FM2014.SharedCyclicMeasSequ == CyclicMeasSequ.idle)
{
if (FM2014.RegisteredFm2014s == null || FM2014.RegisteredFm2014s.Count == 0)
return;
if (FM2014.RegulationMeasurement(Fm2014.RefPulses_per_cm, Fm2014.DutPulses_per_cm,
Fm2014.DoublePulseDeadtime_ms, Fm2014.CurrentOutputAttenuation))
{
_autoProgressBar = true;
ActionControl(true);
btnDutToRefRegulation.Text = Resources.StrBtnStopRegulation;
btnDutToRefRegulation.Enabled = true;
}
}
else
{
// Deactivate the button temporary to avoid repeated execution as the reset takes a certain time
_autoProgressBar = false;
btnDutToRefRegulation.Enabled = false;
FM2014.ResetHardwareAllDevices();
btnDutToRefRegulation.Text = Resources.StrBtnStartRegulation;
}
}));
}
/// <summary>
/// Start and stop the manual REF calibration measurement.
/// </summary>
/// <remarks date="2026-Jan-04..12" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void btnManualRefCalibration_Click(Object sender, EventArgs e)
{
_startTime = DateTimeOffset.UtcNow;
Invoke(new Action(() =>
{
if (FM2014.SharedCyclicMeasSequ == CyclicMeasSequ.idle)
{
tbxRefPulsesMeasured.Text = "";
tbxDutPulsesMeasured.Text = "";
tbxVolumeMeasuredLiters.Text = "";
tbxRefCalibrationResultPulsePerCm.Text = "";
tbxActualFlowRateCmPerHour.Text = "";
tbxActualMeasuredToleranceDutToRef.Text = "";
tbxRefFrequencyHz.Text = "";
if (FM2014.PulseCounterMeasurement(true, cbxMeasuredDutPulses.Checked))
{
_autoProgressBar = true;
ActionControl(true);
btnManualRefCalibration.Text = Resources.StrBtnStopCalibration;
btnManualRefCalibration.Enabled = true;
}
}
else
{
// Deactivate the button temporary to avoid repeated execution as the reset takes a certain time
_autoProgressBar = false;
btnManualRefCalibration.Enabled = false;
FM2014.ResetHardwareAllDevices();
}
}));
}
/// <summary>
/// Selected COM port, tolerance display settings or address item has changed.
/// These values can be setup before any connection to the FM2014 has been established. Therefore, those
/// settings have to be remembered within this object.
/// </summary>
/// <remarks date="2026-Jan-04" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void cbxProgramSettings_Changed(Object sender, EventArgs e)
{
Invoke(new Action(() =>
{
if (_currentOutputDisplayRangeIdx != cbxToleranceDisplayRange.SelectedIndex ||
_addressIdx != cbxFM2014Address.SelectedIndex ||
_comPortIdx != cbxProgramComPort.SelectedIndex)
{
_currentOutputDisplayRangeIdx = cbxToleranceDisplayRange.SelectedIndex;
_addressIdx = cbxFM2014Address.SelectedIndex;
_comPortIdx = cbxProgramComPort.SelectedIndex;
StoreProgramConfiguration();
Init();
}
}));
}
/// <summary>
/// Safe standalone measurement setup.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Jan-18..Feb-17" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void btnSaveSetupToFm2014_Click(Object sender, EventArgs e)
{
if (Fm2014 != null && FM2014.StoreAllConfigurations())
{
_fm2014RegulationSetupHasChanged = false;
btnSaveSetupToFm2014.Enabled = false;
_initialRefPulsesPerCm = Fm2014.RefPulses_per_cm;
_initialDutPulsesPerCm = Fm2014.DutPulses_per_cm;
_initialCurrentOutputAttenuation = Fm2014.CurrentOutputAttenuation;
}
}
/// <summary>
/// Switch between damped or undamped tolerance display. This can be changed during the 'Regulation Measurement'
/// as it will just collect a different dataset and not change anything on the measurement setup.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Jan-14" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void chkUseDampedTolerance_CheckedChanged(Object sender, EventArgs e)
{
Invoke(new Action(() =>
{
if (Fm2014 != null)
Fm2014.UseDampedTolerance = chkUseDampedTolerance.Checked;
}));
}
/// <summary>
/// Common check for key input to edit an integer field:
/// - Allows Left, Right, Back, Delete and 0-9 keys.
/// </summary>
/// <param name="keyCode"></param>
/// <returns>true an allowed key-code</returns>
/// <remarks date="2026-Jan-25" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private static Boolean MaskEditIntegerInput(Keys keyCode)
{
return keyCode == Keys.Back ||
keyCode == Keys.Left ||
keyCode == Keys.Right ||
keyCode == Keys.Delete ||
Regex.IsMatch($"{keyCode}", @"[0-9]");
}
/// <summary>
/// Redirect keystroke 'Enter' to leaving the cell event
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Jan-14..24" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void tbxRefPulsesPerCm_KeyDown(Object sender, KeyEventArgs e)
{
// Catch the enter key, recalculate all settings
if (e.KeyCode == Keys.Enter)
{
tbxRefPulsesPerCm_Leave(this, null);
SelectNextControl(tbxDutPulsesPerCm, true, true, true, true);
}
else if (!MaskEditIntegerInput(e.KeyCode))
{
e.SuppressKeyPress = true;
}
else
{
// Take the new input and calculate the results after the input has been taken after this event!
Task.Factory.StartNew(() =>
{
Thread.Sleep(1);
}, _cancellationToken).ContinueWith(delegate
{
Invoke(new Action(() =>
{
tbxRefPulsesPerCm_Leave(this, null);
}));
}, _cancellationToken);
}
}
/// <summary>
/// After manual input of the REF pulses per cubic meter the scale REF to DUT has to be recalculated
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Jan-14..Feb-17" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void tbxRefPulsesPerCm_Leave(Object sender, EventArgs e)
{
Invoke(new Action(() =>
{
if (Fm2014 == null)
return;
if (string.IsNullOrEmpty(tbxRefPulsesPerCm.Text))
{
btnSaveSetupToFm2014.Enabled = false;
tbxRefPulsesPerCm.BackColor = ColorProcessFailed;
btnDutToRefRegulation.Enabled = false;
return;
}
// Parse and check limits
if (int.TryParse(tbxRefPulsesPerCm.Text, out var pulses_per_cm) &&
FM2014.PULSES_PER_CM_REGULATION_SETUP_MIN <= pulses_per_cm &&
FM2014.REF_PULSES_PER_CM_REGULATION_INPUT_MAX >= pulses_per_cm)
{
// During setup of the 'Ref_pulse_per_cm' the 'RefToDutScaleStr' will be generated
Fm2014.RefPulses_per_cm = (UInt32)pulses_per_cm;
if (!UpdateRefToDutScale())
{
tbxRefPulsesPerCm.BackColor = ColorProcessFailed;
return;
}
tbxRefPulsesPerCm.BackColor = ColorStandardInputField;
CheckForFM2014SetupChanged();
}
else
{
tbxRefPulsesPerCm.BackColor = ColorProcessFailed;
_fm2014RegulationSetupHasChanged = false;
btnSaveSetupToFm2014.Enabled = false;
}
}));
}
/// <summary>
/// Redirect keystroke 'Enter' to leaving the cell event
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Jan-14..24" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void tbxDutPulsesPerCm_KeyDown(Object sender, KeyEventArgs e)
{
// Catch the enter key, recalculate all settings
if (e.KeyCode == Keys.Enter)
{
tbxDutPulsesPerCm_Leave(this, null);
SelectNextControl(ActiveControl, true, true, true, true);
}
else if (!MaskEditIntegerInput(e.KeyCode))
{
e.SuppressKeyPress = true;
}
else
{
// Take the new input and calculate the results after the input has been taken after this event!
Task.Factory.StartNew(() =>
{
Thread.Sleep(1);
}, _cancellationToken).ContinueWith(delegate
{
Invoke(new Action(() =>
{
tbxDutPulsesPerCm_Leave(this, null);
}));
}, _cancellationToken);
}
}
/// <summary>
/// After manual input of the DUT pulses per cubic meter the scale REF to DUT has to be recalculated
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Jan-14..Feb-17" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void tbxDutPulsesPerCm_Leave(Object sender, EventArgs e)
{
Invoke(new Action(() =>
{
if (Fm2014 == null)
return;
if (string.IsNullOrEmpty(tbxDutPulsesPerCm.Text))
{
tbxDutPulsesPerCm.BackColor = ColorProcessFailed;
btnSaveSetupToFm2014.Enabled = false;
btnDutToRefRegulation.Enabled = false;
return;
}
// Parse and check limits
if (int.TryParse(tbxDutPulsesPerCm.Text, out var pulses_per_cm) &&
FM2014.PULSES_PER_CM_REGULATION_SETUP_MIN <= pulses_per_cm &&
FM2014.PULSES_PER_CM_REGULATION_SETUP_MAX >= pulses_per_cm)
{
// During setup of the 'Dut_pulse_per_cm' the 'RefToDutScaleStr' will be generated
Fm2014.DutPulses_per_cm = (UInt16)pulses_per_cm;
if (!UpdateRefToDutScale())
{
tbxDutPulsesPerCm.BackColor = ColorProcessFailed;
return;
}
tbxDutPulsesPerCm.BackColor = ColorStandardInputField;
CheckForFM2014SetupChanged();
}
else
{
tbxDutPulsesPerCm.BackColor = ColorProcessFailed;
_fm2014RegulationSetupHasChanged = false;
btnSaveSetupToFm2014.Enabled = false;
}
}));
}
/// <summary>
/// Redirect keystroke 'Enter' to leaving the cell event
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Jan-15..24" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void tbxManualInputVolumeLiters_KeyStoke(Object sender, KeyEventArgs e)
{
// Catch the enter key, recalculate all settings
if (e.KeyCode == Keys.Enter)
{
tbxManualInputVolumeLiters_Leave(this, null);
SelectNextControl(ActiveControl, true, true, true, true);
}
else if (!MaskEditIntegerInput(e.KeyCode))
{
e.SuppressKeyPress = true;
}
else
{
// Take the new input and calculate the results after the input has been taken after this event!
Task.Factory.StartNew(() =>
{
Thread.Sleep(1);
}, _cancellationToken).ContinueWith(delegate
{
Invoke(new Action(() =>
{
tbxManualInputVolumeLiters_Leave(this, null);
}));
}, _cancellationToken);
}
}
/// <summary>
/// After manual input of measured weight scale or reservoir in liters this will calculate the
/// REF pulses per cubic meter and copy from REF pulses per cubic meter in 'Manual REF Calibration'
/// to 'Regulation Setup'.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Jan-15..Feb-17" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void tbxManualInputVolumeLiters_Leave(Object sender, EventArgs e)
{
Invoke(new Action(() =>
{
if (Fm2014 == null)
return;
if (!string.IsNullOrEmpty(tbxRefPulsesMeasured.Text) &&
string.IsNullOrEmpty(tbxVolumeMeasuredLiters.Text))
{
tbxVolumeMeasuredLiters.BackColor = ColorProcessFailed;
btnSaveSetupToFm2014.Enabled = false;
btnDutToRefRegulation.Enabled = false;
return;
}
// Calculate REF pulses per cubic meter
if (int.TryParse(tbxRefPulsesMeasured.Text, out var pulses) && pulses > 0 &&
int.TryParse(tbxVolumeMeasuredLiters.Text, out var liters) && liters > 0)
{
// Calculate the new pulse ratio based on the manual calibration
var pulses_per_cm = (UInt32)(pulses / (liters / 1000.0) + 0.5);
// Try to set the new calculated REF pulses limited by the property setter
Fm2014.RefPulses_per_cm = pulses_per_cm;
// If this succeeded, then update the new manual calibrated value
if (pulses_per_cm == Fm2014.RefPulses_per_cm)
{
tbxRefCalibrationResultPulsePerCm.Text = $@"{Fm2014.RefPulses_per_cm:D}";
tbxRefCalibrationResultPulsePerCm.BackColor = ColorStandardDisplayField;
tbxRefPulsesPerCm.Text = $@"{Fm2014.RefPulses_per_cm:D}";
tbxVolumeMeasuredLiters.BackColor = ColorStandardInputField;
tbxRefCalibrationResultPulsePerCm.BackColor = ColorStandardDisplayField;
if (!UpdateRefToDutScale())
{
tbxVolumeMeasuredLiters.BackColor = ColorProcessFailed;
tbxRefCalibrationResultPulsePerCm.BackColor = ColorProcessFailed;
return;
}
tbxVolumeMeasuredLiters.BackColor = ColorStandardInputField;
tbxRefCalibrationResultPulsePerCm.BackColor = ColorStandardInputField;
CheckForFM2014SetupChanged();
}
else
{
_fm2014RegulationSetupHasChanged = false;
btnSaveSetupToFm2014.Enabled = false;
tbxVolumeMeasuredLiters.BackColor = ColorProcessFailed;
tbxRefCalibrationResultPulsePerCm.BackColor = ColorProcessFailed;
tbxRefCalibrationResultPulsePerCm.Text = Resources.StrError;
}
}
}));
}
/// <summary>
/// Switch between DEBUG and regular operation
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void picFM2014_Click(Object sender, EventArgs e)
{
Invoke(new Action(() =>
{
}));
}
/// <summary>
/// FM2014 current attenuation item has changed:
/// - This will dispatch the new value to all FM2014s.
/// </summary>
/// <remarks date="2026-Jan-18..Feb-17" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void cbxToleranceDisplayAttenuation_SelectedIndexChanged(Object sender, EventArgs e)
{
Invoke(new Action(() =>
{
//Backup the actual setting to detect changes
var backupAttenuation = Fm2014.CurrentOutputAttenuation;
if (byte.TryParse(cbxToleranceDisplayAttenuation.SelectedItem.ToString(), out var attenuation) &&
attenuation >= FM2014.CURRENT_OUTPUT_ATTENUATION_MIN &&
attenuation <= FM2014.CURRENT_OUTPUT_ATTENUATION_MAX &&
backupAttenuation != attenuation)
{
Fm2014.CurrentOutputAttenuation = attenuation;
CheckForFM2014SetupChanged();
}
}));
}
/// <summary>
/// Select next control on enter key pressed
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void cbxToleranceDisplayAttenuation_KeyDown(Object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Enter)
{
SelectNextControl(ActiveControl, true, true, true, true);
}
}
private void helpToolStripMenuItem_Click(Object sender, EventArgs e)
{
try
{
var filename = "";
if (Thread.CurrentThread.CurrentCulture.Name.Contains("de"))
{
filename = Path.Combine(_assemblyExecRootPath, "Docu", @"MiniPrf Kurzanleitung.pdf");
}
else if (Thread.CurrentThread.CurrentCulture.Name.Contains("en"))
{
filename = Path.Combine(_assemblyExecRootPath, "Docu", @"MiniPrf Quick Reference.pdf");
}
if (!string.IsNullOrEmpty(filename))
{
Process.Start(filename);
}
}
catch (Exception exception)
{
Console.WriteLine(exception.Message);
}
}
#endregion Buttons and Controls
}
}