Phase 0 — mechanical rename ZB.MOM.WW.LmxOpcUa.* → ZB.MOM.WW.OtOpcUa.*
Renames all 11 projects (5 src + 6 tests), the .slnx solution file, all source-file namespaces, all axaml namespace references, and all v1 documentation references in CLAUDE.md and docs/*.md (excluding docs/v2/ which is already in OtOpcUa form). Also updates the TopShelf service registration name from "LmxOpcUa" to "OtOpcUa" per Phase 0 Task 0.6.
Preserves runtime identifiers per Phase 0 Out-of-Scope rules to avoid breaking v1/v2 client trust during coexistence: OPC UA `ApplicationUri` defaults (`urn:{GalaxyName}:LmxOpcUa`), server `EndpointPath` (`/LmxOpcUa`), `ServerName` default (feeds cert subject CN), `MxAccessConfiguration.ClientName` default (defensive — stays "LmxOpcUa" for MxAccess audit-trail consistency), client OPC UA identifiers (`ApplicationName = "LmxOpcUaClient"`, `ApplicationUri = "urn:localhost:LmxOpcUaClient"`, cert directory `%LocalAppData%\LmxOpcUaClient\pki\`), and the `LmxOpcUaServer` class name (class rename out of Phase 0 scope per Task 0.5 sed pattern; happens in Phase 1 alongside `LmxNodeManager → GenericDriverNodeManager` Core extraction). 23 LmxOpcUa references retained, all enumerated and justified in `docs/v2/implementation/exit-gate-phase-0.md`.
Build clean: 0 errors, 30 warnings (lower than baseline 167). Tests at strict improvement over baseline: 821 passing / 1 failing vs baseline 820 / 2 (one flaky pre-existing failure passed this run; the other still fails — both pre-existing and unrelated to the rename). `Client.UI.Tests`, `Historian.Aveva.Tests`, `Client.Shared.Tests`, `IntegrationTests` all match baseline exactly. Exit gate compliance results recorded in `docs/v2/implementation/exit-gate-phase-0.md` with all 7 checks PASS or DEFERRED-to-PR-review (#7 service install verification needs Windows service permissions on the reviewer's box).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
215
src/ZB.MOM.WW.OtOpcUa.Host/Domain/AlarmObjectFilter.cs
Normal file
215
src/ZB.MOM.WW.OtOpcUa.Host/Domain/AlarmObjectFilter.cs
Normal file
@@ -0,0 +1,215 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Serilog;
|
||||
using ZB.MOM.WW.OtOpcUa.Host.Configuration;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Compiles and applies wildcard template patterns against Galaxy objects to decide which
|
||||
/// objects should contribute alarm conditions. The filter is pure data — no OPC UA, no DB —
|
||||
/// so it is fully unit-testable with synthetic hierarchies.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Matching rules:</para>
|
||||
/// <list type="bullet">
|
||||
/// <item>An object is included when any template name in its derivation chain matches
|
||||
/// any configured pattern.</item>
|
||||
/// <item>Matching is case-insensitive and ignores the Galaxy leading <c>$</c> prefix on
|
||||
/// both the chain entry and the user pattern, so <c>TestMachine*</c> matches the stored
|
||||
/// <c>$TestMachine</c>.</item>
|
||||
/// <item>Inclusion propagates to every descendant of a matched object (containment subtree).</item>
|
||||
/// <item>Each object is evaluated once — overlapping matches never produce duplicate
|
||||
/// inclusions (set semantics).</item>
|
||||
/// </list>
|
||||
/// <para>Pattern syntax: literal text plus <c>*</c> wildcards (zero or more characters).
|
||||
/// Other regex metacharacters in the raw pattern are escaped and treated literally.</para>
|
||||
/// </remarks>
|
||||
public class AlarmObjectFilter
|
||||
{
|
||||
private static readonly ILogger Log = Serilog.Log.ForContext<AlarmObjectFilter>();
|
||||
|
||||
private readonly List<Regex> _patterns;
|
||||
private readonly List<string> _rawPatterns;
|
||||
private readonly HashSet<string> _matchedRawPatterns;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new alarm object filter from the supplied configuration section.
|
||||
/// </summary>
|
||||
/// <param name="config">The alarm filter configuration whose <see cref="AlarmFilterConfiguration.ObjectFilters"/>
|
||||
/// entries are parsed into regular expressions. Entries may themselves contain comma-separated patterns.</param>
|
||||
public AlarmObjectFilter(AlarmFilterConfiguration? config)
|
||||
{
|
||||
_patterns = new List<Regex>();
|
||||
_rawPatterns = new List<string>();
|
||||
_matchedRawPatterns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (config?.ObjectFilters == null)
|
||||
return;
|
||||
|
||||
foreach (var entry in config.ObjectFilters)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(entry))
|
||||
continue;
|
||||
|
||||
foreach (var piece in entry.Split(','))
|
||||
{
|
||||
var trimmed = piece.Trim();
|
||||
if (trimmed.Length == 0)
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
var normalized = Normalize(trimmed);
|
||||
var regex = GlobToRegex(normalized);
|
||||
_patterns.Add(regex);
|
||||
_rawPatterns.Add(trimmed);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Failed to compile alarm filter pattern {Pattern} — skipping", trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the filter has any compiled patterns. When <see langword="false"/>,
|
||||
/// callers should treat alarm tracking as unfiltered (current behavior preserved).
|
||||
/// </summary>
|
||||
public bool Enabled => _patterns.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of compiled patterns the filter will evaluate against each object.
|
||||
/// </summary>
|
||||
public int PatternCount => _patterns.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw pattern strings that did not match any object in the most recent call to
|
||||
/// <see cref="ResolveIncludedObjects"/>. Useful for startup warnings about operator typos.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> UnmatchedPatterns =>
|
||||
_rawPatterns.Where(p => !_matchedRawPatterns.Contains(p)).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw pattern strings exactly as supplied by the operator after comma-splitting
|
||||
/// and trimming. Surfaced on the status dashboard so operators can confirm the active filter.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> RawPatterns => _rawPatterns;
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see langword="true"/> when any template name in <paramref name="chain"/> matches any
|
||||
/// compiled pattern. An empty chain never matches unless the operator explicitly supplied a pattern
|
||||
/// equal to <c>*</c> (which collapses to an empty-matching regex after normalization).
|
||||
/// </summary>
|
||||
/// <param name="chain">The template derivation chain to test (own template first, ancestors after).</param>
|
||||
public bool MatchesTemplateChain(IReadOnlyList<string>? chain)
|
||||
{
|
||||
if (chain == null || chain.Count == 0 || _patterns.Count == 0)
|
||||
return false;
|
||||
|
||||
for (var i = 0; i < _patterns.Count; i++)
|
||||
{
|
||||
var regex = _patterns[i];
|
||||
for (var j = 0; j < chain.Count; j++)
|
||||
{
|
||||
var entry = chain[j];
|
||||
if (string.IsNullOrEmpty(entry))
|
||||
continue;
|
||||
if (regex.IsMatch(Normalize(entry)))
|
||||
{
|
||||
_matchedRawPatterns.Add(_rawPatterns[i]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Walks the hierarchy top-down from each root and returns the set of gobject IDs whose alarms
|
||||
/// should be monitored, honoring both template matching and descendant propagation. Returns
|
||||
/// <see langword="null"/> when the filter is disabled so callers can skip the containment check
|
||||
/// entirely.
|
||||
/// </summary>
|
||||
/// <param name="hierarchy">The full deployed Galaxy hierarchy, as returned by the repository service.</param>
|
||||
/// <returns>The set of included gobject IDs, or <see langword="null"/> when filtering is disabled.</returns>
|
||||
public HashSet<int>? ResolveIncludedObjects(IReadOnlyList<GalaxyObjectInfo>? hierarchy)
|
||||
{
|
||||
if (!Enabled)
|
||||
return null;
|
||||
|
||||
_matchedRawPatterns.Clear();
|
||||
var included = new HashSet<int>();
|
||||
if (hierarchy == null || hierarchy.Count == 0)
|
||||
return included;
|
||||
|
||||
var byId = new Dictionary<int, GalaxyObjectInfo>(hierarchy.Count);
|
||||
foreach (var obj in hierarchy)
|
||||
byId[obj.GobjectId] = obj;
|
||||
|
||||
var childrenByParent = new Dictionary<int, List<int>>();
|
||||
foreach (var obj in hierarchy)
|
||||
{
|
||||
var parentId = obj.ParentGobjectId;
|
||||
if (parentId != 0 && !byId.ContainsKey(parentId))
|
||||
parentId = 0; // orphan → treat as root
|
||||
if (!childrenByParent.TryGetValue(parentId, out var list))
|
||||
{
|
||||
list = new List<int>();
|
||||
childrenByParent[parentId] = list;
|
||||
}
|
||||
list.Add(obj.GobjectId);
|
||||
}
|
||||
|
||||
var roots = childrenByParent.TryGetValue(0, out var rootList)
|
||||
? rootList
|
||||
: new List<int>();
|
||||
|
||||
var visited = new HashSet<int>();
|
||||
var queue = new Queue<(int Id, bool ParentIncluded)>();
|
||||
foreach (var rootId in roots)
|
||||
queue.Enqueue((rootId, false));
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var (id, parentIncluded) = queue.Dequeue();
|
||||
if (!visited.Add(id))
|
||||
continue; // cycle defense
|
||||
|
||||
if (!byId.TryGetValue(id, out var obj))
|
||||
continue;
|
||||
|
||||
var nodeIncluded = parentIncluded || MatchesTemplateChain(obj.TemplateChain);
|
||||
if (nodeIncluded)
|
||||
included.Add(id);
|
||||
|
||||
if (childrenByParent.TryGetValue(id, out var children))
|
||||
foreach (var childId in children)
|
||||
queue.Enqueue((childId, nodeIncluded));
|
||||
}
|
||||
|
||||
return included;
|
||||
}
|
||||
|
||||
private static Regex GlobToRegex(string normalized)
|
||||
{
|
||||
var segments = normalized.Split('*');
|
||||
var parts = segments.Select(Regex.Escape);
|
||||
var body = string.Join(".*", parts);
|
||||
return new Regex("^" + body + "$",
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
||||
}
|
||||
|
||||
private static string Normalize(string value)
|
||||
{
|
||||
var trimmed = value.Trim();
|
||||
if (trimmed.StartsWith("$", StringComparison.Ordinal))
|
||||
return trimmed.Substring(1);
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/ZB.MOM.WW.OtOpcUa.Host/Domain/ConnectionState.cs
Normal file
38
src/ZB.MOM.WW.OtOpcUa.Host/Domain/ConnectionState.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// MXAccess connection lifecycle states. (MXA-002)
|
||||
/// </summary>
|
||||
public enum ConnectionState
|
||||
{
|
||||
/// <summary>
|
||||
/// No active session exists to the Galaxy runtime.
|
||||
/// </summary>
|
||||
Disconnected,
|
||||
|
||||
/// <summary>
|
||||
/// The bridge is opening a new MXAccess session to the runtime.
|
||||
/// </summary>
|
||||
Connecting,
|
||||
|
||||
/// <summary>
|
||||
/// The bridge has an active MXAccess session and can service reads, writes, and subscriptions.
|
||||
/// </summary>
|
||||
Connected,
|
||||
|
||||
/// <summary>
|
||||
/// The bridge is closing the current MXAccess session and draining runtime resources.
|
||||
/// </summary>
|
||||
Disconnecting,
|
||||
|
||||
/// <summary>
|
||||
/// The bridge detected a connection fault that requires operator attention or recovery logic.
|
||||
/// </summary>
|
||||
Error,
|
||||
|
||||
/// <summary>
|
||||
/// The bridge is attempting to restore service after a runtime communication failure.
|
||||
/// </summary>
|
||||
Reconnecting
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Event args for connection state transitions. (MXA-002)
|
||||
/// </summary>
|
||||
public class ConnectionStateChangedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConnectionStateChangedEventArgs" /> class.
|
||||
/// </summary>
|
||||
/// <param name="previous">The connection state being exited.</param>
|
||||
/// <param name="current">The connection state being entered.</param>
|
||||
/// <param name="message">Additional context about the transition, such as a connection fault or reconnect attempt.</param>
|
||||
public ConnectionStateChangedEventArgs(ConnectionState previous, ConnectionState current, string message = "")
|
||||
{
|
||||
PreviousState = previous;
|
||||
CurrentState = current;
|
||||
Message = message ?? "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the previous MXAccess connection state before the transition was raised.
|
||||
/// </summary>
|
||||
public ConnectionState PreviousState { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the new MXAccess connection state that the bridge moved into.
|
||||
/// </summary>
|
||||
public ConnectionState CurrentState { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets an operator-facing message that explains why the connection state changed.
|
||||
/// </summary>
|
||||
public string Message { get; }
|
||||
}
|
||||
}
|
||||
76
src/ZB.MOM.WW.OtOpcUa.Host/Domain/GalaxyAttributeInfo.cs
Normal file
76
src/ZB.MOM.WW.OtOpcUa.Host/Domain/GalaxyAttributeInfo.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// DTO matching attributes.sql result columns. (GR-002)
|
||||
/// </summary>
|
||||
public class GalaxyAttributeInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Galaxy object identifier that owns the attribute.
|
||||
/// </summary>
|
||||
public int GobjectId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Wonderware tag name used to associate the attribute with its runtime object.
|
||||
/// </summary>
|
||||
public string TagName { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the attribute name as defined on the Galaxy template or instance.
|
||||
/// </summary>
|
||||
public string AttributeName { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the fully qualified MXAccess reference used for runtime reads and writes.
|
||||
/// </summary>
|
||||
public string FullTagReference { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the numeric Galaxy data type code used to map the attribute into OPC UA.
|
||||
/// </summary>
|
||||
public int MxDataType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the human-readable Galaxy data type name returned by the repository query.
|
||||
/// </summary>
|
||||
public string DataTypeName { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the attribute is an array and should be exposed as a collection node.
|
||||
/// </summary>
|
||||
public bool IsArray { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the array length when the Galaxy attribute is modeled as a fixed-size array.
|
||||
/// </summary>
|
||||
public int? ArrayDimension { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the primitive data type name used when flattening the attribute for OPC UA clients.
|
||||
/// </summary>
|
||||
public string PrimitiveName { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the source classification that explains whether the attribute comes from configuration, calculation,
|
||||
/// or runtime data.
|
||||
/// </summary>
|
||||
public string AttributeSource { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Galaxy security classification that determines OPC UA write access.
|
||||
/// 0=FreeAccess, 1=Operate (default), 2=SecuredWrite, 3=VerifiedWrite, 4=Tune, 5=Configure, 6=ViewOnly.
|
||||
/// </summary>
|
||||
public int SecurityClassification { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the attribute has a HistoryExtension primitive and is historized by the
|
||||
/// Wonderware Historian.
|
||||
/// </summary>
|
||||
public bool IsHistorized { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the attribute has an AlarmExtension primitive and is an alarm.
|
||||
/// </summary>
|
||||
public bool IsAlarm { get; set; }
|
||||
}
|
||||
}
|
||||
64
src/ZB.MOM.WW.OtOpcUa.Host/Domain/GalaxyObjectInfo.cs
Normal file
64
src/ZB.MOM.WW.OtOpcUa.Host/Domain/GalaxyObjectInfo.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// DTO matching hierarchy.sql result columns. (GR-001)
|
||||
/// </summary>
|
||||
public class GalaxyObjectInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Galaxy object identifier used to connect hierarchy rows to attribute rows.
|
||||
/// </summary>
|
||||
public int GobjectId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the runtime tag name for the Galaxy object represented in the OPC UA tree.
|
||||
/// </summary>
|
||||
public string TagName { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the contained name shown for the object inside its parent area or object.
|
||||
/// </summary>
|
||||
public string ContainedName { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the browse name emitted into OPC UA so clients can navigate the Galaxy hierarchy.
|
||||
/// </summary>
|
||||
public string BrowseName { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the parent Galaxy object identifier that establishes the hierarchy relationship.
|
||||
/// </summary>
|
||||
public int ParentGobjectId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the row represents a Galaxy area rather than a contained object.
|
||||
/// </summary>
|
||||
public bool IsArea { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the template derivation chain for this object. Index 0 is the object's own template;
|
||||
/// subsequent entries walk up toward the most ancestral template before <c>$Object</c>. Populated by
|
||||
/// the recursive CTE in <c>hierarchy.sql</c> on <c>gobject.derived_from_gobject_id</c>. Used by
|
||||
/// <see cref="AlarmObjectFilter"/> to decide whether an object's alarms should be monitored.
|
||||
/// </summary>
|
||||
public List<string> TemplateChain { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Galaxy template category id for this object. Category 1 is $WinPlatform,
|
||||
/// 3 is $AppEngine, 13 is $Area, 10 is $UserDefined, and so on. Populated from
|
||||
/// <c>template_definition.category_id</c> by <c>hierarchy.sql</c> and consumed by the runtime
|
||||
/// status probe manager to identify hosts that should receive a <c>ScanState</c> probe.
|
||||
/// </summary>
|
||||
public int CategoryId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Galaxy object id of this object's runtime host, populated from
|
||||
/// <c>gobject.hosted_by_gobject_id</c>. Walk this chain upward to find the nearest
|
||||
/// <c>$WinPlatform</c> or <c>$AppEngine</c> ancestor for subtree quality invalidation when
|
||||
/// a runtime host is reported Stopped. Zero for root objects that have no host.
|
||||
/// </summary>
|
||||
public int HostedByGobjectId { get; set; }
|
||||
}
|
||||
}
|
||||
29
src/ZB.MOM.WW.OtOpcUa.Host/Domain/GalaxyRuntimeState.cs
Normal file
29
src/ZB.MOM.WW.OtOpcUa.Host/Domain/GalaxyRuntimeState.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Runtime state of a deployed Galaxy runtime host ($WinPlatform or $AppEngine) as
|
||||
/// observed by the bridge via its <c>ScanState</c> probe.
|
||||
/// </summary>
|
||||
public enum GalaxyRuntimeState
|
||||
{
|
||||
/// <summary>
|
||||
/// Probe advised but no callback received yet. Transitions to <see cref="Running"/>
|
||||
/// on the first successful <c>ScanState = true</c> callback, or to <see cref="Stopped"/>
|
||||
/// once the unknown-resolution timeout elapses.
|
||||
/// </summary>
|
||||
Unknown,
|
||||
|
||||
/// <summary>
|
||||
/// Last probe callback reported <c>ScanState = true</c> with a successful item status.
|
||||
/// The host is on scan and executing.
|
||||
/// </summary>
|
||||
Running,
|
||||
|
||||
/// <summary>
|
||||
/// Last probe callback reported <c>ScanState != true</c>, or a failed item status, or
|
||||
/// the initial probe never resolved before the unknown timeout elapsed. The host is
|
||||
/// off scan or unreachable.
|
||||
/// </summary>
|
||||
Stopped
|
||||
}
|
||||
}
|
||||
72
src/ZB.MOM.WW.OtOpcUa.Host/Domain/GalaxyRuntimeStatus.cs
Normal file
72
src/ZB.MOM.WW.OtOpcUa.Host/Domain/GalaxyRuntimeStatus.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using System;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Point-in-time runtime state of a single Galaxy runtime host ($WinPlatform or $AppEngine)
|
||||
/// as tracked by the <c>GalaxyRuntimeProbeManager</c>. Surfaced on the status dashboard and
|
||||
/// consumed by <c>HealthCheckService</c> so operators can detect a stopped host before
|
||||
/// downstream clients notice the stale data.
|
||||
/// </summary>
|
||||
public sealed class GalaxyRuntimeStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Galaxy tag_name of the host (e.g., <c>DevPlatform</c> or
|
||||
/// <c>DevAppEngine</c>).
|
||||
/// </summary>
|
||||
public string ObjectName { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Galaxy gobject_id of the host.
|
||||
/// </summary>
|
||||
public int GobjectId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Galaxy template category name — <c>$WinPlatform</c> or
|
||||
/// <c>$AppEngine</c>. Used by the dashboard to group hosts by kind.
|
||||
/// </summary>
|
||||
public string Kind { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current runtime state.
|
||||
/// </summary>
|
||||
public GalaxyRuntimeState State { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the UTC timestamp of the most recent probe callback, whether it
|
||||
/// reported success or failure. <see langword="null"/> before the first callback.
|
||||
/// </summary>
|
||||
public DateTime? LastStateCallbackTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the UTC timestamp of the most recent <see cref="State"/> transition.
|
||||
/// Backs the dashboard "Since" column. <see langword="null"/> in the initial Unknown
|
||||
/// state before any transition.
|
||||
/// </summary>
|
||||
public DateTime? LastStateChangeTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the last <c>ScanState</c> value received from the probe, or
|
||||
/// <see langword="null"/> before the first update or when the last callback carried
|
||||
/// a non-success item status (no value delivered).
|
||||
/// </summary>
|
||||
public bool? LastScanState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the detail message from the most recent failure callback, cleared on
|
||||
/// the next successful <c>ScanState = true</c> delivery.
|
||||
/// </summary>
|
||||
public string? LastError { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the cumulative number of callbacks where <c>ScanState = true</c>.
|
||||
/// </summary>
|
||||
public long GoodUpdateCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the cumulative number of callbacks where <c>ScanState != true</c>
|
||||
/// or the item status reported failure.
|
||||
/// </summary>
|
||||
public long FailureCount { get; set; }
|
||||
}
|
||||
}
|
||||
46
src/ZB.MOM.WW.OtOpcUa.Host/Domain/IGalaxyRepository.cs
Normal file
46
src/ZB.MOM.WW.OtOpcUa.Host/Domain/IGalaxyRepository.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for Galaxy repository database queries. (GR-001 through GR-004)
|
||||
/// </summary>
|
||||
public interface IGalaxyRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves the Galaxy object hierarchy used to construct the OPC UA browse tree.
|
||||
/// </summary>
|
||||
/// <param name="ct">A token that cancels the repository query.</param>
|
||||
/// <returns>A list of Galaxy objects ordered for address-space construction.</returns>
|
||||
Task<List<GalaxyObjectInfo>> GetHierarchyAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the Galaxy attributes that become OPC UA variables under the object hierarchy.
|
||||
/// </summary>
|
||||
/// <param name="ct">A token that cancels the repository query.</param>
|
||||
/// <returns>A list of attribute definitions with MXAccess references and type metadata.</returns>
|
||||
Task<List<GalaxyAttributeInfo>> GetAttributesAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the last Galaxy deploy timestamp used to detect metadata changes that require an address-space rebuild.
|
||||
/// </summary>
|
||||
/// <param name="ct">A token that cancels the repository query.</param>
|
||||
/// <returns>The latest deploy timestamp, or <see langword="null" /> when it cannot be determined.</returns>
|
||||
Task<DateTime?> GetLastDeployTimeAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the service can reach the Galaxy repository before it attempts to build the address space.
|
||||
/// </summary>
|
||||
/// <param name="ct">A token that cancels the connectivity check.</param>
|
||||
/// <returns><see langword="true" /> when repository access succeeds; otherwise, <see langword="false" />.</returns>
|
||||
Task<bool> TestConnectionAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the repository detects a Galaxy deployment change that should trigger an OPC UA rebuild.
|
||||
/// </summary>
|
||||
event Action? OnGalaxyChanged;
|
||||
}
|
||||
}
|
||||
79
src/ZB.MOM.WW.OtOpcUa.Host/Domain/IMxAccessClient.cs
Normal file
79
src/ZB.MOM.WW.OtOpcUa.Host/Domain/IMxAccessClient.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Abstraction over MXAccess COM client for tag read/write/subscribe operations.
|
||||
/// (MXA-001 through MXA-009, OPC-007, OPC-008, OPC-009)
|
||||
/// </summary>
|
||||
public interface IMxAccessClient : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the current runtime connectivity state for the bridge.
|
||||
/// </summary>
|
||||
ConnectionState State { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of active runtime subscriptions currently being mirrored into OPC UA.
|
||||
/// </summary>
|
||||
int ActiveSubscriptionCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of reconnect cycles attempted since the client was created.
|
||||
/// </summary>
|
||||
int ReconnectCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the MXAccess session changes state so the host can update diagnostics and retry logic.
|
||||
/// </summary>
|
||||
event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a subscribed Galaxy attribute publishes a new runtime value.
|
||||
/// </summary>
|
||||
event Action<string, Vtq>? OnTagValueChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Opens the MXAccess session required for runtime reads, writes, and subscriptions.
|
||||
/// </summary>
|
||||
/// <param name="ct">A token that cancels the connection attempt.</param>
|
||||
Task ConnectAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Closes the MXAccess session and releases runtime resources.
|
||||
/// </summary>
|
||||
Task DisconnectAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Starts monitoring a Galaxy attribute so value changes can be pushed to OPC UA subscribers.
|
||||
/// </summary>
|
||||
/// <param name="fullTagReference">The fully qualified MXAccess reference for the target attribute.</param>
|
||||
/// <param name="callback">The callback to invoke when the runtime publishes a new value for the attribute.</param>
|
||||
Task SubscribeAsync(string fullTagReference, Action<string, Vtq> callback);
|
||||
|
||||
/// <summary>
|
||||
/// Stops monitoring a Galaxy attribute when it is no longer needed by the OPC UA layer.
|
||||
/// </summary>
|
||||
/// <param name="fullTagReference">The fully qualified MXAccess reference for the target attribute.</param>
|
||||
Task UnsubscribeAsync(string fullTagReference);
|
||||
|
||||
/// <summary>
|
||||
/// Reads the current runtime value for a Galaxy attribute.
|
||||
/// </summary>
|
||||
/// <param name="fullTagReference">The fully qualified MXAccess reference for the target attribute.</param>
|
||||
/// <param name="ct">A token that cancels the read.</param>
|
||||
/// <returns>The value, timestamp, and quality returned by the runtime.</returns>
|
||||
Task<Vtq> ReadAsync(string fullTagReference, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Writes a new runtime value to a writable Galaxy attribute.
|
||||
/// </summary>
|
||||
/// <param name="fullTagReference">The fully qualified MXAccess reference for the target attribute.</param>
|
||||
/// <param name="value">The value to write to the runtime.</param>
|
||||
/// <param name="ct">A token that cancels the write.</param>
|
||||
/// <returns><see langword="true" /> when the write is accepted by the runtime; otherwise, <see langword="false" />.</returns>
|
||||
Task<bool> WriteAsync(string fullTagReference, object value, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
99
src/ZB.MOM.WW.OtOpcUa.Host/Domain/IMxProxy.cs
Normal file
99
src/ZB.MOM.WW.OtOpcUa.Host/Domain/IMxProxy.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using ArchestrA.MxAccess;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Delegate matching LMXProxyServer.OnDataChange COM event signature.
|
||||
/// </summary>
|
||||
/// <param name="hLMXServerHandle">The runtime connection handle that raised the change.</param>
|
||||
/// <param name="phItemHandle">The runtime item handle for the attribute that changed.</param>
|
||||
/// <param name="pvItemValue">The new raw runtime value for the attribute.</param>
|
||||
/// <param name="pwItemQuality">The OPC DA quality code supplied by the runtime.</param>
|
||||
/// <param name="pftItemTimeStamp">The timestamp object supplied by the runtime for the value.</param>
|
||||
/// <param name="ItemStatus">The MXAccess status payload associated with the callback.</param>
|
||||
public delegate void MxDataChangeHandler(
|
||||
int hLMXServerHandle,
|
||||
int phItemHandle,
|
||||
object pvItemValue,
|
||||
int pwItemQuality,
|
||||
object pftItemTimeStamp,
|
||||
ref MXSTATUS_PROXY[] ItemStatus);
|
||||
|
||||
/// <summary>
|
||||
/// Delegate matching LMXProxyServer.OnWriteComplete COM event signature.
|
||||
/// </summary>
|
||||
/// <param name="hLMXServerHandle">The runtime connection handle that processed the write.</param>
|
||||
/// <param name="phItemHandle">The runtime item handle that was written.</param>
|
||||
/// <param name="ItemStatus">The MXAccess status payload describing the write outcome.</param>
|
||||
public delegate void MxWriteCompleteHandler(
|
||||
int hLMXServerHandle,
|
||||
int phItemHandle,
|
||||
ref MXSTATUS_PROXY[] ItemStatus);
|
||||
|
||||
/// <summary>
|
||||
/// Abstraction over LMXProxyServer COM object to enable testing without the COM runtime. (MXA-001)
|
||||
/// </summary>
|
||||
public interface IMxProxy
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers the bridge as an MXAccess client with the runtime proxy.
|
||||
/// </summary>
|
||||
/// <param name="clientName">The client identity reported to the runtime for diagnostics and session tracking.</param>
|
||||
/// <returns>The runtime connection handle assigned to the client session.</returns>
|
||||
int Register(string clientName);
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters the bridge from the runtime proxy and releases the connection handle.
|
||||
/// </summary>
|
||||
/// <param name="handle">The connection handle returned by <see cref="Register(string)" />.</param>
|
||||
void Unregister(int handle);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a Galaxy attribute reference to the active runtime session.
|
||||
/// </summary>
|
||||
/// <param name="handle">The runtime connection handle.</param>
|
||||
/// <param name="address">The fully qualified attribute reference to resolve.</param>
|
||||
/// <returns>The runtime item handle assigned to the attribute.</returns>
|
||||
int AddItem(int handle, string address);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a previously registered attribute from the runtime session.
|
||||
/// </summary>
|
||||
/// <param name="handle">The runtime connection handle.</param>
|
||||
/// <param name="itemHandle">The item handle returned by <see cref="AddItem(int, string)" />.</param>
|
||||
void RemoveItem(int handle, int itemHandle);
|
||||
|
||||
/// <summary>
|
||||
/// Starts supervisory updates for an attribute so runtime changes are pushed to the bridge.
|
||||
/// </summary>
|
||||
/// <param name="handle">The runtime connection handle.</param>
|
||||
/// <param name="itemHandle">The item handle to monitor.</param>
|
||||
void AdviseSupervisory(int handle, int itemHandle);
|
||||
|
||||
/// <summary>
|
||||
/// Stops supervisory updates for an attribute.
|
||||
/// </summary>
|
||||
/// <param name="handle">The runtime connection handle.</param>
|
||||
/// <param name="itemHandle">The item handle to stop monitoring.</param>
|
||||
void UnAdviseSupervisory(int handle, int itemHandle);
|
||||
|
||||
/// <summary>
|
||||
/// Writes a new value to a runtime attribute through the COM proxy.
|
||||
/// </summary>
|
||||
/// <param name="handle">The runtime connection handle.</param>
|
||||
/// <param name="itemHandle">The item handle to write.</param>
|
||||
/// <param name="value">The new value to push into the runtime.</param>
|
||||
/// <param name="securityClassification">The Wonderware security classification applied to the write.</param>
|
||||
void Write(int handle, int itemHandle, object value, int securityClassification);
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the runtime pushes a data-change callback for a subscribed attribute.
|
||||
/// </summary>
|
||||
event MxDataChangeHandler? OnDataChange;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the runtime acknowledges completion of a write request.
|
||||
/// </summary>
|
||||
event MxWriteCompleteHandler? OnWriteComplete;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Pluggable interface for validating user credentials. Implement for different backing stores (config file, LDAP,
|
||||
/// etc.).
|
||||
/// </summary>
|
||||
public interface IUserAuthenticationProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates a username/password combination.
|
||||
/// </summary>
|
||||
bool ValidateCredentials(string username, string password);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extended interface for providers that can resolve application-level roles for authenticated users.
|
||||
/// When the auth provider implements this interface, OnImpersonateUser uses the returned roles
|
||||
/// to control write and alarm-ack permissions.
|
||||
/// </summary>
|
||||
public interface IRoleProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the set of application-level roles granted to the user.
|
||||
/// </summary>
|
||||
IReadOnlyList<string> GetUserRoles(string username);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Well-known application-level role names used for permission enforcement.
|
||||
/// </summary>
|
||||
public static class AppRoles
|
||||
{
|
||||
public const string ReadOnly = "ReadOnly";
|
||||
public const string WriteOperate = "WriteOperate";
|
||||
public const string WriteTune = "WriteTune";
|
||||
public const string WriteConfigure = "WriteConfigure";
|
||||
public const string AlarmAck = "AlarmAck";
|
||||
}
|
||||
}
|
||||
148
src/ZB.MOM.WW.OtOpcUa.Host/Domain/LdapAuthenticationProvider.cs
Normal file
148
src/ZB.MOM.WW.OtOpcUa.Host/Domain/LdapAuthenticationProvider.cs
Normal file
@@ -0,0 +1,148 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.DirectoryServices.Protocols;
|
||||
using System.Net;
|
||||
using Serilog;
|
||||
using ZB.MOM.WW.OtOpcUa.Host.Configuration;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates credentials via LDAP bind and resolves group membership to application roles.
|
||||
/// </summary>
|
||||
public class LdapAuthenticationProvider : IUserAuthenticationProvider, IRoleProvider
|
||||
{
|
||||
private static readonly ILogger Log = Serilog.Log.ForContext<LdapAuthenticationProvider>();
|
||||
|
||||
private readonly LdapConfiguration _config;
|
||||
private readonly Dictionary<string, string> _groupToRole;
|
||||
|
||||
public LdapAuthenticationProvider(LdapConfiguration config)
|
||||
{
|
||||
_config = config;
|
||||
_groupToRole = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ config.ReadOnlyGroup, AppRoles.ReadOnly },
|
||||
{ config.WriteOperateGroup, AppRoles.WriteOperate },
|
||||
{ config.WriteTuneGroup, AppRoles.WriteTune },
|
||||
{ config.WriteConfigureGroup, AppRoles.WriteConfigure },
|
||||
{ config.AlarmAckGroup, AppRoles.AlarmAck }
|
||||
};
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> GetUserRoles(string username)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var connection = CreateConnection())
|
||||
{
|
||||
// Bind with service account to search
|
||||
connection.Bind(new NetworkCredential(_config.ServiceAccountDn, _config.ServiceAccountPassword));
|
||||
|
||||
var request = new SearchRequest(
|
||||
_config.BaseDN,
|
||||
$"(cn={EscapeLdapFilter(username)})",
|
||||
SearchScope.Subtree,
|
||||
"memberOf");
|
||||
|
||||
var response = (SearchResponse)connection.SendRequest(request);
|
||||
|
||||
if (response.Entries.Count == 0)
|
||||
{
|
||||
Log.Warning("LDAP search returned no entries for {Username}", username);
|
||||
return new[] { AppRoles.ReadOnly }; // safe fallback
|
||||
}
|
||||
|
||||
var entry = response.Entries[0];
|
||||
var memberOf = entry.Attributes["memberOf"];
|
||||
if (memberOf == null || memberOf.Count == 0)
|
||||
{
|
||||
Log.Debug("No memberOf attributes for {Username}, defaulting to ReadOnly", username);
|
||||
return new[] { AppRoles.ReadOnly };
|
||||
}
|
||||
|
||||
var roles = new List<string>();
|
||||
for (var i = 0; i < memberOf.Count; i++)
|
||||
{
|
||||
var dn = memberOf[i]?.ToString() ?? "";
|
||||
// Extract the OU/CN from the memberOf DN (e.g., "ou=ReadWrite,ou=groups,dc=...")
|
||||
var groupName = ExtractGroupName(dn);
|
||||
if (groupName != null && _groupToRole.TryGetValue(groupName, out var role)) roles.Add(role);
|
||||
}
|
||||
|
||||
if (roles.Count == 0)
|
||||
{
|
||||
Log.Debug("No matching role groups for {Username}, defaulting to ReadOnly", username);
|
||||
roles.Add(AppRoles.ReadOnly);
|
||||
}
|
||||
|
||||
Log.Debug("LDAP roles for {Username}: [{Roles}]", username, string.Join(", ", roles));
|
||||
return roles;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Failed to resolve LDAP roles for {Username}, defaulting to ReadOnly", username);
|
||||
return new[] { AppRoles.ReadOnly };
|
||||
}
|
||||
}
|
||||
|
||||
public bool ValidateCredentials(string username, string password)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bindDn = _config.BindDnTemplate.Replace("{username}", username);
|
||||
using (var connection = CreateConnection())
|
||||
{
|
||||
connection.Bind(new NetworkCredential(bindDn, password));
|
||||
}
|
||||
|
||||
Log.Debug("LDAP bind succeeded for {Username}", username);
|
||||
return true;
|
||||
}
|
||||
catch (LdapException ex)
|
||||
{
|
||||
Log.Debug("LDAP bind failed for {Username}: {Error}", username, ex.Message);
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "LDAP error during credential validation for {Username}", username);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private LdapConnection CreateConnection()
|
||||
{
|
||||
var identifier = new LdapDirectoryIdentifier(_config.Host, _config.Port);
|
||||
var connection = new LdapConnection(identifier)
|
||||
{
|
||||
AuthType = AuthType.Basic,
|
||||
Timeout = TimeSpan.FromSeconds(_config.TimeoutSeconds)
|
||||
};
|
||||
connection.SessionOptions.ProtocolVersion = 3;
|
||||
return connection;
|
||||
}
|
||||
|
||||
private static string? ExtractGroupName(string dn)
|
||||
{
|
||||
// Parse "ou=ReadWrite,ou=groups,dc=..." or "cn=ReadWrite,..."
|
||||
if (string.IsNullOrEmpty(dn)) return null;
|
||||
var parts = dn.Split(',');
|
||||
if (parts.Length == 0) return null;
|
||||
var first = parts[0].Trim();
|
||||
var eqIdx = first.IndexOf('=');
|
||||
return eqIdx >= 0 ? first.Substring(eqIdx + 1) : null;
|
||||
}
|
||||
|
||||
private static string EscapeLdapFilter(string input)
|
||||
{
|
||||
return input
|
||||
.Replace("\\", "\\5c")
|
||||
.Replace("*", "\\2a")
|
||||
.Replace("(", "\\28")
|
||||
.Replace(")", "\\29")
|
||||
.Replace("\0", "\\00");
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/ZB.MOM.WW.OtOpcUa.Host/Domain/LmxRoleIds.cs
Normal file
18
src/ZB.MOM.WW.OtOpcUa.Host/Domain/LmxRoleIds.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Stable identifiers for custom OPC UA roles mapped from LDAP groups.
|
||||
/// The namespace URI is registered in the server namespace table at startup,
|
||||
/// and the string identifiers are resolved to runtime NodeIds before use.
|
||||
/// </summary>
|
||||
public static class LmxRoleIds
|
||||
{
|
||||
public const string NamespaceUri = "urn:zbmom:lmxopcua:roles";
|
||||
|
||||
public const string ReadOnly = "Role.ReadOnly";
|
||||
public const string WriteOperate = "Role.WriteOperate";
|
||||
public const string WriteTune = "Role.WriteTune";
|
||||
public const string WriteConfigure = "Role.WriteConfigure";
|
||||
public const string AlarmAck = "Role.AlarmAck";
|
||||
}
|
||||
}
|
||||
87
src/ZB.MOM.WW.OtOpcUa.Host/Domain/MxDataTypeMapper.cs
Normal file
87
src/ZB.MOM.WW.OtOpcUa.Host/Domain/MxDataTypeMapper.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using System;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps Galaxy mx_data_type integers to OPC UA data types and CLR types. (OPC-005)
|
||||
/// See gr/data_type_mapping.md for full mapping table.
|
||||
/// </summary>
|
||||
public static class MxDataTypeMapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps mx_data_type to OPC UA DataType NodeId numeric identifier.
|
||||
/// Unknown types default to String (i=12).
|
||||
/// </summary>
|
||||
/// <param name="mxDataType">The Galaxy MX data type code.</param>
|
||||
/// <returns>The OPC UA built-in data type node identifier.</returns>
|
||||
public static uint MapToOpcUaDataType(int mxDataType)
|
||||
{
|
||||
return mxDataType switch
|
||||
{
|
||||
1 => 1, // Boolean → i=1
|
||||
2 => 6, // Integer → Int32 i=6
|
||||
3 => 10, // Float → Float i=10
|
||||
4 => 11, // Double → Double i=11
|
||||
5 => 12, // String → String i=12
|
||||
6 => 13, // Time → DateTime i=13
|
||||
7 => 11, // ElapsedTime → Double i=11 (seconds)
|
||||
8 => 12, // Reference → String i=12
|
||||
13 => 6, // Enumeration → Int32 i=6
|
||||
14 => 12, // Custom → String i=12
|
||||
15 => 21, // InternationalizedString → LocalizedText i=21
|
||||
16 => 12, // Custom → String i=12
|
||||
_ => 12 // Unknown → String i=12
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps mx_data_type to the corresponding CLR type.
|
||||
/// </summary>
|
||||
/// <param name="mxDataType">The Galaxy MX data type code.</param>
|
||||
/// <returns>The CLR type used to represent runtime values for the MX type.</returns>
|
||||
public static Type MapToClrType(int mxDataType)
|
||||
{
|
||||
return mxDataType switch
|
||||
{
|
||||
1 => typeof(bool),
|
||||
2 => typeof(int),
|
||||
3 => typeof(float),
|
||||
4 => typeof(double),
|
||||
5 => typeof(string),
|
||||
6 => typeof(DateTime),
|
||||
7 => typeof(double), // ElapsedTime as seconds
|
||||
8 => typeof(string), // Reference as string
|
||||
13 => typeof(int), // Enum backing integer
|
||||
14 => typeof(string),
|
||||
15 => typeof(string), // LocalizedText stored as string
|
||||
16 => typeof(string),
|
||||
_ => typeof(string)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the OPC UA type name for a given mx_data_type.
|
||||
/// </summary>
|
||||
/// <param name="mxDataType">The Galaxy MX data type code.</param>
|
||||
/// <returns>The OPC UA type name used in diagnostics.</returns>
|
||||
public static string GetOpcUaTypeName(int mxDataType)
|
||||
{
|
||||
return mxDataType switch
|
||||
{
|
||||
1 => "Boolean",
|
||||
2 => "Int32",
|
||||
3 => "Float",
|
||||
4 => "Double",
|
||||
5 => "String",
|
||||
6 => "DateTime",
|
||||
7 => "Double",
|
||||
8 => "String",
|
||||
13 => "Int32",
|
||||
14 => "String",
|
||||
15 => "LocalizedText",
|
||||
16 => "String",
|
||||
_ => "String"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
76
src/ZB.MOM.WW.OtOpcUa.Host/Domain/MxErrorCodes.cs
Normal file
76
src/ZB.MOM.WW.OtOpcUa.Host/Domain/MxErrorCodes.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Translates MXAccess error codes (1008, 1012, 1013, etc.) to human-readable messages. (MXA-009)
|
||||
/// </summary>
|
||||
public static class MxErrorCodes
|
||||
{
|
||||
/// <summary>
|
||||
/// The requested Galaxy attribute reference does not resolve in the runtime.
|
||||
/// </summary>
|
||||
public const int MX_E_InvalidReference = 1008;
|
||||
|
||||
/// <summary>
|
||||
/// The supplied value does not match the attribute's configured data type.
|
||||
/// </summary>
|
||||
public const int MX_E_WrongDataType = 1012;
|
||||
|
||||
/// <summary>
|
||||
/// The target attribute cannot be written because it is read-only or protected.
|
||||
/// </summary>
|
||||
public const int MX_E_NotWritable = 1013;
|
||||
|
||||
/// <summary>
|
||||
/// The runtime did not complete the operation within the configured timeout.
|
||||
/// </summary>
|
||||
public const int MX_E_RequestTimedOut = 1014;
|
||||
|
||||
/// <summary>
|
||||
/// Communication with the MXAccess runtime failed during the operation.
|
||||
/// </summary>
|
||||
public const int MX_E_CommFailure = 1015;
|
||||
|
||||
/// <summary>
|
||||
/// The operation was attempted without an active MXAccess session.
|
||||
/// </summary>
|
||||
public const int MX_E_NotConnected = 1016;
|
||||
|
||||
/// <summary>
|
||||
/// Converts a numeric MXAccess error code into an operator-facing message.
|
||||
/// </summary>
|
||||
/// <param name="errorCode">The MXAccess error code returned by the runtime.</param>
|
||||
/// <returns>A human-readable description of the runtime failure.</returns>
|
||||
public static string GetMessage(int errorCode)
|
||||
{
|
||||
return errorCode switch
|
||||
{
|
||||
1008 => "Invalid reference: the tag address does not exist or is malformed",
|
||||
1012 => "Wrong data type: the value type does not match the attribute's expected type",
|
||||
1013 => "Not writable: the attribute is read-only or locked",
|
||||
1014 => "Request timed out: the operation did not complete within the allowed time",
|
||||
1015 => "Communication failure: lost connection to the runtime",
|
||||
1016 => "Not connected: no active connection to the Galaxy runtime",
|
||||
_ => $"Unknown MXAccess error code: {errorCode}"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps an MXAccess error code to the OPC quality state that should be exposed to clients.
|
||||
/// </summary>
|
||||
/// <param name="errorCode">The MXAccess error code returned by the runtime.</param>
|
||||
/// <returns>The quality classification that best represents the runtime failure.</returns>
|
||||
public static Quality MapToQuality(int errorCode)
|
||||
{
|
||||
return errorCode switch
|
||||
{
|
||||
1008 => Quality.BadConfigError,
|
||||
1012 => Quality.BadConfigError,
|
||||
1013 => Quality.BadOutOfService,
|
||||
1014 => Quality.BadCommFailure,
|
||||
1015 => Quality.BadCommFailure,
|
||||
1016 => Quality.BadNotConnected,
|
||||
_ => Quality.Bad
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/ZB.MOM.WW.OtOpcUa.Host/Domain/PlatformInfo.cs
Normal file
18
src/ZB.MOM.WW.OtOpcUa.Host/Domain/PlatformInfo.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps a deployed Galaxy platform to the hostname where it executes.
|
||||
/// </summary>
|
||||
public class PlatformInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the gobject_id of the platform object in the Galaxy repository.
|
||||
/// </summary>
|
||||
public int GobjectId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the hostname (node_name) where the platform is deployed.
|
||||
/// </summary>
|
||||
public string NodeName { get; set; } = "";
|
||||
}
|
||||
}
|
||||
122
src/ZB.MOM.WW.OtOpcUa.Host/Domain/Quality.cs
Normal file
122
src/ZB.MOM.WW.OtOpcUa.Host/Domain/Quality.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// OPC DA quality codes mapped from MXAccess quality values. (MXA-009, OPC-005)
|
||||
/// </summary>
|
||||
public enum Quality : byte
|
||||
{
|
||||
// Bad family (0-63)
|
||||
/// <summary>
|
||||
/// No valid process value is available.
|
||||
/// </summary>
|
||||
Bad = 0,
|
||||
|
||||
/// <summary>
|
||||
/// The value is invalid because the Galaxy attribute definition or mapping is wrong.
|
||||
/// </summary>
|
||||
BadConfigError = 4,
|
||||
|
||||
/// <summary>
|
||||
/// The bridge is not currently connected to the Galaxy runtime.
|
||||
/// </summary>
|
||||
BadNotConnected = 8,
|
||||
|
||||
/// <summary>
|
||||
/// The runtime device or adapter failed while obtaining the value.
|
||||
/// </summary>
|
||||
BadDeviceFailure = 12,
|
||||
|
||||
/// <summary>
|
||||
/// The underlying field source reported a bad sensor condition.
|
||||
/// </summary>
|
||||
BadSensorFailure = 16,
|
||||
|
||||
/// <summary>
|
||||
/// Communication with the runtime failed while retrieving the value.
|
||||
/// </summary>
|
||||
BadCommFailure = 20,
|
||||
|
||||
/// <summary>
|
||||
/// The attribute is intentionally unavailable for service, such as a locked or unwritable value.
|
||||
/// </summary>
|
||||
BadOutOfService = 24,
|
||||
|
||||
/// <summary>
|
||||
/// The bridge is still waiting for the first usable value after startup or resubscription.
|
||||
/// </summary>
|
||||
BadWaitingForInitialData = 32,
|
||||
|
||||
// Uncertain family (64-191)
|
||||
/// <summary>
|
||||
/// A value is available, but it should be treated cautiously.
|
||||
/// </summary>
|
||||
Uncertain = 64,
|
||||
|
||||
/// <summary>
|
||||
/// The last usable value is being repeated because a newer one is unavailable.
|
||||
/// </summary>
|
||||
UncertainLastUsable = 68,
|
||||
|
||||
/// <summary>
|
||||
/// The sensor or source is providing a value with reduced accuracy.
|
||||
/// </summary>
|
||||
UncertainSensorNotAccurate = 80,
|
||||
|
||||
/// <summary>
|
||||
/// The value exceeds its engineered limits.
|
||||
/// </summary>
|
||||
UncertainEuExceeded = 84,
|
||||
|
||||
/// <summary>
|
||||
/// The source is operating in a degraded or subnormal state.
|
||||
/// </summary>
|
||||
UncertainSubNormal = 88,
|
||||
|
||||
// Good family (192+)
|
||||
/// <summary>
|
||||
/// The value is current and suitable for normal client use.
|
||||
/// </summary>
|
||||
Good = 192,
|
||||
|
||||
/// <summary>
|
||||
/// The value is good but currently overridden locally rather than flowing from the live source.
|
||||
/// </summary>
|
||||
GoodLocalOverride = 216
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper methods for reasoning about OPC quality families used by the bridge.
|
||||
/// </summary>
|
||||
public static class QualityExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether the quality represents a good runtime value that can be trusted by OPC UA clients.
|
||||
/// </summary>
|
||||
/// <param name="q">The quality code to inspect.</param>
|
||||
/// <returns><see langword="true" /> when the value is in the good quality range; otherwise, <see langword="false" />.</returns>
|
||||
public static bool IsGood(this Quality q)
|
||||
{
|
||||
return (byte)q >= 192;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the quality represents an uncertain runtime value that should be treated cautiously.
|
||||
/// </summary>
|
||||
/// <param name="q">The quality code to inspect.</param>
|
||||
/// <returns><see langword="true" /> when the value is in the uncertain range; otherwise, <see langword="false" />.</returns>
|
||||
public static bool IsUncertain(this Quality q)
|
||||
{
|
||||
return (byte)q >= 64 && (byte)q < 192;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the quality represents a bad runtime value that should not be used as valid process data.
|
||||
/// </summary>
|
||||
/// <param name="q">The quality code to inspect.</param>
|
||||
/// <returns><see langword="true" /> when the value is in the bad range; otherwise, <see langword="false" />.</returns>
|
||||
public static bool IsBad(this Quality q)
|
||||
{
|
||||
return (byte)q < 64;
|
||||
}
|
||||
}
|
||||
}
|
||||
60
src/ZB.MOM.WW.OtOpcUa.Host/Domain/QualityMapper.cs
Normal file
60
src/ZB.MOM.WW.OtOpcUa.Host/Domain/QualityMapper.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps MXAccess integer quality to domain Quality enum and OPC UA StatusCodes. (MXA-009, OPC-005)
|
||||
/// </summary>
|
||||
public static class QualityMapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps an MXAccess quality integer (OPC DA quality byte) to domain Quality.
|
||||
/// Uses category bits: 192+ = Good, 64-191 = Uncertain, 0-63 = Bad.
|
||||
/// </summary>
|
||||
/// <param name="mxQuality">The raw MXAccess quality integer.</param>
|
||||
/// <returns>The mapped bridge quality value.</returns>
|
||||
public static Quality MapFromMxAccessQuality(int mxQuality)
|
||||
{
|
||||
var b = (byte)(mxQuality & 0xFF);
|
||||
|
||||
// Try exact match first
|
||||
if (Enum.IsDefined(typeof(Quality), b))
|
||||
return (Quality)b;
|
||||
|
||||
// Fall back to category
|
||||
if (b >= 192) return Quality.Good;
|
||||
if (b >= 64) return Quality.Uncertain;
|
||||
return Quality.Bad;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps domain Quality to OPC UA StatusCode uint32.
|
||||
/// </summary>
|
||||
/// <param name="quality">The bridge quality value.</param>
|
||||
/// <returns>The OPC UA status code represented as a 32-bit unsigned integer.</returns>
|
||||
public static uint MapToOpcUaStatusCode(Quality quality)
|
||||
{
|
||||
return quality switch
|
||||
{
|
||||
Quality.Good => 0x00000000u, // Good
|
||||
Quality.GoodLocalOverride => 0x00D80000u, // Good_LocalOverride
|
||||
Quality.Uncertain => 0x40000000u, // Uncertain
|
||||
Quality.UncertainLastUsable => 0x40900000u,
|
||||
Quality.UncertainSensorNotAccurate => 0x40930000u,
|
||||
Quality.UncertainEuExceeded => 0x40940000u,
|
||||
Quality.UncertainSubNormal => 0x40950000u,
|
||||
Quality.Bad => 0x80000000u, // Bad
|
||||
Quality.BadConfigError => 0x80890000u,
|
||||
Quality.BadNotConnected => 0x808A0000u,
|
||||
Quality.BadDeviceFailure => 0x808B0000u,
|
||||
Quality.BadSensorFailure => 0x808C0000u,
|
||||
Quality.BadCommFailure => 0x80050000u,
|
||||
Quality.BadOutOfService => 0x808D0000u,
|
||||
Quality.BadWaitingForInitialData => 0x80320000u,
|
||||
_ => quality.IsGood() ? 0x00000000u :
|
||||
quality.IsUncertain() ? 0x40000000u :
|
||||
0x80000000u
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps Galaxy security classification values to OPC UA write access decisions.
|
||||
/// See gr/data_type_mapping.md for the full mapping table.
|
||||
/// </summary>
|
||||
public static class SecurityClassificationMapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether an attribute with the given security classification should allow writes.
|
||||
/// </summary>
|
||||
/// <param name="securityClassification">The Galaxy security classification value.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true" /> for FreeAccess (0), Operate (1), Tune (4), Configure (5);
|
||||
/// <see langword="false" /> for SecuredWrite (2), VerifiedWrite (3), ViewOnly (6).
|
||||
/// </returns>
|
||||
public static bool IsWritable(int securityClassification)
|
||||
{
|
||||
switch (securityClassification)
|
||||
{
|
||||
case 2: // SecuredWrite
|
||||
case 3: // VerifiedWrite
|
||||
case 6: // ViewOnly
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
96
src/ZB.MOM.WW.OtOpcUa.Host/Domain/Vtq.cs
Normal file
96
src/ZB.MOM.WW.OtOpcUa.Host/Domain/Vtq.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using System;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Value-Timestamp-Quality triplet for tag data. (MXA-003, OPC-007)
|
||||
/// </summary>
|
||||
public readonly struct Vtq : IEquatable<Vtq>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the runtime value returned for the Galaxy attribute.
|
||||
/// </summary>
|
||||
public object? Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the timestamp associated with the runtime value.
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the quality classification that tells OPC UA clients whether the value is usable.
|
||||
/// </summary>
|
||||
public Quality Quality { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Vtq" /> struct for a Galaxy attribute value.
|
||||
/// </summary>
|
||||
/// <param name="value">The runtime value returned by MXAccess.</param>
|
||||
/// <param name="timestamp">The timestamp assigned to the runtime value.</param>
|
||||
/// <param name="quality">The quality classification for the runtime value.</param>
|
||||
public Vtq(object? value, DateTime timestamp, Quality quality)
|
||||
{
|
||||
Value = value;
|
||||
Timestamp = timestamp;
|
||||
Quality = quality;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a good-quality VTQ snapshot for a successfully read or subscribed attribute value.
|
||||
/// </summary>
|
||||
/// <param name="value">The runtime value to wrap.</param>
|
||||
/// <returns>A VTQ carrying the provided value with the current UTC timestamp and good quality.</returns>
|
||||
public static Vtq Good(object? value)
|
||||
{
|
||||
return new Vtq(value, DateTime.UtcNow, Quality.Good);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a bad-quality VTQ snapshot when no usable runtime value is available.
|
||||
/// </summary>
|
||||
/// <param name="quality">The specific bad quality reason to expose to clients.</param>
|
||||
/// <returns>A VTQ with no value, the current UTC timestamp, and the requested bad quality.</returns>
|
||||
public static Vtq Bad(Quality quality = Quality.Bad)
|
||||
{
|
||||
return new Vtq(null, DateTime.UtcNow, quality);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an uncertain VTQ snapshot when the runtime value exists but should be treated cautiously.
|
||||
/// </summary>
|
||||
/// <param name="value">The runtime value to wrap.</param>
|
||||
/// <returns>A VTQ carrying the provided value with the current UTC timestamp and uncertain quality.</returns>
|
||||
public static Vtq Uncertain(object? value)
|
||||
{
|
||||
return new Vtq(value, DateTime.UtcNow, Quality.Uncertain);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares two VTQ snapshots for exact value, timestamp, and quality equality.
|
||||
/// </summary>
|
||||
/// <param name="other">The other VTQ snapshot to compare.</param>
|
||||
/// <returns><see langword="true" /> when all fields match; otherwise, <see langword="false" />.</returns>
|
||||
public bool Equals(Vtq other)
|
||||
{
|
||||
return Equals(Value, other.Value) && Timestamp == other.Timestamp && Quality == other.Quality;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is Vtq other && Equals(other);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Value, Timestamp, Quality);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Vtq({Value}, {Timestamp:O}, {Quality})";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user