From e06b2e1f955b50371bf3a94eb825cbe33a2b4d17 Mon Sep 17 00:00:00 2001 From: opcode Date: Thu, 11 Jan 2024 16:05:05 +0100 Subject: [PATCH] Xiaomi: Implement sleep stage parsing This allows sleep stage detection to work by parsing some of the data sent in SleepDetails. It's still missing parsing the summary contained inside SleepDetails. and decoding the large amount of other mostly unknown data. --- .../activity/impl/SleepDetailsParser.java | 110 +++++++++++++++++- 1 file changed, 108 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/SleepDetailsParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/SleepDetailsParser.java index 17eb7217d..90f98700a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/SleepDetailsParser.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/SleepDetailsParser.java @@ -16,6 +16,7 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl; +import android.util.Log; import android.widget.Toast; import org.slf4j.Logger; @@ -23,13 +24,18 @@ import org.slf4j.LoggerFactory; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.util.ArrayList; import java.util.List; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.XiaomiSleepStageSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.XiaomiSleepTimeSampleProvider; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.User; +import nodomain.freeyourgadget.gadgetbridge.entities.XiaomiSleepStageSample; import nodomain.freeyourgadget.gadgetbridge.entities.XiaomiSleepTimeSample; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport; @@ -60,6 +66,68 @@ public class SleepDetailsParser extends XiaomiActivityParser { sample.setWakeupTime(wakeupTime * 1000L); sample.setIsAwake(isAwake == 1); + // SleepAssistItemInfo 2x + // - 0: Heart rate samples + // - 1: Sp02 samples + for (int i = 0; i < 2; i++) { + final int unit = buf.getShort(); // Time unit (i.e sample rate) + final int count = buf.getShort(); + final int firstRecordTime = buf.getInt(); + + // Skip count samples - each sample is a u8 + // timestamp of each sample is firstRecordTime + (unit * index) + buf.position(buf.position() + count); + } + + final List stages = new ArrayList<>(); + + + while (buf.remaining() >= 17 && buf.getInt() == 0xFFFCFAFB) { + final int headerLen = buf.get() & 0xFF; // this seems to always be 17 + + // This timestamp is kind of weird, is seems to sometimes be in seconds + // and other times in nanoseconds. Message types 16 and 17 are in seconds + final long ts = buf.getLong(); + final int unk = buf.get() & 0xFF; + final int type = buf.get() & 0xFF; + + final int dataLen = ((buf.get() & 0xFF) << 8) | (buf.get() & 0xFF); + + final byte[] data = new byte[dataLen]; + buf.get(data); + + final ByteBuffer dataBuf = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN); + +// Known types: +// - acc_unk = 0, +// - ppg_unk = 1, +// - fall_asleep = 2, +// - wake_up = 3, +// - switch_ts_unk1 = 12, +// - switch_ts_unk2 = 13, +// - Summary = 16, +// - Stages = 17 + + if (type == 17) { // Stages + long currentTime = ts * 1000; + for (int i = 0; i < dataLen / 2; i++) { + // when the change to the phase occurs + final int val = dataBuf.getShort() & 0xFFFF; + + final int stage = val >> 12; + final int offsetMinutes = val & 0xFFF; + + final XiaomiSleepStageSample stageSample = new XiaomiSleepStageSample(); + stageSample.setTimestamp(currentTime); + stageSample.setStage(decodeStage(stage)); + stages.add(stageSample); + + currentTime += offsetMinutes * 60000; + } + } + } + + // save all the samples that we got try (DBHandler handler = GBApplication.acquireDB()) { final DaoSession session = handler.getDaoSession(); @@ -83,12 +151,50 @@ public class SleepDetailsParser extends XiaomiActivityParser { } sampleProvider.addSample(sample); - - return true; } catch (final Exception e) { GB.toast(support.getContext(), "Error saving sleep sample", Toast.LENGTH_LONG, GB.ERROR); LOG.error("Error saving sleep sample", e); return false; } + + // Save the sleep stage samples + try (DBHandler handler = GBApplication.acquireDB()) { + final DaoSession session = handler.getDaoSession(); + final GBDevice gbDevice = support.getDevice(); + final Device device = DBHelper.getDevice(gbDevice, session); + final User user = DBHelper.getUser(session); + + final XiaomiSleepStageSampleProvider sampleProvider = new XiaomiSleepStageSampleProvider(gbDevice, session); + + for (final XiaomiSleepStageSample stageSample : stages) { + stageSample.setDevice(device); + stageSample.setUser(user); + } + + sampleProvider.addSamples(stages); + } catch (final Exception e) { + GB.toast(support.getContext(), "Error saving sleep stage samples", Toast.LENGTH_LONG, GB.ERROR); + LOG.error("Error saving sleep stage samples", e); + return false; + } + + return true; + } + + static private int decodeStage(int rawStage) { + switch (rawStage) { + case 0: + return 5; // AWAKE + case 1: + return 3; // LIGHT_SLEEP + case 2: + return 2; // DEEP_SLEEP + case 3: + return 4; // REM_SLEEP + case 4: + return 0; // NOT_SLEEP + default: + return 1; // N/A + } } }