diff --git a/common.props b/common.props index fb4fbe4d15..f4d6be2577 100644 --- a/common.props +++ b/common.props @@ -42,8 +42,8 @@ - - + + diff --git a/docs/opc-publisher/readme.md b/docs/opc-publisher/readme.md index 47128d544e..dce13ae579 100644 --- a/docs/opc-publisher/readme.md +++ b/docs/opc-publisher/readme.md @@ -575,9 +575,11 @@ The `DataSetExtensionFields` object in the [configuration](#configuration-schema Values are formatted using the extended OPC UA Variant [JSON format](#json-encoding). This encoding is compliant with OPC UA Part 6, however it also allows to use simple JSON types which will be interpreted as Variant values using a simple heuristic, mapping the best OPC UA type possible to it. +> IMPORTANT: Extension fields are only sent as part of key frame messages when using Pub Sub encoding. You must configure a key frame count for key frames to be sent as the default key frame count value is 0 and therefore key frames are disabled. + #### Status codes -The status code `value` is the integer received over the wire from the server (full one including all bits). +The status code `value` is the integer received over the wire from the server (full one including all bits). StatusCode "Good" is defined as 0 in OPC UA, which is omitted in JSON encoding (as per Part 6). The `symbol` in the encoding is what OPC Publisher is looking up from the standard defined codes (using the code bits which are the 16 bits defining the error code part of the status code). 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 892f4081ed..3806da5aae 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 @@ -10,8 +10,4 @@ - - - - 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 c8ead4dd47..a1d02b803e 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 @@ -5,12 +5,12 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers @@ -25,8 +25,4 @@ - - - - \ No newline at end of file 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 c7e8bde479..f35bbd77da 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 @@ -57,8 +57,4 @@ Always - - - - \ No newline at end of file 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 4dcb09b5f5..3a491fc13e 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 @@ -36,7 +36,7 @@ - + @@ -49,8 +49,4 @@ - - - - diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Controllers/PublisherController.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Controllers/PublisherController.cs index ffa0c2792b..f43d885622 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Controllers/PublisherController.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Controllers/PublisherController.cs @@ -22,10 +22,33 @@ public class PublisherController : IMethodController /// /// Support restarting the module /// - /// - public PublisherController(ILogger logger) + /// + /// + /// + public PublisherController(IProcessControl process, IApiKeyProvider apikey, + ISslCertProvider certificate) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _apikey = apikey; + _certificate = certificate; + _process = process; + } + + /// + /// Get ApiKey to use when calling the HTTP API. + /// + /// + public Task GetApiKeyAsync() + { + return Task.FromResult(_apikey.ApiKey); + } + + /// + /// Get server certificate as PEM. + /// + /// + public Task GetServerCertificateAsync() + { + return Task.FromResult(_certificate.Certificate?.ExportCertificatePem()); } /// @@ -36,19 +59,16 @@ public PublisherController(ILogger logger) /// public async Task ShutdownAsync(bool failFast = false) { - _logger.LogInformation("Shutdown called."); - if (failFast) - { - Environment.FailFast("Shutdown was invoked remotely."); - } - else + if (!_process.Shutdown(failFast)) { - Environment.Exit(0); + // Should be gone now + await Task.Delay(TimeSpan.FromSeconds(10)).ConfigureAwait(false); + throw new NotSupportedException("Failed to invoke shutdown"); } - await Task.Delay(TimeSpan.FromSeconds(10)).ConfigureAwait(false); - throw new NotSupportedException("Failed to invoke shutdown"); } - private readonly ILogger _logger; + private readonly IApiKeyProvider _apikey; + private readonly ISslCertProvider _certificate; + private readonly IProcessControl _process; } } diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/CommandLine.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/CommandLine.cs index 17f01542bb..7dce51d449 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/CommandLine.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/CommandLine.cs @@ -142,12 +142,12 @@ public CommandLine(string[] args) (bool? b) => this[OpcUaSubscriptionConfig.EnableDataSetKeepAlivesKey] = b?.ToString() ?? "True" }, { $"eip|immediatepublishing:|{OpcUaSubscriptionConfig.EnableImmediatePublishingKey}:", "By default OPC Publisher will create a subscription with publishing disabled and only enable it after it has filled it with all configured monitored items. Use this setting to create the subscription with publishing already enabled.\nDefault: `False`.\n", - (bool? b) => this[OpcUaSubscriptionConfig.DisableDataSetMetaDataKey] = b?.ToString() ?? "True" }, + (bool? b) => this[OpcUaSubscriptionConfig.EnableImmediatePublishingKey] = b?.ToString() ?? "True" }, { $"msi|metadatasendinterval=|{OpcUaSubscriptionConfig.DefaultMetaDataUpdateTimeKey}=", "Default value in milliseconds for the metadata send interval which determines in which interval metadata is sent.\nEven when disabled, metadata is still sent when the metadata version changes unless `--mm=*Samples` is set in which case this setting is ignored. Only valid for network message encodings. \nDefault: `0` which means periodic sending of metadata is disabled.\n", (int i) => this[OpcUaSubscriptionConfig.DefaultMetaDataUpdateTimeKey] = TimeSpan.FromMilliseconds(i).ToString() }, { $"dm|disablemetadata:|{OpcUaSubscriptionConfig.DisableDataSetMetaDataKey}:", - "Disables sending any metadata when metadata version changes. This setting can be used to also override the messaging profile's default support for metadata sending. \nDefault: `False` if the messaging profile selected supports sending metadata, `True` otherwise.\n", + "Disables sending any metadata when metadata version changes. This setting can be used to also override the messaging profile's default support for metadata sending.\nIt is recommended to disable sending metadata when more than 100 nodes are part of a data set.\nDefault: `False` if the messaging profile selected supports sending metadata and `--strict` is set, `True` otherwise.\n", (bool? b) => this[OpcUaSubscriptionConfig.DisableDataSetMetaDataKey] = b?.ToString() ?? "True" }, { $"lc|legacycompatibility=|{LegacyCompatibility}=", "Run the publisher in legacy (2.5.x) compatibility mode.\nDefault: `False` (disabled).\n", @@ -316,13 +316,13 @@ public CommandLine(string[] args) "The interval in seconds after which the publisher re-applies the desired state of the subscription to a session.\nDefault: `never` (only on configuration change).\n", (int i) => this[OpcUaClientConfig.SubscriptionManagementIntervalKey] = TimeSpan.FromSeconds(i).ToString() }, { $"bnr|badnoderetrydelay=|{OpcUaClientConfig.BadMonitoredItemRetryDelayKey}=", - $"The delay in seconds after which nodes that were rejected by the server while added or updating a subscription or while publishing, are re-applied to a subscription.\nDefault: `{OpcUaClientConfig.BadMonitoredItemRetryDelayDefaultSec}` seconds.\n", + $"The delay in seconds after which nodes that were rejected by the server while added or updating a subscription or while publishing, are re-applied to a subscription.\nSet to 0 to disable retrying.\nDefault: `{OpcUaClientConfig.BadMonitoredItemRetryDelayDefaultSec}` seconds.\n", (int i) => this[OpcUaClientConfig.BadMonitoredItemRetryDelayKey] = TimeSpan.FromSeconds(i).ToString() }, { $"inr|invalidnoderetrydelay=|{OpcUaClientConfig.InvalidMonitoredItemRetryDelayKey}=", - $"The delay in seconds after which the publisher attempts to re-apply nodes that were incorrectly configured to a subscription.\nDefault: `{OpcUaClientConfig.InvalidMonitoredItemRetryDelayDefaultSec}` seconds.\n", + $"The delay in seconds after which the publisher attempts to re-apply nodes that were incorrectly configured to a subscription.\nSet to 0 to disable retrying.\nDefault: `{OpcUaClientConfig.InvalidMonitoredItemRetryDelayDefaultSec}` seconds.\n", (int i) => this[OpcUaClientConfig.InvalidMonitoredItemRetryDelayKey] = TimeSpan.FromSeconds(i).ToString() }, { $"ser|subscriptionerrorretrydelay=|{OpcUaClientConfig.SubscriptionErrorRetryDelayKey}=", - $"The delay in seconds between attempts to create a subscription in a session.\nDefault: `{OpcUaClientConfig.SubscriptionErrorRetryDelayDefaultSec}` seconds.\n", + $"The delay in seconds between attempts to create a subscription in a session.\nSet to 0 to disable retrying.\nDefault: `{OpcUaClientConfig.SubscriptionErrorRetryDelayDefaultSec}` seconds.\n", (int i) => this[OpcUaClientConfig.SubscriptionErrorRetryDelayKey] = TimeSpan.FromSeconds(i).ToString() }, { $"dcp|disablecomplextypepreloading:|{OpcUaClientConfig.DisableComplexTypePreloadingKey}:", "Complex types (structures, enumerations) a server exposes are preloaded from the server after the session is connected. In some cases this can cause problems either on the client or server itself. Use this setting to disable pre-loading support.\nNote that since the complex type system is used for meta data messages it will still be loaded at the time the subscription is created, therefore also disable meta data support if you want to ensure the complex types are never loaded for an endpoint.\nDefault: `false`.\n", diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/Configuration.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/Configuration.cs index b1c2a16801..74245128a6 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/Configuration.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/src/Runtime/Configuration.cs @@ -71,6 +71,8 @@ public static void AddPublisherServices(this ContainerBuilder builder) builder.RegisterType() .AsImplementedInterfaces(); + builder.RegisterType() + .AsImplementedInterfaces(); builder.RegisterType() .AsImplementedInterfaces(); builder.RegisterType() 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 319b141c34..e5ca008ac6 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 @@ -6,10 +6,10 @@ - + - + all runtime; build; native; contentfiles; analyzers @@ -53,6 +53,9 @@ Always + + Always + Always @@ -69,8 +72,4 @@ - - - - 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 0209d6904d..dfa1168cd3 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Fixtures/PublisherModule.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Fixtures/PublisherModule.cs @@ -323,6 +323,9 @@ public void ConfigureContainer(ContainerBuilder builder) } // Override client config builder.RegisterInstance(_config).AsImplementedInterfaces(); + // Override process control + builder.RegisterType() + .AsImplementedInterfaces().SingleInstance(); } /// @@ -462,6 +465,17 @@ private IContainer CreateIoTHubSdkClientContainer(IMessageSink messageSink = nul return builder.Build(); } + /// + /// Mock exiting + /// + internal sealed class ExitOverride : IProcessControl + { + public bool Shutdown(bool failFast) + { + return true; + } + } + /// /// Adapter for telemetry handler /// diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Mqtt/ReferenceServer/MqttPubSubIntegrationTests.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Mqtt/ReferenceServer/MqttPubSubIntegrationTests.cs index e3c3d96c00..ac2d04ffb5 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Mqtt/ReferenceServer/MqttPubSubIntegrationTests.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Mqtt/ReferenceServer/MqttPubSubIntegrationTests.cs @@ -44,7 +44,7 @@ public async Task CanSendDataItemToMqttBrokerTest() // Act var (metadata, messages) = await ProcessMessagesAndMetadataAsync( nameof(CanSendDataItemToMqttBrokerTest), "./Resources/DataItems.json", - messageType: "ua-data", arguments: new string[] { "--mm=PubSub", "--mdt={TelemetryTopic}/metadatamessage" }, + messageType: "ua-data", arguments: new string[] { "--mm=PubSub", "--mdt={TelemetryTopic}/metadatamessage", "--dm=False" }, version: MqttVersion.v311); // Assert @@ -125,7 +125,7 @@ public async Task CanEncodeWithoutReversibleEncodingTest(string publishedNodesFi // Arrange // Act var (metadata, result) = await ProcessMessagesAndMetadataAsync(nameof(CanEncodeWithoutReversibleEncodingTest), - publishedNodesFile, messageType: "ua-data", arguments: new[] { "--mm=PubSub", "--me=Json" }, + publishedNodesFile, messageType: "ua-data", arguments: new[] { "--mm=PubSub", "--me=Json", "--dm=false" }, version: MqttVersion.v5); Assert.Single(result); @@ -165,7 +165,7 @@ public async Task CanEncodeWithReversibleEncodingTest(string publishedNodesFile) // Act var (metadata, result) = await ProcessMessagesAndMetadataAsync(nameof(CanEncodeWithReversibleEncodingTest), publishedNodesFile, TimeSpan.FromMinutes(2), 4, messageType: "ua-data", - arguments: new[] { "--mm=PubSub", "--me=JsonReversible" }, + arguments: new[] { "--mm=PubSub", "--me=JsonReversible", "--dm=False" }, version: MqttVersion.v311); var messages = result @@ -294,7 +294,7 @@ public async Task CanSendPendingConditionsToMqttBrokerTest() // Act var (metadata, messages) = await ProcessMessagesAndMetadataAsync(nameof(CanSendPendingConditionsToMqttBrokerTest), "./Resources/PendingAlarms.json", BasicPubSubIntegrationTests.GetAlarmCondition, messageType: "ua-data", - arguments: new string[] { "--mm=PubSub" }, version: MqttVersion.v311); + arguments: new string[] { "--mm=PubSub", "--dm=False" }, version: MqttVersion.v311); // Assert var evt = Assert.Single(messages); diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Resources/SimpleEvents2.json b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Resources/SimpleEvents2.json new file mode 100644 index 0000000000..fe2894787b --- /dev/null +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Resources/SimpleEvents2.json @@ -0,0 +1,23 @@ +[ + { + "EndpointUrl": "{{EndpointUrl}}", + "UseSecurity": false, + "DataSetWriterGroup": "{{DataSetWriterGroup}}", + "OpcNodes": [ + { + "Id": "i=2253", + "DisplayName": "Alarm", + "EventFilter": { + "TypeDefinitionId": "i=10060" + } + }, + { + "Id": "i=2253", + "DisplayName": "CycleStarted", + "EventFilter": { + "TypeDefinitionId": "ns=16;i=235" + } + } + ] + } +] diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Runtime/PublisherControllerTests.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Runtime/PublisherControllerTests.cs new file mode 100644 index 0000000000..b3625969a5 --- /dev/null +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Runtime/PublisherControllerTests.cs @@ -0,0 +1,65 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See License.txt in the repo root for license information. +// ------------------------------------------------------------ + +namespace Azure.IIoT.OpcUa.Publisher.Module.Tests.Runtime +{ + using Azure.IIoT.OpcUa.Publisher.Module.Tests.Fixtures; + using Microsoft.AspNetCore.Hosting.Server; + using NuGet.Frameworks; + using System; + using System.Security.Cryptography.X509Certificates; + using System.Threading.Tasks; + using Xunit; + using Xunit.Abstractions; + + public class PublisherControllerTests : PublisherIntegrationTestBase + { + private readonly ITestOutputHelper _output; + + public PublisherControllerTests(ITestOutputHelper output) : base(output) + { + _output = output; + } + + [Fact] + public async Task GetApiKeyAndCertificateTest() + { + const string name = nameof(GetApiKeyAndCertificateTest); + StartPublisher(name, "./Resources/empty_pn.json", arguments: new string[] { "--mm=PubSub" }); + try + { + var apiKey = await PublisherApi.GetApiKeyAsync(); + Assert.NotNull(apiKey); + Assert.NotNull(Convert.FromBase64String(apiKey)); + + var certificate = await PublisherApi.GetServerCertificateAsync(); + Assert.NotNull(certificate); + var x509 = X509Certificate2.CreateFromPem(certificate); + Assert.StartsWith("DC=", x509.Subject); + } + finally + { + StopPublisher(); + } + } + + [Fact] + public async Task ShutdownTest() + { + const string name = nameof(ShutdownTest); + StartPublisher(name, "./Resources/empty_pn.json"); + try + { + // We mocked this call + await PublisherApi.ShutdownAsync(); + await PublisherApi.ShutdownAsync(true); + } + finally + { + StopPublisher(); + } + } + } +} diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Sdk/ReferenceServer/AdancedPubSubIntegrationTests.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Sdk/ReferenceServer/AdvancedPubSubIntegrationTests.cs similarity index 98% rename from src/Azure.IIoT.OpcUa.Publisher.Module/tests/Sdk/ReferenceServer/AdancedPubSubIntegrationTests.cs rename to src/Azure.IIoT.OpcUa.Publisher.Module/tests/Sdk/ReferenceServer/AdvancedPubSubIntegrationTests.cs index da95fb242d..859b41ea7a 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Sdk/ReferenceServer/AdancedPubSubIntegrationTests.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Sdk/ReferenceServer/AdvancedPubSubIntegrationTests.cs @@ -30,7 +30,7 @@ public async Task SwitchServerWithSameWriterGroupTest() EndpointUrl = server.EndpointUrl; const string name = nameof(SwitchServerWithSameWriterGroupTest); - StartPublisher(name, "./Resources/DataItems.json", arguments: new string[] { "--mm=PubSub" }); + StartPublisher(name, "./Resources/DataItems.json", arguments: new string[] { "--mm=PubSub", "--dm=false" }); try { // Arrange @@ -94,7 +94,7 @@ public async Task SwitchServerWithDifferentWriterGroupTest() var server = new ReferenceServer(); EndpointUrl = server.EndpointUrl; const string name = nameof(SwitchServerWithDifferentWriterGroupTest); - StartPublisher(name, "./Resources/DataItems2.json", arguments: new string[] { "--mm=PubSub" }); + StartPublisher(name, "./Resources/DataItems2.json", arguments: new string[] { "--mm=PubSub", "--dm=false" }); try { // Arrange @@ -169,7 +169,7 @@ public async Task AddNodeToDataSetWriterGroupWithNodeUsingDeviceMethod(bool diff // Set both to the same so that there is a single writer instead of 2 testInput2[0].OpcNodes[0].OpcPublishingInterval = testInput1[0].OpcNodes[0].OpcPublishingInterval; } - StartPublisher(name, arguments: new string[] { "--mm=PubSub" }); + StartPublisher(name, arguments: new string[] { "--mm=PubSub", "--dm=false" }); try { var endpoints = await PublisherApi.GetConfiguredEndpointsAsync(); @@ -234,7 +234,7 @@ public async Task SwitchServerWithDifferentDataTest() var server = new ReferenceServer(); EndpointUrl = server.EndpointUrl; const string name = nameof(SwitchServerWithDifferentDataTest); - StartPublisher(name, "./Resources/DataItems.json", arguments: new string[] { "--mm=PubSub" }); + StartPublisher(name, "./Resources/DataItems.json", arguments: new string[] { "--mm=PubSub", "--dm=false" }); try { // Arrange @@ -302,7 +302,7 @@ public async Task RestartConfigurationTest() for (var cycles = 0; cycles < 5; cycles++) { const string name = nameof(RestartConfigurationTest); - StartPublisher(name, "./Resources/DataItems.json", arguments: new string[] { "--mm=PubSub" }); + StartPublisher(name, "./Resources/DataItems.json", arguments: new string[] { "--mm=PubSub", "--dm=false" }); try { // Arrange diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Sdk/ReferenceServer/BasicPubSubIntegrationTests.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Sdk/ReferenceServer/BasicPubSubIntegrationTests.cs index fc8f387123..b1edd07d61 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Sdk/ReferenceServer/BasicPubSubIntegrationTests.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Sdk/ReferenceServer/BasicPubSubIntegrationTests.cs @@ -7,7 +7,9 @@ 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 FluentAssertions; using System; + using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading.Tasks; @@ -47,7 +49,7 @@ public async Task CanSendDataItemToIoTHubTest() // Act var (metadata, messages) = await ProcessMessagesAndMetadataAsync( nameof(CanSendDataItemToIoTHubTest), "./Resources/DataItems.json", - messageType: "ua-data", arguments: new string[] { "--mm=PubSub" }); + messageType: "ua-data", arguments: new string[] { "--mm=PubSub", "--dm=false" }); // Assert var message = Assert.Single(messages).Message; @@ -122,7 +124,7 @@ public async Task CanEncodeWithoutReversibleEncodingTest(string publishedNodesFi var (metadata, result) = await ProcessMessagesAndMetadataAsync( nameof(CanEncodeWithoutReversibleEncodingTest), publishedNodesFile, messageType: "ua-data", - arguments: new[] { "--mm=PubSub", "--me=Json" } + arguments: new[] { "--mm=PubSub", "--me=Json", "--dm=false" } ); Assert.Single(result); @@ -162,7 +164,7 @@ public async Task CanEncodeWithReversibleEncodingTest(string publishedNodesFile) var (metadata, result) = await ProcessMessagesAndMetadataAsync( nameof(CanEncodeWithReversibleEncodingTest), publishedNodesFile, TimeSpan.FromMinutes(2), 4, messageType: "ua-data", - arguments: new[] { "--mm=PubSub", "--me=JsonReversible" } + arguments: new[] { "--mm=PubSub", "--me=JsonReversible", "--dm=false" } ); var messages = result @@ -282,6 +284,66 @@ public async Task CanEncodeWithReversibleEncodingAndWithCompliantEncodingTestTes AssertCompliantSimpleEventsMetadata(metadata); } + [Theory] + [InlineData("./Resources/SimpleEvents2.json")] + public async Task CanEncode2EventsWithCompliantEncodingTestTest(string publishedNodesFile) + { + var dataSetWriterNames = new HashSet(); + + // Arrange + // Act + var (metadata, result) = await ProcessMessagesAndMetadataAsync( + nameof(CanEncode2EventsWithCompliantEncodingTestTest), + publishedNodesFile, GetBothEvents, messageType: "ua-data", + arguments: new[] { "-c", "--mm=PubSub", "--me=Json" }); + + Assert.Single(result); + + var messages = result + .SelectMany(x => x.Message.GetProperty("Messages").EnumerateArray()) + .ToArray(); + + dataSetWriterNames.Select(d => d.Split('|')[1]) + .Should().Contain(new[] { "CycleStarted", "Alarm" }); + + // Assert + Assert.NotEmpty(messages); + Assert.All(messages, m => + { + var value = m.GetProperty("Payload"); + + // Variant encoding is the default + var eventId = value.GetProperty(kEventId).GetProperty("Value"); + var message = value.GetProperty(kMessage).GetProperty("Value"); + var cycleId = value.GetProperty(kCycleId).GetProperty("Value"); + var currentStep = value.GetProperty(kCurrentStep).GetProperty("Value"); + + Assert.Equal(JsonValueKind.String, eventId.ValueKind); + Assert.Equal(JsonValueKind.String, message.ValueKind); + Assert.Equal(JsonValueKind.String, cycleId.ValueKind); + Assert.Equal(JsonValueKind.String, currentStep.GetProperty("Name").ValueKind); + Assert.Equal(JsonValueKind.Number, currentStep.GetProperty("Duration").ValueKind); + }); + + JsonElement GetBothEvents(JsonElement jsonElement) + { + var messages = jsonElement.GetProperty("Messages"); + if (messages.ValueKind != JsonValueKind.Array) + { + return default; + } + foreach (var element in messages.EnumerateArray()) + { + var dataSetWriterName = element.GetProperty("DataSetWriterName").GetString(); + if (dataSetWriterName != null) + { + dataSetWriterNames.Add(dataSetWriterName); + } + } + return dataSetWriterNames.Count == 2 ? jsonElement : default; + } + } + [Fact] public async Task CanSendPendingConditionsToIoTHubTest() { @@ -289,7 +351,7 @@ public async Task CanSendPendingConditionsToIoTHubTest() // Act var (metadata, messages) = await ProcessMessagesAndMetadataAsync( nameof(CanSendPendingConditionsToIoTHubTest), "./Resources/PendingAlarms.json", GetAlarmCondition, - messageType: "ua-data", arguments: new string[] { "--mm=PubSub" }); + messageType: "ua-data", arguments: new string[] { "--mm=PubSub", "--dm=False" }); // Assert _output.WriteLine(messages.ToString()); @@ -308,7 +370,7 @@ public async Task CanSendKeyFramesWithExtensionFieldsToIoTHubTest() // Act var (metadata, messages) = await ProcessMessagesAndMetadataAsync( nameof(CanSendDataItemToIoTHubTest), "./Resources/KeyFrames.json", - messageType: "ua-data", arguments: new string[] { "--mm=FullNetworkMessages" }); + messageType: "ua-data", arguments: new string[] { "--mm=FullNetworkMessages", "--dm=false" }); // Assert var message = Assert.Single(messages).Message; @@ -375,7 +437,7 @@ public async Task CanSendKeyFramesToIoTHubTest() // Act var (metadata, messages) = await ProcessMessagesAndMetadataAsync( nameof(CanSendDataItemToIoTHubTest), "./Resources/KeyFrames.json", TimeSpan.FromMinutes(2), 11, - messageType: "ua-data"); + messageType: "ua-data", arguments: new[] { "--dm=false" }); // Assert var allDataSetMessages = messages.Select(m => m.Message.GetProperty("Messages")).SelectMany(m => m.EnumerateArray()); diff --git a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Sdk/ReferenceServer/ReverseConnectIntegrationTests.cs b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Sdk/ReferenceServer/ReverseConnectIntegrationTests.cs index a35ec8ba7f..36fc965feb 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Sdk/ReferenceServer/ReverseConnectIntegrationTests.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Module/tests/Sdk/ReferenceServer/ReverseConnectIntegrationTests.cs @@ -33,7 +33,7 @@ public async Task RegisteredReadTestAsync(bool useReverseConnect) EndpointUrl = server.EndpointUrl; var name = nameof(RegisteredReadTestAsync) + (useReverseConnect ? "WithReverseConnect" : "NoReverseConnect"); - StartPublisher(name, "./Resources/RegisteredRead.json", arguments: new string[] { "--mm=PubSub" }, + StartPublisher(name, "./Resources/RegisteredRead.json", arguments: new string[] { "--mm=PubSub", "--dm=false" }, reverseConnectPort: useReverseConnect ? server.ReverseConnectPort : null); try { 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 cbc67fb856..7e11700846 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 @@ -18,8 +18,4 @@ - - - - diff --git a/src/Azure.IIoT.OpcUa.Publisher.Sdk/src/Clients/PublisherApiClient.cs b/src/Azure.IIoT.OpcUa.Publisher.Sdk/src/Clients/PublisherApiClient.cs index 2760cb710e..b1186acd64 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Sdk/src/Clients/PublisherApiClient.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Sdk/src/Clients/PublisherApiClient.cs @@ -259,6 +259,30 @@ public async Task> GetDiagnosticInfoAsync(Cance return _serializer.DeserializeResponse>(response); } + /// + public async Task ShutdownAsync(bool failFast, CancellationToken ct) + { + await _methodClient.CallMethodAsync(_target, "Shutdown", + _serializer.SerializeToMemory(failFast), + ContentMimeType.Json, _timeout, ct).ConfigureAwait(false); + } + + /// + public async Task GetServerCertificateAsync(CancellationToken ct) + { + var response = await _methodClient.CallMethodAsync(_target, + "GetServerCertificate", null, ContentMimeType.Json, _timeout, ct).ConfigureAwait(false); + return _serializer.DeserializeResponse(response); + } + + /// + public async Task GetApiKeyAsync(CancellationToken ct) + { + var response = await _methodClient.CallMethodAsync(_target, + "GetApiKey", null, ContentMimeType.Json, _timeout, ct).ConfigureAwait(false); + return _serializer.DeserializeResponse(response); + } + private readonly IJsonSerializer _serializer; private readonly IMethodClient _methodClient; private readonly string _target; diff --git a/src/Azure.IIoT.OpcUa.Publisher.Sdk/src/IPublisherApi.cs b/src/Azure.IIoT.OpcUa.Publisher.Sdk/src/IPublisherApi.cs index 2f5c6e306b..666bde5a8d 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Sdk/src/IPublisherApi.cs +++ b/src/Azure.IIoT.OpcUa.Publisher.Sdk/src/IPublisherApi.cs @@ -126,5 +126,28 @@ Task PublishListAsync(ConnectionModel connection /// Task> GetDiagnosticInfoAsync( CancellationToken ct = default); + + /// + /// Shutdown publisher + /// + /// + /// + /// + Task ShutdownAsync(bool failFast = false, + CancellationToken ct = default); + + /// + /// Get server certificate as PEM string + /// + /// + /// + Task GetServerCertificateAsync(CancellationToken ct = default); + + /// + /// Get api key as string + /// + /// + /// + Task GetApiKeyAsync(CancellationToken ct = default); } } 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 f8744c2a06..fc8af19945 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 @@ -20,8 +20,4 @@ - - - - \ No newline at end of file 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 b3abbd7921..f1af236011 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 @@ -20,8 +20,4 @@ - - - - \ No newline at end of file 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 538e1e890f..258be9cc12 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 @@ -39,8 +39,4 @@ - - - - diff --git a/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/tests/Azure.IIoT.OpcUa.Publisher.Service.WebApi.Tests.csproj b/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/tests/Azure.IIoT.OpcUa.Publisher.Service.WebApi.Tests.csproj index 50bce6213b..eaabb38a7f 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/tests/Azure.IIoT.OpcUa.Publisher.Service.WebApi.Tests.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Service.WebApi/tests/Azure.IIoT.OpcUa.Publisher.Service.WebApi.Tests.csproj @@ -4,10 +4,10 @@ - + - + all runtime; build; native; contentfiles; analyzers @@ -31,8 +31,4 @@ - - - - \ No newline at end of file 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 5477dcb418..ba389463e8 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 @@ -16,8 +16,4 @@ - - - - diff --git a/src/Azure.IIoT.OpcUa.Publisher.Service/tests/Azure.IIoT.OpcUa.Publisher.Service.Tests.csproj b/src/Azure.IIoT.OpcUa.Publisher.Service/tests/Azure.IIoT.OpcUa.Publisher.Service.Tests.csproj index c4c8c157ba..1768e4ed8d 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Service/tests/Azure.IIoT.OpcUa.Publisher.Service.Tests.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Service/tests/Azure.IIoT.OpcUa.Publisher.Service.Tests.csproj @@ -6,13 +6,13 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers @@ -24,8 +24,4 @@ - - - - \ No newline at end of file diff --git a/src/Azure.IIoT.OpcUa.Publisher.Testing/cli/Azure.IIoT.OpcUa.Publisher.Testing.Cli.csproj b/src/Azure.IIoT.OpcUa.Publisher.Testing/cli/Azure.IIoT.OpcUa.Publisher.Testing.Cli.csproj index 396c2166ad..1367a7115d 100644 --- a/src/Azure.IIoT.OpcUa.Publisher.Testing/cli/Azure.IIoT.OpcUa.Publisher.Testing.Cli.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher.Testing/cli/Azure.IIoT.OpcUa.Publisher.Testing.Cli.csproj @@ -13,8 +13,4 @@ - - - - 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 73f6902f1b..2303a927a9 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 @@ -61,8 +61,4 @@ - - - - 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 675c3af969..eeb4c66468 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 @@ -12,7 +12,7 @@ - + @@ -20,8 +20,4 @@ - - - - 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 9270f2b6cb..2d334a00af 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 @@ -18,8 +18,4 @@ - - - - diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Extensions/PublishedDataSetSourceModelEx.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Extensions/PublishedDataSetSourceModelEx.cs index a07e49584f..6670e4cd9e 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Extensions/PublishedDataSetSourceModelEx.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Extensions/PublishedDataSetSourceModelEx.cs @@ -130,22 +130,18 @@ internal static IEnumerable ToMonitoredItems( internal static IEnumerable ToMonitoredItems( this PublishedEventItemsModel eventItems, OpcUaSubscriptionOptions options) { - if (eventItems?.PublishedData == null) + if (eventItems?.PublishedData != null) { - return Enumerable.Empty(); - } - - var map = new Dictionary(); - foreach (var publishedData in eventItems.PublishedData) - { - var monitoredItem = publishedData?.ToMonitoredItemTemplate(options); - if (monitoredItem == null) + foreach (var publishedData in eventItems.PublishedData) { - continue; + var monitoredItem = publishedData?.ToMonitoredItemTemplate(options); + if (monitoredItem == null) + { + continue; + } + yield return monitoredItem; } - map.AddOrUpdate(monitoredItem.Id ?? Guid.NewGuid().ToString(), monitoredItem); } - return map.Values; } /// diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/IApiKeyProvider.cs b/src/Azure.IIoT.OpcUa.Publisher/src/IApiKeyProvider.cs index 437c143752..6327e418eb 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/IApiKeyProvider.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/IApiKeyProvider.cs @@ -5,6 +5,7 @@ namespace Azure.IIoT.OpcUa.Publisher { + /// /// Provide api key /// diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/IProcessControl.cs b/src/Azure.IIoT.OpcUa.Publisher/src/IProcessControl.cs new file mode 100644 index 0000000000..1ee3cd7d2b --- /dev/null +++ b/src/Azure.IIoT.OpcUa.Publisher/src/IProcessControl.cs @@ -0,0 +1,20 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See License.txt in the repo root for license information. +// ------------------------------------------------------------ + +namespace Azure.IIoT.OpcUa.Publisher +{ + /// + /// Process control provider + /// + public interface IProcessControl + { + /// + /// Shutdown publisher + /// + /// + /// false if shutdown failed + bool Shutdown(bool failFast); + } +} diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Services/NetworkMessageEncoder.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Services/NetworkMessageEncoder.cs index 5a82fe2686..27f6079360 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Services/NetworkMessageEncoder.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Services/NetworkMessageEncoder.cs @@ -260,8 +260,7 @@ public void Dispose() ? new JsonDataSetMessage { UseCompatibilityMode = !standardsCompliant, - DataSetWriterName = Context.Writer.DataSetWriterName - ?? Constants.DefaultDataSetWriterName + DataSetWriterName = GetDataSetWriterName(Notification, Context) } : new UadpDataSetMessage(); @@ -305,8 +304,7 @@ public void Dispose() ? new JsonDataSetMessage { UseCompatibilityMode = !standardsCompliant, - DataSetWriterName = Context.Writer.DataSetWriterName - ?? Constants.DefaultDataSetWriterName + DataSetWriterName = GetDataSetWriterName(Notification, Context) } : new UadpDataSetMessage(); @@ -430,7 +428,7 @@ void AddMessage(BaseDataSetMessage dataSetMessage) DataSetWriterId = Notification.SubscriptionId, MetaData = Notification.MetaData, MessageId = Guid.NewGuid().ToString(), - DataSetWriterName = Context.Writer.DataSetWriterName ?? Constants.DefaultDataSetWriterName + DataSetWriterName = GetDataSetWriterName(Notification, Context) } : new UadpMetaDataMessage { DataSetWriterId = Notification.SubscriptionId, @@ -484,6 +482,18 @@ BaseNetworkMessage CreateMessage(WriterGroupModel writerGroup, MessageEncoding e return currentMessage; } + static string GetDataSetWriterName(IOpcUaSubscriptionNotification Notification, + WriterGroupMessageContext Context) + { + var dataSetWriterName = Context.Writer.DataSetWriterName ?? Constants.DefaultDataSetWriterName; + var dataSetName = Notification.DataSetName; + if (dataSetName != null) + { + return dataSetWriterName + "|" + dataSetName; + } + return dataSetWriterName; + } + DateTime? GetTimestamp(IOpcUaSubscriptionNotification Notification) { switch (_options.Value.MessageTimestamp) diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Services/PublisherModule.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Services/PublisherModule.cs index b2c7269786..6283a809a6 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Services/PublisherModule.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Services/PublisherModule.cs @@ -20,7 +20,7 @@ namespace Azure.IIoT.OpcUa.Publisher.Services /// /// Publisher module hosted service /// - public class PublisherModule : IHostedService, IIoTEdgeClientState + public class PublisherModule : IHostedService, IIoTEdgeClientState, IProcessControl { /// /// Running in container @@ -133,6 +133,21 @@ public Task StopAsync(CancellationToken cancellationToken) return Task.CompletedTask; } + /// + public bool Shutdown(bool failFast) + { + _logger.LogInformation("Received request to shutdown publisher process."); + if (failFast) + { + Environment.FailFast("Shutdown was invoked remotely."); + } + else + { + Environment.Exit(0); + } + return false; + } + private readonly TaskCompletionSource _exit; private readonly ILifetimeScope _scope; private readonly ILogger _logger; diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Services/WriterGroupDataSource.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Services/WriterGroupDataSource.cs index 13e6373ab6..fd90e4f994 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Services/WriterGroupDataSource.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Services/WriterGroupDataSource.cs @@ -722,6 +722,9 @@ public sealed record class MetadataNotificationModel : /// public ushort SubscriptionId { get; } + /// + public string? DataSetName { get; } + /// public string? EndpointUrl { get; } @@ -750,6 +753,7 @@ public sealed record class MetadataNotificationModel : public MetadataNotificationModel(IOpcUaSubscriptionNotification notification) { SequenceNumber = notification.SequenceNumber; + DataSetName = notification.DataSetName; ServiceMessageContext = notification.ServiceMessageContext; MetaData = notification.MetaData; PublishTimestamp = notification.PublishTimestamp; diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaMonitoredItem.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaMonitoredItem.cs index 0eec3ced5e..95ca7935aa 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaMonitoredItem.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaMonitoredItem.cs @@ -45,6 +45,11 @@ public interface IOpcUaMonitoredItem : IDisposable /// MonitoredItem? Item { get; } + /// + /// Data set name + /// + string? DataSetName { get; } + /// /// Resolve relative path first. If this returns null /// the relative path either does not exist or we let @@ -100,7 +105,7 @@ bool MergeWith(IOpcUaMonitoredItem item, IOpcUaSession session, /// /// bool TryCompleteChanges(Subscription subscription, - ref bool applyChanges, Action> cb); /// diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaSubscriptionNotification.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaSubscriptionNotification.cs index 6096c781f6..750d8b9625 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaSubscriptionNotification.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/IOpcUaSubscriptionNotification.cs @@ -41,6 +41,11 @@ public interface IOpcUaSubscriptionNotification : IDisposable /// ushort SubscriptionId { get; } + /// + /// Name of the data set + /// + string? DataSetName { get; } + /// /// Endpoint url /// diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Models/MonitoredItemNotificationModel.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Models/MonitoredItemNotificationModel.cs index f69fa7dfe4..c947bb4389 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Models/MonitoredItemNotificationModel.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Models/MonitoredItemNotificationModel.cs @@ -29,7 +29,6 @@ public sealed record class MonitoredItemNotificationModel /// /// Display name of the data set this item is part of. - /// Applicable only for events in sample mode at this point. /// public string? DataSetName { get; internal set; } diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Models/SubscriptionNotificationModel.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Models/SubscriptionNotificationModel.cs index 918123f76a..72ac6fa26b 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Models/SubscriptionNotificationModel.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Models/SubscriptionNotificationModel.cs @@ -28,6 +28,9 @@ public sealed record class SubscriptionNotificationModel : /// public string? SubscriptionName { get; set; } + /// + public string? DataSetName { get; set; } + /// public ushort SubscriptionId { get; set; } diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Runtime/OpcUaClientConfig.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Runtime/OpcUaClientConfig.cs index b401709891..2065fafe51 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Runtime/OpcUaClientConfig.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Runtime/OpcUaClientConfig.cs @@ -202,7 +202,7 @@ public override void PostConfigure(string? name, OpcUaClientOptions options) if (options.SubscriptionErrorRetryDelay == null) { var retryTimeout = GetIntOrDefault(SubscriptionErrorRetryDelayKey); - if (retryTimeout > 0) + if (retryTimeout >= 0) { options.SubscriptionErrorRetryDelay = TimeSpan.FromSeconds(retryTimeout); } @@ -211,7 +211,7 @@ public override void PostConfigure(string? name, OpcUaClientOptions options) if (options.BadMonitoredItemRetryDelay == null) { var retryTimeout = GetIntOrDefault(BadMonitoredItemRetryDelayKey); - if (retryTimeout > 0) + if (retryTimeout >= 0) { options.BadMonitoredItemRetryDelay = TimeSpan.FromSeconds(retryTimeout); } @@ -220,7 +220,7 @@ public override void PostConfigure(string? name, OpcUaClientOptions options) if (options.InvalidMonitoredItemRetryDelay == null) { var retryTimeout = GetIntOrDefault(InvalidMonitoredItemRetryDelayKey); - if (retryTimeout > 0) + if (retryTimeout >= 0) { options.InvalidMonitoredItemRetryDelay = TimeSpan.FromSeconds(retryTimeout); } diff --git a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Runtime/OpcUaSubscriptionConfig.cs b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Runtime/OpcUaSubscriptionConfig.cs index ce9439385b..fe09560d30 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Runtime/OpcUaSubscriptionConfig.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Runtime/OpcUaSubscriptionConfig.cs @@ -8,6 +8,7 @@ namespace Azure.IIoT.OpcUa.Publisher.Stack.Runtime using Azure.IIoT.OpcUa.Publisher.Models; using Furly.Extensions.Configuration; using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.Options; using System; /// @@ -117,7 +118,9 @@ public override void PostConfigure(string? name, OpcUaSubscriptionOptions option } if (options.DisableDataSetMetaData == null) { - options.DisableDataSetMetaData = GetBoolOrDefault(DisableDataSetMetaDataKey); + // Set a default from the strict setting + options.DisableDataSetMetaData = GetBoolOrDefault(DisableDataSetMetaDataKey, + !(_options.Value.UseStandardsCompliantEncoding ?? false)); } if (options.AsyncMetaDataLoadThreshold == null) { @@ -172,8 +175,12 @@ public override void PostConfigure(string? name, OpcUaSubscriptionOptions option /// Create configurator /// /// - public OpcUaSubscriptionConfig(IConfiguration configuration) : base(configuration) + /// + public OpcUaSubscriptionConfig(IConfiguration configuration, + IOptions options) : base(configuration) { + _options = options; } + private readonly IOptions _options; } } 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 0f641caa8d..8cccd2753a 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaMonitoredItem.cs @@ -35,6 +35,9 @@ internal abstract class OpcUaMonitoredItem : IOpcUaMonitoredItem /// public MonitoredItem? Item { get; protected internal set; } + /// + public virtual string? DataSetName { get; } + /// public bool AttachedToSubscription { get; protected internal set; } @@ -209,7 +212,7 @@ public virtual bool RemoveFrom(Subscription subscription, /// public virtual bool TryCompleteChanges(Subscription subscription, ref bool applyChanges, - Action> cb) + Action> cb) { if (Item == null) { @@ -642,7 +645,7 @@ public override bool RemoveFrom(Subscription subscription, out bool metadataChan /// public override bool TryCompleteChanges(Subscription subscription, ref bool applyChanges, - Action> cb) + Action> cb) { return true; } @@ -1212,7 +1215,7 @@ public override bool MergeWith(IOpcUaMonitoredItem item, IOpcUaSession session, /// public override bool TryCompleteChanges(Subscription subscription, ref bool applyChanges, - Action> cb) + Action> cb) { var result = base.TryCompleteChanges(subscription, ref applyChanges, cb); @@ -1314,14 +1317,14 @@ private void SendHeartbeatNotifications() Flags = MonitoredItemSourceFlags.Heartbeat, SequenceNumber = 0 }; - callback(MessageType.DeltaFrame, heartbeat.YieldReturn()); + callback(MessageType.DeltaFrame, null, heartbeat.YieldReturn()); } private readonly Timer _heartbeatTimer; private TimeSpan _timerInterval; private HeartbeatBehavior _heartbeatBehavior; private TimeSpan _heartbeatInterval; - private Action>? _callback; + private Action>? _callback; private DateTime? _lastValueReceived; } @@ -1442,7 +1445,7 @@ public override bool MergeWith(IOpcUaMonitoredItem item, IOpcUaSession session, /// public override bool TryCompleteChanges(Subscription subscription, ref bool applyChanges, - Action> cb) + Action> cb) { // Dont call base implementation as it is not what we want. if (Item == null) @@ -1504,7 +1507,7 @@ private void OnSampledDataValueReceived(uint sequenceNumber, DataValue value) Flags = MonitoredItemSourceFlags.CyclicRead, Value = value }; - callback(MessageType.DeltaFrame, notification.YieldReturn()); + callback(MessageType.DeltaFrame, null, notification.YieldReturn()); } /// @@ -1527,7 +1530,7 @@ internal DataValue LastSampledValue private readonly ConnectionIdentifier _connection; private readonly IClientSampler _sampler; - private Action>? _callback; + private Action>? _callback; private IAsyncDisposable? _sampling; } @@ -1554,6 +1557,9 @@ public override (string NodeId, string[] Path, UpdateNodeId Update)? Resolve (v, context) => NodeId = v.AsString(context, Template.NamespaceFormat) ?? string.Empty) : null; + /// + public override string? DataSetName => Template.DataSetFieldName; + /// /// Monitored item as event /// @@ -1587,6 +1593,11 @@ public override bool Equals(object? obj) { return false; } + if ((Template.DataSetFieldName ?? string.Empty) != + (eventItem.Template.DataSetFieldName ?? string.Empty)) + { + return false; + } if (!Template.RelativePath.SequenceEqualsSafe(eventItem.Template.RelativePath)) { return false; @@ -1609,6 +1620,9 @@ public override int GetHashCode() hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode( Template.DataSetFieldName ?? string.Empty); + hashCode = (hashCode * -1521134295) + + EqualityComparer.Default.GetHashCode( + Template.DataSetFieldId ?? string.Empty); hashCode = (hashCode * -1521134295) + EqualityComparer>.Default.GetHashCode( Template.RelativePath ?? Array.Empty()); @@ -1675,7 +1689,7 @@ await AddVariableFieldAsync(fields, dataTypes, session, typeSystem, variable, /// public override bool TryCompleteChanges(Subscription subscription, ref bool applyChanges, - Action> cb) + Action> cb) { if (!base.TryCompleteChanges(subscription, ref applyChanges, cb)) { @@ -2339,7 +2353,7 @@ public override bool MergeWith(IOpcUaMonitoredItem item, IOpcUaSession session, /// public override bool TryCompleteChanges(Subscription subscription, ref bool applyChanges, - Action> cb) + Action> cb) { var result = base.TryCompleteChanges(subscription, ref applyChanges, cb); if (!AttachedToSubscription || !result) @@ -2487,7 +2501,7 @@ private void SendPendingConditions() foreach (var conditionNotification in notifications) { - callback(MessageType.Condition, conditionNotification); + callback(MessageType.Condition, DataSetName, conditionNotification); } } @@ -2515,7 +2529,7 @@ private sealed class ConditionHandlingState = new Dictionary>(); } - private Action>? _callback; + private Action>? _callback; private ConditionHandlingState _conditionHandlingState; private DateTime _lastSentPendingConditions = DateTime.UtcNow; private int _snapshotInterval; 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 657e8b6820..b56bd9885a 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaSubscription.cs +++ b/src/Azure.IIoT.OpcUa.Publisher/src/Stack/Services/OpcUaSubscription.cs @@ -325,8 +325,9 @@ public void Dispose() /// Send notification /// /// + /// /// - internal void SendNotification(MessageType messageType, + internal void SendNotification(MessageType messageType, string? dataSetName, IEnumerable notifications) { var subscription = _currentSubscription; @@ -342,7 +343,7 @@ internal void SendNotification(MessageType messageType, { return; } - var message = CreateMessage(notifications, messageType, subscription); + var message = CreateMessage(notifications, messageType, dataSetName, subscription); onSubscriptionEventChange.Invoke(this, message); if (message.Notifications.Count > 0 && onSubscriptionEventDiagnosticsChange != null) { @@ -357,7 +358,7 @@ internal void SendNotification(MessageType messageType, { return; } - var message = CreateMessage(notifications, messageType, subscription); + var message = CreateMessage(notifications, messageType, dataSetName, subscription); onSubscriptionDataChange.Invoke(this, message); if (message.Notifications.Count > 0 && onSubscriptionDataDiagnosticsChange != null) { @@ -367,7 +368,7 @@ internal void SendNotification(MessageType messageType, } Notification CreateMessage(IEnumerable notifications, - MessageType messageType, Subscription subscription) + MessageType messageType, string? dataSetName, Subscription subscription) { return new Notification(this, subscription.Id, notifications) { @@ -375,6 +376,7 @@ Notification CreateMessage(IEnumerable notificat ApplicationUri = subscription.Session?.Endpoint?.Server?.ApplicationUri, EndpointUrl = subscription.Session?.Endpoint?.EndpointUrl, SubscriptionName = Name, + DataSetName = dataSetName, SubscriptionId = Id, SequenceNumber = SequenceNumber.Increment32(ref _sequenceNumber), MessageType = messageType @@ -1239,10 +1241,14 @@ private void GetSubscriptionConfiguration(Subscription defaultSubscription, private void TriggerSubscriptionManagementCallbackIn(TimeSpan? delay, TimeSpan defaultDelay = default) { - if (delay == null || delay == TimeSpan.Zero) + if (delay == null) { delay = defaultDelay; } + else if (delay == TimeSpan.Zero) + { + delay = Timeout.InfiniteTimeSpan; + } if (delay != Timeout.InfiniteTimeSpan) { _logger.LogDebug( @@ -1345,6 +1351,7 @@ private void OnSubscriptionEventNotificationList(Subscription subscription, ApplicationUri = subscription.Session?.Endpoint?.Server?.ApplicationUri, EndpointUrl = subscription.Session?.Endpoint?.EndpointUrl, SubscriptionName = Name, + DataSetName = wrapper.DataSetName, SubscriptionId = Id, SequenceNumber = SequenceNumber.Increment32(ref _sequenceNumber), MessageType = MessageType.Event, @@ -1606,6 +1613,9 @@ internal sealed record class Notification : IOpcUaSubscriptionNotification /// public string? SubscriptionName { get; internal set; } + /// + public string? DataSetName { get; internal set; } + /// public ushort SubscriptionId { get; internal set; } diff --git a/src/Azure.IIoT.OpcUa.Publisher/tests/Azure.IIoT.OpcUa.Publisher.Tests.csproj b/src/Azure.IIoT.OpcUa.Publisher/tests/Azure.IIoT.OpcUa.Publisher.Tests.csproj index cda2cf84dd..38f8936537 100644 --- a/src/Azure.IIoT.OpcUa.Publisher/tests/Azure.IIoT.OpcUa.Publisher.Tests.csproj +++ b/src/Azure.IIoT.OpcUa.Publisher/tests/Azure.IIoT.OpcUa.Publisher.Tests.csproj @@ -6,13 +6,13 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers @@ -65,9 +65,4 @@ Always - - - - - diff --git a/src/Azure.IIoT.OpcUa/src/Azure.IIoT.OpcUa.csproj b/src/Azure.IIoT.OpcUa/src/Azure.IIoT.OpcUa.csproj index 51ad2fcadc..3049b14eb3 100644 --- a/src/Azure.IIoT.OpcUa/src/Azure.IIoT.OpcUa.csproj +++ b/src/Azure.IIoT.OpcUa/src/Azure.IIoT.OpcUa.csproj @@ -15,8 +15,4 @@ - - - - \ No newline at end of file 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 f0579fb0e9..bf6f632c76 100644 --- a/src/Azure.IIoT.OpcUa/tests/Azure.IIoT.OpcUa.Tests.csproj +++ b/src/Azure.IIoT.OpcUa/tests/Azure.IIoT.OpcUa.Tests.csproj @@ -3,13 +3,13 @@ net7.0 - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers @@ -19,8 +19,4 @@ - - - -