Merge branch 'master' into b626-opcuaclient

This commit is contained in:
Joseph Doherty
2026-10-08 14:47:49 -04:00
72 changed files with 3775 additions and 343 deletions
@@ -142,6 +142,13 @@
try
{
_keys = await Keys.ListAsync(default);
// The one-time banner is only meaningful while its key exists; the credential is
// otua_<keyId>_<secret>, so drop it once no listed key matches (deleted here or elsewhere).
if (_created is not null
&& !_keys.Any(k => _created.StartsWith($"otua_{k.ApiKeyId}_", StringComparison.Ordinal)))
{
_created = null;
}
}
catch (Exception ex)
{
@@ -30,6 +30,36 @@
<div class="form-text">Controls ISO-TSAP slot byte during handshake.</div>
</div>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label" for="s7ConnType">Connection type</label>
<InputSelect id="s7ConnType" @bind-Value="_form.ConnectionTypeText" @bind-Value:after="EmitAsync"
class="form-select form-select-sm">
<option value="">Default (slot-derived)</option>
@foreach (var v in Enum.GetValues<S7ConnectionType>())
{
<option value="@v">@v</option>
}
</InputSelect>
<div class="form-text">First byte of the remote TSAP. Leave on default unless the CPU needs a PG/OP resource.</div>
</div>
<div class="col-md-4 mb-3">
<label class="form-label" for="s7LocalTsap">Local TSAP</label>
<InputText id="s7LocalTsap" @bind-Value="_form.LocalTsap" @bind-Value:after="EmitAsync"
class="form-control form-control-sm mono" placeholder="01.00" />
<div class="form-text">Optional hex override, e.g. <code>01.00</code>.</div>
</div>
<div class="col-md-4 mb-3">
<label class="form-label" for="s7RemoteTsap">Remote TSAP</label>
<InputText id="s7RemoteTsap" @bind-Value="_form.RemoteTsap" @bind-Value:after="EmitAsync"
class="form-control form-control-sm mono" placeholder="03.01" />
<div class="form-text">Optional hex override, e.g. <code>03.01</code>. Not combinable with Connection type.</div>
</div>
</div>
@if (_form.TsapConflictError() is { } tsapError)
{
<div class="text-danger small" role="alert">@tsapError</div>
}
</div>
</section>
@@ -141,6 +171,10 @@
public S7CpuType CpuType { get; set; } = S7CpuType.S71500;
public short Rack { get; set; } = 0;
public short Slot { get; set; } = 0;
public string? LocalTsap { get; set; }
public string? RemoteTsap { get; set; }
// Bound to a <select> as text ("" = default) so the nullable enum round-trips through InputSelect.
public string ConnectionTypeText { get; set; } = "";
public int TimeoutSeconds { get; set; } = 5;
// Probe
@@ -153,6 +187,12 @@
public string? ResilienceConfig { get; set; }
public byte[] RowVersion { get; set; } = [];
/// <summary>The same error the driver factory raises for Remote TSAP + Connection type together; null when fine.</summary>
public string? TsapConflictError() =>
!string.IsNullOrWhiteSpace(RemoteTsap) && !string.IsNullOrWhiteSpace(ConnectionTypeText)
? "S7 driver config " + S7DriverOptions.TsapConflictDetail
: null;
public static FormModel FromOptions(S7DriverOptions o) => new()
{
Host = o.Host,
@@ -160,6 +200,9 @@
CpuType = o.CpuType,
Rack = o.Rack,
Slot = o.Slot,
LocalTsap = o.LocalTsap,
RemoteTsap = o.RemoteTsap,
ConnectionTypeText = o.ConnectionType?.ToString() ?? "",
TimeoutSeconds = (int)o.Timeout.TotalSeconds,
ProbeEnabled = o.Probe.Enabled,
ProbeIntervalSeconds = (int)o.Probe.Interval.TotalSeconds,
@@ -174,6 +217,9 @@
CpuType = CpuType,
Rack = Rack,
Slot = Slot,
LocalTsap = string.IsNullOrWhiteSpace(LocalTsap) ? null : LocalTsap.Trim(),
RemoteTsap = string.IsNullOrWhiteSpace(RemoteTsap) ? null : RemoteTsap.Trim(),
ConnectionType = Enum.TryParse<S7ConnectionType>(ConnectionTypeText, out var ct) ? ct : null,
Timeout = TimeSpan.FromSeconds(TimeoutSeconds),
Probe = new S7ProbeOptions
{