mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2025-03-13 17:50:23 +01:00
Fossil/Skagen Hybrids: Fix activity parser
Switch to only saving 0xCE as sample data, mark additional known data Remove unused updateSample from testing Add 0xDD handling Fix byte comparisons Add SpO2 parsing, still unused. Cleanup Clean remaining anomalies Fix order sign blunder filtering out all data Remove unproductive/improper debugging prints
This commit is contained in:
parent
20cc75e7d2
commit
f1e26aeb8b
@ -21,12 +21,15 @@ import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class ActivityFileParser {
|
||||
|
||||
// state flags;
|
||||
int heartRateQuality;
|
||||
ActivityEntry.WEARING_STATE wearingState = ActivityEntry.WEARING_STATE.WEARING;
|
||||
int currentTimestamp = -1;
|
||||
int currentTimestamp = 0; // Aligns with `e2 04` from my testing
|
||||
ActivityEntry currentSample = null;
|
||||
int currentId = 1;
|
||||
int spO2 = -1; // Should actually do something with this
|
||||
|
||||
|
||||
public ArrayList<ActivityEntry> parseFile(byte[] file) {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(file);
|
||||
@ -36,90 +39,157 @@ public class ActivityFileParser {
|
||||
short version = buffer.getShort(2);
|
||||
if (version != 22) throw new RuntimeException("File version " + version + ", 16 required");
|
||||
|
||||
int startTime = buffer.getInt(8);
|
||||
this.currentTimestamp = buffer.getInt(8);
|
||||
|
||||
short timeOffsetMinutes = buffer.getShort(12);
|
||||
|
||||
short fileId = buffer.getShort(16);
|
||||
|
||||
buffer.position(20);
|
||||
buffer.position(52); // Seem to be another 32 bytes after the initial 20 stop
|
||||
|
||||
ArrayList<ActivityEntry> samples = new ArrayList<>();
|
||||
finishCurrentPacket(samples);
|
||||
|
||||
|
||||
while (buffer.position() < buffer.capacity() - 4) {
|
||||
byte next = buffer.get();
|
||||
|
||||
if (paraseFlag(next, buffer, samples)) continue;
|
||||
switch (next) {
|
||||
case (byte) 0xCE:
|
||||
parseWearByte(buffer.get());
|
||||
byte f1 = buffer.get();
|
||||
byte f2 = buffer.get();
|
||||
|
||||
if(currentSample != null) {
|
||||
parseVariabilityBytes(next, buffer.get());
|
||||
if (f1 == (byte) 0xE2 && f2 == (byte) 0x04) {
|
||||
int timestamp = buffer.getInt();
|
||||
buffer.getShort(); // duration
|
||||
buffer.getShort(); // minutes offset
|
||||
this.currentTimestamp = timestamp;
|
||||
|
||||
} else if (f1 == (byte) 0xD3) { // Workout-related
|
||||
int hr1 = f2 & 0xFF; // Might be min HR during workout sometimes?
|
||||
byte[] infoB = new byte[2];
|
||||
buffer.get(infoB);
|
||||
|
||||
int heartRate = buffer.get() & 0xFF;
|
||||
int calories = buffer.get() & 0xFF;
|
||||
boolean isActive = (calories & 0x40) == 0x40; // upper two bits
|
||||
calories &= 0x3F; // delete upper two bits
|
||||
byte v1 = buffer.get();
|
||||
byte v2 = buffer.get(buffer.position()); // Could be important for 11 byte packet
|
||||
if (v1 == (byte) 0xDF) {
|
||||
int hr2 = v2 & 0xFF; // Max HR during workout - extra data inside?
|
||||
buffer.get();
|
||||
if (infoB[0] == (byte) 0x08)
|
||||
buffer.get(new byte[11]); // ?
|
||||
|
||||
else if (!elemValidFlags(buffer.get(buffer.position() + 4)))
|
||||
buffer.get(new byte[3]);
|
||||
|
||||
|
||||
currentSample.heartRate = heartRate;
|
||||
currentSample.calories = calories;
|
||||
currentSample.isActive = isActive;
|
||||
finishCurrentPacket(samples);
|
||||
} else if (v1 == (byte) 0xE2 && v2 == (byte) 0x04) {
|
||||
buffer.get(new byte[13]);
|
||||
|
||||
if (!elemValidFlags(buffer.get(buffer.position())))
|
||||
buffer.get(new byte[3]);
|
||||
|
||||
} else if (!elemValidFlags(buffer.get(buffer.position() + 4)))
|
||||
buffer.get();
|
||||
|
||||
|
||||
} else if (f1 == (byte) 0xCF || f1 == (byte) 0xDF) {
|
||||
continue; // Not sure what to do with this
|
||||
|
||||
} else if (f1 == (byte) 0xD6) {
|
||||
buffer.get(new byte[4]);
|
||||
|
||||
} else if (f1 == (byte) 0xFE && f2 == (byte) 0xFE) {
|
||||
if (buffer.get(buffer.position()) == (byte) 0xFE) { buffer.get(); } // WHY?
|
||||
|
||||
} else if (elemValidFlags(buffer.get(buffer.position() + 2))) {
|
||||
parseVariabilityBytes(f1, f2);
|
||||
int heartRate = buffer.get() & 0xFF;
|
||||
int calories = buffer.get() & 0xFF;
|
||||
boolean isActive = (calories & 0x40) == 0x40;
|
||||
calories &= 0x3F;
|
||||
|
||||
currentSample.heartRate = heartRate;
|
||||
currentSample.calories = calories;
|
||||
currentSample.isActive = isActive;
|
||||
finishCurrentPacket(samples);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (buffer.position() > buffer.capacity() - 4) {
|
||||
continue;
|
||||
}
|
||||
|
||||
parseVariabilityBytes(buffer.get(), 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);
|
||||
|
||||
break;
|
||||
|
||||
case (byte) 0xC2: // Or `c2 X` `ac X` as per #2884
|
||||
buffer.get(new byte[3]);
|
||||
|
||||
break;
|
||||
|
||||
case (byte) 0xE2:
|
||||
buffer.get(new byte[9]);
|
||||
|
||||
if (!elemValidFlags(buffer.get(buffer.position()))) {
|
||||
buffer.get(new byte[6]);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case (byte) 0xE0:
|
||||
// Workout Info
|
||||
for (int i = 0; i < 14; i++) {
|
||||
buffer.get(); // Attribute #
|
||||
byte size = buffer.get();
|
||||
buffer.get(new byte[size & 0xFF]); // Can eventually use this, nowhere to pass for now
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case (byte) 0xDD:
|
||||
buffer.get(new byte[20]); // No idea what this is
|
||||
|
||||
break;
|
||||
|
||||
case (byte) 0xD6: // Seems to only come from intentional spot-checks, despite watch's value updating independently on occasion.
|
||||
spO2 = buffer.get() & 0xFF;
|
||||
|
||||
break;
|
||||
|
||||
case (byte) 0xCB: // Very rare, may even be removed
|
||||
case (byte) 0xCC: // Around 73 or 74
|
||||
case (byte) 0xCF: // Almost always 128 (0x80)
|
||||
buffer.get();
|
||||
break;
|
||||
|
||||
default:
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
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;
|
||||
private static boolean elemValidFlags(byte value) {
|
||||
for (byte i : new byte[]{(byte) 0xCE, (byte) 0xDD, (byte) 0xCB, (byte) 0xCC, (byte) 0xCF, (byte) 0xD6, (byte) 0xE2})
|
||||
if (value == i)
|
||||
return true;
|
||||
|
||||
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;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void parseVariabilityBytes(byte lower, byte higher){
|
||||
@ -141,6 +211,16 @@ public class ActivityFileParser {
|
||||
}
|
||||
}
|
||||
|
||||
private void parseWearByte(byte wearArg) {
|
||||
byte wearBits = (byte)((wearArg & 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)((wearArg & 0b11100000) >> 5);
|
||||
this.heartRateQuality = heartRateQualityBits;
|
||||
}
|
||||
|
||||
private void finishCurrentPacket(ArrayList<ActivityEntry> samples) {
|
||||
if (currentSample != null) {
|
||||
currentSample.timestamp = currentTimestamp;
|
||||
|
Loading…
x
Reference in New Issue
Block a user