diff --git a/docs/opc-publisher/directmethods.md b/docs/opc-publisher/directmethods.md index be9735f338..66f2307414 100644 --- a/docs/opc-publisher/directmethods.md +++ b/docs/opc-publisher/directmethods.md @@ -2,9 +2,15 @@ [Home](./readme.md) -OPC Publisher version 2.8.2 and later implements [IoT Hub Direct Methods](https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-direct-methods), which can be called from an application using the [IoT Hub Device SDK](https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-sdks). +For large-scale deployments, automating the configuration and management of OPC Publisher is critical. OPC Publisher version 2.8.2 and later implements [IoT Hub Direct Methods](https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-direct-methods), which can be called from an application using the [IoT Hub Device SDK](https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-sdks). -The following direct methods are exposed: +Azure IoT Hub's Cloud-to-Device (C2D) commands allow you to remotely configure and control OPC Publisher instances running on IoT Edge devices. For example, you can send commands to update the configuration, restart the module, or change runtime parameters without needing to manually intervene on each device. An example of sending a C2D command to update the configuration: + +```bash +az iot hub invoke-module-method --hub-name --device-id --module-name --method-name SetConfiguredEndpoints --method-payload '{"Endpoints": [{"EndpointUrl": "opc.tcp://new-opc-server:4840", "OpcNodes": [{"Id": "ns=2;i=10853"}]}]}' +``` + +The following direct methods and many more can be used to remotely configure the OPC Publisher: - [PublishNodes\_V1](#publishnodes_v1) - [AddOrUpdateEndpoints\_V1](#addorupdateendpoints_v1) diff --git a/docs/opc-publisher/readme.md b/docs/opc-publisher/readme.md index 70c8074c13..69da9ae313 100644 --- a/docs/opc-publisher/readme.md +++ b/docs/opc-publisher/readme.md @@ -370,6 +370,10 @@ The simplest way to configure OPC Publisher is via a file. A basic configuration ] ``` +This configuration can be placed in a JSON file, typically named publishednodes.json, and provided to OPC Publisher using the [command line](./commandline.md) argument `-f, --pf, --publishfile`, e.g. `--pf=/app/publishednodes.json`. + +> Environment variables can also be used to configure OPC Publisher. This method is particularly useful when deploying at scale or in environments where you want to externalize configuration from the container image. An example is `PublishedNodesFile`. + Example configuration files are [here](publishednodes_2.5.json?raw=1) and [here](publishednodes_2.8.json?raw=1). ### Configuration Schema diff --git a/e2e-tests/OpcPublisher-E2E-Tests/OpcPublisher-AE-E2E-Tests.csproj b/e2e-tests/OpcPublisher-E2E-Tests/OpcPublisher-AE-E2E-Tests.csproj index 7eba23b803..dce02b7083 100644 --- a/e2e-tests/OpcPublisher-E2E-Tests/OpcPublisher-AE-E2E-Tests.csproj +++ b/e2e-tests/OpcPublisher-E2E-Tests/OpcPublisher-AE-E2E-Tests.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Azure.IIoT.OpcUa.Publisher.Models/src/Azure.IIoT.OpcUa.Publisher.Models.csproj b/src/Azure.IIoT.OpcUa.Publisher.Models/src/Azure.IIoT.OpcUa.Publisher.Models.csproj index 06f3f1cf24..84ba60e44e 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Models/src/Azure.IIoT.OpcUa.Publisher.Models.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Models/src/Azure.IIoT.OpcUa.Publisher.Models.csproj @@ -8,7 +8,7 @@ enable - + diff --git a/src/Azure.IIoT.OpcUa.Publisher.Models/tests/Azure.IIoT.OpcUa.Publisher.Models.Tests.csproj b/src/Azure.IIoT.OpcUa.Publisher.Models/tests/Azure.IIoT.OpcUa.Publisher.Models.Tests.csproj index da9faf7a0d..108f33c683 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Models/tests/Azure.IIoT.OpcUa.Publisher.Models.Tests.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Models/tests/Azure.IIoT.OpcUa.Publisher.Models.Tests.csproj @@ -17,9 +17,9 @@ all runtime; build; native; contentfiles; analyzers - - - + + + diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Azure.IIoT.OpcUa.Publisher.Module.Cli.csproj b/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Azure.IIoT.OpcUa.Publisher.Module.Cli.csproj index cf1fff200e..9ca82b5fea 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Azure.IIoT.OpcUa.Publisher.Module.Cli.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/cli/Azure.IIoT.OpcUa.Publisher.Module.Cli.csproj @@ -7,8 +7,8 @@ true - - + + diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Azure.IIoT.OpcUa.Publisher.Module.csproj b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Azure.IIoT.OpcUa.Publisher.Module.csproj index 956274faaa..b9693163bf 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Azure.IIoT.OpcUa.Publisher.Module.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Azure.IIoT.OpcUa.Publisher.Module.csproj @@ -33,15 +33,15 @@ - - - - - - + + + + + + - - + + diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Azure.IIoT.OpcUa.Publisher.Module.Tests.csproj b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Azure.IIoT.OpcUa.Publisher.Module.Tests.csproj index c3dc79992e..efab86844c 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Azure.IIoT.OpcUa.Publisher.Module.Tests.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Azure.IIoT.OpcUa.Publisher.Module.Tests.csproj @@ -33,10 +33,6 @@ Always - - - - diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Fixtures/PublisherIntegrationTestBase.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Fixtures/PublisherIntegrationTestBase.cs index c356ad2341..14f7c8aeec 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Fixtures/PublisherIntegrationTestBase.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Fixtures/PublisherIntegrationTestBase.cs @@ -310,15 +310,18 @@ static void Add(List messages, JsonElement item, ref JsonMessage? m /// /// /// + /// + /// protected void StartPublisher(string test, string publishedNodesFile = null, - string[] arguments = default, MqttVersion? version = null, int? reverseConnectPort = null) + string[] arguments = default, MqttVersion? version = null, int? reverseConnectPort = null, + int keepAliveInterval = 120, SecurityMode? securityMode = null) { var sw = Stopwatch.StartNew(); _logger = _logFactory.CreateLogger(test); arguments ??= Array.Empty(); _publishedNodesFilePath = Path.GetTempFileName(); - WritePublishedNodes(test, publishedNodesFile, reverseConnectPort != null); + WritePublishedNodes(test, publishedNodesFile, reverseConnectPort != null, securityMode); arguments = arguments.Concat( new[] @@ -341,7 +344,7 @@ protected void StartPublisher(string test, string publishedNodesFile = null, } _publisher = new PublisherModule(null, null, null, null, - _testOutputHelper, arguments, version); + _testOutputHelper, arguments, version, keepAliveInterval); _logger.LogInformation("Publisher started in {Elapsed}.", sw.Elapsed); } @@ -351,13 +354,16 @@ protected void StartPublisher(string test, string publishedNodesFile = null, /// /// /// - protected void WritePublishedNodes(string test, string publishedNodesFile, bool useReverseConnect = false) + /// + protected void WritePublishedNodes(string test, string publishedNodesFile, bool useReverseConnect = false, + SecurityMode? securityMode = null) { if (!string.IsNullOrEmpty(publishedNodesFile)) { File.WriteAllText(_publishedNodesFilePath, File.ReadAllText(publishedNodesFile) .Replace("\"{{UseReverseConnect}}\"", useReverseConnect ? "true" : "false", StringComparison.Ordinal) .Replace("{{EndpointUrl}}", EndpointUrl, StringComparison.Ordinal) + .Replace("{{SecurityMode}}", (securityMode ?? SecurityMode.None).ToString(), StringComparison.Ordinal) .Replace("{{DataSetWriterGroup}}", test, StringComparison.Ordinal)); } } @@ -393,14 +399,16 @@ protected async Task StopPublisherAsync() /// /// /// + /// /// protected PublishedNodesEntryModel[] GetEndpointsFromFile(string test, string publishedNodesFile, - bool useReverseConnect = false) + bool useReverseConnect = false, SecurityMode? securityMode = null) { IJsonSerializer serializer = new NewtonsoftJsonSerializer(); var fileContent = File.ReadAllText(publishedNodesFile) .Replace("\"{{UseReverseConnect}}\"", useReverseConnect ? "true" : "false", StringComparison.Ordinal) .Replace("{{EndpointUrl}}", EndpointUrl, StringComparison.Ordinal) + .Replace("{{SecurityMode}}", (securityMode ?? SecurityMode.None).ToString(), StringComparison.Ordinal) .Replace("{{DataSetWriterGroup}}", test, StringComparison.Ordinal); return serializer.Deserialize(fileContent); } diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Fixtures/PublisherModule.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Fixtures/PublisherModule.cs index d061649b71..e71441b774 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Fixtures/PublisherModule.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Fixtures/PublisherModule.cs @@ -102,9 +102,10 @@ public sealed class PublisherModule : WebApplicationFactory, IHtt /// /// /// + /// public PublisherModule(IMessageSink messageSink, IEnumerable devices = null, string deviceId = null, string moduleId = null, ITestOutputHelper testOutputHelper = null, - string[] arguments = default, MqttVersion? version = null) + string[] arguments = default, MqttVersion? version = null, int keepAliveInterval = 120) { _logFactory = testOutputHelper != null ? LogFactory.Create(testOutputHelper, Logging.Config) : null; ClientContainer = CreateIoTHubSdkClientContainer(messageSink, testOutputHelper, devices, version); @@ -163,7 +164,7 @@ public PublisherModule(IMessageSink messageSink, IEnumerable de $"--id={publisherId}", $"--ec={edgeHubCs}", $"--mqc={mqttCs}", - "--ki=90", + $"--ki={keepAliveInterval}", "--aa" }).ToArray(); if (OperatingSystem.IsLinux()) diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Resources/DataItems2.json b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Resources/DataItems2.json index e70350eaab..d0f2e11118 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Resources/DataItems2.json +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Resources/DataItems2.json @@ -2,7 +2,7 @@ { "EndpointUrl": "{{EndpointUrl}}", "UseReverseConnect": "{{UseReverseConnect}}", - "EndpointSecurityMode": "None", + "EndpointSecurityMode": "{{SecurityMode}}", "DataSetWriterGroup": "{{DataSetWriterGroup}}", "OpcNodes": [ { diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Resources/Fixedvalue.json b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Resources/Fixedvalue.json new file mode 100644 index 0000000000..e6eda656ce --- /dev/null +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Resources/Fixedvalue.json @@ -0,0 +1,12 @@ +[ + { + "EndpointUrl": "{{EndpointUrl}}", + "EndpointSecurityMode": "{{SecurityMode}}", + "DataSetFetchDisplayNames": true, + "OpcNodes": [ + { "Id": "i=2271" }, + { "Id": "i=2254" }, + { "Id": "i=2255" } + ] + } +] diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Sdk/ReferenceServer/AdvancedPubSubIntegrationTests.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Sdk/ReferenceServer/AdvancedPubSubIntegrationTests.cs index 443fb7bca2..a0af3647e0 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Sdk/ReferenceServer/AdvancedPubSubIntegrationTests.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Sdk/ReferenceServer/AdvancedPubSubIntegrationTests.cs @@ -8,9 +8,11 @@ namespace Azure.IIoT.OpcUa.Publisher.Module.Tests.Sdk.ReferenceServer using Azure.IIoT.OpcUa.Publisher.Module.Tests.Fixtures; using Azure.IIoT.OpcUa.Publisher.Testing.Fixtures; using Json.More; + using Microsoft.VisualStudio.TestPlatform.Utilities; using System; using System.Linq; using System.Text.Json; + using System.Threading; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; @@ -24,6 +26,119 @@ public AdvancedPubSubIntegrationTests(ITestOutputHelper output) : base(output) _output = output; } + [Fact] + public async Task RestartServerTest() + { + var server = new ReferenceServer(); + EndpointUrl = server.EndpointUrl; + const string name = nameof(RestartServerTest); + StartPublisher(name, "./Resources/Fixedvalue.json", + arguments: new string[] { "--mm=PubSub", "--dm=false" }, keepAliveInterval: 1); + try + { + // Arrange + // Act + var (metadata, messages) = await WaitForMessagesAndMetadataAsync(TimeSpan.FromMinutes(2), 1, + messageType: "ua-data"); + + // Assert + var message = Assert.Single(messages).Message; + AssertFixedValueMessage(message); + Assert.NotNull(metadata); + + await server.RestartAsync(WaitUntilDisconnected); + _output.WriteLine("Restarted server"); + + (metadata, messages) = await WaitForMessagesAndMetadataAsync(TimeSpan.FromMinutes(2), 1, + messageType: "ua-data"); + + message = Assert.Single(messages).Message; + AssertFixedValueMessage(message); + Assert.Null(metadata); + } + finally + { + server.Dispose(); + await StopPublisherAsync(); + } + } + + [Fact] + public async Task RestartServerWithHeartbeatTest() + { + var server = new ReferenceServer(); + EndpointUrl = server.EndpointUrl; + const string name = nameof(RestartServerWithHeartbeatTest); + StartPublisher(name, "./Resources/Heartbeat2.json", + arguments: new string[] { "--mm=PubSub", "--dm=false", "--bs=1" }, keepAliveInterval: 1); + try + { + // Arrange + // Act + var (metadata, messages) = await WaitForMessagesAndMetadataAsync(TimeSpan.FromMinutes(2), 1, + messageType: "ua-data"); + + // Assert + var message = Assert.Single(messages).Message; + Assert.NotNull(metadata); + + await server.RestartAsync(WaitUntilDisconnected); + _output.WriteLine("Restarted server"); + + (metadata, messages) = await WaitForMessagesAndMetadataAsync(TimeSpan.FromSeconds(10), 1000, + messageType: "ua-data"); + (metadata, messages) = await WaitForMessagesAndMetadataAsync(TimeSpan.FromSeconds(10), 1, + messageType: "ua-data"); + + message = Assert.Single(messages).Message; + _output.WriteLine(message.ToJsonString()); + var output = message.GetProperty("Messages")[0].GetProperty("Payload").GetProperty("Output"); + Assert.NotEqual(JsonValueKind.Null, output.ValueKind); + Assert.InRange(output.GetProperty("Value").GetDouble(), double.MinValue, double.MaxValue); + } + finally + { + server.Dispose(); + await StopPublisherAsync(); + } + } + + [Fact] + public async Task RestartServerWithCyclicReadTest() + { + var server = new ReferenceServer(); + EndpointUrl = server.EndpointUrl; + const string name = nameof(RestartServerWithCyclicReadTest); + StartPublisher(name, "./Resources/CyclicRead.json", + arguments: new string[] { "--mm=PubSub", "--dm=false" }, keepAliveInterval: 1); + try + { + // Arrange + // Act + var (metadata, messages) = await WaitForMessagesAndMetadataAsync(TimeSpan.FromMinutes(2), 1, + messageType: "ua-data"); + + // Assert + var message = Assert.Single(messages).Message; + Assert.NotNull(metadata); + + await server.RestartAsync(WaitUntilDisconnected); + _output.WriteLine("Restarted server"); + + (metadata, messages) = await WaitForMessagesAndMetadataAsync(TimeSpan.FromSeconds(10), 1000, + messageType: "ua-data"); + (metadata, messages) = await WaitForMessagesAndMetadataAsync(TimeSpan.FromMinutes(2), 1, + messageType: "ua-data"); + + message = Assert.Single(messages).Message; + } + finally + { + server.Dispose(); + await StopPublisherAsync(); + } + } + [Fact] public async Task SwitchServerWithSameWriterGroupTest() { @@ -298,6 +413,63 @@ public async Task SwitchServerWithDifferentDataTest() } } + [Fact] + public async Task SwitchSecuritySettingsTest() + { + var server = new ReferenceServer(); + EndpointUrl = server.EndpointUrl; + const string name = nameof(SwitchSecuritySettingsTest); + StartPublisher(name, "./Resources/Fixedvalue.json", arguments: new string[] { "--mm=PubSub", "--dm=false", "--aa" }, + securityMode: Models.SecurityMode.SignAndEncrypt); + try + { + // Arrange + // Act + var (metadata, messages) = await WaitForMessagesAndMetadataAsync(TimeSpan.FromMinutes(2), 1, + messageType: "ua-data"); + + // Assert + var message = Assert.Single(messages).Message; + AssertFixedValueMessage(message); + Assert.NotNull(metadata); + + var diagnostics = await PublisherApi.GetDiagnosticInfoAsync(); + var diag = Assert.Single(diagnostics); + Assert.Equal(Models.SecurityMode.SignAndEncrypt, diag.Endpoint.EndpointSecurityMode); + + WritePublishedNodes(name, "./Resources/Fixedvalue.json", securityMode: Models.SecurityMode.None); + (metadata, messages) = await WaitForMessagesAndMetadataAsync(TimeSpan.FromMinutes(2), 1, + messageType: "ua-data"); + + // Assert + message = Assert.Single(messages).Message; + AssertFixedValueMessage(message); + Assert.NotNull(metadata); + + diagnostics = await PublisherApi.GetDiagnosticInfoAsync(); + diag = Assert.Single(diagnostics); + Assert.Null(diag.Endpoint.EndpointSecurityMode); + + WritePublishedNodes(name, "./Resources/Fixedvalue.json", securityMode: Models.SecurityMode.Sign); + (metadata, messages) = await WaitForMessagesAndMetadataAsync(TimeSpan.FromMinutes(2), 1, + messageType: "ua-data"); + + // Assert + message = Assert.Single(messages).Message; + AssertFixedValueMessage(message); + Assert.NotNull(metadata); + + diagnostics = await PublisherApi.GetDiagnosticInfoAsync(); + diag = Assert.Single(diagnostics); + Assert.Equal(Models.SecurityMode.Sign, diag.Endpoint.EndpointSecurityMode); + } + finally + { + server.Dispose(); + await StopPublisherAsync(); + } + } + [Fact] public async Task RestartConfigurationTest() { @@ -346,5 +518,38 @@ internal static JsonElement WaitUntilOutput2(JsonElement jsonElement) } return default; } + + private async Task WaitUntilDisconnected() + { + using var cts = new CancellationTokenSource(60000); + while (true) + { + cts.Token.ThrowIfCancellationRequested(); + var diagnostics = await PublisherApi.GetDiagnosticInfoAsync(cts.Token); + var diag = Assert.Single(diagnostics); + if (!diag.OpcEndpointConnected) + { + _output.WriteLine("Disconnected!"); + break; + } + await Task.Delay(1000, cts.Token); + } + } + + internal static void AssertFixedValueMessage(JsonElement message) + { + var m = message.GetProperty("Messages")[0]; + var type = m.GetProperty("MessageType").GetString(); + // TODO Assert.Equal("ua-keyframe", type); + var payload1 = m.GetProperty("Payload"); + var items1 = new[] + { + payload1.GetProperty("LocaleIdArray"), + payload1.GetProperty("ServerArray"), + payload1.GetProperty("NamespaceArray") + }; + Assert.All(items1, item => + Assert.Equal(JsonValueKind.Array, item.GetProperty("Value").ValueKind)); + } } } diff --git a/src/Azure.IIoT.OpcUa.Publisher.Sdk/src/Azure.IIoT.OpcUa.Publisher.Sdk.csproj b/src/Azure.IIoT.OpcUa.Publisher.Sdk/src/Azure.IIoT.OpcUa.Publisher.Sdk.csproj index 5f83417bd0..6e29316792 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Sdk/src/Azure.IIoT.OpcUa.Publisher.Sdk.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Sdk/src/Azure.IIoT.OpcUa.Publisher.Sdk.csproj @@ -8,9 +8,9 @@ enable - - - + + + diff --git a/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk/cli/Azure.IIoT.OpcUa.Publisher.Service.Cli.csproj b/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk/cli/Azure.IIoT.OpcUa.Publisher.Service.Cli.csproj index bd4ecec558..f70cf1e976 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk/cli/Azure.IIoT.OpcUa.Publisher.Service.Cli.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk/cli/Azure.IIoT.OpcUa.Publisher.Service.Cli.csproj @@ -15,9 +15,9 @@ - + - + diff --git a/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk.csproj b/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk.csproj index d437e29f5d..8e5d9a23ce 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk/src/Azure.IIoT.OpcUa.Publisher.Service.Sdk.csproj @@ -12,10 +12,10 @@ - - - - + + + + diff --git a/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi.csproj b/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi.csproj index 627a1344cc..3cc1f06ca4 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi.csproj @@ -20,7 +20,7 @@ - + @@ -28,11 +28,11 @@ - - - + + + - + diff --git a/src/Azure.IIoT.OpcUa.Publisher.Service/src/Azure.IIoT.OpcUa.Publisher.Service.csproj b/src/Azure.IIoT.OpcUa.Publisher.Service/src/Azure.IIoT.OpcUa.Publisher.Service.csproj index 018f9e2562..1032f3d979 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Service/src/Azure.IIoT.OpcUa.Publisher.Service.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Service/src/Azure.IIoT.OpcUa.Publisher.Service.csproj @@ -6,7 +6,7 @@ enable - + diff --git a/src/Azure.IIoT.OpcUa.Publisher.Testing/src/Azure.IIoT.OpcUa.Publisher.Testing.Servers.csproj b/src/Azure.IIoT.OpcUa.Publisher.Testing/src/Azure.IIoT.OpcUa.Publisher.Testing.Servers.csproj index 00fd51f89e..a3d1337a89 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Testing/src/Azure.IIoT.OpcUa.Publisher.Testing.Servers.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Testing/src/Azure.IIoT.OpcUa.Publisher.Testing.Servers.csproj @@ -58,7 +58,7 @@ - + diff --git a/src/Azure.IIoT.OpcUa.Publisher.Testing/src/IServerHost.cs b/src/Azure.IIoT.OpcUa.Publisher.Testing/src/IServerHost.cs index 45845b4a8d..7fabad7564 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Testing/src/IServerHost.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Testing/src/IServerHost.cs @@ -33,6 +33,13 @@ public interface IServerHost : IDisposable /// Task StartAsync(IEnumerable ports); + /// + /// Restart server. + /// + /// + /// + Task RestartAsync(Func predicate = null); + /// /// Add reverse connection /// diff --git a/src/Azure.IIoT.OpcUa.Publisher.Testing/src/ServerConsoleHost.cs b/src/Azure.IIoT.OpcUa.Publisher.Testing/src/ServerConsoleHost.cs index 8eff353ce7..e64efaaf7d 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Testing/src/ServerConsoleHost.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Testing/src/ServerConsoleHost.cs @@ -13,6 +13,8 @@ namespace Azure.IIoT.OpcUa.Publisher.Stack.Services using Opc.Ua.Server; using System; using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; @@ -139,6 +141,7 @@ public async Task StartAsync(IEnumerable ports) if (_server == null) { await StartServerInternalAsync(ports, PkiRootPath).ConfigureAwait(false); + _ports = ports.ToArray(); return; } #pragma warning restore CA1508 // Avoid dead conditional code @@ -158,6 +161,35 @@ public async Task StartAsync(IEnumerable ports) throw new InvalidOperationException($"Server {this} already started"); } + /// + public async Task RestartAsync(Func predicate) + { + await _lock.WaitAsync().ConfigureAwait(false); + try + { + if (_server != null) + { + _server.Stop(); + _server.Dispose(); + + if (predicate != null) + { + await predicate().ConfigureAwait(false); + } + + _logger.LogInformation("Restarting server {Instance}...", this); + Debug.Assert(_ports != null); + + await StartServerInternalAsync(_ports, + PkiRootPath).ConfigureAwait(false); + } + } + finally + { + _lock.Release(); + } + } + /// public void Dispose() { @@ -257,5 +289,6 @@ public override Task ShowAsync() private readonly IServerFactory _factory; private readonly SemaphoreSlim _lock = new(1, 1); private ServerBase _server; + private int[] _ports; } } diff --git a/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Azure.IIoT.OpcUa.Publisher.Testing.csproj b/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Azure.IIoT.OpcUa.Publisher.Testing.csproj index 5f2fe259bc..d928520749 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Azure.IIoT.OpcUa.Publisher.Testing.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Azure.IIoT.OpcUa.Publisher.Testing.csproj @@ -5,9 +5,9 @@ enable - - - + + + diff --git a/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Fixtures/BaseServerFixture.cs b/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Fixtures/BaseServerFixture.cs index 0963f33e31..99a2a3330f 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Fixtures/BaseServerFixture.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Fixtures/BaseServerFixture.cs @@ -32,6 +32,7 @@ namespace Azure.IIoT.OpcUa.Publisher.Testing.Fixtures using System.Net.Sockets; using System.Security.Cryptography.X509Certificates; using System.Timers; + using System.Threading.Tasks; /// /// Adds sample server as fixture to unit tests @@ -247,6 +248,16 @@ public void Dispose() GC.SuppressFinalize(this); } + /// + /// Restart server + /// + /// + /// + public Task RestartAsync(Func predicate) + { + return _serverHost.RestartAsync(predicate); + } + /// /// Override to dispose /// diff --git a/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Runtime/TestClientConfig.cs b/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Runtime/TestClientConfig.cs index 2df55f09a0..bb57c3ad85 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Runtime/TestClientConfig.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Testing/tests/Runtime/TestClientConfig.cs @@ -18,10 +18,8 @@ namespace Azure.IIoT.OpcUa.Publisher.Testing.Runtime public sealed class TestClientConfig : ConfigureOptionBase, IDisposable { - public TestClientConfig(IConfiguration configuration, - bool autoAccept = false) : base(configuration) + public TestClientConfig(IConfiguration configuration) : base(configuration) { - _autoAccept = autoAccept; _path = Path.Combine(Directory.GetCurrentDirectory(), "pki", Guid.NewGuid().ToByteArray().ToBase16String()); } @@ -29,9 +27,7 @@ public TestClientConfig(IConfiguration configuration, /// public override void Configure(string? name, OpcUaClientOptions options) { - options.Security.AutoAcceptUntrustedCertificates = _autoAccept; options.Security.PkiRootPath = _path; - options.KeepAliveIntervalDuration = TimeSpan.FromSeconds(120); options.LingerTimeoutDuration = TimeSpan.FromSeconds(20); } @@ -44,7 +40,6 @@ public void Dispose() } } - private readonly bool _autoAccept; private readonly string _path; } } diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Azure.IIoT.OpcUa.Publisher.csproj b/src/Azure.IIoT.OpcUa.Publisher/src/Azure.IIoT.OpcUa.Publisher.csproj index befa13c473..e09e76cdd9 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Azure.IIoT.OpcUa.Publisher.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Azure.IIoT.OpcUa.Publisher.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Models/MessagingProfile.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Models/MessagingProfile.cs index afd7fc18f2..666bf0671a 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Models/MessagingProfile.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Models/MessagingProfile.cs @@ -441,7 +441,7 @@ private static DataSetFieldContentFlags BuildDataSetFieldContentMask( (DataSetFieldContentFlags.NodeId | DataSetFieldContentFlags.DisplayName | DataSetFieldContentFlags.EndpointUrl) : - DataSetFieldContentFlags.ServerTimestamp ) + DataSetFieldContentFlags.ServerTimestamp) ; } diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Services/NetworkMessageSink.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Services/NetworkMessageSink.cs index 3ca68f9005..8a367e59ce 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Services/NetworkMessageSink.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Services/NetworkMessageSink.cs @@ -440,6 +440,7 @@ private async Task SendAsync((IEvent Event, Action Complete) message) { message.Complete(); message.Event.Dispose(); + _outer._logger.LogDebug("Closed. Network message dropped."); return; } try diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.Condition.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.Condition.cs index 5332040ade..574e355020 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.Condition.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.Condition.cs @@ -31,7 +31,10 @@ internal abstract partial class OpcUaMonitoredItem [KnownType(typeof(AggregateFilter))] internal class Condition : Event { - public bool TimerEnabled => _conditionTimer?.Enabled ?? false; + /// + /// Whether timer is enabled + /// + public bool TimerEnabled { get; set; } /// /// Create condition item @@ -66,9 +69,7 @@ private Condition(Condition item, bool copyEventHandlers, _updateInterval = item._updateInterval; _conditionHandlingState = item._conditionHandlingState; _lastSentPendingConditions = item._lastSentPendingConditions; - _callback = item._callback; - - if (_callback != null) + if (item.TimerEnabled) { EnableConditionTimer(); } @@ -272,17 +273,15 @@ public override bool MergeWith(OpcUaMonitoredItem item, IOpcUaSession session, /// public override bool TryCompleteChanges(Subscription subscription, - ref bool applyChanges, Callback cb) + ref bool applyChanges) { - var result = base.TryCompleteChanges(subscription, ref applyChanges, cb); + var result = base.TryCompleteChanges(subscription, ref applyChanges); if (!AttachedToSubscription || !result) { DisableConditionTimer(); - _callback = null; } else { - _callback = cb; EnableConditionTimer(); } return result; @@ -409,11 +408,6 @@ private void OnConditionTimerElapsed(object? sender, ElapsedEventArgs e) private void SendPendingConditions() { var state = _conditionHandlingState; - var callback = _callback; - if (callback == null) - { - return; - } var notifications = state.Active .Select(entry => entry.Value @@ -425,7 +419,7 @@ private void SendPendingConditions() foreach (var conditionNotification in notifications) { - callback(Owner, MessageType.Condition, conditionNotification, + Publish(Owner, MessageType.Condition, conditionNotification, eventTypeName: EventTypeName); } } @@ -452,6 +446,7 @@ private void EnableConditionTimer() } _conditionTimer.Interval = TimeSpan.FromSeconds(1); _conditionTimer.Enabled = true; + TimerEnabled = true; } } @@ -469,6 +464,7 @@ private void DisableConditionTimer() _conditionTimer = null; _logger.LogDebug("Disabled condition timer."); } + TimerEnabled = false; } } @@ -496,7 +492,6 @@ private sealed class ConditionHandlingState = new Dictionary>(); } - private Callback? _callback; private ConditionHandlingState _conditionHandlingState; private DateTimeOffset _lastSentPendingConditions; private int _snapshotInterval; diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.CyclicRead.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.CyclicRead.cs index 695abd0cd1..33d6b507da 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.CyclicRead.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.CyclicRead.cs @@ -66,7 +66,8 @@ private CyclicRead(CyclicRead item, bool copyEventHandlers, : base(item, copyEventHandlers, copyClientHandle) { _client = item._client; - if (item._sampling) + _subscriptionName = item._subscriptionName; + if (_subscriptionName != null) { EnsureSamplerRunning(); } @@ -161,6 +162,7 @@ public override bool MergeWith(OpcUaMonitoredItem item, IOpcUaSession session, } else { + _subscriptionName = Subscription.DisplayName; Debug.Assert(MonitoringMode == MonitoringMode.Disabled); EnsureSamplerRunning(); } @@ -171,7 +173,6 @@ public override bool MergeWith(OpcUaMonitoredItem item, IOpcUaSession session, /// private void EnsureSamplerRunning() { - Debug.Assert(AttachedToSubscription); lock (_lock) { if (_disposed) @@ -180,7 +181,7 @@ private void EnsureSamplerRunning() } if (_sampler == null) { - _sampling = true; + Debug.Assert(_subscriptionName != null); _sampler = _client.Sample( TimeSpan.FromMilliseconds(SamplingInterval), Template.CyclicReadMaxAge ?? TimeSpan.Zero, @@ -190,7 +191,7 @@ private void EnsureSamplerRunning() IndexRange = IndexRange, NodeId = ResolvedNodeId }, - Subscription.DisplayName, ClientHandle); + _subscriptionName, ClientHandle); _logger.LogDebug("Item {Item} successfully registered with sampler.", this); } @@ -207,7 +208,7 @@ private async Task StopSamplerAsync() lock (_lock) { _sampler = null; - _sampling = false; + _subscriptionName = null; } if (sampler != null) { @@ -234,7 +235,7 @@ public override bool TryGetMonitoredItemNotifications(DateTimeOffset timestamp, private readonly OpcUaClient _client; private IAsyncDisposable? _sampler; - private bool _sampling; + private string? _subscriptionName; private readonly object _lock = new(); private bool _disposed; } diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.DataChange.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.DataChange.cs index f35a4fc439..9de95929c5 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.DataChange.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.DataChange.cs @@ -47,7 +47,8 @@ public override (string NodeId, UpdateNodeId Update)? Register NodeId = v.AsString(context, Template.NamespaceFormat) ?? string.Empty; // We only want to register the node once for reading inside a session _registeredForReading = true; - }) : null; + } + ) : null; /// public override (string NodeId, UpdateString Update)? GetDisplayName diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.Field.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.Field.cs index a96adc3340..206e2c665c 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.Field.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.Field.cs @@ -177,8 +177,7 @@ public override bool RemoveFrom(Subscription subscription, out bool metadataChan /// public override bool TryCompleteChanges(Subscription subscription, - ref bool applyChanges, - Callback cb) + ref bool applyChanges) { return true; } diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.Heartbeat.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.Heartbeat.cs index e377a1806f..7414134306 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.Heartbeat.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.Heartbeat.cs @@ -72,7 +72,6 @@ private Heartbeat(Heartbeat item, bool copyEventHandlers, { _heartbeatInterval = item._heartbeatInterval; _heartbeatBehavior = item._heartbeatBehavior; - _callback = item._callback; if (item.TimerEnabled) { EnableHeartbeatTimer(); @@ -185,7 +184,7 @@ public override bool MergeWith(OpcUaMonitoredItem item, IOpcUaSession session, /// public override bool TryCompleteChanges(Subscription subscription, - ref bool applyChanges, Callback cb) + ref bool applyChanges) { if (_disposed) { @@ -193,20 +192,18 @@ public override bool TryCompleteChanges(Subscription subscription, "and the timer is handled by the new subscription now.", this); return false; } - var result = base.TryCompleteChanges(subscription, ref applyChanges, cb); + var result = base.TryCompleteChanges(subscription, ref applyChanges); { var lkg = (_heartbeatBehavior & HeartbeatBehavior.WatchdogLKG) == HeartbeatBehavior.WatchdogLKG; if (!AttachedToSubscription || (!result && lkg)) { - _callback = null; // Stop heartbeat DisableHeartbeatTimer(); } else { Debug.Assert(AttachedToSubscription); - _callback = cb; EnableHeartbeatTimer(); } } @@ -288,12 +285,17 @@ private static bool IsGoodDataValue(DataValue? value) /// private void SendHeartbeatNotifications(object? sender, ElapsedEventArgs e) { - var callback = _callback; - if (callback == null || !Valid) + if (!Valid) { return; } + if (!AttachedToSubscription) + { + _logger.LogInformation("{Item}: Missing subscription.", this); + return; + } + var lastSequenceNumber = _lastSequenceNumber; var lastNotification = LastReceivedValue as MonitoredItemNotification; if ((_heartbeatBehavior & HeartbeatBehavior.WatchdogLKG) @@ -301,7 +303,7 @@ private void SendHeartbeatNotifications(object? sender, ElapsedEventArgs e) !IsGoodDataValue(lastNotification?.Value)) { // Currently no last known good value (LKG) to send - _logger.LogDebug("{Item}: No last known good value to send.", this); + _logger.LogInformation("{Item}: No last known good value to send.", this); return; } @@ -314,7 +316,7 @@ private void SendHeartbeatNotifications(object? sender, ElapsedEventArgs e) if (lastValue == null) { // Currently no last known value (LKV) to send - _logger.LogDebug("{Item}: No last known value to send.", this); + _logger.LogInformation("{Item}: No last known value to send.", this); return; } if ((_heartbeatBehavior & HeartbeatBehavior.WatchdogLKVWithUpdatedTimestamps) @@ -351,7 +353,7 @@ private void SendHeartbeatNotifications(object? sender, ElapsedEventArgs e) // New value came in while running the timer callback - no need to send heartbeat return; } - callback(Owner, MessageType.DeltaFrame, heartbeat.YieldReturn().ToList(), + Publish(Owner, MessageType.DeltaFrame, heartbeat.YieldReturn().ToList(), diagnosticsOnly: (_heartbeatBehavior & HeartbeatBehavior.WatchdogLKVDiagnosticsOnly) == HeartbeatBehavior.WatchdogLKVDiagnosticsOnly, timestamp: e.SignalTime); } @@ -374,7 +376,7 @@ private void EnableHeartbeatTimer() AutoReset = true }; _heartbeatTimer.Elapsed += SendHeartbeatNotifications; - _logger.LogDebug("Re-enable heartbeat timer"); + _logger.LogInformation("Re-enable heartbeat timer"); } _heartbeatTimer.Interval = _heartbeatInterval; _heartbeatTimer.Enabled = true; @@ -403,7 +405,6 @@ private void DisableHeartbeatTimer() private TimerEx? _heartbeatTimer; private HeartbeatBehavior _heartbeatBehavior; private TimeSpan _heartbeatInterval; - private Callback? _callback; private StatusCode? _lastStatusCode; private uint _lastSequenceNumber; private readonly object _timerLock = new(); diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.ModelChange.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.ModelChange.cs index 6ac14ef148..6482ff01f1 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.ModelChange.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.ModelChange.cs @@ -64,7 +64,6 @@ private ModelChangeEventItem(ModelChangeEventItem item, bool copyEventHandlers, { Template = item.Template; _client = item._client; - _callback = item._callback; _fields = item._fields; } @@ -165,22 +164,6 @@ public override ValueTask GetMetaDataAsync(IOpcUaSession session, return ValueTask.CompletedTask; } - /// - public override bool TryCompleteChanges(Subscription subscription, - ref bool applyChanges, Callback cb) - { - var result = base.TryCompleteChanges(subscription, ref applyChanges, cb); - if (!AttachedToSubscription) - { - _callback = null; - } - else - { - _callback = cb; - } - return result; - } - /// public override Func? FinalizeCompleteChanges => async _ => { @@ -320,7 +303,7 @@ protected override bool OnSamplingIntervalOrQueueSizeRevised( /// private void OnNodeChange(object? sender, Change e) { - _callback?.Invoke(Owner, MessageType.Event, + Publish(Owner, MessageType.Event, CreateEvent(_nodeChangeType, e).ToList(), sender as ISession, EventTypeName); } @@ -332,7 +315,7 @@ private void OnNodeChange(object? sender, Change e) /// private void OnReferenceChange(object? sender, Change e) { - _callback?.Invoke(Owner, MessageType.Event, + Publish(Owner, MessageType.Event, CreateEvent(_refChangeType, e).ToList(), sender as ISession, EventTypeName); } @@ -468,7 +451,6 @@ private static readonly ExpandedNodeId _nodeChangeType private readonly OpcUaClient _client; private readonly object _lock = new(); private IOpcUaBrowser? _browser; - private Callback? _callback; private bool _disposed; } } diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.cs index 81a8bb8ade..348c0399bc 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.cs @@ -42,21 +42,6 @@ public delegate void UpdateNodeId(NodeId nodeId, public delegate void UpdateRelativePath(RelativePath path, IServiceMessageContext messageContext); - /// - /// Callback - /// - /// - /// - /// - /// - /// - /// - /// - public delegate void Callback(ISubscriber owner, MessageType messageType, - IList notifications, - ISession? session = null, string? eventTypeName = null, - bool diagnosticsOnly = false, DateTimeOffset? timestamp = null); - /// /// Monitored item /// @@ -402,10 +387,9 @@ public virtual bool RemoveFrom(Subscription subscription, /// /// /// - /// /// public virtual bool TryCompleteChanges(Subscription subscription, - ref bool applyChanges, Callback cb) + ref bool applyChanges) { if (!Valid) { @@ -1028,6 +1012,33 @@ public void Add(ISubscriber callback, } } + /// + /// Callback + /// + /// + /// + /// + /// + /// + /// + /// + protected void Publish(ISubscriber owner, MessageType messageType, + IList notifications, + ISession? session = null, string? eventTypeName = null, + bool diagnosticsOnly = false, DateTimeOffset? timestamp = null) + { + if (Subscription is not OpcUaSubscription subscription) + { + _logger.LogDebug( + "Cannot publish notification. Missing subscription for {Item}.", + this); + return; + } + subscription.SendNotification( + owner, messageType, notifications, session, eventTypeName, + diagnosticsOnly, timestamp); + } + /// /// Logger /// diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaSubscription.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaSubscription.cs index 6ca974acda..4b2538139c 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaSubscription.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaSubscription.cs @@ -825,7 +825,7 @@ private async Task FinalizeSyncAsync(CancellationToken ct) var shouldEnable = MonitoredItems .OfType() - .Any(m => m.AttachedToSubscription + .Any(m => m.AttachedToSubscription && m.MonitoringMode != Opc.Ua.MonitoringMode.Disabled); if (PublishingEnabled ^ shouldEnable) { @@ -1308,14 +1308,14 @@ private async ValueTask SynchronizeMonitoredItemsAsync( desiredMonitoredItems.Count, remove.Count, this); foreach (var monitoredItem in desiredMonitoredItems.Concat(remove)) { - if (!monitoredItem.TryCompleteChanges(this, ref applyChanges, SendNotification)) + if (!monitoredItem.TryCompleteChanges(this, ref applyChanges)) { // Apply more changes in future passes invalidItems++; } } - Debug.Assert(remove.All(m => !m.AttachedToSubscription), + Debug.Assert(remove.All(m => !m.AttachedToSubscription), "All removed items should be detached now"); var set = desiredMonitoredItems.Where(m => m.Valid).ToList(); _logger.LogDebug( @@ -1737,7 +1737,7 @@ private void TriggerManageSubscription() /// /// /// - private void SendNotification(ISubscriber callback, MessageType messageType, + internal void SendNotification(ISubscriber callback, MessageType messageType, IList notifications, ISession? session, string? eventTypeName, bool diagnosticsOnly, DateTimeOffset? timestamp) { @@ -1749,7 +1749,7 @@ private void SendNotification(ISubscriber callback, MessageType messageType, if (session == null) { // Can only send with context - _logger.LogDebug("Failed to send notification since no session exists " + + _logger.LogWarning("Failed to send notification since no session exists " + "to use as context. Notification was dropped."); return; } diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Storage/PublishedNodesConverter.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Storage/PublishedNodesConverter.cs index fd44d7d32b..ba3db97002 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Storage/PublishedNodesConverter.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Storage/PublishedNodesConverter.cs @@ -532,8 +532,8 @@ IEnumerable GetNodeModels(PublishedNodesEntryModel item, int scale IndexRange = node.IndexRange, RegisterNode = node.RegisterNode, UseCyclicRead = node.UseCyclicRead, - CyclicReadMaxAgeTimespan = node.GetNormalizedCyclicReadMaxAge(), - SkipFirst = node.SkipFirst, + CyclicReadMaxAgeTimespan = node.GetNormalizedCyclicReadMaxAge(), + SkipFirst = node.SkipFirst, DataChangeTrigger = node.DataChangeTrigger, DeadbandType = node.DeadbandType, DeadbandValue = node.DeadbandValue, diff --git a/src/Azure.IIoT.OpcUa/src/Azure.IIoT.OpcUa.csproj b/src/Azure.IIoT.OpcUa/src/Azure.IIoT.OpcUa.csproj index 2e2750fc60..333f9ee957 100644 --- a/src/Azure.IIoT.OpcUa/src/Azure.IIoT.OpcUa.csproj +++ b/src/Azure.IIoT.OpcUa/src/Azure.IIoT.OpcUa.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/Azure.IIoT.OpcUa/tests/Azure.IIoT.OpcUa.Tests.csproj b/src/Azure.IIoT.OpcUa/tests/Azure.IIoT.OpcUa.Tests.csproj index 998463c692..b15fa5a395 100644 --- a/src/Azure.IIoT.OpcUa/tests/Azure.IIoT.OpcUa.Tests.csproj +++ b/src/Azure.IIoT.OpcUa/tests/Azure.IIoT.OpcUa.Tests.csproj @@ -14,7 +14,7 @@ all runtime; build; native; contentfiles; analyzers - +