Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
607d9a5
Scaffold Opc.Ua.PubSub.Kafka library (#3942)
marcschier Jul 3, 2026
7cfcb39
Implement Kafka PubSub transport, dual-source by TFM (#3942)
marcschier Jul 3, 2026
a87fa39
Add PubSub redundancy/HA coordination abstraction (#3942, #3924)
marcschier Jul 3, 2026
53cd72d
Add lease-based PubSub activation coordinator (#3942, #3924)
marcschier Jul 3, 2026
5b49219
Wire PubSub HA coordination into the DI builder (#3942, #3924)
marcschier Jul 3, 2026
4a3e1c0
Wire Kafka into the reference sample + docs; AOT-validated (#3942)
marcschier Jul 3, 2026
2e057fb
Add Opc.Ua.PubSub.Kafka transport tests + Testcontainers Docker test …
marcschier Jul 3, 2026
97a0ec4
Wire active/standby HA activation into the PubSub runtime + failover …
marcschier Jul 3, 2026
a069b0c
Merge branch 'master' into feature/3942-kafka-pubsub
marcschier Jul 3, 2026
553605e
Address PR #3949 Kafka review feedback
marcschier Jul 3, 2026
00426db
Fold Kafka transport doc into PubSub.md and document PubSub redundancy
marcschier Jul 3, 2026
695d7e0
Fix Kafka Docker integration test endpoint construction (CI)
marcschier Jul 4, 2026
09fb99e
Use a Kafka 4.0 broker for the Kafka Docker integration test (CI)
marcschier Jul 4, 2026
52d243a
Make Kafka Docker round-trip test resilient to consumer-assignment ra…
marcschier Jul 4, 2026
888c462
Fix Kafka Docker round-trip test: create topic before subscribing (CI)
marcschier Jul 5, 2026
d48f3ff
Merge remote-tracking branch 'origin/master' into feature/3942-kafka-…
marcschier Jul 6, 2026
1145467
Merge branch 'master' into feature/3942-kafka-pubsub
marcschier Jul 6, 2026
b9ff3d1
Update Dekaf to 1.0.0, consolidate Kafka package entries, bump packag…
marcschier Jul 6, 2026
1a16e7d
Merge remote-tracking branch 'origin/master' into feature/3942-kafka-…
marcschier Jul 6, 2026
6a942cc
Suppress Dekaf 1.0.0 assembly-level AOT rollup warning in the AOT tes…
marcschier Jul 6, 2026
cbc965f
Promote transcoded DataSet fields into Kafka record headers
marcschier Jul 6, 2026
b86e2ce
Revert SourceGenerator.Foundations 2.0.16 -> 2.0.14 (broken analyzer,…
marcschier Jul 6, 2026
8b2983b
Deterministic wait in PubSub failover tests (fix net48 flake, CI)
marcschier Jul 7, 2026
a32121a
Update JsonSchema.Net to 9.2.2 and migrate schema tests (review feedb…
marcschier Jul 7, 2026
cca32a3
Merge remote-tracking branch 'origin/master' into feature/3942-kafka-…
marcschier Jul 7, 2026
b8e3eed
Bump Dekaf to 1.2.0 (net10 client); keep the dual-source Kafka split
marcschier Jul 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<ProjectReference Include="..\..\Libraries\Opc.Ua.PubSub.Udp\Opc.Ua.PubSub.Udp.csproj" />
<ProjectReference Include="..\..\Libraries\Opc.Ua.PubSub.Eth\Opc.Ua.PubSub.Eth.csproj" />
<ProjectReference Include="..\..\Libraries\Opc.Ua.PubSub.Mqtt\Opc.Ua.PubSub.Mqtt.csproj" />
<ProjectReference Include="..\..\Libraries\Opc.Ua.PubSub.Kafka\Opc.Ua.PubSub.Kafka.csproj" />
<ProjectReference Include="..\..\Libraries\Opc.Ua.PubSub.Adapter\Opc.Ua.PubSub.Adapter.csproj" />
</ItemGroup>
<ItemGroup>
Expand Down
109 changes: 84 additions & 25 deletions Applications/ConsoleReferencePubSubClient/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ namespace Quickstarts.ConsoleReferencePubSubClient
/// One executable exposes three command-line-selectable modes:
/// <list type="bullet">
/// <item><description>
/// <c>publisher</c> - publishes sample DataSets over UDP/UADP or MQTT (UADP/JSON).
/// <c>publisher</c> - publishes sample DataSets over UDP/UADP, Ethernet/UADP, MQTT, or Kafka (UADP/JSON).
/// </description></item>
/// <item><description>
/// <c>subscriber</c> - receives DataSets and logs each decoded message.
Expand Down Expand Up @@ -88,14 +88,16 @@ public static async Task<int> Main(string[] args)
}

/// <summary>
/// Builds the <c>publisher</c> subcommand: a UDP/UADP or MQTT publisher
/// Builds the <c>publisher</c> subcommand: a UDP/UADP, Ethernet/UADP, MQTT, or Kafka publisher
/// that publishes a built-in sample DataSet.
/// </summary>
private static Command BuildPublisherCommand(Action<int> setExitCode)
{
var profileOption = new Option<string>("--profile")
{
Description = "Transport profile: udp-uadp | mqtt-uadp | mqtt-json.",
Description =
"Transport profile: udp-uadp | eth-uadp | mqtt-uadp | mqtt-json | " +
"kafka-uadp | kafka-json.",
DefaultValueFactory = _ => "udp-uadp"
};
var configFileOption = new Option<string?>("--config-file")
Expand All @@ -121,7 +123,8 @@ private static Command BuildPublisherCommand(Action<int> setExitCode)
{
Description =
"Transport endpoint URL. Defaults: opc.udp://239.0.0.1:4840 (UDP), " +
"mqtt://localhost:1883 (MQTT)."
"opc.eth://01-00-5E-7F-00-01 (Ethernet), mqtt://localhost:1883 (MQTT), " +
"kafka://localhost:9092 (Kafka)."
};
var intervalOption = new Option<int>("--interval")
{
Expand All @@ -131,7 +134,7 @@ private static Command BuildPublisherCommand(Action<int> setExitCode)

var command = new Command(
"publisher",
"Publish a sample DataSet over UDP/UADP or MQTT (UADP/JSON).")
"Publish a sample DataSet over UDP/UADP, Ethernet/UADP, MQTT, or Kafka (UADP/JSON).")
{
profileOption,
configFileOption,
Expand All @@ -149,7 +152,7 @@ private static Command BuildPublisherCommand(Action<int> setExitCode)
{
await Console.Error.WriteLineAsync(
$"Unknown --profile value '{profileArg}'. " +
"Expected one of: udp-uadp, mqtt-uadp, mqtt-json.")
"Expected one of: udp-uadp, eth-uadp, mqtt-uadp, mqtt-json, kafka-uadp, kafka-json.")
.ConfigureAwait(false);
setExitCode(2);
return;
Expand All @@ -169,14 +172,16 @@ await Console.Error.WriteLineAsync(
}

/// <summary>
/// Builds the <c>subscriber</c> subcommand: a UDP/UADP or MQTT subscriber
/// Builds the <c>subscriber</c> subcommand: a UDP/UADP, Ethernet/UADP, MQTT, or Kafka subscriber
/// that logs each decoded DataSetMessage.
/// </summary>
private static Command BuildSubscriberCommand(Action<int> setExitCode)
{
var profileOption = new Option<string>("--profile")
{
Description = "Transport profile: udp-uadp | mqtt-uadp | mqtt-json.",
Description =
"Transport profile: udp-uadp | eth-uadp | mqtt-uadp | mqtt-json | " +
"kafka-uadp | kafka-json.",
DefaultValueFactory = _ => "udp-uadp"
};
var configFileOption = new Option<string?>("--config-file")
Expand All @@ -200,8 +205,10 @@ private static Command BuildSubscriberCommand(Action<int> setExitCode)
};
var endpointOption = new Option<string?>("--endpoint")
{
Description = "Transport endpoint URL. Defaults: opc.udp://239.0.0.1:4840 " +
"(UDP), mqtt://localhost:1883 (MQTT)."
Description =
"Transport endpoint URL. Defaults: opc.udp://239.0.0.1:4840 (UDP), " +
"opc.eth://01-00-5E-7F-00-01 (Ethernet), mqtt://localhost:1883 (MQTT), " +
"kafka://localhost:9092 (Kafka)."
};

var command = new Command(
Expand All @@ -223,7 +230,7 @@ private static Command BuildSubscriberCommand(Action<int> setExitCode)
{
await Console.Error.WriteLineAsync(
$"Unknown --profile value '{profileArg}'. " +
"Expected one of: udp-uadp, mqtt-uadp, mqtt-json.")
"Expected one of: udp-uadp, eth-uadp, mqtt-uadp, mqtt-json, kafka-uadp, kafka-json.")
.ConfigureAwait(false);
setExitCode(2);
return;
Expand Down Expand Up @@ -373,13 +380,23 @@ private static async Task<int> RunPublisherAsync(
.AddUdpTransport()
.AddSecurityKeyProvider(SampleSecurity.CreateKeyProvider())
.AddDataSetSource(PublisherConfigurationBuilder.DataSetName, sampleSource);
if (profile == PublisherProfile.EthUadp)
switch (profile)
{
publisher.AddEthTransport();
}
else if (profile != PublisherProfile.UdpUadp)
{
publisher.AddMqttTransport();
case PublisherProfile.UdpUadp:
break;
case PublisherProfile.EthUadp:
publisher.AddEthTransport();
break;
case PublisherProfile.MqttUadp:
case PublisherProfile.MqttJson:
publisher.AddMqttTransport();
break;
case PublisherProfile.KafkaUadp:
case PublisherProfile.KafkaJson:
publisher.AddKafkaTransport();
break;
default:
throw new ArgumentOutOfRangeException(nameof(profile), profile, null);
}
publisher.ConfigureApplication(app =>
{
Expand Down Expand Up @@ -441,13 +458,23 @@ private static async Task<int> RunSubscriberAsync(
sp => new ConsoleLoggingSink(
sp.GetRequiredService<ILoggerFactory>()
.CreateLogger<ConsoleLoggingSink>()));
if (profile == SubscriberProfile.EthUadp)
{
subscriber.AddEthTransport();
}
else if (profile != SubscriberProfile.UdpUadp)
switch (profile)
{
subscriber.AddMqttTransport();
case SubscriberProfile.UdpUadp:
break;
case SubscriberProfile.EthUadp:
subscriber.AddEthTransport();
break;
case SubscriberProfile.MqttUadp:
case SubscriberProfile.MqttJson:
subscriber.AddMqttTransport();
break;
case SubscriberProfile.KafkaUadp:
case SubscriberProfile.KafkaJson:
subscriber.AddKafkaTransport();
break;
default:
throw new ArgumentOutOfRangeException(nameof(profile), profile, null);
}
subscriber.ConfigureApplication(app =>
{
Expand Down Expand Up @@ -719,6 +746,12 @@ private static bool TryParsePublisherProfile(string? text, out PublisherProfile
case "eth-uadp":
profile = PublisherProfile.EthUadp;
return true;
case "kafka-uadp":
profile = PublisherProfile.KafkaUadp;
return true;
case "kafka-json":
profile = PublisherProfile.KafkaJson;
return true;
default:
profile = PublisherProfile.UdpUadp;
return false;
Expand All @@ -741,6 +774,12 @@ private static bool TryParseSubscriberProfile(string? text, out SubscriberProfil
case "eth-uadp":
profile = SubscriberProfile.EthUadp;
return true;
case "kafka-uadp":
profile = SubscriberProfile.KafkaUadp;
return true;
case "kafka-json":
profile = SubscriberProfile.KafkaJson;
return true;
default:
profile = SubscriberProfile.UdpUadp;
return false;
Expand Down Expand Up @@ -843,7 +882,17 @@ public enum PublisherProfile
/// <summary>
/// Ethernet (Layer 2) transport with UADP message mapping.
/// </summary>
EthUadp = 3
EthUadp = 3,

/// <summary>
/// Kafka broker transport with UADP message mapping.
/// </summary>
KafkaUadp = 4,

/// <summary>
/// Kafka broker transport with JSON message mapping.
/// </summary>
KafkaJson = 5
}

/// <summary>
Expand All @@ -869,7 +918,17 @@ public enum SubscriberProfile
/// <summary>
/// Ethernet (Layer 2) transport with UADP message mapping.
/// </summary>
EthUadp = 3
EthUadp = 3,

/// <summary>
/// Kafka broker transport with UADP message mapping.
/// </summary>
KafkaUadp = 4,

/// <summary>
/// Kafka broker transport with JSON message mapping.
/// </summary>
KafkaJson = 5
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@
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 three demo wire profiles using the fluent
/// 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).
Expand All @@ -47,15 +48,19 @@ public static class PublisherConfigurationBuilder
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,
_ => DefaultMqttEndpoint
PublisherProfile.KafkaUadp or PublisherProfile.KafkaJson => DefaultKafkaEndpoint,
PublisherProfile.MqttUadp or PublisherProfile.MqttJson => DefaultMqttEndpoint,
_ => throw new ArgumentOutOfRangeException(nameof(profile))
};
}

Expand All @@ -68,20 +73,25 @@ public static PubSubConfigurationDataType Build(
int intervalMs)
{
// UDP and Ethernet are datagram transports (no broker queue);
// the MQTT profiles use broker transport settings instead.
// 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 != PublisherProfile.MqttJson;
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))
};

Expand All @@ -104,7 +114,7 @@ public static PubSubConfigurationDataType Build(
? new DatagramWriterGroupTransportDataType()
: new BrokerWriterGroupTransportDataType
{
QueueName = MqttQueueName
QueueName = brokerQueueName
});
if (secured)
{
Expand All @@ -126,7 +136,7 @@ public static PubSubConfigurationDataType Build(
writer.WithTransportSettings(
new BrokerDataSetWriterTransportDataType
{
QueueName = MqttQueueName,
QueueName = brokerQueueName,
RequestedDeliveryGuarantee
= BrokerTransportQualityOfService.BestEffort
});
Expand All @@ -138,7 +148,7 @@ public static PubSubConfigurationDataType Build(

private static IEncodeable WriterGroupMessageSettings(PublisherProfile profile)
{
if (profile == PublisherProfile.MqttJson)
if (profile is PublisherProfile.MqttJson or PublisherProfile.KafkaJson)
{
return new JsonWriterGroupMessageDataType
{
Expand All @@ -163,7 +173,7 @@ private static IEncodeable WriterGroupMessageSettings(PublisherProfile profile)

private static IEncodeable WriterMessageSettings(PublisherProfile profile)
{
if (profile == PublisherProfile.MqttJson)
if (profile is PublisherProfile.MqttJson or PublisherProfile.KafkaJson)
{
return new JsonDataSetWriterMessageDataType
{
Expand Down
Loading
Loading