chore: organize solution into module folders (Core/Server/Drivers/Client/Tooling)
Group all 69 projects into category subfolders under src/ and tests/ so the Rider Solution Explorer mirrors the module structure. Folders: Core, Server, Drivers (with a nested Driver CLIs subfolder), Client, Tooling. - Move every project folder on disk with git mv (history preserved as renames). - Recompute relative paths in 57 .csproj files: cross-category ProjectReferences, the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external mxaccessgw refs in Driver.Galaxy and its test project. - Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders. - Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL, integration, install). Build green (0 errors); unit tests pass. Docs left for a separate pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browse;
|
||||
|
||||
/// <summary>
|
||||
/// Populates the five sub-attribute references on <see cref="AlarmConditionInfo"/>
|
||||
/// by Galaxy convention. The server-level <c>AlarmConditionService</c> (PR 2.2) uses
|
||||
/// these to subscribe to live alarm-state attributes and to route ack writes back to
|
||||
/// the alarm tag.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Galaxy alarms expose four runtime attributes plus a write-only ack target,
|
||||
/// consistently named on every alarm-bearing object:
|
||||
/// <list type="bullet">
|
||||
/// <item><c><tag>.<attr>.InAlarm</c></item>
|
||||
/// <item><c><tag>.<attr>.Priority</c></item>
|
||||
/// <item><c><tag>.<attr>.DescAttrName</c></item>
|
||||
/// <item><c><tag>.<attr>.Acked</c></item>
|
||||
/// <item><c><tag>.<attr>.AckMsg</c></item>
|
||||
/// </list>
|
||||
/// This is the same convention the legacy <c>GalaxyAlarmTracker</c> hard-coded; we
|
||||
/// concentrate it here so PR 2.2's service receives complete <c>AlarmConditionInfo</c>
|
||||
/// rows during discovery without the server needing to know the convention.
|
||||
/// </remarks>
|
||||
internal static class AlarmRefBuilder
|
||||
{
|
||||
private const string InAlarmSuffix = ".InAlarm";
|
||||
private const string PrioritySuffix = ".Priority";
|
||||
private const string DescAttrNameSuffix = ".DescAttrName";
|
||||
private const string AckedSuffix = ".Acked";
|
||||
private const string AckMsgSuffix = ".AckMsg";
|
||||
|
||||
/// <summary>
|
||||
/// Build an <see cref="AlarmConditionInfo"/> for an alarm-bearing attribute with all
|
||||
/// five sub-attribute references populated. <paramref name="fullReference"/> is the
|
||||
/// attribute's full reference (e.g. <c>"Tank1.Level.HiHi"</c>); the convention prefixes
|
||||
/// each suffix to it.
|
||||
/// </summary>
|
||||
public static AlarmConditionInfo Build(
|
||||
string fullReference,
|
||||
AlarmSeverity initialSeverity = AlarmSeverity.Medium,
|
||||
string? initialDescription = null) => new(
|
||||
SourceName: fullReference,
|
||||
InitialSeverity: initialSeverity,
|
||||
InitialDescription: initialDescription,
|
||||
InAlarmRef: fullReference + InAlarmSuffix,
|
||||
PriorityRef: fullReference + PrioritySuffix,
|
||||
DescAttrNameRef: fullReference + DescAttrNameSuffix,
|
||||
AckedRef: fullReference + AckedSuffix,
|
||||
AckMsgWriteRef: fullReference + AckMsgSuffix);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browse;
|
||||
|
||||
/// <summary>
|
||||
/// Maps Galaxy <c>mx_data_type</c> integer codes to <see cref="DriverDataType"/>.
|
||||
/// Ported from the legacy <c>GalaxyProxyDriver.MapDataType</c> with the same fallback
|
||||
/// to <see cref="DriverDataType.String"/> for unknown codes — keeps wire compatibility
|
||||
/// with deployed configs while we tighten this through the parity matrix.
|
||||
/// </summary>
|
||||
internal static class DataTypeMap
|
||||
{
|
||||
public static DriverDataType Map(int mxDataType) => mxDataType switch
|
||||
{
|
||||
0 => DriverDataType.Boolean,
|
||||
1 => DriverDataType.Int32,
|
||||
2 => DriverDataType.Float32,
|
||||
3 => DriverDataType.Float64,
|
||||
4 => DriverDataType.String,
|
||||
5 => DriverDataType.DateTime,
|
||||
_ => DriverDataType.String,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using MxGateway.Contracts.Proto.Galaxy;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browse;
|
||||
|
||||
/// <summary>
|
||||
/// Long-lived consumer of <see cref="IGalaxyDeployWatchSource"/>. Translates
|
||||
/// gateway <see cref="DeployEvent"/> stream into
|
||||
/// <see cref="IRediscoverable.OnRediscoveryNeeded"/>-shaped events whenever the
|
||||
/// observed <c>time_of_last_deploy</c> actually changes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The first event the gateway emits on subscribe is the bootstrap snapshot
|
||||
/// carrying the current cached deploy time — even when the caller passed a
|
||||
/// <c>lastSeenDeployTime</c>, a different gateway instance / cache invalidation
|
||||
/// may still re-deliver it. The watcher therefore suppresses the first event
|
||||
/// it observes locally, recording its (presence, time) pair as the baseline,
|
||||
/// and only raises rediscover for subsequent events whose pair differs.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// When <see cref="IGalaxyDeployWatchSource.WatchAsync"/> throws (transport
|
||||
/// drop, gateway restart) the loop logs a warning, waits with capped
|
||||
/// exponential backoff, then re-subscribes using the last-observed deploy time
|
||||
/// so a reconnect doesn't fan out a redundant rediscover for state we already
|
||||
/// knew about.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class DeployWatcher : IDisposable
|
||||
{
|
||||
private static readonly TimeSpan DefaultInitialBackoff = TimeSpan.FromSeconds(1);
|
||||
private static readonly TimeSpan DefaultMaxBackoff = TimeSpan.FromSeconds(30);
|
||||
|
||||
private readonly IGalaxyDeployWatchSource _source;
|
||||
private readonly ILogger _logger;
|
||||
private readonly TimeSpan _initialBackoff;
|
||||
private readonly TimeSpan _maxBackoff;
|
||||
private readonly Func<int, TimeSpan>? _jitter;
|
||||
|
||||
private CancellationTokenSource? _cts;
|
||||
private Task? _loopTask;
|
||||
private int _started; // 0 = not started, 1 = started
|
||||
|
||||
/// <inheritdoc cref="IRediscoverable.OnRediscoveryNeeded"/>
|
||||
public event EventHandler<RediscoveryEventArgs>? OnRediscoveryNeeded;
|
||||
|
||||
public DeployWatcher(IGalaxyDeployWatchSource source, ILogger? logger = null)
|
||||
: this(source, logger, DefaultInitialBackoff, DefaultMaxBackoff, jitter: null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test-only ctor lets tests collapse the retry backoff so a fault-injection
|
||||
/// scenario doesn't sit in <see cref="Task.Delay(TimeSpan, CancellationToken)"/>.
|
||||
/// </summary>
|
||||
internal DeployWatcher(
|
||||
IGalaxyDeployWatchSource source,
|
||||
ILogger? logger,
|
||||
TimeSpan initialBackoff,
|
||||
TimeSpan maxBackoff,
|
||||
Func<int, TimeSpan>? jitter)
|
||||
{
|
||||
_source = source ?? throw new ArgumentNullException(nameof(source));
|
||||
_logger = logger ?? NullLogger.Instance;
|
||||
_initialBackoff = initialBackoff;
|
||||
_maxBackoff = maxBackoff;
|
||||
_jitter = jitter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kicks off the background watch loop. Returns immediately once the loop task
|
||||
/// has been scheduled — the loop itself runs until <see cref="StopAsync"/> or
|
||||
/// the supplied <paramref name="cancellationToken"/> is signaled.
|
||||
/// </summary>
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (Interlocked.Exchange(ref _started, 1) != 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"DeployWatcher.StartAsync has already been called. Construct a new instance to restart.");
|
||||
}
|
||||
|
||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_loopTask = Task.Run(() => RunLoopAsync(_cts.Token), CancellationToken.None);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Cancels the loop and waits for it to exit cleanly.</summary>
|
||||
public async Task StopAsync()
|
||||
{
|
||||
var cts = _cts;
|
||||
var loop = _loopTask;
|
||||
if (cts is null || loop is null) return;
|
||||
|
||||
try { cts.Cancel(); } catch (ObjectDisposedException) { }
|
||||
|
||||
try
|
||||
{
|
||||
await loop.ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Expected: cancellation propagated up from the source enumerator.
|
||||
}
|
||||
finally
|
||||
{
|
||||
cts.Dispose();
|
||||
_cts = null;
|
||||
_loopTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_loopTask is null) return;
|
||||
StopAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private async Task RunLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
DateTimeOffset? lastSeenDeployTime = null;
|
||||
bool? lastSeenPresent = null;
|
||||
bool baselineCaptured = false;
|
||||
TimeSpan backoff = _initialBackoff;
|
||||
int attempt = 0;
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await foreach (DeployEvent ev in _source
|
||||
.WatchAsync(lastSeenDeployTime, cancellationToken)
|
||||
.WithCancellation(cancellationToken)
|
||||
.ConfigureAwait(false))
|
||||
{
|
||||
// Successful read — reset retry state.
|
||||
backoff = _initialBackoff;
|
||||
attempt = 0;
|
||||
|
||||
DateTimeOffset? observedTime = ev.TimeOfLastDeployPresent && ev.TimeOfLastDeploy is not null
|
||||
? ev.TimeOfLastDeploy.ToDateTimeOffset()
|
||||
: null;
|
||||
bool observedPresent = ev.TimeOfLastDeployPresent;
|
||||
|
||||
if (!baselineCaptured)
|
||||
{
|
||||
// Bootstrap event — record state and suppress.
|
||||
baselineCaptured = true;
|
||||
lastSeenDeployTime = observedTime;
|
||||
lastSeenPresent = observedPresent;
|
||||
_logger.LogDebug(
|
||||
"DeployWatcher bootstrap event sequence={Sequence} present={Present} time={Time} suppressed.",
|
||||
ev.Sequence, observedPresent, observedTime);
|
||||
continue;
|
||||
}
|
||||
|
||||
bool presenceFlipped = lastSeenPresent != observedPresent;
|
||||
bool timeChanged = observedPresent && lastSeenDeployTime != observedTime;
|
||||
|
||||
if (!presenceFlipped && !timeChanged)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"DeployWatcher event sequence={Sequence} matches last-seen state; skipping rediscover.",
|
||||
ev.Sequence);
|
||||
continue;
|
||||
}
|
||||
|
||||
lastSeenDeployTime = observedTime;
|
||||
lastSeenPresent = observedPresent;
|
||||
|
||||
string? scopeHint = observedTime?.ToString("O");
|
||||
var args = new RediscoveryEventArgs("deploy-time-changed", scopeHint);
|
||||
|
||||
_logger.LogInformation(
|
||||
"DeployWatcher raising rediscover sequence={Sequence} reason={Reason} scopeHint={ScopeHint}.",
|
||||
ev.Sequence, args.Reason, args.ScopeHint);
|
||||
|
||||
try
|
||||
{
|
||||
OnRediscoveryNeeded?.Invoke(this, args);
|
||||
}
|
||||
catch (Exception handlerEx)
|
||||
{
|
||||
_logger.LogError(handlerEx,
|
||||
"DeployWatcher subscriber threw while handling rediscover; continuing.");
|
||||
}
|
||||
}
|
||||
|
||||
// Stream completed normally — gateway closed the subscription. Re-open
|
||||
// immediately if we weren't asked to stop.
|
||||
_logger.LogDebug("DeployWatcher stream completed; re-subscribing.");
|
||||
continue;
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
attempt++;
|
||||
TimeSpan jitterAmount = _jitter?.Invoke(attempt) ?? RandomJitter(backoff);
|
||||
TimeSpan delay = backoff + jitterAmount;
|
||||
_logger.LogWarning(ex,
|
||||
"DeployWatcher source threw; retrying in {Delay} (attempt {Attempt}, last-seen time {LastSeen}).",
|
||||
delay, attempt, lastSeenDeployTime);
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Exponential backoff capped at _maxBackoff.
|
||||
var doubled = TimeSpan.FromTicks(Math.Min(_maxBackoff.Ticks, backoff.Ticks * 2));
|
||||
backoff = doubled < _initialBackoff ? _initialBackoff : doubled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static TimeSpan RandomJitter(TimeSpan baseDelay)
|
||||
{
|
||||
// Up to +/- 25% of the base delay, biased non-negative.
|
||||
long maxTicks = Math.Max(1L, baseDelay.Ticks / 4);
|
||||
long ticks = Random.Shared.NextInt64(0, maxTicks);
|
||||
return TimeSpan.FromTicks(ticks);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using MxGateway.Contracts.Proto.Galaxy;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browse;
|
||||
|
||||
/// <summary>
|
||||
/// Translates a Galaxy object hierarchy (from <see cref="IGalaxyHierarchySource"/>) into
|
||||
/// <see cref="IAddressSpaceBuilder"/> calls — folders for each gobject, variables for
|
||||
/// each dynamic attribute. Alarm-bearing attributes get all five sub-attribute refs
|
||||
/// populated via <see cref="AlarmRefBuilder"/> so the server-level alarm subsystem
|
||||
/// (PR 2.2) can subscribe + ack without help from the driver.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Hierarchy materialisation rules (mirror legacy <c>MxAccessGalaxyBackend.DiscoverAsync</c>):
|
||||
/// <list type="bullet">
|
||||
/// <item>Browse name = <c>contained_name</c> when present; falls back to <c>tag_name</c>.</item>
|
||||
/// <item>Folder per gobject; variables placed inside their owner folder.</item>
|
||||
/// <item>Variable's full reference = <c>tag_name.attribute_name</c> — the format MXAccess
|
||||
/// expects for read/write addressing (translated from the contained-name browse path).</item>
|
||||
/// <item>Hierarchy is rendered flat (one folder per gobject under the driver root) for
|
||||
/// this PR. PR 4.W's address-space wiring revisits whether to nest under
|
||||
/// <c>parent_gobject_id</c> for a true tree shape.</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public sealed class GalaxyDiscoverer
|
||||
{
|
||||
private readonly IGalaxyHierarchySource _source;
|
||||
|
||||
public GalaxyDiscoverer(IGalaxyHierarchySource source)
|
||||
{
|
||||
_source = source ?? throw new ArgumentNullException(nameof(source));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drive the supplied builder with one folder + N variables per Galaxy object the
|
||||
/// gateway returns. Idempotent — caller can re-invoke after a redeploy event.
|
||||
/// </summary>
|
||||
public async Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
var objects = await _source.GetHierarchyAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var obj in objects)
|
||||
{
|
||||
var browseName = string.IsNullOrEmpty(obj.ContainedName) ? obj.TagName : obj.ContainedName;
|
||||
if (string.IsNullOrEmpty(browseName)) continue; // skip objects with no usable identity
|
||||
|
||||
var folder = builder.Folder(browseName, browseName);
|
||||
|
||||
foreach (var attr in obj.Attributes)
|
||||
{
|
||||
if (string.IsNullOrEmpty(attr.AttributeName)) continue;
|
||||
|
||||
var fullReference = !string.IsNullOrEmpty(attr.FullTagReference)
|
||||
? StripArraySuffix(attr.FullTagReference)
|
||||
: obj.TagName + "." + attr.AttributeName;
|
||||
|
||||
var info = new DriverAttributeInfo(
|
||||
FullName: fullReference,
|
||||
DriverDataType: DataTypeMap.Map(attr.MxDataType),
|
||||
IsArray: attr.IsArray,
|
||||
ArrayDim: attr.IsArray && attr.ArrayDimensionPresent && attr.ArrayDimension > 0
|
||||
? (uint)attr.ArrayDimension
|
||||
: null,
|
||||
SecurityClass: SecurityMap.Map(attr.SecurityClassification),
|
||||
IsHistorized: attr.IsHistorized,
|
||||
IsAlarm: attr.IsAlarm);
|
||||
|
||||
var handle = folder.Variable(attr.AttributeName, attr.AttributeName, info);
|
||||
|
||||
// Alarm-bearing attributes ship the full sub-attribute ref set so the server's
|
||||
// AlarmConditionService can subscribe + ack-write without re-deriving the names.
|
||||
if (attr.IsAlarm)
|
||||
{
|
||||
handle.MarkAsAlarmCondition(AlarmRefBuilder.Build(fullReference));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PR 5.W workaround for mxaccessgw GalaxyRepository.cs:173-175 — the gateway's
|
||||
// SQL appends `[]` to array-typed `full_tag_reference` values, but MxAccess COM
|
||||
// `IInstance.AddItem` doesn't accept `[]`-suffixed addresses (so any downstream
|
||||
// Subscribe/Read/Write through the worker would fail with the suffixed form).
|
||||
// Strip defensively here so the parity matrix can run today; remove once the
|
||||
// gw fix (mxaccessgw/requirements-array-suffix-fix.md) lands.
|
||||
private static string StripArraySuffix(string fullReference) =>
|
||||
fullReference.EndsWith("[]", StringComparison.Ordinal)
|
||||
? fullReference[..^2]
|
||||
: fullReference;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using MxGateway.Client;
|
||||
using MxGateway.Contracts.Proto.Galaxy;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browse;
|
||||
|
||||
/// <summary>
|
||||
/// Default <see cref="IGalaxyDeployWatchSource"/> wrapping the gateway's
|
||||
/// <see cref="GalaxyRepositoryClient"/>. Forwards
|
||||
/// <c>WatchDeployEventsAsync(lastSeenDeployTime, ct)</c> verbatim — paging /
|
||||
/// bootstrap suppression policy lives on the gateway, while
|
||||
/// <see cref="DeployWatcher"/> owns the change-detection and reconnect-loop
|
||||
/// concerns above this seam.
|
||||
/// </summary>
|
||||
public sealed class GatewayGalaxyDeployWatchSource : IGalaxyDeployWatchSource
|
||||
{
|
||||
private readonly GalaxyRepositoryClient _client;
|
||||
|
||||
public GatewayGalaxyDeployWatchSource(GalaxyRepositoryClient client)
|
||||
{
|
||||
_client = client ?? throw new ArgumentNullException(nameof(client));
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<DeployEvent> WatchAsync(
|
||||
DateTimeOffset? lastSeenDeployTime, CancellationToken cancellationToken)
|
||||
=> _client.WatchDeployEventsAsync(lastSeenDeployTime, cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using MxGateway.Client;
|
||||
using MxGateway.Contracts.Proto.Galaxy;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browse;
|
||||
|
||||
/// <summary>
|
||||
/// Default <see cref="IGalaxyHierarchySource"/> wrapping the gateway's
|
||||
/// <see cref="GalaxyRepositoryClient"/>. Pages internally via the client's overload.
|
||||
/// </summary>
|
||||
public sealed class GatewayGalaxyHierarchySource : IGalaxyHierarchySource
|
||||
{
|
||||
private readonly GalaxyRepositoryClient _client;
|
||||
|
||||
public GatewayGalaxyHierarchySource(GalaxyRepositoryClient client)
|
||||
{
|
||||
_client = client ?? throw new ArgumentNullException(nameof(client));
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<GalaxyObject>> GetHierarchyAsync(CancellationToken cancellationToken)
|
||||
=> _client.DiscoverHierarchyAsync(cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using MxGateway.Contracts.Proto.Galaxy;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browse;
|
||||
|
||||
/// <summary>
|
||||
/// Driver-side seam between <see cref="DeployWatcher"/> and the gateway. Production
|
||||
/// wraps <c>GalaxyRepositoryClient.WatchDeployEventsAsync</c>; tests substitute a fake
|
||||
/// yielding controlled <see cref="DeployEvent"/> instances so the watcher's bootstrap
|
||||
/// suppression, change detection, reconnect, and shutdown semantics can be exercised
|
||||
/// without a real gRPC stream.
|
||||
/// </summary>
|
||||
public interface IGalaxyDeployWatchSource
|
||||
{
|
||||
/// <summary>
|
||||
/// Subscribe to Galaxy deploy events. The server emits a bootstrap event with the
|
||||
/// current cached state on subscribe, then one event per new
|
||||
/// <c>time_of_last_deploy</c>. Pass <paramref name="lastSeenDeployTime"/> to ask the
|
||||
/// gateway to suppress its bootstrap when the caller already has the current value;
|
||||
/// <see cref="DeployWatcher"/> still suppresses the first event it observes locally
|
||||
/// so a transport reconnect doesn't re-fire on identical state.
|
||||
/// </summary>
|
||||
IAsyncEnumerable<DeployEvent> WatchAsync(
|
||||
DateTimeOffset? lastSeenDeployTime, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using MxGateway.Contracts.Proto.Galaxy;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browse;
|
||||
|
||||
/// <summary>
|
||||
/// Driver-side seam between <see cref="GalaxyDiscoverer"/> and the gateway. Production
|
||||
/// wraps <c>GalaxyRepositoryClient</c>; tests substitute a fake returning canned
|
||||
/// <see cref="GalaxyObject"/> rows so the discoverer's translation logic can be exercised
|
||||
/// without a real gRPC channel.
|
||||
/// </summary>
|
||||
public interface IGalaxyHierarchySource
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the full materialised Galaxy hierarchy. The gateway client pages
|
||||
/// internally; this interface deliberately exposes only the post-paging shape so
|
||||
/// callers don't reimplement paging.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<GalaxyObject>> GetHierarchyAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browse;
|
||||
|
||||
/// <summary>
|
||||
/// Maps Galaxy <c>security_classification</c> integer codes to
|
||||
/// <see cref="SecurityClassification"/>. Ported from the legacy
|
||||
/// <c>GalaxyProxyDriver.MapSecurity</c>; unknown codes fall back to
|
||||
/// <see cref="SecurityClassification.FreeAccess"/> so a forward-compatible Galaxy
|
||||
/// deployment with new classifications doesn't break discovery.
|
||||
/// </summary>
|
||||
internal static class SecurityMap
|
||||
{
|
||||
public static SecurityClassification Map(int mxSec) => mxSec switch
|
||||
{
|
||||
0 => SecurityClassification.FreeAccess,
|
||||
1 => SecurityClassification.Operate,
|
||||
2 => SecurityClassification.SecuredWrite,
|
||||
3 => SecurityClassification.VerifiedWrite,
|
||||
4 => SecurityClassification.Tune,
|
||||
5 => SecurityClassification.Configure,
|
||||
6 => SecurityClassification.ViewOnly,
|
||||
_ => SecurityClassification.FreeAccess,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using MxGateway.Contracts.Proto.Galaxy;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browse;
|
||||
|
||||
/// <summary>
|
||||
/// PR 6.1 — Decorator that emits one <see cref="System.Diagnostics.Activity"/> span
|
||||
/// per <c>GetHierarchy</c> RPC. <c>galaxy.object_count</c> on the span lets ops
|
||||
/// correlate slow Discover passes with Galaxy size without instrumenting the
|
||||
/// discoverer's translation step.
|
||||
/// </summary>
|
||||
internal sealed class TracedGalaxyHierarchySource(IGalaxyHierarchySource inner, string clientName) : IGalaxyHierarchySource
|
||||
{
|
||||
public async Task<IReadOnlyList<GalaxyObject>> GetHierarchyAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using var activity = GalaxyTelemetry.ActivitySource.StartActivity("galaxy.get_hierarchy");
|
||||
activity?.SetTag("galaxy.client", clientName);
|
||||
try
|
||||
{
|
||||
var hierarchy = await inner.GetHierarchyAsync(cancellationToken).ConfigureAwait(false);
|
||||
activity?.SetTag("galaxy.object_count", hierarchy.Count);
|
||||
return hierarchy;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
activity.RecordError(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user