feat(modbus-rtu): add ModbusCrc CRC-16/MODBUS helper + golden vectors

Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
Joseph Doherty
2026-07-24 13:30:05 -04:00
parent e787aa572b
commit 8d0f60ec51
2 changed files with 75 additions and 0 deletions
@@ -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 &amp; 0xFF, crc &gt;&gt; 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;
}
}