Phase 3 PR 17 — complete OPC UA server startup + live integration test #16
@@ -29,6 +29,9 @@ public sealed class DriverNodeManager : CustomNodeManager2, IAddressSpaceBuilder
|
||||
private readonly IWritable? _writable;
|
||||
private readonly ILogger<DriverNodeManager> _logger;
|
||||
|
||||
/// <summary>The driver whose address space this node manager exposes.</summary>
|
||||
public IDriver Driver => _driver;
|
||||
|
||||
private FolderState? _driverRoot;
|
||||
private readonly Dictionary<string, BaseDataVariableState> _variablesByFullRef = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
|
||||
181
src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OpcUaApplicationHost.cs
Normal file
181
src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OpcUaApplicationHost.cs
Normal file
@@ -0,0 +1,181 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Configuration;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.OpcUa;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Server.OpcUa;
|
||||
|
||||
/// <summary>
|
||||
/// Wraps <see cref="ApplicationInstance"/> to bring the OPC UA server online — builds an
|
||||
/// <see cref="ApplicationConfiguration"/> programmatically (no external XML file), ensures
|
||||
/// the application certificate exists in the PKI store (auto-generates self-signed on first
|
||||
/// run), starts the server, then walks each <see cref="DriverNodeManager"/> and invokes
|
||||
/// <see cref="GenericDriverNodeManager.BuildAddressSpaceAsync"/> against it so the driver's
|
||||
/// discovery streams into the already-running server's address space.
|
||||
/// </summary>
|
||||
public sealed class OpcUaApplicationHost : IAsyncDisposable
|
||||
{
|
||||
private readonly OpcUaServerOptions _options;
|
||||
private readonly DriverHost _driverHost;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly ILogger<OpcUaApplicationHost> _logger;
|
||||
private ApplicationInstance? _application;
|
||||
private OtOpcUaServer? _server;
|
||||
private bool _disposed;
|
||||
|
||||
public OpcUaApplicationHost(OpcUaServerOptions options, DriverHost driverHost,
|
||||
ILoggerFactory loggerFactory, ILogger<OpcUaApplicationHost> logger)
|
||||
{
|
||||
_options = options;
|
||||
_driverHost = driverHost;
|
||||
_loggerFactory = loggerFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public OtOpcUaServer? Server => _server;
|
||||
|
||||
/// <summary>
|
||||
/// Builds the <see cref="ApplicationConfiguration"/>, validates/creates the application
|
||||
/// certificate, constructs + starts the <see cref="OtOpcUaServer"/>, then drives
|
||||
/// <see cref="GenericDriverNodeManager.BuildAddressSpaceAsync"/> per registered driver so
|
||||
/// the address space is populated before the first client connects.
|
||||
/// </summary>
|
||||
public async Task StartAsync(CancellationToken ct)
|
||||
{
|
||||
_application = new ApplicationInstance
|
||||
{
|
||||
ApplicationName = _options.ApplicationName,
|
||||
ApplicationType = ApplicationType.Server,
|
||||
ApplicationConfiguration = BuildConfiguration(),
|
||||
};
|
||||
|
||||
var hasCert = await _application.CheckApplicationInstanceCertificate(silent: true, minimumKeySize: CertificateFactory.DefaultKeySize).ConfigureAwait(false);
|
||||
if (!hasCert)
|
||||
throw new InvalidOperationException(
|
||||
$"OPC UA application certificate could not be validated or created in {_options.PkiStoreRoot}");
|
||||
|
||||
_server = new OtOpcUaServer(_driverHost, _loggerFactory);
|
||||
await _application.Start(_server).ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation("OPC UA server started — endpoint={Endpoint} driverCount={Count}",
|
||||
_options.EndpointUrl, _server.DriverNodeManagers.Count);
|
||||
|
||||
// Drive each driver's discovery through its node manager. The node manager IS the
|
||||
// IAddressSpaceBuilder; GenericDriverNodeManager captures alarm-condition sinks into
|
||||
// its internal map and wires OnAlarmEvent → sink routing.
|
||||
foreach (var nodeManager in _server.DriverNodeManagers)
|
||||
{
|
||||
var driverId = nodeManager.Driver.DriverInstanceId;
|
||||
try
|
||||
{
|
||||
var generic = new GenericDriverNodeManager(nodeManager.Driver);
|
||||
await generic.BuildAddressSpaceAsync(nodeManager, ct).ConfigureAwait(false);
|
||||
_logger.LogInformation("Address space populated for driver {Driver}", driverId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Per decision #12: driver exceptions isolate — log and keep the server serving
|
||||
// the other drivers' subtrees. Re-building this one takes a Reinitialize call.
|
||||
_logger.LogError(ex, "Discovery failed for driver {Driver}; subtree faulted", driverId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ApplicationConfiguration BuildConfiguration()
|
||||
{
|
||||
Directory.CreateDirectory(_options.PkiStoreRoot);
|
||||
|
||||
var cfg = new ApplicationConfiguration
|
||||
{
|
||||
ApplicationName = _options.ApplicationName,
|
||||
ApplicationUri = _options.ApplicationUri,
|
||||
ApplicationType = ApplicationType.Server,
|
||||
ProductUri = "urn:OtOpcUa:Server",
|
||||
|
||||
SecurityConfiguration = new SecurityConfiguration
|
||||
{
|
||||
ApplicationCertificate = new CertificateIdentifier
|
||||
{
|
||||
StoreType = CertificateStoreType.Directory,
|
||||
StorePath = Path.Combine(_options.PkiStoreRoot, "own"),
|
||||
SubjectName = "CN=" + _options.ApplicationName,
|
||||
},
|
||||
TrustedIssuerCertificates = new CertificateTrustList
|
||||
{
|
||||
StoreType = CertificateStoreType.Directory,
|
||||
StorePath = Path.Combine(_options.PkiStoreRoot, "issuers"),
|
||||
},
|
||||
TrustedPeerCertificates = new CertificateTrustList
|
||||
{
|
||||
StoreType = CertificateStoreType.Directory,
|
||||
StorePath = Path.Combine(_options.PkiStoreRoot, "trusted"),
|
||||
},
|
||||
RejectedCertificateStore = new CertificateTrustList
|
||||
{
|
||||
StoreType = CertificateStoreType.Directory,
|
||||
StorePath = Path.Combine(_options.PkiStoreRoot, "rejected"),
|
||||
},
|
||||
AutoAcceptUntrustedCertificates = _options.AutoAcceptUntrustedClientCertificates,
|
||||
AddAppCertToTrustedStore = true,
|
||||
},
|
||||
|
||||
TransportConfigurations = new TransportConfigurationCollection(),
|
||||
TransportQuotas = new TransportQuotas { OperationTimeout = 15000 },
|
||||
|
||||
ServerConfiguration = new ServerConfiguration
|
||||
{
|
||||
BaseAddresses = new StringCollection { _options.EndpointUrl },
|
||||
SecurityPolicies = new ServerSecurityPolicyCollection
|
||||
{
|
||||
new ServerSecurityPolicy
|
||||
{
|
||||
SecurityMode = MessageSecurityMode.None,
|
||||
SecurityPolicyUri = SecurityPolicies.None,
|
||||
},
|
||||
},
|
||||
UserTokenPolicies = new UserTokenPolicyCollection
|
||||
{
|
||||
new UserTokenPolicy(UserTokenType.Anonymous)
|
||||
{
|
||||
PolicyId = "Anonymous",
|
||||
SecurityPolicyUri = SecurityPolicies.None,
|
||||
},
|
||||
},
|
||||
MinRequestThreadCount = 5,
|
||||
MaxRequestThreadCount = 100,
|
||||
MaxQueuedRequestCount = 200,
|
||||
},
|
||||
|
||||
TraceConfiguration = new TraceConfiguration(),
|
||||
};
|
||||
|
||||
cfg.Validate(ApplicationType.Server).GetAwaiter().GetResult();
|
||||
|
||||
if (cfg.SecurityConfiguration.AutoAcceptUntrustedCertificates)
|
||||
{
|
||||
cfg.CertificateValidator.CertificateValidation += (_, e) =>
|
||||
{
|
||||
if (e.Error.StatusCode == StatusCodes.BadCertificateUntrusted)
|
||||
e.Accept = true;
|
||||
};
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
try
|
||||
{
|
||||
_server?.Stop();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "OPC UA server stop threw during dispose");
|
||||
}
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
42
src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OpcUaServerOptions.cs
Normal file
42
src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OpcUaServerOptions.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Server.OpcUa;
|
||||
|
||||
/// <summary>
|
||||
/// OPC UA server endpoint + application-identity configuration. Bound from the
|
||||
/// <c>OpcUaServer</c> section of <c>appsettings.json</c>. PR 17 minimum-viable scope: no LDAP,
|
||||
/// no security profiles beyond None — those wire in alongside a future deployment-policy PR
|
||||
/// that reads from the central config DB instead of appsettings.
|
||||
/// </summary>
|
||||
public sealed class OpcUaServerOptions
|
||||
{
|
||||
public const string SectionName = "OpcUaServer";
|
||||
|
||||
/// <summary>
|
||||
/// Fully-qualified endpoint URI clients connect to. Use <c>0.0.0.0</c> to bind all
|
||||
/// interfaces; the stack rewrites to the machine's hostname for the returned endpoint
|
||||
/// description at GetEndpoints time.
|
||||
/// </summary>
|
||||
public string EndpointUrl { get; init; } = "opc.tcp://0.0.0.0:4840/OtOpcUa";
|
||||
|
||||
/// <summary>Human-readable application name surfaced in the endpoint description.</summary>
|
||||
public string ApplicationName { get; init; } = "OtOpcUa Server";
|
||||
|
||||
/// <summary>Stable application URI — must match the subjectAltName of the app cert.</summary>
|
||||
public string ApplicationUri { get; init; } = "urn:OtOpcUa:Server";
|
||||
|
||||
/// <summary>
|
||||
/// Directory where the OPC UA stack stores the application certificate + trusted /
|
||||
/// rejected cert folders. Defaults to <c>%ProgramData%\OtOpcUa\pki</c>; the stack
|
||||
/// creates the directory tree on first run and generates a self-signed cert.
|
||||
/// </summary>
|
||||
public string PkiStoreRoot { get; init; } =
|
||||
System.IO.Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
|
||||
"OtOpcUa", "pki");
|
||||
|
||||
/// <summary>
|
||||
/// When true, the stack auto-trusts client certs on first connect. Dev-default = true,
|
||||
/// production deployments should flip this to false and manually trust clients via the
|
||||
/// Admin UI.
|
||||
/// </summary>
|
||||
public bool AutoAcceptUntrustedClientCertificates { get; init; } = true;
|
||||
}
|
||||
@@ -1,18 +1,20 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
||||
using ZB.MOM.WW.OtOpcUa.Server.OpcUa;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Server;
|
||||
|
||||
/// <summary>
|
||||
/// BackgroundService that owns the OPC UA server lifecycle (decision #30, replacing TopShelf).
|
||||
/// Bootstraps config, starts the <see cref="DriverHost"/>, and runs until stopped.
|
||||
/// Phase 1 scope: bootstrap-only — the OPC UA transport layer that serves endpoints stays in
|
||||
/// the legacy Host until the Phase 2 cutover.
|
||||
/// Bootstraps config, starts the <see cref="DriverHost"/>, starts the OPC UA server via
|
||||
/// <see cref="OpcUaApplicationHost"/>, drives each driver's discovery into the address space,
|
||||
/// runs until stopped.
|
||||
/// </summary>
|
||||
public sealed class OpcUaServerService(
|
||||
NodeBootstrap bootstrap,
|
||||
DriverHost driverHost,
|
||||
OpcUaApplicationHost applicationHost,
|
||||
ILogger<OpcUaServerService> logger) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
@@ -22,8 +24,11 @@ public sealed class OpcUaServerService(
|
||||
var result = await bootstrap.LoadCurrentGenerationAsync(stoppingToken);
|
||||
logger.LogInformation("Bootstrap complete: source={Source} generation={Gen}", result.Source, result.GenerationId);
|
||||
|
||||
// Phase 1: no drivers are wired up at bootstrap — Galaxy still lives in legacy Host.
|
||||
// Phase 2 will register drivers here based on the fetched generation.
|
||||
// PR 17: stand up the OPC UA server + drive discovery per registered driver. Driver
|
||||
// registration itself (RegisterAsync on DriverHost) happens during an earlier DI
|
||||
// extension once the central config DB query + per-driver factory land; for now the
|
||||
// server comes up with whatever drivers are in DriverHost at start time.
|
||||
await applicationHost.StartAsync(stoppingToken);
|
||||
|
||||
logger.LogInformation("OtOpcUa.Server running. Hosted drivers: {Count}", driverHost.RegisteredDriverIds.Count);
|
||||
|
||||
@@ -40,6 +45,7 @@ public sealed class OpcUaServerService(
|
||||
public override async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await base.StopAsync(cancellationToken);
|
||||
await applicationHost.DisposeAsync();
|
||||
await driverHost.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using Serilog;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.LocalCache;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
||||
using ZB.MOM.WW.OtOpcUa.Server;
|
||||
using ZB.MOM.WW.OtOpcUa.Server.OpcUa;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
|
||||
@@ -29,10 +30,23 @@ var options = new NodeOptions
|
||||
LocalCachePath = nodeSection.GetValue<string>("LocalCachePath") ?? "config_cache.db",
|
||||
};
|
||||
|
||||
var opcUaSection = builder.Configuration.GetSection(OpcUaServerOptions.SectionName);
|
||||
var opcUaOptions = new OpcUaServerOptions
|
||||
{
|
||||
EndpointUrl = opcUaSection.GetValue<string>("EndpointUrl") ?? "opc.tcp://0.0.0.0:4840/OtOpcUa",
|
||||
ApplicationName = opcUaSection.GetValue<string>("ApplicationName") ?? "OtOpcUa Server",
|
||||
ApplicationUri = opcUaSection.GetValue<string>("ApplicationUri") ?? "urn:OtOpcUa:Server",
|
||||
PkiStoreRoot = opcUaSection.GetValue<string>("PkiStoreRoot")
|
||||
?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "OtOpcUa", "pki"),
|
||||
AutoAcceptUntrustedClientCertificates = opcUaSection.GetValue<bool?>("AutoAcceptUntrustedClientCertificates") ?? true,
|
||||
};
|
||||
|
||||
builder.Services.AddSingleton(options);
|
||||
builder.Services.AddSingleton(opcUaOptions);
|
||||
builder.Services.AddSingleton<ILocalConfigCache>(_ => new LiteDbConfigCache(options.LocalCachePath));
|
||||
builder.Services.AddSingleton<DriverHost>();
|
||||
builder.Services.AddSingleton<NodeBootstrap>();
|
||||
builder.Services.AddSingleton<OpcUaApplicationHost>();
|
||||
builder.Services.AddHostedService<OpcUaServerService>();
|
||||
|
||||
var host = builder.Build();
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Client;
|
||||
using Opc.Ua.Configuration;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
||||
using ZB.MOM.WW.OtOpcUa.Server.OpcUa;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Server.Tests;
|
||||
|
||||
[Trait("Category", "Integration")]
|
||||
public sealed class OpcUaServerIntegrationTests : IAsyncLifetime
|
||||
{
|
||||
// Use a non-default port + per-test-run PKI root to avoid colliding with anything else
|
||||
// running on the box (a live v1 Host or a developer's previous run).
|
||||
private static readonly int Port = 48400 + Random.Shared.Next(0, 99);
|
||||
private readonly string _endpoint = $"opc.tcp://localhost:{Port}/OtOpcUaTest";
|
||||
private readonly string _pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-test-{Guid.NewGuid():N}");
|
||||
|
||||
private DriverHost _driverHost = null!;
|
||||
private OpcUaApplicationHost _server = null!;
|
||||
private FakeDriver _driver = null!;
|
||||
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
_driverHost = new DriverHost();
|
||||
_driver = new FakeDriver();
|
||||
await _driverHost.RegisterAsync(_driver, "{}", CancellationToken.None);
|
||||
|
||||
var options = new OpcUaServerOptions
|
||||
{
|
||||
EndpointUrl = _endpoint,
|
||||
ApplicationName = "OtOpcUaTest",
|
||||
ApplicationUri = "urn:OtOpcUa:Server:Test",
|
||||
PkiStoreRoot = _pkiRoot,
|
||||
AutoAcceptUntrustedClientCertificates = true,
|
||||
};
|
||||
|
||||
_server = new OpcUaApplicationHost(options, _driverHost, NullLoggerFactory.Instance,
|
||||
NullLogger<OpcUaApplicationHost>.Instance);
|
||||
await _server.StartAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _server.DisposeAsync();
|
||||
await _driverHost.DisposeAsync();
|
||||
try { Directory.Delete(_pkiRoot, recursive: true); } catch { /* best-effort */ }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Client_can_connect_and_browse_driver_subtree()
|
||||
{
|
||||
using var session = await OpenSessionAsync();
|
||||
|
||||
// Browse the driver subtree registered under ObjectsFolder. FakeDriver registers one
|
||||
// folder ("TestFolder") with one variable ("Var1"), so we expect to see our driver's
|
||||
// root folder plus standard UA children.
|
||||
var rootRef = new NodeId("fake", (ushort)session.NamespaceUris.GetIndex("urn:OtOpcUa:fake"));
|
||||
session.Browse(null, null, rootRef, 0, BrowseDirection.Forward, ReferenceTypeIds.HierarchicalReferences,
|
||||
true, (uint)NodeClass.Object | (uint)NodeClass.Variable, out _, out var references);
|
||||
|
||||
references.Count.ShouldBeGreaterThan(0);
|
||||
references.ShouldContain(r => r.BrowseName.Name == "TestFolder");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Client_can_read_a_driver_variable_through_the_node_manager()
|
||||
{
|
||||
using var session = await OpenSessionAsync();
|
||||
|
||||
var nsIndex = (ushort)session.NamespaceUris.GetIndex("urn:OtOpcUa:fake");
|
||||
var varNodeId = new NodeId("TestFolder.Var1", nsIndex);
|
||||
|
||||
var dv = session.ReadValue(varNodeId);
|
||||
dv.ShouldNotBeNull();
|
||||
// FakeDriver.ReadAsync returns 42 as the value.
|
||||
dv.Value.ShouldBe(42);
|
||||
}
|
||||
|
||||
private async Task<ISession> OpenSessionAsync()
|
||||
{
|
||||
var cfg = new ApplicationConfiguration
|
||||
{
|
||||
ApplicationName = "OtOpcUaTestClient",
|
||||
ApplicationUri = "urn:OtOpcUa:TestClient",
|
||||
ApplicationType = ApplicationType.Client,
|
||||
SecurityConfiguration = new SecurityConfiguration
|
||||
{
|
||||
ApplicationCertificate = new CertificateIdentifier
|
||||
{
|
||||
StoreType = CertificateStoreType.Directory,
|
||||
StorePath = Path.Combine(_pkiRoot, "client-own"),
|
||||
SubjectName = "CN=OtOpcUaTestClient",
|
||||
},
|
||||
TrustedIssuerCertificates = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(_pkiRoot, "client-issuers") },
|
||||
TrustedPeerCertificates = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(_pkiRoot, "client-trusted") },
|
||||
RejectedCertificateStore = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(_pkiRoot, "client-rejected") },
|
||||
AutoAcceptUntrustedCertificates = true,
|
||||
AddAppCertToTrustedStore = true,
|
||||
},
|
||||
TransportConfigurations = new TransportConfigurationCollection(),
|
||||
TransportQuotas = new TransportQuotas { OperationTimeout = 15000 },
|
||||
ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60000 },
|
||||
};
|
||||
await cfg.Validate(ApplicationType.Client);
|
||||
cfg.CertificateValidator.CertificateValidation += (_, e) => e.Accept = true;
|
||||
|
||||
var instance = new ApplicationInstance { ApplicationConfiguration = cfg, ApplicationType = ApplicationType.Client };
|
||||
await instance.CheckApplicationInstanceCertificate(true, CertificateFactory.DefaultKeySize);
|
||||
|
||||
// Let the client fetch the live endpoint description from the running server so the
|
||||
// UserTokenPolicy it signs with matches what the server actually advertised (including
|
||||
// the PolicyId = "Anonymous" the server sets).
|
||||
var selected = CoreClientUtils.SelectEndpoint(cfg, _endpoint, useSecurity: false);
|
||||
var endpointConfig = EndpointConfiguration.Create(cfg);
|
||||
var configuredEndpoint = new ConfiguredEndpoint(null, selected, endpointConfig);
|
||||
|
||||
return await Session.Create(cfg, configuredEndpoint, false, "OtOpcUaTestClientSession", 60000,
|
||||
new UserIdentity(new AnonymousIdentityToken()), null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimum driver that implements enough of IDriver + ITagDiscovery + IReadable to drive
|
||||
/// the integration test. Returns a single folder with one variable that reads as 42.
|
||||
/// </summary>
|
||||
private sealed class FakeDriver : IDriver, ITagDiscovery, IReadable
|
||||
{
|
||||
public string DriverInstanceId => "fake";
|
||||
public string DriverType => "Fake";
|
||||
|
||||
public Task InitializeAsync(string driverConfigJson, CancellationToken ct) => Task.CompletedTask;
|
||||
public Task ReinitializeAsync(string driverConfigJson, CancellationToken ct) => Task.CompletedTask;
|
||||
public Task ShutdownAsync(CancellationToken ct) => Task.CompletedTask;
|
||||
public DriverHealth GetHealth() => new(DriverState.Healthy, DateTime.UtcNow, null);
|
||||
public long GetMemoryFootprint() => 0;
|
||||
public Task FlushOptionalCachesAsync(CancellationToken ct) => Task.CompletedTask;
|
||||
|
||||
public Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken ct)
|
||||
{
|
||||
var folder = builder.Folder("TestFolder", "TestFolder");
|
||||
folder.Variable("Var1", "Var1", new DriverAttributeInfo(
|
||||
"TestFolder.Var1", DriverDataType.Int32, false, null, SecurityClassification.FreeAccess, false, IsAlarm: false));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<DataValueSnapshot>> ReadAsync(
|
||||
IReadOnlyList<string> fullReferences, CancellationToken cancellationToken)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
IReadOnlyList<DataValueSnapshot> result =
|
||||
fullReferences.Select(_ => new DataValueSnapshot(42, 0u, now, now)).ToArray();
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
<PackageReference Include="Shouldly" Version="4.3.0"/>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0"/>
|
||||
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client" Version="1.5.374.126"/>
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
Reference in New Issue
Block a user