diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusCrc.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusCrc.cs new file mode 100644 index 00000000..0e81ed1a --- /dev/null +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusCrc.cs @@ -0,0 +1,48 @@ +namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus; + +/// +/// CRC-16/MODBUS helper: reflected polynomial 0xA001, initial value 0xFFFF. +/// On the wire the CRC is appended low byte first, high byte second — +/// does that; just returns the 16-bit value. Byte-stream-agnostic: +/// no socket or framing knowledge lives here. +/// +public static class ModbusCrc +{ + /// Computes the CRC-16/MODBUS checksum over . + /// The bytes to checksum (e.g. the RTU frame minus the trailing CRC). + /// The 16-bit CRC value. + public static ushort Compute(ReadOnlySpan 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; + } + + /// + /// Returns with its CRC-16/MODBUS appended, low byte first. + /// + /// The frame body to checksum. + /// A new array: followed by [crc & 0xFF, crc >> 8]. + public static byte[] Append(ReadOnlySpan 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; + } +} diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ModbusCrcTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ModbusCrcTests.cs new file mode 100644 index 00000000..cd4c2a56 --- /dev/null +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ModbusCrcTests.cs @@ -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 + } +}