PR 4.2 — IReadable abstraction + StatusCodeMap + MxValueDecoder
Read path scaffold + the byte→uint quality mapping table that the parity matrix (PR 5.x) pins. PR 4.4 supplies the production GW-backed reader; this PR ships the abstraction and the supporting infrastructure so 4.4 just plugs the implementation in. Files: - Runtime/StatusCodeMap.cs — explicit OPC DA quality byte → OPC UA StatusCode uint mapping. Extends the legacy Galaxy.Host HistorianQualityMapper with named constants (Good / GoodLocalOverride, Uncertain + 4 substatuses, Bad + 7 substatuses, BadInternalError) and an MxStatusProxy → uint helper that honors success flag → detail byte → detected_by transport-error fallback. Unknown bytes fall back to category bucket with a once-per-session diagnostic log so field captures can extend the table. - Runtime/MxValueDecoder.cs — gateway MxValue → boxed CLR value for the seven Galaxy data types (Boolean, Int32, Int64, Float32, Float64, String, DateTime) plus their array variants. Honors MxValue.IsNull and RawValue passthrough. - Runtime/IGalaxyDataReader.cs — driver-side seam for one-shot reads. PR 4.4 ships the production wrapper around MxGatewaySession.SubscribeBulk + StreamEvents + UnsubscribeBulk; this PR exposes the contract so GalaxyDriver.ReadAsync wires through it. - Runtime/GalaxyMxSession.cs — wrapper around MxGatewaySession that owns the Register handle. ConnectAsync opens session + Register; AttachForTests lets tests bypass real gw construction. PR 4.3/4.4/4.5 add write, subscribe, and reconnect surfaces. GalaxyDriver: - Implements IReadable. ReadAsync routes through the injected IGalaxyDataReader (test seam) when present; production path throws NotSupportedException pointing at PR 4.4 — protects deployments running this PR from silent wrong reads while signaling that the legacy-host backend (Galaxy:Backend=legacy-host) handles reads in the meantime. - Internal ctor extended with optional dataReader parameter (default null, preserves PR 4.0/4.1 callers). Tests: 42 new — exhaustive byte→uint table for StatusCodeMap (15 known codes + category-bucket fallback for unknowns + MxStatusProxy precedence rules + OPC UA top-byte invariants), every MxValue oneof case for the decoder (bool/int32/int64/float/double/string/timestamp/3 array variants/ raw bytes/null), GalaxyDriver IReadable wiring (route-through, empty- request, no-reader-throws, post-dispose-throws, status-code preservation). 62 Galaxy tests total pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ using MxGateway.Client;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browse;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy;
|
||||
|
||||
@@ -22,7 +23,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy;
|
||||
/// <see cref="GalaxyDriverFactoryExtensions"/> registers under driver-type name
|
||||
/// "GalaxyMxGateway" so both paths can be live simultaneously during parity testing.
|
||||
/// </remarks>
|
||||
public sealed class GalaxyDriver : IDriver, ITagDiscovery, IDisposable
|
||||
public sealed class GalaxyDriver : IDriver, ITagDiscovery, IReadable, IDisposable
|
||||
{
|
||||
private readonly string _driverInstanceId;
|
||||
private readonly GalaxyDriverOptions _options;
|
||||
@@ -35,6 +36,13 @@ public sealed class GalaxyDriver : IDriver, ITagDiscovery, IDisposable
|
||||
private IGalaxyHierarchySource? _hierarchySource;
|
||||
private GalaxyRepositoryClient? _ownedRepositoryClient;
|
||||
|
||||
// PR 4.2 — IGalaxyDataReader is the test seam for IReadable. PR 4.4 supplies the
|
||||
// production implementation that wraps GalaxyMxSession's SubscribeBulk + StreamEvents
|
||||
// pump; until then ReadAsync throws NotSupportedException when the reader is null
|
||||
// (legacy-host backend handles reads in production via DriverNodeManager's
|
||||
// capability-routing).
|
||||
private readonly IGalaxyDataReader? _dataReader;
|
||||
|
||||
private DriverHealth _health = new(DriverState.Unknown, null, null);
|
||||
private bool _disposed;
|
||||
|
||||
@@ -42,19 +50,20 @@ public sealed class GalaxyDriver : IDriver, ITagDiscovery, IDisposable
|
||||
string driverInstanceId,
|
||||
GalaxyDriverOptions options,
|
||||
ILogger<GalaxyDriver>? logger = null)
|
||||
: this(driverInstanceId, options, hierarchySource: null, logger)
|
||||
: this(driverInstanceId, options, hierarchySource: null, dataReader: null, logger)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test-visible ctor — inject a custom <see cref="IGalaxyHierarchySource"/> so
|
||||
/// <see cref="DiscoverAsync"/> can be exercised against canned hierarchies without
|
||||
/// building a real gRPC channel.
|
||||
/// Test-visible ctor — inject custom seams so <see cref="DiscoverAsync"/> +
|
||||
/// <see cref="ReadAsync"/> can be exercised against canned data without building
|
||||
/// real gRPC channels.
|
||||
/// </summary>
|
||||
internal GalaxyDriver(
|
||||
string driverInstanceId,
|
||||
GalaxyDriverOptions options,
|
||||
IGalaxyHierarchySource? hierarchySource,
|
||||
IGalaxyDataReader? dataReader = null,
|
||||
ILogger<GalaxyDriver>? logger = null)
|
||||
{
|
||||
_driverInstanceId = !string.IsNullOrWhiteSpace(driverInstanceId)
|
||||
@@ -63,6 +72,7 @@ public sealed class GalaxyDriver : IDriver, ITagDiscovery, IDisposable
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? NullLogger<GalaxyDriver>.Instance;
|
||||
_hierarchySource = hierarchySource;
|
||||
_dataReader = dataReader;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -130,6 +140,31 @@ public sealed class GalaxyDriver : IDriver, ITagDiscovery, IDisposable
|
||||
await discoverer.DiscoverAsync(builder, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// ===== IReadable (PR 4.2 — abstraction; PR 4.4 supplies production reader) =====
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<DataValueSnapshot>> ReadAsync(
|
||||
IReadOnlyList<string> fullReferences, CancellationToken cancellationToken)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
ArgumentNullException.ThrowIfNull(fullReferences);
|
||||
if (fullReferences.Count == 0) return Task.FromResult<IReadOnlyList<DataValueSnapshot>>([]);
|
||||
|
||||
if (_dataReader is null)
|
||||
{
|
||||
// The production GW-backed reader builds on the StreamEvents pump that PR 4.4
|
||||
// ships; until then a real gateway-driver instance can't fulfill reads.
|
||||
// Tests that need to exercise IReadable inject a fake reader via the internal
|
||||
// ctor; production deployments running on this PR should keep the
|
||||
// legacy-host backend selected via the Galaxy:Backend flag (PR 4.W).
|
||||
throw new NotSupportedException(
|
||||
"GalaxyDriver.ReadAsync requires the StreamEvents-backed reader from PR 4.4. " +
|
||||
"Until that lands, route reads through the legacy-host backend (Galaxy:Backend=legacy-host).");
|
||||
}
|
||||
|
||||
return _dataReader.ReadAsync(fullReferences, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lazily builds the default <see cref="IGalaxyHierarchySource"/> from
|
||||
/// <c>_options.Gateway</c>. Owned <see cref="GalaxyRepositoryClient"/> is disposed in
|
||||
|
||||
Reference in New Issue
Block a user