wangzhibo
4 天以前 ae6f40460dcd56af6c5f60ba52c883854c3bac55
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
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.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.fit.messages.FitRecordDataFactory;
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 List<RecordData> dataRecords;
    private final boolean canGenerateOutput;
 
    public FitFile(Header header, List<RecordData> dataRecords) {
        this.header = header;
        this.dataRecords = dataRecords;
        this.canGenerateOutput = false;
    }
 
    public FitFile(List<RecordData> dataRecords) {
        this.dataRecords = dataRecords;
        this.header = new Header(true, 16, 21117);
        this.canGenerateOutput = true;
    }
 
    private static byte[] readFileToByteArray(File file) throws IOException {
        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();
        }
    }
 
    public static FitFile parseIncoming(File file) throws IOException {
        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);
 
        // needed because the headers can be redefined in the file. The last header for a local message number wins
        Map<Integer, RecordDefinition> recordDefinitionMap = new HashMap<>();
        List<RecordData> dataRecords = new ArrayList<>();
        Long referenceTimestamp = null;
 
        while (garminByteBufferReader.getPosition() < header.getHeaderSize() + header.getDataSize()) {
            byte rawRecordHeader = (byte) garminByteBufferReader.readByte();
            RecordHeader recordHeader = new RecordHeader(rawRecordHeader);
            final Integer timeOffset = recordHeader.getTimeOffset();
            if (timeOffset != null) {
                if (referenceTimestamp == null) {
                    throw new IllegalArgumentException("Got compressed timestamp without knowing current timestamp");
                }
 
                if (timeOffset >= (referenceTimestamp & 0x1FL)) {
                    referenceTimestamp = (referenceTimestamp & ~0x1FL) + timeOffset;
                } else if (timeOffset < (referenceTimestamp & 0x1FL)) {
                    referenceTimestamp = (referenceTimestamp & ~0x1FL) + timeOffset + 0x20;
                }
            }
            if (recordHeader.isDefinition()) {
                final RecordDefinition recordDefinition = RecordDefinition.parseIncoming(garminByteBufferReader, recordHeader);
                if (recordDefinition != null) {
                    if (recordHeader.isDeveloperData())
                        for (RecordData rd : dataRecords) {
                            if (GlobalFITMessage.FIELD_DESCRIPTION.equals(rd.getGlobalFITMessage()))
                                recordDefinition.populateDevFields(rd);
                        }
                    recordDefinitionMap.put(recordHeader.getLocalMessageType(), recordDefinition);
                }
            } else {
                final RecordDefinition referenceRecordDefinition = recordDefinitionMap.get(recordHeader.getLocalMessageType());
                if (referenceRecordDefinition != null) {
                    final RecordData runningData = FitRecordDataFactory.create(referenceRecordDefinition, recordHeader);
                    dataRecords.add(runningData);
                    Long newTimestamp = runningData.parseDataMessage(garminByteBufferReader, referenceTimestamp);
                    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 (RecordData rd : dataRecords) {
            if (globalFITMessage.equals(rd.getGlobalFITMessage()))
                filtered.add(rd);
        }
        return filtered;
    }
 
    public List<RecordData> getRecords() {
        return dataRecords;
    }
 
    public void generateOutgoingDataPayload(MessageWriter writer) {
        if (!canGenerateOutput)
            throw new IllegalArgumentException("Generation of previously parsed FIT file not supported.");
 
        MessageWriter temporary = new MessageWriter(writer.getLimit());
        temporary.setByteOrder(ByteOrder.LITTLE_ENDIAN);
        RecordDefinition prevDefinition = null;
        for (final RecordData rd : dataRecords) {
            if (!rd.getRecordDefinition().equals(prevDefinition)) {
                rd.getRecordDefinition().generateOutgoingPayload(temporary);
                prevDefinition = rd.getRecordDefinition();
            }
 
            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()));
    }
 
    public byte[] getOutgoingMessage() {
        // Compute the worst case scenario buffer size for the fit file
        // A ~1.6MB gpx file with ~16k points results in a ~320KB buffer, ~150KB of which get actually used
        final int dataRecordsSize = dataRecords.stream()
                .mapToInt(r -> {
                    // Worst case scenario, for each data record
 
                    // one distinct record definition: 5 bytes + (number of field definitions * 3 + 1)
                    final List<FieldDefinition> definitions = r.getRecordDefinition().getFieldDefinitions();
                    final int recordDefinitionOverhead = 5 + (definitions != null ? definitions.size() * 3 + 1 : 0);
 
                    // 1 + size of the value holder
                    final int dataRecordOverhead = 1 + r.valueHolder.limit();
 
                    return recordDefinitionOverhead + dataRecordOverhead;
                }).sum();
 
        // Final size = 14b header + data records + 2b crc
        final MessageWriter writer = new MessageWriter(14 + dataRecordsSize + 2);
        this.generateOutgoingDataPayload(writer);
        return writer.getBytes();
    }
 
    @NonNull
    @Override
    public String toString() {
        return dataRecords.toString();
    }
 
    public 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 != 0 && 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));
        }
 
    }
}