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:
Benjamin Swartley 2022-10-15 13:57:23 -04:00
parent 20cc75e7d2
commit f1e26aeb8b

View File

@ -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;