Auto: ablegacy-7 — array contiguous block addressing

Closes #250
This commit is contained in:
Joseph Doherty
2026-04-25 23:36:01 -04:00
parent 05528bf71c
commit c689ac58b1
14 changed files with 779 additions and 15 deletions

View File

@@ -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);