feat(opcuaclient): subscription tuning, OperationLimits chunking, diagnostics counters, revocation options, DiscoveryUrl (#273 #275 #276 #277 #278)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+91
@@ -185,6 +185,63 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@* Subscriptions, limits, revocation, discovery (#273/#275/#277/#278) *@
|
||||
<section class="panel rise mt-3" style="animation-delay:.13s">
|
||||
<div class="panel-head">Subscriptions, limits & discovery</div>
|
||||
<div style="padding:1rem">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Publishing interval override (ms)</label>
|
||||
<InputNumber @bind-Value="_form.OpcUa.SubscriptionPublishingIntervalMs" @bind-Value:after="EmitAsync" class="form-control form-control-sm" />
|
||||
<div class="form-text">Blank = use the interval the runtime requests per subscription.</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Keep-alive count</label>
|
||||
<InputNumber @bind-Value="_form.OpcUa.SubscriptionKeepAliveCount" @bind-Value:after="EmitAsync" class="form-control form-control-sm" />
|
||||
<div class="form-text">Default 10.</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Lifetime count</label>
|
||||
<InputNumber @bind-Value="_form.OpcUa.SubscriptionLifetimeCount" @bind-Value:after="EmitAsync" class="form-control form-control-sm" />
|
||||
<div class="form-text">Default 1000; at least 3x keep-alive count.</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Max notifications per publish</label>
|
||||
<InputNumber @bind-Value="_form.OpcUa.SubscriptionMaxNotificationsPerPublish" @bind-Value:after="EmitAsync" class="form-control form-control-sm" />
|
||||
<div class="form-text">0 = server decides.</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Subscription priority (0-255)</label>
|
||||
<InputNumber @bind-Value="_form.OpcUa.SubscriptionPriority" @bind-Value:after="EmitAsync" class="form-control form-control-sm" />
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-check form-switch mt-4">
|
||||
<InputCheckbox @bind-Value="_form.OpcUa.HonorServerOperationLimits" @bind-Value:after="EmitAsync" class="form-check-input" id="honorOpLimits" />
|
||||
<label class="form-check-label" for="honorOpLimits">Honor server OperationLimits</label>
|
||||
</div>
|
||||
<div class="form-text">Split oversized Read / Write / monitored-item calls.</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-check form-switch mt-4">
|
||||
<InputCheckbox @bind-Value="_form.OpcUa.RejectUnknownRevocationStatus" @bind-Value:after="EmitAsync" class="form-check-input" id="rejectUnkRevocation" />
|
||||
<label class="form-check-label" for="rejectUnkRevocation">Reject unknown revocation status</label>
|
||||
</div>
|
||||
<div class="form-text">Revoked certificates are always rejected.</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Issuer store / CRL directory</label>
|
||||
<InputText @bind-Value="_form.OpcUa.IssuerStorePath" @bind-Value:after="EmitAsync" class="form-control form-control-sm mono" />
|
||||
<div class="form-text">CRLs are read from its <code>crl/</code> sub-folder. Blank = default PKI store.</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Discovery URL (FindServers)</label>
|
||||
<InputText @bind-Value="_form.OpcUa.DiscoveryUrl" @bind-Value:after="EmitAsync" class="form-control form-control-sm mono" placeholder="opc.tcp://lds:4840" />
|
||||
<div class="form-text">Optional. Discovered endpoints are tried before the failover list.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@* Diagnostics *@
|
||||
<section class="panel rise mt-3" style="animation-delay:.14s">
|
||||
<div class="panel-head">Diagnostics</div>
|
||||
@@ -318,6 +375,18 @@
|
||||
|
||||
public int ProbeTimeoutSeconds { get; set; } = 15;
|
||||
|
||||
// #273 / #275 / #277 / #278 — without these the typed form silently drops the keys on save (#576).
|
||||
public int? SubscriptionPublishingIntervalMs { get; set; }
|
||||
// int, not uint/byte: InputNumber<byte> 500s the page (#505).
|
||||
public int SubscriptionKeepAliveCount { get; set; } = 10;
|
||||
public int SubscriptionLifetimeCount { get; set; } = 1000;
|
||||
public int SubscriptionMaxNotificationsPerPublish { get; set; }
|
||||
public int SubscriptionPriority { get; set; }
|
||||
public bool HonorServerOperationLimits { get; set; } = true;
|
||||
public bool RejectUnknownRevocationStatus { get; set; }
|
||||
public string? IssuerStorePath { get; set; }
|
||||
public string? DiscoveryUrl { get; set; }
|
||||
|
||||
internal IReadOnlyDictionary<string, string> _unsMappingTable = new Dictionary<string, string>();
|
||||
|
||||
public static OpcUaClientFormModel FromRecord(OpcUaClientDriverOptions r) => new()
|
||||
@@ -343,6 +412,15 @@
|
||||
UserCertificatePassword = r.UserCertificatePassword,
|
||||
TargetNamespaceKind = r.TargetNamespaceKind,
|
||||
ProbeTimeoutSeconds = r.ProbeTimeoutSeconds,
|
||||
SubscriptionPublishingIntervalMs = r.SubscriptionPublishingInterval is { } spi ? (int)spi.TotalMilliseconds : null,
|
||||
SubscriptionKeepAliveCount = (int)Math.Min(r.SubscriptionKeepAliveCount, int.MaxValue),
|
||||
SubscriptionLifetimeCount = (int)Math.Min(r.SubscriptionLifetimeCount, int.MaxValue),
|
||||
SubscriptionMaxNotificationsPerPublish = (int)Math.Min(r.SubscriptionMaxNotificationsPerPublish, int.MaxValue),
|
||||
SubscriptionPriority = r.SubscriptionPriority,
|
||||
HonorServerOperationLimits = r.HonorServerOperationLimits,
|
||||
RejectUnknownRevocationStatus = r.CertificateValidation.RejectUnknownRevocationStatus,
|
||||
IssuerStorePath = r.CertificateValidation.IssuerStorePath,
|
||||
DiscoveryUrl = r.DiscoveryUrl,
|
||||
_unsMappingTable = r.UnsMappingTable,
|
||||
};
|
||||
|
||||
@@ -371,6 +449,19 @@
|
||||
TargetNamespaceKind = TargetNamespaceKind,
|
||||
UnsMappingTable = _unsMappingTable,
|
||||
ProbeTimeoutSeconds = ProbeTimeoutSeconds,
|
||||
SubscriptionPublishingInterval = SubscriptionPublishingIntervalMs is { } ms and > 0
|
||||
? TimeSpan.FromMilliseconds(ms) : null,
|
||||
SubscriptionKeepAliveCount = (uint)Math.Max(SubscriptionKeepAliveCount, 0),
|
||||
SubscriptionLifetimeCount = (uint)Math.Max(SubscriptionLifetimeCount, 0),
|
||||
SubscriptionMaxNotificationsPerPublish = (uint)Math.Max(SubscriptionMaxNotificationsPerPublish, 0),
|
||||
SubscriptionPriority = (byte)Math.Clamp(SubscriptionPriority, 0, 255),
|
||||
HonorServerOperationLimits = HonorServerOperationLimits,
|
||||
CertificateValidation = new OpcUaCertificateValidationOptions
|
||||
{
|
||||
RejectUnknownRevocationStatus = RejectUnknownRevocationStatus,
|
||||
IssuerStorePath = string.IsNullOrWhiteSpace(IssuerStorePath) ? null : IssuerStorePath,
|
||||
},
|
||||
DiscoveryUrl = string.IsNullOrWhiteSpace(DiscoveryUrl) ? null : DiscoveryUrl,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user