Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests/Pccc/PcccServicesTests.cs
T

204 lines
9.7 KiB
C#

using System.Buffers.Binary;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Cip;
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Pccc;
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Captures;
namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Pccc;
// ---- Step 1: hand-decode of the golden captures (the capture is the spec) ----
//
// slc500-read-n7.hex, request frame (SendRRData 0x6F, one CPF item 0xB2 = unconnected data):
// 4B 02 20 67 24 01 Execute PCCC, path size 2 words: class 0x67 inst 1 -- SENT BARE: no Unconnected
// Send (0x52) wrapper, no route bytes, not connected (no Forward Open)
// 07 3D F3 45 43 50 21 requestor id: length 7, vendor 0xF33D (LE 3D F3), serial 0x21504345
// 0F 00 99 87 A2 02 07 89 00 00 PCCC: cmd 0x0F, sts 0, TNS 0x8799, fnc 0xA2 (typed logical read),
// byteCount 2, file 7, type 0x89 (N), element 0, sub-element 0
// reply item: CB 00 00 00 (service 0x4B|0x80, status 0, no ext) | 07 3D F3 45 43 50 21 |
// 4F 00 99 87 (cmd 0x4F, sts 0, same TNS) | E1 10 -> N7:0 = 0x10E1 = 4321
// slc500-read-f8: ... A2 04 08 8A 00 00 (4 bytes, file 8, type 0x8A F) ; reply data 00 00 60 40 = 3.5f
// micrologix-read-n7: identical request shape; reply data A4 09 = 2468
// slc500-write-n7: 0F 00 tns AA 02 07 89 00 00 E1 10 (fnc 0xAA, data appended); reply has no data
// plc5-read-n7: PLC-5 does NOT use 0xA2/0x68. Captured PCCC = 0F 00 <tns> 01 | 00 00 | 01 00 | 06 07 00 | 02 :
// fnc 0x01 (range read), packet offset u16 0, total-trans u16 1, level mask 0x06 (levels file+element),
// file 7, element 0, then a single byte count 2. No file-type byte. Reply data 4D 05 = 1357.
// (The brief's fnc 0x68 / size-u16 shape is NOT what libplctag sends; the capture wins.)
public sealed class PcccServicesTests
{
private const ushort Vendor = 0xF33D;
private const uint Serial = 0x21504345;
private static byte[] Hex(string h) => Convert.FromHexString(h.Replace(" ", ""));
private static PcccAddress A(string s) => PcccAddress.TryParse(s)!;
[Fact]
public void ExecutePccc_wraps_with_requestor_id() =>
Convert.ToHexString(PcccServices.ExecutePccc(Vendor, Serial, [0xAA, 0xBB]))
.ShouldBe("4B0220672401" + "073DF3454350" + "21" + "AABB");
[Fact]
public void SlcTypedRead_N7_0() =>
PcccServices.SlcTypedRead(0x8799, A("N7:0"), 2).ShouldBe(Hex("0F00 9987 A2 02 07 89 00 00"));
[Fact]
public void SlcTypedRead_F8_3() =>
PcccServices.SlcTypedRead(0x0102, A("F8:3"), 4).ShouldBe(Hex("0F00 0201 A2 04 08 8A 03 00"));
[Fact]
public void SlcTypedWrite_N7_0() =>
PcccServices.SlcTypedWrite(0x8799, A("N7:0"), [0x2A, 0x00]).ShouldBe(Hex("0F00 9987 AA 02 07 89 00 00 2A00"));
[Fact]
public void Element_300_uses_FF_escape() =>
PcccServices.SlcTypedRead(1, A("N7:300"), 2).ShouldBe(Hex("0F00 0100 A2 02 07 89 FF2C01 00"));
[Fact]
public void File_300_uses_FF_escape() =>
PcccServices.SlcTypedRead(1, A("N300:0"), 2).ShouldBe(Hex("0F00 0100 A2 02 FF2C01 89 00 00"));
[Fact]
public void Value_255_uses_FF_escape_boundary() =>
PcccServices.SlcTypedRead(1, A("N7:255"), 2).ShouldBe(Hex("0F00 0100 A2 02 07 89 FFFF00 00"));
[Fact]
public void Timer_ACC_sub_element_is_word_2() =>
PcccServices.SlcTypedRead(1, A("T4:0.ACC"), 2).ShouldBe(Hex("0F00 0100 A2 02 04 86 00 02"));
[Fact]
public void Implicit_file_numbers_for_IOS()
{
PcccServices.SlcTypedRead(1, A("O:0"), 2).ShouldBe(Hex("0F00 0100 A2 02 00 8B 00 00"));
PcccServices.SlcTypedRead(1, A("I:1"), 2).ShouldBe(Hex("0F00 0100 A2 02 01 8C 01 00"));
PcccServices.SlcTypedRead(1, A("S:3"), 2).ShouldBe(Hex("0F00 0100 A2 02 02 84 03 00"));
}
[Fact]
public void ByteCount_236_ok_237_throws()
{
PcccServices.SlcTypedRead(1, A("N7:0"), 236)[5].ShouldBe((byte)236);
Should.Throw<AbTransportException>(() => PcccServices.SlcTypedRead(1, A("N7:0"), 237)).Status.ShouldBe(AbStatus.TooLarge);
PcccServices.SlcTypedWrite(1, A("N7:0"), new byte[236]).Length.ShouldBe(10 + 236); // 0F 00 tns2 fnc count file type elem sub + data
Should.Throw<AbTransportException>(() => PcccServices.SlcTypedWrite(1, A("N7:0"), new byte[237])).Status.ShouldBe(AbStatus.TooLarge);
PcccServices.Plc5RangeRead(1, A("N7:0"), 1, 236)[^1].ShouldBe((byte)236);
Should.Throw<AbTransportException>(() => PcccServices.Plc5RangeRead(1, A("N7:0"), 1, 237)).Status.ShouldBe(AbStatus.TooLarge);
}
[Fact]
public void Plc5RangeRead_N7_0_matches_captured_shape() =>
PcccServices.Plc5RangeRead(0xC473, A("N7:0"), 1, 2).ShouldBe(Hex("0F00 73C4 01 0000 0100 06 07 00 02"));
[Fact]
public void Plc5_write_is_not_shipped_and_flagged() => PcccServices.Plc5WriteVerified.ShouldBeFalse();
// ---- ParseReply ----
private static CipReply Reply(string hex, byte general = 0) =>
new(0x4B, general, [], Hex(hex));
private const string Id = "073DF3454350" + "21";
[Fact]
public void ParseReply_success_returns_data() =>
PcccServices.ParseReply(Reply(Id + "4F00 9987 E110"), 0x8799).ToArray().ShouldBe(Hex("E110"));
[Fact]
public void ParseReply_sts_0x10_is_Unsupported() =>
Should.Throw<AbTransportException>(() => PcccServices.ParseReply(Reply(Id + "4F10 9987"), 0x8799))
.Status.ShouldBe(AbStatus.Unsupported);
[Fact]
public void ParseReply_F0_ext_06_is_NotFound() =>
Should.Throw<AbTransportException>(() => PcccServices.ParseReply(Reply(Id + "4FF0 9987 06"), 0x8799))
.Status.ShouldBe(AbStatus.NotFound);
[Fact]
public void ParseReply_tns_mismatch_is_BadReply() =>
Should.Throw<AbTransportException>(() => PcccServices.ParseReply(Reply(Id + "4F00 9987 E110"), 0x1234))
.Status.ShouldBe(AbStatus.BadReply);
[Fact]
public void ParseReply_truncated_is_BadReply() =>
Should.Throw<AbTransportException>(() => PcccServices.ParseReply(Reply(Id + "4F00 99"), 0x8799))
.Status.ShouldBe(AbStatus.BadReply);
[Fact]
public void ParseReply_cip_error_status_throws() =>
Should.Throw<AbTransportException>(() => PcccServices.ParseReply(Reply("", 0x08), 1)).Status.ShouldBe(AbStatus.Unsupported);
[Fact]
public void ParseReply_wrong_command_byte_is_BadReply() =>
Should.Throw<AbTransportException>(() => PcccServices.ParseReply(Reply(Id + "0F00 9987 E110"), 0x8799))
.Status.ShouldBe(AbStatus.BadReply);
[Fact]
public void ParseReply_stale_error_reply_with_wrong_tns_is_BadReply() =>
Should.Throw<AbTransportException>(() => PcccServices.ParseReply(Reply(Id + "4F10 1111"), 0x8799))
.Status.ShouldBe(AbStatus.BadReply);
[Fact]
public void ParseReply_requestor_id_mismatch_is_BadReply()
{
var expected = Hex(Id);
PcccServices.ParseReply(Reply(Id + "4F00 9987 E110"), 0x8799, expected).Length.ShouldBe(2);
Should.Throw<AbTransportException>(() =>
PcccServices.ParseReply(Reply("073DF3454350" + "22" + "4F00 9987 E110"), 0x8799, expected))
.Status.ShouldBe(AbStatus.BadReply);
}
// ---- Capture parity ----
// The request is frame index 2 (after RegisterSession pair); its TNS is session-assigned, so we read it
// out of the capture and rebuild with that TNS (TNS is the only normalized field). Vendor/serial come
// from the captured requestor id.
private static (byte[] Request, byte[] Reply, ushort Tns) Exchange(string file, int requestIndex = 2)
{
var frames = CaptureFile.Load(file);
var req = CaptureFile.CipPayload(frames[requestIndex]);
var rep = CaptureFile.CipPayload(frames[requestIndex + 1]);
// 6 bytes of 4B-header + 7 requestor id + 0F 00 -> TNS at offset 15.
var tns = BinaryPrimitives.ReadUInt16LittleEndian(req.AsSpan(15, 2));
return (req, rep, tns);
}
private static byte[] Wrap(byte[] pccc) => PcccServices.ExecutePccc(Vendor, Serial, pccc);
[Theory]
[InlineData("slc500-read-n7.hex", "N7:0", 2, 4321)]
[InlineData("micrologix-read-n7.hex", "N7:0", 2, 2468)]
public void Capture_read_n7_request_parity_and_value(string file, string addr, int bytes, int expected)
{
var (req, rep, tns) = Exchange(file);
Wrap(PcccServices.SlcTypedRead(tns, A(addr), bytes)).ShouldBe(req);
var data = PcccServices.ParseReply(CipReply.Parse(rep), tns).ToArray();
data.Length.ShouldBe(2);
BinaryPrimitives.ReadInt16LittleEndian(data).ShouldBe((short)expected);
}
[Fact]
public void Capture_slc500_read_f8_request_parity_and_value()
{
var (req, rep, tns) = Exchange("slc500-read-f8.hex");
Wrap(PcccServices.SlcTypedRead(tns, A("F8:0"), 4)).ShouldBe(req);
var data = PcccServices.ParseReply(CipReply.Parse(rep), tns).ToArray();
data.Length.ShouldBe(4);
BinaryPrimitives.ReadSingleLittleEndian(data).ShouldBe(3.5f);
}
[Fact]
public void Capture_plc5_read_n7_request_parity_and_value()
{
var (req, rep, tns) = Exchange("plc5-read-n7.hex");
Wrap(PcccServices.Plc5RangeRead(tns, A("N7:0"), 1, 2)).ShouldBe(req);
var data = PcccServices.ParseReply(CipReply.Parse(rep), tns).ToArray();
data.Length.ShouldBe(2);
BinaryPrimitives.ReadInt16LittleEndian(data).ShouldBe((short)1357);
}
[Fact]
public void Capture_slc500_write_n7_request_parity_and_empty_reply()
{
// frame 2 = initialize read, frame 4 = write of 4321
var (req, rep, tns) = Exchange("slc500-write-n7.hex", 4);
Wrap(PcccServices.SlcTypedWrite(tns, A("N7:0"), [0xE1, 0x10])).ShouldBe(req);
PcccServices.ParseReply(CipReply.Parse(rep), tns).Length.ShouldBe(0);
}
}