1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-06-02 03:16:07 +02:00
Gadgetbridge/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/vivomovehr/fit/FitSerializer.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

259 lines
11 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 android.util.SparseBooleanArray;
import android.util.SparseIntArray;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.BinaryUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ChecksumCalculator;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.MessageWriter;
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.List;
public class FitSerializer {
private static final Logger LOG = LoggerFactory.getLogger(FitSerializer.class);
private final SparseBooleanArray knownMessageIDs = new SparseBooleanArray(16);
private final SparseIntArray localMessageIDs = new SparseIntArray(16);
private final SparseArray<FitLocalMessageDefinition> localMessageDefinitions;
// β€œ.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;
public FitSerializer() {
this(new SparseArray<FitLocalMessageDefinition>(16));
}
public FitSerializer(SparseArray<FitLocalMessageDefinition> initialDefinitions) {
this.localMessageDefinitions = initialDefinitions;
for (int i = 0; i < initialDefinitions.size(); ++i) {
final int localId = initialDefinitions.keyAt(i);
final FitLocalMessageDefinition definition = initialDefinitions.valueAt(i);
knownMessageIDs.put(definition.globalDefinition.globalMessageID, true);
localMessageIDs.put(definition.globalDefinition.globalMessageID, localId);
}
}
public byte[] serializeFitFile(List<FitMessage> messages) {
final MessageWriter writer = new MessageWriter();
writer.writeByte(14);
writer.writeByte(0x10);
writer.writeShort(2057);
// dataSize will be rewritten later
writer.writeInt(0);
writer.writeInt(FIT_MAGIC);
// CRC will be rewritten later
writer.writeShort(0);
// first, gather additional needed definitions (if any)
for (final FitMessage message : messages) {
final FitMessageDefinition messageDefinition = message.definition;
final int globalMessageID = messageDefinition.globalMessageID;
if (!knownMessageIDs.get(globalMessageID)) {
LOG.debug("FitSerializer needs to add definition for {}", globalMessageID);
final int localMessageID = localMessageIDs.size() == 0 ? 0 : localMessageIDs.keyAt(localMessageIDs.size() - 1) + 1;
localMessageIDs.put(globalMessageID, localMessageID);
knownMessageIDs.put(globalMessageID, true);
final List<FitMessageFieldDefinition> fieldDefinitions = messageDefinition.fieldDefinitions;
final List<FitLocalFieldDefinition> localFieldDefinitions = new ArrayList<>(fieldDefinitions.size());
for (FitMessageFieldDefinition definition : fieldDefinitions) {
localFieldDefinitions.add(new FitLocalFieldDefinition(definition, definition.fieldSize, definition.fieldType));
}
localMessageDefinitions.put(localMessageID, new FitLocalMessageDefinition(messageDefinition, localFieldDefinitions));
}
}
// now, write definition messages for all used message types
final SparseBooleanArray definedMessages = new SparseBooleanArray();
for (final FitMessage message : messages) {
int localMessageID = localMessageIDs.get(message.definition.globalMessageID);
if (!definedMessages.get(localMessageID)) {
definedMessages.put(localMessageID, true);
writeDefinitionMessage(writer, localMessageID, localMessageDefinitions.get(localMessageID));
}
}
// and now, write the data messages
for (final FitMessage message : messages) {
int localMessageID = localMessageIDs.get(message.definition.globalMessageID);
final FitLocalMessageDefinition localMessageDefinition = localMessageDefinitions.get(localMessageID);
writeDataMessage(writer, message, localMessageID, localMessageDefinition);
}
writer.writeShort(ChecksumCalculator.computeCrc(writer.peekBytes(), 14, writer.getSize() - 14));
final byte[] bytes = writer.getBytes();
// rewrite size
BinaryUtils.writeInt(bytes, 4, bytes.length - 14 - 2);
// rewrite header CRC
BinaryUtils.writeShort(bytes, 12, ChecksumCalculator.computeCrc(bytes, 0, 12));
return bytes;
}
private void writeDefinitionMessage(MessageWriter writer, int localMessageID, FitLocalMessageDefinition localMessageDefinition) {
writer.writeByte(FLAG_DEFINITION_MESSAGE | localMessageID);
writer.writeByte(0);
writer.writeByte(0);
writer.writeShort(localMessageDefinition.globalDefinition.globalMessageID);
writer.writeByte(localMessageDefinition.fieldDefinitions.size());
for (FitLocalFieldDefinition localFieldDefinition : localMessageDefinition.fieldDefinitions) {
writer.writeByte(localFieldDefinition.globalDefinition.fieldNumber);
writer.writeByte(localFieldDefinition.size);
writer.writeByte(localFieldDefinition.baseType.typeID);
}
}
private void writeDataMessage(MessageWriter writer, FitMessage message, int localMessageID, FitLocalMessageDefinition localMessageDefinition) {
writer.writeByte(localMessageID);
for (FitLocalFieldDefinition localFieldDefinition : localMessageDefinition.fieldDefinitions) {
Object value = message.getField(localFieldDefinition.globalDefinition.fieldNumber);
if (value == null) {
value = localFieldDefinition.baseType.invalidValue;
}
writeValue(writer, localFieldDefinition, value);
}
}
private void writeValue(MessageWriter writer, FitLocalFieldDefinition fieldDefinition, Object value) {
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:
writeFitNumber(writer, value, fieldDefinition.size, fieldDefinition.globalDefinition.scale, fieldDefinition.globalDefinition.offset);
break;
case BYTE:
if (fieldDefinition.size == 1) {
writer.writeByte((int) value);
} else {
writer.writeBytes((byte[]) value);
}
break;
case STRING:
writeFitString(writer, (String) value, fieldDefinition.size);
break;
case FLOAT32:
writeFloat32(writer, (float) value);
break;
case FLOAT64:
writeFloat64(writer, (double) value);
break;
default:
throw new IllegalArgumentException("Unable to write value of type " + fieldDefinition.baseType);
}
}
private void writeFitString(MessageWriter writer, String value, int size) {
if (value.length() >= size) throw new IllegalArgumentException("Too long string");
final byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
writer.writeBytes(bytes);
final byte[] zeroes = new byte[size - value.length()];
writer.writeBytes(zeroes);
}
private void writeFloat32(MessageWriter writer, float value) {
writer.writeBytes(ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putFloat(value).array(), 0, 4);
}
private void writeFloat64(MessageWriter writer, double value) {
writer.writeBytes(ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putDouble(value).array(), 0, 4);
}
private void writeFitNumber(MessageWriter writer, Object value, int size, double scale, double offset) {
if (scale == 0) {
writeRawFitNumber(writer, value, size);
} else {
final long rawValue = Math.round((double) value * scale - offset);
switch (size) {
case 1:
writer.writeByte((int) rawValue);
break;
case 2:
writer.writeShort((int) rawValue);
break;
case 4:
writer.writeInt((int) rawValue);
break;
case 8:
writer.writeLong(rawValue);
break;
default:
throw new IllegalArgumentException("Unable to write number of size " + size);
}
}
}
private void writeRawFitNumber(MessageWriter writer, Object value, int size) {
switch (size) {
case 1:
writer.writeByte((int) value);
break;
case 2:
writer.writeShort((int) value);
break;
case 3: {
// this is strange?
byte[] bytes = new byte[4];
BinaryUtils.writeInt(bytes, 0, (int) value);
writer.writeBytes(bytes, 0, 3);
break;
}
case 4:
writer.writeInt((int) value);
break;
case 7: {
// this is strange?
byte[] bytes = new byte[8];
BinaryUtils.writeLong(bytes, 0, (long) value);
writer.writeBytes(bytes, 0, 7);
break;
}
case 8:
writer.writeLong((long) value);
break;
case 12: {
// this is strange? (and probably losing precision anyway)
final double val = (double) value;
final long upper = Math.round(val / Long.MAX_VALUE);
final long lower = Math.round(val - upper);
writer.writeLong(lower);
writer.writeInt((int) upper);
break;
}
case 16: {
// this is strange? (and probably losing precision anyway)
final double val = (double) value;
final long upper = Math.round(val / Long.MAX_VALUE);
final long lower = Math.round(val - upper);
writer.writeLong(lower);
writer.writeLong(upper);
break;
}
default:
throw new IllegalArgumentException("Unable to read number of size " + size);
}
}
}