Garmin protocol: create helper class GarminByteBufferReader

separate the logic specific for GFDI messages from the generally useful logic.
Also centralize the logging in case of leftover bytes while parsing GFDI messages.
This commit is contained in:
Daniele Gobbetti 2024-04-02 14:36:35 +02:00
parent 2aa8667998
commit 379b8912cb
20 changed files with 166 additions and 185 deletions

View File

@ -0,0 +1,79 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
public class GarminByteBufferReader {
protected final ByteBuffer byteBuffer;
public GarminByteBufferReader(byte[] data) {
this.byteBuffer = ByteBuffer.wrap(data);
}
public ByteBuffer asReadOnlyBuffer() {
return byteBuffer.asReadOnlyBuffer();
}
public void setByteOrder(ByteOrder byteOrder) {
this.byteBuffer.order(byteOrder);
}
public int readByte() {
if (!byteBuffer.hasRemaining()) throw new IllegalStateException();
return Byte.toUnsignedInt(byteBuffer.get());
}
public int getPosition() {
return byteBuffer.position();
}
public int readShort() {
if (byteBuffer.remaining() < 2) throw new IllegalStateException();
return Short.toUnsignedInt(byteBuffer.getShort());
}
public int readInt() {
if (byteBuffer.remaining() < 4) throw new IllegalStateException();
return byteBuffer.getInt();
}
public long readLong() {
if (byteBuffer.remaining() < 8) throw new IllegalStateException();
return byteBuffer.getLong();
}
public float readFloat32() {
if (byteBuffer.remaining() < 4) throw new IllegalStateException();
return byteBuffer.getFloat();
}
public double readFloat64() {
if (byteBuffer.remaining() < 8) throw new IllegalStateException();
return byteBuffer.getDouble();
}
public String readString() {
final int size = readByte();
byte[] bytes = new byte[size];
if (byteBuffer.remaining() < size) throw new IllegalStateException();
byteBuffer.get(bytes);
return new String(bytes, StandardCharsets.UTF_8);
}
public byte[] readBytes(int size) {
byte[] bytes = new byte[size];
if (byteBuffer.remaining() < size) throw new IllegalStateException();
byteBuffer.get(bytes);
return bytes;
}
}

View File

