From 6a6ac9a11f43a887c8be87d9bb1efe89b44c5ab8 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 8 Oct 2026 06:52:47 -0400 Subject: [PATCH] fix(abcip): libplctag ErrorBadParam at tag create is a per-tag config error, no device backoff (#570, #593) Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe --- .../LibplctagStatusTranslation.cs | 26 +++++++++++++ .../LibplctagTagRuntime.cs | 2 +- .../LibplctagStatusTranslationTests.cs | 37 +++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagStatusTranslation.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagStatusTranslation.cs index 3d695bdd..059dbc87 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagStatusTranslation.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagStatusTranslation.cs @@ -37,6 +37,32 @@ internal static class LibplctagStatusTranslation ? ToAbStatus(s) : AbStatus.BadConnection; + /// + /// True when is libplctag refusing the tag's own attributes (ErrorBadParam: + /// the name or a create attribute does not parse), as opposed to a transport failure. + /// + /// The exception. + public static bool IsTagParameterRefusal(LibPlcTagException ex) => + Enum.TryParse((ex.Message ?? string.Empty).Trim(), ignoreCase: true, out var s) && s == Status.ErrorBadParam; + + /// + /// Runs a tag's initialisation: ErrorBadParam there means libplctag rejected the tag's own + /// name before any I/O, so it is raised as (per-tag + /// BadConfigurationError, no backoff, no health signal — Gitea #570/#593) instead of falling through + /// to the generic default that backs the whole device off. Only the + /// create phase: a BadParam from a read or write on a live handle is not a name problem. + /// + /// The libplctag initialisation. + /// A task completing when the initialisation does. + public static async Task WrapInitialize(Func op) + { + try { await op().ConfigureAwait(false); } + catch (LibPlcTagException ex) when (IsTagParameterRefusal(ex)) + { + throw new AbCipTagConfigurationException($"libplctag rejected the tag's name or attributes: {ex.Message}", ex); + } + } + /// Runs , rethrowing any as . /// The libplctag operation. /// A task completing when the operation does. 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 a12885ff..1b4d051e 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs @@ -52,7 +52,7 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime /// public Task InitializeAsync(CancellationToken cancellationToken) => - RunReleasingOrphanAsync(() => _tag.InitializeAsync(cancellationToken)); + RunReleasingOrphanAsync(() => LibplctagStatusTranslation.WrapInitialize(() => _tag.InitializeAsync(cancellationToken))); /// /// libplctag initialises an uninitialised tag on its first read, so this path can orphan one too. diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/LibplctagStatusTranslationTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/LibplctagStatusTranslationTests.cs index 39bc958a..fba8f7ea 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/LibplctagStatusTranslationTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/LibplctagStatusTranslationTests.cs @@ -111,4 +111,41 @@ public sealed class LibplctagStatusTranslationTests ex.Status.ShouldBe(AbStatus.Timeout); ex.InnerException.ShouldBeSameAs(inner); } + + /// + /// #593 / #570: ErrorBadParam during initialisation is libplctag refusing the tag's own name — a + /// per-tag configuration error (the driver's no-backoff, no-health path), not a device connect failure. + /// + [Fact] + public async Task WrapInitialize_maps_ErrorBadParam_to_a_tag_configuration_exception() + { + var thrown = await Should.ThrowAsync(() => + LibplctagStatusTranslation.WrapInitialize(() => throw new LibPlcTagException(Status.ErrorBadParam))); + thrown.InnerException.ShouldBeOfType(); + } + + /// + /// Falsification pair for the above: every OTHER initialisation failure is left alone (it still reaches + /// Wrap and becomes a transport status that opens the backoff window). + /// + /// The libplctag status. + [Theory] + [InlineData(Status.ErrorTimeout)] + [InlineData(Status.ErrorBadGateway)] + [InlineData(Status.ErrorNotFound)] + public async Task WrapInitialize_leaves_other_statuses_untouched(Status status) + { + var thrown = await Should.ThrowAsync(() => + LibplctagStatusTranslation.WrapInitialize(() => throw new LibPlcTagException(status))); + thrown.ShouldNotBeOfType(); + } + + /// A BadParam on the read/write path (through Wrap) is not reclassified as configuration. + [Fact] + public async Task Wrap_still_maps_ErrorBadParam_to_a_transport_status() + { + var thrown = await Should.ThrowAsync(() => + LibplctagStatusTranslation.Wrap(() => throw new LibPlcTagException(Status.ErrorBadParam))); + thrown.Status.ShouldBe(AbStatus.BadConnection); + } }