mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-17 05:11:13 +08:00
d525bc3f28
# Description This PR fixes bounded ManagedSession reconnect cleanup and reorganizes the sample applications into domain-focused folders. ## Reconnect cleanup - Bounds reconnect integration cleanup with independent 30-second close and dispose limits. - Sends `CloseSession` through the awaited transport request path during `Session.DisposeAsync`. - Preserves explicit `Session.CloseAsync` behavior that waits for or cancels outstanding publish requests. - Covers the disposal request path with a regression test. ## Sample organization - `samples/OpenUsd`: `GeneratorServer`, `SiteCompositionServer` - `samples/Redundancy`: `RedundantClient`, `RedundantServer`, `RedundantPubSub` - `samples/Isa95`: `MinimalIsa95Server` - `samples/MinimalApi`: `MinimalBoilerServer`, `MinimalCalcServer`, `MinimalClient` - `samples/DI`: `PumpDeviceIntegrationServer` - `samples/Lds`: `ConsoleLdsServer` - `samples/Reference`: `ConsoleReferenceClient`, `ConsoleReferenceServer` - `samples/PubSub`: `ConsoleReferencePubSubClient` `UA.slnx` now exposes the same requested domain groups. Project references, tests, runtime sample probes, CI workflows, Dockerfiles, Compose files, helper scripts, and documentation were updated to the new paths. `samples/Robotics/MinimalRobotServer` remains under `Robotics`. ## Validation - `dotnet build UA.slnx` - 3 Redundancy short-haul sample tests - 70 DI sample tests - 914 OpenUSD tests - 1 WotCon model-materialization test - 137 AOT tests - `docker compose config` for all 6 moved Redundancy Compose files ## Related Issues - Fixes #4273 ## Checklist - [ ] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce new build or analyzer warnings. - [x] I built the complete `UA.slnx` solution locally. - [ ] I ran all tests locally against at least .NET Framework and .NET 10. - [ ] I fixed all failing and flaky tests in the CI pipelines and all CodeQL warnings. - [x] I have addressed all PR feedback received. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com> Co-authored-by: Marc Schier <mschier@microsoft.com> Co-authored-by: Marc Schier <marcschier@users.noreply.github.com> Copilot-Session: a2c2838f-74ae-4997-a1d4-50574d9e0636
196 lines
9.5 KiB
C#
196 lines
9.5 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
|
|
*
|
|
* OPC Foundation MIT License 1.00
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following
|
|
* conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* The complete license agreement can be found here:
|
|
* http://opcfoundation.org/License/MIT/1.00/
|
|
* ======================================================================*/
|
|
|
|
using System;
|
|
using Opc.Ua;
|
|
using Opc.Ua.PubSub.Configuration;
|
|
using Opc.Ua.PubSub.Eth;
|
|
using Opc.Ua.PubSub.Kafka;
|
|
|
|
namespace Quickstarts.ConsoleReferencePubSubClient
|
|
{
|
|
/// <summary>
|
|
/// Builds minimal Part 14 <see cref="PubSubConfigurationDataType"/>
|
|
/// payloads for the demo wire profiles using the fluent
|
|
/// <see cref="PubSubConfigurationBuilder"/>. The payloads use the
|
|
/// "Simple" DataSet exposed by <see cref="SampleDataSetSource"/>
|
|
/// (BoolToggle, Int32 counter, DateTime).
|
|
/// </summary>
|
|
public static class PublisherConfigurationBuilder
|
|
{
|
|
public const string DataSetName = "Simple";
|
|
public const string DefaultUdpEndpoint = "opc.udp://239.0.0.1:4840";
|
|
public const string DefaultEthEndpoint = "opc.eth://01-00-5E-7F-00-01";
|
|
public const string DefaultMqttEndpoint = "mqtt://localhost:1883";
|
|
public const string DefaultKafkaEndpoint = "kafka://localhost:9092";
|
|
private const string MqttQueueName = "Quickstarts/Reference/Simple";
|
|
private const string KafkaTopicName = "Quickstarts.Reference.Simple";
|
|
|
|
public static string DefaultEndpointFor(PublisherProfile profile)
|
|
{
|
|
return profile switch
|
|
{
|
|
PublisherProfile.UdpUadp => DefaultUdpEndpoint,
|
|
PublisherProfile.EthUadp => DefaultEthEndpoint,
|
|
PublisherProfile.KafkaUadp or PublisherProfile.KafkaJson => DefaultKafkaEndpoint,
|
|
PublisherProfile.MqttUadp or PublisherProfile.MqttJson => DefaultMqttEndpoint,
|
|
_ => throw new ArgumentOutOfRangeException(nameof(profile))
|
|
};
|
|
}
|
|
|
|
public static PubSubConfigurationDataType Build(
|
|
PublisherProfile profile,
|
|
string endpoint,
|
|
ushort publisherId,
|
|
ushort writerGroupId,
|
|
ushort dataSetWriterId,
|
|
int intervalMs)
|
|
{
|
|
// UDP and Ethernet are datagram transports (no broker queue);
|
|
// broker profiles use broker transport settings instead.
|
|
bool udp = profile is PublisherProfile.UdpUadp or PublisherProfile.EthUadp;
|
|
|
|
// UADP message security (SignAndEncrypt) is wired for the UADP
|
|
// profiles via the shared StaticSecurityKeyProvider. The JSON
|
|
// profile has no UADP security wrapper, so it stays unsecured.
|
|
bool secured = profile is not (PublisherProfile.MqttJson or PublisherProfile.KafkaJson);
|
|
string brokerQueueName = profile is PublisherProfile.KafkaUadp or PublisherProfile.KafkaJson
|
|
? KafkaTopicName
|
|
: MqttQueueName;
|
|
|
|
string transportProfileUri = profile switch
|
|
{
|
|
PublisherProfile.UdpUadp => Profiles.PubSubUdpUadpTransport,
|
|
PublisherProfile.EthUadp => EthProfiles.PubSubEthUadpTransport,
|
|
PublisherProfile.MqttUadp => Profiles.PubSubMqttUadpTransport,
|
|
PublisherProfile.MqttJson => Profiles.PubSubMqttJsonTransport,
|
|
PublisherProfile.KafkaUadp => KafkaProfiles.PubSubKafkaUadpTransport,
|
|
PublisherProfile.KafkaJson => KafkaProfiles.PubSubKafkaJsonTransport,
|
|
_ => throw new ArgumentOutOfRangeException(nameof(profile))
|
|
};
|
|
|
|
return PubSubConfigurationBuilder.Create()
|
|
.AddPublishedDataSet(DataSetName, ds => ds
|
|
.AddField("BoolToggle", (byte)DataTypes.Boolean, DataTypeIds.Boolean)
|
|
.AddField("Int32", (byte)DataTypes.Int32, DataTypeIds.Int32)
|
|
.AddField("DateTime", (byte)DataTypes.DateTime, DataTypeIds.DateTime))
|
|
.AddConnection("Publisher Connection", connection => connection
|
|
.WithPublisherId(new Variant(publisherId))
|
|
.WithTransportProfile(transportProfileUri)
|
|
.WithAddress(endpoint)
|
|
.AddWriterGroup("WriterGroup 1", group =>
|
|
{
|
|
group
|
|
.WithWriterGroupId(writerGroupId)
|
|
.WithPublishingInterval(intervalMs)
|
|
.WithMessageSettings(WriterGroupMessageSettings(profile))
|
|
.WithTransportSettings(udp
|
|
? new DatagramWriterGroupTransportDataType()
|
|
: new BrokerWriterGroupTransportDataType
|
|
{
|
|
QueueName = brokerQueueName
|
|
});
|
|
if (secured)
|
|
{
|
|
group.WithSecurity(
|
|
MessageSecurityMode.SignAndEncrypt,
|
|
SampleSecurity.SecurityGroupId,
|
|
SampleSecurity.SecurityKeyServiceUrl);
|
|
}
|
|
group.AddDataSetWriter("Writer 1", writer =>
|
|
{
|
|
writer
|
|
.WithDataSetWriterId(dataSetWriterId)
|
|
.WithDataSetName(DataSetName)
|
|
.WithKeyFrameCount(1)
|
|
.WithFieldContentMask(DataSetFieldContentMask.RawData)
|
|
.WithMessageSettings(WriterMessageSettings(profile));
|
|
if (!udp)
|
|
{
|
|
writer.WithTransportSettings(
|
|
new BrokerDataSetWriterTransportDataType
|
|
{
|
|
QueueName = brokerQueueName,
|
|
RequestedDeliveryGuarantee
|
|
= BrokerTransportQualityOfService.BestEffort
|
|
});
|
|
}
|
|
});
|
|
}))
|
|
.Build();
|
|
}
|
|
|
|
private static IEncodeable WriterGroupMessageSettings(PublisherProfile profile)
|
|
{
|
|
if (profile is PublisherProfile.MqttJson or PublisherProfile.KafkaJson)
|
|
{
|
|
return new JsonWriterGroupMessageDataType
|
|
{
|
|
NetworkMessageContentMask = (uint)(
|
|
JsonNetworkMessageContentMask.NetworkMessageHeader |
|
|
JsonNetworkMessageContentMask.DataSetMessageHeader |
|
|
JsonNetworkMessageContentMask.PublisherId)
|
|
};
|
|
}
|
|
return new UadpWriterGroupMessageDataType
|
|
{
|
|
DataSetOrdering = DataSetOrderingType.AscendingWriterId,
|
|
NetworkMessageContentMask = (uint)(
|
|
UadpNetworkMessageContentMask.PublisherId |
|
|
UadpNetworkMessageContentMask.GroupHeader |
|
|
UadpNetworkMessageContentMask.WriterGroupId |
|
|
UadpNetworkMessageContentMask.PayloadHeader |
|
|
UadpNetworkMessageContentMask.NetworkMessageNumber |
|
|
UadpNetworkMessageContentMask.SequenceNumber)
|
|
};
|
|
}
|
|
|
|
private static IEncodeable WriterMessageSettings(PublisherProfile profile)
|
|
{
|
|
if (profile is PublisherProfile.MqttJson or PublisherProfile.KafkaJson)
|
|
{
|
|
return new JsonDataSetWriterMessageDataType
|
|
{
|
|
DataSetMessageContentMask = (uint)(
|
|
JsonDataSetMessageContentMask.DataSetWriterId |
|
|
JsonDataSetMessageContentMask.SequenceNumber |
|
|
JsonDataSetMessageContentMask.Status |
|
|
JsonDataSetMessageContentMask.Timestamp)
|
|
};
|
|
}
|
|
return new UadpDataSetWriterMessageDataType
|
|
{
|
|
DataSetMessageContentMask = (uint)(
|
|
UadpDataSetMessageContentMask.Status |
|
|
UadpDataSetMessageContentMask.SequenceNumber)
|
|
};
|
|
}
|
|
}
|
|
}
|