Mi Band 10: Fix sleep parsing

This commit is contained in:
José Rebelo
2025-07-26 13:43:40 +01:00
parent d96b7a184c
commit 69b2a4b88a
3 changed files with 62 additions and 29 deletions
@@ -129,4 +129,8 @@ public abstract class XiaomiActivityParser {
return null;
}
public static boolean validData(final byte[] header, final int i) {
return (header[i / 8] & (1 << (7 - (i % 8)))) != 0;
}
}
@@ -50,15 +50,26 @@ public class SleepDetailsParser extends XiaomiActivityParser {
@Override
public boolean parse(final XiaomiSupport support, final XiaomiActivityFileId fileId, final byte[] bytes) {
// Seems to come both as DetailType.DETAILS (version 2) and DetailType.SUMMARY (version 4)
if (fileId.getVersion() > 5) {
LOG.warn("Unknown sleep details version {}", fileId.getVersion());
return false;
// Seems to come both as DetailType.DETAILS (version 2) and DetailType.SUMMARY (version 4, 5)
final int version = fileId.getVersion();
final int headerSize;
switch (version) {
case 1:
case 2:
case 3:
case 4:
headerSize = 1;
break;
case 5:
headerSize = 2;
break;
default:
LOG.warn("Unknown sleep details version {}", fileId.getVersion());
return false;
}
// Stores number of fields which are only present in certain versions of the message
// this is required for correct header offset calculation
int versionDependentFields = 0;
// Current offset in the header, which only advances if we process a field available in the version
int headerIdx = 0;
final ByteBuffer buf = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
buf.get(new byte[7]); // skip fileId bytes
@@ -67,18 +78,24 @@ public class SleepDetailsParser extends XiaomiActivityParser {
LOG.warn("Expected 0 padding after fileId, got {} - parsing might fail", fileIdPadding);
}
final byte header = buf.get();
if (fileId.getVersion() > 4) {
buf.get();
}
final byte[] header = new byte[headerSize];
buf.get(header);
final int isAwake = buf.get() & 0xff; // 0/1 - more correctly this would be !isSleepFinish
headerIdx++;
final int bedTime = buf.getInt();
headerIdx++;
final int wakeupTime = buf.getInt();
headerIdx++;
int sleepQuality = -1;
if (fileId.getVersion() >= 4) {
versionDependentFields += 1;
sleepQuality = buf.get() & 0xff;
if (validData(header, headerIdx)) {
sleepQuality = buf.get() & 0xff;
}
headerIdx++;
}
// note on RR-intervals (msg type 1) on Xiaomi band 9, FW 2.3.93, HW M2345B1:
@@ -103,8 +120,15 @@ public class SleepDetailsParser extends XiaomiActivityParser {
sample.setWakeupTime(wakeupTime * 1000L);
sample.setIsAwake(isAwake == 1);
if (fileId.getVersion() >= 5) {
buf.get(new byte[9]); // ?
final int bedTime2 = buf.getInt(); // around ~30 min before bedTime, is previous one fall asleep time?
final int wakeupTime2 = buf.getInt(); // == wakeupTime
headerIdx += 5;
}
// Heart rate samples
if ((header & (1 << (5 - versionDependentFields))) != 0) {
if (validData(header, headerIdx)) {
LOG.debug("Heart rate samples from offset {}", Integer.toHexString(buf.position()));
final int unit = buf.getShort(); // Time unit (i.e sample rate)
final int count = buf.getShort();
@@ -120,9 +144,10 @@ public class SleepDetailsParser extends XiaomiActivityParser {
buf.position(buf.position() + count);
}
}
headerIdx++;
// SpO2 samples
if ((header & (1 << (4 - versionDependentFields))) != 0) {
if (validData(header, headerIdx)) {
LOG.debug("SpO₂ samples from offset {}", Integer.toHexString(buf.position()));
final int unit = buf.getShort(); // Time unit (i.e sample rate)
final int count = buf.getShort();
@@ -138,23 +163,27 @@ public class SleepDetailsParser extends XiaomiActivityParser {
buf.position(buf.position() + count);
}
}
headerIdx++;
// snore samples
if (fileId.getVersion() >= 3 && (header & (1 << (3 - versionDependentFields))) != 0) {
LOG.debug("Snore level samples from offset {}", Integer.toHexString(buf.position()));
final int unit = buf.getShort(); // Time unit (i.e sample rate)
final int count = buf.getShort();
if (fileId.getVersion() >= 3) {
if (validData(header, headerIdx)) {
LOG.debug("Snore level samples from offset {}", Integer.toHexString(buf.position()));
final int unit = buf.getShort(); // Time unit (i.e sample rate)
final int count = buf.getShort();
if (count > 0) {
// If version is less than 2 firstRecordTime is bedTime
if (fileId.getVersion() >= 2) {
final int firstRecordTime = buf.getInt();
if (count > 0) {
// If version is less than 2 firstRecordTime is bedTime
if (fileId.getVersion() >= 2) {
final int firstRecordTime = buf.getInt();
}
// Skip count samples - each sample is a float
// timestamp of each sample is firstRecordTime + (unit * index)
buf.position(buf.position() + count * 4);
}
// Skip count samples - each sample is a float
// timestamp of each sample is firstRecordTime + (unit * index)
buf.position(buf.position() + count * 4);
}
headerIdx++;
}
final List<XiaomiSleepStageSample> stages = new ArrayList<>();
@@ -77,7 +77,7 @@ public class XiaomiSimpleActivityParser {
// Each bit in the header marks whether the data is valid or not, in order of the fields
final boolean validData = (header[i / 8] & (1 << (7 - (i % 8)))) != 0;
// FIXME: We can't use the header before identifying the correct field lenggths for unknown fields
// FIXME: We can't use the header before identifying the correct field lengths for unknown fields
// or parsing gets out of sync with the header and we will potentially ignore valid data
//if (!validData) {
// LOG.debug("Ignoring non-valid data {}", i);
@@ -152,7 +152,7 @@ public class XiaomiSimpleActivityParser {
public static class Builder {
private int headerSize;
private List<XiaomiSimpleDataEntry> dataEntries = new ArrayList<>();
private final List<XiaomiSimpleDataEntry> dataEntries = new ArrayList<>();
public Builder setHeaderSize(final int headerSize) {
this.headerSize = headerSize;