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"); + } +}