diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/DailyDetailsParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/DailyDetailsParser.java index 624368676..dc990fbde 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/DailyDetailsParser.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/DailyDetailsParser.java @@ -49,19 +49,13 @@ public class DailyDetailsParser extends XiaomiActivityParser { public boolean parse(final XiaomiSupport support, final XiaomiActivityFileId fileId, final byte[] bytes) { final int version = fileId.getVersion(); final int headerSize; - final int sampleSize; switch (version) { case 1: - headerSize = 4; - sampleSize = 7; - break; case 2: headerSize = 4; - sampleSize = 10; break; case 3: headerSize = 5; - sampleSize = 12; break; default: LOG.warn("Unable to parse daily details version {}", fileId.getVersion()); @@ -74,38 +68,83 @@ public class DailyDetailsParser extends XiaomiActivityParser { LOG.debug("Daily Details Header: {}", GB.hexdump(header)); - if ((buf.limit() - buf.position()) % sampleSize != 0) { - LOG.warn("Remaining data in the buffer is not a multiple of {}", sampleSize); - return false; - } + final XiaomiComplexActivityParser complexParser = new XiaomiComplexActivityParser(header, buf); final Calendar timestamp = Calendar.getInstance(); timestamp.setTime(fileId.getTimestamp()); final List samples = new ArrayList<>(); - while (buf.position() < buf.limit()) { + complexParser.reset(); + final XiaomiActivitySample sample = new XiaomiActivitySample(); sample.setTimestamp((int) (timestamp.getTimeInMillis() / 1000)); - sample.setSteps(buf.getShort()); + int includeExtraEntry = 0; + if (complexParser.nextGroup(16)) { + // TODO what's the first bit? - final int calories = buf.get() & 0xff; - final int unk2 = buf.get() & 0xff; - final int distance = buf.getShort(); // not just walking, includes workouts like cycling + if (complexParser.hasSecond()) { + includeExtraEntry = complexParser.get(1, 1); + } + if (complexParser.hasThird()) { + sample.setSteps(complexParser.get(2, 14)); + } + } - // TODO persist calories and distance, add UI + if (complexParser.nextGroup(8)) { + // TODO activity type? + if (complexParser.hasSecond()) { + final int calories = complexParser.get(2, 6); + } + } - sample.setHeartRate(buf.get() & 0xff); + if (complexParser.nextGroup(8)) { + // TODO + } - if (version >= 2) { - final byte[] unknown2 = new byte[3]; - buf.get(unknown2); // TODO intensity and kind? energy? + if (complexParser.nextGroup(16)) { + // TODO distance + } - if (version == 3) { - // TODO gadgets with versions 2 also should have stress, but the values don't make sense - sample.setSpo2(buf.get() & 0xff); - sample.setStress(buf.get() & 0xff); + if (complexParser.nextGroup(8)) { + if (complexParser.hasFirst()) { + // hr, 8 bits + sample.setHeartRate(complexParser.get(0, 8)); + } + } + + if (complexParser.nextGroup(8)) { + if (complexParser.hasFirst()) { + // energy, 8 bits + } + } + + if (complexParser.nextGroup(16)) { + // TODO + } + + if (version >= 3) { + if (complexParser.nextGroup(8)) { + if (complexParser.hasFirst()) { + // spo2, 8 bits + sample.setSpo2(complexParser.get(0, 8)); + } + } + if (complexParser.nextGroup(8)) { + if (complexParser.hasFirst()) { + // stress, 8 bits + final int stress = complexParser.get(0, 8); + if (stress != 255) { + sample.setStress(stress); + } + } + } + } + + if (includeExtraEntry == 1) { + if (complexParser.nextGroup(8)) { + // TODO } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/XiaomiComplexActivityParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/XiaomiComplexActivityParser.java new file mode 100644 index 000000000..c2d33a6b6 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/XiaomiComplexActivityParser.java @@ -0,0 +1,116 @@ +/* Copyright (C) 2024 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; + +public class XiaomiComplexActivityParser { + private static final Logger LOG = LoggerFactory.getLogger(XiaomiComplexActivityParser.class); + + private final byte[] header; + private final ByteBuffer buf; + + private int currentGroup = -1; + private int currentGroupBits; + private int currentVal; + + public XiaomiComplexActivityParser(final byte[] header, final ByteBuffer buf) { + this.header = header; + this.buf = buf; + } + + public void reset() { + currentGroup = -1; + currentGroupBits = 0; + currentVal = 0; + } + + /** + * Initializes the next group, for n bits. + * @return whether the next group exists + */ + public boolean nextGroup(final int nBits) { + currentGroup++; + if (currentGroup >= header.length * 2) { + LOG.error("Header too small for group {}", currentGroup); + // We're now in an error state, but we'll consume so the buffer advances and we avoid an + // infinite loop + consume(nBits); + return false; + } + + if ((getCurrentNibble() & 8) == 0) { + // group does not exist, return and do not consume anything from the buffer + return false; + } + + currentGroupBits = nBits; + currentVal = consume(nBits); + + return (getCurrentNibble() & 8) != 0; + } + + private int consume(final int nBits) { + switch (nBits) { + case 8: + return buf.get() & 0xff; + case 16: + return buf.getShort() & 0xffff; + case 32: + return buf.getInt(); + } + + throw new IllegalArgumentException("Unsupported number of bits " + nBits); + } + + private int getCurrentNibble() { + final int headerByte = currentGroup / 2; + if (currentGroup % 2 == 0) { + return (header[headerByte] & 0xf0) >> 4; + } else { + return header[headerByte] & 0x0f; + } + } + + public boolean hasFirst() { + return isValid(0); + } + + public boolean hasSecond() { + return isValid(1); + } + + public boolean hasThird() { + return isValid(2); + } + + public boolean isValid(final int idx) { + if (idx < 0 || idx > 2) { + throw new IllegalArgumentException("Invalid idx " + idx); + } + + return (getCurrentNibble() & (1 << (2 - idx))) != 0; + } + + public int get(final int idx, final int nBits) { + final int shift = currentGroupBits - idx - nBits; + return (currentVal & (((1 << nBits) - 1) << shift)) >>> shift; + } +}