189 lines
8.0 KiB
C#
189 lines
8.0 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Galaxy backend that uses the live <c>ZB</c> repository for <see cref="DiscoverAsync"/> —
|
|
/// 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.
|
|
/// </summary>
|
|
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<OnDataChangeNotification>? OnDataChange;
|
|
public event System.EventHandler<GalaxyAlarmEvent>? OnAlarmEvent;
|
|
public event System.EventHandler<HostConnectivityStatus>? OnHostStatusChanged;
|
|
#pragma warning restore CS0067
|
|
|
|
public Task<OpenSessionResponse> 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<DiscoverHierarchyResponse> 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<GalaxyAttributeInfo>(),
|
|
}).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<GalaxyObjectInfo>(),
|
|
};
|
|
}
|
|
}
|
|
|
|
public Task<ReadValuesResponse> 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<GalaxyDataValue>(),
|
|
});
|
|
|
|
public Task<WriteValuesResponse> 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<SubscribeResponse> 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<HistoryReadResponse> 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<HistoryTagValues>(),
|
|
});
|
|
|
|
public Task<HistoryReadProcessedResponse> 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<GalaxyDataValue>(),
|
|
});
|
|
|
|
public Task<HistoryReadAtTimeResponse> 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<GalaxyDataValue>(),
|
|
});
|
|
|
|
public Task<HistoryReadEventsResponse> 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<GalaxyHistoricalEvent>(),
|
|
});
|
|
|
|
public Task<RecycleStatusResponse> 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,
|
|
};
|
|
|
|
/// <summary>
|
|
/// Galaxy <c>template_definition.category_id</c> → human-readable name.
|
|
/// Mirrors v1 Host's <c>AlarmObjectFilter</c> mapping.
|
|
/// </summary>
|
|
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}",
|
|
};
|
|
}
|