1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-06-09 22:57:54 +02:00
Gadgetbridge/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/vivomovehr/fit/FitParser.java
Mormegil 3a58314db6 Garmin Vivomove HR support
- communication protocols
- device support implementation
- download FIT file storage

Features:
- basic connectivity: time sync, battery status, HW/FW version info
- real-time activity tracking
- fitness data sync
- find the device, find the phone
- factory reset

Features implemented but not working:
- notifications: fully implemented, seem to communicate correctly, but not shown on watch

Features implemented partially (not expected to work now):
- weather information (and in future possibly weather alerts)
- music info
- firmware update: only the initial file upload implemented, not used

Things to improve/change:
- Device name hardcoded in `VivomoveHrCoordinator.getSupportedType`, service UUIDs not available
- Download FIT file storage: Should be store (and offer the user to export?) the FIT data forever?
- Obviously, various code improvements, cleanup, etc.
2023-07-20 20:30:14 +00:00

286 lines
13 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.fit;
import android.util.SparseArray;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.MessageReader;
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class FitParser {
private static final Logger LOG = LoggerFactory.getLogger(FitParser.class);
// β€œ.FIT” – magic value indicating a .FIT file
private static final int FIT_MAGIC = 0x5449462E;
private static final int FLAG_NORMAL_HEADER = 0x80;
private static final int FLAG_DEFINITION_MESSAGE = 0x40;
private static final int FLAG_DEVELOPER_FIELDS = 0x20;
private static final int MASK_LOCAL_MESSAGE_TYPE = 0x0F;
private static final int MASK_TIME_OFFSET = 0x1F;
private static final int MASK_COMPRESSED_LOCAL_MESSAGE_TYPE = 0x60;
private final SparseArray<FitMessageDefinition> globalMessageDefinitions;
private final SparseArray<FitLocalMessageDefinition> localMessageDefinitions = new SparseArray<>(16);
public FitParser(Collection<FitMessageDefinition> knownDefinitions) {
globalMessageDefinitions = new SparseArray<>(knownDefinitions.size());
for (FitMessageDefinition definition : knownDefinitions) {
globalMessageDefinitions.append(definition.globalMessageID, definition);
}
}
public SparseArray<FitLocalMessageDefinition> getLocalMessageDefinitions() {
return localMessageDefinitions;
}
public List<FitMessage> parseFitFile(byte[] data) {
if (data.length < 12) throw new IllegalArgumentException("Too short data");
final MessageReader reader = new MessageReader(data);
final List<FitMessage> result = new ArrayList<>();
while (!reader.isEof()) {
final int fileHeaderStart = reader.getPosition();
final int fileHeaderSize = reader.readByte();
final int protocolVersion = reader.readByte();
final int profileVersion = reader.readShort();
final int dataSize = reader.readInt();
final int dataTypeMagic = reader.readInt();
final int headerCrc = fileHeaderSize >= 14 ? reader.readShort() : 0;
if (dataTypeMagic != FIT_MAGIC) {
throw new IllegalArgumentException("Not a FIT file, data type signature not found");
}
if (fileHeaderSize < 12) throw new IllegalArgumentException("Header size too low");
reader.skip(fileHeaderSize - 14);
// TODO: Check header CRC
localMessageDefinitions.clear();
int lastTimestamp = 0;
final int end = fileHeaderStart + fileHeaderSize + dataSize;
while (reader.getPosition() < end) {
final int recordHeader = reader.readByte();
final boolean isDefinitionMessage;
final int localMessageType;
final int currentTimestamp;
if ((recordHeader & FLAG_NORMAL_HEADER) == 0) {
// normal header
isDefinitionMessage = (recordHeader & FLAG_DEFINITION_MESSAGE) != 0;
localMessageType = recordHeader & MASK_LOCAL_MESSAGE_TYPE;
currentTimestamp = -1;
} else {
// compressed timestamp header
final int timestampOffset = recordHeader & MASK_TIME_OFFSET;
localMessageType = (recordHeader & MASK_COMPRESSED_LOCAL_MESSAGE_TYPE) >> 4;
currentTimestamp = lastTimestamp + timestampOffset;
isDefinitionMessage = false;
throw new IllegalArgumentException("Compressed timestamps not supported yet");
}
if (isDefinitionMessage) {
final boolean hasDeveloperFields = (recordHeader & FLAG_DEVELOPER_FIELDS) != 0;
final FitLocalMessageDefinition definition = parseDefinitionMessage(reader, hasDeveloperFields);
LOG.trace("Defining local message {} to global message {}", localMessageType, definition.globalDefinition.globalMessageID);
localMessageDefinitions.put(localMessageType, definition);
} else {
final FitLocalMessageDefinition definition = localMessageDefinitions.get(localMessageType);
if (definition == null) {
LOG.error("Use of undefined local message {}", localMessageType);
throw new IllegalArgumentException("Use of undefined local message " + localMessageType);
}
final FitMessage dataMessage = new FitMessage(definition.globalDefinition);
parseDataMessage(reader, definition, dataMessage);
result.add(dataMessage);
}
}
final int fileCrc = reader.readShort();
// TODO: Check file CRC
}
return result;
}
private void parseDataMessage(MessageReader reader, FitLocalMessageDefinition localMessageDefinition, FitMessage dataMessage) {
for (FitLocalFieldDefinition localFieldDefinition : localMessageDefinition.fieldDefinitions) {
final Object value = readValue(reader, localFieldDefinition);
if (!localFieldDefinition.baseType.invalidValue.equals(value)) {
dataMessage.setField(localFieldDefinition.globalDefinition.fieldNumber, value);
}
}
}
private Object readValue(MessageReader reader, FitLocalFieldDefinition fieldDefinition) {
//switch (fieldDefinition.baseType) {
switch (fieldDefinition.globalDefinition.fieldType) {
case ENUM:
case SINT8:
case UINT8:
case SINT16:
case UINT16:
case SINT32:
case UINT32:
case UINT8Z:
case UINT16Z:
case UINT32Z:
case SINT64:
case UINT64:
case UINT64Z:
return readFitNumber(reader, fieldDefinition.size, fieldDefinition.globalDefinition.scale, fieldDefinition.globalDefinition.offset);
case BYTE:
return fieldDefinition.size == 1 ? reader.readByte() : reader.readBytes(fieldDefinition.size);
case STRING:
return readFitString(reader, fieldDefinition.size);
case FLOAT32:
return readFloat32(reader, fieldDefinition.size);
case FLOAT64:
return readFloat64(reader, fieldDefinition.size);
// TODO: Float data types
default:
throw new IllegalArgumentException("Unable to read value of type " + fieldDefinition.baseType);
}
}
private float readFloat32(MessageReader reader, int size) {
if (size != 4) {
throw new IllegalArgumentException("Invalid size for Float32: " + size);
}
final byte[] bytes = reader.readBytes(size);
return ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getFloat();
}
private double readFloat64(MessageReader reader, int size) {
if (size != 8) {
throw new IllegalArgumentException("Invalid size for Float64: " + size);
}
final byte[] bytes = reader.readBytes(size);
return ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getDouble();
}
private String readFitString(MessageReader reader, int size) {
final byte[] bytes = reader.readBytes(size);
final int zero = ArrayUtils.indexOf((byte) 0, bytes);
if (zero < 0) {
LOG.warn("Unterminated string");
return new String(bytes, StandardCharsets.UTF_8);
}
return new String(bytes, 0, zero, StandardCharsets.UTF_8);
}
private Object readRawFitNumber(MessageReader reader, int size) {
switch (size) {
case 1:
return reader.readByte();
case 2:
return reader.readShort();
case 3: {
// this is strange?
byte[] bytes = new byte[4];
reader.readBytesTo(3, bytes, 0);
return BinaryUtils.readInt(bytes, 0);
}
case 4:
return reader.readInt();
case 7: {
// this is strange?
byte[] bytes = new byte[8];
reader.readBytesTo(7, bytes, 0);
return BinaryUtils.readLong(bytes, 0);
}
case 8:
return reader.readLong();
case 12:
// this is strange?
long lower = reader.readLong();
int upper = reader.readInt();
return upper * ((double) Long.MAX_VALUE) + lower;
case 16:
// this is strange?
return reader.readLong() + reader.readLong() * (double) (Long.MAX_VALUE);
case 32:
// this is strange?
// TODO: FIXME: 32-byte integer?!?
reader.skip(16);
return Math.pow(2, 128) * (reader.readLong() + reader.readLong() * (double) (Long.MAX_VALUE));
default:
throw new IllegalArgumentException("Unable to read number of size " + size);
}
}
private Object readFitNumber(MessageReader reader, int size, double scale, double offset) {
if (scale == 0) {
return readRawFitNumber(reader, size);
} else {
switch (size) {
case 1:
return reader.readByte() / scale + offset;
case 2:
return reader.readShort() / scale + offset;
case 4:
return reader.readInt() / scale + offset;
case 8:
return reader.readLong() / scale + offset;
default:
throw new IllegalArgumentException("Unable to read number of size " + size);
}
}
}
private FitLocalMessageDefinition parseDefinitionMessage(MessageReader reader, boolean hasDeveloperFields) {
reader.skip(1);
final int architecture = reader.readByte();
final boolean isBigEndian = architecture == 1;
if (isBigEndian) throw new IllegalArgumentException("Big-endian data not supported yet");
final int globalMessageType = reader.readShort();
final FitMessageDefinition messageDefinition = getGlobalDefinition(globalMessageType);
final int fieldCount = reader.readByte();
final List<FitLocalFieldDefinition> fields = new ArrayList<>(fieldCount);
for (int i = 0; i < fieldCount; ++i) {
final int globalField = reader.readByte();
final int size = reader.readByte();
final int baseTypeNum = reader.readByte();
final FitFieldBaseType baseType = FitFieldBaseType.decodeTypeID(baseTypeNum);
final FitMessageFieldDefinition globalFieldDefinition = getFieldDefinition(messageDefinition, globalField, size, baseType);
fields.add(new FitLocalFieldDefinition(globalFieldDefinition, size, baseType));
}
if (hasDeveloperFields) {
final int developerFieldCount = reader.readByte();
if (developerFieldCount != 0) throw new IllegalArgumentException("Developer fields not supported yet");
}
return new FitLocalMessageDefinition(messageDefinition, fields);
}
private FitMessageFieldDefinition getFieldDefinition(FitMessageDefinition messageDefinition, int field, int size, FitFieldBaseType baseType) {
final FitMessageFieldDefinition definition = messageDefinition.getField(field);
if (definition != null) return definition;
LOG.warn("Unknown field {} in message {}", field, messageDefinition.globalMessageID);
// System.out.println(String.format(Locale.ROOT, "Unknown field %d in message %d", field, messageDefinition.globalMessageID));
final FitMessageFieldDefinition newDefinition = new FitMessageFieldDefinition("unknown_" + field, field, size, baseType, baseType.invalidValue);
messageDefinition.addField(newDefinition);
return newDefinition;
}
private FitMessageDefinition getGlobalDefinition(int globalMessageType) {
final FitMessageDefinition messageDefinition = globalMessageDefinitions.get(globalMessageType);
if (messageDefinition != null) return messageDefinition;
LOG.warn("Unknown global message {}", globalMessageType);
// System.out.println(String.format(Locale.ROOT, "Unknown message %d", globalMessageType));
final FitMessageDefinition newDefinition = new FitMessageDefinition("unknown_" + globalMessageType, globalMessageType, 0);
globalMessageDefinitions.append(globalMessageType, newDefinition);
return newDefinition;
}
}