@@ -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>
|
||||
|
||||
@@ -141,6 +141,25 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
|
||||
}
|
||||
|
||||
var parsed = AbLegacyAddress.TryParse(def.Address, device.Options.PlcFamily);
|
||||
// PR 7 — array contiguous block. Decode N consecutive elements via the runtime's
|
||||
// per-index accessor and box the result as a typed .NET array. The parser has
|
||||
// already rejected array+bit and array+sub-element combinations, so the array
|
||||
// path can ignore the bit/sub-element decoders entirely.
|
||||
int arrayCount;
|
||||
if (parsed is not null && (def.ArrayLength is not null || (parsed.ArrayCount ?? 1) > 1))
|
||||
{
|
||||
arrayCount = ResolveElementCount(def, parsed);
|
||||
}
|
||||
else arrayCount = 1;
|
||||
|
||||
if (arrayCount > 1)
|
||||
{
|
||||
var arr = DecodeArrayAs(runtime, def.DataType, arrayCount);
|
||||
results[i] = new DataValueSnapshot(arr, AbLegacyStatusMapper.Good, now, now);
|
||||
_health = new DriverHealth(DriverState.Healthy, now, null);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Timer/Counter/Control status bits route through GetBit at the parent-word
|
||||
// address — translate the .DN/.EN/etc. sub-element to its standard bit position
|
||||
// and pass it down to the runtime as a synthetic bitIndex.
|
||||
@@ -275,11 +294,16 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
|
||||
tag.DataType, parsed?.SubElement);
|
||||
var plcSetBit = AbLegacyDataTypeExtensions.IsPlcSetStatusBit(
|
||||
tag.DataType, parsed?.SubElement);
|
||||
// PR 7 — array contiguous-block tags advertise IsArray + ArrayDim so the OPC UA
|
||||
// generic node-manager builds a 1-D array variable. ArrayLength on the tag
|
||||
// definition wins over the parsed `,N` / `[N]` suffix; both null = scalar.
|
||||
var arrayLen = tag.ArrayLength
|
||||
?? (parsed?.ArrayCount is int n && n > 1 ? n : (int?)null);
|
||||
deviceFolder.Variable(tag.Name, tag.Name, new DriverAttributeInfo(
|
||||
FullName: tag.Name,
|
||||
DriverDataType: effectiveType,
|
||||
IsArray: false,
|
||||
ArrayDim: null,
|
||||
IsArray: arrayLen is int al && al > 1,
|
||||
ArrayDim: arrayLen is int al2 && al2 > 1 ? (uint)al2 : null,
|
||||
SecurityClass: tag.Writable && !plcSetBit
|
||||
? SecurityClassification.Operate
|
||||
: SecurityClassification.ViewOnly,
|
||||
@@ -454,13 +478,24 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
|
||||
throw new NotSupportedException(
|
||||
$"AbLegacy tag '{def.Name}' uses indirect addressing ('{def.Address}'); runtime resolution is not yet implemented.");
|
||||
|
||||
// PR 7 — resolve the effective array length: explicit ArrayLength override on the tag
|
||||
// definition wins over the parsed `,N` / `[N]` suffix. ElementCount of 1 means
|
||||
// single-element scalar (libplctag's default); >1 triggers the contiguous-block path.
|
||||
var elementCount = ResolveElementCount(def, parsed);
|
||||
// Drop the parsed array suffix from the libplctag tag name when ArrayLength overrides
|
||||
// it — libplctag would otherwise read the parsed length, not the override.
|
||||
var tagName = (def.ArrayLength is int && parsed.ArrayCount is not null)
|
||||
? (parsed with { ArrayCount = null }).ToLibplctagName()
|
||||
: parsed.ToLibplctagName();
|
||||
|
||||
var runtime = _tagFactory.Create(new AbLegacyTagCreateParams(
|
||||
Gateway: device.ParsedAddress.Gateway,
|
||||
Port: device.ParsedAddress.Port,
|
||||
CipPath: device.ParsedAddress.CipPath,
|
||||
LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute,
|
||||
TagName: parsed.ToLibplctagName(),
|
||||
Timeout: _options.Timeout));
|
||||
TagName: tagName,
|
||||
Timeout: _options.Timeout,
|
||||
ElementCount: elementCount));
|
||||
try
|
||||
{
|
||||
await runtime.InitializeAsync(ct).ConfigureAwait(false);
|
||||
@@ -474,6 +509,54 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
|
||||
return runtime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PR 7 — pull <paramref name="elementCount"/> consecutive elements from a runtime that
|
||||
/// just completed a single contiguous-block read. Element type drives both the .NET
|
||||
/// array shape (Int32[] / Single[] / Boolean[]) and the per-index decoder routing.
|
||||
/// </summary>
|
||||
private static object DecodeArrayAs(IAbLegacyTagRuntime runtime, AbLegacyDataType type, int elementCount)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
AbLegacyDataType.Bit => BuildArray<bool>(runtime, type, elementCount),
|
||||
AbLegacyDataType.Int or AbLegacyDataType.AnalogInt => BuildArray<int>(runtime, type, elementCount),
|
||||
AbLegacyDataType.Long => BuildArray<int>(runtime, type, elementCount),
|
||||
AbLegacyDataType.Float => BuildArray<float>(runtime, type, elementCount),
|
||||
_ => throw new NotSupportedException(
|
||||
$"AbLegacyDataType {type} is not supported in array contiguous-block reads."),
|
||||
};
|
||||
}
|
||||
|
||||
private static T[] BuildArray<T>(IAbLegacyTagRuntime runtime, AbLegacyDataType type, int n)
|
||||
{
|
||||
var arr = new T[n];
|
||||
for (var i = 0; i < n; i++)
|
||||
{
|
||||
var element = runtime.DecodeArrayElement(type, i);
|
||||
arr[i] = (T)Convert.ChangeType(element!, typeof(T))!;
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PR 7 — resolve the effective array element count for a tag. Explicit
|
||||
/// <see cref="AbLegacyTagDefinition.ArrayLength"/> on the tag definition wins; otherwise
|
||||
/// the parsed <see cref="AbLegacyAddress.ArrayCount"/> from the address suffix is used;
|
||||
/// otherwise 1 (scalar). Validates the override against the same PCCC frame ceiling
|
||||
/// enforced by the parser so config-overrides can't bypass the limit.
|
||||
/// </summary>
|
||||
internal static int ResolveElementCount(AbLegacyTagDefinition def, AbLegacyAddress parsed)
|
||||
{
|
||||
if (def.ArrayLength is int n)
|
||||
{
|
||||
if (n < 1 || n > AbLegacyAddress.MaxArrayCount)
|
||||
throw new InvalidOperationException(
|
||||
$"AbLegacy tag '{def.Name}' has ArrayLength {n}; expected 1..{AbLegacyAddress.MaxArrayCount}.");
|
||||
return n;
|
||||
}
|
||||
return parsed.ArrayCount ?? 1;
|
||||
}
|
||||
|
||||
public void Dispose() => DisposeAsync().AsTask().GetAwaiter().GetResult();
|
||||
public async ValueTask DisposeAsync() => await ShutdownAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
|
||||
@@ -51,7 +51,8 @@ public static class AbLegacyDriverFactoryExtensions
|
||||
DataType: ParseEnum<AbLegacyDataType>(t.DataType, driverInstanceId, "DataType",
|
||||
tagName: t.Name),
|
||||
Writable: t.Writable ?? true,
|
||||
WriteIdempotent: t.WriteIdempotent ?? false))]
|
||||
WriteIdempotent: t.WriteIdempotent ?? false,
|
||||
ArrayLength: t.ArrayLength))]
|
||||
: [],
|
||||
Probe = new AbLegacyProbeOptions
|
||||
{
|
||||
@@ -112,6 +113,11 @@ public static class AbLegacyDriverFactoryExtensions
|
||||
public string? DataType { get; init; }
|
||||
public bool? Writable { get; init; }
|
||||
public bool? WriteIdempotent { get; init; }
|
||||
/// <summary>
|
||||
/// PR 7 — optional override for the parsed array suffix. When set and > 1 the
|
||||
/// driver issues a single contiguous PCCC block read for N elements.
|
||||
/// </summary>
|
||||
public int? ArrayLength { get; init; }
|
||||
}
|
||||
|
||||
internal sealed class AbLegacyProbeDto
|
||||
|
||||
@@ -31,7 +31,8 @@ public sealed record AbLegacyTagDefinition(
|
||||
string Address,
|
||||
AbLegacyDataType DataType,
|
||||
bool Writable = true,
|
||||
bool WriteIdempotent = false);
|
||||
bool WriteIdempotent = false,
|
||||
int? ArrayLength = null);
|
||||
|
||||
public sealed class AbLegacyProbeOptions
|
||||
{
|
||||
|
||||
@@ -13,6 +13,16 @@ public interface IAbLegacyTagRuntime : IDisposable
|
||||
int GetStatus();
|
||||
object? DecodeValue(AbLegacyDataType type, int? bitIndex);
|
||||
void EncodeValue(AbLegacyDataType type, int? bitIndex, object? value);
|
||||
|
||||
/// <summary>
|
||||
/// PR 7 — decode element <paramref name="elementIndex"/> of an N-element contiguous
|
||||
/// block read. Implementations call the same per-element accessors used by
|
||||
/// <see cref="DecodeValue"/> at offset <c>elementIndex × elementBytes</c>. Default
|
||||
/// implementation throws so existing fakes that don't override remain explicit.
|
||||
/// </summary>
|
||||
object? DecodeArrayElement(AbLegacyDataType type, int elementIndex)
|
||||
=> throw new NotSupportedException(
|
||||
"Array decoding requires an IAbLegacyTagRuntime that overrides DecodeArrayElement.");
|
||||
}
|
||||
|
||||
public interface IAbLegacyTagFactory
|
||||
@@ -26,4 +36,5 @@ public sealed record AbLegacyTagCreateParams(
|
||||
string CipPath,
|
||||
string LibplctagPlcAttribute,
|
||||
string TagName,
|
||||
TimeSpan Timeout);
|
||||
TimeSpan Timeout,
|
||||
int ElementCount = 1);
|
||||
|
||||
@@ -32,6 +32,11 @@ internal sealed class LibplctagLegacyTagRuntime : IAbLegacyTagRuntime
|
||||
Name = p.TagName,
|
||||
Timeout = p.Timeout,
|
||||
};
|
||||
// PR 7 — array contiguous-block reads. Setting ElementCount tells libplctag to allocate
|
||||
// a buffer covering N consecutive PCCC words (one frame, up to ~120 elements). The
|
||||
// driver decodes element-by-element through DecodeArrayElement after a single ReadAsync.
|
||||
if (p.ElementCount > 1)
|
||||
_tag.ElementCount = p.ElementCount;
|
||||
}
|
||||
|
||||
public Task InitializeAsync(CancellationToken cancellationToken) => _tag.InitializeAsync(cancellationToken);
|
||||
@@ -127,6 +132,32 @@ internal sealed class LibplctagLegacyTagRuntime : IAbLegacyTagRuntime
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PR 7 — decode element <paramref name="elementIndex"/> of an N-element contiguous
|
||||
/// PCCC block read. Element width is fixed per data type: Int / AnalogInt / Bit-as-word
|
||||
/// are 16-bit (2 bytes/element), Long / Float are 32-bit (4 bytes/element). Mirrors the
|
||||
/// non-array decoder shape but at byte offset <c>elementIndex × elementBytes</c>.
|
||||
/// </summary>
|
||||
public object? DecodeArrayElement(AbLegacyDataType type, int elementIndex)
|
||||
{
|
||||
if (elementIndex < 0) throw new ArgumentOutOfRangeException(nameof(elementIndex));
|
||||
return type switch
|
||||
{
|
||||
// Bit / N-array reads — Rockwell convention is one BOOL per word (e.g. `B3:0,10`
|
||||
// returns 10 BOOLs, not 160 individual bits). Each word is non-zero → true.
|
||||
AbLegacyDataType.Bit => _tag.GetInt16(elementIndex * 2) != 0,
|
||||
AbLegacyDataType.Int or AbLegacyDataType.AnalogInt => (int)_tag.GetInt16(elementIndex * 2),
|
||||
AbLegacyDataType.Long => _tag.GetInt32(elementIndex * 4),
|
||||
AbLegacyDataType.Float => _tag.GetFloat32(elementIndex * 4),
|
||||
// String + element types are out-of-scope for PR 7 array reads — the PCCC layer's
|
||||
// 240-byte frame ceiling means an ST array would only fit a couple of strings, and
|
||||
// sub-element arrays (`T4:0,5.ACC`) are rejected at parse time. Surface a clear
|
||||
// error if the driver mis-routes us here.
|
||||
_ => throw new NotSupportedException(
|
||||
$"AbLegacyDataType {type} cannot be decoded as a contiguous array element."),
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose() => _tag.Dispose();
|
||||
|
||||
private static PlcType MapPlcType(string attribute) => attribute switch
|
||||
|
||||
Reference in New Issue
Block a user