Skip to content

Commit

Permalink
Improve protocol header documentation
Browse files Browse the repository at this point in the history
Signed-off-by: Jacob Laursen <[email protected]>
  • Loading branch information
jlaur committed Jan 30, 2025
1 parent c0ad353 commit 741b0b1
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ public class CRC16Calculator {
48554, 44427, 40424, 36297, 31782, 27655, 23652, 19525, 15522, 11395, 7392, 3265, 61215, 65342, 53085,
57212, 44955, 49082, 36825, 40952, 28183, 32310, 20053, 24180, 11923, 16050, 3793, 7920 };

private static final int CRC16_INITIAL_VALUE = 0xFFFF;
private static final int CRC16_FINAL_XOR = 0xFFFF;
private static final int BYTE_MASK = 0xFF;
private static final int CRC16_INITIAL_VALUE = 0xffff;
private static final int CRC16_FINAL_XOR = 0xffff;
private static final int BYTE_MASK = 0xff;

private CRC16Calculator() {
// Prevent instantiation
Expand All @@ -59,35 +59,53 @@ public static boolean check(byte[] data) {
throw new IllegalArgumentException("Data array must contain at least 3 bytes.");
}

int dataLength = (data[1] & BYTE_MASK) + 1;
if (dataLength + 3 > data.length) {
int dataLength = (data[1] & BYTE_MASK);
if (dataLength + 4 > data.length) {
throw new IllegalArgumentException("Invalid data length specified in the array.");
}

int crcValue = calculateCRC(data, 1, dataLength);
byte highByte = (byte) getHighByte(crcValue);
byte lowByte = (byte) getLowByte(crcValue);
int crcValue = calculate(data, 1, dataLength + 1);
byte lowByte = getLowByte(crcValue);
byte highByte = getHighByte(crcValue);

return data[dataLength + 1] == highByte && data[dataLength + 2] == lowByte;
return data[dataLength + 2] == highByte && data[dataLength + 3] == lowByte;
}

private static int calculateCRC(byte[] data, int start, int length) {
/**
* Calculates the CRC-16 checksum for the given data and puts it into
* the last two bytes of the given data array.
*
* @param data The data array to calculate CRC-16 with the two last bytes reserved for the result
* @param length The message size excluding start delimiter/size (first two bytes) and CRC-16 checksum (last two
* bytes)
*/
public static void put(byte[] data, int length) {
int dataLength = (data[1] & BYTE_MASK);
if (dataLength + 4 > data.length) {
throw new IllegalArgumentException("Invalid data length specified in the array.");
}
int crcValue = calculate(data, 1, length + 1);
data[length + 2] = getHighByte(crcValue);
data[length + 3] = getLowByte(crcValue);
}

private static int calculate(byte[] data, int start, int length) {
int value = CRC16_INITIAL_VALUE;

for (int i = 0; i < length; i++) {
int inputByte = data[start + i] & BYTE_MASK;
int tableIndex = (inputByte ^ (value >> 8)) & BYTE_MASK;
value = (LOOKUP_TABLE[tableIndex] ^ (value << 8)) & 0xFFFF;
value = (LOOKUP_TABLE[tableIndex] ^ (value << 8)) & 0xffff;
}

return value ^ CRC16_FINAL_XOR;
}

private static int getHighByte(int word) {
return (word >> 8) & BYTE_MASK;
private static byte getLowByte(int word) {
return (byte) (word & BYTE_MASK);
}

private static int getLowByte(int word) {
return word & BYTE_MASK;
private static byte getHighByte(int word) {
return (byte) ((word >> 8) & BYTE_MASK);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bluetooth.grundfosalpha.internal.protocol;

import org.eclipse.jdt.annotation.NonNullByDefault;

/**
* This defines protocol header related constants.
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public class MessageHeader {

/**
* Header length including {@link MessageStartDelimiter} and size byte.
*/
public static final int LENGTH = 5;

private static final byte OFFSET_START_DELIMITER = 0;
private static final byte OFFSET_LENGTH = 1;
private static final byte OFFSET_SOURCE_ADDRESS = 2;
private static final byte OFFSET_DESTINATION_ADDRESS = 3;
private static final byte OFFSET_HEADER4 = 4;

/**
* Address of the controller/client (openHAB).
*/
private static final byte CONTROLLER_ADDRESS = (byte) 0xe7;

/**
* Address of the peripheral/server (pump).
*/
private static final byte PERIPHERAL_ADDRESS = (byte) 0xf8;

/**
* Last byte in header used for flowhead/power requests/responses.
* Not sure about meaning.
*/
private static final byte HEADER4_VALUE = (byte) 0x0a;

/**
* Fill in header for a request.
*
* @param request Request buffer
* @param messageLength The request size excluding {@link MessageStartDelimiter}, size byte and CRC-16 checksum
*/
public static void setRequestHeader(byte[] request, int messageLength) {
if (request.length < LENGTH) {
throw new IllegalArgumentException("Buffer is too small for header");
}

request[OFFSET_START_DELIMITER] = MessageStartDelimiter.Request.getValue();
request[OFFSET_LENGTH] = (byte) messageLength;
request[OFFSET_SOURCE_ADDRESS] = CONTROLLER_ADDRESS;
request[OFFSET_DESTINATION_ADDRESS] = PERIPHERAL_ADDRESS;
request[OFFSET_HEADER4] = HEADER4_VALUE;
}

/**
* Check if this packet is the first packet in a response payload.
*
* @param packet The packet to inspect
* @return true if determined to be first packet, otherwise false
*/
public static boolean isInitialResponsePacket(byte[] packet) {
return packet.length >= LENGTH && packet[OFFSET_START_DELIMITER] == MessageStartDelimiter.Reply.getValue()
&& packet[OFFSET_SOURCE_ADDRESS] == PERIPHERAL_ADDRESS
&& packet[OFFSET_DESTINATION_ADDRESS] == CONTROLLER_ADDRESS && packet[OFFSET_HEADER4] == HEADER4_VALUE;
}

/**
* Get total size of message including {@link MessageStartDelimiter}, size byte and CRC-16 checksum.
*
* @param header Header bytes (at least)
* @return total size
*/
public static int getTotalSize(byte[] header) {
return ((byte) header[MessageHeader.OFFSET_LENGTH]) + 4;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bluetooth.grundfosalpha.internal.protocol;

import org.eclipse.jdt.annotation.NonNullByDefault;

/**
* This defines the start delimiters for different kinds of messages.
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public enum MessageStartDelimiter {
Reply((byte) 0x24),
Message((byte) 0x26),
Request((byte) 0x27);

private final byte value;

MessageStartDelimiter(byte value) {
this.value = value;
}

public byte getValue() {
return value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,26 @@
@NonNullByDefault
public enum MessageType {
FlowHead(new byte[] { 0x1f, 0x00, 0x01, 0x30, 0x01, 0x00, 0x00, 0x18 },
new byte[] { (byte) 0x27, (byte) 0x07, (byte) 0xe7, (byte) 0xf8, (byte) 0x0a, (byte) 0x03, (byte) 0x5d,
(byte) 0x01, (byte) 0x21, (byte) 0x52, (byte) 0x1f }),
new byte[] { (byte) 0x03, (byte) 0x5d, (byte) 0x01, (byte) 0x21 }),
Power(new byte[] { 0x2c, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x25 },
new byte[] { (byte) 0x27, (byte) 0x07, (byte) 0xe7, (byte) 0xf8, (byte) 0x0a, (byte) 0x03, (byte) 0x57,
(byte) 0x00, (byte) 0x45, (byte) 0x8a, (byte) 0xcd });
new byte[] { (byte) 0x03, (byte) 0x57, (byte) 0x00, (byte) 0x45 });

private final byte[] identifier;
private final byte[] request;

MessageType(byte[] identifier, byte[] request) {
MessageType(byte[] identifier, byte[] requestMessage) {
this.identifier = identifier;
this.request = request;

int messageLength = requestMessage.length + 3;
request = new byte[messageLength + 4];

// Append the header
MessageHeader.setRequestHeader(request, messageLength);

// Append the message type-specific part
System.arraycopy(requestMessage, 0, request, MessageHeader.LENGTH, requestMessage.length);

CRC16Calculator.put(request, messageLength);
}

public byte[] identifier() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ public class ResponseMessage {
private final Logger logger = LoggerFactory.getLogger(ResponseMessage.class);

private static final int GENI_RESPONSE_MAX_SIZE = 259;
private static final int GENI_RESPONSE_HEADER_LENGTH = 5;
private static final int GENI_RESPONSE_TYPE_LENGTH = 8;

private int responseTotalSize;
Expand All @@ -58,27 +57,27 @@ public boolean addPacket(byte[] packet) {
logger.trace("GENI response: {}", HexUtils.bytesToHex(packet));
}

boolean isFirstPacket = isInitialPacket(packet);
boolean isFirstPacket = MessageHeader.isInitialResponsePacket(packet);

if (responseRemaining == Integer.MAX_VALUE) {
if (!isInitialPacket(packet)) {
if (!MessageHeader.isInitialResponsePacket(packet)) {
if (logger.isDebugEnabled()) {
byte[] header = new byte[GENI_RESPONSE_HEADER_LENGTH];
System.arraycopy(packet, 0, header, 0, GENI_RESPONSE_HEADER_LENGTH);
byte[] header = new byte[MessageHeader.LENGTH];
System.arraycopy(packet, 0, header, 0, MessageHeader.LENGTH);
if (logger.isDebugEnabled()) {
logger.debug("Response bytes {} don't match GENI header", HexUtils.bytesToHex(header));
}
}
return false;
}

responseTotalSize = getTotalSize(packet);
responseTotalSize = MessageHeader.getTotalSize(packet);
responseOffset = 0;
responseRemaining = responseTotalSize;
} else if (isFirstPacket && responseRemaining > 0) {
logger.debug("Received new first packet while awaiting continuation, resetting");

responseTotalSize = getTotalSize(packet);
responseTotalSize = MessageHeader.getTotalSize(packet);
responseOffset = 0;
responseRemaining = responseTotalSize;
}
Expand Down Expand Up @@ -124,24 +123,15 @@ public Map<SensorDataType, BigDecimal> decode() {
private Optional<ByteBuffer> decode(SensorDataType dataType) {
byte[] expectedResponseType = dataType.messageType().identifier();
byte[] responseType = new byte[GENI_RESPONSE_TYPE_LENGTH];
System.arraycopy(response, GENI_RESPONSE_HEADER_LENGTH, responseType, 0, GENI_RESPONSE_TYPE_LENGTH);
System.arraycopy(response, MessageHeader.LENGTH, responseType, 0, GENI_RESPONSE_TYPE_LENGTH);

if (!Arrays.equals(expectedResponseType, responseType)) {
return Optional.empty();
}

int valueOffset = GENI_RESPONSE_HEADER_LENGTH + GENI_RESPONSE_TYPE_LENGTH + dataType.offset();
int valueOffset = MessageHeader.LENGTH + GENI_RESPONSE_TYPE_LENGTH + dataType.offset();
byte[] valueBuffer = Arrays.copyOfRange(response, valueOffset, valueOffset + 4);

return Optional.of(ByteBuffer.wrap(valueBuffer).order(ByteOrder.BIG_ENDIAN));
}

private boolean isInitialPacket(byte[] packet) {
return packet[0] == (byte) 0x24 && packet[2] == (byte) 0xf8 && packet[3] == (byte) 0xe7
&& packet[4] == (byte) 0x0a;
}

private int getTotalSize(byte[] initialPacket) {
return ((byte) initialPacket[1]) + 4;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public class CRC16CalculatorTest {

private static final int MAX_16BIT = 0xFFFF;
private static final int MSB_16BIT = 0x8000;
private static final int POLYNOMIAL = 0x1021; // CRC-16-CCITT polynomial (4129 in decimal)
private static final int POLYNOMIAL = 0x1021; // CRC-16-CCITT polynomial

@Test
void precomputedValuesAreCorrect() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bluetooth.grundfosalpha.internal.protocol;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.core.util.HexUtils;

/**
* Tests for {@link MessageType}.
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public class MessageTypeTest {
@Test
void requestFlowHead() {
String expected = "27 07 E7 F8 0A 03 5D 01 21 52 1F";
assertThat(HexUtils.bytesToHex(MessageType.FlowHead.request(), " "), is(expected));
}

@Test
void requestPower() {
String expected = "27 07 E7 F8 0A 03 57 00 45 8A CD";
assertThat(HexUtils.bytesToHex(MessageType.Power.request(), " "), is(expected));
}
}

0 comments on commit 741b0b1

Please sign in to comment.