119 lines
4.9 KiB
C#
119 lines
4.9 KiB
C#
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<AbTransportException>(() => 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<ArgumentException>(() => 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);
|
|
}
|
|
}
|