diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
new file mode 100644
index 0000000..48a526b
--- /dev/null
+++ b/.github/workflows/dotnet.yml
@@ -0,0 +1,25 @@
+name: .NET WPF CI
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+
+jobs:
+ build:
+ runs-on: windows-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup .NET 10.0
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '10.0.x'
+
+ - name: Restore dependencies
+ run: dotnet restore
+
+ - name: Build
+ run: dotnet build --no-restore --configuration Release
diff --git a/MainWindow.xaml b/MainWindow.xaml
index 6f0a056..a4959e1 100644
--- a/MainWindow.xaml
+++ b/MainWindow.xaml
@@ -1309,7 +1309,18 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Services/ErrorHandlingService.cs b/Services/ErrorHandlingService.cs
index ab206af..cfca526 100644
--- a/Services/ErrorHandlingService.cs
+++ b/Services/ErrorHandlingService.cs
@@ -257,6 +257,56 @@ public bool ExecuteWithRetry(
return fallback;
}
+ public async System.Threading.Tasks.Task ExecuteWithRetryAsync(
+ Func> func,
+ string operationName,
+ ErrorCategory category,
+ T? fallback = default,
+ int maxRetries = 3,
+ int baseDelayMs = 50)
+ {
+ var breaker = GetOrCreateCircuitBreaker(operationName);
+ if (!breaker.AllowAttempt())
+ {
+ ReportError(
+ ErrorSeverity.Warning,
+ category,
+ operationName,
+ $"Circuit breaker open for '{operationName}'. Returning fallback.",
+ wasRecovered: false);
+ return fallback;
+ }
+
+ for (int attempt = 0; attempt <= maxRetries; attempt++)
+ {
+ try
+ {
+ T result = await func().ConfigureAwait(false);
+ breaker.RecordSuccess();
+ return result;
+ }
+ catch (Exception ex) when (attempt < maxRetries && IsTransient(ex))
+ {
+ int delay = baseDelayMs * (int)Math.Pow(2, attempt);
+ await System.Threading.Tasks.Task.Delay(delay).ConfigureAwait(false);
+ ReportError(
+ ErrorSeverity.Warning,
+ category,
+ operationName,
+ $"Retry {attempt + 1}/{maxRetries}: {ex.Message}",
+ wasRecovered: true);
+ }
+ catch (Exception ex)
+ {
+ breaker.RecordFailure();
+ ReportException(category, operationName, ex, wasRecovered: false);
+ return fallback;
+ }
+ }
+
+ return fallback;
+ }
+
public IReadOnlyList GetRecentErrors(int count = 50)
{
return _errorLog
diff --git a/Services/HmiServiceContracts.cs b/Services/HmiServiceContracts.cs
index 168a8cb..ebbaec6 100644
--- a/Services/HmiServiceContracts.cs
+++ b/Services/HmiServiceContracts.cs
@@ -4,10 +4,21 @@
namespace XTSPrimeMoverProject.Services
{
+ public enum GatewayConnectionState
+ {
+ Connected,
+ Degraded,
+ Reconnecting,
+ Offline
+ }
+
public interface IMachineGatewayService
{
event EventHandler? StateChanged;
event EventHandler? LogGenerated;
+ event EventHandler? ConnectionStateChanged;
+
+ GatewayConnectionState ConnectionState { get; }
IReadOnlyList Movers { get; }
IReadOnlyList Machines { get; }
@@ -29,24 +40,24 @@ public interface IMachineGatewayService
IReadOnlyList GetWatchdogStatus();
IReadOnlyList GetOrchestrationSteps();
- bool TryApplyOrchestration(IReadOnlyList stepDefinitions, out string message);
- IReadOnlyList PreviewOrchestrationValidation(IReadOnlyList stepDefinitions);
- IReadOnlyList GetOrchestrationSafetyGateStatuses();
+ Task<(bool Success, string Message)> TryApplyOrchestrationAsync(IReadOnlyList stepDefinitions);
+ Task> PreviewOrchestrationValidationAsync(IReadOnlyList stepDefinitions);
+ Task> GetOrchestrationSafetyGateStatusesAsync();
}
public interface IDataGatewayService
{
string DatabasePath { get; }
- IReadOnlyList GetPartHistory(string trackingNumber);
- PartSummaryRecord? GetPartSummary(string trackingNumber);
+ Task> GetPartHistoryAsync(string trackingNumber);
+ Task GetPartSummaryAsync(string trackingNumber);
- IReadOnlyList GetExportableTables();
- IReadOnlyList GetAllTables();
- IReadOnlyList GetTableColumns(string tableName);
- int GetTableRowCount(string tableName);
- IReadOnlyList> GetTableRows(string tableName, int maxRows = 500);
- string ExportTableToCsv(string tableName, string? exportDirectory = null);
- string GetDefaultExportDirectory();
+ Task> GetExportableTablesAsync();
+ Task> GetAllTablesAsync();
+ Task> GetTableColumnsAsync(string tableName);
+ Task GetTableRowCountAsync(string tableName);
+ Task>> GetTableRowsAsync(string tableName, int maxRows = 500);
+ Task ExportTableToCsvAsync(string tableName, string? exportDirectory = null);
+ Task GetDefaultExportDirectoryAsync();
}
}
diff --git a/Services/LocalSimulationServiceGateway.cs b/Services/LocalSimulationServiceGateway.cs
index f912dd6..f69825a 100644
--- a/Services/LocalSimulationServiceGateway.cs
+++ b/Services/LocalSimulationServiceGateway.cs
@@ -26,6 +26,9 @@ public LocalSimulationServiceGateway(XTSSimulationEngine engine)
public event EventHandler? StateChanged;
public event EventHandler? LogGenerated;
+ public event EventHandler? ConnectionStateChanged { add { } remove { } }
+
+ public GatewayConnectionState ConnectionState => GatewayConnectionState.Connected;
public IReadOnlyList Movers => _engine.Movers;
public IReadOnlyList Machines => _engine.Machines;
@@ -106,115 +109,115 @@ public IReadOnlyList GetOrchestrationSteps()
fallback: Array.Empty())!;
}
- public bool TryApplyOrchestration(IReadOnlyList stepDefinitions, out string message)
+ public Task<(bool Success, string Message)> TryApplyOrchestrationAsync(IReadOnlyList stepDefinitions)
{
try
{
- return _engine.TryApplyOrchestration(stepDefinitions, out message);
+ bool success = _engine.TryApplyOrchestration(stepDefinitions, out string msg);
+ return Task.FromResult((success, msg));
}
catch (Exception ex)
{
_errorHandler.ReportException(ErrorCategory.Gateway, "LocalGateway.TryApplyOrchestration", ex);
- message = $"Gateway error: {ex.Message}";
- return false;
+ return Task.FromResult((false, $"Gateway error: {ex.Message}"));
}
}
- public IReadOnlyList PreviewOrchestrationValidation(IReadOnlyList stepDefinitions)
+ public Task> PreviewOrchestrationValidationAsync(IReadOnlyList stepDefinitions)
{
- return _errorHandler.ExecuteWithRetry(
+ return Task.FromResult(_errorHandler.ExecuteWithRetry(
() => _engine.PreviewOrchestrationValidation(stepDefinitions),
"LocalGateway.PreviewOrchestrationValidation",
ErrorCategory.Gateway,
- fallback: new List { "Validation unavailable due to gateway error." })!;
+ fallback: new List { "Validation unavailable due to gateway error." })!);
}
- public IReadOnlyList GetOrchestrationSafetyGateStatuses()
+ public Task> GetOrchestrationSafetyGateStatusesAsync()
{
- return _errorHandler.ExecuteWithRetry(
+ return Task.FromResult(_errorHandler.ExecuteWithRetry(
() => _engine.GetOrchestrationSafetyGateStatuses(),
"LocalGateway.GetOrchestrationSafetyGateStatuses",
ErrorCategory.Gateway,
- fallback: Array.Empty())!;
+ fallback: Array.Empty())!);
}
// --- IDataGatewayService ---
public string DatabasePath => _engine.DatabasePath;
- public IReadOnlyList GetPartHistory(string trackingNumber)
+ public Task> GetPartHistoryAsync(string trackingNumber)
{
- return _errorHandler.ExecuteWithRetry(
+ return Task.FromResult(_errorHandler.ExecuteWithRetry(
() => _engine.GetPartHistory(trackingNumber),
"LocalGateway.GetPartHistory",
ErrorCategory.Gateway,
- fallback: Array.Empty())!;
+ fallback: Array.Empty())!);
}
- public PartSummaryRecord? GetPartSummary(string trackingNumber)
+ public Task GetPartSummaryAsync(string trackingNumber)
{
- return _errorHandler.ExecuteWithRetry(
+ return Task.FromResult(_errorHandler.ExecuteWithRetry(
() => _engine.GetPartSummary(trackingNumber),
"LocalGateway.GetPartSummary",
ErrorCategory.Gateway,
- fallback: null);
+ fallback: null));
}
- public IReadOnlyList GetExportableTables()
+ public Task> GetExportableTablesAsync()
{
- return _errorHandler.ExecuteWithRetry(
+ return Task.FromResult(_errorHandler.ExecuteWithRetry(
() => _engine.GetExportableTables(),
"LocalGateway.GetExportableTables",
ErrorCategory.Gateway,
- fallback: Array.Empty())!;
+ fallback: Array.Empty())!);
}
- public IReadOnlyList GetAllTables()
+ public Task> GetAllTablesAsync()
{
- return _errorHandler.ExecuteWithRetry(
+ return Task.FromResult(_errorHandler.ExecuteWithRetry(
() => _engine.GetAllTables(),
"LocalGateway.GetAllTables",
ErrorCategory.Gateway,
- fallback: Array.Empty())!;
+ fallback: Array.Empty())!);
}
- public IReadOnlyList GetTableColumns(string tableName)
+ public Task> GetTableColumnsAsync(string tableName)
{
- return _errorHandler.ExecuteWithRetry(
+ return Task.FromResult(_errorHandler.ExecuteWithRetry(
() => _engine.GetTableColumns(tableName),
"LocalGateway.GetTableColumns",
ErrorCategory.Gateway,
- fallback: Array.Empty())!;
+ fallback: Array.Empty())!);
}
- public int GetTableRowCount(string tableName)
+ public Task GetTableRowCountAsync(string tableName)
{
- return _errorHandler.ExecuteWithRetry(
+ return Task.FromResult(_errorHandler.ExecuteWithRetry(
() => _engine.GetTableRowCount(tableName),
"LocalGateway.GetTableRowCount",
ErrorCategory.Gateway,
- fallback: 0);
+ fallback: 0));
}
- public IReadOnlyList> GetTableRows(string tableName, int maxRows = 500)
+ public Task>> GetTableRowsAsync(string tableName, int maxRows = 500)
{
- return _errorHandler.ExecuteWithRetry(
+ return Task.FromResult(_errorHandler.ExecuteWithRetry(
() => _engine.GetTableRows(tableName, maxRows),
"LocalGateway.GetTableRows",
ErrorCategory.Gateway,
- fallback: Array.Empty>())!;
+ fallback: Array.Empty>())!);
}
- public string ExportTableToCsv(string tableName, string? exportDirectory = null)
+ public Task ExportTableToCsvAsync(string tableName, string? exportDirectory = null)
{
- return _errorHandler.ExecuteWithRetry(
+ return Task.FromResult(_errorHandler.ExecuteWithRetry(
() => _engine.ExportTableToCsv(tableName, exportDirectory),
"LocalGateway.ExportTableToCsv",
ErrorCategory.Gateway,
- fallback: string.Empty)!;
+ fallback: string.Empty)!);
}
- public string GetDefaultExportDirectory() => _engine.GetDefaultExportDirectory();
+ public Task GetDefaultExportDirectoryAsync() => Task.FromResult(_engine.GetDefaultExportDirectory());
// --- Event forwarding ---
diff --git a/Services/RemoteTwinCatMock/RemoteTwinCatMachineGatewayMock.cs b/Services/RemoteTwinCatMock/RemoteTwinCatMachineGatewayMock.cs
index 1af6776..a5ff68c 100644
--- a/Services/RemoteTwinCatMock/RemoteTwinCatMachineGatewayMock.cs
+++ b/Services/RemoteTwinCatMock/RemoteTwinCatMachineGatewayMock.cs
@@ -31,6 +31,13 @@ public RemoteTwinCatMachineGatewayMock(IMachineGatewayService inner, int command
public event EventHandler? StateChanged;
public event EventHandler? LogGenerated;
+ public event EventHandler? ConnectionStateChanged
+ {
+ add => _inner.ConnectionStateChanged += value;
+ remove => _inner.ConnectionStateChanged -= value;
+ }
+
+ public GatewayConnectionState ConnectionState => _inner.ConnectionState;
public IReadOnlyList Movers => _inner.Movers;
public IReadOnlyList Machines => _inner.Machines;
@@ -83,37 +90,47 @@ public IReadOnlyList GetOrchestrationSteps()
fallback: Array.Empty())!;
}
- public bool TryApplyOrchestration(IReadOnlyList stepDefinitions, out string message)
+ public async Task<(bool Success, string Message)> TryApplyOrchestrationAsync(IReadOnlyList stepDefinitions)
{
try
{
- SimulateNetworkLatencySync();
- return _inner.TryApplyOrchestration(stepDefinitions, out message);
+ if (_commandLatencyMs > 0)
+ {
+ await Task.Delay(_commandLatencyMs).ConfigureAwait(false);
+ }
+ return await _inner.TryApplyOrchestrationAsync(stepDefinitions).ConfigureAwait(false);
}
catch (Exception ex)
{
_errorHandler.ReportException(ErrorCategory.Gateway, "RemoteMock.TryApplyOrchestration", ex);
- message = $"Remote gateway error: {ex.Message}";
- return false;
+ return (false, $"Remote gateway error: {ex.Message}");
}
}
- public IReadOnlyList PreviewOrchestrationValidation(IReadOnlyList stepDefinitions)
+ public async Task> PreviewOrchestrationValidationAsync(IReadOnlyList stepDefinitions)
{
- return _errorHandler.ExecuteWithRetry(
- () => _inner.PreviewOrchestrationValidation(stepDefinitions),
+ if (_commandLatencyMs > 0)
+ {
+ await Task.Delay(_commandLatencyMs).ConfigureAwait(false);
+ }
+ return await _errorHandler.ExecuteWithRetryAsync(
+ async () => await _inner.PreviewOrchestrationValidationAsync(stepDefinitions),
"RemoteMock.PreviewOrchestrationValidation",
ErrorCategory.Gateway,
- fallback: new List { "Validation unavailable due to remote gateway error." })!;
+ fallback: new List { "Validation unavailable due to remote gateway error." }).ConfigureAwait(false) ?? new List();
}
- public IReadOnlyList GetOrchestrationSafetyGateStatuses()
+ public async Task> GetOrchestrationSafetyGateStatusesAsync()
{
- return _errorHandler.ExecuteWithRetry(
- () => _inner.GetOrchestrationSafetyGateStatuses(),
+ if (_commandLatencyMs > 0)
+ {
+ await Task.Delay(_commandLatencyMs).ConfigureAwait(false);
+ }
+ return await _errorHandler.ExecuteWithRetryAsync(
+ async () => await _inner.GetOrchestrationSafetyGateStatusesAsync(),
"RemoteMock.GetOrchestrationSafetyGateStatuses",
ErrorCategory.Gateway,
- fallback: Array.Empty())!;
+ fallback: (IReadOnlyList)Array.Empty()).ConfigureAwait(false) ?? Array.Empty();
}
private void DispatchWithLatency(Action command)
diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs
index 29cbd8c..a8fa19f 100644
--- a/ViewModels/MainViewModel.cs
+++ b/ViewModels/MainViewModel.cs
@@ -34,6 +34,10 @@ public class MainViewModel : INotifyPropertyChanged
private string _orchestrationStatus;
private string _orchestrationValidationStatus;
private string _gatewayModeStatus;
+ private string _gatewayMode;
+ private string _gatewayModeIndicatorColor = "Gray";
+ private string _connectionState = "Connected";
+ private string _connectionStateColor = "Green";
public ObservableCollection Movers { get; }
public ObservableCollection Machines { get; }
@@ -90,12 +94,30 @@ public string PartHistoryStatus
set { _partHistoryStatus = value; OnPropertyChanged(); }
}
+ public string GatewayModeIndicatorColor
+ {
+ get => _gatewayModeIndicatorColor;
+ private set { _gatewayModeIndicatorColor = value; OnPropertyChanged(); }
+ }
+
public string PartHistorySummary
{
get => _partHistorySummary;
set { _partHistorySummary = value; OnPropertyChanged(); }
}
+ public string ConnectionState
+ {
+ get => _connectionState;
+ private set { _connectionState = value; OnPropertyChanged(); }
+ }
+
+ public string ConnectionStateColor
+ {
+ get => _connectionStateColor;
+ private set { _connectionStateColor = value; OnPropertyChanged(); }
+ }
+
public string SelectedExportTable
{
get => _selectedExportTable;
@@ -203,8 +225,14 @@ public MainViewModel(IMachineGatewayService machine, IDataGatewayService data, s
_gatewayModeStatus = string.IsNullOrWhiteSpace(gatewayModeStatus)
? "Machine Gateway: Local"
: gatewayModeStatus;
+
+ _gatewayMode = _gatewayModeStatus.Contains("Remote") ? "Remote" : "Local";
+ _gatewayModeIndicatorColor = _gatewayMode == "Remote" ? "Orange" : "LightBlue";
+
_machine.StateChanged += OnEngineStateChanged;
_machine.LogGenerated += OnEngineLogGenerated;
+ _machine.ConnectionStateChanged += OnMachineConnectionStateChanged;
+ UpdateConnectionState(_machine.ConnectionState);
_statusText = "SYSTEM STOPPED";
_partHistoryTrackingNumber = string.Empty;
@@ -276,47 +304,63 @@ private void InitializeViewModels()
}
}
- private void LoadExportTables()
+ private async void LoadExportTables()
{
- ExportTables.Clear();
- foreach (var table in _data.GetExportableTables())
+ try
{
- ExportTables.Add(table);
- }
+ var tables = await _data.GetExportableTablesAsync();
+ ExportTables.Clear();
+ foreach (var table in tables)
+ {
+ ExportTables.Add(table);
+ }
- if (ExportTables.Count > 0)
+ if (ExportTables.Count > 0)
+ {
+ SelectedExportTable = ExportTables[0];
+ }
+ }
+ catch (Exception ex)
{
- SelectedExportTable = ExportTables[0];
+ _errorHandler.ReportException(ErrorCategory.ViewModel, "LoadExportTables", ex);
}
}
- private void LoadDbTables()
+ private async void LoadDbTables()
{
- DbTables.Clear();
- foreach (var table in _data.GetAllTables())
+ try
{
- DbTables.Add(table);
- }
+ var tables = await _data.GetAllTablesAsync();
+ DbTables.Clear();
+ foreach (var table in tables)
+ {
+ DbTables.Add(table);
+ }
- if (DbTables.Count == 0)
- {
- SelectedDbTable = string.Empty;
- DbTableRowsView = CreateEmptyDbTableView();
- DbTableStatus = "No tables found in the runtime database.";
- return;
- }
+ if (DbTables.Count == 0)
+ {
+ SelectedDbTable = string.Empty;
+ DbTableRowsView = CreateEmptyDbTableView();
+ DbTableStatus = "No tables found in the runtime database.";
+ return;
+ }
- if (string.IsNullOrWhiteSpace(SelectedDbTable) || !DbTables.Contains(SelectedDbTable))
- {
- SelectedDbTable = DbTables[0];
+ if (string.IsNullOrWhiteSpace(SelectedDbTable) || !DbTables.Contains(SelectedDbTable))
+ {
+ SelectedDbTable = DbTables[0];
+ }
+ else
+ {
+ await LoadSelectedDbTableRowsAsync();
+ }
}
- else
+ catch (Exception ex)
{
- LoadSelectedDbTableRows();
+ _errorHandler.ReportException(ErrorCategory.ViewModel, "LoadDbTables", ex);
}
}
- private void LoadSelectedDbTableRows()
+ private async System.Threading.Tasks.Task LoadSelectedDbTableRowsAsync()
{
if (string.IsNullOrWhiteSpace(SelectedDbTable))
{
@@ -327,9 +371,9 @@ private void LoadSelectedDbTableRows()
try
{
- var columns = _data.GetTableColumns(SelectedDbTable);
- int totalRows = _data.GetTableRowCount(SelectedDbTable);
- var rows = _data.GetTableRows(SelectedDbTable, 500);
+ var columns = await _data.GetTableColumnsAsync(SelectedDbTable);
+ int totalRows = await _data.GetTableRowCountAsync(SelectedDbTable);
+ var rows = await _data.GetTableRowsAsync(SelectedDbTable, 500);
var table = new DataTable(SelectedDbTable);
foreach (var col in columns)
@@ -354,12 +398,17 @@ private void LoadSelectedDbTableRows()
}
catch (Exception ex)
{
- _errorHandler.ReportException(ErrorCategory.ViewModel, "MainVM.LoadSelectedDbTableRows", ex);
+ _errorHandler.ReportException(ErrorCategory.ViewModel, "LoadSelectedDbTableRowsAsync", ex);
DbTableRowsView = CreateEmptyDbTableView();
DbTableStatus = $"DB table validation/load failed: {ex.Message}";
}
}
+ private void LoadSelectedDbTableRows()
+ {
+ _ = LoadSelectedDbTableRowsAsync();
+ }
+
private static DataView CreateEmptyDbTableView()
{
var t = new DataTable("DbTableRows");
@@ -451,23 +500,24 @@ private void ReindexOrchestrationSteps()
}
}
- private void ApplyOrchestrationFromHmi()
+ private async void ApplyOrchestrationFromHmi()
{
try
{
var stepDefs = BuildStepDefinitionsFromEditor();
- if (_machine.TryApplyOrchestration(stepDefs, out var message))
+ var result = await _machine.TryApplyOrchestrationAsync(stepDefs);
+ if (result.Success)
{
- OrchestrationStatus = message;
+ OrchestrationStatus = result.Message;
OrchestrationValidationStatus = "Apply successful.";
LoadOrchestrationSteps();
}
else
{
- OrchestrationStatus = message;
+ OrchestrationStatus = result.Message;
}
- RefreshSafetyGates();
+ await RefreshSafetyGatesAsync();
}
catch (Exception ex)
{
@@ -476,10 +526,10 @@ private void ApplyOrchestrationFromHmi()
}
}
- private void PreviewOrchestrationValidation()
+ private async void PreviewOrchestrationValidation()
{
var stepDefs = BuildStepDefinitionsFromEditor();
- var errors = _machine.PreviewOrchestrationValidation(stepDefs);
+ var errors = await _machine.PreviewOrchestrationValidationAsync(stepDefs);
if (errors.Count == 0)
{
OrchestrationValidationStatus = "Validation OK: no rule violations.";
@@ -504,10 +554,10 @@ private List BuildStepDefinitionsFromEditor()
.ToList();
}
- private void RefreshSafetyGates()
+ private async System.Threading.Tasks.Task RefreshSafetyGatesAsync()
{
SafetyGates.Clear();
- foreach (var gate in _machine.GetOrchestrationSafetyGateStatuses())
+ foreach (var gate in await _machine.GetOrchestrationSafetyGateStatusesAsync())
{
SafetyGates.Add(new SafetyGateStatusItemViewModel
{
@@ -518,7 +568,12 @@ private void RefreshSafetyGates()
}
}
- private void InspectPartHistory()
+ private void RefreshSafetyGates()
+ {
+ _ = RefreshSafetyGatesAsync();
+ }
+
+ private async void InspectPartHistory()
{
string tracking = PartHistoryTrackingNumber?.Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(tracking))
@@ -532,13 +587,13 @@ private void InspectPartHistory()
try
{
PartHistoryEvents.Clear();
- var events = _data.GetPartHistory(tracking);
+ var events = await _data.GetPartHistoryAsync(tracking);
foreach (var item in events)
{
PartHistoryEvents.Add(item);
}
- var summary = _data.GetPartSummary(tracking);
+ var summary = await _data.GetPartSummaryAsync(tracking);
if (summary == null)
{
PartHistoryStatus = $"No records found for {tracking}.";
@@ -565,12 +620,12 @@ private async void ExportCsv()
}
string tableName = SelectedExportTable;
- string exportDir = _data.GetDefaultExportDirectory();
+ string exportDir = await _data.GetDefaultExportDirectoryAsync();
CsvExportStatus = $"Exporting {tableName}...";
try
{
- string filePath = await Task.Run(() => _data.ExportTableToCsv(tableName, exportDir));
+ string filePath = await _data.ExportTableToCsvAsync(tableName, exportDir);
CsvExportStatus = $"Exported {tableName} -> {filePath}";
}
catch (Exception ex)
@@ -595,6 +650,29 @@ private void OnEngineLogGenerated(object? sender, string message)
}
}
+ private void OnMachineConnectionStateChanged(object? sender, GatewayConnectionState state)
+ {
+ if (!_dispatcher.CheckAccess())
+ {
+ _dispatcher.BeginInvoke(() => OnMachineConnectionStateChanged(sender, state));
+ return;
+ }
+ UpdateConnectionState(state);
+ }
+
+ private void UpdateConnectionState(GatewayConnectionState state)
+ {
+ ConnectionState = state.ToString();
+ ConnectionStateColor = state switch
+ {
+ GatewayConnectionState.Connected => "Green",
+ GatewayConnectionState.Degraded => "Orange",
+ GatewayConnectionState.Reconnecting => "Gold",
+ GatewayConnectionState.Offline => "Red",
+ _ => "Gray"
+ };
+ }
+
private void RefreshWatchdogStatuses()
{
var latest = _machine.GetWatchdogStatus();
diff --git a/XTSPrimeMoverProject.csproj b/XTSPrimeMoverProject.csproj
index a5b1003..7c83c4d 100644
--- a/XTSPrimeMoverProject.csproj
+++ b/XTSPrimeMoverProject.csproj
@@ -3,6 +3,7 @@
WinExe
net10.0-windows
+ true
enable
enable
true