using System.Text; 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.Logix; using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Captures; namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Logix; // CAPTURE DECISION: libplctag sends Read Tag FRAGMENTED (0x52, offset 0) for plain reads, connected, // with no MSP and no Unconnected Send wrapper. The brief's "ReadTag 0x4C" comparison is therefore made // against ReadTagFragmented(path, 1, 0). Replies are service 0xD2 (0x52|0x80). public sealed class LogixServicesTests { private static byte[] Int32Le(int v) { var a = new byte[4]; BinaryPrimitives.WriteInt32LittleEndian(a, v); return a; } [Fact] public void Error_read_reply_surfaces_status_from_capture() => Should.Throw(() => LogixServices.ParseReadReply(Rep("logix-read-unknown-tag.hex"))) .Status.ShouldBe(AbStatus.NotFound); private static byte[] Req(string file, int i = 0) => CaptureFile.CipPayload(CaptureFile.Load(file).Where(f => f.FromClient).ElementAt(i)); private static CipReply Rep(string file, int i = 0) => CipReply.Parse(CaptureFile.CipPayload(CaptureFile.Load(file).Where(f => !f.FromClient).ElementAt(i))); [Fact] public void Read_request_matches_capture() => LogixServices.ReadTagFragmented(CipPath.EncodeSymbolic("TestDINT"), 1, 0).ShouldBe(Req("logix-read-dint.hex")); [Fact] public void Read_array_request_matches_capture() => LogixServices.ReadTagFragmented(CipPath.EncodeSymbolic("TestArray"), 16, 0).ShouldBe(Req("logix-read-dint-array16.hex")); [Fact] public void Read_dint_value() { var (type, handle, data) = LogixServices.ParseReadReply(Rep("logix-read-dint.hex")); type.ShouldBe(LogixTypeCode.DInt); handle.ShouldBeNull(); data.Length.ShouldBe(4); BinaryPrimitives.ReadInt32LittleEndian(data.Span).ShouldBe(123456); } [Fact] public void Read_array_values() { var (type, _, data) = LogixServices.ParseReadReply(Rep("logix-read-dint-array16.hex")); type.ShouldBe(LogixTypeCode.DInt); data.Length.ShouldBe(64); for (var i = 0; i < 16; i++) BinaryPrimitives.ReadUInt32LittleEndian(data.Span[(i * 4)..]).ShouldBe((uint)(((i + 1) << 24) | (i + 1))); } [Fact] public void Read_bool_value() { var (type, _, data) = LogixServices.ParseReadReply(Rep("logix-read-bool.hex")); type.ShouldBe(LogixTypeCode.Bool); data.Length.ShouldBe(1); data.Span[0].ShouldBe((byte)1); } // ab_server types STRING as atomic D0 00 (a real Logix STRING is A0 02 + handle): simulator behaviour. [Fact] public void Read_string_value_from_sim() { var (type, handle, data) = LogixServices.ParseReadReply(Rep("logix-read-string.hex")); type.ShouldBe((ushort)0x00D0); handle.ShouldBeNull(); data.Length.ShouldBe(88); BinaryPrimitives.ReadInt32LittleEndian(data.Span).ShouldBe(7); Encoding.ASCII.GetString(data.Span.Slice(4, 7)).ShouldBe("HelloAB"); } [Fact] public void Read_unknown_tag_reply_status() { var r = Rep("logix-read-unknown-tag.hex"); r.GeneralStatus.ShouldBe((byte)0x04); r.Status.ShouldBe(AbStatus.NotFound); } [Fact] public void Write_request_matches_capture() => LogixServices.WriteTag(CipPath.EncodeSymbolic("TestDINT"), LogixTypeCode.DInt, null, 1, Int32Le(123456)) .ShouldBe(Req("logix-write-dint.hex", 1)); [Fact] public void Write_reply_is_ok() => Rep("logix-write-dint.hex", 1).GeneralStatus.ShouldBe((byte)0); [Fact] public void Read_tag_plain_layout() => Convert.ToHexString(LogixServices.ReadTag(CipPath.EncodeSymbolic("Abc"), 3)).ShouldBe("4C03" + "910341626300" + "0300"); [Fact] public void Fragmented_write_layout() => Convert.ToHexString(LogixServices.WriteTagFragmented(CipPath.EncodeSymbolic("Abc"), LogixTypeCode.DInt, null, 2, 8, [1, 2, 3, 4])) .ShouldBe("5303" + "910341626300" + "C400" + "0200" + "08000000" + "01020304"); [Fact] public void Struct_write_includes_handle() => Convert.ToHexString(LogixServices.WriteTag(CipPath.EncodeSymbolic("Abc"), LogixTypeCode.Struct, 0x0FCE, 1, [9])) .ShouldBe("4D03" + "910341626300" + "A002" + "CE0F" + "0100" + "09"); [Fact] public void Struct_write_without_handle_throws() => Should.Throw(() => LogixServices.WriteTag(CipPath.EncodeSymbolic("Abc"), LogixTypeCode.Struct, null, 1, [9])); [Fact] public void Read_reply_with_struct_handle() { var reply = CipReply.Parse(Convert.FromHexString("CC000000" + "A002" + "CE0F" + "05000000" + "48656C6C6F")); var (type, handle, data) = LogixServices.ParseReadReply(reply); type.ShouldBe((ushort)0x02A0); handle.ShouldBe((ushort)0x0FCE); data.Length.ShouldBe(9); } }