diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileId.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileId.java index a42d536ef..7bb4141a4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileId.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/XiaomiActivityFileId.java @@ -192,6 +192,7 @@ public class XiaomiActivityFileId implements Comparable { public enum Subtype { UNKNOWN(Type.UNKNOWN, -1), ACTIVITY_DAILY(Type.ACTIVITY, 0x00), + ACTIVITY_SLEEP_STAGES(Type.ACTIVITY, 0x03), ACTIVITY_SLEEP(Type.ACTIVITY, 0x08), SPORTS_OUTDOOR_RUNNING(Type.SPORTS, 0x01), SPORTS_FREESTYLE(Type.SPORTS, 0x08), 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 index d4b14b040..9ec8df4a9 100644 --- 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 @@ -34,6 +34,7 @@ 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.DailySummaryParser; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl.SleepDetailsParser; +import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl.SleepStagesParser; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl.WorkoutGpsParser; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl.WorkoutSummaryParser; @@ -95,6 +96,12 @@ public abstract class XiaomiActivityParser { return new DailySummaryParser(); } + break; + case ACTIVITY_SLEEP_STAGES: + if (fileId.getDetailType() == XiaomiActivityFileId.DetailType.DETAILS) { + return new SleepStagesParser(); + } + break; case ACTIVITY_SLEEP: if (fileId.getDetailType() == XiaomiActivityFileId.DetailType.DETAILS) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/SleepStagesParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/SleepStagesParser.java new file mode 100644 index 000000000..fcf1413b2 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/SleepStagesParser.java @@ -0,0 +1,80 @@ +/* Copyright (C) 2023 Alice, 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.impl; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +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; + +public class SleepStagesParser extends XiaomiActivityParser { + private static final Logger LOG = LoggerFactory.getLogger(SleepStagesParser.class); + + @Override + public boolean parse(final XiaomiSupport support, final XiaomiActivityFileId fileId, final byte[] bytes) { + if (fileId.getVersion() != 2) { + LOG.warn("Unknown sleep stages version {}", fileId.getVersion()); + return false; + } + + final ByteBuffer buf = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); + + // over 4 days + // first 2 bytes: always FF FF + // bytes 3,4 small-medium + // byte 5 ??? + // byte 6,7 small + final byte[] unk1 = new byte[7]; + buf.get(unk1); + + // total sleep duration in minutes + final short sleepDuration = buf.getShort(); + // timestamp when watch counts "real" sleep start, might be later than first phase change + final int bedTime = buf.getInt(); + // timestamp when sleep ended (have not observed, but may also be earlier than last phase?) + final int wakeUpTime = buf.getInt(); + + // byte 8 medium + // bytes 9,10 look like a short + final byte[] unk2 = new byte[3]; + buf.get(unk2); + + // sum of all "real" deep sleep durations + final short deepSleepDuration = buf.getShort(); + // sum of all "real" light sleep durations + final short lightSleepDuration = buf.getShort(); + // sum of all "real" REM durations + final short REMDuration = buf.getShort(); + // sum of all "real" awake durations + final short wakeDuration = buf.getShort(); + + // byte 11 small-medium + final byte unk3 = buf.get(); + while (buf.position() < buf.limit()) { + // when the change to the phase occurs + final int time = buf.getInt(); + // what phase state changed to + final byte sleepPhase = buf.get(); + } + return true; + } +}