feat(adminui): editable FOCAS device + tag lists via CollectionEditor
This commit is contained in:
+176
-77
@@ -189,43 +189,65 @@ else
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@* Devices — read-only JSON view *@
|
@* Devices *@
|
||||||
<section class="panel rise mt-3" style="animation-delay:.20s">
|
<CollectionEditor TRow="FocasDeviceRow" Items="_devices" Title="Devices" ItemNoun="device"
|
||||||
<div class="panel-head">Devices</div>
|
AnimationDelay=".20s"
|
||||||
<div style="padding:1rem">
|
NewRow="@(() => new FocasDeviceRow())" Clone="@(r => r.Clone())"
|
||||||
<div class="form-text mb-2">
|
Validate="FocasDeviceRow.ValidateRow">
|
||||||
Each device represents one CNC. Device list editor (with CNC series selector) coming in a follow-up phase.
|
<HeaderTemplate>
|
||||||
Format: <code>[{"hostAddress":"192.168.0.10:8193","deviceName":"CNC1","series":"Thirty_i"}]</code>
|
<tr><th>Host address</th><th>CNC series</th><th>Device name</th><th></th></tr>
|
||||||
|
</HeaderTemplate>
|
||||||
|
<RowTemplate Context="d">
|
||||||
|
<td class="mono">@d.HostAddress</td><td>@d.Series</td>
|
||||||
|
<td>@(string.IsNullOrWhiteSpace(d.DeviceName) ? "—" : d.DeviceName)</td>
|
||||||
|
</RowTemplate>
|
||||||
|
<EditTemplate Context="d">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6"><label class="form-label">Host address</label>
|
||||||
|
<input class="form-control form-control-sm mono" @bind="d.HostAddress"
|
||||||
|
placeholder="192.168.0.10:8193" /></div>
|
||||||
|
<div class="col-md-3"><label class="form-label">CNC series</label>
|
||||||
|
<select class="form-select form-select-sm" @bind="d.Series">
|
||||||
|
@foreach (var e in Enum.GetValues<FocasCncSeries>()) { <option value="@e">@e</option> }
|
||||||
|
</select></div>
|
||||||
|
<div class="col-md-3"><label class="form-label">Device name</label>
|
||||||
|
<input class="form-control form-control-sm" @bind="d.DeviceName" /></div>
|
||||||
</div>
|
</div>
|
||||||
@if (_form.DevicesJson is not null)
|
</EditTemplate>
|
||||||
{
|
</CollectionEditor>
|
||||||
<pre class="form-control form-control-sm mono" style="min-height:4rem;max-height:12rem;overflow:auto;white-space:pre-wrap">@_form.DevicesJson</pre>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<p class="text-muted"><em>No devices configured.</em></p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
@* Tags — read-only JSON view *@
|
@* Tags *@
|
||||||
<section class="panel rise mt-3" style="animation-delay:.23s">
|
<CollectionEditor TRow="FocasTagRow" Items="_tags" Title="Tags" ItemNoun="tag"
|
||||||
<div class="panel-head">Tags</div>
|
AnimationDelay=".23s"
|
||||||
<div style="padding:1rem">
|
NewRow="@(() => new FocasTagRow())" Clone="@(r => r.Clone())"
|
||||||
<div class="form-text mb-2">
|
Validate="FocasTagRow.ValidateRow">
|
||||||
Tag list editor coming in a follow-up phase. Tags reference device host addresses and FOCAS address strings
|
<HeaderTemplate>
|
||||||
(e.g. <code>X0.0</code>, <code>R100</code>, <code>PARAM:1815/0</code>, <code>MACRO:500</code>).
|
<tr><th>Name</th><th>Device</th><th>Address</th><th>Type</th><th>Writable</th><th></th></tr>
|
||||||
|
</HeaderTemplate>
|
||||||
|
<RowTemplate Context="t">
|
||||||
|
<td class="mono">@t.Name</td><td class="mono">@t.DeviceHostAddress</td>
|
||||||
|
<td class="mono">@t.Address</td><td>@t.DataType</td><td>@(t.Writable ? "yes" : "no")</td>
|
||||||
|
</RowTemplate>
|
||||||
|
<EditTemplate Context="t">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6"><label class="form-label">Name</label>
|
||||||
|
<input class="form-control form-control-sm" @bind="t.Name" /></div>
|
||||||
|
<div class="col-md-6"><label class="form-label">Device host address</label>
|
||||||
|
<input class="form-control form-control-sm mono" @bind="t.DeviceHostAddress"
|
||||||
|
placeholder="192.168.0.10:8193" /></div>
|
||||||
|
<div class="col-md-6"><label class="form-label">Address</label>
|
||||||
|
<input class="form-control form-control-sm mono" @bind="t.Address"
|
||||||
|
placeholder="e.g. X0.0, R100, PARAM:1815/0, MACRO:500" /></div>
|
||||||
|
<div class="col-md-3"><label class="form-label">Data type</label>
|
||||||
|
<select class="form-select form-select-sm" @bind="t.DataType">
|
||||||
|
@foreach (var e in Enum.GetValues<FocasDataType>()) { <option value="@e">@e</option> }
|
||||||
|
</select></div>
|
||||||
|
<div class="col-md-3"><div class="form-check form-switch mt-4">
|
||||||
|
<input type="checkbox" class="form-check-input" @bind="t.Writable" id="tagWritable" />
|
||||||
|
<label class="form-check-label" for="tagWritable">Writable</label></div></div>
|
||||||
</div>
|
</div>
|
||||||
@if (_form.TagsJson is not null)
|
</EditTemplate>
|
||||||
{
|
</CollectionEditor>
|
||||||
<pre class="form-control form-control-sm mono" style="min-height:4rem;max-height:12rem;overflow:auto;white-space:pre-wrap">@_form.TagsJson</pre>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<p class="text-muted"><em>No tags configured.</em></p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<DriverResilienceSection @bind-ResilienceConfig="_form.ResilienceConfig" />
|
<DriverResilienceSection @bind-ResilienceConfig="_form.ResilienceConfig" />
|
||||||
</DriverFormShell>
|
</DriverFormShell>
|
||||||
@@ -260,6 +282,10 @@ else
|
|||||||
|
|
||||||
private void OnAddressPicked(string address) => _pickedAddress = address;
|
private void OnAddressPicked(string address) => _pickedAddress = address;
|
||||||
|
|
||||||
|
// Held separately because Devices/Tags are collections — edited via the CollectionEditor modal.
|
||||||
|
private List<FocasDeviceRow> _devices = [];
|
||||||
|
private List<FocasTagRow> _tags = [];
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
await using var db = await DbFactory.CreateDbContextAsync();
|
await using var db = await DbFactory.CreateDbContextAsync();
|
||||||
@@ -290,6 +316,8 @@ else
|
|||||||
_form = FormModel.FromOptions(opts);
|
_form = FormModel.FromOptions(opts);
|
||||||
_form.ResilienceConfig = _existing.ResilienceConfig;
|
_form.ResilienceConfig = _existing.ResilienceConfig;
|
||||||
_form.RowVersion = _existing.RowVersion;
|
_form.RowVersion = _existing.RowVersion;
|
||||||
|
_devices = opts.Devices.Select(FocasDeviceRow.FromDefinition).ToList();
|
||||||
|
_tags = opts.Tags.Select(FocasTagRow.FromDefinition).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_loaded = true;
|
_loaded = true;
|
||||||
@@ -300,7 +328,9 @@ else
|
|||||||
_busy = true; _error = null;
|
_busy = true; _error = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var opts = _form.ToOptions();
|
var opts = _form.ToOptions(
|
||||||
|
_devices.Select(r => r.ToDefinition()).ToList(),
|
||||||
|
_tags.Select(r => r.ToDefinition()).ToList());
|
||||||
var configJson = System.Text.Json.JsonSerializer.Serialize(opts, _jsonOpts);
|
var configJson = System.Text.Json.JsonSerializer.Serialize(opts, _jsonOpts);
|
||||||
await using var db = await DbFactory.CreateDbContextAsync();
|
await using var db = await DbFactory.CreateDbContextAsync();
|
||||||
if (IsNew)
|
if (IsNew)
|
||||||
@@ -371,7 +401,11 @@ else
|
|||||||
}
|
}
|
||||||
|
|
||||||
private string SerializeCurrentConfig()
|
private string SerializeCurrentConfig()
|
||||||
=> System.Text.Json.JsonSerializer.Serialize(_form.ToOptions(), _jsonOpts);
|
=> System.Text.Json.JsonSerializer.Serialize(
|
||||||
|
_form.ToOptions(
|
||||||
|
_devices.Select(r => r.ToDefinition()).ToList(),
|
||||||
|
_tags.Select(r => r.ToDefinition()).ToList()),
|
||||||
|
_jsonOpts);
|
||||||
|
|
||||||
private static FocasDriverOptions? TryDeserialize(string json)
|
private static FocasDriverOptions? TryDeserialize(string json)
|
||||||
{
|
{
|
||||||
@@ -379,6 +413,93 @@ else
|
|||||||
catch { return null; }
|
catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mutable VM for the modal editor — FocasDeviceOptions is an immutable record.
|
||||||
|
public sealed class FocasDeviceRow
|
||||||
|
{
|
||||||
|
public string HostAddress { get; set; } = "";
|
||||||
|
public FocasCncSeries Series { get; set; } = FocasCncSeries.Unknown;
|
||||||
|
public string? DeviceName { get; set; }
|
||||||
|
|
||||||
|
// Original record (null for newly-added rows). Preserves any fields the editor doesn't
|
||||||
|
// expose across a load→save.
|
||||||
|
private FocasDeviceOptions? _source;
|
||||||
|
|
||||||
|
public FocasDeviceRow Clone() => (FocasDeviceRow)MemberwiseClone(); // _source is an immutable record ref — safe to share
|
||||||
|
|
||||||
|
public static FocasDeviceRow FromDefinition(FocasDeviceOptions d) => new()
|
||||||
|
{
|
||||||
|
HostAddress = d.HostAddress, Series = d.Series, DeviceName = d.DeviceName,
|
||||||
|
_source = d,
|
||||||
|
};
|
||||||
|
|
||||||
|
public FocasDeviceOptions ToDefinition()
|
||||||
|
{
|
||||||
|
var baseDef = _source ?? new FocasDeviceOptions(HostAddress.Trim());
|
||||||
|
return baseDef with
|
||||||
|
{
|
||||||
|
HostAddress = HostAddress.Trim(),
|
||||||
|
Series = Series,
|
||||||
|
DeviceName = string.IsNullOrWhiteSpace(DeviceName) ? null : DeviceName.Trim(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? ValidateRow(FocasDeviceRow row, IReadOnlyList<FocasDeviceRow> all, int? editIndex)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(row.HostAddress)) return "Host address is required.";
|
||||||
|
for (var i = 0; i < all.Count; i++)
|
||||||
|
if (i != editIndex && string.Equals(all[i].HostAddress, row.HostAddress, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return $"Duplicate device host address '{row.HostAddress}'.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mutable VM for the modal editor — FocasTagDefinition is an immutable record.
|
||||||
|
public sealed class FocasTagRow
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string DeviceHostAddress { get; set; } = "";
|
||||||
|
public string Address { get; set; } = "";
|
||||||
|
public FocasDataType DataType { get; set; } = FocasDataType.Int32;
|
||||||
|
public bool Writable { get; set; } = true;
|
||||||
|
|
||||||
|
// Original record (null for newly-added rows). Preserves fields the editor doesn't expose
|
||||||
|
// (WriteIdempotent) across a load→save.
|
||||||
|
private FocasTagDefinition? _source;
|
||||||
|
|
||||||
|
public FocasTagRow Clone() => (FocasTagRow)MemberwiseClone(); // _source is an immutable record ref — safe to share
|
||||||
|
|
||||||
|
public static FocasTagRow FromDefinition(FocasTagDefinition d) => new()
|
||||||
|
{
|
||||||
|
Name = d.Name, DeviceHostAddress = d.DeviceHostAddress, Address = d.Address,
|
||||||
|
DataType = d.DataType, Writable = d.Writable,
|
||||||
|
_source = d,
|
||||||
|
};
|
||||||
|
|
||||||
|
public FocasTagDefinition ToDefinition()
|
||||||
|
{
|
||||||
|
var baseDef = _source ?? new FocasTagDefinition(Name.Trim(), DeviceHostAddress.Trim(), Address.Trim(), DataType);
|
||||||
|
return baseDef with
|
||||||
|
{
|
||||||
|
Name = Name.Trim(),
|
||||||
|
DeviceHostAddress = DeviceHostAddress.Trim(),
|
||||||
|
Address = Address.Trim(),
|
||||||
|
DataType = DataType,
|
||||||
|
Writable = Writable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? ValidateRow(FocasTagRow row, IReadOnlyList<FocasTagRow> all, int? editIndex)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(row.Name)) return "Name is required.";
|
||||||
|
for (var i = 0; i < all.Count; i++)
|
||||||
|
if (i != editIndex && string.Equals(all[i].Name, row.Name, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return $"Duplicate tag name '{row.Name}'.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flat mutable model — all scalar properties settable for Blazor @bind-Value.
|
||||||
|
// Collections (Devices, Tags) are kept on the component and passed in on ToOptions().
|
||||||
public sealed class FormModel
|
public sealed class FormModel
|
||||||
{
|
{
|
||||||
// Connection
|
// Connection
|
||||||
@@ -404,52 +525,30 @@ else
|
|||||||
public int FixedTreeProgramPollIntervalSeconds { get; set; } = 1;
|
public int FixedTreeProgramPollIntervalSeconds { get; set; } = 1;
|
||||||
public int FixedTreeTimerPollIntervalSeconds { get; set; } = 30;
|
public int FixedTreeTimerPollIntervalSeconds { get; set; } = 30;
|
||||||
|
|
||||||
// Collections JSON view (read-only)
|
|
||||||
public string? DevicesJson { get; set; }
|
|
||||||
public string? TagsJson { get; set; }
|
|
||||||
|
|
||||||
// Preserved originals (round-tripped unchanged)
|
|
||||||
private IReadOnlyList<FocasDeviceOptions> _devices = [];
|
|
||||||
private IReadOnlyList<FocasTagDefinition> _tags = [];
|
|
||||||
|
|
||||||
// Common
|
// Common
|
||||||
public string? ResilienceConfig { get; set; }
|
public string? ResilienceConfig { get; set; }
|
||||||
public byte[] RowVersion { get; set; } = [];
|
public byte[] RowVersion { get; set; } = [];
|
||||||
|
|
||||||
private static readonly System.Text.Json.JsonSerializerOptions _displayOpts = new()
|
public static FormModel FromOptions(FocasDriverOptions o) => new()
|
||||||
{
|
{
|
||||||
WriteIndented = true,
|
TimeoutSeconds = (int)o.Timeout.TotalSeconds,
|
||||||
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase,
|
ProbeEnabled = o.Probe.Enabled,
|
||||||
|
ProbeIntervalSeconds = (int)o.Probe.Interval.TotalSeconds,
|
||||||
|
ProbeTimeoutSeconds = (int)o.Probe.Timeout.TotalSeconds,
|
||||||
|
AdminProbeTimeoutSeconds = o.ProbeTimeoutSeconds,
|
||||||
|
AlarmProjectionEnabled = o.AlarmProjection.Enabled,
|
||||||
|
AlarmProjectionPollIntervalSeconds = (int)o.AlarmProjection.PollInterval.TotalSeconds,
|
||||||
|
HandleRecycleEnabled = o.HandleRecycle.Enabled,
|
||||||
|
HandleRecycleIntervalMinutes = (int)o.HandleRecycle.Interval.TotalMinutes,
|
||||||
|
FixedTreeEnabled = o.FixedTree.Enabled,
|
||||||
|
FixedTreePollIntervalMs = (int)o.FixedTree.PollInterval.TotalMilliseconds,
|
||||||
|
FixedTreeProgramPollIntervalSeconds = (int)o.FixedTree.ProgramPollInterval.TotalSeconds,
|
||||||
|
FixedTreeTimerPollIntervalSeconds = (int)o.FixedTree.TimerPollInterval.TotalSeconds,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static FormModel FromOptions(FocasDriverOptions o)
|
public FocasDriverOptions ToOptions(
|
||||||
{
|
IReadOnlyList<FocasDeviceOptions> devices,
|
||||||
var m = new FormModel
|
IReadOnlyList<FocasTagDefinition> tags) => new()
|
||||||
{
|
|
||||||
TimeoutSeconds = (int)o.Timeout.TotalSeconds,
|
|
||||||
ProbeEnabled = o.Probe.Enabled,
|
|
||||||
ProbeIntervalSeconds = (int)o.Probe.Interval.TotalSeconds,
|
|
||||||
ProbeTimeoutSeconds = (int)o.Probe.Timeout.TotalSeconds,
|
|
||||||
AdminProbeTimeoutSeconds = o.ProbeTimeoutSeconds,
|
|
||||||
AlarmProjectionEnabled = o.AlarmProjection.Enabled,
|
|
||||||
AlarmProjectionPollIntervalSeconds = (int)o.AlarmProjection.PollInterval.TotalSeconds,
|
|
||||||
HandleRecycleEnabled = o.HandleRecycle.Enabled,
|
|
||||||
HandleRecycleIntervalMinutes = (int)o.HandleRecycle.Interval.TotalMinutes,
|
|
||||||
FixedTreeEnabled = o.FixedTree.Enabled,
|
|
||||||
FixedTreePollIntervalMs = (int)o.FixedTree.PollInterval.TotalMilliseconds,
|
|
||||||
FixedTreeProgramPollIntervalSeconds = (int)o.FixedTree.ProgramPollInterval.TotalSeconds,
|
|
||||||
FixedTreeTimerPollIntervalSeconds = (int)o.FixedTree.TimerPollInterval.TotalSeconds,
|
|
||||||
_devices = o.Devices,
|
|
||||||
_tags = o.Tags,
|
|
||||||
};
|
|
||||||
m.DevicesJson = o.Devices.Count == 0 ? null
|
|
||||||
: System.Text.Json.JsonSerializer.Serialize(o.Devices, _displayOpts);
|
|
||||||
m.TagsJson = o.Tags.Count == 0 ? null
|
|
||||||
: System.Text.Json.JsonSerializer.Serialize(o.Tags, _displayOpts);
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
|
|
||||||
public FocasDriverOptions ToOptions() => new()
|
|
||||||
{
|
{
|
||||||
Timeout = TimeSpan.FromSeconds(TimeoutSeconds),
|
Timeout = TimeSpan.FromSeconds(TimeoutSeconds),
|
||||||
Probe = new FocasProbeOptions
|
Probe = new FocasProbeOptions
|
||||||
@@ -476,8 +575,8 @@ else
|
|||||||
ProgramPollInterval = TimeSpan.FromSeconds(FixedTreeProgramPollIntervalSeconds),
|
ProgramPollInterval = TimeSpan.FromSeconds(FixedTreeProgramPollIntervalSeconds),
|
||||||
TimerPollInterval = TimeSpan.FromSeconds(FixedTreeTimerPollIntervalSeconds),
|
TimerPollInterval = TimeSpan.FromSeconds(FixedTreeTimerPollIntervalSeconds),
|
||||||
},
|
},
|
||||||
Devices = _devices,
|
Devices = devices,
|
||||||
Tags = _tags,
|
Tags = tags,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+108
-1
@@ -2,6 +2,7 @@ using System.Text.Json;
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Shouldly;
|
using Shouldly;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers;
|
||||||
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
|
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests;
|
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests;
|
||||||
@@ -14,6 +15,12 @@ public sealed class FocasDriverPageFormSerializationTests
|
|||||||
WriteIndented = false,
|
WriteIndented = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions TestJsonOpts = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip,
|
||||||
|
};
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RoundTrip_PreservesKnownFields()
|
public void RoundTrip_PreservesKnownFields()
|
||||||
{
|
{
|
||||||
@@ -116,7 +123,7 @@ public sealed class FocasDriverPageFormSerializationTests
|
|||||||
|
|
||||||
var form = ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers
|
var form = ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers
|
||||||
.FocasDriverPage.FormModel.FromOptions(opts);
|
.FocasDriverPage.FormModel.FromOptions(opts);
|
||||||
var roundTripped = form.ToOptions();
|
var roundTripped = form.ToOptions([], []);
|
||||||
|
|
||||||
roundTripped.Timeout.ShouldBe(TimeSpan.FromSeconds(4));
|
roundTripped.Timeout.ShouldBe(TimeSpan.FromSeconds(4));
|
||||||
roundTripped.Probe.Enabled.ShouldBeTrue();
|
roundTripped.Probe.Enabled.ShouldBeTrue();
|
||||||
@@ -132,4 +139,104 @@ public sealed class FocasDriverPageFormSerializationTests
|
|||||||
roundTripped.FixedTree.ProgramPollInterval.ShouldBe(TimeSpan.FromSeconds(5));
|
roundTripped.FixedTree.ProgramPollInterval.ShouldBe(TimeSpan.FromSeconds(5));
|
||||||
roundTripped.FixedTree.TimerPollInterval.ShouldBe(TimeSpan.FromSeconds(45));
|
roundTripped.FixedTree.TimerPollInterval.ShouldBe(TimeSpan.FromSeconds(45));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeviceRow_round_trips_through_definition()
|
||||||
|
{
|
||||||
|
var row = new FocasDriverPage.FocasDeviceRow
|
||||||
|
{
|
||||||
|
HostAddress = "192.168.0.10:8193", Series = FocasCncSeries.Thirty_i, DeviceName = "CNC1",
|
||||||
|
};
|
||||||
|
var def = row.ToDefinition();
|
||||||
|
var back = FocasDriverPage.FocasDeviceRow.FromDefinition(def);
|
||||||
|
|
||||||
|
back.HostAddress.ShouldBe("192.168.0.10:8193");
|
||||||
|
back.Series.ShouldBe(FocasCncSeries.Thirty_i);
|
||||||
|
back.DeviceName.ShouldBe("CNC1");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeviceRow_preserves_unedited_fields()
|
||||||
|
{
|
||||||
|
var original = new FocasDeviceOptions("192.168.0.10:8193", "CNC1", FocasCncSeries.Thirty_i);
|
||||||
|
var row = FocasDriverPage.FocasDeviceRow.FromDefinition(original);
|
||||||
|
row.HostAddress = "192.168.0.20:8193";
|
||||||
|
|
||||||
|
var back = row.ToDefinition();
|
||||||
|
back.HostAddress.ShouldBe("192.168.0.20:8193");
|
||||||
|
back.DeviceName.ShouldBe("CNC1");
|
||||||
|
back.Series.ShouldBe(FocasCncSeries.Thirty_i);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TagRow_round_trips_through_definition()
|
||||||
|
{
|
||||||
|
var row = new FocasDriverPage.FocasTagRow
|
||||||
|
{
|
||||||
|
Name = "MacroVar", DeviceHostAddress = "192.168.0.10:8193", Address = "MACRO:500",
|
||||||
|
DataType = FocasDataType.Float64, Writable = true,
|
||||||
|
};
|
||||||
|
var def = row.ToDefinition();
|
||||||
|
var back = FocasDriverPage.FocasTagRow.FromDefinition(def);
|
||||||
|
|
||||||
|
back.Name.ShouldBe("MacroVar");
|
||||||
|
back.DeviceHostAddress.ShouldBe("192.168.0.10:8193");
|
||||||
|
back.Address.ShouldBe("MACRO:500");
|
||||||
|
back.DataType.ShouldBe(FocasDataType.Float64);
|
||||||
|
back.Writable.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TagRow_preserves_unedited_fields()
|
||||||
|
{
|
||||||
|
var original = new FocasTagDefinition(
|
||||||
|
"MacroVar", "192.168.0.10:8193", "MACRO:500", FocasDataType.Float64,
|
||||||
|
Writable: true, WriteIdempotent: true);
|
||||||
|
var row = FocasDriverPage.FocasTagRow.FromDefinition(original);
|
||||||
|
row.Name = "Renamed";
|
||||||
|
|
||||||
|
var back = row.ToDefinition();
|
||||||
|
back.Name.ShouldBe("Renamed");
|
||||||
|
back.WriteIdempotent.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateDeviceRow_rejects_duplicate_host()
|
||||||
|
{
|
||||||
|
var rows = new List<FocasDriverPage.FocasDeviceRow> { new() { HostAddress = "192.168.0.10:8193" } };
|
||||||
|
FocasDriverPage.FocasDeviceRow.ValidateRow(new() { HostAddress = "192.168.0.10:8193" }, rows, null)
|
||||||
|
.ShouldNotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateTagRow_rejects_duplicate_name()
|
||||||
|
{
|
||||||
|
var rows = new List<FocasDriverPage.FocasTagRow> { new() { Name = "MacroVar" } };
|
||||||
|
FocasDriverPage.FocasTagRow.ValidateRow(new() { Name = "MacroVar" }, rows, null)
|
||||||
|
.ShouldNotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Device_and_tag_lists_survive_options_serialize_round_trip()
|
||||||
|
{
|
||||||
|
var devices = new List<FocasDeviceOptions>
|
||||||
|
{
|
||||||
|
new("192.168.0.10:8193", "CNC1", FocasCncSeries.Thirty_i),
|
||||||
|
new("192.168.0.20:8193", "CNC2", FocasCncSeries.Zero_i_F),
|
||||||
|
};
|
||||||
|
var tags = new List<FocasTagDefinition>
|
||||||
|
{
|
||||||
|
new("MacroVar", "192.168.0.10:8193", "MACRO:500", FocasDataType.Float64),
|
||||||
|
new("Flag", "192.168.0.20:8193", "X0.0", FocasDataType.Bit),
|
||||||
|
};
|
||||||
|
var opts = new FocasDriverPage.FormModel().ToOptions(devices, tags);
|
||||||
|
var json = JsonSerializer.Serialize(opts, TestJsonOpts);
|
||||||
|
var back = JsonSerializer.Deserialize<FocasDriverOptions>(json, TestJsonOpts)!;
|
||||||
|
back.Devices.Count.ShouldBe(2);
|
||||||
|
back.Devices[0].HostAddress.ShouldBe("192.168.0.10:8193");
|
||||||
|
back.Devices[0].Series.ShouldBe(FocasCncSeries.Thirty_i);
|
||||||
|
back.Tags.Count.ShouldBe(2);
|
||||||
|
back.Tags[0].Name.ShouldBe("MacroVar");
|
||||||
|
back.Tags[0].Address.ShouldBe("MACRO:500");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user