1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-28 21:06:50 +01:00

Xiaomi: Implement complex activity details header parsing

This should improve activity parsing across all devices, as we now take
the header into account, which indicates what groups are actually
present.

Thanks to @opcode for figuring out the header structure and providing
the ImHex patterns for the activity data.
This commit is contained in:
José Rebelo 2024-02-25 22:27:20 +00:00
parent 0b0aedfb52
commit dd952e335f
2 changed files with 179 additions and 24 deletions

View File

@ -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<XiaomiActivitySample> 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
}
}

View File

@ -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 <https://www.gnu.org/licenses/>. */
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;
}
}