diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java index 570b76ee8..825c2c68d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java @@ -413,8 +413,7 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport { @Override public void onFetchRecordedData(final int dataTypes) { - // TODO - super.onFetchRecordedData(dataTypes); + healthService.onFetchRecordedData(dataTypes); } @Override @@ -514,7 +513,8 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport { final byte[] commandBytes = command.toByteArray(); final byte[] encryptedCommandBytes = authService.encrypt(commandBytes, encryptedIndex); final int commandLength = 6 + encryptedCommandBytes.length; - if (commandLength > getMTU()) { + if (getMTU() != 0 && commandLength > getMTU()) { + // TODO MTU is 0 sometimes? LOG.warn("Command with {} bytes is too large for MTU of {}", commandLength, getMTU()); } @@ -524,7 +524,7 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport { buf.put((byte) 1); // 1 for encrypted buf.putShort(encryptedIndex++); buf.put(encryptedCommandBytes); - LOG.debug("Sending command {} as {}", GB.hexdump(commandBytes), GB.hexdump(buf.array())); + LOG.debug("Sending command {}", GB.hexdump(commandBytes)); builder.write(getCharacteristic(UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE), buf.array()); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityParser.java new file mode 100644 index 000000000..026172577 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityParser.java @@ -0,0 +1,21 @@ +/* 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 . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity; + +public class XiaomiActivityParser { + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiHealthService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiHealthService.java index 70f2cd1fd..45aa6e760 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiHealthService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiHealthService.java @@ -23,6 +23,8 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; @@ -47,6 +49,7 @@ import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiPreferences; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport; +import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class XiaomiHealthService extends AbstractXiaomiService { @@ -55,6 +58,8 @@ public class XiaomiHealthService extends AbstractXiaomiService { public static final int COMMAND_TYPE = 8; private static final int CMD_SET_USER_INFO = 0; + private static final int CMD_ACTIVITY_FETCH_1 = 1; + private static final int CMD_ACTIVITY_FETCH_2 = 2; private static final int CMD_CONFIG_SPO2_GET = 8; private static final int CMD_CONFIG_SPO2_SET = 9; private static final int CMD_CONFIG_HEART_RATE_GET = 10; @@ -84,6 +89,10 @@ public class XiaomiHealthService extends AbstractXiaomiService { case CMD_SET_USER_INFO: LOG.debug("Got user info set ack, status={}", cmd.getStatus()); return; + case CMD_ACTIVITY_FETCH_1: + case CMD_ACTIVITY_FETCH_2: + handleActivityFetchResponse(cmd.getHealth().getActivityRecordIds().toByteArray()); + return; case CMD_CONFIG_SPO2_GET: handleSpo2Config(cmd.getHealth().getSpo2()); return; @@ -174,7 +183,7 @@ public class XiaomiHealthService extends AbstractXiaomiService { // TODO max heart rate should be input by the user int maxHeartRate = (int) Math.round(age <= 40 ? 220 - age : 207 - 0.7 * age); if (maxHeartRate < 100 || maxHeartRate > 220) { - maxHeartRate = 175; + maxHeartRate = 175; } final XiaomiProto.UserInfo userInfo = XiaomiProto.UserInfo.newBuilder() @@ -386,6 +395,65 @@ public class XiaomiHealthService extends AbstractXiaomiService { ); } + public void onFetchRecordedData(final int dataTypes) { + LOG.debug("Fetch recorded data: {}", dataTypes); + final TransactionBuilder builder = getSupport().createTransactionBuilder("fetch recorded data step 1"); + + getSupport().sendCommand( + builder, + XiaomiProto.Command.newBuilder() + .setType(COMMAND_TYPE) + .setSubtype(CMD_ACTIVITY_FETCH_1) + .setHealth(XiaomiProto.Health.newBuilder().setActivitySyncRequest1( + // TODO official app sends 0, but doesn't work every time? + XiaomiProto.ActivitySyncRequest1.newBuilder().setUnknown1(1) + )) + .build() + ); + + // TODO we need to wait for the reply from the previous before sending this one? + //getSupport().sendCommand( + // builder, + // XiaomiProto.Command.newBuilder() + // .setType(COMMAND_TYPE) + // .setSubtype(CMD_ACTIVITY_FETCH_2) + // .build() + //); + + builder.queue(getSupport().getQueue()); + } + + public void handleActivityFetchResponse(final byte[] recordIds) { + if ((recordIds.length % 7) != 0) { + LOG.warn("recordIds {} length = {}, not a multiple of 7, can't parse", GB.hexdump(recordIds), recordIds.length); + return; + + } + + LOG.debug("Got {} record IDs", recordIds.length / 7); + + final ByteBuffer buf = ByteBuffer.wrap(recordIds).order(ByteOrder.LITTLE_ENDIAN); + + while (buf.position() < buf.limit()) { + final int ts = buf.getInt(); + final int tz = buf.get(); // 15 min blocks + final int version = buf.get(); + final int flags = buf.get(); + // bit 0 is type - 0 activity, 1 sports + final int type = (flags >> 7) & 1; + // bit 1 to 6 bits are subtype + // for activity: activity, sleep, measurements, etc + // for workout: workout type (8 freestyle) + final int subtype = (flags & 127) >> 2; + // bit 6 and 7 - 0/1 - summary vs details + final int detailType = flags & 3; + LOG.debug( + "Activity Record: ts = {}, tz = {}, flags = {}, type = {}, subtype = {}, detailType = {}, version = {}", + ts, tz, String.format("0x%02X", flags), type, subtype, detailType, version + ); + } + } + public void onHeartRateTest() { LOG.debug("Trigger heart rate one-shot test"); diff --git a/app/src/main/proto/xiaomi.proto b/app/src/main/proto/xiaomi.proto index 16ae1d06d..8e87d23bb 100644 --- a/app/src/main/proto/xiaomi.proto +++ b/app/src/main/proto/xiaomi.proto @@ -281,8 +281,10 @@ message Charger { message Health { optional UserInfo userInfo = 1; - // 8, 1 get | 8, 3 set - optional bytes unknownActivity2 = 2; + + optional bytes activityRecordIds = 2; + optional ActivitySyncRequest1 activitySyncRequest1 = 5; + optional SpO2 spo2 = 7; optional HeartRate heartRate = 8; // 8, 12 get | 8, 13 set @@ -319,6 +321,10 @@ message UserInfo { optional uint32 goalMoving = 11; // minutes } +message ActivitySyncRequest1 { + optional uint32 unknown1 = 1; // 0/1 +} + message SpO2 { optional uint32 unknown1 = 1; // 1 optional bool allDayTracking = 2; @@ -385,17 +391,21 @@ message WorkoutStatusWatch { } message WorkoutOpenWatch { - optional uint32 unknown1 = 1; // 2 + // This is only called when gps is needed? + // 1 outdoor running, 2 walking, 3 hiking, 4 trekking, 5 trail run, 6 outdoor cycling + optional uint32 sport = 1; optional uint32 unknown2 = 2; // 2 } message WorkoutOpenReply { - // 3 2 10 + // 3 2 10 when no gps permissions at all + // 5 2 10 when no background permissions? // ... + // 0 * * when phone gps is working fine // 0 2 10 // 0 2 2 optional uint32 unknown1 = 1; - optional uint32 unknown2 = 2; + optional uint32 unknown2 = 2; // always 2? optional uint32 unknown3 = 3; }