Xiaomi: Improve sleep parsing

- Some devices send a random int 0, which would prevent sleep stage
  parsing
- Some devices send the details as a file of type summary, but same
  structure
- It is still not stable for all devices

Thanks to @opcode for the parsing logic
This commit is contained in:
José Rebelo 2024-01-20 23:16:36 +00:00
parent 6b2cb05027
commit 7955bdfb6f
2 changed files with 147 additions and 107 deletions

View File

@ -111,13 +111,9 @@ public abstract class XiaomiActivityParser {
break;
case ACTIVITY_SLEEP:
if (fileId.getDetailType() == XiaomiActivityFileId.DetailType.DETAILS) {
return new SleepDetailsParser();
}
break;
}
return null;
}

View File

@ -16,12 +16,12 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl;
import android.util.Log;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
@ -48,17 +48,23 @@ public class SleepDetailsParser extends XiaomiActivityParser {
@Override
public boolean parse(final XiaomiSupport support, final XiaomiActivityFileId fileId, final byte[] bytes) {
if (fileId.getVersion() != 2) {
// Seems to come both as DetailType.DETAILS (version 2) and DetailType.SUMMARY (version 4)
if (fileId.getVersion() < 2 || fileId.getVersion() > 4) {
LOG.warn("Unknown sleep details version {}", fileId.getVersion());
return false;
}
final ByteBuffer buf = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
buf.get(); // header ? 0xF0
final byte header = buf.get();
final int isAwake = buf.get() & 0xff; // 0/1 - more correctly this would be !isSleepFinish
final int bedTime = buf.getInt();
final int wakeupTime = buf.getInt();
int sleepQuality = -1;
if (fileId.getVersion() >= 4) {
sleepQuality = buf.get() & 0xff;
}
LOG.debug("Sleep sample: bedTime: {}, wakeupTime: {}, isAwake: {}", bedTime, wakeupTime, isAwake);
final List<XiaomiSleepTimeSample> summaries = new ArrayList<>();
@ -68,10 +74,8 @@ 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++) {
// Heart rate samples
if ((header & (1 << 4)) != 0) {
final int unit = buf.getShort(); // Time unit (i.e sample rate)
final int count = buf.getShort();
final int firstRecordTime = buf.getInt();
@ -81,8 +85,40 @@ public class SleepDetailsParser extends XiaomiActivityParser {
buf.position(buf.position() + count);
}
// SpO2 samples
if ((header & (1 << 3)) != 0) {
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);
}
// snore samples
if (fileId.getVersion() >= 3 && (header & (1 << 2)) != 0) {
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 float
// timestamp of each sample is firstRecordTime + (unit * index)
buf.position(buf.position() + count * 4);
}
final List<XiaomiSleepStageSample> stages = new ArrayList<>();
// Do not crash if we face a buffer underflow, as the next parsing is not 100% fool-proof,
// and we still want to persist whatever we got so far
boolean stagesParseFailed = false;
try {
// FIXME: Sometimes there's a random zero here..?
if (buf.getInt() != 0) {
// If it wasn't a zero, rewind
buf.position(buf.position() - 4);
}
while (buf.remaining() >= 17 && buf.getInt() == 0xFFFCFAFB) {
final int headerLen = buf.get() & 0xFF; // this seems to always be 17
@ -139,6 +175,8 @@ public class SleepDetailsParser extends XiaomiActivityParser {
sample.setRemSleepDuration(rem_duration);
sample.setAwakeDuration(wake_duration);
// FIXME: This is an array, but we end up persisting only the last sample, since
// the timestamp is the primary key
summaries.add(sample);
sample = null;
}
@ -160,7 +198,10 @@ public class SleepDetailsParser extends XiaomiActivityParser {
}
}
}
} catch (final BufferUnderflowException e) {
LOG.warn("Buffer underflow while parsing sleep stages...", e);
stagesParseFailed = true;
}
// save all the samples that we got
try (DBHandler handler = GBApplication.acquireDB()) {
@ -169,7 +210,6 @@ public class SleepDetailsParser extends XiaomiActivityParser {
final XiaomiSleepTimeSampleProvider sampleProvider = new XiaomiSleepTimeSampleProvider(gbDevice, session);
for (final XiaomiSleepTimeSample summary : summaries) {
summary.setDevice(DBHelper.getDevice(gbDevice, session));
summary.setUser(DBHelper.getUser(session));
@ -195,6 +235,9 @@ public class SleepDetailsParser extends XiaomiActivityParser {
return false;
}
if (!stagesParseFailed && !stages.isEmpty()) {
LOG.debug("Persisting {} sleep stage samples", stages.size());
// Save the sleep stage samples
try (DBHandler handler = GBApplication.acquireDB()) {
final DaoSession session = handler.getDaoSession();
@ -215,8 +258,9 @@ public class SleepDetailsParser extends XiaomiActivityParser {
LOG.error("Error saving sleep stage samples", e);
return false;
}
}
return true;
return stagesParseFailed;
}
static private int decodeStage(int rawStage) {