diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagPath.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagPath.cs
new file mode 100644
index 00000000..69414ab7
--- /dev/null
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagPath.cs
@@ -0,0 +1,13 @@
+namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
+
+///
+/// Normalizes a parsed CIP route for libplctag's Path attribute. libplctag rejects an
+/// empty-string path with ErrorBadData; "no route" (Micro800, unrouted MicroLogix) must be
+/// expressed by omitting the attribute, i.e. .
+///
+internal static class LibplctagPath
+{
+ /// Returns for a null, empty or whitespace path; otherwise the path unchanged.
+ /// The parsed CIP path.
+ public static string? Normalize(string? path) => string.IsNullOrWhiteSpace(path) ? null : path;
+}
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagEnumerator.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagEnumerator.cs
index 304baae7..486dcbfc 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagEnumerator.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagEnumerator.cs
@@ -37,7 +37,7 @@ internal static class LibplctagListingReader
internal static Tag BuildSymbolListTag(AbCipTagCreateParams deviceParams, string listingName = "@tags") => new()
{
Gateway = deviceParams.LibplctagGateway,
- Path = deviceParams.CipPath,
+ Path = LibplctagPath.Normalize(deviceParams.CipPath),
PlcType = MapPlcType(deviceParams.LibplctagPlcAttribute),
Protocol = Protocol.ab_eip,
Name = listingName,
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs
index f0655678..cfe3761c 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs
@@ -18,12 +18,18 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
/// The tag creation parameters.
public LibplctagTagRuntime(AbCipTagCreateParams p)
{
- _tag = new Tag
+ _tag = BuildTag(p);
+ }
+
+ /// Builds the (uninitialized) libplctag tag for .
+ /// The tag creation parameters.
+ internal static Tag BuildTag(AbCipTagCreateParams p) =>
+ new Tag
{
// host:port — see AbLegacyTagCreateParams/AbCipTagCreateParams.LibplctagGateway.
// Passing p.Gateway alone drops the authored port onto libplctag's 44818 default (#534).
Gateway = p.LibplctagGateway,
- Path = p.CipPath,
+ Path = LibplctagPath.Normalize(p.CipPath),
PlcType = MapPlcType(p.LibplctagPlcAttribute),
Protocol = Protocol.ab_eip,
Name = p.TagName,
@@ -33,7 +39,6 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
// count so libplctag pulls every element in one CIP read; scalar tags pass 1.
ElementCount = p.ElementCount > 1 ? p.ElementCount : 1,
};
- }
///
public Task InitializeAsync(CancellationToken cancellationToken) =>
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTemplateReader.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTemplateReader.cs
index 2fc4d94f..e04864d6 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTemplateReader.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTemplateReader.cs
@@ -39,7 +39,7 @@ internal sealed class LibplctagTemplateReader : IAbCipTemplateReader
internal static Tag BuildTemplateTag(AbCipTagCreateParams deviceParams, uint templateInstanceId) => new()
{
Gateway = deviceParams.LibplctagGateway,
- Path = deviceParams.CipPath,
+ Path = LibplctagPath.Normalize(deviceParams.CipPath),
PlcType = MapPlcType(deviceParams.LibplctagPlcAttribute),
Protocol = Protocol.ab_eip,
Name = $"@udt/{templateInstanceId}",
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyTagRuntime.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyTagRuntime.cs
index c15605f6..5cb03a08 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyTagRuntime.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyTagRuntime.cs
@@ -17,19 +17,24 @@ internal sealed class LibplctagLegacyTagRuntime : IAbLegacyTagRuntime
/// The parameters for tag creation.
public LibplctagLegacyTagRuntime(AbLegacyTagCreateParams p)
{
- _tag = new Tag
+ _tag = BuildTag(p);
+ }
+
+ /// Builds the (uninitialized) libplctag tag for .
+ /// The tag creation parameters.
+ internal static Tag BuildTag(AbLegacyTagCreateParams p) =>
+ new Tag
{
// host:port — see AbLegacyTagCreateParams/AbCipTagCreateParams.LibplctagGateway.
// Passing p.Gateway alone drops the authored port onto libplctag's 44818 default (#534).
Gateway = p.LibplctagGateway,
- Path = p.CipPath,
+ Path = LibplctagPath.Normalize(p.CipPath),
PlcType = MapPlcType(p.LibplctagPlcAttribute),
Protocol = Protocol.ab_eip, // PCCC-over-EIP; libplctag routes via the PlcType-specific PCCC layer
Name = p.TagName,
Timeout = p.Timeout,
ElementCount = p.ElementCount > 1 ? p.ElementCount : null,
};
- }
///
public Task InitializeAsync(CancellationToken cancellationToken) =>
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagPath.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagPath.cs
new file mode 100644
index 00000000..a7104292
--- /dev/null
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagPath.cs
@@ -0,0 +1,13 @@
+namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
+
+///
+/// Normalizes a parsed CIP route for libplctag's Path attribute. libplctag rejects an
+/// empty-string path with ErrorBadData; "no route" (Micro800, unrouted MicroLogix) must be
+/// expressed by omitting the attribute, i.e. .
+///
+internal static class LibplctagPath
+{
+ /// Returns for a null, empty or whitespace path; otherwise the path unchanged.
+ /// The parsed CIP path.
+ public static string? Normalize(string? path) => string.IsNullOrWhiteSpace(path) ? null : path;
+}
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbCipReadSmokeTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbCipReadSmokeTests.cs
index 44d142e7..a6a5c1cd 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbCipReadSmokeTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbCipReadSmokeTests.cs
@@ -32,7 +32,9 @@ public sealed class AbCipReadSmokeTests
// smoke test was missed.
// Micro800 has no backplane: its cip-path is empty (the compose service runs
// ab_server --plc=Micro800 with no --path, which refuses a routed request). Every
- // other family reaches slot 0 through `1,0`.
+ // other family reaches slot 0 through `1,0`. Both transports are expected to pass
+ // for Micro800: the libplctag adapters map the empty route to a null `path` attribute
+ // (LibplctagPath.Normalize) because libplctag rejects "" with ErrorBadData.
var cipPath = profile.Family == AbCipPlcFamily.Micro800 ? "" : "1,0";
var deviceUri = $"ab://{fixture.Host}:{fixture.Port}/{cipPath}";
var drv = new AbCipDriver(new AbCipDriverOptions
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/LibplctagEmptyPathTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/LibplctagEmptyPathTests.cs
new file mode 100644
index 00000000..4cd93ba5
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/LibplctagEmptyPathTests.cs
@@ -0,0 +1,51 @@
+using Shouldly;
+using Xunit;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
+
+///
+/// libplctag rejects an empty-string path with ErrorBadData; "no route" (Micro800, or any
+/// unrouted device authored as ab://host:44818/) must reach it as null. Pre-dates the native transport.
+///
+public sealed class LibplctagEmptyPathTests
+{
+ private static AbCipTagCreateParams Params(string cipPath) =>
+ new(Gateway: "10.100.0.35", Port: 44818, CipPath: cipPath, LibplctagPlcAttribute: "micro800",
+ TagName: "TestDINT", Timeout: TimeSpan.FromSeconds(5));
+
+ [Theory]
+ [InlineData("", null)]
+ [InlineData(" ", null)]
+ [InlineData(null, null)]
+ [InlineData("1,0", "1,0")]
+ public void Normalize_maps_blank_to_null_and_keeps_a_route(string? input, string? expected) =>
+ LibplctagPath.Normalize(input).ShouldBe(expected);
+
+ [Fact]
+ public void The_data_tag_omits_the_path_for_an_empty_route()
+ {
+ using var tag = LibplctagTagRuntime.BuildTag(Params(""));
+ tag.Path.ShouldBeNull();
+ }
+
+ [Fact]
+ public void The_data_tag_keeps_a_real_route()
+ {
+ using var tag = LibplctagTagRuntime.BuildTag(Params("1,0"));
+ tag.Path.ShouldBe("1,0");
+ }
+
+ [Fact]
+ public void The_symbol_list_tag_omits_the_path_for_an_empty_route()
+ {
+ using var tag = LibplctagListingReader.BuildSymbolListTag(Params(""));
+ tag.Path.ShouldBeNull();
+ }
+
+ [Fact]
+ public void The_template_tag_omits_the_path_for_an_empty_route()
+ {
+ using var tag = LibplctagTemplateReader.BuildTemplateTag(Params(""), templateInstanceId: 1);
+ tag.Path.ShouldBeNull();
+ }
+}
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/LibplctagEmptyPathTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/LibplctagEmptyPathTests.cs
new file mode 100644
index 00000000..9871bfa4
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/LibplctagEmptyPathTests.cs
@@ -0,0 +1,37 @@
+using Shouldly;
+using Xunit;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
+
+///
+/// libplctag rejects an empty-string path with ErrorBadData; an unrouted MicroLogix
+/// (ab://host:44818/) must reach it as null.
+///
+public sealed class LibplctagEmptyPathTests
+{
+ private static AbLegacyTagCreateParams Params(string cipPath) =>
+ new(Gateway: "10.100.0.35", Port: 44818, CipPath: cipPath, LibplctagPlcAttribute: "micrologix",
+ TagName: "N7:0", Timeout: TimeSpan.FromSeconds(5));
+
+ [Theory]
+ [InlineData("", null)]
+ [InlineData(" ", null)]
+ [InlineData(null, null)]
+ [InlineData("1,0", "1,0")]
+ public void Normalize_maps_blank_to_null_and_keeps_a_route(string? input, string? expected) =>
+ LibplctagPath.Normalize(input).ShouldBe(expected);
+
+ [Fact]
+ public void The_data_tag_omits_the_path_for_an_empty_route()
+ {
+ using var tag = LibplctagLegacyTagRuntime.BuildTag(Params(""));
+ tag.Path.ShouldBeNull();
+ }
+
+ [Fact]
+ public void The_data_tag_keeps_a_real_route()
+ {
+ using var tag = LibplctagLegacyTagRuntime.BuildTag(Params("1,0"));
+ tag.Path.ShouldBe("1,0");
+ }
+}