Full OPC UA server on .NET Framework 4.8 (x86) exposing AVEVA System Platform Galaxy tags via MXAccess. Mirrors Galaxy object hierarchy as OPC UA address space, translating contained-name browse paths to tag-name runtime references. Components implemented: - Configuration: AppConfiguration with 4 sections, validator - Domain: ConnectionState, Quality, Vtq, MxDataTypeMapper, error codes - MxAccess: StaComThread, MxAccessClient (partial classes), MxProxyAdapter using strongly-typed ArchestrA.MxAccess COM interop - Galaxy Repository: SQL queries (hierarchy, attributes, change detection), ChangeDetectionService with auto-rebuild on deploy - OPC UA Server: LmxNodeManager (CustomNodeManager2), LmxOpcUaServer, OpcUaServerHost with programmatic config, SecurityPolicy None - Status Dashboard: HTTP server with HTML/JSON/health endpoints - Integration: Full 14-step startup, graceful shutdown, component wiring 175 tests (174 unit + 1 integration), all passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
77 lines
2.7 KiB
C#
77 lines
2.7 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
|
|
|
namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers
|
|
{
|
|
public class FakeMxAccessClient : IMxAccessClient
|
|
{
|
|
public ConnectionState State { get; set; } = ConnectionState.Connected;
|
|
public int ActiveSubscriptionCount => _subscriptions.Count;
|
|
public int ReconnectCount { get; set; }
|
|
|
|
public event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;
|
|
public event Action<string, Vtq>? OnTagValueChanged;
|
|
|
|
private readonly ConcurrentDictionary<string, Action<string, Vtq>> _subscriptions = new(StringComparer.OrdinalIgnoreCase);
|
|
public ConcurrentDictionary<string, Vtq> TagValues { get; } = new(StringComparer.OrdinalIgnoreCase);
|
|
public List<(string Tag, object Value)> WrittenValues { get; } = new();
|
|
public bool WriteResult { get; set; } = true;
|
|
|
|
public Task ConnectAsync(CancellationToken ct = default)
|
|
{
|
|
State = ConnectionState.Connected;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task DisconnectAsync()
|
|
{
|
|
State = ConnectionState.Disconnected;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task SubscribeAsync(string fullTagReference, Action<string, Vtq> callback)
|
|
{
|
|
_subscriptions[fullTagReference] = callback;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task UnsubscribeAsync(string fullTagReference)
|
|
{
|
|
_subscriptions.TryRemove(fullTagReference, out _);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task<Vtq> ReadAsync(string fullTagReference, CancellationToken ct = default)
|
|
{
|
|
if (TagValues.TryGetValue(fullTagReference, out var vtq))
|
|
return Task.FromResult(vtq);
|
|
return Task.FromResult(Vtq.Bad(Quality.BadNotConnected));
|
|
}
|
|
|
|
public Task<bool> WriteAsync(string fullTagReference, object value, CancellationToken ct = default)
|
|
{
|
|
WrittenValues.Add((fullTagReference, value));
|
|
return Task.FromResult(WriteResult);
|
|
}
|
|
|
|
public void SimulateDataChange(string address, Vtq vtq)
|
|
{
|
|
OnTagValueChanged?.Invoke(address, vtq);
|
|
if (_subscriptions.TryGetValue(address, out var callback))
|
|
callback(address, vtq);
|
|
}
|
|
|
|
public void RaiseConnectionStateChanged(ConnectionState prev, ConnectionState curr)
|
|
{
|
|
State = curr;
|
|
ConnectionStateChanged?.Invoke(this, new ConnectionStateChangedEventArgs(prev, curr));
|
|
}
|
|
|
|
public void Dispose() { }
|
|
}
|
|
}
|