fix(ablegacy): translate S/O/I and R status bits for libplctag's PCCC parser (#659)
libplctag 2.6.0 (bundled by libplctag 1.5.2 / NativeImport 1.0.41) pccc.c needs an explicit file number (parse_pccc_file_num; S is only the status file when a digit follows) and has no R-file status-bit mnemonics (sub_element_lookup: con/len/pos only; a /bit is refused on a 6-byte element). So the canonical S:0 (the default probe), O:0/3, I:0/0 and R6:0.DN failed with BadCommunicationError on a real MicroLogix 1400 while native read them. LibplctagLegacyName (libplctag adapter only) now emits S2:/O0:/I1: (the native encoder's implicit file numbers) and R6:0.CON/<bit> for status bits libplctag lacks (R EN/EU/DN/EM/ER/UL/IN/FD, C UA), bit numbers taken from the native table. The authored grammar is unchanged. Unit tests pin the translation per family and check, through a model of the pccc.c parser, that the translated name addresses exactly what the native encoder does (46 fail without the fix). Live: Z-33323 idle, reads only - libplctag now equals native for S/O/I/R status bits; controls unchanged. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Pccc;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
|
||||
|
||||
/// <summary>
|
||||
/// Translates a canonical AbLegacy address (<see cref="AbLegacyAddress.ToLibplctagName"/> — what operators
|
||||
/// author and the native transport parses) into the <c>name=</c> form libplctag's PCCC parser accepts, so both
|
||||
/// transports address the same file type / file number / element / sub-element / bit (Gitea #659).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>libplctag 2.6.0 (<c>libplctag.NativeImport</c> 1.0.41, bundled by the <c>libplctag</c> 1.5.2 package),
|
||||
/// <c>src/protocols/ab/pccc.c</c>, has two gaps the canonical grammar falls into — both proven on a real
|
||||
/// MicroLogix 1400 (Z-33323, 2026-10-09), where the untranslated names failed while native read them:</para>
|
||||
/// <list type="bullet">
|
||||
/// <item><b>No implicit file number.</b> <c>parse_pccc_file_num</c> requires a digit after the file letter,
|
||||
/// and <c>parse_pccc_file_type</c> only recognises <c>S</c> as the status file when a digit follows it. So
|
||||
/// <c>S:0</c>, <c>O:0/3</c>, <c>I:0/0</c> fail to parse. They are emitted with the PCCC default file numbers
|
||||
/// the native encoder uses (<see cref="PcccFileType.ImplicitFileNumber"/>): <c>O0</c>, <c>I1</c>, <c>S2</c>
|
||||
/// — the same numbers on SLC 500, MicroLogix and PLC-5.</item>
|
||||
/// <item><b>Control status bits are missing from its sub-element table.</b> <c>sub_element_lookup</c> lists
|
||||
/// only <c>con</c>/<c>len</c>/<c>pos</c> for R files (and no <c>ua</c> for counters), and a bare
|
||||
/// <c>R6:0/13</c> is refused because <c>parse_pccc_bit_num</c> allows a bit only on a 2-byte element. A
|
||||
/// status bit libplctag does not know is therefore emitted as control word + bit, <c>R6:0.CON/13</c>, with
|
||||
/// the bit number taken from the native transport's own table
|
||||
/// (<see cref="PcccAddress.WithSubElementName"/>) so the two transports cannot disagree.</item>
|
||||
/// </list>
|
||||
/// <para>Everything else passes through unchanged — including names libplctag already reads correctly
|
||||
/// (<c>T4:0.DN</c>, <c>C5:0.ACC</c>, <c>R6:0.LEN</c>), and names this translator cannot parse (function files,
|
||||
/// indirect forms, unknown sub-elements), which keep failing in libplctag exactly as before.</para>
|
||||
/// </remarks>
|
||||
internal static class LibplctagLegacyName
|
||||
{
|
||||
/// <summary>The sub-element mnemonics libplctag 2.6.0's <c>sub_element_lookup</c> table knows, per file letter.</summary>
|
||||
private static bool LibplctagKnowsSubElement(string letter, string sub) => (letter, sub) switch
|
||||
{
|
||||
("T", "CON" or "EN" or "TT" or "DN" or "PRE" or "ACC") => true,
|
||||
("C", "CON" or "CU" or "CD" or "DN" or "OV" or "UN" or "PRE" or "ACC") => true,
|
||||
("R", "CON" or "LEN" or "POS") => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
/// <summary>Returns the libplctag tag name for a canonical AbLegacy address.</summary>
|
||||
/// <param name="canonical">The canonical (decimal-normalised) address, e.g. <c>S:0</c> or <c>R6:0.DN</c>.</param>
|
||||
/// <returns>The libplctag <c>name=</c> value, e.g. <c>S2:0</c> or <c>R6:0.CON/13</c>.</returns>
|
||||
public static string Translate(string canonical)
|
||||
{
|
||||
// Family-neutral parse is correct: the driver hands the runtime AbLegacyAddress.ToLibplctagName(), already
|
||||
// decimal-normalised (PLC-5 octal I/O included) — the same reasoning as NativeAbLegacyTagRuntime.ToPcccAddress.
|
||||
if (AbLegacyAddress.TryParse(canonical) is not { IsIndirect: false } a) return canonical;
|
||||
|
||||
var fileNumber = a.FileNumber ?? PcccFileType.ImplicitFileNumber(a.FileLetter);
|
||||
if (fileNumber is null) return canonical;
|
||||
|
||||
var name = $"{a.FileLetter}{fileNumber}:{a.WordNumber}";
|
||||
if (a.SubElement is { } sub)
|
||||
{
|
||||
if (LibplctagKnowsSubElement(a.FileLetter, sub))
|
||||
{
|
||||
name += $".{sub}";
|
||||
}
|
||||
else
|
||||
{
|
||||
PcccAddress native;
|
||||
try
|
||||
{
|
||||
native = PcccAddress.WithSubElementName(a.FileLetter, fileNumber, a.WordNumber, sub);
|
||||
}
|
||||
catch (AbTransportException)
|
||||
{
|
||||
return canonical; // a mnemonic neither transport knows; libplctag rejects it as before
|
||||
}
|
||||
|
||||
// Only status bits live in the control word; a word sub-element libplctag lacks has no bit form.
|
||||
if (native is not { SubElement: 0, Bit: int bit }) return canonical;
|
||||
name += $".CON/{bit}";
|
||||
}
|
||||
}
|
||||
|
||||
if (a.BitIndex is int b) name += $"/{b}";
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,9 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
|
||||
/// <summary>
|
||||
/// Default libplctag-backed <see cref="IAbLegacyTagRuntime"/>. Uses <c>ab_pccc</c> protocol
|
||||
/// on top of EtherNet/IP — libplctag's PCCC layer handles the file-letter + word + bit +
|
||||
/// sub-element decoding internally, so our wrapper just has to forward the atomic type to
|
||||
/// the right Get/Set call.
|
||||
/// sub-element decoding internally, once <see cref="LibplctagLegacyName"/> has put the canonical
|
||||
/// address in the form its parser accepts; our wrapper then forwards the atomic type to the
|
||||
/// right Get/Set call.
|
||||
/// </summary>
|
||||
internal sealed class LibplctagLegacyTagRuntime : IAbLegacyTagRuntime
|
||||
{
|
||||
@@ -41,7 +42,9 @@ internal sealed class LibplctagLegacyTagRuntime : IAbLegacyTagRuntime
|
||||
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,
|
||||
// libplctag's PCCC parser needs explicit O0/I1/S2 file numbers and has no R-file status-bit mnemonics;
|
||||
// translate so it addresses what the native transport does (#659).
|
||||
Name = LibplctagLegacyName.Translate(p.TagName),
|
||||
Timeout = p.Timeout,
|
||||
ElementCount = p.ElementCount > 1 ? p.ElementCount : null,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user