feat(modbus-rtu): add ModbusCrc CRC-16/MODBUS helper + golden vectors
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus;
|
||||
|
||||
/// <summary>
|
||||
/// CRC-16/MODBUS helper: reflected polynomial <c>0xA001</c>, initial value <c>0xFFFF</c>.
|
||||
/// On the wire the CRC is appended low byte first, high byte second — <see cref="Append"/>
|
||||
/// does that; <see cref="Compute"/> just returns the 16-bit value. Byte-stream-agnostic:
|
||||
/// no socket or framing knowledge lives here.
|
||||
/// </summary>
|
||||
public static class ModbusCrc
|
||||
{
|
||||
/// <summary>Computes the CRC-16/MODBUS checksum over <paramref name="data"/>.</summary>
|
||||
/// <param name="data">The bytes to checksum (e.g. the RTU frame minus the trailing CRC).</param>
|
||||
/// <returns>The 16-bit CRC value.</returns>
|
||||
public static ushort Compute(ReadOnlySpan<byte> data)
|
||||
{
|
||||
ushort crc = 0xFFFF;
|
||||
foreach (var b in data)
|
||||
{
|
||||
crc ^= b;
|
||||
for (var i = 0; i < 8; i++)
|
||||
{
|
||||
var carry = (crc & 0x0001) != 0;
|
||||
crc >>= 1;
|
||||
if (carry)
|
||||
{
|
||||
crc ^= 0xA001;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return crc;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <paramref name="body"/> with its CRC-16/MODBUS appended, low byte first.
|
||||
/// </summary>
|
||||
/// <param name="body">The frame body to checksum.</param>
|
||||
/// <returns>A new array: <paramref name="body"/> followed by <c>[crc & 0xFF, crc >> 8]</c>.</returns>
|
||||
public static byte[] Append(ReadOnlySpan<byte> body)
|
||||
{
|
||||
var crc = Compute(body);
|
||||
var result = new byte[body.Length + 2];
|
||||
body.CopyTo(result);
|
||||
result[^2] = (byte)(crc & 0xFF);
|
||||
result[^1] = (byte)(crc >> 8);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests;
|
||||
|
||||
public sealed class ModbusCrcTests
|
||||
{
|
||||
// Known CRC-16/MODBUS vectors (init 0xFFFF, poly 0xA001). CRC returned as ushort;
|
||||
// on the wire it is appended low-byte-first.
|
||||
[Theory]
|
||||
// FC03 read HR: unit 1, addr 0, qty 1 -> CRC 0x0A84 (bytes 84 0A on the wire)
|
||||
[InlineData(new byte[] { 0x01, 0x03, 0x00, 0x00, 0x00, 0x01 }, (ushort)0x0A84)]
|
||||
// "123456789" canonical check value for CRC-16/MODBUS = 0x4B37
|
||||
[InlineData(new byte[] { 0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39 }, (ushort)0x4B37)]
|
||||
public void Compute_matches_known_vectors(byte[] frame, ushort expected)
|
||||
=> ModbusCrc.Compute(frame).ShouldBe(expected);
|
||||
|
||||
[Fact]
|
||||
public void AppendLowByteFirst_writes_lo_then_hi()
|
||||
{
|
||||
var body = new byte[] { 0x01, 0x03, 0x00, 0x00, 0x00, 0x01 };
|
||||
var withCrc = ModbusCrc.Append(body);
|
||||
withCrc.Length.ShouldBe(body.Length + 2);
|
||||
withCrc[^2].ShouldBe((byte)0x84); // CRC low byte
|
||||
withCrc[^1].ShouldBe((byte)0x0A); // CRC high byte
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user