mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Colmi V76: Fix workout parsing
This commit is contained in:
+16
-8
@@ -48,20 +48,21 @@ public class MoyoungWorkoutSummaryParser implements ActivitySummaryParser {
|
|||||||
|
|
||||||
final ActivitySummaryData summaryData = new ActivitySummaryData();
|
final ActivitySummaryData summaryData = new ActivitySummaryData();
|
||||||
|
|
||||||
int protocolVersion = 0;
|
final int protocolVersion;
|
||||||
if (rawSummaryData.length % 24 == 0) {
|
if (rawSummaryData.length % 24 == 0) {
|
||||||
protocolVersion = 1;
|
protocolVersion = 1;
|
||||||
} else if (rawSummaryData.length % 26 == 0) {
|
} else if (rawSummaryData.length % 26 == 0) {
|
||||||
protocolVersion = 2;
|
protocolVersion = 2;
|
||||||
}
|
} else if (rawSummaryData.length % 30 == 0) {
|
||||||
if (protocolVersion == 0) {
|
protocolVersion = 3;
|
||||||
LOG.error("Invalid raw data");
|
} else {
|
||||||
|
LOG.error("Unknown protocol version");
|
||||||
return summary;
|
return summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
byte num = 0;
|
byte num = 0;
|
||||||
byte avgHR = 0;
|
byte avgHR = 0;
|
||||||
if (protocolVersion == 2) {
|
if (protocolVersion == 2 || protocolVersion == 3) {
|
||||||
buffer.get(); // skip packet subtype
|
buffer.get(); // skip packet subtype
|
||||||
num = buffer.get();
|
num = buffer.get();
|
||||||
}
|
}
|
||||||
@@ -70,8 +71,10 @@ public class MoyoungWorkoutSummaryParser implements ActivitySummaryParser {
|
|||||||
int validTime = buffer.getShort();
|
int validTime = buffer.getShort();
|
||||||
if (protocolVersion == 1) {
|
if (protocolVersion == 1) {
|
||||||
num = buffer.get(); // == i
|
num = buffer.get(); // == i
|
||||||
} else {
|
} else if (protocolVersion == 2) {
|
||||||
avgHR = buffer.get();
|
avgHR = buffer.get();
|
||||||
|
} else {
|
||||||
|
buffer.get(); // unknown (always 0x00)
|
||||||
}
|
}
|
||||||
byte type = buffer.get();
|
byte type = buffer.get();
|
||||||
int steps = buffer.getInt();
|
int steps = buffer.getInt();
|
||||||
@@ -79,15 +82,20 @@ public class MoyoungWorkoutSummaryParser implements ActivitySummaryParser {
|
|||||||
int calories;
|
int calories;
|
||||||
if (protocolVersion == 1) {
|
if (protocolVersion == 1) {
|
||||||
calories = buffer.getShort();
|
calories = buffer.getShort();
|
||||||
} else {
|
} else if (protocolVersion == 2) {
|
||||||
calories = buffer.getInt();
|
calories = buffer.getInt();
|
||||||
|
} else {
|
||||||
|
calories = buffer.getShort();
|
||||||
|
avgHR = buffer.get();
|
||||||
|
buffer.get(); // 0?
|
||||||
|
// todo last 4 bytes?
|
||||||
}
|
}
|
||||||
|
|
||||||
summaryData.add(ActivitySummaryEntries.ACTIVE_SECONDS, validTime, ActivitySummaryEntries.UNIT_SECONDS);
|
summaryData.add(ActivitySummaryEntries.ACTIVE_SECONDS, validTime, ActivitySummaryEntries.UNIT_SECONDS);
|
||||||
summaryData.add(ActivitySummaryEntries.STEPS, steps, ActivitySummaryEntries.UNIT_STEPS);
|
summaryData.add(ActivitySummaryEntries.STEPS, steps, ActivitySummaryEntries.UNIT_STEPS);
|
||||||
summaryData.add(ActivitySummaryEntries.DISTANCE_METERS, distance, ActivitySummaryEntries.UNIT_METERS);
|
summaryData.add(ActivitySummaryEntries.DISTANCE_METERS, distance, ActivitySummaryEntries.UNIT_METERS);
|
||||||
summaryData.add(ActivitySummaryEntries.CALORIES_BURNT, calories, ActivitySummaryEntries.UNIT_KCAL);
|
summaryData.add(ActivitySummaryEntries.CALORIES_BURNT, calories, ActivitySummaryEntries.UNIT_KCAL);
|
||||||
if (protocolVersion == 2) {
|
if (avgHR > 0) {
|
||||||
summaryData.add(ActivitySummaryEntries.HR_AVG, avgHR, ActivitySummaryEntries.UNIT_BPM);
|
summaryData.add(ActivitySummaryEntries.HR_AVG, avgHR, ActivitySummaryEntries.UNIT_BPM);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+9
@@ -116,6 +116,15 @@ public class MoyoungActivitySampleProvider extends AbstractSampleProvider<Moyoun
|
|||||||
return MoyoungConstants.WORKOUT_TYPES_TO_ACTIVITY_KIND.getOrDefault((byte) rawType, ActivityKind.ACTIVITY);
|
return MoyoungConstants.WORKOUT_TYPES_TO_ACTIVITY_KIND.getOrDefault((byte) rawType, ActivityKind.ACTIVITY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME we should actually persist the raw kinds...
|
||||||
|
public ActivityKind normalizeTypeV3(int rawType) {
|
||||||
|
return switch (rawType) {
|
||||||
|
case 13 -> ActivityKind.WALKING; // gps on foot?
|
||||||
|
case 14 -> ActivityKind.WALKING;
|
||||||
|
default -> ActivityKind.UNKNOWN;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int toRawActivityKind(ActivityKind activityKind) {
|
public int toRawActivityKind(ActivityKind activityKind) {
|
||||||
if (activityKind == ActivityKind.NOT_MEASURED)
|
if (activityKind == ActivityKind.NOT_MEASURED)
|
||||||
|
|||||||
+26
-12
@@ -1339,26 +1339,33 @@ public class MoyoungDeviceSupport extends AbstractBTLESingleDeviceSupport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void handleTrainingData(byte[] data) {
|
public void handleTrainingData(byte[] data) {
|
||||||
int protocolVersion = 0;
|
final int protocolVersion;
|
||||||
int trainingBytesLength = 24;
|
final int trainingBytesLength;
|
||||||
if (data.length % 24 == 0) {
|
if (data.length % 24 == 0) {
|
||||||
protocolVersion = 1;
|
protocolVersion = 1;
|
||||||
|
trainingBytesLength = 24;
|
||||||
} else if (data.length % 26 == 0) {
|
} else if (data.length % 26 == 0) {
|
||||||
protocolVersion = 2;
|
protocolVersion = 2;
|
||||||
trainingBytesLength = 26;
|
trainingBytesLength = 26;
|
||||||
}
|
} else if (data.length % 30 == 0) {
|
||||||
if (protocolVersion == 0) {
|
protocolVersion = 3;
|
||||||
LOG.error("Invalid training data received");
|
trainingBytesLength = 30;
|
||||||
|
} else {
|
||||||
|
LOG.error("Invalid training data received of length {}", data.length);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < data.length / trainingBytesLength; i++) {
|
for (int i = 0; i < data.length / trainingBytesLength; i++) {
|
||||||
if (protocolVersion == 1 && ArrayUtils.isAllZeros(data, trainingBytesLength * i, trainingBytesLength)) {
|
if (protocolVersion == 1 && ArrayUtils.isAllZeros(data, trainingBytesLength * i, trainingBytesLength)) {
|
||||||
LOG.info("Skipping empty workout details packet");
|
LOG.info("Skipping empty workout details packet v1");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (protocolVersion == 2 && ArrayUtils.isAllZeros(data, 2, trainingBytesLength - 2)) {
|
if (protocolVersion == 2 && ArrayUtils.isAllZeros(data, 2, trainingBytesLength - 2)) {
|
||||||
LOG.info("Skipping empty workout details packet");
|
LOG.info("Skipping empty workout details packet v2");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (protocolVersion == 3 && ArrayUtils.isAllZeros(data, trainingBytesLength * i + 2, 24)) {
|
||||||
|
LOG.info("Skipping empty workout details packet v3");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1366,7 +1373,7 @@ public class MoyoungDeviceSupport extends AbstractBTLESingleDeviceSupport {
|
|||||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
byte num = 0;
|
byte num = 0;
|
||||||
byte avgHR = 0;
|
byte avgHR = 0;
|
||||||
if (protocolVersion == 2) {
|
if (protocolVersion == 2 || protocolVersion == 3) {
|
||||||
buffer.get(); // skip packet subtype
|
buffer.get(); // skip packet subtype
|
||||||
num = buffer.get();
|
num = buffer.get();
|
||||||
}
|
}
|
||||||
@@ -1375,8 +1382,10 @@ public class MoyoungDeviceSupport extends AbstractBTLESingleDeviceSupport {
|
|||||||
int validTime = buffer.getShort();
|
int validTime = buffer.getShort();
|
||||||
if (protocolVersion == 1) {
|
if (protocolVersion == 1) {
|
||||||
num = buffer.get(); // == i
|
num = buffer.get(); // == i
|
||||||
} else {
|
} else if (protocolVersion == 2) {
|
||||||
avgHR = buffer.get();
|
avgHR = buffer.get();
|
||||||
|
} else {
|
||||||
|
buffer.get(); // unknown (always 0x00)
|
||||||
}
|
}
|
||||||
byte type = buffer.get();
|
byte type = buffer.get();
|
||||||
int steps = buffer.getInt();
|
int steps = buffer.getInt();
|
||||||
@@ -1384,8 +1393,13 @@ public class MoyoungDeviceSupport extends AbstractBTLESingleDeviceSupport {
|
|||||||
int calories;
|
int calories;
|
||||||
if (protocolVersion == 1) {
|
if (protocolVersion == 1) {
|
||||||
calories = buffer.getShort();
|
calories = buffer.getShort();
|
||||||
} else {
|
} else if (protocolVersion == 2) {
|
||||||
calories = buffer.getInt();
|
calories = buffer.getInt();
|
||||||
|
} else {
|
||||||
|
calories = buffer.getShort();
|
||||||
|
avgHR = buffer.get();
|
||||||
|
buffer.get(); // 0?
|
||||||
|
// todo last 4 bytes?
|
||||||
}
|
}
|
||||||
LOG.info("Training data: start={} end={} totalTimeWithoutPause={} num={} type={} steps={} avgHR={} distance={} calories={}", startTime, endTime, validTime, num, type, steps, avgHR, distance, calories);
|
LOG.info("Training data: start={} end={} totalTimeWithoutPause={} num={} type={} steps={} avgHR={} distance={} calories={}", startTime, endTime, validTime, num, type, steps, avgHR, distance, calories);
|
||||||
|
|
||||||
@@ -1415,8 +1429,8 @@ public class MoyoungDeviceSupport extends AbstractBTLESingleDeviceSupport {
|
|||||||
summary.setDevice(device);
|
summary.setDevice(device);
|
||||||
summary.setUser(user);
|
summary.setUser(user);
|
||||||
|
|
||||||
ActivityKind gbType = provider.normalizeType(type);
|
ActivityKind gbType = protocolVersion < 3 ? provider.normalizeType(type) : provider.normalizeTypeV3(type);
|
||||||
summary.setName(gbType.name());
|
summary.setName(gbType.getLabel(getContext()));
|
||||||
summary.setActivityKind(gbType.getCode());
|
summary.setActivityKind(gbType.getCode());
|
||||||
|
|
||||||
summary.setStartTime(startTime);
|
summary.setStartTime(startTime);
|
||||||
|
|||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.devices.moyoung;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.test.TestBase;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
|
||||||
|
public class MoyoungWorkoutSummaryParserTest extends TestBase {
|
||||||
|
@Test
|
||||||
|
public void testParseWorkout1() {
|
||||||
|
// #6053 - 00:36:34, 2.3km, 163kcal, 15'41'', 2767 steps, 76 spm, 89bpm
|
||||||
|
final BaseActivitySummary summary = new BaseActivitySummary();
|
||||||
|
summary.setRawSummaryData(GB.hexStringToByteArray("030D257C126AB984126A92080022CF0A00001A090000A3005900A9C1C440"));
|
||||||
|
|
||||||
|
new MoyoungWorkoutSummaryParser(null).parseBinaryData(summary, false);
|
||||||
|
|
||||||
|
final ActivitySummaryData summaryData = ActivitySummaryData.fromJson(summary.getSummaryData());
|
||||||
|
assertNotNull(summaryData);
|
||||||
|
|
||||||
|
assertEquals((double) (36 * 60 + 34), summaryData.getNumber(ActivitySummaryEntries.ACTIVE_SECONDS, -1));
|
||||||
|
assertEquals(2330d, summaryData.getNumber(ActivitySummaryEntries.DISTANCE_METERS, -1));
|
||||||
|
assertEquals(163d, summaryData.getNumber(ActivitySummaryEntries.CALORIES_BURNT, -1));
|
||||||
|
assertEquals(2767d, summaryData.getNumber(ActivitySummaryEntries.STEPS, -1));
|
||||||
|
assertEquals(89d, summaryData.getNumber(ActivitySummaryEntries.HR_AVG, -1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testParseWorkout2() {
|
||||||
|
// #6053 - 00:32:01, 1.7km, 122kcal, 18'20'', 2011 steps, 62 spm, 93bpm
|
||||||
|
final BaseActivitySummary summary = new BaseActivitySummary();
|
||||||
|
summary.setRawSummaryData(GB.hexStringToByteArray("030E9BAF136A1CB7136A8107001EDB070000D20600007A005D003DB8C740"));
|
||||||
|
|
||||||
|
new MoyoungWorkoutSummaryParser(null).parseBinaryData(summary, false);
|
||||||
|
|
||||||
|
final ActivitySummaryData summaryData = ActivitySummaryData.fromJson(summary.getSummaryData());
|
||||||
|
assertNotNull(summaryData);
|
||||||
|
|
||||||
|
assertEquals((double) (32 * 60 + 1), summaryData.getNumber(ActivitySummaryEntries.ACTIVE_SECONDS, -1));
|
||||||
|
assertEquals(1746d, summaryData.getNumber(ActivitySummaryEntries.DISTANCE_METERS, -1));
|
||||||
|
assertEquals(122d, summaryData.getNumber(ActivitySummaryEntries.CALORIES_BURNT, -1));
|
||||||
|
assertEquals(2011d, summaryData.getNumber(ActivitySummaryEntries.STEPS, -1));
|
||||||
|
assertEquals(93d, summaryData.getNumber(ActivitySummaryEntries.HR_AVG, -1));
|
||||||
|
}
|
||||||
|
}
|
||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.moyoung;
|
||||||
|
|
||||||
|
import android.util.Pair;
|
||||||
|
|
||||||
|
import junit.framework.TestCase;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.test.TestBase;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||||
|
|
||||||
|
public class MoyoungDeviceSupportTest extends TestBase {
|
||||||
|
@Test
|
||||||
|
public void testWorkoutColumnV76() {
|
||||||
|
final MoyoungDeviceSupport support = new MoyoungDeviceSupport();
|
||||||
|
// #6053
|
||||||
|
final byte[][] packets = new byte[][] {
|
||||||
|
GB.hexStringToByteArray("FEEA2023B2030000000000000000000000000000000000000000000000000000000000"),
|
||||||
|
GB.hexStringToByteArray("FEEA2023B2030100000000000000000000000000000000000000000000000000000000"),
|
||||||
|
GB.hexStringToByteArray("FEEA2023B2030200000000000000000000000000000000000000000000000000000000"),
|
||||||
|
GB.hexStringToByteArray("FEEA2023B2030300000000000000000000000000000000000000000000000000000000"),
|
||||||
|
GB.hexStringToByteArray("FEEA2023B2030400000000000000000000000000000000000000000000000000000000"),
|
||||||
|
GB.hexStringToByteArray("FEEA2023B2030500000000000000000000000000000000000000000000000000000000"),
|
||||||
|
GB.hexStringToByteArray("FEEA2023B2030600000000000000000000000000000000000000000000000000000000"),
|
||||||
|
GB.hexStringToByteArray("FEEA2023B2030700000000000000000000000000000000000000000000000000000000"),
|
||||||
|
GB.hexStringToByteArray("FEEA2023B2030800000000000000000000000000000000000000000000000000000000"),
|
||||||
|
GB.hexStringToByteArray("FEEA2023B2030900000000000000000000000000000000000000000000000000000000"),
|
||||||
|
GB.hexStringToByteArray("FEEA2023B2030A00000000000000000000000000000000000000000000000000000000"),
|
||||||
|
GB.hexStringToByteArray("FEEA2023B2030B00000000000000000000000000000000000000000000000000000000"),
|
||||||
|
GB.hexStringToByteArray("FEEA2023B2030C00000000000000000000000000000000000000000000000052B8CB41"),
|
||||||
|
|
||||||
|
// 00:36:34
|
||||||
|
// 2.3km
|
||||||
|
// 163kcal
|
||||||
|
// 15'41"
|
||||||
|
// 2767 steps
|
||||||
|
// 76 steps/min
|
||||||
|
// 80bpm
|
||||||
|
GB.hexStringToByteArray("FEEA2023B2030D257C126AB984126A92080022CF0A00001A090000A3005900A9C1C440"),
|
||||||
|
|
||||||
|
// 00:32:01
|
||||||
|
// 1.7km
|
||||||
|
// 122kcal
|
||||||
|
// 18'20"
|
||||||
|
// GPS trace
|
||||||
|
// 2011 steps
|
||||||
|
// 62 steps/min
|
||||||
|
// 93bpm
|
||||||
|
GB.hexStringToByteArray("FEEA2023B2030E9BAF136A1CB7136A8107001EDB070000D20600007A005D003DB8C740")
|
||||||
|
};
|
||||||
|
MoyoungPacketIn packetIn = new MoyoungPacketIn();
|
||||||
|
for (byte[] value : packets) {
|
||||||
|
if (packetIn.putFragment(value)) {
|
||||||
|
Pair<Byte, byte[]> packet = MoyoungPacketIn.parsePacket(packetIn.getPacket());
|
||||||
|
packetIn = new MoyoungPacketIn();
|
||||||
|
|
||||||
|
if (packet != null) {
|
||||||
|
byte packetType = packet.first;
|
||||||
|
byte[] payload = packet.second;
|
||||||
|
support.handleTrainingData(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user