feat(solarman): add Modbus RTU frame with CRC16

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Sonnet 5.5 committed 2026-10-24 00:28:04 +02:00
1 parent b99414d829
commit 04a17041ec
2 files changed
+101

No files matched your search

@@ -0,0 +1,54 @@
package home.gateway.adapters.solarman;
import java.io.IOException;
/** Modbus RTU frame: unit id, PDU, CRC16 with the low byte first. */
final class ModbusRtuFrame {
private static final int CRC_LENGTH = 2;
private static final int MIN_LENGTH = 1 + 1 + CRC_LENGTH;
private ModbusRtuFrame() {
}
static byte[] build(int unitId, byte[] pdu) {
byte[] frame = new byte[1 + pdu.length + CRC_LENGTH];
frame[0] = (byte) unitId;
System.arraycopy(pdu, 0, frame, 1, pdu.length);
int crc = crc16(frame, frame.length - CRC_LENGTH);
frame[frame.length - 2] = (byte) crc;
frame[frame.length - 1] = (byte) (crc >> 8);
return frame;
}
static boolean hasValidCrc(byte[] frame) {
if (frame.length < MIN_LENGTH) {
return false;
}
int crc = crc16(frame, frame.length - CRC_LENGTH);
return frame[frame.length - 2] == (byte) crc && frame[frame.length - 1] == (byte) (crc >> 8);
}
static byte[] parse(byte[] frame, int expectedUnitId) throws IOException {
if (!hasValidCrc(frame)) {
throw new IOException("Modbus RTU frame has a bad CRC or is too short");
}
if ((frame[0] & 0xFF) != expectedUnitId) {
throw new IOException("Modbus RTU frame from unit " + (frame[0] & 0xFF) + ", expected " + expectedUnitId);
}
byte[] pdu = new byte[frame.length - 1 - CRC_LENGTH];
System.arraycopy(frame, 1, pdu, 0, pdu.length);
return pdu;
}
static int crc16(byte[] data, int length) {
int crc = 0xFFFF;
for (int i = 0; i < length; i++) {
crc ^= data[i] & 0xFF;
for (int bit = 0; bit < 8; bit++) {
crc = (crc & 1) != 0 ? (crc >>> 1) ^ 0xA001 : crc >>> 1;
}
}
return crc;
}
}
@@ -0,0 +1,47 @@
package home.gateway.adapters.solarman;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.IOException;
import org.junit.jupiter.api.Test;
class ModbusRtuFrameTest {
private static final byte[] READ_10_AT_0 = {0x01, 0x03, 0x00, 0x00, 0x00, 0x0A, (byte) 0xC5, (byte) 0xCD};
@Test
void buildsTheStandardExampleWithCrcLowByteFirst() {
byte[] pdu = {0x03, 0x00, 0x00, 0x00, 0x0A};
assertArrayEquals(READ_10_AT_0, ModbusRtuFrame.build(1, pdu));
}
@Test
void crcOfAKnownResponse() {
byte[] frame = {0x01, 0x04, 0x02, (byte) 0xFF, (byte) 0xFF};
assertEquals(0x80B8, ModbusRtuFrame.crc16(frame, frame.length));
}
@Test
void parsesBackThePdu() throws IOException {
assertArrayEquals(new byte[] {0x03, 0x00, 0x00, 0x00, 0x0A}, ModbusRtuFrame.parse(READ_10_AT_0, 1));
}
@Test
void rejectsBadCrc() {
byte[] frame = READ_10_AT_0.clone();
frame[7] ^= 1;
assertThrows(IOException.class, () -> ModbusRtuFrame.parse(frame, 1));
}
@Test
void rejectsOtherUnit() {
assertThrows(IOException.class, () -> ModbusRtuFrame.parse(READ_10_AT_0, 2));
}
@Test
void rejectsTooShortFrame() {
assertThrows(IOException.class, () -> ModbusRtuFrame.parse(new byte[] {0x01, 0x03}, 1));
}
}