using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browse; /// /// Translates a Galaxy object hierarchy (from ) into /// calls — folders for each gobject, variables for /// each dynamic attribute. Alarm-bearing attributes get all five sub-attribute refs /// populated via so the server-level alarm subsystem /// (PR 2.2) can subscribe + ack without help from the driver. /// /// /// Hierarchy materialisation rules (mirror legacy MxAccessGalaxyBackend.DiscoverAsync): /// /// Browse name = contained_name when present; falls back to tag_name. /// Folder per gobject; variables placed inside their owner folder. /// Variable's full reference = tag_name.attribute_name — the format MXAccess /// expects for read/write addressing (translated from the contained-name browse path). /// Hierarchy is rendered flat (one folder per gobject under the driver root). /// Nesting under parent_gobject_id for a true tree shape is not implemented; /// the flat layout is the current shipping behaviour. /// /// public sealed class GalaxyDiscoverer { private readonly IGalaxyHierarchySource _source; /// Initializes a new GalaxyDiscoverer with the specified hierarchy source. /// The Galaxy hierarchy source to use for discovery. public GalaxyDiscoverer(IGalaxyHierarchySource source) { _source = source ?? throw new ArgumentNullException(nameof(source)); } /// /// Drive the supplied builder with one folder + N variables per Galaxy object the /// gateway returns. Idempotent — caller can re-invoke after a redeploy event. /// /// The address space builder to populate with discovery results. /// The cancellation token for the operation. 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; }