feat(drivers): Contracts-side TagConfig Inspect for Sql, MTConnect, Calculation and OpcUaClient
Deferment-register G-7: these four were validated by the AdminUI's TagConfigValidator at authoring but by nothing at deploy, so config arriving by CSV import or API met no gate at all. The deploy gate dispatches to a driver's *TagDefinitionFactory.Inspect in its Contracts assembly, and none of the four had one. Rather than mirror the AdminUI models (a mirror of an accept/reject boundary drifts silently, with the gate describing a shape the driver no longer reads), each Inspect is built on the driver's own runtime parse: - Sql wraps the existing SqlEquipmentTagParser, which already lived in Sql.Contracts. The P3-deferred Query model stays refused; Inspect surfaces the refusal rather than weakening it. - MTConnect's blob reader MOVED out of MTConnectDriver (it was private, with a private DTO) into MTConnectTagDefinitionFactory; the driver now calls it. Behaviour unchanged. - OpcUaClient's nodeId read MOVED off NamespaceMap into OpcUaClientTagDefinitionFactory; NamespaceMap now calls it. Behaviour unchanged. - Calculation gains a Contracts assembly it never had. Only the JSON reading moved there (CalculationTagDefinitionFactory.TryReadTagConfig); building a CalculationTagDefinition still needs EquipmentScriptPaths from Commons and stays in the driver, so the split is at the blob boundary. The new project is deliberately reference-free — a calc blob is strings, a bool and an int, with no Core.Abstractions type in it. Each Inspect runs the parse FIRST and returns empty on success, so "never stricter than the runtime" is structural rather than a promise: a warning is only reachable for a blob the driver would have refused and then skipped silently. Calculation additionally reports two present-but-silently-discarded fields (a non-boolean changeTriggered reads as true; a non-positive-integer timerIntervalMs is dropped), and deliberately does NOT warn about a trigger-less tag or a sub-50ms timer — the editor refuses both, the runtime honours both, and the deploy gate follows the runtime.
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
using System.Text.Json;
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Client;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
@@ -119,37 +118,14 @@ public sealed class NamespaceMap
|
||||
foreach (var entry in rawTags)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(entry.RawPath)) continue;
|
||||
if (!TryReadNodeId(entry.TagConfig, out var nodeId)) continue;
|
||||
// The blob read lives on OpcUaClientTagDefinitionFactory so the deploy-time TagConfig gate and
|
||||
// this table build cannot disagree about what a usable nodeId is (G-7).
|
||||
if (!OpcUaClientTagDefinitionFactory.TryReadNodeId(entry.TagConfig, out var nodeId)) continue;
|
||||
table[entry.RawPath] = new RawTagBinding(nodeId, entry.WriteIdempotent);
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read the upstream node id string from an authored <c>TagConfig</c> blob's <c>nodeId</c> key
|
||||
/// (camelCase, per the AdminUI <c>OpcUaClientTagConfigModel</c>; a legacy PascalCase <c>NodeId</c>
|
||||
/// is also accepted). Never throws — a malformed blob yields <see langword="false"/>.
|
||||
/// </summary>
|
||||
private static bool TryReadNodeId(string tagConfig, out string nodeId)
|
||||
{
|
||||
nodeId = string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(tagConfig)) return false;
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(tagConfig);
|
||||
var root = doc.RootElement;
|
||||
if (root.ValueKind != JsonValueKind.Object) return false;
|
||||
if ((root.TryGetProperty("nodeId", out var n) || root.TryGetProperty("NodeId", out n))
|
||||
&& n.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var s = n.GetString();
|
||||
if (!string.IsNullOrWhiteSpace(s)) { nodeId = s; return true; }
|
||||
}
|
||||
}
|
||||
catch (JsonException) { /* malformed blob — treat as unresolvable */ }
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Number of namespaces captured. Index 0 is always the OPC UA core namespace.</summary>
|
||||
public int Count => _uris.Length;
|
||||
|
||||
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient;
|
||||
|
||||
/// <summary>
|
||||
/// The authored-raw-tag reader for the OPC UA Client (gateway) driver: one authority, consumed by both
|
||||
/// the runtime (<see cref="NamespaceMap"/>'s <c>RawPath → upstream node id</c> table build) and the
|
||||
/// deploy-time TagConfig gate (<see cref="Inspect"/>, deferment-register G-7).
|
||||
/// <para>
|
||||
/// <b>This read used to be private to <see cref="NamespaceMap"/>.</b> It moved here — unchanged —
|
||||
/// rather than being mirrored: a mirror of an accept/reject boundary drifts the moment either side
|
||||
/// is touched, and the drift is silent, with the gate describing a shape the driver no longer
|
||||
/// reads. <see cref="NamespaceMap"/> now calls <see cref="TryReadNodeId"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b><see cref="Inspect"/> is never stricter than <see cref="TryReadNodeId"/></b> — it asks the
|
||||
/// read for the verdict and only explains a rejection, so a blob the driver resolves can never draw
|
||||
/// a warning calling it invalid. Nothing else in the blob is checked: the upstream node id is an
|
||||
/// opaque reference string that the SDK re-binds against the live session, so whether it names a
|
||||
/// node that exists is knowable only at connect time, not at deploy time.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class OpcUaClientTagDefinitionFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Read the upstream node id string from an authored <c>TagConfig</c> blob's <c>nodeId</c> key
|
||||
/// (camelCase, per the AdminUI <c>OpcUaClientTagConfigModel</c>; a legacy PascalCase <c>NodeId</c>
|
||||
/// is also accepted). Never throws — a malformed blob yields <see langword="false"/>.
|
||||
/// </summary>
|
||||
/// <param name="tagConfig">The authored equipment-tag TagConfig JSON.</param>
|
||||
/// <param name="nodeId">The upstream node id string when this returns <see langword="true"/>.</param>
|
||||
/// <returns><see langword="true"/> when the blob carries a usable upstream node reference.</returns>
|
||||
public static bool TryReadNodeId(string tagConfig, out string nodeId)
|
||||
{
|
||||
nodeId = string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(tagConfig)) return false;
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(tagConfig);
|
||||
var root = doc.RootElement;
|
||||
if (root.ValueKind != JsonValueKind.Object) return false;
|
||||
if ((root.TryGetProperty("nodeId", out var n) || root.TryGetProperty("NodeId", out n))
|
||||
&& n.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var s = n.GetString();
|
||||
if (!string.IsNullOrWhiteSpace(s)) { nodeId = s; return true; }
|
||||
}
|
||||
}
|
||||
catch (JsonException) { /* malformed blob — treat as unresolvable */ }
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deploy-time inspection of an equipment-tag <c>TagConfig</c> blob: warns for a blob
|
||||
/// <see cref="TryReadNodeId"/> would reject — unparseable JSON, a non-object root, or no usable
|
||||
/// <c>nodeId</c> (absent, non-string, or blank). Such a tag is dropped from the driver's resolution
|
||||
/// table and reads <c>BadNodeIdInvalid</c> forever, which before Gitea #541 was the quietest skip
|
||||
/// of the twelve drivers — not even a log line. Empty when the blob resolves, is blank, or is not a
|
||||
/// JSON object at all. Never throws.
|
||||
/// </summary>
|
||||
/// <param name="reference">The equipment tag's TagConfig JSON.</param>
|
||||
/// <returns>The warnings; empty when clean.</returns>
|
||||
public static IReadOnlyList<string> Inspect(string reference)
|
||||
{
|
||||
var warnings = new List<string>();
|
||||
if (string.IsNullOrWhiteSpace(reference) || reference.TrimStart().FirstOrDefault() != '{')
|
||||
return warnings;
|
||||
|
||||
// The accept gate: whatever the driver resolves draws nothing. See the class remarks.
|
||||
if (TryReadNodeId(reference, out _)) return warnings;
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(reference);
|
||||
if (doc.RootElement.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
warnings.Add(
|
||||
"OpcUaClient TagConfig root is not a JSON object — the tag binds no upstream node and "
|
||||
+ "will read BadNodeIdInvalid.");
|
||||
return warnings;
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
warnings.Add("OpcUaClient TagConfig is not valid JSON — the tag will not resolve (BadNodeIdInvalid).");
|
||||
return warnings;
|
||||
}
|
||||
|
||||
warnings.Add(
|
||||
"OpcUaClient TagConfig has no usable 'nodeId' (a non-blank string holding the upstream "
|
||||
+ "nsu=…/ns=N;… reference) — the driver drops this tag from its resolution table and it reads "
|
||||
+ "BadNodeIdInvalid.");
|
||||
return warnings;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user