29b566d51b
ParseAlarms took the 76-byte vendor layout whenever Length % 76 == 0, so a legacy 80-byte reply of exactly 19 records (1,520 B = 20 x 76) decoded as 20 garbage vendor records. The response block carries no record count (its 16-byte header is length/class/path/ command/rc/payload length), so the layout is chosen from what is known: the record count the request asked for (a reply never holds more), then each layout's field invariants on every record. For legacy records, the int32 type/axis/length must fit a short and the length must be at most 64. For vendor records, the i16 length must be at most 64. If both still fit, the layout whose declared lengths match the decoded text wins. Vendor stays the default when both readings are equally plausible, and unambiguous lengths are unchanged. Refs #740 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
97 lines
4.0 KiB
C#
97 lines
4.0 KiB
C#
using System.Buffers.Binary;
|
||
using System.Text;
|
||
using Shouldly;
|
||
using Xunit;
|
||
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Wire;
|
||
|
||
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests;
|
||
|
||
/// <summary>
|
||
/// #740 / R07:H20 — <c>ParseAlarms</c> picked the 76-byte vendor layout whenever <c>Length % 76 == 0</c>, so a
|
||
/// legacy 80-byte reply of exactly 19 records (1,520 B = 20 × 76) decoded as 20 garbage vendor records. The
|
||
/// response block carries no record count, so the layout is now chosen from the two things that are known: the
|
||
/// count the REQUEST asked for (a reply never holds more) and each layout's own field invariants.
|
||
/// </summary>
|
||
[Trait("Category", "Unit")]
|
||
public sealed class FocasAlarmLayoutTests
|
||
{
|
||
private static byte[] Legacy(params (int no, int type, int axis, string msg)[] records)
|
||
{
|
||
var buf = new byte[records.Length * 80];
|
||
for (var i = 0; i < records.Length; i++)
|
||
{
|
||
var o = i * 80;
|
||
var (no, type, axis, msg) = records[i];
|
||
BinaryPrimitives.WriteInt32BigEndian(buf.AsSpan(o, 4), no);
|
||
BinaryPrimitives.WriteInt32BigEndian(buf.AsSpan(o + 4, 4), type);
|
||
BinaryPrimitives.WriteInt32BigEndian(buf.AsSpan(o + 8, 4), axis);
|
||
BinaryPrimitives.WriteInt32BigEndian(buf.AsSpan(o + 12, 4), msg.Length);
|
||
Encoding.ASCII.GetBytes(msg).CopyTo(buf.AsSpan(o + 16));
|
||
}
|
||
return buf;
|
||
}
|
||
|
||
private static byte[] Vendor(params (int no, short type, short axis, string msg)[] records)
|
||
{
|
||
var buf = new byte[records.Length * 76];
|
||
for (var i = 0; i < records.Length; i++)
|
||
{
|
||
var o = i * 76;
|
||
var (no, type, axis, msg) = records[i];
|
||
BinaryPrimitives.WriteInt32BigEndian(buf.AsSpan(o, 4), no);
|
||
BinaryPrimitives.WriteInt16BigEndian(buf.AsSpan(o + 4, 2), type);
|
||
BinaryPrimitives.WriteInt16BigEndian(buf.AsSpan(o + 6, 2), axis);
|
||
BinaryPrimitives.WriteInt16BigEndian(buf.AsSpan(o + 10, 2), (short)msg.Length);
|
||
Encoding.ASCII.GetBytes(msg).CopyTo(buf.AsSpan(o + 12));
|
||
}
|
||
return buf;
|
||
}
|
||
|
||
private static (int, int, int, string)[] LegacySet(int n) =>
|
||
[.. Enumerable.Range(0, n).Select(i => (400 + i, 2 + (i % 3), 1 + (i % 4), $"SERVO ALARM {400 + i}"))];
|
||
|
||
private static (int, short, short, string)[] VendorSet(int n) =>
|
||
[.. Enumerable.Range(0, n).Select(i => (1000 + i, (short)(4 + (i % 3)), (short)(i % 2), $"OVERTRAVEL +{i}"))];
|
||
|
||
/// <summary>The ambiguous length: 19 legacy records decode as 19 legacy records, not 20 vendor ones.</summary>
|
||
[Fact]
|
||
public void Nineteen_legacy_records_decode_as_legacy()
|
||
{
|
||
var payload = Legacy(LegacySet(19));
|
||
payload.Length.ShouldBe(1520);
|
||
(payload.Length % 76).ShouldBe(0, "the length really is ambiguous");
|
||
|
||
var alarms = FocasWireClient.ParseAlarms(payload, 32);
|
||
|
||
alarms.Count.ShouldBe(19);
|
||
alarms[0].AlarmNumber.ShouldBe(400);
|
||
alarms[0].Message.ShouldBe("SERVO ALARM 400");
|
||
alarms[18].AlarmNumber.ShouldBe(418);
|
||
alarms[18].Axis.ShouldBe((short)(1 + (18 % 4)));
|
||
}
|
||
|
||
/// <summary>Positive control at the same length: 20 vendor records still decode as vendor.</summary>
|
||
[Fact]
|
||
public void Twenty_vendor_records_decode_as_vendor()
|
||
{
|
||
var payload = Vendor(VendorSet(20));
|
||
payload.Length.ShouldBe(1520);
|
||
|
||
var alarms = FocasWireClient.ParseAlarms(payload, 32);
|
||
|
||
alarms.Count.ShouldBe(20);
|
||
alarms[0].AlarmNumber.ShouldBe(1000);
|
||
alarms[19].Message.ShouldBe("OVERTRAVEL +19");
|
||
}
|
||
|
||
/// <summary>The unambiguous lengths are unchanged.</summary>
|
||
[Fact]
|
||
public void Unambiguous_lengths_are_unchanged()
|
||
{
|
||
FocasWireClient.ParseAlarms(Vendor(VendorSet(3)), 32).Count.ShouldBe(3);
|
||
FocasWireClient.ParseAlarms(Legacy(LegacySet(3)), 32).Count.ShouldBe(3);
|
||
FocasWireClient.ParseAlarms([], 32).ShouldBeEmpty();
|
||
Should.Throw<FocasWireException>(() => FocasWireClient.ParseAlarms(new byte[77], 32));
|
||
}
|
||
}
|