fix(ablegacy): encode array writes at the data file's element width
Review r0 I3. The array encoder used the AUTHORED dataType's width while both runtimes size their buffer from the FILE: an Int array on F8 wrote four shorts into a 16-byte float buffer and sent it all (garbage neighbours, answered Good); Long/Float on N overran it. - The write path resolves the element width from the data file the address names (N/B/A/I/O/S 16-bit word, L 32-bit, F float) and encodes at THAT width. - An authored type that does not match its file is refused BadTypeMismatch before any runtime exists (warned once, no health write). - The same rule (AbLegacyTagDefinitionFactory.ArrayFileMismatch) backs an Inspect deploy warning and the AdminUI model's Validate error; FromTagConfig still accepts the tag so persisted configs keep loading. - M6: the skipped F1 parity test gains the shapes the write path refuses (/bit, .sub, indirect, type/file mismatch, PLC-5 native). Tests: one case per file type, a mismatch per pairing with nothing created, and a positive control. Refs #738 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
+51
@@ -177,6 +177,9 @@ public static class AbLegacyTagDefinitionFactory
|
||||
{
|
||||
if (AbLegacyAddress.Validate(accepted.Address, family) is { } addressError)
|
||||
warnings.Add($"AbLegacy tag address: {addressError} The driver reads this tag as BadNodeIdUnknown.");
|
||||
else if (accepted.ArrayLength is not null
|
||||
&& ArrayFileMismatch(accepted.DataType, accepted.Address) is { } widthError)
|
||||
warnings.Add(widthError);
|
||||
return warnings;
|
||||
}
|
||||
|
||||
@@ -252,6 +255,54 @@ public static class AbLegacyTagDefinitionFactory
|
||||
$"AbLegacy {type} is not supported as an array element (String, Timer, Counter and Control elements are "
|
||||
+ "structured); author a scalar tag per element instead. The driver skips this tag and it reads BadNodeIdUnknown.";
|
||||
|
||||
/// <summary>
|
||||
/// The element width an array on <paramref name="address"/>'s data file has, as the data type that decodes /
|
||||
/// encodes it: N, B, A and the I/O/S word files are 16-bit words (<see cref="AbLegacyDataType.Int"/>), L is
|
||||
/// 32-bit (<see cref="AbLegacyDataType.Long"/>), F is IEEE float (<see cref="AbLegacyDataType.Float"/>).
|
||||
/// <see langword="null"/> for a file with no flat array layout (ST, T, C, R, function files) or an address that
|
||||
/// does not parse.
|
||||
/// </summary>
|
||||
/// <param name="address">The authored address.</param>
|
||||
/// <returns>The file's element type, or <see langword="null"/>.</returns>
|
||||
public static AbLegacyDataType? ArrayFileElementType(string address) =>
|
||||
AbLegacyAddress.TryParse(address) is { } a ? ArrayFileElementType(a) : null;
|
||||
|
||||
/// <inheritdoc cref="ArrayFileElementType(string)"/>
|
||||
public static AbLegacyDataType? ArrayFileElementType(AbLegacyAddress a) => a.FileLetter switch
|
||||
{
|
||||
"N" or "B" or "A" or "I" or "O" or "S" => AbLegacyDataType.Int,
|
||||
"L" => AbLegacyDataType.Long,
|
||||
"F" => AbLegacyDataType.Float,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Why an ARRAY authored as <paramref name="type"/> on <paramref name="address"/> does not match its data
|
||||
/// file's element width (review 07 r0 I3), or <see langword="null"/> when it does. Int / AnalogInt need a 16-bit
|
||||
/// word file (N/B/A/I/O/S), Long an L file, Float an F file; Bit arrays are bit-packed words and fit any 16-bit
|
||||
/// word file. A mismatch reads the file at the wrong width (garbage) and the driver refuses to WRITE it
|
||||
/// (<c>BadTypeMismatch</c>), because encoding at the authored width would corrupt the neighbouring elements.
|
||||
/// </summary>
|
||||
/// <param name="type">The authored element data type.</param>
|
||||
/// <param name="address">The authored address.</param>
|
||||
/// <returns>The mismatch message, or <see langword="null"/>.</returns>
|
||||
public static string? ArrayFileMismatch(AbLegacyDataType type, string address)
|
||||
{
|
||||
if (!IsArrayElementTypeSupported(type)) return null; // structured arrays have their own refusal
|
||||
if (AbLegacyAddress.TryParse(address) is not { } a || a.IsIndirect) return null;
|
||||
var file = ArrayFileElementType(a);
|
||||
var ok = type switch
|
||||
{
|
||||
AbLegacyDataType.Int or AbLegacyDataType.AnalogInt or AbLegacyDataType.Bit => file == AbLegacyDataType.Int,
|
||||
_ => file == type,
|
||||
};
|
||||
return ok
|
||||
? null
|
||||
: $"AbLegacy {type} array on '{address}' does not match the {a.FileLetter} file's element width "
|
||||
+ "(Int/AnalogInt/Bit need an N/B/A word file, Long an L file, Float an F file): reads decode the wrong width "
|
||||
+ "and writes are refused BadTypeMismatch.";
|
||||
}
|
||||
|
||||
private static int ReadInt(JsonElement o, string name)
|
||||
=> o.TryGetProperty(name, out var e) && e.ValueKind == JsonValueKind.Number
|
||||
&& e.TryGetInt32(out var v) ? v : 0;
|
||||
|
||||
@@ -885,12 +885,29 @@ public sealed partial class AbLegacyDriver : IDriver, IReadable, IWritable, ITag
|
||||
|
||||
// Array writes (review 07 U10): convert and length-check BEFORE any runtime exists, then encode every
|
||||
// element. A bit / sub-element array has no flat encoding.
|
||||
// The encode width is the DATA FILE's element width (review 07 r0 I3), never the authored type's: an
|
||||
// authored type that does not match its file is refused BadTypeMismatch before anything is encoded.
|
||||
Array? arrayValue = null;
|
||||
AbLegacyDataType arrayFileType = default;
|
||||
if (IsArrayTag(def))
|
||||
{
|
||||
var conversion = parsed.BitIndex is not null || parsed.SubElement is not null
|
||||
? AbLegacyStatusMapper.BadNotSupported
|
||||
: AbLegacyArrayWriteEncoder.TryConvert(def.DataType, w.Value, EffectiveArrayLength(def), out arrayValue);
|
||||
var fileType = AbLegacyTagDefinitionFactory.ArrayFileElementType(parsed);
|
||||
var widthRefusal = AbLegacyTagDefinitionFactory.ArrayFileMismatch(def.DataType, def.Address)
|
||||
?? (fileType is null ? $"{def.Address} has no flat array layout." : null);
|
||||
uint conversion;
|
||||
if (parsed.BitIndex is not null || parsed.SubElement is not null || def.DataType == AbLegacyDataType.Bit)
|
||||
conversion = AbLegacyStatusMapper.BadNotSupported;
|
||||
else if (widthRefusal is not null || fileType is null)
|
||||
{
|
||||
WarnRefusalOnce(device, def, "write", widthRefusal ?? "no flat array layout", AbLegacyStatusMapper.BadTypeMismatch);
|
||||
results[i] = new WriteResult(AbLegacyStatusMapper.BadTypeMismatch);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
arrayFileType = fileType.Value;
|
||||
conversion = AbLegacyArrayWriteEncoder.TryConvert(arrayFileType, w.Value, EffectiveArrayLength(def), out arrayValue);
|
||||
}
|
||||
if (conversion != AbLegacyStatusMapper.Good)
|
||||
{
|
||||
if (conversion == AbLegacyStatusMapper.BadNotSupported)
|
||||
@@ -925,7 +942,7 @@ public sealed partial class AbLegacyDriver : IDriver, IReadable, IWritable, ITag
|
||||
await opLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (arrayValue is not null) runtime.EncodeArray(def.DataType, arrayValue);
|
||||
if (arrayValue is not null) runtime.EncodeArray(arrayFileType, arrayValue);
|
||||
else runtime.EncodeValue(def.DataType, parsed.BitIndex, w.Value);
|
||||
await runtime.WriteAsync(cancellationToken).ConfigureAwait(false);
|
||||
status = runtime.GetStatus();
|
||||
|
||||
@@ -92,6 +92,11 @@ public sealed class AbLegacyTagConfigModel
|
||||
if (isArray && arrayLength >= 1 && !AbLegacyTagDefinitionFactory.IsArrayElementTypeSupported(DataType))
|
||||
return AbLegacyTagDefinitionFactory.ArrayElementTypeError(DataType);
|
||||
|
||||
// An array must match its data file's element width (review 07 r0 I3): the driver refuses to write a
|
||||
// mismatched one, and reads it at the wrong width.
|
||||
if (isArray && arrayLength >= 1 && AbLegacyTagDefinitionFactory.ArrayFileMismatch(DataType, Address.Trim()) is { } widthError)
|
||||
return widthError;
|
||||
|
||||
// Parse-gate backstop: the checks above mirror the reader's whole rejection surface today, so this is
|
||||
// expected to be silent for AbLegacy. It is here so a future reader rule cannot become a gap.
|
||||
return TagConfigParseGate.Describe(
|
||||
|
||||
Reference in New Issue
Block a user