mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-08-07 22:41:43 +02:00
157 lines
5.9 KiB
Java
157 lines
5.9 KiB
Java
/* Copyright (C) 2020-2021 Andreas Shimokawa, Daniel Dakhno
|
|
|
|
This file is part of Gadgetbridge.
|
|
|
|
Gadgetbridge is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as published
|
|
by the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
Gadgetbridge is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.parser;
|
|
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.ByteOrder;
|
|
import java.util.ArrayList;
|
|
|
|
public class ActivityFileParser {
|
|
// state flags;
|
|
int heartRateQuality;
|
|
ActivityEntry.WEARING_STATE wearingState = ActivityEntry.WEARING_STATE.UNKNOWN;
|
|
int currentTimestamp = -1;
|
|
ActivityEntry currentSample = null;
|
|
int currentId = 1;
|
|
|
|
public ArrayList<ActivityEntry> parseFile(byte[] file) {
|
|
ByteBuffer buffer = ByteBuffer.wrap(file);
|
|
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
|
|
|
// read file version
|
|
short version = buffer.getShort(2);
|
|
if (version != 22) throw new RuntimeException("File version " + version + ", 16 required");
|
|
|
|
int startTime = buffer.getInt(8);
|
|
short timeOffsetMinutes = buffer.getShort(12);
|
|
|
|
short fileId = buffer.getShort(16);
|
|
|
|
buffer.position(20);
|
|
|
|
ArrayList<ActivityEntry> samples = new ArrayList<>();
|
|
finishCurrentPacket(samples);
|
|
|
|
while (buffer.position() < buffer.capacity() - 4) {
|
|
byte next = buffer.get();
|
|
|
|
if (paraseFlag(next, buffer, samples)) continue;
|
|
|
|
if(currentSample != null) {
|
|
parseVariabilityBytes(next, buffer.get());
|
|
|
|
int heartRate = buffer.get() & 0xFF;
|
|
int calories = buffer.get() & 0xFF;
|
|
boolean isActive = (calories & 0x40) == 0x40; // upper two bits
|
|
calories &= 0x3F; // delete upper two bits
|
|
|
|
currentSample.heartRate = heartRate;
|
|
currentSample.calories = calories;
|
|
currentSample.isActive = isActive;
|
|
finishCurrentPacket(samples);
|
|
}
|
|
}
|
|
return samples;
|
|
}
|
|
|
|
private boolean paraseFlag(byte flag, ByteBuffer buffer, ArrayList<ActivityEntry> samples) {
|
|
switch (flag) {
|
|
case (byte) 0xCA:
|
|
case (byte) 0xCB:
|
|
case (byte) 0xCC:
|
|
case (byte) 0xCD:
|
|
buffer.get();
|
|
break;
|
|
case (byte) 0xCE:
|
|
byte arg = buffer.get();
|
|
byte wearBits = (byte)((arg & 0b00011000) >> 3);
|
|
if(wearBits == 0) this.wearingState = ActivityEntry.WEARING_STATE.NOT_WEARING;
|
|
else if(wearBits == 1) this.wearingState = ActivityEntry.WEARING_STATE.WEARING;
|
|
else this.wearingState = ActivityEntry.WEARING_STATE.UNKNOWN;
|
|
|
|
byte heartRateQualityBits = (byte)((arg & 0b11100000) >> 5);
|
|
this.heartRateQuality = heartRateQualityBits;
|
|
break;
|
|
case (byte) 0xCF:
|
|
case (byte) 0xDE:
|
|
case (byte) 0xDF:
|
|
case (byte) 0xE1:
|
|
buffer.get();
|
|
break;
|
|
case (byte) 0xE2:
|
|
byte type = buffer.get();
|
|
if (type == 0x04) {
|
|
int timestamp = buffer.getInt();
|
|
short duration = buffer.getShort();
|
|
short minutesOffset = buffer.getShort();
|
|
this.currentTimestamp = timestamp;
|
|
}else if(type == 0x09){
|
|
byte[] args = new byte[2];
|
|
buffer.get(args);
|
|
// dunno what to do with that
|
|
}
|
|
break;
|
|
case (byte) 0xDD:
|
|
case (byte) 0xFD:
|
|
buffer.get();
|
|
break;
|
|
case (byte) 0xFE:
|
|
byte arg2 = buffer.get();
|
|
if(arg2 == (byte) 0xFE) {
|
|
// this.currentSample = new ActivitySample();
|
|
// this.currentSample.id = currentId++;
|
|
}
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private void parseVariabilityBytes(byte lower, byte higher){
|
|
if((lower & 0b0000001) == 0b0000001){
|
|
currentSample.maxVariability = (higher & 0b00000011) * 25 + 1;
|
|
currentSample.stepCount = lower & 0b1110;
|
|
if((lower & 0b10000000) == 0b10000000){
|
|
int factor = (lower >> 4) & 0b111;
|
|
currentSample.variability = 512 + factor * 64 + (higher >> 2 & 0b111111);
|
|
}else {
|
|
currentSample.variability = lower & 0b01110000;
|
|
currentSample.variability <<= 2;
|
|
currentSample.variability |= (higher >> 2) & 0b111111;
|
|
}
|
|
}else{
|
|
currentSample.stepCount = lower & 0b11111110;
|
|
currentSample.variability = (int) higher * (int) higher * 64;
|
|
currentSample.maxVariability = 10000;
|
|
}
|
|
}
|
|
|
|
private void finishCurrentPacket(ArrayList<ActivityEntry> samples) {
|
|
if (currentSample != null) {
|
|
currentSample.timestamp = currentTimestamp;
|
|
currentSample.heartRateQuality = this.heartRateQuality;
|
|
currentSample.wearingState = wearingState;
|
|
currentTimestamp += 60;
|
|
samples.add(currentSample);
|
|
currentSample = null;
|
|
}
|
|
this.currentSample = new ActivityEntry();
|
|
this.currentSample.id = currentId++;
|
|
}
|
|
}
|