@ -2,7 +2,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit;
import java.nio.ByteBuffer;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageReader;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminByteBufferReader;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter;
public class DevFieldDefinition {
@ -20,10 +20,10 @@ public class DevFieldDefinition {
this.valueHolder = ByteBuffer.allocate(size);
}
public static DevFieldDefinition parseIncoming(MessageReader reader) {
int number = reader.readByte();
int size = reader.readByte();
int developerDataIndex = reader.readByte();
public static DevFieldDefinition parseIncoming(GarminByteBufferReader garminByteBufferReader) {
int number = garminByteBufferReader.readByte();
int size = garminByteBufferReader.readByte();
int developerDataIndex = garminByteBufferReader.readByte();
return new DevFieldDefinition(number, size, developerDataIndex, "");

View File

@ -2,8 +2,8 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit;
import java.nio.ByteBuffer;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminByteBufferReader;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageReader;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter;
public class FieldDefinition implements FieldInterface {
@ -27,11 +27,10 @@ public class FieldDefinition implements FieldInterface {
this(localNumber, size, baseType, name, 1, 0);
}
public static FieldDefinition parseIncoming(MessageReader reader) {
int localNumber = reader.readByte();
int size = reader.readByte();
int baseTypeIdentifier = reader.readByte();
public static FieldDefinition parseIncoming(GarminByteBufferReader garminByteBufferReader) {
int localNumber = garminByteBufferReader.readByte();
int size = garminByteBufferReader.readByte();
int baseTypeIdentifier = garminByteBufferReader.readByte();
BaseType baseType = BaseType.fromIdentifier(baseTypeIdentifier);
if (size % baseType.getSize() != 0) {

View File

@ -6,8 +6,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageReader;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminByteBufferReader;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter;
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
@ -44,10 +43,10 @@ public class RecordData {
}
public void parseDataMessage(MessageReader reader) {
reader.setByteOrder(valueHolder.order());
public void parseDataMessage(GarminByteBufferReader garminByteBufferReader) {
garminByteBufferReader.setByteOrder(valueHolder.order());
for (FieldData fieldData : fieldDataList) {
fieldData.parseDataMessage(reader);
fieldData.parseDataMessage(garminByteBufferReader);
}
}
@ -167,9 +166,9 @@ public class RecordData {
valueHolder.position(position);
}
public void parseDataMessage(MessageReader reader) {
private void parseDataMessage(GarminByteBufferReader garminByteBufferReader) {
goToPosition();
valueHolder.put(reader.readBytes(size));
valueHolder.put(garminByteBufferReader.readBytes(size));
}
public void encode(Object... objects) {

View File

@ -4,7 +4,7 @@ import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageReader;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminByteBufferReader;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter;
public class RecordDefinition {
@ -33,35 +33,33 @@ public class RecordDefinition {
this(recordHeader, byteOrder, mesgType, globalMesgNum, null, null);
}
public static RecordDefinition parseIncoming(MessageReader reader, RecordHeader recordHeader) {
public static RecordDefinition parseIncoming(GarminByteBufferReader garminByteBufferReader, RecordHeader recordHeader) {
if (!recordHeader.isDefinition())
return null;
reader.readByte();//ignore
ByteOrder byteOrder = reader.readByte() == 0x01 ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN;
reader.setByteOrder(byteOrder);
final int globalMesgNum = reader.readShort();
garminByteBufferReader.readByte();//ignore
ByteOrder byteOrder = garminByteBufferReader.readByte() == 0x01 ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN;
garminByteBufferReader.setByteOrder(byteOrder);
final int globalMesgNum = garminByteBufferReader.readShort();
RecordDefinition definitionMessage = new RecordDefinition(recordHeader, byteOrder, recordHeader.getMesgType(), globalMesgNum);
final int numFields = reader.readByte();
final int numFields = garminByteBufferReader.readByte();
List<FieldDefinition> fieldDefinitions = new ArrayList<>(numFields);
for (int i = 0; i < numFields; i++) {
fieldDefinitions.add(FieldDefinition.parseIncoming(reader));
fieldDefinitions.add(FieldDefinition.parseIncoming(garminByteBufferReader));
}
definitionMessage.setFieldDefinitions(fieldDefinitions);
if (recordHeader.isDeveloperData()) {
final int numDevFields = reader.readByte();
final int numDevFields = garminByteBufferReader.readByte();
List<DevFieldDefinition> devFieldDefinitions = new ArrayList<>(numDevFields);
for (int i = 0; i < numDevFields; i++) {
devFieldDefinitions.add(DevFieldDefinition.parseIncoming(reader));
devFieldDefinitions.add(DevFieldDefinition.parseIncoming(garminByteBufferReader));
}
definitionMessage.setDevFieldDefinitions(devFieldDefinitions);
}
reader.warnIfLeftover();
return definitionMessage;
}

View File

@ -25,7 +25,6 @@ public class ConfigurationMessage extends GFDIMessage {
public static ConfigurationMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
final int endOfPayload = reader.readByte();
ConfigurationMessage configurationMessage = new ConfigurationMessage(garminMessage, reader.readBytes(endOfPayload - reader.getPosition()));
reader.warnIfLeftover();
return configurationMessage;
}

View File

@ -18,7 +18,6 @@ public class CurrentTimeRequestMessage extends GFDIMessage {
public static CurrentTimeRequestMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
final int referenceID = reader.readInt();
reader.warnIfLeftover();
return new CurrentTimeRequestMessage(referenceID, garminMessage);
}

View File

@ -48,7 +48,6 @@ public class DeviceInformationMessage extends GFDIMessage {
final String deviceName = reader.readString();
final String deviceModel = reader.readString();
reader.warnIfLeftover();
return new DeviceInformationMessage(garminMessage, protocolVersion, productNumber, unitNumber, softwareVersion, maxPacketSize, bluetoothFriendlyName, deviceName, deviceModel);
}

View File

@ -17,7 +17,6 @@ public class FindMyPhoneRequestMessage extends GFDIMessage {
public static FindMyPhoneRequestMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
final int duration = reader.readByte();
reader.warnIfLeftover();
return new FindMyPhoneRequestMessage(garminMessage, duration);
}

View File

@ -24,7 +24,7 @@ public class FitDataMessage extends GFDIMessage {
List<RecordData> recordDataList = new ArrayList<>();
while (!reader.isEndOfPayload()) {
while (reader.remaining() > 0) {
RecordHeader recordHeader = new RecordHeader((byte) reader.readByte());
if (recordHeader.isDefinition())
return null;

View File

@ -24,7 +24,7 @@ public class FitDefinitionMessage extends GFDIMessage {
public static FitDefinitionMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
List<RecordDefinition> recordDefinitions = new ArrayList<>();
while (!reader.isEndOfPayload()) {
while (reader.remaining() > 0) {
RecordHeader recordHeader = new RecordHeader((byte) reader.readByte());
recordDefinitions.add(RecordDefinition.parseIncoming(reader, recordHeader));
}

View File

@ -10,8 +10,10 @@ import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminByteBufferReader;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.GFDIStatusMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.GenericStatusMessage;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public abstract class GFDIMessage {
public static final int MESSAGE_REQUEST = 5001;
@ -45,6 +47,8 @@ public abstract class GFDIMessage {
} catch (Exception e) {
LOG.error("UNHANDLED GFDI MESSAGE TYPE {}, MESSAGE {}", messageType, message);
return new UnhandledMessage(messageType);
} finally {
messageReader.warnIfLeftover();
}
}
@ -156,4 +160,53 @@ public abstract class GFDIMessage {
}
protected static class MessageReader extends GarminByteBufferReader {
private final int payloadSize;
public MessageReader(byte[] data) {
super(data);
this.byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
this.payloadSize = readShort(); //includes CRC
checkSize();
checkCRC();
this.byteBuffer.limit(payloadSize - 2); //remove CRC
}
public int remaining() {
return byteBuffer.remaining();
}
public void skip(int offset) {
if (remaining() < offset) throw new IllegalStateException();
byteBuffer.position(byteBuffer.position() + offset);
}
private void checkSize() {
if (payloadSize != byteBuffer.capacity()) {
LOG.error("Received GFDI packet with invalid length: {} vs {}", payloadSize, byteBuffer.capacity());
throw new IllegalArgumentException("Received GFDI packet with invalid length");
}
}
private void checkCRC() {
final int crc = Short.toUnsignedInt(byteBuffer.getShort(payloadSize - 2));
final int correctCrc = ChecksumCalculator.computeCrc(byteBuffer.asReadOnlyBuffer(), 0, payloadSize - 2);
if (crc != correctCrc) {
LOG.error("Received GFDI packet with invalid CRC: {} vs {}", crc, correctCrc);
throw new IllegalArgumentException("Received GFDI packet with invalid CRC");
}
}
public void warnIfLeftover() {
if (byteBuffer.hasRemaining() && byteBuffer.position() < (byteBuffer.limit())) {
int pos = byteBuffer.position();
int numBytes = (byteBuffer.limit()) - byteBuffer.position();
byte[] leftover = new byte[numBytes];
byteBuffer.get(leftover);
byteBuffer.position(pos);
LOG.warn("Leftover bytes when parsing message. Bytes: {}, complete message: {}", GB.hexdump(leftover), GB.hexdump(byteBuffer.array()));
}
}
}
}

View File

@ -1,131 +0,0 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class MessageReader {
protected static final Logger LOG = LoggerFactory.getLogger(MessageReader.class);
private final ByteBuffer byteBuffer;
private final int payloadSize;
public MessageReader(byte[] data) {
this.byteBuffer = ByteBuffer.wrap(data);
this.byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
this.payloadSize = readShort();
checkSize();
checkCRC();
}
public void setByteOrder(ByteOrder byteOrder) {
this.byteBuffer.order(byteOrder);
}
public boolean isEof() {
return !byteBuffer.hasRemaining();
}
public boolean isEndOfPayload() {
return byteBuffer.position() >= payloadSize - 2;
}
public int getPosition() {
return byteBuffer.position();
}
public void skip(int offset) {
if (byteBuffer.remaining() < offset) throw new IllegalStateException();
byteBuffer.position(byteBuffer.position() + offset);
}
public int readByte() {
if (!byteBuffer.hasRemaining()) throw new IllegalStateException();
return Byte.toUnsignedInt(byteBuffer.get());
}
public int readShort() {
if (byteBuffer.remaining() < 2) throw new IllegalStateException();
return Short.toUnsignedInt(byteBuffer.getShort());
}
public int readInt() {
if (byteBuffer.remaining() < 4) throw new IllegalStateException();
return byteBuffer.getInt();
}
public long readLong() {
if (byteBuffer.remaining() < 8) throw new IllegalStateException();
return byteBuffer.getLong();
}
public float readFloat32() {
if (byteBuffer.remaining() < 4) throw new IllegalStateException();
return byteBuffer.getFloat();
}
public double readFloat64() {
if (byteBuffer.remaining() < 8) throw new IllegalStateException();
return byteBuffer.getDouble();
}
public String readString() {
final int size = readByte();
byte[] bytes = new byte[size];
if (byteBuffer.remaining() < size) throw new IllegalStateException();
byteBuffer.get(bytes);
return new String(bytes, StandardCharsets.UTF_8);
}
public byte[] readBytes(int size) {
byte[] bytes = new byte[size];
if (byteBuffer.remaining() < size) throw new IllegalStateException();
byteBuffer.get(bytes);
return bytes;
}
private int getCapacity() {
return byteBuffer.capacity();
}
private void checkSize() {
if (payloadSize > getCapacity()) {
LOG.error("Received GFDI packet with invalid length: {} vs {}", payloadSize, getCapacity());
throw new IllegalArgumentException("Received GFDI packet with invalid length");
}
}
private void checkCRC() {
final int crc = Short.toUnsignedInt(byteBuffer.getShort(payloadSize - 2));
final int correctCrc = ChecksumCalculator.computeCrc(byteBuffer.asReadOnlyBuffer(), 0, payloadSize - 2);
if (crc != correctCrc) {
LOG.error("Received GFDI packet with invalid CRC: {} vs {}", crc, correctCrc);
throw new IllegalArgumentException("Received GFDI packet with invalid CRC");
}
}
public void warnIfLeftover() {
if (byteBuffer.hasRemaining() && byteBuffer.position() < (byteBuffer.limit() - 2)) {
int pos = byteBuffer.position();
int numBytes = (byteBuffer.limit() - 2) - byteBuffer.position();
byte[] leftover = new byte[numBytes];
byteBuffer.get(leftover);
byteBuffer.position(pos);
LOG.warn("Leftover bytes when parsing message. Bytes: {}, complete message: {}", GB.hexdump(leftover), GB.hexdump(byteBuffer.array()));
}
}
}

View File

@ -14,7 +14,6 @@ public class MusicControlCapabilitiesMessage extends GFDIMessage {
public static MusicControlCapabilitiesMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
final int supportedCapabilities = reader.readByte();
reader.warnIfLeftover();
return new MusicControlCapabilitiesMessage(garminMessage, supportedCapabilities);
}

View File

@ -29,7 +29,6 @@ public class MusicControlMessage extends GFDIMessage {
public static MusicControlMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
MusicControlCapabilitiesMessage.GarminMusicControlCommand command = commands[reader.readByte()];
reader.warnIfLeftover();
return new MusicControlMessage(garminMessage, command);
}

View File

@ -48,7 +48,6 @@ public class ProtobufMessage extends GFDIMessage {
final int protobufDataLength = reader.readInt();
final byte[] messageBytes = reader.readBytes(protobufDataLength);
reader.warnIfLeftover();
return new ProtobufMessage(garminMessage, requestID, dataOffset, totalProtobufLength, protobufDataLength, messageBytes, false);
}

View File

@ -2,8 +2,6 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.sta
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageReader;
public class FitDataStatusMessage extends GFDIStatusMessage {
private final Status status;
@ -27,7 +25,6 @@ public class FitDataStatusMessage extends GFDIStatusMessage {
final Status status = Status.fromCode(reader.readByte());
final FitDataStatusCode fitDataStatusCode = FitDataStatusCode.fromCode(reader.readByte());
reader.warnIfLeftover();
return new FitDataStatusMessage(garminMessage, status, fitDataStatusCode);
}

View File

@ -2,8 +2,6 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.sta
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageReader;
public class FitDefinitionStatusMessage extends GFDIStatusMessage {
private final Status status;
@ -26,7 +24,6 @@ public class FitDefinitionStatusMessage extends GFDIStatusMessage {
final Status status = Status.fromCode(reader.readByte());
final FitDefinitionStatusCode fitDefinitionStatusCode = FitDefinitionStatusCode.fromCode(reader.readByte());
reader.warnIfLeftover();
return new FitDefinitionStatusMessage(garminMessage, status, fitDefinitionStatusCode);
}

View File

@ -2,29 +2,28 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.sta
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.GFDIMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageReader;
public abstract class GFDIStatusMessage extends GFDIMessage {
private Status status;
public static GFDIStatusMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
final GarminMessage originalGarminMessage = GFDIMessage.GarminMessage.fromId(reader.readShort());
int originalMessageType = reader.readShort();
final GarminMessage originalGarminMessage = GFDIMessage.GarminMessage.fromId(originalMessageType);
if (GarminMessage.PROTOBUF_REQUEST.equals(originalGarminMessage) || GarminMessage.PROTOBUF_RESPONSE.equals(originalGarminMessage)) {
return ProtobufStatusMessage.parseIncoming(reader, garminMessage);
return ProtobufStatusMessage.parseIncoming(reader, originalGarminMessage);
} else if (GarminMessage.FIT_DEFINITION.equals(originalGarminMessage)) {
return FitDefinitionStatusMessage.parseIncoming(reader, garminMessage);
return FitDefinitionStatusMessage.parseIncoming(reader, originalGarminMessage);
} else if (GarminMessage.FIT_DATA.equals(originalGarminMessage)) {
return FitDataStatusMessage.parseIncoming(reader, garminMessage);
return FitDataStatusMessage.parseIncoming(reader, originalGarminMessage);
} else {
final Status status = Status.fromCode(reader.readByte());
if (Status.ACK == status) {
LOG.info("Received ACK for message {}", originalGarminMessage.name());
} else {
LOG.warn("Received {} for message {}", status, originalGarminMessage.name());
LOG.warn("Received {} for message {}", status, (null == originalGarminMessage) ? originalMessageType : originalGarminMessage.name());
}
reader.warnIfLeftover();
return new GenericStatusMessage(garminMessage, status);
}
}

View File

@ -2,7 +2,6 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.sta
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageReader;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter;
public class ProtobufStatusMessage extends GFDIStatusMessage {
@ -35,7 +34,6 @@ public class ProtobufStatusMessage extends GFDIStatusMessage {
final ProtobufChunkStatus protobufStatus = ProtobufChunkStatus.fromCode(reader.readByte());
final ProtobufStatusCode error = ProtobufStatusCode.fromCode(reader.readByte());
reader.warnIfLeftover();
return new ProtobufStatusMessage(garminMessage, status, requestID, dataOffset, protobufStatus, error, false);
}