Merge branch 'master' into b626-opcuaclient
This commit is contained in:
@@ -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)
|
||||
{
|
||||
|
||||
+46
@@ -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
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Types;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Validation;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
|
||||
@@ -615,9 +616,16 @@ public sealed class RawTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
|
||||
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
|
||||
if (!await db.DriverInstances.AnyAsync(d => d.DriverInstanceId == driverInstanceId, ct))
|
||||
var owner = await db.DriverInstances.FirstOrDefaultAsync(d => d.DriverInstanceId == driverInstanceId, ct);
|
||||
if (owner is null)
|
||||
return new UnsMutationResult(false, $"Driver '{driverInstanceId}' not found.");
|
||||
|
||||
// #628: MTConnect owns one agent client per driver — mirror the deploy gate
|
||||
// (DraftValidator.ValidateMTConnectSingleDevice) so the second Device is refused where it is authored.
|
||||
if (string.Equals(owner.DriverType, DriverTypeNames.MTConnect, StringComparison.Ordinal)
|
||||
&& await db.Devices.AnyAsync(dev => dev.DriverInstanceId == driverInstanceId, ct))
|
||||
return new UnsMutationResult(false, DraftValidator.MTConnectMultipleDevicesMessage(owner.Name));
|
||||
|
||||
if (await db.Devices.AnyAsync(dev => dev.DriverInstanceId == driverInstanceId && dev.Name == name, ct))
|
||||
return new UnsMutationResult(false, $"A device named '{name}' already exists on this driver.");
|
||||
|
||||
|
||||
@@ -68,8 +68,11 @@ public sealed class AbLegacyTagConfigModel
|
||||
if (TagConfigJson.DescribeInvalidEnum<AbLegacyDataType>(_bag, "dataType") is { } dtError)
|
||||
return dtError;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Address))
|
||||
return "Address is required.";
|
||||
// The editor cannot see the device's PLC family, so it checks only what no family accepts (the same
|
||||
// AbLegacyAddress authority the deploy gate and runtime use) — never stricter than the gate, which adds the
|
||||
// family-specific verdict (PLC-5 octal I/O, MicroLogix-only function files).
|
||||
if (AbLegacyAddress.Validate(Address, family: null) is { } addressError)
|
||||
return addressError;
|
||||
|
||||
// DELIBERATELY STRICTER THAN THE RUNTIME, and re-confirmed as such by the Gitea #554 survey — do not
|
||||
// "align" it. PCCC data-file elements are limited to AbLegacyArray.MaxElements (256). The reader
|
||||
|
||||
@@ -229,10 +229,25 @@ public sealed class MTConnectTagConfigModel
|
||||
/// passed as null; it can only DEMOTE an item to String (DATA_SET/TABLE), which cannot create a false
|
||||
/// warning because a numeric declaration on a String-inferred item is warned either way, and a SAMPLE
|
||||
/// inferred Float64 is only warned about for a non-numeric, non-String declaration or an integer one.
|
||||
/// TIME_SERIES arrays are not detectable without it and are not covered.
|
||||
/// TIME_SERIES arrays are covered separately by the isArray rule in <see cref="Warnings"/>.
|
||||
/// </remarks>
|
||||
/// <returns>Zero or more human-readable warnings.</returns>
|
||||
public IReadOnlyList<string> Warnings()
|
||||
{
|
||||
// #627: an array (TIME_SERIES) tag is served as double[] only. Independent of the probe metadata —
|
||||
// the array flag is authored on the tag, and any other declared element type reads BadNotSupported.
|
||||
var arrayWarning = _bag.TryGetPropertyValue("isArray", out var arr)
|
||||
&& arr is JsonValue av && av.TryGetValue<bool>(out var isArray) && isArray
|
||||
&& DataType != DriverDataType.Float64
|
||||
? $"Data type {DataType} on an array tag: MTConnect TIME_SERIES arrays are served as Float64 (double[]) only; "
|
||||
+ "declare Float64 or the tag reads BadNotSupported. Saving is still allowed."
|
||||
: null;
|
||||
|
||||
var scalar = ScalarWarnings();
|
||||
return arrayWarning is null ? scalar : [arrayWarning, .. scalar];
|
||||
}
|
||||
|
||||
private IReadOnlyList<string> ScalarWarnings()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(MtCategory)) { return []; }
|
||||
|
||||
|
||||
@@ -120,7 +120,9 @@ public sealed class S7TagConfigModel
|
||||
{
|
||||
var dt = DataType;
|
||||
if (dt is S7DataType.Int64 or S7DataType.UInt64 or S7DataType.Float64
|
||||
or S7DataType.String or S7DataType.DateTime)
|
||||
or S7DataType.String or S7DataType.DateTime or S7DataType.WString or S7DataType.Char
|
||||
or S7DataType.WChar or S7DataType.Dtl or S7DataType.Date or S7DataType.Tod
|
||||
or S7DataType.Time or S7DataType.S5Time)
|
||||
{
|
||||
return $"S7 does not support {dt} arrays — use Bool (read-only), Byte, Int16, UInt16, Int32, "
|
||||
+ "UInt32 or Float32 for an array tag, or a scalar tag.";
|
||||
|
||||
@@ -48,3 +48,14 @@
|
||||
flex: 0 0 18px;
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
/* Modal backdrop overran the viewport on /raw (#631). The theme's `.rise` entrance animation runs with
|
||||
`animation-fill-mode: both`, which keeps its final keyframe's transform applied for good — and ANY
|
||||
transform (even the identity it leaves) makes that section the containing block for position:fixed
|
||||
descendants. A modal rendered inside a `.panel.rise` therefore had its fixed backdrop offset by the
|
||||
panel's left edge, so it ran 240px past the viewport and the page grew a horizontal scrollbar.
|
||||
`backwards` keeps the hidden start state during the stagger delay but drops the transform once the
|
||||
animation ends. (Upstream fix belongs in ZB.MOM.WW.Theme's theme.css.) */
|
||||
.rise {
|
||||
animation-fill-mode: backwards;
|
||||
}
|
||||
|
||||
@@ -262,12 +262,16 @@ public sealed class AdminOperationsActor : ReceiveActor
|
||||
var driverByDevice = draft.Devices
|
||||
.GroupBy(d => d.DeviceId, StringComparer.Ordinal)
|
||||
.ToDictionary(g => g.Key, g => g.First().DriverInstanceId, StringComparer.Ordinal);
|
||||
var deviceConfigById = draft.Devices
|
||||
.GroupBy(d => d.DeviceId, StringComparer.Ordinal)
|
||||
.ToDictionary(g => g.Key, g => g.First().DeviceConfig, StringComparer.Ordinal);
|
||||
var tagConfigWarnings = new List<(string TagId, string Text)>();
|
||||
foreach (var t in draft.Tags)
|
||||
{
|
||||
if (!driverByDevice.TryGetValue(t.DeviceId, out var driverInstanceId)) continue;
|
||||
if (!driverTypeById.TryGetValue(driverInstanceId, out var driverType)) continue;
|
||||
foreach (var w in EquipmentTagConfigInspector.Inspect(driverType, t.TagConfig))
|
||||
foreach (var w in EquipmentTagConfigInspector.Inspect(
|
||||
driverType, t.TagConfig, deviceConfigById.GetValueOrDefault(t.DeviceId)))
|
||||
tagConfigWarnings.Add((t.TagId, $"tag '{t.TagId}': {w}"));
|
||||
}
|
||||
if (tagConfigWarnings.Count > 0)
|
||||
|
||||
+42
@@ -1,6 +1,7 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.PlcFamilies;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Calculation;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Modbus;
|
||||
@@ -78,6 +79,47 @@ public static class EquipmentTagConfigInspector
|
||||
return Inspectors.TryGetValue(driverType, out var inspect) ? inspect(tagConfig) : Array.Empty<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Inspect(string?, string?)"/> with the owning device's <c>DeviceConfig</c>, so a driver whose
|
||||
/// address grammar depends on the device (AbLegacy: the <c>PlcFamily</c>) is checked against the family the
|
||||
/// runtime will use. Other drivers ignore the device config. A missing family key is the runtime default
|
||||
/// (SLC 500); an unrecognised value is "unknown" and only the family-independent check applies — the gate
|
||||
/// is never stricter than the runtime.
|
||||
/// </summary>
|
||||
/// <param name="driverType">The bound driver's <c>DriverType</c>.</param>
|
||||
/// <param name="tagConfig">The equipment tag's <c>TagConfig</c> JSON.</param>
|
||||
/// <param name="deviceConfig">The owning device's <c>DeviceConfig</c> JSON (may be null).</param>
|
||||
/// <returns>The warnings; empty when clean.</returns>
|
||||
public static IReadOnlyList<string> Inspect(string? driverType, string? tagConfig, string? deviceConfig)
|
||||
{
|
||||
if (string.IsNullOrEmpty(driverType) || tagConfig is null) return Array.Empty<string>();
|
||||
if (string.Equals(driverType, DriverTypeNames.AbLegacy, StringComparison.OrdinalIgnoreCase))
|
||||
return AbLegacyTagDefinitionFactory.Inspect(tagConfig, ReadAbLegacyFamily(deviceConfig));
|
||||
return Inspect(driverType, tagConfig);
|
||||
}
|
||||
|
||||
private static AbLegacyPlcFamily? ReadAbLegacyFamily(string? deviceConfig)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(deviceConfig)) return AbLegacyPlcFamily.Slc500;
|
||||
try
|
||||
{
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(deviceConfig);
|
||||
if (doc.RootElement.ValueKind != System.Text.Json.JsonValueKind.Object) return AbLegacyPlcFamily.Slc500;
|
||||
foreach (var prop in doc.RootElement.EnumerateObject())
|
||||
{
|
||||
if (!string.Equals(prop.Name, "PlcFamily", StringComparison.OrdinalIgnoreCase)) continue;
|
||||
var raw = prop.Value.ValueKind == System.Text.Json.JsonValueKind.String ? prop.Value.GetString() : null;
|
||||
if (string.IsNullOrWhiteSpace(raw)) return AbLegacyPlcFamily.Slc500;
|
||||
return Enum.TryParse<AbLegacyPlcFamily>(raw, ignoreCase: true, out var f) && Enum.IsDefined(f) ? f : null;
|
||||
}
|
||||
return AbLegacyPlcFamily.Slc500;
|
||||
}
|
||||
catch (System.Text.Json.JsonException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Whether the given driver type has a mapped inspector.</summary>
|
||||
/// <param name="driverType">The bound driver's <c>DriverType</c>.</param>
|
||||
/// <returns><see langword="true"/> when an inspector is registered for the driver type.</returns>
|
||||
|
||||
Reference in New Issue
Block a user