using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend.Galaxy; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.Contracts; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend; /// /// Galaxy backend that uses the live ZB repository for — /// real gobject hierarchy + attributes flow through to the Proxy without needing the MXAccess /// COM client. Runtime data-plane calls (Read/Write/Subscribe/Alarm/History) still surface /// as "MXAccess code lift pending" until the COM client port lands. This is the highest-value /// intermediate state because Discover is what powers the OPC UA address-space build, so /// downstream Proxy + parity tests can exercise the complete tree shape today. /// public sealed class DbBackedGalaxyBackend(GalaxyRepository repository) : IGalaxyBackend { private long _nextSessionId; private long _nextSubscriptionId; // DB-only backend doesn't have a runtime data plane; never raises events. #pragma warning disable CS0067 public event System.EventHandler? OnDataChange; public event System.EventHandler? OnAlarmEvent; public event System.EventHandler? OnHostStatusChanged; #pragma warning restore CS0067 public Task OpenSessionAsync(OpenSessionRequest req, CancellationToken ct) { var id = Interlocked.Increment(ref _nextSessionId); return Task.FromResult(new OpenSessionResponse { Success = true, SessionId = id }); } public Task CloseSessionAsync(CloseSessionRequest req, CancellationToken ct) => Task.CompletedTask; public async Task DiscoverAsync(DiscoverHierarchyRequest req, CancellationToken ct) { try { var hierarchy = await repository.GetHierarchyAsync(ct).ConfigureAwait(false); var attributes = await repository.GetAttributesAsync(ct).ConfigureAwait(false); // Group attributes by their owning gobject for the IPC payload. var attrsByGobject = attributes .GroupBy(a => a.GobjectId) .ToDictionary(g => g.Key, g => g.Select(MapAttribute).ToArray()); var parentByChild = hierarchy .ToDictionary(o => o.GobjectId, o => o.ParentGobjectId); var nameByGobject = hierarchy .ToDictionary(o => o.GobjectId, o => o.TagName); var objects = hierarchy.Select(o => new GalaxyObjectInfo { ContainedName = string.IsNullOrEmpty(o.ContainedName) ? o.TagName : o.ContainedName, TagName = o.TagName, ParentContainedName = parentByChild.TryGetValue(o.GobjectId, out var p) && p != 0 && nameByGobject.TryGetValue(p, out var pName) ? pName : null, TemplateCategory = MapCategory(o.CategoryId), Attributes = attrsByGobject.TryGetValue(o.GobjectId, out var a) ? a : System.Array.Empty(), }).ToArray(); return new DiscoverHierarchyResponse { Success = true, Objects = objects }; } catch (Exception ex) when (ex is System.Data.SqlClient.SqlException or InvalidOperationException or TimeoutException) { return new DiscoverHierarchyResponse { Success = false, Error = $"Galaxy ZB repository error: {ex.Message}", Objects = System.Array.Empty(), }; } } public Task ReadValuesAsync(ReadValuesRequest req, CancellationToken ct) => Task.FromResult(new ReadValuesResponse { Success = false, Error = "MXAccess code lift pending (Phase 2 Task B.1) — DB-backed backend covers Discover only", Values = System.Array.Empty(), }); public Task WriteValuesAsync(WriteValuesRequest req, CancellationToken ct) { var results = new WriteValueResult[req.Writes.Length]; for (var i = 0; i < req.Writes.Length; i++) { results[i] = new WriteValueResult { TagReference = req.Writes[i].TagReference, StatusCode = 0x80020000u, Error = "MXAccess code lift pending (Phase 2 Task B.1)", }; } return Task.FromResult(new WriteValuesResponse { Results = results }); } public Task SubscribeAsync(SubscribeRequest req, CancellationToken ct) { var sid = Interlocked.Increment(ref _nextSubscriptionId); return Task.FromResult(new SubscribeResponse { Success = true, SubscriptionId = sid, ActualIntervalMs = req.RequestedIntervalMs, }); } public Task UnsubscribeAsync(UnsubscribeRequest req, CancellationToken ct) => Task.CompletedTask; public Task SubscribeAlarmsAsync(AlarmSubscribeRequest req, CancellationToken ct) => Task.CompletedTask; public Task AcknowledgeAlarmAsync(AlarmAckRequest req, CancellationToken ct) => Task.CompletedTask; public Task HistoryReadAsync(HistoryReadRequest req, CancellationToken ct) => Task.FromResult(new HistoryReadResponse { Success = false, Error = "MXAccess + Historian code lift pending (Phase 2 Task B.1)", Tags = System.Array.Empty(), }); public Task HistoryReadProcessedAsync( HistoryReadProcessedRequest req, CancellationToken ct) => Task.FromResult(new HistoryReadProcessedResponse { Success = false, Error = "MXAccess + Historian code lift pending (Phase 2 Task B.1)", Values = System.Array.Empty(), }); public Task HistoryReadAtTimeAsync( HistoryReadAtTimeRequest req, CancellationToken ct) => Task.FromResult(new HistoryReadAtTimeResponse { Success = false, Error = "MXAccess + Historian code lift pending (Phase 2 Task B.1)", Values = System.Array.Empty(), }); public Task HistoryReadEventsAsync( HistoryReadEventsRequest req, CancellationToken ct) => Task.FromResult(new HistoryReadEventsResponse { Success = false, Error = "MXAccess + Historian code lift pending (Phase 2 Task B.1)", Events = System.Array.Empty(), }); public Task RecycleAsync(RecycleHostRequest req, CancellationToken ct) => Task.FromResult(new RecycleStatusResponse { Accepted = true, GraceSeconds = 15 }); private static GalaxyAttributeInfo MapAttribute(GalaxyAttributeRow row) => new() { AttributeName = row.AttributeName, MxDataType = row.MxDataType, IsArray = row.IsArray, ArrayDim = row.ArrayDimension is int d and > 0 ? (uint)d : null, SecurityClassification = row.SecurityClassification, IsHistorized = row.IsHistorized, IsAlarm = row.IsAlarm, }; /// /// Galaxy template_definition.category_id → human-readable name. /// Mirrors v1 Host's AlarmObjectFilter mapping. /// private static string MapCategory(int categoryId) => categoryId switch { 1 => "$WinPlatform", 3 => "$AppEngine", 4 => "$Area", 10 => "$UserDefined", 11 => "$ApplicationObject", 13 => "$Area", 17 => "$DeviceIntegration", 24 => "$ViewEngine", 26 => "$ViewApp", _ => $"category-{categoryId}", }; }