using ZB.MOM.WW.OtOpcUa.Configuration.Entities; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Core.OpcUa; /// /// Phase 6.4 Stream D: materializes the OPC 40010 Machinery companion-spec Identification /// sub-folder under an Equipment node. Reads the nine documented columns off the /// row and emits one property per non-null field. /// /// /// Pure-function shape — testable without a real OPC UA node manager. The caller /// passes the builder scoped to the Equipment node; this class handles the Identification /// sub-folder creation + per-field calls. /// /// ACL binding: the sub-folder + its properties inherit the Equipment scope's /// grants (no new scope level). Phase 6.2's trie treats them as part of the Equipment /// ScopeId — a user with Equipment-level grant reads Identification; a user without the /// grant gets BadUserAccessDenied on both the Equipment node + its Identification variables. /// See docs/v2/acl-design.md §Identification cross-reference. /// /// The nine fields are exposed exactly when they carry a non-null /// value. A row with all nine null produces no Identification sub-folder at all — the /// caller can use to skip the Folder call entirely /// and avoid a pointless empty folder appearing in browse trees. /// public static class IdentificationFolderBuilder { /// Browse + display name of the sub-folder — fixed per OPC 40010 convention. public const string FolderName = "Identification"; /// /// Canonical decision #139 field set exposed in the Identification sub-folder. Order /// matches the decision-log entry so any browse-order reader can cross-reference /// without re-sorting. /// public static IReadOnlyList FieldNames { get; } = new[] { "Manufacturer", "Model", "SerialNumber", "HardwareRevision", "SoftwareRevision", "YearOfConstruction", "AssetLocation", "ManufacturerUri", "DeviceManualUri", }; /// Checks whether the equipment row has at least one non-null Identification field. /// The equipment entity. /// True if any Identification field is non-null; otherwise false. public static bool HasAnyFields(Equipment equipment) { ArgumentNullException.ThrowIfNull(equipment); return equipment.Manufacturer is not null || equipment.Model is not null || equipment.SerialNumber is not null || equipment.HardwareRevision is not null || equipment.SoftwareRevision is not null || equipment.YearOfConstruction is not null || equipment.AssetLocation is not null || equipment.ManufacturerUri is not null || equipment.DeviceManualUri is not null; } /// /// Builds the Identification sub-folder under the given equipment builder. Returns the sub-folder builder /// (or null when no fields are present) so callers can attach additional nodes if needed. /// /// The equipment address space builder. /// The equipment entity with identification fields. /// The sub-folder builder, or null if no fields are present. public static IAddressSpaceBuilder? Build(IAddressSpaceBuilder equipmentBuilder, Equipment equipment) { ArgumentNullException.ThrowIfNull(equipmentBuilder); ArgumentNullException.ThrowIfNull(equipment); if (!HasAnyFields(equipment)) return null; var folder = equipmentBuilder.Folder(FolderName, FolderName); AddIfPresent(folder, "Manufacturer", DriverDataType.String, equipment.Manufacturer); AddIfPresent(folder, "Model", DriverDataType.String, equipment.Model); AddIfPresent(folder, "SerialNumber", DriverDataType.String, equipment.SerialNumber); AddIfPresent(folder, "HardwareRevision", DriverDataType.String, equipment.HardwareRevision); AddIfPresent(folder, "SoftwareRevision", DriverDataType.String, equipment.SoftwareRevision); AddIfPresent(folder, "YearOfConstruction", DriverDataType.Int32, equipment.YearOfConstruction is null ? null : (object)(int)equipment.YearOfConstruction.Value); AddIfPresent(folder, "AssetLocation", DriverDataType.String, equipment.AssetLocation); AddIfPresent(folder, "ManufacturerUri", DriverDataType.String, equipment.ManufacturerUri); AddIfPresent(folder, "DeviceManualUri", DriverDataType.String, equipment.DeviceManualUri); return folder; } private static void AddIfPresent(IAddressSpaceBuilder folder, string name, DriverDataType dataType, object? value) { if (value is null) return; folder.AddProperty(name, dataType, value); } }