tbf/GenesisCordonelInterface/UI/StaraTuraAPI_GenesisCordonelInterface/FrmGCIAPI.cs

442 lines
14 KiB
C#

using System;
using System.Threading.Tasks;
using System.Windows.Forms;
using GenesisCordonelInterface.API;
using Xylem.Common.Hardware.Interfaces.Ports.PortCore;
using static Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore.GenesisMeter;
namespace GenesisCordonelInterface.UI.StaraTuraAPI_GenesisCordonelInterface
{
public partial class FrmGCIAPI : Form
{
/*
GCI UI async execution model
The UI thread must never execute long-running meter communication directly.
When the user clicks a button, the UI only reads input values from controls
(for example slot number) and then delegates the real work to the GCI API.
The API call is executed by a per-slot worker queue:
- one slot / one meter / one communication port has its own worker
- commands for the same meter are executed sequentially
- commands for different meters can run in parallel
- after await completes, execution returns back to the UI thread safely
This prevents the WinForms UI from freezing while keeping meter communication
safe and ordered.
workflow:
User clicks button
|
v
WinForms UI thread
(read textbox values only)
|
v
await _api.GetPcbIdAsync(slot)
|
v
InterfaceOutsideToGCI
(clean public facade)
|
v
InterfaceGCIToLaatzen
(API/business logic)
|
v
Per-slot ApiWorker queue
(slot 1 / slot 2 / slot 3 ...)
|
v
GenesisMeter communication
(blocking HW operation)
|
v
Result returned
|
v
UI thread continues after await
(update labels / show MessageBox)
The UI does not freeze because hardware communication is not executed directly inside the event handler.
The event handler only calls an async method and waits using await. While waiting, the UI thread remains free.
The actual work is executed on a worker thread assigned to a specific slot/meter.
For a single meter, requests are queued, so they are executed sequentially and do not run at the same time,
which prevents communication conflicts.
*/
private readonly InterfaceOutsideToGCI _api;
public FrmGCIAPI() : this(new InterfaceOutsideToGCI())
{
}
/// <summary>
/// Initializes the form and default UI values.
/// </summary>
public FrmGCIAPI(InterfaceOutsideToGCI api)
{
_api = api ?? throw new ArgumentNullException(nameof(api));
InitializeComponent();
InitializeDefaults();
}
/// <summary>
/// Sets default values for combo boxes.
/// </summary>
private void InitializeDefaults()
{
cmbConfigSource.DataSource = Enum.GetValues(typeof(ConfigSource));
cmbPasswordSource.DataSource = Enum.GetValues(typeof(PasswordSource));
cmbConfigSource.SelectedItem = ConfigSource.InterfaceInputConfig;
cmbPasswordSource.SelectedItem = PasswordSource.OfflineFile;
}
#region UI Helpers
/// <summary>
/// Executes an async API action with UI busy state and centralized error handling.
/// </summary>
private async Task ExecuteApiActionAsync(Func<Task> action)
{
try
{
SetBusy(true);
await action();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "API call failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
SetBusy(false);
}
}
/// <summary>
/// Enables/disables UI controls and updates cursor to indicate busy state.
/// </summary>
private void SetBusy(bool busy)
{
Cursor = busy ? Cursors.WaitCursor : Cursors.Default;
btnInit.Enabled = !busy;
btnConnectOne.Enabled = !busy;
btnConnectAll.Enabled = !busy;
btnDisconnect.Enabled = !busy;
btnGetPcbId.Enabled = !busy;
btnReadRegister.Enabled = !busy;
btnWriteRegister.Enabled = !busy;
btnSetPassword.Enabled = !busy;
}
/// <summary>
/// Safely parses slot number from UI textbox.
/// </summary>
private int GetSlotSafe()
{
if (!int.TryParse(txtSlot.Text, out int slot))
throw new Exception("Invalid slot number.");
return slot;
}
/// <summary>
/// Converts string input to best matching primitive type.
/// </summary>
private object ParseValue(string input)
{
if (int.TryParse(input, out int i)) return i;
if (uint.TryParse(input, out uint ui)) return ui;
if (bool.TryParse(input, out bool b)) return b;
return input;
}
/// <summary>
/// Creates a public GCI port configuration object from UI input values.
/// </summary>
/// <param name="portName">Name of the port (e.g. COM3).</param>
/// <param name="baudRateText">Baud rate entered in the UI.</param>
/// <returns>
/// Instance of <see cref="GciPublicModels.GciPortConfig"/> or null if port name is empty.
/// </returns>
/// <exception cref="Exception">Thrown when baud rate is invalid.</exception>
private GciPublicModels.GciPortConfig CreateGciPortConfig(string portName, string baudRateText)
{
if (string.IsNullOrWhiteSpace(portName))
return null;
if (!int.TryParse(baudRateText, out int baudRate))
throw new Exception($"Invalid baud rate for port {portName}.");
return new GciPublicModels.GciPortConfig
{
PortName = portName,
Type = "Serial",
};
}
/// <summary>
/// Gets selected configuration source from UI and maps it to public GCI model.
/// </summary>
/// <returns>Selected <see cref="GciPublicModels.GciConfigSource"/>.</returns>
/// <exception cref="Exception">Thrown when no value is selected.</exception>
private GciPublicModels.GciConfigSource GetConfigSource()
{
if (cmbConfigSource.SelectedItem == null)
throw new Exception("Config source is not selected.");
return (GciPublicModels.GciConfigSource)cmbConfigSource.SelectedItem;
}
/// <summary>
/// Gets selected password source from UI and maps it to public GCI model.
/// </summary>
/// <returns>Selected <see cref="GciPublicModels.GciPasswordSource"/>.</returns>
/// <exception cref="Exception">Thrown when no value is selected.</exception>
private GciPublicModels.GciPasswordSource GetPasswordSource()
{
if (cmbPasswordSource.SelectedItem == null)
throw new Exception("Password source is not selected.");
return (GciPublicModels.GciPasswordSource)cmbPasswordSource.SelectedItem;
}
#endregion
#region Buttons
/// <summary>
/// Initializes meter using external configuration.
/// </summary>
private async void btnInit_Click(object sender, EventArgs e)
{
int slot;
try { slot = GetSlotSafe(); }
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Invalid input");
return;
}
await ExecuteApiActionAsync(async () =>
{
var request = new GciPublicModels.GciInitSlotRequest
{
SlotId = slot,
ConfigSource = GetConfigSource(),
PasswordSource = GetPasswordSource(),
RequestPort = CreateGciPortConfig(txtRequestPort.Text, txtRequestBaudRate.Text),
StreamingPort = CreateGciPortConfig(txtStreamingPort.Text, txtStreamingBaudRate.Text)
};
var result = await _api.InitSlotAsync(request);
if (!result.Success)
throw new Exception(result.Message);
MessageBox.Show("Meter initialized.");
});
}
/// <summary>
/// Connects to one meter.
/// </summary>
private async void btnConnectOne_Click(object sender, EventArgs e)
{
int slot;
try { slot = GetSlotSafe(); }
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Invalid input");
return;
}
await ExecuteApiActionAsync(async () =>
{
var result = await _api.ConnectOneSlotAsync(slot);
if (!result.Success)
throw new Exception(result.Message);
MessageBox.Show($"Connected.\r\nPCB ID: {result.PcbId}");
});
}
/// <summary>
/// Disconnects from current meter.
/// </summary>
private async void btnDisconnect_Click(object sender, EventArgs e)
{
int slot;
try { slot = GetSlotSafe(); }
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Invalid input");
return;
}
await ExecuteApiActionAsync(async () =>
{
await _api.DisconnectAsync(slot);
MessageBox.Show("Disconnected.");
});
}
/// <summary>
/// Reads PCB ID asynchronously.
/// </summary>
private async void btnGetPcbId_Click(object sender, EventArgs e)
{
int slot;
try { slot = GetSlotSafe(); }
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Invalid input");
return;
}
await ExecuteApiActionAsync(async () =>
{
GciPublicModels.GciGetPcbIdResult result =
await _api.GetPcbIdAsync(slot);
if (!result.Success)
throw new Exception(result.Message);
MessageBox.Show($"PCB ID: {result.PcbId}");
});
}
/// <summary>
/// Reads register value.
/// </summary>
private async void btnReadRegister_Click(object sender, EventArgs e)
{
int slot;
string registerName = txtRegister.Text;
try
{
slot = GetSlotSafe();
if (string.IsNullOrWhiteSpace(registerName))
throw new Exception("Register name is empty.");
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Invalid input");
return;
}
await ExecuteApiActionAsync(async () =>
{
var result = await _api.ReadRegisterAsync(slot, registerName);
if (!result.Success)
throw new Exception(result.ErrorMessage);
MessageBox.Show($"Register: {result.RegisterName}\r\nRaw: {result.RawHex}");
});
}
private async void btnConnectAll_Click(object sender, EventArgs e)
{
await ExecuteApiActionAsync(async () =>
{
var slots = _api.GetSelectedSlots();
if (slots.Count == 0)
throw new Exception("No slots selected.");
foreach (int slot in slots)
{
var result = await _api.ConnectOneSlotAsync(slot);
if (!result.Success)
throw new Exception($"Slot {slot}: {result.Message}");
}
MessageBox.Show("Selected meters connected.");
});
}
private async void btnWriteRegister_Click(object sender, EventArgs e)
{
int slot;
string registerName = txtRegister.Text;
string valueText = txtValue.Text;
try
{
slot = GetSlotSafe();
if (string.IsNullOrWhiteSpace(registerName))
throw new Exception("Register name is empty.");
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Invalid input");
return;
}
await ExecuteApiActionAsync(async () =>
{
object value = ParseValue(valueText);
var result = await _api.WriteRegisterAsync(
slot,
registerName,
value,
false,
false);
if (!result.Success)
throw new Exception(result.ErrorMessage);
MessageBox.Show($"Write OK: {registerName} = {value}");
});
}
private async void btnSetPassword_Click(object sender, EventArgs e)
{
int slot;
string password = txtPassword.Text;
try
{
slot = GetSlotSafe();
if (string.IsNullOrWhiteSpace(password))
throw new Exception("Password is empty.");
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Invalid input");
return;
}
await ExecuteApiActionAsync(async () =>
{
bool ok = await _api.SetMeterPasswordAsync(slot, password);
if (!ok)
throw new Exception("Set password failed.");
MessageBox.Show("Password set successfully.");
});
}
#endregion
}
}