1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-04 09:17:29 +01:00

Mi Band 8: Basic workout summary parser (wip)

This commit is contained in:
José Rebelo 2023-11-05 21:22:41 +00:00
parent fa20bf66c6
commit 0dcb316461
5 changed files with 179 additions and 29 deletions

View File

@ -1,28 +0,0 @@
/* Copyright (C) 2023 José Rebelo
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.devices.xiaomi;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
public class XiaomiActivitySummaryParser implements ActivitySummaryParser {
@Override
public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary) {
// TODO parse it
return summary;
}
}

View File

@ -52,6 +52,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.SleepRespiratoryRateSample;
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample; import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
import nodomain.freeyourgadget.gadgetbridge.model.StressSample; import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiLanguageType; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiLanguageType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl.WorkoutSummaryParser;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator { public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator {
@ -111,7 +112,7 @@ public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator {
@Nullable @Nullable
@Override @Override
public ActivitySummaryParser getActivitySummaryParser(final GBDevice device) { public ActivitySummaryParser getActivitySummaryParser(final GBDevice device) {
return new XiaomiActivitySummaryParser(); return new WorkoutSummaryParser();
} }
@Override @Override

View File

@ -154,6 +154,7 @@ public class XiaomiActivityFileId {
SPORTS_OUTDOOR_RUNNING(Type.SPORTS, 1), SPORTS_OUTDOOR_RUNNING(Type.SPORTS, 1),
SPORTS_FREESTYLE(Type.SPORTS, 8), SPORTS_FREESTYLE(Type.SPORTS, 8),
SPORTS_ELLIPTICAL(Type.SPORTS, 11), SPORTS_ELLIPTICAL(Type.SPORTS, 11),
SPORTS_OUTDOOR_CYCLING(Type.SPORTS, 23),
; ;
private final Type type; private final Type type;
@ -182,6 +183,7 @@ public class XiaomiActivityFileId {
UNKNOWN(-1), UNKNOWN(-1),
DETAILS(0), DETAILS(0),
SUMMARY(1), SUMMARY(1),
GPS_TRACK(2),
; ;
private final int code; private final int code;

View File

@ -24,6 +24,7 @@ import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl.DailyDetailsParser; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl.DailyDetailsParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl.SleepDetailsParser; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl.SleepDetailsParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl.WorkoutSummaryParser;
public abstract class XiaomiActivityParser { public abstract class XiaomiActivityParser {
private static final Logger LOG = LoggerFactory.getLogger(XiaomiActivityParser.class); private static final Logger LOG = LoggerFactory.getLogger(XiaomiActivityParser.class);
@ -67,6 +68,11 @@ public abstract class XiaomiActivityParser {
private static XiaomiActivityParser createForSports(final XiaomiActivityFileId fileId) { private static XiaomiActivityParser createForSports(final XiaomiActivityFileId fileId) {
assert fileId.getType() == XiaomiActivityFileId.Type.SPORTS; assert fileId.getType() == XiaomiActivityFileId.Type.SPORTS;
switch (fileId.getDetailType()) {
case SUMMARY:
return new WorkoutSummaryParser();
}
return null; return null;
} }
} }

View File

@ -0,0 +1,169 @@
/* Copyright (C) 2023 José Rebelo
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.xiaomi.activity.impl;
import android.widget.Toast;
import org.apache.commons.lang3.ArrayUtils;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Date;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.XiaomiActivityFileId;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.XiaomiActivityParser;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class WorkoutSummaryParser extends XiaomiActivityParser implements ActivitySummaryParser {
private static final Logger LOG = LoggerFactory.getLogger(WorkoutSummaryParser.class);
@Override
public boolean parse(final XiaomiSupport support, final XiaomiActivityFileId fileId, final byte[] bytes) {
BaseActivitySummary summary = new BaseActivitySummary();
summary.setStartTime(fileId.getTimestamp()); // due to a bug this has to be set
summary.setRawSummaryData(ArrayUtils.addAll(fileId.toBytes(), bytes));
try {
summary = parseBinaryData(summary);
} catch (final Exception e) {
LOG.error("Failed to parse workout summary", e);
GB.toast(support.getContext(), "Failed to parse workout summary", Toast.LENGTH_LONG, GB.ERROR, e);
return false;
}
summary.setSummaryData(null); // remove json before saving to database
try (DBHandler dbHandler = GBApplication.acquireDB()) {
final DaoSession session = dbHandler.getDaoSession();
final Device device = DBHelper.getDevice(support.getDevice(), session);
final User user = DBHelper.getUser(session);
summary.setDevice(device);
summary.setUser(user);
session.getBaseActivitySummaryDao().insertOrReplace(summary);
} catch (final Exception e) {
GB.toast(support.getContext(), "Error saving activity summary", Toast.LENGTH_LONG, GB.ERROR, e);
return false;
}
return true;
}
@Override
public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary) {
final JSONObject summaryData = new JSONObject();
final ByteBuffer buf = ByteBuffer.wrap(summary.getRawSummaryData()).order(ByteOrder.LITTLE_ENDIAN);
final XiaomiActivityFileId fileId = XiaomiActivityFileId.from(buf);
final int version = fileId.getVersion();
final int headerSize;
switch (version) {
case 4:
headerSize = 6;
break;
default:
LOG.warn("Unable to parse workout summary version {}", fileId.getVersion());
return null;
}
final byte[] header = new byte[headerSize];
buf.get(header);
final short workoutType = buf.getShort();
switch (workoutType) {
case 6:
summary.setActivityKind(ActivityKind.TYPE_CYCLING);
break;
default:
summary.setActivityKind(ActivityKind.TYPE_UNKNOWN);
}
final int startTime = buf.getInt();
final int endTime = buf.getInt();
summary.setStartTime(new Date(startTime * 1000L));
summary.setEndTime(new Date(endTime * 1000L));
final int duration = buf.getInt();
addSummaryData(summaryData, "activeSeconds", duration, "seconds");
final int unknown1 = buf.getInt();
final int distance = buf.getInt();
addSummaryData(summaryData, "distanceMeters", distance, "meters");
final int unknown2 = buf.getShort();
final int calories = buf.getShort();
addSummaryData(summaryData, "caloriesBurnt", calories, "calories_unit");
final int unknown3 = buf.getInt();
final int unknown4 = buf.getInt();
final float maxSpeed = buf.getFloat();
final float avgHr = buf.get() & 0xff;
final float maxHr = buf.get() & 0xff;
final float minHr = buf.get() & 0xff;
addSummaryData(summaryData, "averageHR", avgHr, "bpm");
addSummaryData(summaryData, "maxHR", maxHr, "bpm");
addSummaryData(summaryData, "minHR", minHr, "bpm");
summary.setSummaryData(summaryData.toString());
return summary;
}
protected void addSummaryData(final JSONObject summaryData, final String key, final float value, final String unit) {
if (value > 0) {
try {
final JSONObject innerData = new JSONObject();
innerData.put("value", value);
innerData.put("unit", unit);
summaryData.put(key, innerData);
} catch (final JSONException ignore) {
}
}
}
protected void addSummaryData(final JSONObject summaryData, final String key, final String value) {
if (key != null && !key.equals("") && value != null && !value.equals("")) {
try {
final JSONObject innerData = new JSONObject();
innerData.put("value", value);
innerData.put("unit", "string");
summaryData.put(key, innerData);
} catch (final JSONException ignore) {
}
}
}
}