merge: Task 6 — CIP path encoding + Logix type codes
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Wire;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Cip;
|
||||
|
||||
public static class CipPath
|
||||
{
|
||||
/// <summary>Route path from "port,link[,port,link…]". Links that are not a byte value are IP
|
||||
/// addresses, encoded as extended link-address port segments.</summary>
|
||||
public static byte[] EncodeRoute(string cipPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(cipPath)) return [];
|
||||
var w = new CipWriter();
|
||||
var parts = cipPath.Split(',', StringSplitOptions.TrimEntries);
|
||||
if (parts.Length % 2 != 0) throw new ArgumentException($"CIP path '{cipPath}' must be port,link pairs.", nameof(cipPath));
|
||||
for (var i = 0; i < parts.Length; i += 2)
|
||||
{
|
||||
var port = int.Parse(parts[i], CultureInfo.InvariantCulture);
|
||||
var link = parts[i + 1];
|
||||
var isByte = byte.TryParse(link, NumberStyles.None, CultureInfo.InvariantCulture, out var linkByte);
|
||||
if (port < 15)
|
||||
{
|
||||
if (isByte) w.U8((byte)port).U8(linkByte);
|
||||
else { var a = Encoding.ASCII.GetBytes(link); w.U8((byte)(0x10 | port)).U8((byte)a.Length).Bytes(a).PadToEven(); }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isByte) w.U8(0x0F).U16((ushort)port).U8(linkByte);
|
||||
else { var a = Encoding.ASCII.GetBytes(link); w.U8(0x1F).U8((byte)a.Length).U16((ushort)port).Bytes(a).PadToEven(); }
|
||||
}
|
||||
}
|
||||
return w.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>Splits a trailing purely numeric member ("Tag.5") into the tag name and bit index.</summary>
|
||||
public static (string Name, int? Bit) SplitBitSelector(string tagName)
|
||||
{
|
||||
var dot = tagName.LastIndexOf('.');
|
||||
if (dot > 0 && dot < tagName.Length - 1 && tagName[(dot + 1)..].All(char.IsAsciiDigit))
|
||||
return (tagName[..dot], int.Parse(tagName[(dot + 1)..], CultureInfo.InvariantCulture));
|
||||
return (tagName, null);
|
||||
}
|
||||
|
||||
public static byte[] EncodeSymbolic(string tagName)
|
||||
{
|
||||
if (SplitBitSelector(tagName).Bit is not null)
|
||||
throw new ArgumentException($"'{tagName}' ends in a bit selector; strip it with SplitBitSelector first.", nameof(tagName));
|
||||
var w = new CipWriter();
|
||||
foreach (var part in SplitMembers(tagName))
|
||||
{
|
||||
var bracket = part.IndexOf('[');
|
||||
var name = bracket < 0 ? part : part[..bracket];
|
||||
var a = Encoding.ASCII.GetBytes(name);
|
||||
w.U8(0x91).U8((byte)a.Length).Bytes(a).PadToEven();
|
||||
if (bracket >= 0)
|
||||
{
|
||||
foreach (var idxText in part[(bracket + 1)..part.IndexOf(']')].Split(',', StringSplitOptions.TrimEntries))
|
||||
{
|
||||
var idx = uint.Parse(idxText, CultureInfo.InvariantCulture);
|
||||
if (idx < 0x100) w.U8(0x28).U8((byte)idx);
|
||||
else if (idx < 0x10000) w.U8(0x29).U8(0).U16((ushort)idx);
|
||||
else w.U8(0x2A).U8(0).U32(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
return w.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>Split on '.' outside brackets, keeping "Program:X" whole.</summary>
|
||||
private static IEnumerable<string> SplitMembers(string name)
|
||||
{
|
||||
var depth = 0; var start = 0;
|
||||
for (var i = 0; i < name.Length; i++)
|
||||
{
|
||||
if (name[i] == '[') depth++;
|
||||
else if (name[i] == ']') depth--;
|
||||
else if (name[i] == '.' && depth == 0) { yield return name[start..i]; start = i + 1; }
|
||||
}
|
||||
yield return name[start..];
|
||||
}
|
||||
|
||||
public static byte[] EncodeClassInstance(ushort cls, uint instance)
|
||||
{
|
||||
var w = new CipWriter();
|
||||
if (cls < 0x100) w.U8(0x20).U8((byte)cls); else w.U8(0x21).U8(0).U16(cls);
|
||||
if (instance < 0x100) w.U8(0x24).U8((byte)instance);
|
||||
else if (instance < 0x10000) w.U8(0x25).U8(0).U16((ushort)instance);
|
||||
else w.U8(0x26).U8(0).U32(instance);
|
||||
return w.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>Prefixes the path with its size in 16-bit words.</summary>
|
||||
public static byte[] WithPathSize(byte[] segments)
|
||||
{
|
||||
var w = new CipWriter();
|
||||
w.U8((byte)(segments.Length / 2)).Bytes(segments);
|
||||
return w.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>Message Router (class 2, instance 1).</summary>
|
||||
public static readonly byte[] MessageRouter = [0x20, 0x02, 0x24, 0x01];
|
||||
|
||||
/// <summary>Connection Manager (class 6, instance 1).</summary>
|
||||
public static readonly byte[] ConnectionManager = [0x20, 0x06, 0x24, 0x01];
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Logix;
|
||||
|
||||
public static class LogixTypeCode
|
||||
{
|
||||
public const ushort Bool = 0xC1, SInt = 0xC2, Int = 0xC3, DInt = 0xC4, LInt = 0xC5, USInt = 0xC6,
|
||||
UInt = 0xC7, UDInt = 0xC8, ULInt = 0xC9, Real = 0xCA, LReal = 0xCB, BoolArray = 0xD3, Struct = 0x02A0;
|
||||
|
||||
/// <summary>Byte width of an atomic type, or -1 when the code is not atomic (e.g. <see cref="Struct"/>).</summary>
|
||||
public static int ByteWidth(ushort code) => code switch
|
||||
{
|
||||
Bool or SInt or USInt => 1,
|
||||
Int or UInt => 2,
|
||||
DInt or UDInt or Real or BoolArray => 4,
|
||||
LInt or ULInt or LReal => 8,
|
||||
_ => -1,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Cip;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Cip;
|
||||
|
||||
public sealed class CipPathTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("1,0", "0100")]
|
||||
[InlineData("", "")]
|
||||
[InlineData("1,0,2,10.0.0.5", "0100" + "1208" + "31302E302E302E35")]
|
||||
[InlineData("2,10.0.0.55", "1209" + "31302E302E302E3535" + "00")]
|
||||
[InlineData("15,3", "0F0F0003")]
|
||||
[InlineData("16,10.0.0.5", "1F08" + "1000" + "31302E302E302E35")]
|
||||
public void Encodes_route(string path, string hex) =>
|
||||
Convert.ToHexString(CipPath.EncodeRoute(path)).ShouldBe(hex);
|
||||
|
||||
[Fact]
|
||||
public void Odd_route_parts_throw() =>
|
||||
Should.Throw<ArgumentException>(() => CipPath.EncodeRoute("1,0,2"));
|
||||
|
||||
[Theory]
|
||||
[InlineData("TestDINT", "9108546573744449 4E54")]
|
||||
[InlineData("Abc", "910341626300")]
|
||||
[InlineData("Program:Main.Arr[3].Val", "910C50726F6772616D3A4D61696E" + "910341727200" + "2803" + "910356616C00")]
|
||||
[InlineData("Big[300]", "910342696700" + "29002C01")]
|
||||
[InlineData("Huge[70000]", "910448756765" + "2A00" + "70110100")]
|
||||
[InlineData("Grid[1,2]", "910447726964" + "2801" + "2802")]
|
||||
public void Encodes_symbolic(string name, string hex) =>
|
||||
Convert.ToHexString(CipPath.EncodeSymbolic(name)).ShouldBe(hex.Replace(" ", ""));
|
||||
|
||||
[Fact]
|
||||
public void Symbolic_rejects_bit_selector() =>
|
||||
Should.Throw<ArgumentException>(() => CipPath.EncodeSymbolic("Tag.5"));
|
||||
|
||||
[Fact]
|
||||
public void Splits_bit_selector()
|
||||
{
|
||||
CipPath.SplitBitSelector("Tag.5").ShouldBe(("Tag", 5));
|
||||
CipPath.SplitBitSelector("Tag.Member").ShouldBe(("Tag.Member", null));
|
||||
CipPath.SplitBitSelector("Tag").ShouldBe(("Tag", null));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(2, 1u, "20022401")]
|
||||
[InlineData(0x6B, 0x1234u, "206B25003412")]
|
||||
[InlineData(0x300, 1u, "21000003" + "2401")]
|
||||
[InlineData(2, 70000u, "2002" + "2600" + "70110100")]
|
||||
public void Encodes_class_instance(ushort cls, uint inst, string hex) =>
|
||||
Convert.ToHexString(CipPath.EncodeClassInstance(cls, inst)).ShouldBe(hex);
|
||||
|
||||
[Fact]
|
||||
public void Prefixes_path_size_in_words() =>
|
||||
Convert.ToHexString(CipPath.WithPathSize([0x20, 0x02, 0x24, 0x01])).ShouldBe("0220022401");
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Logix;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Logix;
|
||||
|
||||
public sealed class LogixTypeCodeTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(LogixTypeCode.Bool, 1)]
|
||||
[InlineData(LogixTypeCode.SInt, 1)]
|
||||
[InlineData(LogixTypeCode.Int, 2)]
|
||||
[InlineData(LogixTypeCode.DInt, 4)]
|
||||
[InlineData(LogixTypeCode.LInt, 8)]
|
||||
[InlineData(LogixTypeCode.USInt, 1)]
|
||||
[InlineData(LogixTypeCode.UInt, 2)]
|
||||
[InlineData(LogixTypeCode.UDInt, 4)]
|
||||
[InlineData(LogixTypeCode.ULInt, 8)]
|
||||
[InlineData(LogixTypeCode.Real, 4)]
|
||||
[InlineData(LogixTypeCode.LReal, 8)]
|
||||
[InlineData(LogixTypeCode.BoolArray, 4)]
|
||||
[InlineData(LogixTypeCode.Struct, -1)]
|
||||
public void Byte_width(ushort code, int width) => LogixTypeCode.ByteWidth(code).ShouldBe(width);
|
||||
|
||||
[Fact]
|
||||
public void Code_values()
|
||||
{
|
||||
LogixTypeCode.Bool.ShouldBe((ushort)0xC1);
|
||||
LogixTypeCode.LReal.ShouldBe((ushort)0xCB);
|
||||
LogixTypeCode.Struct.ShouldBe((ushort)0x02A0);
|
||||
LogixTypeCode.BoolArray.ShouldBe((ushort)0xD3);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user