442 lines
14 KiB
C#
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="PublicModels.GciPortConfig"/> or null if port name is empty.
|
|
/// </returns>
|
|
/// <exception cref="Exception">Thrown when baud rate is invalid.</exception>
|
|
private PublicModels.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 PublicModels.GciPortConfig
|
|
{
|
|
PortName = portName,
|
|
Type = "Serial",
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets selected configuration source from UI and maps it to public GCI model.
|
|
/// </summary>
|
|
/// <returns>Selected <see cref="PublicModels.GciConfigSource"/>.</returns>
|
|
/// <exception cref="Exception">Thrown when no value is selected.</exception>
|
|
private PublicModels.GciConfigSource GetConfigSource()
|
|
{
|
|
if (cmbConfigSource.SelectedItem == null)
|
|
throw new Exception("Config source is not selected.");
|
|
|
|
return (PublicModels.GciConfigSource)cmbConfigSource.SelectedItem;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets selected password source from UI and maps it to public GCI model.
|
|
/// </summary>
|
|
/// <returns>Selected <see cref="PublicModels.GciPasswordSource"/>.</returns>
|
|
/// <exception cref="Exception">Thrown when no value is selected.</exception>
|
|
private PublicModels.GciPasswordSource GetPasswordSource()
|
|
{
|
|
if (cmbPasswordSource.SelectedItem == null)
|
|
throw new Exception("Password source is not selected.");
|
|
|
|
return (PublicModels.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 PublicModels.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\n");
|
|
});
|
|
}
|
|
|
|
/// <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 () =>
|
|
{
|
|
PublicModels.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 () =>
|
|
{
|
|
PublicModels.GciSetPasswordResult result = await _api.SetMeterPasswordAsync(slot, password);
|
|
|
|
if (!result.Success)
|
|
throw new Exception("Set password failed.");
|
|
|
|
MessageBox.Show("Password set successfully.");
|
|
});
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
} |