feat(ab-transport): project skeleton with AbStatus, CIP reply parsing and the ICipChannel seam
This commit is contained in:
@@ -40,6 +40,7 @@
|
||||
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Contracts/ZB.MOM.WW.OtOpcUa.Driver.S7.Contracts.csproj" />
|
||||
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/ZB.MOM.WW.OtOpcUa.Driver.AbCip.csproj" />
|
||||
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts.csproj" />
|
||||
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport/ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.csproj" />
|
||||
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.csproj" />
|
||||
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts.csproj" />
|
||||
<Project Path="src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.csproj" />
|
||||
@@ -109,6 +110,7 @@
|
||||
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests.csproj" />
|
||||
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests/ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.csproj" />
|
||||
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests.csproj" />
|
||||
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.csproj" />
|
||||
<Project Path="tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests.csproj" />
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Native AB transport gate results
|
||||
|
||||
## ab_server suites (T17)
|
||||
|
||||
_(pending)_
|
||||
|
||||
## Differential vs ab_server (T18)
|
||||
|
||||
_(pending)_
|
||||
|
||||
## AdminUI /run (T19)
|
||||
|
||||
_(pending)_
|
||||
|
||||
## Real press (T21)
|
||||
|
||||
_(pending)_
|
||||
|
||||
## Latency (T21)
|
||||
|
||||
_(pending)_
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
|
||||
|
||||
/// <summary>Transport-neutral status shared by the libplctag and native AB transports.
|
||||
/// Zero is success; every error is negative so existing <c>status != 0</c> checks stay correct.</summary>
|
||||
public enum AbStatus
|
||||
{
|
||||
Ok = 0,
|
||||
Pending = 1,
|
||||
Timeout = -1,
|
||||
NotFound = -2,
|
||||
NotAllowed = -3,
|
||||
OutOfBounds = -4,
|
||||
TooLarge = -5,
|
||||
TooSmall = -6,
|
||||
Unsupported = -7,
|
||||
BadConnection = -8,
|
||||
BadReply = -9,
|
||||
Partial = -10,
|
||||
Abort = -11,
|
||||
TypeMismatch = -12,
|
||||
DeviceStateConflict = -13,
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
|
||||
|
||||
public static class AbStatusCodes
|
||||
{
|
||||
public static AbStatus FromCip(byte general, ReadOnlySpan<ushort> extended) => general switch
|
||||
{
|
||||
0x00 or 0x0B => AbStatus.Ok,
|
||||
0x04 or 0x05 or 0x16 => AbStatus.NotFound,
|
||||
0x06 => AbStatus.Partial,
|
||||
0x08 => AbStatus.Unsupported,
|
||||
0x0A => AbStatus.OutOfBounds,
|
||||
0x0C or 0x10 => AbStatus.DeviceStateConflict,
|
||||
0x0E or 0x0F => AbStatus.NotAllowed,
|
||||
0x13 => AbStatus.TooSmall,
|
||||
0x15 => AbStatus.TooLarge,
|
||||
0xFF when extended.Length > 0 && extended[0] == 0x2105 => AbStatus.OutOfBounds,
|
||||
0xFF when extended.Length > 0 && extended[0] == 0x2107 => AbStatus.TypeMismatch,
|
||||
_ => AbStatus.BadReply,
|
||||
};
|
||||
|
||||
public static AbStatus FromPccc(byte sts, byte? extSts) => sts switch
|
||||
{
|
||||
0x00 => AbStatus.Ok,
|
||||
0x10 => AbStatus.Unsupported,
|
||||
0x20 => AbStatus.NotFound,
|
||||
0x30 => AbStatus.NotAllowed,
|
||||
0x40 or 0x50 => AbStatus.DeviceStateConflict,
|
||||
0xF0 when extSts is 0x06 or 0x07 => AbStatus.NotFound,
|
||||
_ => AbStatus.BadReply,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
|
||||
|
||||
/// <summary>Thrown by both AB transports. <see cref="Status"/> drives OPC UA status mapping and the
|
||||
/// driver's "controller answered, don't throttle the device" classification (05/STAB-8).</summary>
|
||||
public sealed class AbTransportException(AbStatus status, string message, Exception? inner = null)
|
||||
: Exception(message, inner)
|
||||
{
|
||||
public AbStatus Status { get; } = status;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Wire;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Cip;
|
||||
|
||||
/// <summary>A parsed CIP message-router reply: service (request code, 0x80 bit stripped), general
|
||||
/// status, extended status words, and the reply data.</summary>
|
||||
public sealed record CipReply(byte Service, byte GeneralStatus, ushort[] ExtendedStatus, ReadOnlyMemory<byte> Data)
|
||||
{
|
||||
public AbStatus Status => AbStatusCodes.FromCip(GeneralStatus, ExtendedStatus);
|
||||
|
||||
public static CipReply Parse(ReadOnlyMemory<byte> raw)
|
||||
{
|
||||
var r = new CipReader(raw.Span);
|
||||
var service = (byte)(r.U8() & 0x7F);
|
||||
r.U8(); // reserved
|
||||
var general = r.U8();
|
||||
var extWords = r.U8();
|
||||
var ext = new ushort[extWords];
|
||||
for (var i = 0; i < extWords; i++) ext[i] = r.U16();
|
||||
var data = raw[r.Position..];
|
||||
return new CipReply(service, general, ext, data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Cip;
|
||||
|
||||
/// <summary>A path to a CIP message router: connected (Forward Open) or unconnected (UCMM). Task 9
|
||||
/// implements it; Task 10's scheduler and the native adapters consume it.</summary>
|
||||
public interface ICipChannel
|
||||
{
|
||||
/// <summary>Largest CIP request (service byte through end of data) one call may carry.</summary>
|
||||
int MaxRequestBytes { get; }
|
||||
|
||||
/// <summary>Send one CIP request and return the raw CIP reply bytes (reply-service byte first).
|
||||
/// Throws <see cref="AbTransportException"/> on transport failure or timeout.</summary>
|
||||
Task<ReadOnlyMemory<byte>> SendAsync(ReadOnlyMemory<byte> cipRequest, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Buffers.Binary;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Wire;
|
||||
|
||||
/// <summary>Bounds-checked little-endian reader. Any overrun throws AbTransportException(BadReply).</summary>
|
||||
public ref struct CipReader(ReadOnlySpan<byte> data)
|
||||
{
|
||||
private readonly ReadOnlySpan<byte> _data = data;
|
||||
public int Position { get; private set; }
|
||||
public readonly int Remaining => _data.Length - Position;
|
||||
|
||||
private ReadOnlySpan<byte> Take(int n)
|
||||
{
|
||||
if (n < 0 || Position + n > _data.Length)
|
||||
throw new AbTransportException(AbStatus.BadReply, $"Reply truncated: need {n} bytes at {Position}, have {Remaining}.");
|
||||
var s = _data.Slice(Position, n);
|
||||
Position += n;
|
||||
return s;
|
||||
}
|
||||
|
||||
public byte U8() => Take(1)[0];
|
||||
public ushort U16() => BinaryPrimitives.ReadUInt16LittleEndian(Take(2));
|
||||
public uint U32() => BinaryPrimitives.ReadUInt32LittleEndian(Take(4));
|
||||
public ulong U64() => BinaryPrimitives.ReadUInt64LittleEndian(Take(8));
|
||||
public ReadOnlySpan<byte> Bytes(int n) => Take(n);
|
||||
public ReadOnlySpan<byte> Rest() => Take(Remaining);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Buffers.Binary;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Wire;
|
||||
|
||||
/// <summary>Growable little-endian writer for CIP / EIP frames.</summary>
|
||||
public sealed class CipWriter
|
||||
{
|
||||
private byte[] _buf = new byte[256];
|
||||
public int Length { get; private set; }
|
||||
|
||||
private Span<byte> Take(int n)
|
||||
{
|
||||
if (Length + n > _buf.Length) Array.Resize(ref _buf, Math.Max(_buf.Length * 2, Length + n));
|
||||
var s = _buf.AsSpan(Length, n);
|
||||
Length += n;
|
||||
return s;
|
||||
}
|
||||
|
||||
public CipWriter U8(byte v) { Take(1)[0] = v; return this; }
|
||||
public CipWriter U16(ushort v) { BinaryPrimitives.WriteUInt16LittleEndian(Take(2), v); return this; }
|
||||
public CipWriter U32(uint v) { BinaryPrimitives.WriteUInt32LittleEndian(Take(4), v); return this; }
|
||||
public CipWriter U64(ulong v) { BinaryPrimitives.WriteUInt64LittleEndian(Take(8), v); return this; }
|
||||
public CipWriter Bytes(ReadOnlySpan<byte> v) { v.CopyTo(Take(v.Length)); return this; }
|
||||
public CipWriter PadToEven() { if ((Length & 1) == 1) U8(0); return this; }
|
||||
|
||||
/// <summary>Overwrite a u16 already written at <paramref name="offset"/> (length back-patching).</summary>
|
||||
public void PatchU16(int offset, ushort v) => BinaryPrimitives.WriteUInt16LittleEndian(_buf.AsSpan(offset, 2), v);
|
||||
|
||||
public byte[] ToArray() => _buf.AsSpan(0, Length).ToArray();
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
<RootNamespace>ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport</RootNamespace>
|
||||
<AssemblyName>ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport</AssemblyName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions"/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts\ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts.csproj"/>
|
||||
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport\ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.csproj"/>
|
||||
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Core.Abstractions\ZB.MOM.WW.OtOpcUa.Core.Abstractions.csproj"/>
|
||||
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Core\ZB.MOM.WW.OtOpcUa.Core.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts\ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts.csproj"/>
|
||||
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport\ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.csproj"/>
|
||||
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Core.Abstractions\ZB.MOM.WW.OtOpcUa.Core.Abstractions.csproj"/>
|
||||
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Core\ZB.MOM.WW.OtOpcUa.Core.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests;
|
||||
|
||||
public sealed class AbStatusCodesTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData((byte)0x00, AbStatus.Ok)]
|
||||
[InlineData((byte)0x04, AbStatus.NotFound)]
|
||||
[InlineData((byte)0x05, AbStatus.NotFound)]
|
||||
[InlineData((byte)0x16, AbStatus.NotFound)]
|
||||
[InlineData((byte)0x06, AbStatus.Partial)]
|
||||
[InlineData((byte)0x08, AbStatus.Unsupported)]
|
||||
[InlineData((byte)0x0F, AbStatus.NotAllowed)]
|
||||
[InlineData((byte)0x10, AbStatus.DeviceStateConflict)]
|
||||
[InlineData((byte)0x13, AbStatus.TooSmall)]
|
||||
[InlineData((byte)0x15, AbStatus.TooLarge)]
|
||||
[InlineData((byte)0x0B, AbStatus.Ok)]
|
||||
[InlineData((byte)0x0A, AbStatus.OutOfBounds)]
|
||||
[InlineData((byte)0x0C, AbStatus.DeviceStateConflict)]
|
||||
[InlineData((byte)0x0E, AbStatus.NotAllowed)]
|
||||
[InlineData((byte)0x1E, AbStatus.BadReply)]
|
||||
[InlineData((byte)0x01, AbStatus.BadReply)]
|
||||
public void Cip_general_status_maps(byte general, AbStatus expected) =>
|
||||
AbStatusCodes.FromCip(general, ReadOnlySpan<ushort>.Empty).ShouldBe(expected);
|
||||
|
||||
[Theory]
|
||||
[InlineData((ushort)0x2105, AbStatus.OutOfBounds)]
|
||||
[InlineData((ushort)0x2107, AbStatus.TypeMismatch)]
|
||||
[InlineData((ushort)0x9999, AbStatus.BadReply)]
|
||||
public void General_status_FF_maps_by_extended_status(ushort ext, AbStatus expected) =>
|
||||
AbStatusCodes.FromCip(0xFF, [ext]).ShouldBe(expected);
|
||||
|
||||
[Theory]
|
||||
[InlineData((byte)0x00, null, AbStatus.Ok)]
|
||||
[InlineData((byte)0x10, null, AbStatus.Unsupported)]
|
||||
[InlineData((byte)0x20, null, AbStatus.NotFound)]
|
||||
[InlineData((byte)0x30, null, AbStatus.NotAllowed)]
|
||||
[InlineData((byte)0x40, null, AbStatus.DeviceStateConflict)]
|
||||
[InlineData((byte)0xF0, (byte)0x06, AbStatus.NotFound)]
|
||||
[InlineData((byte)0xF0, (byte)0x07, AbStatus.NotFound)]
|
||||
[InlineData((byte)0xF0, (byte)0x0A, AbStatus.BadReply)]
|
||||
public void Pccc_status_maps(byte sts, byte? ext, AbStatus expected) =>
|
||||
AbStatusCodes.FromPccc(sts, ext).ShouldBe(expected);
|
||||
|
||||
[Fact]
|
||||
public void Ok_is_zero_and_every_error_is_negative()
|
||||
{
|
||||
((int)AbStatus.Ok).ShouldBe(0);
|
||||
foreach (var s in Enum.GetValues<AbStatus>().Where(s => s is not AbStatus.Ok and not AbStatus.Pending))
|
||||
((int)s).ShouldBeLessThan(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Cip;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Wire;
|
||||
|
||||
public sealed class CipReplyTests
|
||||
{
|
||||
[Fact]
|
||||
public void Parses_success_reply_with_data()
|
||||
{
|
||||
// 0xCC = reply to Read Tag 0x4C; reserved; status 0; ext size 0; data C4 00 2A 00 00 00
|
||||
var r = CipReply.Parse(new byte[] { 0xCC, 0x00, 0x00, 0x00, 0xC4, 0x00, 0x2A, 0x00, 0x00, 0x00 });
|
||||
r.Service.ShouldBe((byte)0x4C);
|
||||
r.GeneralStatus.ShouldBe((byte)0);
|
||||
r.ExtendedStatus.Length.ShouldBe(0);
|
||||
r.Data.ToArray().ShouldBe(new byte[] { 0xC4, 0x00, 0x2A, 0x00, 0x00, 0x00 });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parses_reply_with_one_extended_word()
|
||||
{
|
||||
var r = CipReply.Parse(new byte[] { 0xCC, 0x00, 0xFF, 0x01, 0x07, 0x21 });
|
||||
r.ExtendedStatus.ToArray().ShouldBe(new ushort[] { 0x2107 });
|
||||
r.Status.ShouldBe(AbStatus.TypeMismatch);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parses_error_reply_with_two_extended_words()
|
||||
{
|
||||
var r = CipReply.Parse(new byte[] { 0xCC, 0x00, 0xFF, 0x02, 0x05, 0x21, 0x00, 0x00 });
|
||||
r.GeneralStatus.ShouldBe((byte)0xFF);
|
||||
r.ExtendedStatus.ToArray().ShouldBe(new ushort[] { 0x2105, 0x0000 });
|
||||
r.Data.Length.ShouldBe(0);
|
||||
r.Status.ShouldBe(AbStatus.OutOfBounds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Truncated_reply_throws_BadReply()
|
||||
{
|
||||
var ex = Should.Throw<AbTransportException>(() => CipReply.Parse(new byte[] { 0xCC, 0x00 }));
|
||||
ex.Status.ShouldBe(AbStatus.BadReply);
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RootNamespace>ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit.v3"/>
|
||||
<PackageReference Include="Shouldly"/>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk"/>
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport\ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user