@@ -34,8 +34,17 @@ public sealed record AbLegacyAddress(
|
||||
int? BitIndex,
|
||||
string? SubElement,
|
||||
AbLegacyAddress? IndirectFileSource = null,
|
||||
AbLegacyAddress? IndirectWordSource = null)
|
||||
AbLegacyAddress? IndirectWordSource = null,
|
||||
int? ArrayCount = null)
|
||||
{
|
||||
/// <summary>
|
||||
/// PR 7 — PCCC frame ceiling. A single SLC/PLC-5 PCCC read can return up to about 240
|
||||
/// bytes (~120 INT words / 60 DINTs / 60 floats). The parser caps <see cref="ArrayCount"/>
|
||||
/// at 120 so a misconfigured tag fails fast instead of bouncing off the wire as a fragmented
|
||||
/// multi-frame read.
|
||||
/// </summary>
|
||||
public const int MaxArrayCount = 120;
|
||||
|
||||
/// <summary>
|
||||
/// True when either the file number or the word number is sourced from another PCCC
|
||||
/// address evaluated at runtime (PLC-5 / SLC indirect addressing — <c>N7:[N7:0]</c> or
|
||||
@@ -66,6 +75,12 @@ public sealed record AbLegacyAddress(
|
||||
: WordNumber.ToString();
|
||||
|
||||
var wordPart = $"{filePart}:{wordSegment}";
|
||||
// PR 7 — emit libplctag's `[N]` array suffix when the parsed address carries an
|
||||
// ArrayCount. libplctag's PCCC text decoder treats `N7:0[10]` as "10 consecutive
|
||||
// words starting at N7:0"; the comma form (`N7:0,10`) is Rockwell-native and gets
|
||||
// canonicalised to bracket form here so the driver always hands libplctag a single
|
||||
// recognisable shape.
|
||||
if (ArrayCount is int n) wordPart += $"[{n}]";
|
||||
if (SubElement is not null) wordPart += $".{SubElement}";
|
||||
if (BitIndex is not null) wordPart += $"/{BitIndex}";
|
||||
return wordPart;
|
||||
@@ -173,6 +188,46 @@ public sealed record AbLegacyAddress(
|
||||
|
||||
var octalForIo = profile?.OctalIoAddressing == true && (letter == "I" || letter == "O");
|
||||
|
||||
// PR 7 — strip an optional array suffix from the trailing edge of the word part.
|
||||
// Two accepted forms: Rockwell-native `,N` (e.g. `N7:0,10`) and libplctag-native
|
||||
// `[N]` (e.g. `N7:0[10]`). Both resolve to the same ArrayCount. The bracket form
|
||||
// collides syntactically with the indirect-word form (`N7:[N7:0]`) — the
|
||||
// disambiguation is "leading bracket = indirect; trailing bracket after the
|
||||
// numeric word literal = array". A trailing `[N]` may also follow an indirect
|
||||
// word (`N7:[N7:0][10]`) — supported.
|
||||
int? arrayCount = null;
|
||||
|
||||
// Try comma form first — only meaningful when no leading-bracket indirect form is
|
||||
// present. Comma never appears in indirect-word source addresses (those use ':').
|
||||
var commaIdx = wordPart.LastIndexOf(',');
|
||||
if (commaIdx > 0 && wordPart[0] != '[')
|
||||
{
|
||||
var arrayText = wordPart[(commaIdx + 1)..];
|
||||
if (!int.TryParse(arrayText, out var ac) || ac < 1 || ac > MaxArrayCount) return null;
|
||||
arrayCount = ac;
|
||||
wordPart = wordPart[..commaIdx];
|
||||
}
|
||||
else if (wordPart.Length > 0 && wordPart[^1] == ']')
|
||||
{
|
||||
// Trailing `[N]` — only valid when there's already a primary word/indirect
|
||||
// segment in front of it. Walk back to the matching `[`.
|
||||
// Use top-level-aware index so a nested indirect like `[N7:0]` doesn't trip us.
|
||||
// We want the LAST top-level `[` whose body is a pure integer.
|
||||
var openIdx = MatchingOpenBracket(wordPart, wordPart.Length - 1);
|
||||
if (openIdx > 0)
|
||||
{
|
||||
var arrayText = wordPart[(openIdx + 1)..^1];
|
||||
if (int.TryParse(arrayText, out var ac))
|
||||
{
|
||||
if (ac < 1 || ac > MaxArrayCount) return null;
|
||||
arrayCount = ac;
|
||||
wordPart = wordPart[..openIdx];
|
||||
}
|
||||
// If the bracket body isn't a pure integer, leave wordPart alone — likely
|
||||
// an indirect-word source address (handled below) or malformed input.
|
||||
}
|
||||
}
|
||||
|
||||
// Word part: either a numeric literal (octal-aware for PLC-5 I:/O:) or a bracketed
|
||||
// indirect address.
|
||||
int word = 0;
|
||||
@@ -196,7 +251,38 @@ public sealed record AbLegacyAddress(
|
||||
bitIndex = bit;
|
||||
}
|
||||
|
||||
return new AbLegacyAddress(letter, fileNumber, word, bitIndex, subElement, indirectFile, indirectWord);
|
||||
// PR 7 — array tags can't combine with a bit suffix (`N7:0,10/3` is meaningless —
|
||||
// "the third bit of ten different words"?) or with a sub-element pull (`T4:0,5.ACC`
|
||||
// is also meaningless — the sub-element targets one timer's accumulator). The
|
||||
// libplctag PCCC layer would silently accept the combination; reject up-front so
|
||||
// the OPC UA client sees a clean parse failure rather than a wire-level surprise.
|
||||
if (arrayCount is not null)
|
||||
{
|
||||
if (bitIndex is not null) return null;
|
||||
if (subElement is not null) return null;
|
||||
}
|
||||
|
||||
return new AbLegacyAddress(letter, fileNumber, word, bitIndex, subElement, indirectFile, indirectWord, arrayCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find the index of the `[` that matches the `]` at <paramref name="closeIdx"/> in
|
||||
/// <paramref name="s"/>, accounting for nested brackets. Returns -1 if no match.
|
||||
/// </summary>
|
||||
private static int MatchingOpenBracket(string s, int closeIdx)
|
||||
{
|
||||
if (closeIdx < 0 || closeIdx >= s.Length || s[closeIdx] != ']') return -1;
|
||||
var depth = 1;
|
||||
for (var i = closeIdx - 1; i >= 0; i--)
|
||||
{
|
||||
if (s[i] == ']') depth++;
|
||||
else if (s[i] == '[')
|
||||
{
|
||||
depth--;
|
||||
if (depth == 0) return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user