Gadgetbridge/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitFile.java

218 lines
9.1 KiB
Java

package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.ChecksumCalculator;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminByteBufferReader;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter;
public class FitFile {
protected static final Logger LOG = LoggerFactory.getLogger(FitFile.class);
private final Header header;
private final Map<RecordDefinition, List<RecordData>> dataRecords;
private final boolean canGenerateOutput;
public FitFile(Header header, Map<RecordDefinition, List<RecordData>> dataRecords) {
this.header = header;
this.dataRecords = dataRecords;
this.canGenerateOutput = false;
}
public FitFile(LinkedHashMap<RecordDefinition, List<RecordData>> dataRecords) {
this.dataRecords = dataRecords;
this.header = new Header(true, 16, 21117);
this.canGenerateOutput = true;
}
private static byte[] readFileToByteArray(File file) {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); InputStream inputStream = new FileInputStream(file)) {
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
return outputStream.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static FitFile parseIncoming(File file) {
return parseIncoming(readFileToByteArray(file));
}
//TODO: process file in chunks??
public static FitFile parseIncoming(byte[] fileContents) {
final GarminByteBufferReader garminByteBufferReader = new GarminByteBufferReader(fileContents);
garminByteBufferReader.setByteOrder(ByteOrder.LITTLE_ENDIAN);
final Header header = Header.parseIncomingHeader(garminByteBufferReader);
Map<RecordHeader, RecordDefinition> recordDefinitionMap = new HashMap<>(); //needed because the headers can be redefined in the file. The last header wins
Map<RecordDefinition, List<RecordData>> dataRecords = new LinkedHashMap<>();
Long referenceTimestamp = null;
while (garminByteBufferReader.getPosition() < header.getHeaderSize() + header.getDataSize()) {
byte rawRecordHeader = (byte) garminByteBufferReader.readByte();
RecordHeader recordHeader = new RecordHeader(rawRecordHeader);
if (recordHeader.isCompressedTimestamp()) {
referenceTimestamp += recordHeader.getTimeOffset();
recordHeader.setReferenceTimestamp(referenceTimestamp);
}
if (recordHeader.isDefinition()) {
final RecordDefinition recordDefinition = RecordDefinition.parseIncoming(garminByteBufferReader, recordHeader);
if (recordDefinition != null) {
if (recordHeader.isDeveloperData())
for (RecordDefinition rd : dataRecords.keySet()) {
if (GlobalFITMessage.FIELD_DESCRIPTION.equals(rd.getGlobalFITMessage()))
recordDefinition.populateDevFields(dataRecords.get(rd));
}
recordDefinitionMap.put(recordHeader, recordDefinition);
dataRecords.put(recordDefinition, new ArrayList<>());
}
} else {
final RecordDefinition referenceRecordDefinition = recordDefinitionMap.get(recordHeader);
final List<RecordData> myList = dataRecords.get(referenceRecordDefinition);
if (referenceRecordDefinition != null) {
final RecordData runningData = new RecordData(referenceRecordDefinition, recordHeader);
myList.add(runningData);
Long newTimestamp = runningData.parseDataMessage(garminByteBufferReader);
if (newTimestamp != null)
referenceTimestamp = newTimestamp;
}
}
}
garminByteBufferReader.setByteOrder(ByteOrder.LITTLE_ENDIAN);
int fileCrc = garminByteBufferReader.readShort();
if (fileCrc != ChecksumCalculator.computeCrc(fileContents, header.getHeaderSize(), fileContents.length - header.getHeaderSize() - 2)) {
throw new IllegalArgumentException("Wrong CRC for FIT file");
}
return new FitFile(header, dataRecords);
}
public List<RecordData> getRecordsByGlobalMessage(GlobalFITMessage globalFITMessage) {
final List<RecordData> filtered = new ArrayList<>();
for (RecordDefinition rd : dataRecords.keySet()) {
if (globalFITMessage.equals(rd.getGlobalFITMessage()))
filtered.addAll(dataRecords.get(rd));
}
return filtered;
}
public void generateOutgoingDataPayload(MessageWriter writer) {
if (!canGenerateOutput)
throw new IllegalArgumentException("Generation of previously parsed FIT file not supported.");
MessageWriter temporary = new MessageWriter();
temporary.setByteOrder(ByteOrder.LITTLE_ENDIAN);
for (Map.Entry<RecordDefinition, List<RecordData>> entry : dataRecords.entrySet()) {
RecordDefinition key = entry.getKey();
List<RecordData> valueList = entry.getValue();
key.generateOutgoingPayload(temporary);
for (RecordData rd :
valueList) {
rd.generateOutgoingDataPayload(temporary);
}
}
this.header.setDataSize(temporary.getSize());
this.header.generateOutgoingDataPayload(writer);
writer.writeBytes(temporary.getBytes());
writer.writeShort(ChecksumCalculator.computeCrc(writer.getBytes(), this.header.getHeaderSize(), writer.getBytes().length - this.header.getHeaderSize()));
}
@NonNull
@Override
public String toString() {
return dataRecords.toString();
}
static class Header {
public static final int MAGIC = 0x5449462E;
private final int headerSize;
private final int protocolVersion;
private final int profileVersion;
private final boolean hasCRC;
private int dataSize;
public Header(boolean hasCRC, int protocolVersion, int profileVersion) {
this(hasCRC, protocolVersion, profileVersion, 0);
}
public Header(boolean hasCRC, int protocolVersion, int profileVersion, int dataSize) {
this.hasCRC = hasCRC;
headerSize = hasCRC ? 14 : 12;
this.protocolVersion = protocolVersion;
this.profileVersion = profileVersion;
this.dataSize = dataSize;
}
static Header parseIncomingHeader(GarminByteBufferReader garminByteBufferReader) {
int headerSize = garminByteBufferReader.readByte();
if (headerSize < 12) {
throw new IllegalArgumentException("Too short header in FIT file.");
}
boolean hasCRC = headerSize == 14;
int protocolVersion = garminByteBufferReader.readByte();
int profileVersion = garminByteBufferReader.readShort();
int dataSize = garminByteBufferReader.readInt();
int magic = garminByteBufferReader.readInt();
if (magic != MAGIC) {
throw new IllegalArgumentException("Wrong magic header in FIT file");
}
if (hasCRC) {
int incomingCrc = garminByteBufferReader.readShort();
if (incomingCrc != ChecksumCalculator.computeCrc(garminByteBufferReader.asReadOnlyBuffer(), 0, headerSize - 2)) {
throw new IllegalArgumentException("Wrong CRC for header in FIT file");
}
// LOG.info("Fit File Header didn't have CRC, no check performed.");
}
return new Header(hasCRC, protocolVersion, profileVersion, dataSize);
}
public int getHeaderSize() {
return headerSize;
}
public int getDataSize() {
return dataSize;
}
public void setDataSize(int dataSize) {
this.dataSize = dataSize;
}
public void generateOutgoingDataPayload(MessageWriter writer) {
writer.setByteOrder(ByteOrder.LITTLE_ENDIAN);
writer.writeByte(headerSize);
writer.writeByte(protocolVersion);
writer.writeShort(profileVersion);
writer.writeInt(dataSize);
writer.writeInt(MAGIC);//magic
if (hasCRC)
writer.writeShort(ChecksumCalculator.computeCrc(writer.getBytes(), 0, writer.getBytes().length));
}
}
}