fix(ablegacy): libplctag refuses MicroLogix function files locally instead of a comms fault (#592)
v2-ci / unit-tests (push) Has been cancelled
v2-ci / unit-tests (push) Has been cancelled
libplctag 2.6.0 cannot parse RTC/HSC/... names; tag creation failed with PLCTAG_ERR_BAD_PARAM, which the status translation reported as BadConnection: evicted handle, degraded health, counted toward marking a healthy device down. The libplctag runtime now refuses function-file addresses before creating a native tag (Unsupported -> BadNotSupported, health untouched), matching native. Docs: function files are unsupported on both transports, not "unverified". Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
This commit is contained in:
@@ -177,7 +177,7 @@ re-emit a concrete default. The defaults differ per driver (final review, 2026-1
|
||||
only: a libplctag WRITE to any address the translation changes (`S:`/`O:`/`I:` incl. bit writes, R `EN…FD`, `C…UA`)
|
||||
is refused `BadNotSupported` before any runtime is created (none could reach the PLC before — no write-surface
|
||||
widening on the default transport); libplctag T/C status-bit writes it parses itself are unchanged.
|
||||
L/A files, function files, SLC, PLC-5 and `logixpccc` remain unverified natively. Explicit Native on PLC-5/`LogixPccc` logs a once-per-instance Warning.
|
||||
L/A files, SLC, PLC-5 and `logixpccc` remain unverified natively. MicroLogix function files are refused `BadNotSupported` on BOTH transports (libplctag cannot parse them; its local `ERR_BAD_PARAM` used to surface as a comms fault until 2026-11-13, #592). Explicit Native on PLC-5/`LogixPccc` logs a once-per-instance Warning.
|
||||
AbLegacy native refuses status-bit sub-element writes (`T4:0.DN`…, `BadNotSupported`).
|
||||
|
||||
Refusals are not comms faults: an Unsupported refusal (`BadNotSupported`) and an AbCip native **type
|
||||
|
||||
@@ -118,9 +118,15 @@ physical inputs and may reject the write.
|
||||
- **MicroLogix function files (#245).** `RTC`, `HSC`, `DLS`, `MMI`, `PTO`, `PWM`, `STI`, `EII`, `IOS`, `BHI`
|
||||
(single fixed instance — no file number — with a `.SUBELEMENT`; no bit index) parse on MicroLogix devices only.
|
||||
`AbLegacyFunctionFile.SubElementType` carries the sub-element catalogue (flags → Boolean, words → Int32; unknown
|
||||
members default to Int32). ⚠️ **Wire-unverified on both transports:** there is no capture and no simulator
|
||||
coverage; the libplctag path passes the name through and the native transport has no file-type code for them, so
|
||||
it refuses the tag as `Unsupported` (`BadNotSupported` to the client; see Wire evidence).
|
||||
members default to Int32). ⚠️ **Neither transport can read them — they author and deploy, but every read and
|
||||
write answers `BadNotSupported`.** The native transport has no file-type code for them, and libplctag 2.6.0's
|
||||
`parse_pccc_file_type` has no function-file letters: `RTC` parses as an R file followed by a non-digit, `HSC`/`EII`
|
||||
fall to its default case, so tag creation fails locally with `PLCTAG_ERR_BAD_PARAM` and nothing reaches the wire.
|
||||
Until 2026-11-13 the libplctag adapter reported that as a **comms fault** (`BadCommunicationError`, evicted handle,
|
||||
degraded health, counted toward marking a healthy device down); it now refuses the address before creating a tag
|
||||
(`LibplctagLegacyTagRuntime.Refusal`), health untouched, the same answer native gives (#592). So this is a
|
||||
missing capability, not an unverified one: a real-controller read would show nothing new. Supporting them means
|
||||
a native PCCC encoding for function files (a feature decision); libplctag removal (P6) loses nothing here.
|
||||
- **Indirect addressing (#247 parse, #646 runtime).** `N7:[N7:0]` (word from another address) and `N[N7:0]:5` (file
|
||||
from another address) parse, one level deep (the inner address must be direct, and carries the family's octal rule).
|
||||
At runtime the driver reads the inner address first (same runtime factory, so libplctag and Native alike), requires
|
||||
@@ -187,8 +193,8 @@ Native routing: SLC500/MicroLogix/PLC-5 are direct; `logixpccc` uses Unconnected
|
||||
- **Known limits of Native** (why it is opt-in):
|
||||
- ⚠️ **Real-hardware evidence is one MicroLogix 1400** (Z-33323, 2026-10-09) — see
|
||||
[Wire evidence](#wire-evidence-native-pccc): S (incl. `S:0`), N, F, B, T, C, R, ST, O, I reads, T/C/R
|
||||
sub-elements and status bits, and N/F/B-bit writes all correct. **L and A files, function files, SLC 5/05,
|
||||
PLC-5 and `logixpccc` are unverified.** Opt in per driver after checking it against the real controller.
|
||||
sub-elements and status bits, and N/F/B-bit writes all correct. **L and A files, SLC 5/05,
|
||||
PLC-5 and `logixpccc` are unverified; function files are unsupported on both transports.** Opt in per driver after checking it against the real controller.
|
||||
- ⚠️ **Status-bit writes are refused** — `T4:0.DN`, `T4:0.TT`, `T4:0.EN`, `C5:0.CU`, `C5:0.DN`, `R6:0.EN`
|
||||
and the rest return **`BadNotSupported`** whatever the tag's authored data type (the refusal keys on the
|
||||
address); nothing is written to the PLC; health is unchanged. The driver answers the refusal **before it
|
||||
@@ -255,7 +261,9 @@ such as `O:0/3` is refused ahead of the parent-word read-modify-write) and the r
|
||||
sent, health unchanged (`LibplctagLegacyTagRuntime.WriteRefusal`, asked through `IAbLegacyTagFactory.WriteRefusal`, the
|
||||
same plumbing as the native refusal). Those writes never worked on libplctag, so nothing is lost; the native
|
||||
transport (opt-in) does write `S`/`O`/`I`. T/C status bits libplctag parses itself keep their pre-existing write behaviour (note above).
|
||||
Not tested on hardware: L and A files (no press has them), function files, PLC-5, SLC, `logixpccc`.
|
||||
Not tested on hardware: L and A files (no press has them), PLC-5, SLC, `logixpccc`. Function files are not
|
||||
supported by either transport (refused locally, nothing sent — see the function-file note above), so there is
|
||||
nothing to test on hardware.
|
||||
|
||||
Simulator evidence:
|
||||
|
||||
@@ -265,7 +273,8 @@ Simulator evidence:
|
||||
| N-file write (`N7:5`) | capture `slc500-write-n7` + native `AbLegacyReadSmokeTests` write/read round trip (SLC 500) |
|
||||
| F-file read (`F8`) | one capture, `slc500-read-f8` (codec parity only; no native F write on a wire) |
|
||||
| S (incl. `S:0`), B, T, C, R, ST, O, I | **real MicroLogix 1400 (above)**; L and A: none |
|
||||
| MicroLogix function files (RTC/HSC/...), PLC-5 octal I/O | **none** — parsed only; native refuses function files as Unsupported |
|
||||
| MicroLogix function files (RTC/HSC/...) | **not supported** — parsed and deployable; both transports refuse them before the wire (`BadNotSupported`, health untouched) |
|
||||
| PLC-5 octal I/O | **none** — parsed only |
|
||||
| Bit-within-word write (`B3:0/5`) | `ab_server` SLC500 + MicroLogix, both transports (`AbLegacyBitWriteTests`): the driver RMWs the parent word, only the addressed bit changes (the parent-word read-modify-write is not atomic against ladder writes to the same word). PLC-5 native refuses it (`BadNotSupported`, #571) |
|
||||
| Indirect addressing (`N7:[N7:0]`) | `ab_server` N-file, both transports (#646) - resolved by the driver, the transport sees the composed address |
|
||||
| T/C/R sub-elements (`.PRE`/`.ACC`/`.LEN`/`.POS`, status bits) | **real MicroLogix 1400 (above)** |
|
||||
@@ -273,7 +282,7 @@ Simulator evidence:
|
||||
### Native known gaps
|
||||
|
||||
- ⚠️ **One real controller.** A MicroLogix 1400 (Z-33323) passed reads across S/N/F/B/T/C/R/ST/O/I and N/F/B-bit
|
||||
writes; SLC 5/05, PLC-5, `logixpccc`, L/A files and function files are unverified. See
|
||||
writes; SLC 5/05, PLC-5, `logixpccc` and L/A files are unverified; function files are unsupported (both transports). See
|
||||
[Wire evidence](#wire-evidence-native-pccc).
|
||||
- **PLC-5 native is N-file reads only.** Every other file type and all PLC-5 writes are refused before the
|
||||
wire and reach the client as `BadNotSupported` (handle kept, health unchanged; #571 closed). Native is opt-in
|
||||
|
||||
@@ -1641,9 +1641,11 @@ public sealed partial class AbLegacyDriver : IDriver, IReadable, IWritable, ITag
|
||||
&& (IsDeviceRefusal(ate.Status) || ate is AbDeviceReplyException { Status: AbStatus.Unsupported });
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a freshly created runtime. A NATIVE runtime whose init (one read) is refused locally is
|
||||
/// still KEPT: the refusal is deterministic and per-operation, so caching it avoids re-creating — and
|
||||
/// Initializes a freshly created runtime. A runtime whose init is refused locally is still KEPT: the
|
||||
/// refusal is deterministic and per-operation, so caching it avoids re-creating — and, for NATIVE,
|
||||
/// re-leasing the pooled device connection — on every poll. Any other failure disposes it, as before.
|
||||
/// (A libplctag runtime refuses only before creating its native tag — e.g. a MicroLogix function file —
|
||||
/// so keeping it holds no native handle.)
|
||||
/// </summary>
|
||||
private static async Task InitializeOrKeepRefusedAsync(IAbLegacyTagRuntime runtime, Action keep, CancellationToken ct)
|
||||
{
|
||||
@@ -1651,7 +1653,8 @@ public sealed partial class AbLegacyDriver : IDriver, IReadable, IWritable, ITag
|
||||
{
|
||||
await runtime.InitializeAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex) when (IsLocalRefusal(ex) && runtime is NativeAbLegacyTagRuntime)
|
||||
catch (Exception ex) when (IsLocalRefusal(ex)
|
||||
&& runtime is NativeAbLegacyTagRuntime or LibplctagLegacyTagRuntime { IsRefused: true })
|
||||
{
|
||||
keep();
|
||||
throw;
|
||||
|
||||
@@ -5,9 +5,9 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
|
||||
/// <summary>
|
||||
/// MicroLogix 1100/1400 function-file sub-element catalogue: the driver-surface type of
|
||||
/// <c>RTC:0.HR</c>, <c>HSC:0.ACC</c>, ... Covers the commonly-addressed members per file — not exhaustive
|
||||
/// (Rockwell defines 30+ on RTC alone). <b>Unverified on the wire:</b> the layouts are transcribed from the
|
||||
/// MicroLogix instruction-set reference; neither transport has been captured against these files and the
|
||||
/// native transport has no encoding for them at all.
|
||||
/// (Rockwell defines 30+ on RTC alone). <b>Not readable today:</b> the layouts are transcribed from the
|
||||
/// MicroLogix instruction-set reference; the native transport has no encoding for these files and libplctag
|
||||
/// cannot parse their names, so both refuse them before the wire (<c>BadNotSupported</c>, #592).
|
||||
/// </summary>
|
||||
public static class AbLegacyFunctionFile
|
||||
{
|
||||
|
||||
@@ -21,6 +21,9 @@ internal sealed class LibplctagLegacyTagRuntime : IAbLegacyTagRuntime
|
||||
/// <summary>Why a write to this tag is refused before the wire (<see cref="WriteRefusal"/>), else null.</summary>
|
||||
private readonly string? _writeRefusal;
|
||||
|
||||
/// <summary>Why EVERY operation on this tag is refused before the wire (<see cref="Refusal"/>), else null.</summary>
|
||||
private readonly string? _refusal;
|
||||
|
||||
/// <summary>
|
||||
/// The status the native tag held when a failed initialisation destroyed it (see
|
||||
/// <see cref="LibplctagOrphanRelease"/>); null while the wrapper's own handle is authoritative.
|
||||
@@ -37,6 +40,7 @@ internal sealed class LibplctagLegacyTagRuntime : IAbLegacyTagRuntime
|
||||
_tag = BuildTag(p);
|
||||
_count = Math.Max(1, p.ElementCount);
|
||||
_writeRefusal = WriteRefusal(p);
|
||||
_refusal = Refusal(p);
|
||||
}
|
||||
|
||||
/// <summary>Builds the (uninitialized) libplctag tag for <paramref name="p"/>.</summary>
|
||||
@@ -60,12 +64,16 @@ internal sealed class LibplctagLegacyTagRuntime : IAbLegacyTagRuntime
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task InitializeAsync(CancellationToken cancellationToken) =>
|
||||
RunReleasingOrphanAsync(() => _tag.InitializeAsync(cancellationToken));
|
||||
_refusal is { } refusal
|
||||
? Task.FromException(new AbTransportException(AbStatus.Unsupported, refusal))
|
||||
: RunReleasingOrphanAsync(() => _tag.InitializeAsync(cancellationToken));
|
||||
|
||||
// libplctag initialises an uninitialised tag on its first read, so this path can orphan one too.
|
||||
/// <inheritdoc />
|
||||
public Task ReadAsync(CancellationToken cancellationToken) =>
|
||||
RunReleasingOrphanAsync(() => _tag.ReadAsync(cancellationToken));
|
||||
_refusal is { } refusal
|
||||
? Task.FromException(new AbTransportException(AbStatus.Unsupported, refusal))
|
||||
: RunReleasingOrphanAsync(() => _tag.ReadAsync(cancellationToken));
|
||||
|
||||
// Throws the WriteRefusal refusal (Unsupported) before anything is sent — defence in depth;
|
||||
// the driver asks the factory first and never creates a runtime for a refused write.
|
||||
@@ -88,8 +96,24 @@ internal sealed class LibplctagLegacyTagRuntime : IAbLegacyTagRuntime
|
||||
/// <param name="p">The tag's create parameters.</param>
|
||||
/// <returns>The refusal reason, or <see langword="null"/>.</returns>
|
||||
internal static string? WriteRefusal(AbLegacyTagCreateParams p) =>
|
||||
LibplctagLegacyName.IsTranslated(p.TagName)
|
||||
Refusal(p) ?? (LibplctagLegacyName.IsTranslated(p.TagName)
|
||||
? $"Writes to '{p.TagName}' are not supported on the libplctag transport (its name is translated for libplctag; reads only)."
|
||||
: null);
|
||||
|
||||
/// <summary>
|
||||
/// The addresses libplctag cannot address at all, refused before a native tag is created: MicroLogix function
|
||||
/// files (<c>RTC:0.HR</c>, <c>HSC:0.ACC</c>, ...). libplctag 2.6.0's <c>parse_pccc_file_type</c> has no function-file
|
||||
/// letters (<c>RTC</c> reads as an R file followed by a non-digit, <c>HSC</c> falls to the default case), so tag
|
||||
/// creation fails locally with <c>PLCTAG_ERR_BAD_PARAM</c> — which the status translation has no case for and so
|
||||
/// reported as <see cref="AbStatus.BadConnection"/>: a comms fault that evicted the handle, degraded health and
|
||||
/// counted toward marking a healthy device down. Refusing it as <see cref="AbStatus.Unsupported"/> answers
|
||||
/// <c>BadNotSupported</c> with health untouched — the same answer the native transport gives (#592, 2026-11-13).
|
||||
/// </summary>
|
||||
/// <param name="p">The tag's create parameters.</param>
|
||||
/// <returns>The refusal reason, or <see langword="null"/>.</returns>
|
||||
internal static string? Refusal(AbLegacyTagCreateParams p) =>
|
||||
NativeAbLegacyTagRuntime.IsFunctionFileName(p.TagName)
|
||||
? $"'{p.TagName}' is a MicroLogix function file; the libplctag transport cannot address function files."
|
||||
: null;
|
||||
|
||||
/// <summary>
|
||||
@@ -121,6 +145,9 @@ internal sealed class LibplctagLegacyTagRuntime : IAbLegacyTagRuntime
|
||||
/// (0 if none was created). Tests only.</summary>
|
||||
internal int NativeHandle => _releasedHandle != 0 ? _releasedHandle : LibplctagOrphanRelease.HandleOf(_tag);
|
||||
|
||||
/// <summary>True when every operation is refused before the wire (<see cref="Refusal"/>); no native tag is ever created.</summary>
|
||||
internal bool IsRefused => _refusal is not null;
|
||||
|
||||
/// <summary>The handle the libplctag wrapper itself still holds (0 once a release cleared it). Tests only.</summary>
|
||||
internal int WrapperHandle => LibplctagOrphanRelease.HandleOf(_tag);
|
||||
|
||||
|
||||
@@ -168,7 +168,10 @@ internal sealed class NativeAbLegacyTagRuntime : IAbLegacyTagRuntime
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsFunctionFileName(string tagName)
|
||||
/// <summary>True when <paramref name="tagName"/> names a MicroLogix function file (<c>RTC:0.HR</c>, <c>HSC:0.ACC</c>, ...).</summary>
|
||||
/// <param name="tagName">The canonical tag name.</param>
|
||||
/// <returns><see langword="true"/> for a function-file address.</returns>
|
||||
internal static bool IsFunctionFileName(string tagName)
|
||||
{
|
||||
var colon = tagName.IndexOf(':');
|
||||
return colon > 0 && AbLegacyAddress.IsFunctionFileLetter(tagName[..colon].ToUpperInvariant());
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.PlcFamilies;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// #592 (2026-11-13): libplctag 2.6.0 cannot parse a MicroLogix function-file name (<c>RTC:0.HR</c>, <c>HSC:0.ACC</c>),
|
||||
/// so its tag creation failed locally with <c>PLCTAG_ERR_BAD_PARAM</c> — translated to <see cref="AbStatus.BadConnection"/>,
|
||||
/// i.e. a comms fault that evicted the handle, degraded health and counted toward marking the device down. The libplctag
|
||||
/// runtime now refuses these addresses before creating a native tag, as the native transport already did.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class LibplctagFunctionFileRefusalTests
|
||||
{
|
||||
private static AbLegacyTagCreateParams Params(string tag) =>
|
||||
new("192.0.2.1", 44818, "", "micrologix", tag, TimeSpan.FromSeconds(1));
|
||||
|
||||
[Theory]
|
||||
[InlineData("RTC:0.HR", true)]
|
||||
[InlineData("RTC:0.YR", true)]
|
||||
[InlineData("HSC:0.ACC", true)]
|
||||
[InlineData("DLS:0.EN", true)]
|
||||
[InlineData("BHI:0.OS", true)]
|
||||
[InlineData("STI:0.EN", true)]
|
||||
[InlineData("N7:0", false)]
|
||||
[InlineData("R6:0.LEN", false)]
|
||||
[InlineData("ST14:0", false)]
|
||||
[InlineData("S:0", false)]
|
||||
public void Only_function_files_are_refused(string tag, bool refused)
|
||||
{
|
||||
(LibplctagLegacyTagRuntime.Refusal(Params(tag)) is not null).ShouldBe(refused);
|
||||
if (refused) LibplctagLegacyTagRuntime.WriteRefusal(Params(tag)).ShouldNotBeNull();
|
||||
}
|
||||
|
||||
/// <summary>Init, read and write all refuse without touching the network (the gateway is unroutable, the timeout 1 s).</summary>
|
||||
[Fact]
|
||||
public async Task The_runtime_refuses_every_operation_before_the_wire()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
using var rt = new LibplctagLegacyTagRuntime(Params("RTC:0.HR"));
|
||||
rt.IsRefused.ShouldBeTrue();
|
||||
(await Should.ThrowAsync<AbTransportException>(() => rt.InitializeAsync(ct))).Status.ShouldBe(AbStatus.Unsupported);
|
||||
(await Should.ThrowAsync<AbTransportException>(() => rt.ReadAsync(ct))).Status.ShouldBe(AbStatus.Unsupported);
|
||||
(await Should.ThrowAsync<AbTransportException>(() => rt.WriteAsync(ct))).Status.ShouldBe(AbStatus.Unsupported);
|
||||
rt.NativeHandle.ShouldBe(0, "no native libplctag tag is ever created for a refused address");
|
||||
}
|
||||
|
||||
#pragma warning disable OTOPCUA0001 // direct driver calls pin the driver's own status mapping (no resilience wrapper)
|
||||
/// <summary>Through the production libplctag factory: BadNotSupported, health unchanged.</summary>
|
||||
[Fact]
|
||||
public async Task A_function_file_read_on_libplctag_is_BadNotSupported_with_health_unchanged()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
const string host = "ab://192.0.2.1/";
|
||||
var driver = new AbLegacyDriver(new AbLegacyDriverOptions
|
||||
{
|
||||
Devices = [new AbLegacyDeviceOptions(host, AbLegacyPlcFamily.MicroLogix)],
|
||||
RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("Hr", host, "RTC:0.HR", AbLegacyDataType.Int)]),
|
||||
Timeout = TimeSpan.FromSeconds(1),
|
||||
Probe = new AbLegacyProbeOptions { Enabled = false },
|
||||
Transport = AbLegacyTransport.Libplctag,
|
||||
}, "drv-libplctag-function-file", new LibplctagLegacyTagFactory());
|
||||
await driver.InitializeAsync("{}", ct);
|
||||
var health = driver.GetHealth();
|
||||
|
||||
for (var i = 0; i < 3; i++)
|
||||
(await driver.ReadAsync(["Hr"], ct)).Single().StatusCode.ShouldBe(AbLegacyStatusMapper.BadNotSupported);
|
||||
|
||||
driver.GetHealth().ShouldBe(health);
|
||||
await driver.ShutdownAsync(ct);
|
||||
}
|
||||
#pragma warning restore OTOPCUA0001
|
||||
}
|
||||
Reference in New Issue
Block a user