laatzen/FM2014TestApp/Ui/Fm2014sTest/FM2014TestBenchWindow.xaml.cs
2026-02-14 11:17:10 +01:00

2512 lines
108 KiB
C#

/*********************************************************************************************************************/
/*! @file FM2014TestBench.xaml.cs
* @brief <i>Test bench implementation for manual tests of FM2014 driver</i>
*
* @author Thomas Wiedebusch
* @date 2026-Feb-02
*
* @details <i>Multiple FM2014s can be tested using an FM2014 user control.</i>
*
* @copyright © SENSUS GmbH 2026. All rights reserved.
*********************************************************************************************************************/
using Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Config;
using Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core;
using Sensus.Ui.Fm2014TestBench.UserControls;
using System;
using System.Collections.Generic;
using System.Globalization;
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;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using Xylem.Common.CommonCore.Consts;
using Xylem.Common.Utils.ProcessExec.EventArguments;
using static Sensus.Common.Hardware.WaterMeter.MechanicalMeter.FM2014.FM2014Core.Consts.FM2014CmdDef;
using CheckBox = System.Windows.Controls.CheckBox;
using KeyEventArgs = System.Windows.Input.KeyEventArgs;
using Label = System.Windows.Controls.Label;
using Properties = Sensus.Ui.Fm2014TestBench.Properties;
using TextBox = System.Windows.Controls.TextBox;
using UserControl = System.Windows.Controls.UserControl;
namespace Sensus.Ui.FM2014TestBench
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class FM2014TestBenchWindow
{
#region ------------------------------------------- Properties ------------------------------------------------
// cancellation token
private CancellationTokenSource _cancellationTokenSource;
private CancellationToken _cancellationToken;
private const Int32 MaxFm2014s = 10;
/// <summary>
/// The FM2014s served by this program
/// </summary>
private readonly List<FM2014> Fm2014s = new List<FM2014>();
/// <summary>
/// Stack panels for UcFM2014Device
/// </summary>
private readonly List<StackPanel> PanelsForUcFM2014 = new List<StackPanel>();
/// <summary>
/// Slot selections for FM2014s
/// </summary>
private readonly List<CheckBox> SlotSelections = new List<CheckBox>();
/// <summary>
/// User controls of FM2014Device
/// </summary>
private readonly List<UserControl> UcFM2014s = new List<UserControl>();
/// <summary>
/// Variable measurement labels depending on ongoing measurement shared for all
/// FM2014 devices.
/// </summary>
private readonly List<Label> MeasurementLabels = new List<Label>();
/// <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>
/// Program internal timer
/// </summary>
private readonly DispatcherTimer _tmrProgressUpdate = new DispatcherTimer();
/// <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;
private Version _version;
// status information
private static readonly Brush ColorDefault = Brushes.Black;
private static readonly Brush ColorSuccess = Brushes.Green;
private static readonly Brush ColorProcessFailed = Brushes.Red;
//private static readonly Color ColorOngoingProcess = Color.Blue;
//private static readonly Color ColorUnknownStatus = Color.Gray;
private static Brush ColorStandardInputField;
private static Brush ColorStandardDisplayField;
//private static Brush ColorStandardTextColor;
//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;
// Backups of results from 'Manual REF Calibration' being able to restore those if a manual change
// of the REF pulses per cubic meter clears those fields. Bringing the last calibrated REF pulses
// per cubic meters back will automatically restore these results being able to adjust the measured
// volume in liters without a restart of the 'Manual REF Calibration'
//private String _backupWeightScaleVolumeLitersStr;
//private String _backupRefCalibrationResultPulsePerCmStr;
//private String _backupMeasuredRefPulsesStr;
private Boolean _regulationSetupHasChanged;
private Boolean _configSetupHasChanged;
private Boolean _calibrationSetupHasChanged;
private String _lastLoggingTextToAvoidRepetition;
// DUT to REF regulation label index published to UcFM2014s measurement positions 2
private const Int32 REG_LBL_IDX_REF_FREQU = 0;
private const Int32 REG_LBL_IDX_FLOW_RATE = 1;
private const Int32 REG_LBL_IDX_DUT_TO_REF_TOL = 2;
private const Int32 REG_MEASURE_COUNT = 3;
// DUT to REF calibration label index published to UcFM2014s measurement positions 0..6
private const Int32 CAL_LBL_IDX_REF_REQ_PLS = 0;
private const Int32 CAL_LBL_IDX_REF_RMN_PLS = 1;
private const Int32 CAL_LBL_IDX_DUT_REQ_PLS = 2;
private const Int32 CAL_LBL_IDX_DUT_RMN_PLS = 3;
private const Int32 CAL_LBL_IDX_REF_MEAS_TMR = 4;
private const Int32 CAL_LBL_IDX_DUT_MEAS_TMR = 5;
private const Int32 CAL_LBL_IDX_DUT_TO_REF_TOL = 6;
private const Int32 CAL_MEASURE_COUNT = 7;
// Pulse counter measurements label index published to UcFM2014s measurement positions 2..3
private const Int32 CTR_LBL_IDX_REF_PLS = 0;
private const Int32 CTR_LBL_IDX_DUT_PLS = 1;
private const Int32 CTR_MEASURE_COUNT = 2;
#endregion ---------------------------------------- Properties ------------------------------------------------
#region ------------------------------------------- FormControls ----------------------------------------------
/// <summary>
/// Ctor
/// </summary>
/// <remarks date="2026-Feb-02" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
public FM2014TestBenchWindow()
{
InitializeComponent();
//var cultureInfo = new CultureInfo("en");
//Thread.CurrentThread.CurrentUICulture = cultureInfo;
//Thread.CurrentThread.CurrentCulture = cultureInfo;
ColorStandardDisplayField = lblRefPulsesPerCm.Background;
ColorStandardInputField = tbxRefPulsesPerCm.Background;
//ColorStandardTextColor = lblRefPulsesPerCm.Foreground;
_cancellationTokenSource = new CancellationTokenSource();
_cancellationToken = _cancellationTokenSource.Token;
// Set timer to 500 ms interval
_tmrProgressUpdate.Interval = new TimeSpan(0, 0, 0, 0, 500);
_tmrProgressUpdate.Tick += tmrProgressUpdate_Tick;
}
private void Window_Loaded(Object sender, RoutedEventArgs e)
{
//if (!Software.IsAutenticated)
// OnLogin_Click(this, null);
}
/// <summary>
/// Initialize the main window after it is completely loaded and assign all objects:
/// - 10 x Reserve panels for 10 x FM2014.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <returns></returns>
/// <remarks date="2026-Feb-03..04" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void Window_Initialized(Object sender, EventArgs e)
{
_version = Assembly.GetExecutingAssembly().GetName().Version;
// ---- SORTED! ---- list of the stack panels
PanelsForUcFM2014.Add(spDevice1);
PanelsForUcFM2014.Add(spDevice2);
PanelsForUcFM2014.Add(spDevice3);
PanelsForUcFM2014.Add(spDevice4);
PanelsForUcFM2014.Add(spDevice5);
PanelsForUcFM2014.Add(spDevice6);
PanelsForUcFM2014.Add(spDevice7);
PanelsForUcFM2014.Add(spDevice8);
PanelsForUcFM2014.Add(spDevice9);
PanelsForUcFM2014.Add(spDevice10);
// ---- SORTED! ---- list of check boxes for slot selection
SlotSelections.Add(chkSlot1);
SlotSelections.Add(chkSlot2);
SlotSelections.Add(chkSlot3);
SlotSelections.Add(chkSlot4);
SlotSelections.Add(chkSlot5);
SlotSelections.Add(chkSlot6);
SlotSelections.Add(chkSlot7);
SlotSelections.Add(chkSlot8);
SlotSelections.Add(chkSlot9);
SlotSelections.Add(chkSlot10);
// ---- SORTED! ---- list of measurement labels for all devices
MeasurementLabels.Add(lblMeasurement1Text);
MeasurementLabels.Add(lblMeasurement2Text);
MeasurementLabels.Add(lblMeasurement3Text);
MeasurementLabels.Add(lblMeasurement4Text);
MeasurementLabels.Add(lblMeasurement5Text);
MeasurementLabels.Add(lblMeasurement6Text);
MeasurementLabels.Add(lblMeasurement7Text);
// Preset attenuation
var attenuation = FM2014.CURRENT_OUTPUT_ATTENUATION_MIN;
for (var idx = 0; idx < FM2014.CURRENT_OUTPUT_ATTENUATION_MAX - FM2014.CURRENT_OUTPUT_ATTENUATION_MIN; idx++)
{
cbxDisplayAttenuation.Items.Add(attenuation.ToString());
attenuation++;
}
// Preset tolerance setup for feedback calculation of DUT to REF tolerance
cbxToleranceCalc.Items.Add("3");
cbxToleranceCalc.Items.Add("5");
Init();
}
/// <summary>
/// Dispatch new user control to main screen or remove it with uc == null
/// </summary>
/// <param name="sp"></param>
/// <param name="uc">new UserControl or null to remove it</param>
private void SetNewUserControl(StackPanel sp, UserControl uc)
{
sp.Dispatcher.Invoke(DispatcherPriority.Normal,
new Action(() =>
{
// remove last userControl
sp.Children.Clear();
if (uc != null)
{
sp.Children.Add(uc);
DockPanel.SetDock(uc, Dock.Top); // & Dock.Left);
}
}
));
}
/// <summary>
/// Exit with disposure of objects.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Window_Closing(Object sender, System.ComponentModel.CancelEventArgs e)
{
_cancellationTokenSource.Cancel();
Thread.Sleep(500);
foreach (var fm2014 in Fm2014s)
{
fm2014.OnRawRecordReceived -= DataReceived_Handler;
}
// Dispose all registered FM2014
FM2014.Dispose();
}
/// <summary>
/// Update UI elements text with content and color
/// </summary>
/// <param name="c"></param>
/// <param name="content"></param>
/// <param name="color"></param>
private void UpdateTextBox(TextBox c, String content, Brush color = null)
{
color = color ?? Brushes.Black;
c.Dispatcher.Invoke(DispatcherPriority.Normal,
new Action(() =>
{
c.Text = content;
c.Foreground = color;
}));
}
/// <summary>
/// Update progress bar
/// </summary>
/// <param name="pb"></param>
/// <param name="value"></param>
/// <param name="color"></param>
private void UpdateProgressBar(ProgressBar pb, Double value, Brush color = null)
{
color = color ?? Brushes.Green;
pb.Dispatcher.Invoke(DispatcherPriority.Normal,
new Action(() =>
{
if (value < 0.0)
value = 0.0;
if (value > 100.0)
value = 100.0;
pb.Value = value;
pb.Foreground = color;
}));
}
/// <summary>
/// Enable UI elements
/// </summary>
/// <param name="elm"></param>
/// <param name="isEnabled"></param>
/// <param name="isVisible"></param>
private void UiElmEnable(UIElement elm, Boolean isEnabled, Boolean isVisible = true)
{
elm.Dispatcher.Invoke(DispatcherPriority.Normal,
new Action(() =>
{
elm.Visibility = isVisible ? Visibility.Visible : Visibility.Hidden;
elm.IsEnabled = isEnabled;
}));
}
///// <summary>
///// Dispatch GUI messages
///// </summary>
///// <param name="sender"></param>
///// <param name="e"></param>
///// <remarks date="2025-Sep-15" author="Thomas Wiedebusch">
///// - Initial
///// </remarks>
//private void GuiMessageDispatcher_Handler(Object sender, GuiMessageArgs e)
//{
// if (e?.Obj is Int32 obj)
// {
// switch (e.GuiItem)
// {
// case GuiItem.OrderAmount:
// OrderAmount = e.Obj != null ? obj : 0;
// break;
// case GuiItem.OrderCounter:
// OrderCounter = e.Obj != null ? obj : 0;
// break;
// }
// }
//}
/// <summary>
/// Output message to control without access violation.
/// </summary>
/// <returns></returns>
/// <remarks date="????" author="Roland Drabesch">
/// - Initial
/// </remarks>
/// <remarks date="2023-07-11" author="Thomas Wiedebusch">
/// - Introduced multi line text with center alignment.
/// </remarks>
private static void UpdateContentControl(ContentControl ctl, String text)
{
ctl.Dispatcher.Invoke(DispatcherPriority.Normal,
new Action(() =>
{
ctl.Content = new TextBlock()
{
Text = text,
TextWrapping = TextWrapping.Wrap,
TextAlignment = TextAlignment.Center
};
}));
}
/// <summary>
/// Update the elapsed time as information
/// </summary>
/// <returns>information attached to process depending on region</returns>
/// <remarks date="2026-Feb-03" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
private void SetTimeDisplay()
{
var time = DateTimeOffset.UtcNow;
var timeSpan = time - _startTime;
UpdateContentControl(lblTimeValue, $"{(UInt32)timeSpan.TotalHours}:" +
$"{(UInt32)timeSpan.TotalMinutes:00}:" +
$"{timeSpan.Seconds:00}");
}
#endregion FormControls
#region ------------------------------------------- TimerControls ---------------------------------------------
private void tmrProgressUpdate_Tick(Object sender, EventArgs e)
{
Dispatcher.Invoke(() =>
{
if (_autoProgressBar && pbActualProgress.IsVisible)
{
pbActualProgress.Value = pbActualProgress.Value + 4 > 100 ? 0 : pbActualProgress.Value + 4;
}
});
SetTimeDisplay();
}
#endregion ---------------------------------------- TimerControls ---------------------------------------------
#region ------------------------------------------- BoardControls ---------------------------------------------
/// <summary>
/// Clear object lists, initialize and assign all objects:
/// - 10 x FM2014 objects including address assignment based on the panel number,
/// - 10 x FM2014 user controls,
/// - Assign a common receive handler,
/// - Load configuration:
/// - ComPort,
/// - Slot selections,
/// - Individual hardware tolerance selection.
/// </summary>
/// <remarks date="2025-Nov-13..09" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void Init()
{
ActionControl(false);
ClearInfoWindow();
if (_version != null)
{
var version = $"Version: {_version.Major}.{_version.Minor}.{_version.Build}";
Title = $@"FM2014 Test Bench - {version}";
LogText($"FM2014 Test Bench: {version}");
LogText(StrSeparator);
}
_regulationSetupHasChanged = false;
_calibrationSetupHasChanged = false;
// Load the configuration from the local file stored in AppData\FM2014
_fm2014Config.ReadFM2014Config();
// Clear and dispose all FM2014 objects
if (Fm2014s.Count != 0)
{
for (var ctr = 0; ctr < Fm2014s.Count; ctr++)
{
// Remove objects from user control panel
SetNewUserControl(PanelsForUcFM2014[ctr], null);
// Kill FM2014
Fm2014s[ctr].OnRawRecordReceived -= DataReceived_Handler;
}
Fm2014s.Clear();
UcFM2014s.Clear();
FM2014.Dispose();
}
// Assign and build a list of FM2014s and dispatch these to the according user control
// Disable manual input
Dispatcher.Invoke(() =>
{
for (var ctr = 0; ctr < MaxFm2014s; ctr++)
{
var fm2014 = new FM2014
{
Address = ctr + 1
};
fm2014.OnRawRecordReceived += DataReceived_Handler;
Fm2014s.Add(fm2014);
var userControl = new UcFM2014Device(fm2014, StoreFM2014SettingsToConfigFile);
UcFM2014s.Add(userControl);
SetNewUserControl(PanelsForUcFM2014[ctr], UcFM2014s[ctr]);
// Restore previous slot selection
if (_fm2014Config?.SlotIsSelected?[ctr] != null &&
_fm2014Config.SlotIsSelected.Count == MaxFm2014s &&
SlotSelections.Count == MaxFm2014s)
SlotSelections[ctr].IsChecked = _fm2014Config?.SlotIsSelected[ctr] ?? false;
// Restore individual tolerance selection
if (_fm2014Config?.IndividualTolerancePercent?[ctr] != null &&
_fm2014Config.IndividualTolerancePercent.Count == MaxFm2014s)
{
fm2014.CurrentOutputToleranceNominalInt_percent = _fm2014Config?.IndividualTolerancePercent[ctr] ??
FM2014.CURRENT_OUTPUT_TOLERANCE_STANDARD_NOMINAL_INT_percent;
}
}
if (_fm2014Config != null)
{
// Restore baudrate
FM2014.Baudrate = _fm2014Config.Baudrate;
// Restore calibration settings for user convenience
tbxRefPulsesRequired.Text = $"{_fm2014Config.RefPulsesRequired:D}";
tbxDutPulsesRequired.Text = $"{_fm2014Config.DutPulsesRequired:D}";
tbxDoublePulseDeadtime.Text = $"{_fm2014Config.DoublePulseDeadtime:D}";
tbxDutToRefTolMax.Text = $"{_fm2014Config.DutToRefToleranceMax:F2}";
tbxDutToRefTolMin.Text = $"{_fm2014Config.DutToRefToleranceMin:F2}";
// Display on regulation page as common setup for ALL
cbxToleranceCalc.SelectedItem = _fm2014Config.TolerancePercent?.ToString();
}
// Disable and hide dynamic measurement labels
foreach (var measLbl in MeasurementLabels)
UiElmEnable(measLbl, false, false);
// Display actual com-port, if not assigned use the '?'. This will be placed on Items[0]
if (_fm2014Config?.SerialPort != null && !cbxFM2014ComPort.Items.Contains(_fm2014Config.SerialPort))
{
cbxFM2014ComPort.Items.Add(_fm2014Config.SerialPort);
}
else if (_fm2014Config?.SerialPort == null && !cbxFM2014ComPort.Items.Contains("?"))
{
cbxFM2014ComPort.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 => !cbxFM2014ComPort.Items.Contains(comPort)))
{
cbxFM2014ComPort.Items.Add(comPort);
}
cbxFM2014ComPort.Text = cbxFM2014ComPort.Items[_comPortIdx]?.ToString();
// Dispatch multilingual denotation to GUI elements
btnFM2014Connect.Content = Properties.Resources.StrBtnConnect;
// Measurement selection tab setup
tabDutToRefRegulation.Header = Properties.Resources.StrGbxDutToRefRegulation;
tabDutToRefCalibration.Header = Properties.Resources.StrGbxDutToRefCalibration;
tabRefToScaleCalibration.Header = Properties.Resources.StrGbxManualRefCalibration;
// Menu setup
hlpMenu.Header = Properties.Resources.StrLblHelp;
optMenu.Header = Properties.Resources.StrLblOptions;
optMenuLogin.Header = Properties.Resources.StrLblOptionsLogin;
optMenuLogout.Header = Properties.Resources.StrLblOptionsLogout;
optMenuChangePassword.Header = Properties.Resources.StrLblOptionsChangePassword;
portMenu.Content = Properties.Resources.StrLblComPort;
// Static labels for all devices
lblSerialNumberText.Content = Properties.Resources.StrLblFM2014SerialNumber;
lblLifeTimeText.Content = Properties.Resources.StrLblLifetime;
lblSlotSelection.Content = Properties.Resources.StrLblSlotSelection;
// DUT to REF Regulation Setup
lblRefPulsesPerCm.Content = Properties.Resources.StrLblRefPulsesPerCmRatio;
tbxRefPulsesPerCm.Text = "";
lblDutPulsesPerCm.Content = Properties.Resources.StrLblDutPulsesPerCmRatio;
tbxDutPulsesPerCm.Text = "";
lblScaleRefToDut.Content = Properties.Resources.StrLblRefToDutScale;
tbxScaleRefToDut.Text = "";
tbxScaleRefToDut.IsReadOnly = true;
lblDisplayAttenuation.Content = Properties.Resources.StrLblDisplayAttenuation;
btnSaveRegulationSetup.Content = Properties.Resources.StrBtnSaveRegulationSetup;
lblToleranceCalc.Content = Properties.Resources.StrLblFM2014Tolerance;
chkUseDampedTolerance.Content = Properties.Resources.StrChkUseDampedTolerance;
btnDutToRefRegulation.Content = Properties.Resources.StrBtnStartRegulation;
// DUT to REF Calibration
lblRefPulsesRequired.Content = Properties.Resources.StrLblRefPulsesRequired;
lblDutPulsesRequired.Content = Properties.Resources.StrLblDutPulsesRequired;
lblDoublePulseDeadtime.Content = Properties.Resources.StrLblDoublePulseDeadTime;
lblDutToRefTolMax.Content = Properties.Resources.StrLblDutToRefToleranceMax;
lblDutToRefTolMin.Content = Properties.Resources.StrLblDutToRefToleranceMin;
// Manual REF to Volume Calibration
lblRefPulsesMeasured.Content = Properties.Resources.StrLblRefPulsesMeasured;
tbxRefPulsesMeasured.Text = "";
tbxRefPulsesMeasured.IsReadOnly = true;
chkDutPulsesMeasured.Content = Properties.Resources.StrLblDutPulsesMeasured;
tbxDutPulsesMeasured.Text = "";
tbxDutPulsesMeasured.IsReadOnly = true;
lblVolumeMeasuredLiters.Content = Properties.Resources.StrLblVolumeMeasured;
tbxVolumeMeasuredLiters.Text = "";
lblRefPulsesPerCmScale.Content = Properties.Resources.StrLblRefPulsesPerCmRatio;
tbxRefPulsesPerCmCalib.Text = "";
tbxRefPulsesPerCmCalib.IsReadOnly = true;
btnRefToVolumeCalibration.Content = Properties.Resources.StrBtnStartCalibration;
//// Process status
//lblActualProcessLabel.Content = Properties.Resources.StrLblActualProcessStatus;
//lblTotalProcessLabel.Content = Properties.Resources.StrLblOverallProcessStatus;
lblTimeText.Text = Properties.Resources.StrLblTime;
});
}
/// <summary>
/// Store the settings adjusted by the UI or the UcFM2014Device to a configuration file!
/// </summary>
/// <remarks date="2025-Nov-13" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
/// <remarks date="2026-Jan-15" author="Thomas Wiedebusch">
/// - Update FM2014 properties.
/// </remarks>
/// <remarks date="2026-Feb-05" author="Thomas Wiedebusch">
/// - Store individual tolerances and test bench slot selections.
/// </remarks>
/// <remarks date="2026-Feb-10" author="Thomas Wiedebusch">
/// - Store calibration settings for user convenience.
/// </remarks>
private void StoreFM2014SettingsToConfigFile()
{
if (_fm2014Config == null)
return;
Dispatcher.Invoke(() =>
{
_configSetupHasChanged = false;
_calibrationSetupHasChanged = false;
_fm2014Config.IndividualTolerancePercent?.Clear();
_fm2014Config.IndividualTolerancePercent = null;
_fm2014Config.IndividualTolerancePercent = new List<Int32?>();
_fm2014Config.SlotIsSelected?.Clear();
_fm2014Config.SlotIsSelected = null;
_fm2014Config.SlotIsSelected = new List<Boolean?>();
_fm2014Config.SerialPort = cbxFM2014ComPort.Text;
for (var ctr = 0; ctr < MaxFm2014s; ctr++)
{
var fm2014 = Fm2014s[ctr];
if (fm2014 == null)
return;
_fm2014Config.IndividualTolerancePercent.Add(fm2014.CurrentOutputToleranceNominalInt_percent);
_fm2014Config.SlotIsSelected.Add(SlotSelections[ctr].IsChecked);
}
// Restore Calibration settings for user convenience
var firstFm2014 = FM2014.GetFirstConnectedAndLoggedInFm2014();
if (null == firstFm2014)
return;
_fm2014Config.RefPulsesRequired = (UInt16)firstFm2014.RefPulsesRequired;
_fm2014Config.DutPulsesRequired = (UInt16)firstFm2014.DutPulsesRequired;
_fm2014Config.DoublePulseDeadtime = firstFm2014.DoublePulseDeadtime_ms;
_fm2014Config.DutToRefToleranceMax = firstFm2014.DutToRefCalibrationToleranceMax_percent;
_fm2014Config.DutToRefToleranceMin = firstFm2014.DutToRefCalibrationToleranceMin_percent;
});
_fm2014Config.Update();
}
/// <summary>
/// Establish connection to FM2014
/// </summary>
/// <remarks date="2025-Oct-02" author="Thomas Wiedebusch">
/// - Catch error message on unknown data type and kill meter.
/// </remarks>
private void Connect()
{
try
{
if (_configSetupHasChanged || _calibrationSetupHasChanged)
StoreFM2014SettingsToConfigFile();
Init();
if (string.IsNullOrEmpty(cbxFM2014ComPort?.SelectedItem?.ToString()) ||
cbxFM2014ComPort.SelectedItem.ToString().Equals("?"))
{
var text = Properties.Resources.StrErrorMsgComPortNotAssigned;
//MessageBox.Show(text, Properties.Resources.StrError, MessageBoxButtons.OK, MessageBoxIcon.Error);
LogText(text);
return;
}
_autoProgressBar = true;
ActionControl(true);
ResetCancellationToken();
FM2014.SharedCancellationToken = _cancellationToken;
UpdateContentControl(lblActualProcessText, Properties.Resources.StrMsgConnecting);
var comPort = cbxFM2014ComPort.SelectedItem.ToString();
Task.Factory.StartNew(() =>
{
for (var ctr = 0; ctr < MaxFm2014s; ctr++)
{
var ucFm2014Device = (UcFM2014Device)UcFM2014s[ctr];
var fm2014 = Fm2014s[ctr];
// Connect only to selected slots
var slotShallBeUsed = false;
var ctr1 = ctr;
SlotSelections[ctr].Dispatcher.Invoke(() =>
{
var isChecked = SlotSelections[ctr1].IsChecked;
if (isChecked != null)
slotShallBeUsed = (Boolean)isChecked;
});
if (!slotShallBeUsed || !fm2014.Connect(comPort))
{
ucFm2014Device.SetConnectionStatus(ConnectionStatusName.OFFLINE);
continue;
}
ucFm2014Device.SetConnectionStatus(ConnectionStatusName.ONLINE);
if (!fm2014.IsLoggedOn)
{
LogErrorText(Properties.Resources.StrErrorMsgFM2014AccessDenied);
continue;
}
if (fm2014.IsLoggedOn)
{
// Logging to history window
LogText(StrSeparator);
LogText("FM2014 ID: " + fm2014.ConnectResponse);
LogText("FM2014 Firmware Version: " + fm2014.FwVersion);
LogText($"FM2014 {Properties.Resources.StrLblFM2014SerialNumber} {fm2014.SerialNumber}");
LogText($"FW2014 {Properties.Resources.StrLblFM2014Address} {fm2014.Address}");
var dt = DateTime.UtcNow;
var msg = $"{dt:yyyy-MM-dd HH:mm:ss} UTC";
LogText($"{Properties.Resources.StrMsgPcDateTime} {msg}");
LogText(StrSeparator);
}
}
}, _cancellationToken).ContinueWith(delegate
{
ActionControl(false);
}, _cancellationToken);
}
catch (Exception ex)
{
ActionControl(false);
LogErrorText(ex.Message);
}
}
#endregion ---------------------------------------- BoardControls ---------------------------------------------
#region ------------------------------------------- ProcessControls -------------------------------------------
/// <summary>
/// Common method to (de-)activate controls and timer.
/// </summary>
/// <remarks date="2026-Jan-14..Feb-14" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void ActionControl(Boolean isActive)
{
if (isActive)
{
UiElmEnable(lblStatusProcessInfo, true);
UiElmEnable(lblActualProcessText, true);
//UiElmEnable(lblTotalProcessText, true);
//UiElmEnable(lblActualProcessLabel, true);
//UiElmEnable(lblTotalProcessLabel, true);
UiElmEnable(lblActualProgressPercent, !_autoProgressBar, !_autoProgressBar);
//UiElmEnable(lblTotalProgressPercent, true);
UiElmEnable(pbActualProgress, true);
//UiElmEnable(pbTotalProgress, true);
SetFM2014AccessLocked();
Dispatcher.Invoke(() =>
{
pbActualProgress.Value = 0.0;
_tmrProgressUpdate.IsEnabled = true;
});
}
else
{
_autoProgressBar = false;
Dispatcher.Invoke(() => _tmrProgressUpdate.IsEnabled = false);
UiElmEnable(lblStatusProcessInfo, false, false);
UiElmEnable(lblActualProcessText, false, false);
//UiElmEnable(lblTotalProcessText, false, false);
//UiElmEnable(lblActualProcessLabel, false, false);
//UiElmEnable(lblTotalProcessLabel, false, false);
UiElmEnable(lblActualProgressPercent, false, false);
//UiElmEnable(lblTotalProgressPercent, false, false);
UiElmEnable(pbActualProgress, false, false);
//UiElmEnable(pbTotalProgress, false, false);
// Check if any FM2014 is connected and logged in to enable access buttons
if (FM2014.RegisteredFm2014s == null || FM2014.RegisteredFm2014s.Count == 0 ||
FM2014.RegisteredFm2014s.All(fm2014 => !fm2014.IsLoggedOn))
{
SetFM2014AccessLocked();
UiElmEnable(btnFM2014Connect, true);
UiElmEnable(cbxFM2014ComPort, true);
foreach (var slotSelector in SlotSelections)
{
UiElmEnable(slotSelector, true);
}
return;
}
SetFM2014AccessEnabled();
}
}
/// <summary>
/// Common method to prepare for changed regulation setup.
/// </summary>
/// <remarks date="2026-Jan-14..20" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void DutToRefRegulationSetupHasChanged()
{
_regulationSetupHasChanged = true;
Dispatcher.Invoke(() =>
{
btnSaveRegulationSetup.IsEnabled = true;
btnDutToRefRegulation.IsEnabled = true;
tbxRefPulsesPerCm.Background = ColorStandardInputField;
tbxDutPulsesPerCm.Background = ColorStandardInputField;
tbxScaleRefToDut.Background = ColorStandardDisplayField;
tbxVolumeMeasuredLiters.Background = ColorStandardInputField;
tbxRefPulsesPerCmCalib.Background = ColorStandardDisplayField;
});
}
/// <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 firstFm2014 = FM2014.GetFirstConnectedAndLoggedInFm2014();
if (null == firstFm2014)
return false;
UpdateTextBox(tbxScaleRefToDut, $@"{firstFm2014.RefToDutScale_norm:F4}");
// Display error if scale doesn't fit
var tempRefToDutScale_norm = (Double)firstFm2014.RefPulses_per_cm / firstFm2014.DutPulses_per_cm;
if (tempRefToDutScale_norm < FM2014.REF_TO_DUT_SCALE_MIN ||
tempRefToDutScale_norm > FM2014.REF_TO_DUT_SCALE_MAX ||
firstFm2014.RefToDutScale_norm < FM2014.REF_TO_DUT_SCALE_MIN ||
firstFm2014.RefToDutScale_norm > FM2014.REF_TO_DUT_SCALE_MAX)
{
Dispatcher.Invoke(() =>
{
tbxScaleRefToDut.Background = ColorProcessFailed;
tbxScaleRefToDut.Text = Properties.Resources.StrError;
btnSaveRegulationSetup.IsEnabled = false;
btnDutToRefRegulation.IsEnabled = false;
});
return false;
}
Dispatcher.Invoke(() =>
{
tbxScaleRefToDut.Background = ColorStandardDisplayField;
});
return true;
}
/// <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 info window.
/// </summary>
/// <remarks date="2026-Feb-02" author="Thomas Wiedebusch">
/// - Initial
/// </remarks>
private void ClearInfoWindow()
{
rtbLog.Dispatcher.Invoke(() => rtbLog.Document.Blocks.Clear());
}
/// <summary>
/// Output exclusively to user update remarks text window.
/// </summary>
/// <remarks date="2021-Apr-26" author="Thomas Wiedebusch">
/// - Color added.
/// </remarks>
/// <remarks date="2023-Aug-15" author="Thomas Wiedebusch">
/// - File output added.
/// </remarks>
private void LogText(String txtHistory, String filename = "")
{
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 message)
{
InfoWindowColoredText(message, ColorSuccess);
}
/// <summary>
/// Output exclusively to user update remarks text window.
/// </summary>
private void InfoWindowColoredText(String message, Brush color)
{
rtbLog.Dispatcher.Invoke(DispatcherPriority.Normal,
new Action(() =>
{
try
{
// remove unacceptable spacing between the lines
rtbLog.Document.LineHeight = 1;
color = color ?? Brushes.Black;
if (!string.IsNullOrEmpty(message) && !message.Equals(_lastLoggingTextToAvoidRepetition))
{
var tr = new TextRange(rtbLog.Document.ContentEnd, rtbLog.Document.ContentEnd)
{
Text = message
};
tr.ApplyPropertyValue(TextElement.ForegroundProperty, color);
rtbLog.ScrollToEnd();
rtbLog.AppendText(Environment.NewLine);
}
_lastLoggingTextToAvoidRepetition = message;
}
catch (FormatException)
{
// forget it
}
}
));
}
#endregion ---------------------------------------- InfoWindow ------------------------------------------------
#region ------------------------------------------- ActivationControls ----------------------------------------
/// <summary>
/// Lock all buttons
/// </summary>
private void SetFM2014AccessLocked()
{
// Disable all buttons
UiElmEnable(btnDutToRefCalibration, false);
UiElmEnable(btnSaveRegulationSetup, false);
UiElmEnable(btnDutToRefRegulation, false);
UiElmEnable(btnFM2014Connect, false);
UiElmEnable(btnRefToVolumeCalibration, false);
// Disable manual input
Dispatcher.Invoke(() =>
{
tbxVolumeMeasuredLiters.IsReadOnly = true;
tbxRefPulsesPerCm.IsReadOnly = true;
tbxDutPulsesPerCm.IsReadOnly = true;
});
UiElmEnable(tbxRefPulsesRequired, false);
UiElmEnable(tbxDutPulsesRequired, false);
UiElmEnable(tbxDoublePulseDeadtime, false);
UiElmEnable(tbxDutToRefTolMax, false);
UiElmEnable(tbxDutToRefTolMin, false);
UiElmEnable(tbxRefPulsesPerCm, false);
UiElmEnable(tbxDutPulsesPerCm, false);
UiElmEnable(tbxScaleRefToDut, false); // Is permanent read only
UiElmEnable(cbxDisplayAttenuation, false);
UiElmEnable(cbxToleranceCalc, false);
UiElmEnable(cbxFM2014ComPort, false);
UiElmEnable(tbxRefPulsesMeasured, false); // Is permanent read only
UiElmEnable(chkDutPulsesMeasured, false);
UiElmEnable(tbxDutPulsesMeasured, false); // Is permanent read only
UiElmEnable(tbxVolumeMeasuredLiters, false);
UiElmEnable(tbxRefPulsesPerCmCalib, false); // Is permanent read only
foreach (var slotSelector in SlotSelections)
{
UiElmEnable(slotSelector, false);
}
}
/// <summary>
/// Enable all buttons, FM2014 has to be connected
/// </summary>
private void SetFM2014AccessEnabled()
{
// Enable buttons and display correct information
UiElmEnable(btnDutToRefCalibration, true);
UiElmEnable(btnSaveRegulationSetup, _regulationSetupHasChanged); // Activate if setup has changed
UiElmEnable(btnDutToRefRegulation, true);
UiElmEnable(btnFM2014Connect, true);
UiElmEnable(btnRefToVolumeCalibration, true);
// Prepare test output to start as it is used for strat and stop
UpdateContentControl(btnRefToVolumeCalibration, Properties.Resources.StrBtnStartCalibration);
UpdateContentControl(btnDutToRefCalibration, Properties.Resources.StrBtnStartCalibration);
UpdateContentControl(btnDutToRefRegulation, Properties.Resources.StrBtnStartRegulation);
var dutPulseMeasurementIsActive = false;
// Enable manual input
Dispatcher.Invoke(() =>
{
if (chkDutPulsesMeasured.IsChecked != null)
dutPulseMeasurementIsActive = (Boolean)chkDutPulsesMeasured.IsChecked;
tbxVolumeMeasuredLiters.IsReadOnly = false;
tbxRefPulsesPerCm.IsReadOnly = false;
tbxDutPulsesPerCm.IsReadOnly = false;
});
//// Enable setting selections
UiElmEnable(tbxRefPulsesRequired, true);
UiElmEnable(tbxDutPulsesRequired, true);
UiElmEnable(tbxDoublePulseDeadtime, true);
UiElmEnable(tbxDutToRefTolMax, true);
UiElmEnable(tbxDutToRefTolMin, true);
UiElmEnable(tbxRefPulsesPerCm, true);
UiElmEnable(tbxDutPulsesPerCm, true);
UiElmEnable(tbxScaleRefToDut, true); // Is permanent read only
UiElmEnable(cbxDisplayAttenuation, true);
UiElmEnable(cbxToleranceCalc, true);
UiElmEnable(cbxFM2014ComPort, true);
UiElmEnable(tbxRefPulsesMeasured, true); // Is permanent read only
UiElmEnable(chkDutPulsesMeasured, true);
UiElmEnable(tbxDutPulsesMeasured, dutPulseMeasurementIsActive); // Is permanent read only
UiElmEnable(tbxVolumeMeasuredLiters, true);
UiElmEnable(tbxRefPulsesPerCmCalib, true); // Is permanent read only
foreach (var slotSelector in SlotSelections)
{
UiElmEnable(slotSelector, true);
}
}
#endregion ---------------------------------------- ActivationControls ----------------------------------------
#region ------------------------------------------- Event handler ---------------------------------------------
/// <summary>
/// Feedback from FM2014being parsed to GUI
/// </summary>
/// <remarks date="2026-Feb-02..14" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void DataReceived_Handler(Object sender, ProcessExecEventArgs e)
{
try
{
// Remind the FM2014 user control based on the address of the FM2014 to dispatch received message to the
// correct device
UcFM2014Device ucFm2014;
if (sender is FM2014 fm2014)
{
var idx = fm2014.Address - 1;
ucFm2014 = (UcFM2014Device)UcFM2014s[idx];
}
else return;
var firstFm2014 = FM2014.GetFirstConnectedAndLoggedInFm2014();
if (e.ActualProcessMessage != null)
{
UpdateContentControl(lblActualProcessText, e.ActualProcessMessage);
}
if (e.ActualProcessPercent != null && !_autoProgressBar)
{
UpdateProgressBar(pbActualProgress, (Double)e.ActualProcessPercent);
}
// A circulating process bar will be used to observe an activity, but as it restarts at the beginning
// after reaching the end, a value isn't of importance
if (!_autoProgressBar)
Dispatcher.Invoke(() => lblActualProgressPercent.Content = $@"{pbActualProgress.Value:##0.0} %");
SetTimeDisplay();
// Data dispatcher
var resp = (CmdResponse)e.SpecificInfoObj;
String valueStr;
// Used as marker for error, null means measurement is in range or status or health is good
Brush statusColor = null;
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)
{
if (e.StatusReturn == StatusReturn.MeasurementOutOfRange)
statusColor = ColorProcessFailed;
LogText($"{resp.AnswerStr}: {resp.IntValue:D} {resp.Unit}");
switch (resp.CmdName)
{
case CmdName.CMD_REF_GET_PLS_RMN:
ucFm2014.SetValue(CAL_LBL_IDX_REF_RMN_PLS, $"{resp.IntValue:D}", statusColor);
break;
case CmdName.CMD_DUT_GET_PLS_RMN:
ucFm2014.SetValue(CAL_LBL_IDX_DUT_RMN_PLS, $"{resp.IntValue:D}", statusColor);
break;
case CmdName.CMD_REF_GET_PLS_CTR:
case CmdName.CMD_REF_GET_PLS_CTR_BU:
if (firstFm2014 != null && firstFm2014.Address == fm2014.Address)
UpdateTextBox(tbxRefPulsesMeasured, $"{resp.IntValue:D}", statusColor);
ucFm2014.SetValue(CTR_LBL_IDX_REF_PLS, $"{resp.IntValue:D}", statusColor);
break;
case CmdName.CMD_DUT_GET_PLS_CTR:
case CmdName.CMD_DUT_GET_PLS_CTR_BU:
if (firstFm2014 != null && firstFm2014.Address == fm2014.Address)
UpdateTextBox(tbxDutPulsesMeasured, $"{resp.IntValue:D}", statusColor);
ucFm2014.SetValue(CTR_LBL_IDX_DUT_PLS, $"{resp.IntValue:D}", statusColor);
break;
case CmdName.CMD_REF_LPP_SCALE:
UpdateTextBox(tbxRefPulsesPerCm, $"{resp.IntValue:D}", statusColor);
break;
case CmdName.CMD_DUT_LPP_SCALE:
UpdateTextBox(tbxDutPulsesPerCm, $"{resp.IntValue:D}", statusColor);
break;
case CmdName.CMD_MEAS_SET_ATTN:
Dispatcher.Invoke(() => cbxDisplayAttenuation.SelectedItem = $"{resp.IntValue}");
break;
case CmdName.CMD_GET_REF_FREQU:
valueStr = $"{resp.IntValue:D}";
ucFm2014.SetValue(REG_LBL_IDX_REF_FREQU, valueStr, statusColor);
break;
case CmdName.CMD_RST_MEAS:
// Immediately lock all buttons and input fields until reset is finished
SetFM2014AccessLocked();
UiElmEnable(lblActualProgressPercent, true);
// Check countdown of reset method
if (resp.IntValue == 0)
ActionControl(false);
break;
}
}
else if (resp.DoubleValue != null)
{
if (e.StatusReturn == StatusReturn.MeasurementOutOfRange)
statusColor = ColorProcessFailed;
LogText($"{resp.AnswerStr}: {resp.DoubleValue:F2} {resp.Unit}");
switch (resp.CmdName)
{
case CmdName.CMD_REF_GET_TMR:
valueStr = $"{resp.DoubleValue:F3}";
ucFm2014.SetValue(CAL_LBL_IDX_REF_MEAS_TMR, valueStr, statusColor);
break;
case CmdName.CMD_DUT_GET_TMR:
valueStr = $"{resp.DoubleValue:F3}";
ucFm2014.SetValue(CAL_LBL_IDX_DUT_MEAS_TMR, valueStr, statusColor);
break;
case CmdName.CMD_CAL_DUT_TO_REF_TOL:
valueStr = $"{resp.DoubleValue:F3}";
ucFm2014.SetValue(CAL_LBL_IDX_DUT_TO_REF_TOL, valueStr, statusColor);
break;
case CmdName.CMD_GET_UDTLC:
case CmdName.CMD_GET_DTLC:
valueStr = $"{resp.DoubleValue:F2}";
ucFm2014.SetValue(REG_LBL_IDX_DUT_TO_REF_TOL, valueStr, statusColor);
break;
case CmdName.CMD_REF_SET_SCALE:
UpdateTextBox(tbxScaleRefToDut, $"{resp.DoubleValue:F4}", statusColor);
break;
// // DEBUG
// case FM2014CmdDef.CmdName.CMD_GET_REF_PERIOD:
// tbxRefPeriodMs.Text = $"{resp.DoubleValue:F3}";
// break;
// case FM2014CmdDef.CmdName.CMD_GET_DUT_PERIOD:
// tbxDutPeriodMs.Text = $"{resp.DoubleValue:F3}";
// break;
// case FM2014CmdDef.CmdName.CMD_CAL_FREQU_REF_PERIOD:
// tbxRefFrequencyFromRefPeriodHz.Text = $"{resp.DoubleValue:F3}";
// break;
// case FM2014CmdDef.CmdName.CMD_CAL_FREQU_DUT_PERIOD:
// tbxDutFrequencyFromDutPeriodHz.Text = $"{resp.DoubleValue:F3}";
// break;
case CmdName.CMD_CAL_FLOW_REF_FREQU:
valueStr = $"{resp.DoubleValue:F3}";
ucFm2014.SetValue(REG_LBL_IDX_FLOW_RATE, valueStr, statusColor);
//UpdateTextBox(tbxRefFlowRateFromRefFrequencyCmPerH, valueStr);
// TODO THW Check if the actual flow shall be taken based on 'REF Frequency'
//UpdateTextBox(tbxActualFlowRateCmPerHour, valueStr);
break;
// case FM2014CmdDef.CmdName.CMD_CAL_FLOW_REF_PERIOD:
// tbxRefFlowRateFromRefPeriodCmPerH.Text = $"{resp.DoubleValue:F3}";
// break;
// case FM2014CmdDef.CmdName.CMD_CAL_FLOW_DUT_PERIOD:
// tbxDutFlowRateFromDutPeriodCmPerH.Text = $"{resp.DoubleValue:F3}";
// break;
}
}
}
catch (Exception)
{
//
}
}
#endregion ---------------------------------------- Event handler ---------------------------------------------
#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 calibration measurement.
/// </summary>
/// <remarks date="2026-Feb-07..11" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void btnDutToRefCalibration_Click(Object sender, EventArgs e)
{
_startTime = DateTimeOffset.UtcNow;
if (FM2014.SharedCyclicMeasSequ == CyclicMeasSequ.IDLE)
{
// Disable and hide dynamic measurement labels
foreach (var measLbl in MeasurementLabels)
UiElmEnable(measLbl, false, false);
// Set the measurement labels
UpdateContentControl(MeasurementLabels[CAL_LBL_IDX_REF_REQ_PLS], Properties.Resources.StrLblRefPulsesRequired);
UpdateContentControl(MeasurementLabels[CAL_LBL_IDX_REF_RMN_PLS], Properties.Resources.StrLblRefPulsesRemaining);
UpdateContentControl(MeasurementLabels[CAL_LBL_IDX_DUT_REQ_PLS], Properties.Resources.StrLblDutPulsesRequired);
UpdateContentControl(MeasurementLabels[CAL_LBL_IDX_DUT_RMN_PLS], Properties.Resources.StrLblDutPulsesRemaining);
UpdateContentControl(MeasurementLabels[CAL_LBL_IDX_REF_MEAS_TMR], Properties.Resources.StrLblRefTimeMeasured);
UpdateContentControl(MeasurementLabels[CAL_LBL_IDX_DUT_MEAS_TMR], Properties.Resources.StrLblDutTimeMeasured);
UpdateContentControl(MeasurementLabels[CAL_LBL_IDX_DUT_TO_REF_TOL], Properties.Resources.StrLblDutToRefToleranceMeasured);
UiElmEnable(MeasurementLabels[CAL_LBL_IDX_REF_REQ_PLS], true);
UiElmEnable(MeasurementLabels[CAL_LBL_IDX_REF_RMN_PLS], true);
UiElmEnable(MeasurementLabels[CAL_LBL_IDX_DUT_REQ_PLS], true);
UiElmEnable(MeasurementLabels[CAL_LBL_IDX_DUT_RMN_PLS], true);
UiElmEnable(MeasurementLabels[CAL_LBL_IDX_REF_MEAS_TMR], true);
UiElmEnable(MeasurementLabels[CAL_LBL_IDX_DUT_MEAS_TMR], true);
UiElmEnable(MeasurementLabels[CAL_LBL_IDX_DUT_TO_REF_TOL], true);
if (FM2014.RegisteredFm2014s == null || FM2014.RegisteredFm2014s.Count == 0)
return;
// Take the settings from the inputs
UInt16? refPulsesRequired = null;
UInt16? dutPulsesRequired = null;
UInt16 doublePulseDeadtime_ms = 0;
Double toleranceThresholdMax_percent = 1.0;
Double toleranceThresholdMin_percent = -0.7;
Dispatcher.Invoke(() =>
{
if (int.TryParse(tbxRefPulsesRequired.Text, out var pulses) &&
FM2014.PULSES_TIME_MEASUREMENT_INPUT_MIN <= pulses &&
FM2014.PULSES_TIME_MEASUREMENT_INPUT_MAX >= pulses)
{
refPulsesRequired = (UInt16)pulses;
}
if (int.TryParse(tbxDutPulsesRequired.Text, out pulses) &&
FM2014.PULSES_TIME_MEASUREMENT_INPUT_MIN <= pulses &&
FM2014.PULSES_TIME_MEASUREMENT_INPUT_MAX >= pulses)
{
dutPulsesRequired = (UInt16)pulses;
}
if (int.TryParse(tbxDoublePulseDeadtime.Text, out var deadtime_ms) &&
FM2014.DOUBLE_PULSE_DEADTIME_MIN_ms <= deadtime_ms &&
FM2014.DOUBLE_PULSE_DEADTIME_MAX_ms >= deadtime_ms)
{
doublePulseDeadtime_ms = (UInt16)deadtime_ms;
}
var englishNumberFormat = tbxDutToRefTolMax.Text.Replace(',', '.');
if (double.TryParse(englishNumberFormat, NumberStyles.Any, new CultureInfo("en"), out var tolerance))
{
toleranceThresholdMax_percent = tolerance;
}
englishNumberFormat = tbxDutToRefTolMin.Text.Replace(',', '.');
if (double.TryParse(englishNumberFormat, NumberStyles.Any, new CultureInfo("en"), out tolerance))
{
toleranceThresholdMin_percent = tolerance;
}
});
// Adjust the user control
foreach (var fm2014 in FM2014.RegisteredFm2014s.Where(x => x.IsLoggedOn))
{
var idx = fm2014.Address - 1;
var ucFM2014 = (UcFM2014Device)UcFM2014s[idx];
ucFM2014.EnableMeasurements(CAL_MEASURE_COUNT);
// Set initial required pulses as those are static upon the entire measurement
ucFM2014.SetValue(CAL_LBL_IDX_REF_REQ_PLS, refPulsesRequired != null ? $"{refPulsesRequired}" : "---");
ucFM2014.SetValue(CAL_LBL_IDX_DUT_REQ_PLS, dutPulsesRequired != null ? $"{dutPulsesRequired}" : "---");
// Set thresholds for tolerance verification
fm2014.DutToRefCalibrationToleranceMax_percent = toleranceThresholdMax_percent;
fm2014.DutToRefCalibrationToleranceMin_percent = toleranceThresholdMin_percent;
}
if (FM2014.PulseTimeMeasurement(refPulsesRequired, dutPulsesRequired, doublePulseDeadtime_ms))
{
// Backup if meanwhile anything has changed
if (_configSetupHasChanged || _calibrationSetupHasChanged)
StoreFM2014SettingsToConfigFile();
_autoProgressBar = false;
ActionControl(true);
UpdateContentControl(btnDutToRefCalibration, Properties.Resources.StrBtnStopCalibration);
UiElmEnable(btnDutToRefCalibration, true);
}
}
else
{
// Deactivate the button temporary to avoid repeated execution as the reset takes a certain time
UiElmEnable(btnDutToRefCalibration, false);
FM2014.ResetHardwareAllDevices();
}
}
/// <summary>
/// Start and stop the DUT to REF regulation measurement.
/// </summary>
/// <remarks date="2026-Jan-14..Feb-11" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void btnDutToRefRegulation_Click(Object sender, EventArgs e)
{
_startTime = DateTimeOffset.UtcNow;
if (FM2014.SharedCyclicMeasSequ == CyclicMeasSequ.IDLE)
{
// Disable and hide dynamic measurement labels
foreach (var measLbl in MeasurementLabels)
UiElmEnable(measLbl, false, false);
// Set the measurement labels
UpdateContentControl(MeasurementLabels[REG_LBL_IDX_REF_FREQU], Properties.Resources.StrLblRefFrequencyHz);
UpdateContentControl(MeasurementLabels[REG_LBL_IDX_FLOW_RATE], Properties.Resources.StrLblFlowRateMeasured);
UpdateContentControl(MeasurementLabels[REG_LBL_IDX_DUT_TO_REF_TOL], Properties.Resources.StrLblDutToRefToleranceMeasured);
UiElmEnable(MeasurementLabels[REG_LBL_IDX_REF_FREQU], true);
UiElmEnable(MeasurementLabels[REG_LBL_IDX_FLOW_RATE], true);
UiElmEnable(MeasurementLabels[REG_LBL_IDX_DUT_TO_REF_TOL], true);
if (FM2014.RegisteredFm2014s == null || FM2014.RegisteredFm2014s.Count == 0)
return;
// Take the settings from the inputs
UInt32 refPulses_per_cm = 1000;
UInt16 dutPulses_per_cm = 100;
UInt16 doublePulseDeadtime_ms = 0;
Byte currentOutputAttenuation = 1;
Int32 toleranceSetup = 1;
Dispatcher.Invoke(() =>
{
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)
{
refPulses_per_cm = (UInt32)pulses_per_cm;
}
if (int.TryParse(tbxDutPulsesPerCm.Text, out pulses_per_cm) &&
FM2014.PULSES_PER_CM_REGULATION_SETUP_MIN <= pulses_per_cm &&
FM2014.PULSES_PER_CM_REGULATION_SETUP_MAX >= pulses_per_cm)
{
dutPulses_per_cm = (UInt16)pulses_per_cm;
}
if (int.TryParse(tbxDoublePulseDeadtime.Text, out var deadtime_ms) &&
FM2014.DOUBLE_PULSE_DEADTIME_MIN_ms <= deadtime_ms &&
FM2014.DOUBLE_PULSE_DEADTIME_MAX_ms >= deadtime_ms)
{
doublePulseDeadtime_ms = (UInt16)deadtime_ms;
}
if (byte.TryParse(cbxDisplayAttenuation.SelectedItem.ToString(), out var attenuation) &&
attenuation >= FM2014.CURRENT_OUTPUT_ATTENUATION_MIN &&
attenuation <= FM2014.CURRENT_OUTPUT_ATTENUATION_MAX)
{
currentOutputAttenuation = attenuation;
}
if (int.TryParse(cbxToleranceCalc.SelectedItem.ToString(), out var tolerance) &&
(tolerance == FM2014.CURRENT_OUTPUT_TOLERANCE_STANDARD_NOMINAL_INT_percent ||
tolerance == FM2014.CURRENT_OUTPUT_TOLERANCE_EXTENDED_VALUE_INT_percent))
{
toleranceSetup = tolerance;
}
});
// Adjust the user control
foreach (var fm2014 in FM2014.RegisteredFm2014s.Where(x => x.IsLoggedOn))
{
var idx = fm2014.Address - 1;
var ucFM2014 = (UcFM2014Device)UcFM2014s[idx];
ucFM2014.EnableMeasurements(REG_MEASURE_COUNT);
fm2014.CurrentOutputToleranceNominalInt_percent = toleranceSetup;
}
if (FM2014.RegulationMeasurement(refPulses_per_cm, dutPulses_per_cm, doublePulseDeadtime_ms,
currentOutputAttenuation))
{
_autoProgressBar = true;
ActionControl(true);
UpdateContentControl(btnDutToRefRegulation, Properties.Resources.StrBtnStopRegulation);
UiElmEnable(btnDutToRefRegulation, true);
}
}
else
{
// Deactivate the button temporary to avoid repeated execution as the reset takes a certain time
UiElmEnable(btnDutToRefRegulation, false);
_autoProgressBar = false;
FM2014.ResetHardwareAllDevices();
}
}
/// <summary>
/// Start and stop the manual REF calibration measurement.
/// </summary>
/// <remarks date="2026-Jan-04..Feb-11" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void btnRefToVolumeCalibration_Click(Object sender, EventArgs e)
{
_startTime = DateTimeOffset.UtcNow;
if (FM2014.SharedCyclicMeasSequ == CyclicMeasSequ.IDLE)
{
// Disable and hide dynamic measurement labels
foreach (var measLbl in MeasurementLabels)
UiElmEnable(measLbl, false, false);
// Set the measurement labels
UpdateContentControl(MeasurementLabels[CTR_LBL_IDX_REF_PLS], Properties.Resources.StrLblRefPulsesMeasured);
UpdateContentControl(MeasurementLabels[CTR_LBL_IDX_DUT_PLS], Properties.Resources.StrLblDutPulsesMeasured);
UiElmEnable(MeasurementLabels[CTR_LBL_IDX_REF_PLS], true);
UiElmEnable(MeasurementLabels[CTR_LBL_IDX_DUT_PLS], true);
UpdateTextBox(tbxRefPulsesMeasured, "");
UpdateTextBox(tbxDutPulsesMeasured, "");
UpdateTextBox(tbxVolumeMeasuredLiters, "");
UpdateTextBox(tbxRefPulsesPerCmCalib, "");
if (FM2014.RegisteredFm2014s == null || FM2014.RegisteredFm2014s.Count == 0)
return;
var measureDutPulses = false;
Dispatcher.Invoke(() => measureDutPulses = chkDutPulsesMeasured.IsChecked ?? false);
// Adjust the user control
foreach (var fm2014 in FM2014.RegisteredFm2014s.Where(x => x.IsLoggedOn))
{
var idx = fm2014.Address - 1;
var ucFM2014 = (UcFM2014Device)UcFM2014s[idx];
ucFM2014.EnableMeasurements(CTR_MEASURE_COUNT);
}
if (FM2014.PulseCounterMeasurement(true, measureDutPulses))
{
_autoProgressBar = true;
ActionControl(true);
UpdateContentControl(btnRefToVolumeCalibration, Properties.Resources.StrBtnStopCalibration);
UiElmEnable(btnRefToVolumeCalibration, true);
}
}
else
{
// Deactivate the button temporary to avoid repeated execution as the reset takes a certain time
UiElmEnable(btnRefToVolumeCalibration, false);
_autoProgressBar = false;
FM2014.ResetHardwareAllDevices();
}
}
/// <summary>
/// Select next control on enter key pressed
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Feb-09" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void cbxToleranceCalc_KeyDown(Object sender, KeyEventArgs e)
{
// Catch the enter key, recalculate all settings
if (e.Key == Key.Enter || e.Key == Key.Tab)
{
Keyboard.Focus(btnSaveRegulationSetup);
}
}
/// <summary>
/// FM2014 tolerance setup for calculation of it item has changed:
/// - This will dispatch the new value to all FM2014s.
/// </summary>
/// <remarks date="2026-Feb-09" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void cbxToleranceCalc_SelectedIndexChanged(Object sender, EventArgs e)
{
var firstFm2014 = FM2014.GetFirstConnectedAndLoggedInFm2014();
if (null == firstFm2014)
return;
Dispatcher.Invoke(() =>
{
if (byte.TryParse(cbxToleranceCalc.SelectedItem.ToString(), out var tolerance) &&
(tolerance == FM2014.CURRENT_OUTPUT_TOLERANCE_STANDARD_NOMINAL_INT_percent ||
tolerance == FM2014.CURRENT_OUTPUT_TOLERANCE_EXTENDED_VALUE_INT_percent) &&
firstFm2014.CurrentOutputToleranceNominalInt_percent != tolerance)
{
_regulationSetupHasChanged = true;
btnSaveRegulationSetup.IsEnabled = true;
}
});
}
/// <summary>
/// Select next control on enter key pressed
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Jan-24" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void cbxAttenuation_KeyDown(Object sender, KeyEventArgs e)
{
// Catch the enter key, recalculate all settings
if (e.Key == Key.Enter || e.Key == Key.Tab)
{
Keyboard.Focus(cbxToleranceCalc);
}
}
/// <summary>
/// FM2014 current attenuation item has changed:
/// - This will dispatch the new value to all FM2014s.
/// </summary>
/// <remarks date="2026-Jan-18" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void cbxAttenuation_SelectedIndexChanged(Object sender, EventArgs e)
{
var firstFm2014 = FM2014.GetFirstConnectedAndLoggedInFm2014();
if (null == firstFm2014)
return;
Dispatcher.Invoke(() =>
{
if (byte.TryParse(cbxDisplayAttenuation.SelectedItem.ToString(), out var attenuation) &&
attenuation >= FM2014.CURRENT_OUTPUT_ATTENUATION_MIN &&
attenuation <= FM2014.CURRENT_OUTPUT_ATTENUATION_MAX &&
firstFm2014.CurrentOutputAttenuation != attenuation)
{
_regulationSetupHasChanged = true;
btnSaveRegulationSetup.IsEnabled = true;
}
});
}
/// <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 cbxFM2014BaseSettings_SelectedValueChanged(Object sender, EventArgs e)
{
if (_comPortIdx != cbxFM2014ComPort.SelectedIndex)
{
_comPortIdx = cbxFM2014ComPort.SelectedIndex;
StoreFM2014SettingsToConfigFile();
}
}
/// <summary>
/// Safe standalone measurement setup:
/// - This will save the setup in all FM2014s.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Jan-18" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void btnSaveRegulationSetup_Click(Object sender, EventArgs e)
{
if (FM2014.StoreAllConfigurations())
{
_regulationSetupHasChanged = false;
UiElmEnable(btnSaveRegulationSetup, false);
}
}
/// <summary>
/// Switch between damped or undamped tolerance display. This can be changed during the
/// 'Regulation Measurement' on the fly as it will just collect a different dataset and
/// not change anything on the measurement setup:
/// - This will dispatch the new value to all FM2014s.
/// </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)
{
Dispatcher.Invoke(DispatcherPriority.Normal,
new Action(() =>
{
if (FM2014.RegisteredFm2014s == null || FM2014.RegisteredFm2014s.Count <= 0)
return;
foreach (var fm2014 in FM2014.RegisteredFm2014s)
{
if (fm2014 != null && chkUseDampedTolerance.IsChecked != null)
fm2014.UseDampedTolerance = (Boolean)chkUseDampedTolerance.IsChecked;
}
}));
}
/// <summary>
/// Enable/Disable input
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void chkDutPulseMeasurement_CheckChanged(Object sender, EventArgs e)
{
Dispatcher.Invoke(() =>
{
var isChecked = false;
if (chkDutPulsesMeasured.IsChecked != null)
isChecked = (Boolean)chkDutPulsesMeasured.IsChecked;
UiElmEnable(tbxDutPulsesMeasured, isChecked);
});
}
/// <summary>
/// Common check for key input to edit an integer field:
/// - Allows Left, Right, Back, Delete and 0-9 keys . and ,
/// </summary>
/// <param name="keyCode"></param>
/// <returns>true an allowed key-code</returns>
/// <remarks date="2026-Feb-09" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private static Boolean MaskEditDoubleInput(Key keyCode)
{
return keyCode == Key.Back ||
keyCode == Key.Left ||
keyCode == Key.Right ||
keyCode == Key.Delete ||
keyCode == Key.Decimal ||
keyCode == Key.OemComma ||
keyCode == Key.OemPeriod ||
keyCode == Key.OemMinus ||
keyCode == Key.OemPlus ||
Regex.IsMatch($"{keyCode}", @"[0-9]");
}
/// <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-Feb-06" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private static Boolean MaskEditIntegerInput(Key keyCode)
{
return keyCode == Key.Back ||
keyCode == Key.Left ||
keyCode == Key.Right ||
keyCode == Key.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-Feb-09" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void tbxRefPulsesRequired_KeyDown(Object sender, KeyEventArgs e)
{
// Catch the enter key, recalculate all settings
if (e.Key == Key.Enter || e.Key == Key.Tab)
{
tbxRefPulsesRequired_LostFocus(this, null);
Keyboard.Focus(tbxDutPulsesRequired);
TextBoxSelectAll(tbxDutPulsesRequired);
}
else if (!MaskEditIntegerInput(e.Key))
{
e.Handled = true;
}
}
/// <summary>
/// Changed test to cover the delete key which is not in the key-down event!?
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Feb-09" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void tbxRefPulsesRequired_TextChanged(Object sender, TextChangedEventArgs e)
{
// 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
{
Dispatcher.Invoke(() => tbxRefPulsesRequired_LostFocus(this, null));
}, _cancellationToken);
}
/// <summary>
/// After manual input of the required REF pulses for the calibration measurement:
/// - This will dispatch the new value to all FM2014s.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Feb-09" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void tbxRefPulsesRequired_LostFocus(Object sender, EventArgs e)
{
var firstFm2014 = FM2014.GetFirstConnectedAndLoggedInFm2014();
if (null == firstFm2014)
return;
Dispatcher.Invoke(() =>
{
if (string.IsNullOrEmpty(tbxRefPulsesRequired.Text))
{
tbxRefPulsesRequired.Background = ColorStandardInputField;
return;
}
// Check for change
var backupPulsesRequired = firstFm2014.RefPulsesRequired;
// Convert the input and check limits
if (int.TryParse(tbxRefPulsesRequired.Text, out var pulses) &&
FM2014.PULSES_TIME_MEASUREMENT_INPUT_MIN <= pulses &&
FM2014.PULSES_TIME_MEASUREMENT_INPUT_MAX >= pulses)
{
if (backupPulsesRequired != pulses)
_calibrationSetupHasChanged = true;
tbxRefPulsesRequired.Background = ColorStandardInputField;
}
else
{
tbxRefPulsesRequired.Background = ColorProcessFailed;
}
});
}
/// <summary>
/// Redirect keystroke 'Enter' to leaving the cell event
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Feb-09" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void tbxDutPulsesRequired_KeyDown(Object sender, KeyEventArgs e)
{
// Catch the enter key, recalculate all settings
if (e.Key == Key.Enter || e.Key == Key.Tab)
{
tbxDutPulsesRequired_LostFocus(this, null);
Keyboard.Focus(tbxDoublePulseDeadtime);
TextBoxSelectAll(tbxDoublePulseDeadtime);
}
else if (!MaskEditIntegerInput(e.Key))
{
e.Handled = true;
}
}
/// <summary>
/// Select all text in text box
/// </summary>
/// <param name="textBox"></param>
private void TextBoxSelectAll(TextBox textBox)
{
Dispatcher.Invoke(textBox.SelectAll);
}
/// <summary>
/// Changed test to cover the delete key which is not in the key-down event!?
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Feb-09" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void tbxDutPulsesRequired_TextChanged(Object sender, TextChangedEventArgs e)
{
// 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
{
Dispatcher.Invoke(() => tbxDutPulsesRequired_LostFocus(this, null));
}, _cancellationToken);
}
/// <summary>
/// After manual input of the required DUT pulses for the calibration measurement:
/// - This will dispatch the new value to all FM2014s.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Feb-09" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void tbxDutPulsesRequired_LostFocus(Object sender, EventArgs e)
{
var firstFm2014 = FM2014.GetFirstConnectedAndLoggedInFm2014();
if (null == firstFm2014)
return;
Dispatcher.Invoke(() =>
{
if (string.IsNullOrEmpty(tbxDutPulsesRequired.Text))
{
tbxDutPulsesRequired.Background = ColorProcessFailed;
return;
}
// Check for change
var backupPulsesRequired = firstFm2014.DutPulsesRequired;
// Convert the input and check limits
if (int.TryParse(tbxDutPulsesRequired.Text, out var pulses) &&
FM2014.PULSES_TIME_MEASUREMENT_INPUT_MIN <= pulses &&
FM2014.PULSES_TIME_MEASUREMENT_INPUT_MAX >= pulses)
{
if (backupPulsesRequired != pulses)
_calibrationSetupHasChanged = true;
tbxDutPulsesRequired.Background = ColorStandardInputField;
}
else
{
tbxDutPulsesRequired.Background = ColorProcessFailed;
}
});
}
/// <summary>
/// Redirect keystroke 'Enter' to leaving the cell event
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Feb-09" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void tbxDoublePulseDeadtime_KeyDown(Object sender, KeyEventArgs e)
{
// Catch the enter key, recalculate all settings
if (e.Key == Key.Enter || e.Key == Key.Tab)
{
tbxDoublePulseDeadtime_LostFocus(this, null);
Keyboard.Focus(tbxDutToRefTolMax);
TextBoxSelectAll(tbxDutToRefTolMax);
}
else if (!MaskEditIntegerInput(e.Key))
{
e.Handled = true;
}
}
/// <summary>
/// Changed test to cover the delete key which is not in the key-down event!?
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Feb-09" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void tbxDoublePulseDeadtime_TextChanged(Object sender, TextChangedEventArgs e)
{
// 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
{
Dispatcher.Invoke(() => tbxDoublePulseDeadtime_LostFocus(this, null));
}, _cancellationToken);
}
/// <summary>
/// After manual input of the double pulse detection deadtime:
/// - This will dispatch the new value to all FM2014s.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Feb-09" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void tbxDoublePulseDeadtime_LostFocus(Object sender, EventArgs e)
{
var firstFm2014 = FM2014.GetFirstConnectedAndLoggedInFm2014();
if (null == firstFm2014)
return;
Dispatcher.Invoke(() =>
{
if (string.IsNullOrEmpty(tbxDoublePulseDeadtime.Text))
{
tbxDoublePulseDeadtime.Background = ColorProcessFailed;
return;
}
// Check for change
var backupDoublePulseDeadtime = firstFm2014.DoublePulseDeadtime_ms;
// Convert the input and check limits
if (int.TryParse(tbxDoublePulseDeadtime.Text, out var deadtime) &&
FM2014.DOUBLE_PULSE_DEADTIME_MIN_ms <= deadtime &&
FM2014.DOUBLE_PULSE_DEADTIME_MAX_ms >= deadtime)
{
if (backupDoublePulseDeadtime != deadtime)
_calibrationSetupHasChanged = true;
tbxDoublePulseDeadtime.Background = ColorStandardInputField;
}
else
{
tbxDoublePulseDeadtime.Background = ColorProcessFailed;
}
});
}
/// <summary>
/// Redirect keystroke 'Enter' to leaving the cell event
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Feb-09" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void tbxDutToRefToleranceMax_KeyDown(Object sender, KeyEventArgs e)
{
// Catch the enter key and tab key as indicator for leaving the cell
if (e.Key == Key.Enter || e.Key == Key.Tab)
{
tbxDutToRefToleranceMax_LostFocus(this, null);
Keyboard.Focus(tbxDutToRefTolMin);
TextBoxSelectAll(tbxDutToRefTolMin);
}
else if (!MaskEditDoubleInput(e.Key))
{
e.Handled = true;
}
}
/// <summary>
/// Changed test to cover the delete key which is not in the key-down event!?
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Feb-06" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void tbxDutToRefToleranceMax_TextChanged(Object sender, TextChangedEventArgs e)
{
// 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
{
Dispatcher.Invoke(() => tbxDutToRefToleranceMax_LostFocus(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':
/// - This will dispatch the new value to all FM2014s.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Feb-09" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void tbxDutToRefToleranceMax_LostFocus(Object sender, EventArgs e)
{
var firstFm2014 = FM2014.GetFirstConnectedAndLoggedInFm2014();
if (null == firstFm2014)
return;
Dispatcher.Invoke(() =>
{
if (string.IsNullOrEmpty(tbxDutToRefTolMax.Text))
{
tbxDutToRefTolMax.Background = ColorProcessFailed;
return;
}
// Check for change
var backupDutToRefTolerance = firstFm2014.DutToRefCalibrationToleranceMax_percent;
var englishNumberFormat = tbxDutToRefTolMax.Text.Replace(',', '.');
if (double.TryParse(englishNumberFormat, NumberStyles.Any, new CultureInfo("en"), out var tolerance))
{
if (Math.Abs(backupDutToRefTolerance - tolerance) > 0.05)
_calibrationSetupHasChanged = true;
tbxDutToRefTolMax.Background = ColorStandardInputField;
// Dispatch settings to all connected FM2014!!!!
foreach (var fm2014 in FM2014.RegisteredFm2014s)
{
fm2014.DutToRefCalibrationToleranceMax_percent = tolerance;
}
}
else
{
tbxDutToRefTolMax.Background = ColorProcessFailed;
}
});
}
/// <summary>
/// Redirect keystroke 'Enter' to leaving the cell event
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Feb-09" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void tbxDutToRefToleranceMin_KeyDown(Object sender, KeyEventArgs e)
{
// Catch the enter key, recalculate all settings
if (e.Key == Key.Enter || e.Key == Key.Tab)
{
tbxDutToRefToleranceMin_LostFocus(this, null);
Keyboard.Focus(btnDutToRefCalibration);
}
else if (!MaskEditDoubleInput(e.Key))
{
e.Handled = true;
}
}
/// <summary>
/// Changed test to cover the delete key which is not in the key-down event!?
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Feb-06" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void tbxDutToRefToleranceMin_TextChanged(Object sender, TextChangedEventArgs e)
{
// 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
{
Dispatcher.Invoke(() => tbxDutToRefToleranceMin_LostFocus(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':
/// - This will dispatch the new value to all FM2014s.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Feb-09" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void tbxDutToRefToleranceMin_LostFocus(Object sender, EventArgs e)
{
var firstFm2014 = FM2014.GetFirstConnectedAndLoggedInFm2014();
if (null == firstFm2014)
return;
Dispatcher.Invoke(() =>
{
if (string.IsNullOrEmpty(tbxDutToRefTolMin.Text))
{
tbxDutToRefTolMin.Background = ColorProcessFailed;
return;
}
// Check for change
var backupDutToRefTolerance = firstFm2014.DutToRefCalibrationToleranceMin_percent;
var englishNumberFormat = tbxDutToRefTolMin.Text.Replace(',', '.');
if (double.TryParse(englishNumberFormat, NumberStyles.Any, new CultureInfo("en"), out var tolerance))
{
if (Math.Abs(backupDutToRefTolerance - tolerance) > 0.05)
_calibrationSetupHasChanged = true;
tbxDutToRefTolMin.Background = ColorStandardInputField;
// Dispatch settings to all connected FM2014!!!!
foreach (var fm2014 in FM2014.RegisteredFm2014s)
{
fm2014.DutToRefCalibrationToleranceMin_percent = tolerance;
}
}
else
{
tbxDutToRefTolMin.Background = ColorProcessFailed;
}
});
}
/// <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.Key == Key.Enter || e.Key == Key.Tab)
{
tbxRefPulsesPerCm_LostFocus(this, null);
Keyboard.Focus(tbxDutPulsesPerCm);
TextBoxSelectAll(tbxDutPulsesPerCm);
}
else if (!MaskEditIntegerInput(e.Key))
{
e.Handled = true;
}
}
/// <summary>
/// Changed test to cover the delete key which is not in the key-down event!?
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Feb-06" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void tbxRefPulsesPerCm_TextChanged(Object sender, TextChangedEventArgs e)
{
// 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
{
Dispatcher.Invoke(() => tbxRefPulsesPerCm_LostFocus(this, null));
}, _cancellationToken);
}
/// <summary>
/// After manual input of the REF pulses per cubic meter the scale REF to DUT has to be recalculated:
/// - This will dispatch the new value to all FM2014s.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Jan-14..24" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void tbxRefPulsesPerCm_LostFocus(Object sender, EventArgs e)
{
var firstFm2014 = FM2014.GetFirstConnectedAndLoggedInFm2014();
if (null == firstFm2014)
return;
Dispatcher.Invoke(() =>
{
if (string.IsNullOrEmpty(tbxRefPulsesPerCm.Text))
{
tbxRefPulsesPerCm.Background = ColorProcessFailed;
btnSaveRegulationSetup.IsEnabled = false;
btnDutToRefRegulation.IsEnabled = false;
return;
}
//Backup the actual setting to detect changes
var backupPulses_per_cm = firstFm2014.RefPulses_per_cm;
// 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
foreach (var fm2014 in FM2014.RegisteredFm2014s)
{
fm2014.RefPulses_per_cm = (UInt32)pulses_per_cm;
}
if (!UpdateRefToDutScale())
{
tbxRefPulsesPerCm.Background = ColorProcessFailed;
return;
}
// Check if values have changed and need to be updated in the standalone setup
if (backupPulses_per_cm != pulses_per_cm || tbxRefPulsesPerCm.Background == ColorProcessFailed)
{
DutToRefRegulationSetupHasChanged();
}
}
else
{
tbxRefPulsesPerCm.Background = ColorProcessFailed;
}
});
}
/// <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.Key == Key.Enter || e.Key == Key.Tab)
{
tbxDutPulsesPerCm_LostFocus(this, null);
Keyboard.Focus(cbxDisplayAttenuation);
}
else if (!MaskEditIntegerInput(e.Key))
{
e.Handled = true;
}
}
/// <summary>
/// Changed test to cover the delete key which is not in the key-down event!?
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Feb-06" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void tbxDutPulsesPerCm_TextChanged(Object sender, TextChangedEventArgs e)
{
// 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
{
Dispatcher.Invoke(() => tbxDutPulsesPerCm_LostFocus(this, null));
}, _cancellationToken);
}
/// <summary>
/// After manual input of the DUT pulses per cubic meter the scale REF to DUT has to be recalculated:
/// - This will dispatch the new value to all FM2014s.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Jan-14..24" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void tbxDutPulsesPerCm_LostFocus(Object sender, EventArgs e)
{
var firstFm2014 = FM2014.GetFirstConnectedAndLoggedInFm2014();
if (null == firstFm2014)
return;
Dispatcher.Invoke(() =>
{
if (string.IsNullOrEmpty(tbxDutPulsesPerCm.Text))
{
tbxDutPulsesPerCm.Background = ColorProcessFailed;
btnSaveRegulationSetup.IsEnabled = false;
btnDutToRefRegulation.IsEnabled = false;
return;
}
//Backup the actual setting to detect changes
var backupPulses_per_cm = firstFm2014.DutPulses_per_cm;
// 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
foreach (var fm2014 in FM2014.RegisteredFm2014s)
{
fm2014.DutPulses_per_cm = (UInt16)pulses_per_cm;
}
if (!UpdateRefToDutScale())
{
tbxDutPulsesPerCm.Background = ColorProcessFailed;
return;
}
// Check if values have changed and need to be updated in the standalone setup
if (backupPulses_per_cm != pulses_per_cm || tbxDutPulsesPerCm.Background == ColorProcessFailed)
{
DutToRefRegulationSetupHasChanged();
}
}
else
{
tbxDutPulsesPerCm.Background = ColorProcessFailed;
}
});
}
/// <summary>
/// Redirect keystroke 'Enter' to leaving the cell event
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Feb-09" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void tbxVolumeMeasuredLiters_KeyDown(Object sender, KeyEventArgs e)
{
// Catch the enter key, recalculate all settings
if (e.Key == Key.Enter || e.Key == Key.Tab)
{
tbxVolumeMeasuredLiters_LostFocus(this, null);
Keyboard.Focus(tbxDutToRefTolMax);
TextBoxSelectAll(tbxDutToRefTolMax);
}
else if (!MaskEditIntegerInput(e.Key))
{
e.Handled = true;
}
}
/// <summary>
/// Changed test to cover the delete key which is not in the key-down event!?
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Feb-06" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void tbxVolumeMeasuredLiters_TextChanged(Object sender, TextChangedEventArgs e)
{
// 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
{
Dispatcher.Invoke(() => tbxVolumeMeasuredLiters_LostFocus(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':
/// - This will dispatch the new value to all FM2014s.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2026-Feb-09..10" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void tbxVolumeMeasuredLiters_LostFocus(Object sender, EventArgs e)
{
var firstFm2014 = FM2014.GetFirstConnectedAndLoggedInFm2014();
if (null == firstFm2014)
return;
Dispatcher.Invoke(() =>
{
// If REF pulses have been acquired the volume field has to contain a value
if (!string.IsNullOrEmpty(tbxRefPulsesMeasured.Text) &&
string.IsNullOrEmpty(tbxVolumeMeasuredLiters.Text))
{
tbxVolumeMeasuredLiters.Background = ColorProcessFailed;
return;
}
// Backup the old value
var backup_pulses_per_cm = firstFm2014.DutPulses_per_cm;
// 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
firstFm2014.RefPulses_per_cm = pulses_per_cm;
// If this succeeded, then update the new manual calibrated value
if (pulses_per_cm == firstFm2014.RefPulses_per_cm)
{
tbxRefPulsesPerCm.Text = $@"{firstFm2014.RefPulses_per_cm:D}";
tbxRefPulsesPerCm.Background = ColorStandardDisplayField;
tbxRefPulsesPerCmCalib.Text = $@"{firstFm2014.RefPulses_per_cm:D}";
tbxRefPulsesPerCmCalib.Background = ColorStandardDisplayField;
tbxVolumeMeasuredLiters.Background = ColorStandardInputField;
if (!UpdateRefToDutScale())
{
tbxVolumeMeasuredLiters.Background = ColorProcessFailed;
tbxRefPulsesPerCmCalib.Background = ColorProcessFailed;
return;
}
// Check if values have changed and need to be updated in the standalone setup
if (backup_pulses_per_cm != firstFm2014.RefPulses_per_cm)
{
DutToRefRegulationSetupHasChanged();
}
}
else
{
tbxVolumeMeasuredLiters.Background = ColorProcessFailed;
tbxRefPulsesPerCmCalib.Background = ColorProcessFailed;
tbxRefPulsesPerCm.Text = Properties.Resources.StrError;
}
}
});
}
private void chkAnySlot_Click(Object sender, RoutedEventArgs e)
{
_configSetupHasChanged = true;
}
/// <summary>
/// Access to help menu
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void HlpMenu_Click(Object sender, RoutedEventArgs e)
{
}
#endregion ---------------------------------------- Buttons and Controls --------------------------------------
}
}