+
+
+ @GetDisplayName()
+
+
+
+ @GetStatusText()
+
+
+
+ @if (ViewModel.SensorState.LiveData.Count == 0)
+ {
+
+ No live-data fields are defined.
+
+ }
+ else
+ {
+
+
+ @foreach (var entry in ViewModel.SensorState.LiveData)
+ {
+
+ |
+ @FormatName(entry.Name)
+ |
+
+
+ @FormatValue(entry.Value)
+ |
+
+ }
+
+
+ }
+
+}
+
+@code {
+ private IDisposable? _viewModelSubscription;
+
+ [Parameter]
+ public DelimitedStreamSensorControlViewModel? ViewModel
+ {
+ get;
+ set;
+ }
+
+ protected override void OnParametersSet()
+ {
+ _viewModelSubscription?.Dispose();
+ _viewModelSubscription = null;
+
+ if (ViewModel is null)
+ return;
+
+ _viewModelSubscription = ViewModel
+ .Changed
+ .Subscribe(
+ _ => InvokeAsync(StateHasChanged));
+ }
+
+ private string GetDisplayName()
+ {
+ if (ViewModel is null)
+ return "Delimited Stream Sensor";
+
+ if (!string.IsNullOrWhiteSpace(
+ ViewModel.SensorState.Name))
+ {
+ return ViewModel.SensorState.Name;
+ }
+
+ return ViewModel.DeviceName;
+ }
+
+ private string GetStatusText()
+ {
+ if (ViewModel?.CapturingLiveData == true)
+ return "LIVE";
+
+ if (ViewModel?.HasValidData == true)
+ return "CONNECTED";
+
+ return "INITIALIZING";
+ }
+
+ private string GetStatusClass()
+ {
+ if (ViewModel?.CapturingLiveData == true)
+ return "device-state connected";
+
+ if (ViewModel?.HasValidData == true)
+ return "device-state connected";
+
+ return "device-state initializing";
+ }
+
+ private static string GetValueClass(
+ DelimitedStreamSensorLiveDataEntry entry)
+ {
+ return entry.Value.HasValue
+ ? "parameter-value"
+ : "parameter-value unavailable";
+ }
+
+ private static string FormatValue(double? value)
+ {
+ return value.HasValue
+ ? value.Value.ToString("G6")
+ : "—";
+ }
+
+ private static string FormatName(string name)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ return "Value";
+
+ var result =
+ new System.Text.StringBuilder(
+ name.Length + 8);
+
+ for (var index = 0;
+ index < name.Length;
+ index++)
+ {
+ var current = name[index];
+
+ if (index > 0 &&
+ char.IsUpper(current) &&
+ !char.IsUpper(name[index - 1]))
+ {
+ result.Append(' ');
+ }
+
+ result.Append(current);
+ }
+
+ return result.ToString();
+ }
+
+ public void Dispose()
+ {
+ _viewModelSubscription?.Dispose();
+ _viewModelSubscription = null;
+ }
+}
diff --git a/SerialStreamingSensor/UI/DelimitedStreamSensorControl.razor.css b/SerialStreamingSensor/UI/DelimitedStreamSensorControl.razor.css
new file mode 100644
index 0000000..17971f2
--- /dev/null
+++ b/SerialStreamingSensor/UI/DelimitedStreamSensorControl.razor.css
@@ -0,0 +1,68 @@
+.device-container {
+ display: grid;
+ grid-template:
+ "header"
+ "state"
+ / minmax(0, 1fr);
+ justify-content: center;
+}
+
+.title-row {
+ grid-area: header;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ min-width: 0;
+ padding: 8px 10px;
+ gap: 10px;
+ border-radius: 6px;
+ box-sizing: border-box;
+}
+
+.device-name {
+ flex: 1 1 auto;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size:larger;
+}
+
+.device-state {
+ flex: 0 0 auto;
+}
+
+.status-connected {
+ background-color: #28a745;
+}
+
+.status-initializing {
+ background-color: #f0ad4e;
+}
+
+.status-error {
+ background-color: #dc3545;
+}
+
+.parameter-table {
+ grid-area: state;
+ width: 95%;
+ min-width: 0;
+ border-collapse: collapse;
+ table-layout: auto;
+}
+
+.parameter-name {
+ min-width: 80px;
+ white-space: normal;
+ overflow-wrap: anywhere;
+ font-size: smaller;
+}
+
+.parameter-value {
+ width: 120px;
+ min-width: 90px;
+ white-space: nowrap;
+ font-size: smaller;
+}
diff --git a/SerialStreamingSensor/UI/DelimitedStreamSensorControlViewModel.cs b/SerialStreamingSensor/UI/DelimitedStreamSensorControlViewModel.cs
new file mode 100644
index 0000000..3ff034f
--- /dev/null
+++ b/SerialStreamingSensor/UI/DelimitedStreamSensorControlViewModel.cs
@@ -0,0 +1,119 @@
+using Ares.Toolkit.Device.UI;
+using Microsoft.Extensions.Logging;
+using ReactiveUI;
+using SerialStreamingSensor.UI.State;
+using System.Reactive.Linq;
+
+namespace SerialStreamingSensor.UI
+{
+ public sealed class DelimitedStreamSensorControlViewModel
+ : DeviceUnitControlViewModel