laatzen/MiniPrf/Ui/FrmMainMiniPrf.cs
2026-01-30 09:15:58 +01:00

1416 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'
*
*=======================================================================================\n
* @copyright
*
*_______________________________________________________________________________________\n
* Copyright (c) 2025..2026 SENSUS GmbH.\n
* All Rights Reserved.\n
* \n
* Confidential property of\n
* SENSUS GmbH,\n
* Meineckestr. 10, 30880 LAATZEN, GERMANY\n
* \n
*=======================================================================================\n
* @authors
*
*_______________________________________________________________________________________\n
* Thomas Wiedebusch\n
*
*
*=======================================================================================\n
* @version
*
*_______________________________________________________________________________________\n
* V1.00 13-Nov-2025..22-Dec-2025 by Thomas Wiedebusch\n
* - Initial.
*
*
*=======================================================================================\n
*/
//#define TEST_MULTIPLE_FM2014
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.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
// DEBUG to test multiple objects of FM2014
// cancellation token
private CancellationTokenSource _cancellationTokenSource;
private CancellationToken _cancellationToken;
/// <summary>
/// The single FM2014 for this program
/// </summary>
private FM2014 Fm2014 { get; set; }
#if TEST_MULTIPLE_FM2014
//TODO THW TEST FM2014 multiple objects
/// <summary>
/// The single FM2014 for this program
/// </summary>
private FM2014 Fm2014_TEST { get; set; }
#endif
/// <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 _toleranceIdx;
// 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;
#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 = cbxFM2014ComPort.BackColor;
_cancellationTokenSource = new CancellationTokenSource();
_cancellationToken = _cancellationTokenSource.Token;
}
/// <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;
_regulationSetupHasChanged = false;
grpBoxDebug.Visible = false;
gbxDutToRefRegulation.Visible = true;
gbxRegulationSetup.Visible = true;
// 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?.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();
var itemContent = _fm2014Config?.Address ?? 1;
for (var idx = 0; idx < cbxFM2014Address.MaxDropDownItems; idx++)
{
if (!cbxFM2014Address.Items[idx].ToString().Equals(itemContent.ToString())) continue;
_addressIdx = idx;
cbxFM2014Address.SelectedItem = cbxFM2014Address.Items[idx];
break;
}
itemContent = _fm2014Config?.TolerancePercent ?? 3;
for (var idx = 0; idx < cbxFM2014TolerancePercent.MaxDropDownItems; idx++)
{
if (!cbxFM2014TolerancePercent.Items[idx].ToString().Equals(itemContent.ToString())) continue;
_toleranceIdx = idx;
cbxFM2014TolerancePercent.SelectedItem = cbxFM2014TolerancePercent.Items[idx];
break;
}
// 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;
tbxRefPulsePerCm.Text = "";
lblDutPulsePerVolume.Text = Resources.StrLblDutPulsesPerCm;
tbxDutPulsePerCm.Text = "";
lblScaleRefToDut.Text = Resources.StrLblScaleRefToDut;
tbxScaleRefToDut.Text = "";
lblAttenuation.Text = Resources.StrLblAttenuation;
btnSaveRegulationSetup.Text = Resources.StrBtnSaveRegulationSetup;
// Manual REF Calibration group box
gbxManualRefCalibration.Text = Resources.StrGbxManualRefCalibration;
lblMeasuredRefPulses.Text = Resources.StrLblMeasuredRefPulses;
tbxMeasuredRefPulses.Text = "";
lblMeasuredDutPulses.Text = Resources.StrLblMeasuredDutPulses;
tbxMeasuredDutPulses.Text = "";
lblWeightScaleVolume.Text = Resources.StrLblMeasuredWeightScaleVolume;
tbxManualInputVolumeLiters.Text = "";
lblPulseVolumeCmRelation.Text = Resources.StrLblRefPulsesPerCm;
tbxRefCalibrationResultPulsePerCm.Text = "";
btnManualRefCalibration.Text = Resources.StrBtnStartCalibration;
// DUT to REF Regulation group box
gbxDutToRefRegulation.Text = Resources.StrGbxDutToRefRegulation;
lblActualFlowRateCmPerHour.Text = Resources.StrLblActualMeasuredFlowRate;
tbxActualFlowRateCmPerHour.Text = "";
lblActualMeasuredTolerance.Text = Resources.StrLblActualMeasuredToleranceDutToRef;
tbxActualMeasuredToleranceDutToRef.Text = "";
chkUseDampedTolerance.Text = Resources.StrChkUseDampedTolerance;
btnDutToRefRegulation.Text = Resources.StrBtnStartRegulation;
tbxRefFrequencyHz.Text = "";
// 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" author="Thomas Wiedebusch">
/// - Update FM2014 properties.
/// </remarks>
private void StoreFM2014Settings()
{
_fm2014Config.SerialPort = cbxFM2014ComPort.Text;
if (int.TryParse(cbxFM2014Address.SelectedItem.ToString(), out var address))
{
_fm2014Config.Address = address;
if (Fm2014 != null)
Fm2014.Address = address;
}
if (int.TryParse(cbxFM2014TolerancePercent.SelectedItem.ToString(), out var tolerancePercent)
&& (tolerancePercent == 3 || tolerancePercent == 5))
{
_fm2014Config.TolerancePercent = tolerancePercent;
if (Fm2014 != null)
Fm2014.Tolerance_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
btnSaveRegulationSetup.Enabled = false;
btnManualRefCalibration.Enabled = false;
btnDutToRefRegulation.Enabled = false;
btnFM2014Connect.Enabled = false;
// Disable manual input
tbxManualInputVolumeLiters.ReadOnly = true;
tbxRefPulsePerCm.ReadOnly = true;
tbxDutPulsePerCm.ReadOnly = true;
// Disable setting selections
cbxAttenuation.Enabled = false;
cbxFM2014Address.Enabled = false;
cbxFM2014ComPort.Enabled = false;
cbxFM2014TolerancePercent.Enabled = false;
}
/// <summary>
/// Enable all buttons, FM2014 has to be connected
/// </summary>
private void SetFM2014AccessEnabled()
{
// Enable save setup depending on changed properties
btnSaveRegulationSetup.Enabled = _regulationSetupHasChanged;
// 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
tbxManualInputVolumeLiters.ReadOnly = false;
tbxRefPulsePerCm.ReadOnly = false;
tbxDutPulsePerCm.ReadOnly = false;
// Enable setting selections
cbxAttenuation.Enabled = true;
cbxFM2014Address.Enabled = true;
cbxFM2014ComPort.Enabled = true;
cbxFM2014TolerancePercent.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;
cbxFM2014ComPort.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
{
Init();
if (string.IsNullOrEmpty(cbxFM2014ComPort?.SelectedItem?.ToString()) ||
cbxFM2014ComPort.SelectedItem.ToString().Equals("?"))
{
var text = Resources.StrErrorMsgComPortNotAssigned;
MessageBox.Show(text, Resources.StrError, MessageBoxButtons.OK, MessageBoxIcon.Error);
LogText(text);
return;
}
StoreFM2014Settings();
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(cbxFM2014TolerancePercent.SelectedItem.ToString(), out var tolerancePercent)
&& (tolerancePercent == 3 || tolerancePercent == 5))
{
Fm2014.Tolerance_percent = tolerancePercent;
}
else
{
ActionControl(false);
LogErrorText(Resources.StrErrorMsgFM2014ToleranceInvalid);
}
lblActualProcess.Text = Resources.StrMsgConnecting;
Fm2014.Connect(cbxFM2014ComPort.SelectedItem.ToString());
#if TEST_MULTIPLE_FM2014
//TODO THW TEST FM2014 multiple objects
Fm2014_TEST = new FM2014
{
Address = 2
};
Fm2014_TEST.OnRawRecordReceived += DataReceived_Handler_TEST;
Fm2014_TEST.Connect(cbxFM2014ComPort.SelectedItem.ToString());
#endif
Task.Factory.StartNew(() =>
{
Fm2014.Login();
#if TEST_MULTIPLE_FM2014
//TODO THW TEST FM2014 multiple objects
Fm2014_TEST.Login();
#endif
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}");
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 SetupHasChanged()
{
_regulationSetupHasChanged = true;
btnSaveRegulationSetup.Enabled = true;
tbxRefFrequencyHz.Text = "";
tbxRefFrequencyHz.BackColor = ColorStandardDisplayField;
tbxActualFlowRateCmPerHour.Text = "";
tbxActualFlowRateCmPerHour.BackColor = ColorStandardDisplayField;
tbxActualMeasuredToleranceDutToRef.Text = "";
tbxActualMeasuredToleranceDutToRef.BackColor = ColorStandardDisplayField;
tbxRefPulsePerCm.BackColor = ColorStandardInputField;
tbxDutPulsePerCm.BackColor = ColorStandardInputField;
tbxScaleRefToDut.BackColor = ColorStandardDisplayField;
tbxManualInputVolumeLiters.BackColor = ColorStandardInputField;
tbxRefCalibrationResultPulsePerCm.BackColor = 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()
{
tbxScaleRefToDut.Text = $@"{Fm2014.RefToDutScale_norm:F4}";
// Display error if scale doesn't fit
var tempRefToDutScale_norm = (Double)Fm2014.Ref_pulse_per_cm / Fm2014.Dut_pulse_per_cm;
if (tempRefToDutScale_norm < FM2014.RefToDutScaleMin ||
tempRefToDutScale_norm > FM2014.RefToDutScaleMax ||
Fm2014.RefToDutScale_norm < FM2014.RefToDutScaleMin ||
Fm2014.RefToDutScale_norm > FM2014.RefToDutScaleMax)
{
tbxScaleRefToDut.BackColor = ColorProcessFailed;
tbxScaleRefToDut.Text = Resources.StrError;
btnSaveRegulationSetup.Enabled = false;
return false;
}
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 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="2023-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 Tools
/// <summary>
/// Set the actual process and log the text.
/// </summary>
/// <remarks date="2023-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="2023-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="2023-Jan-14" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void btnDutToRefRegulation_Click(Object sender, EventArgs e)
{
_startTime = DateTimeOffset.UtcNow;
if (FM2014.SharedCyclicMeasSequ == FM2014CmdDef.CyclicMeasSequ.IDLE)
{
ActionControl(true);
_autoProgressBar = true;
if (Fm2014.RegulationMeasurement())
{
btnDutToRefRegulation.Text = Resources.StrBtnStopRegulation;
btnDutToRefRegulation.Enabled = true;
}
}
else
{
_autoProgressBar = false;
// Deactivate the button temporary to avoid repeated execution as the reset takes a certain time
btnDutToRefRegulation.Enabled = false;
Fm2014.ResetMeasurement();
ActionControl(false);
btnDutToRefRegulation.Text = Resources.StrBtnStartRegulation;
}
}
/// <summary>
/// Start and stop the manual REF calibration measurement.
/// </summary>
/// <remarks date="2023-Jan-04..12" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void btnManualRefCalibration_Click(Object sender, EventArgs e)
{
_startTime = DateTimeOffset.UtcNow;
if (FM2014.SharedCyclicMeasSequ == FM2014CmdDef.CyclicMeasSequ.IDLE)
{
ActionControl(true);
tbxMeasuredRefPulses.Text = "";
tbxMeasuredDutPulses.Text = "";
tbxManualInputVolumeLiters.Text = "";
tbxRefCalibrationResultPulsePerCm.Text = "";
tbxActualFlowRateCmPerHour.Text = "";
tbxActualMeasuredToleranceDutToRef.Text = "";
tbxRefFrequencyHz.Text = "";
_backupMeasuredRefPulsesStr = "";
_backupRefCalibrationResultPulsePerCmStr = "";
_backupWeightScaleVolumeLitersStr = "";
_autoProgressBar = true;
if (Fm2014.PulseCounterMeasurement(true, true))
{
btnManualRefCalibration.Text = Resources.StrBtnStopCalibration;
btnManualRefCalibration.Enabled = true;
}
#if TEST_MULTIPLE_FM2014
//TODO THW TEST FM2014 multiple objects
Fm2014_TEST.PulseCounterMeasurement(false, true);
#endif
}
else
{
_autoProgressBar = false;
// Deactivate the button temporary to avoid repeated execution as the reset takes a certain time
btnManualRefCalibration.Enabled = false;
Fm2014.ResetMeasurement();
ActionControl(false);
btnManualRefCalibration.Text = Resources.StrBtnStartCalibration;
}
}
/// <summary>
/// Attenuation item has changed.
/// </summary>
/// <remarks date="2023-Jan-18" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void cbxAttenuation_SelectedIndexChanged(Object sender, EventArgs e)
{
if (Fm2014 != null)
{
if (byte.TryParse(cbxAttenuation.SelectedItem.ToString(), out var attenuation) &&
attenuation > 0 && attenuation <= 9 && Fm2014.Attenuation != attenuation)
{
Fm2014.Attenuation = attenuation;
_regulationSetupHasChanged = true;
btnSaveRegulationSetup.Enabled = 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="2023-Jan-04" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void cbxFM2014BaseSettings_SelectedValueChanged(Object sender, EventArgs e)
{
if (_toleranceIdx != cbxFM2014TolerancePercent.SelectedIndex ||
_addressIdx != cbxFM2014Address.SelectedIndex ||
_comPortIdx != cbxFM2014ComPort.SelectedIndex)
{
_toleranceIdx = cbxFM2014TolerancePercent.SelectedIndex;
_addressIdx = cbxFM2014Address.SelectedIndex;
_comPortIdx = cbxFM2014ComPort.SelectedIndex;
StoreFM2014Settings();
}
}
/// <summary>
/// Safe standalone measurement setup.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2023-Jan-18" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void btnSaveRegulationSetup_Click(Object sender, EventArgs e)
{
if (Fm2014 != null && Fm2014.SaveStandAloneMeasurement())
{
_regulationSetupHasChanged = false;
btnSaveRegulationSetup.Enabled = false;
}
}
/// <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="2023-Jan-14" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void chkUseDampedTolerance_CheckedChanged(Object sender, EventArgs e)
{
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="2023-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="2023-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(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(() =>
{
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="2023-Jan-14..24" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void tbxRefPulsesPerCm_Leave(Object sender, EventArgs e)
{
if (Fm2014 == null)
return;
if (string.IsNullOrEmpty(tbxRefPulsePerCm.Text))
{
tbxRefPulsePerCm.Text = $@"{Fm2014.Ref_pulse_per_cm:D}";
}
// Setup FM2014
if (int.TryParse(tbxRefPulsePerCm.Text, out var pulses_per_cm))
{
//Backup the actual setting to detect changes
var backupPulses_per_cm = Fm2014.Ref_pulse_per_cm;
Fm2014.Ref_pulse_per_cm = (UInt32)pulses_per_cm;
// Output the Fm2014 setting to avoid wrong display of invalid ranges as this will be
// limited during the setup of the FM2014 property!
tbxRefPulsePerCm.Text = $@"{Fm2014.Ref_pulse_per_cm:D}";
// During setup of the 'Ref_pulse_per_cm' the 'RefToDutScaleStr' will be generated
if (!UpdateRefToDutScale())
{
tbxRefPulsePerCm.BackColor = ColorProcessFailed;
return;
}
// Check if values have changed and need to be updated in the standalone setup
if (backupPulses_per_cm != Fm2014.Ref_pulse_per_cm)
{
SetupHasChanged();
}
// SPECIAL BACKUP AND RESTORE FOR REF CALIBRATION
// If the REF pulses per cubic meter have been changed manually, the 'Manual REF Calibration'
// fields have to be cleared if those are different. A backup to restore it may be useful.
if (!tbxRefPulsePerCm.Text.Equals(tbxRefCalibrationResultPulsePerCm.Text) &&
!string.IsNullOrEmpty(tbxRefCalibrationResultPulsePerCm.Text))
{
_backupMeasuredRefPulsesStr = tbxMeasuredRefPulses.Text;
tbxMeasuredRefPulses.Text = "";
_backupRefCalibrationResultPulsePerCmStr = tbxRefCalibrationResultPulsePerCm.Text;
tbxRefCalibrationResultPulsePerCm.Text = "";
_backupWeightScaleVolumeLitersStr = tbxManualInputVolumeLiters.Text;
tbxManualInputVolumeLiters.Text = "";
}
// Restore values if REF pulses per cubic meters is back to the calibrated one
else if (tbxRefPulsePerCm.Text.Equals(_backupRefCalibrationResultPulsePerCmStr))
{
tbxMeasuredRefPulses.Text = _backupMeasuredRefPulsesStr;
tbxRefCalibrationResultPulsePerCm.Text = _backupRefCalibrationResultPulsePerCmStr;
tbxManualInputVolumeLiters.Text = _backupWeightScaleVolumeLitersStr;
}
}
}
/// <summary>
/// Redirect keystroke 'Enter' to leaving the cell event
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2023-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="2023-Jan-14..24" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void tbxDutPulsesPerCm_Leave(Object sender, EventArgs e)
{
if (Fm2014 == null)
return;
if (string.IsNullOrEmpty(tbxDutPulsePerCm.Text))
{
tbxDutPulsePerCm.Text = $@"{Fm2014.Dut_pulse_per_cm:D}";
}
// Setup FM2014
if (int.TryParse(tbxDutPulsePerCm.Text, out var pulses_per_cm))
{
//Backup the actual setting to detect changes
var backupPulses_per_cm = Fm2014.Dut_pulse_per_cm;
// Try to set the new calculated REF pulses limited by the property setter
Fm2014.Dut_pulse_per_cm = (UInt32)pulses_per_cm;
// Output the Fm2014 setting to avoid wrong display of invalid ranges as this will be
// limited during the setup of the FM2014 property!
tbxDutPulsePerCm.Text = $@"{Fm2014.Dut_pulse_per_cm:D}";
// During setup of the 'Dut_pulse_per_cm' the 'RefToDutScaleStr' will be generated
if (!UpdateRefToDutScale())
{
tbxDutPulsePerCm.BackColor = ColorProcessFailed;
return;
}
// Check if values have changed and need to be updated in the standalone setup
if (backupPulses_per_cm != Fm2014.Dut_pulse_per_cm)
{
SetupHasChanged();
}
}
}
/// <summary>
/// Redirect keystroke 'Enter' to leaving the cell event
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks date="2023-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="2023-Jan-15..24" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
private void tbxManualInputVolumeLiters_Leave(Object sender, EventArgs e)
{
if (Fm2014 == null)
return;
if (string.IsNullOrEmpty(tbxRefPulsePerCm.Text))
{
tbxManualInputVolumeLiters.Text = @"0";
}
// Calculate REF pulses per cubic meter
if (int.TryParse(tbxMeasuredRefPulses.Text, out var pulses) && pulses > 0 &&
int.TryParse(tbxManualInputVolumeLiters.Text, out var liters) && liters > 0)
{
//Backup the actual setting to detect changes
var backupPulses_per_cm = Fm2014.Ref_pulse_per_cm;
// 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.Ref_pulse_per_cm = pulses_per_cm;
// If this succeeded, then update the new manual calibrated value
if (pulses_per_cm == Fm2014.Ref_pulse_per_cm)
{
tbxRefCalibrationResultPulsePerCm.Text = $@"{Fm2014.Ref_pulse_per_cm:D}";
tbxRefPulsePerCm.Text = $@"{Fm2014.Ref_pulse_per_cm:D}";
tbxManualInputVolumeLiters.BackColor = ColorStandardInputField;
tbxRefCalibrationResultPulsePerCm.BackColor = ColorStandardDisplayField;
if (!UpdateRefToDutScale())
{
tbxManualInputVolumeLiters.BackColor = ColorProcessFailed;
return;
}
// Check if values have changed and need to be updated in the standalone setup
if (backupPulses_per_cm != Fm2014.Ref_pulse_per_cm)
{
SetupHasChanged();
}
}
else
{
tbxManualInputVolumeLiters.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)
{
grpBoxDebug.Visible = !grpBoxDebug.Visible;
if (Fm2014 != null)
Fm2014.RequestDebugInformation = grpBoxDebug.Visible;
//gbxDutToRefRegulation.Visible = !grpBoxDebug.Visible;
gbxRegulationSetup.Visible = !grpBoxDebug.Visible;
}
/// <summary>
/// Select next control on enter key pressed
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void cbxAttenuation_KeyDown(Object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Enter)
{
SelectNextControl(ActiveControl, true, true, true, true);
}
}
#endregion Buttons and Controls
#region Event handler
/// <summary>
/// Feedback from FM2014being parsed to GUI
/// </summary>
/// <remarks date="2023-Jan-04..21" 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 = (FM2014CmdDef.CmdResponse)e.SpecificInfoObj;
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.SiUnit}");
switch (resp.CmdName)
{
case FM2014CmdDef.CmdName.CMD_REF_GET_PLS_CTR:
case FM2014CmdDef.CmdName.CMD_REF_GET_PLS_CTR_BU:
tbxMeasuredRefPulses.Text = $@"{resp.IntValue:D}";
break;
case FM2014CmdDef.CmdName.CMD_DUT_GET_PLS_CTR:
case FM2014CmdDef.CmdName.CMD_DUT_GET_PLS_CTR_BU:
tbxMeasuredDutPulses.Text = $@"{resp.IntValue:D}";
break;
case FM2014CmdDef.CmdName.CMD_REF_LPP_SCALE:
tbxRefPulsePerCm.Text = $@"{resp.IntValue:D}";
break;
case FM2014CmdDef.CmdName.CMD_DUT_LPP_SCALE:
tbxDutPulsePerCm.Text = $@"{resp.IntValue:D}";
break;
case FM2014CmdDef.CmdName.CMD_MEAS_SET_ATTN:
var idx = cbxAttenuation.FindString($@"{resp.IntValue}");
cbxAttenuation.SelectedIndex = idx;
break;
case FM2014CmdDef.CmdName.CMD_GET_REF_FREQU:
tbxRefFrequencyHz.Text = $@"{resp.IntValue:D}";
tbxRefFrequencyDirectHz.Text = $@"{resp.IntValue:D}";
if (resp.IntValue < 1 || resp.IntValue > 254)
{
tbxRefFrequencyHz.BackColor = ColorProcessFailed;
tbxActualFlowRateCmPerHour.BackColor = ColorProcessFailed;
}
else
{
tbxRefFrequencyHz.BackColor = ColorStandardDisplayField;
tbxActualFlowRateCmPerHour.BackColor = ColorStandardDisplayField;
}
break;
}
}
else if (resp.DoubleValue != null)
{
LogText($"{resp.AnswerStr}: {resp.DoubleValue:F2} {resp.SiUnit}");
switch (resp.CmdName)
{
case FM2014CmdDef.CmdName.CMD_GET_UDTLC:
case FM2014CmdDef.CmdName.CMD_GET_DTLC:
tbxActualMeasuredToleranceDutToRef.Text = $@"{resp.DoubleValue:F2}";
if (resp.DoubleValue < -Fm2014.Tolerance_percent ||
resp.DoubleValue > Fm2014.Tolerance_percent)
{
tbxActualMeasuredToleranceDutToRef.BackColor = ColorProcessFailed;
}
else
{
tbxActualMeasuredToleranceDutToRef.BackColor = ColorStandardDisplayField;
}
break;
case FM2014CmdDef.CmdName.CMD_REF_SET_SCALE:
tbxScaleRefToDut.Text = $@"{resp.DoubleValue:F4}";
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 FM2014CmdDef.CmdName.CMD_CAL_FLOW_REF_FREQU:
tbxRefFlowRateFromRefFrequencyCmPerH.Text = $@"{resp.DoubleValue:F3}";
// TODO THW Check if the actual flow shall be taken based on 'REF Frequency'
tbxActualFlowRateCmPerHour.Text = $@"{resp.DoubleValue:F3}";
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 the latest information
Update();
}));
}
#if TEST_MULTIPLE_FM2014
/// <summary>
/// Feedback from FM2014being parsed to GUI
/// </summary>
/// <remarks date="2023-Jan-30" author="Thomas Wiedebusch">
/// - Initial.
/// </remarks>
//TODO THW TEST FM2014 multiple objects
private void DataReceived_Handler_TEST(Object sender, ProcessExecEventArgs e)
{
Invoke(new Action(() =>
{
// Data dispatcher
var resp = (FM2014CmdDef.CmdResponse)e.SpecificInfoObj;
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.SiUnit}");
}
else if (resp.DoubleValue != null)
{
LogText($"{resp.AnswerStr}: {resp.DoubleValue:F2} {resp.SiUnit}");
}
}));
}
#endif
#endregion
}
}