mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-25 03:16:51 +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:
parent
0b0aedfb52
commit
dd952e335f
@ -49,19 +49,13 @@ public class DailyDetailsParser extends XiaomiActivityParser {
|
|||||||
public boolean parse(final XiaomiSupport support, final XiaomiActivityFileId fileId, final byte[] bytes) {
|
public boolean parse(final XiaomiSupport support, final XiaomiActivityFileId fileId, final byte[] bytes) {
|
||||||
final int version = fileId.getVersion();
|
final int version = fileId.getVersion();
|
||||||
final int headerSize;
|
final int headerSize;
|
||||||
final int sampleSize;
|
|
||||||
switch (version) {
|
switch (version) {
|
||||||
case 1:
|
case 1:
|
||||||
headerSize = 4;
|
|
||||||
sampleSize = 7;
|
|
||||||
break;
|
|
||||||
case 2:
|
case 2:
|
||||||
headerSize = 4;
|
headerSize = 4;
|
||||||
sampleSize = 10;
|
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
headerSize = 5;
|
headerSize = 5;
|
||||||
sampleSize = 12;
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
LOG.warn("Unable to parse daily details version {}", fileId.getVersion());
|
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));
|
LOG.debug("Daily Details Header: {}", GB.hexdump(header));
|
||||||
|
|
||||||
if ((buf.limit() - buf.position()) % sampleSize != 0) {
|
final XiaomiComplexActivityParser complexParser = new XiaomiComplexActivityParser(header, buf);
|
||||||
LOG.warn("Remaining data in the buffer is not a multiple of {}", sampleSize);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Calendar timestamp = Calendar.getInstance();
|
final Calendar timestamp = Calendar.getInstance();
|
||||||
timestamp.setTime(fileId.getTimestamp());
|
timestamp.setTime(fileId.getTimestamp());
|
||||||
|
|
||||||
final List<XiaomiActivitySample> samples = new ArrayList<>();
|
final List<XiaomiActivitySample> samples = new ArrayList<>();
|
||||||
|
|
||||||
while (buf.position() < buf.limit()) {
|
while (buf.position() < buf.limit()) {
|
||||||
|
complexParser.reset();
|
||||||
|
|
||||||
final XiaomiActivitySample sample = new XiaomiActivitySample();
|
final XiaomiActivitySample sample = new XiaomiActivitySample();
|
||||||
sample.setTimestamp((int) (timestamp.getTimeInMillis() / 1000));
|
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;
|
if (complexParser.hasSecond()) {
|
||||||
final int unk2 = buf.get() & 0xff;
|
includeExtraEntry = complexParser.get(1, 1);
|
||||||
final int distance = buf.getShort(); // not just walking, includes workouts like cycling
|
}
|
||||||
|
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) {
|
if (complexParser.nextGroup(16)) {
|
||||||
final byte[] unknown2 = new byte[3];
|
// TODO distance
|
||||||
buf.get(unknown2); // TODO intensity and kind? energy?
|
}
|
||||||
|
|
||||||
if (version == 3) {
|
if (complexParser.nextGroup(8)) {
|
||||||
// TODO gadgets with versions 2 also should have stress, but the values don't make sense
|
if (complexParser.hasFirst()) {
|
||||||
sample.setSpo2(buf.get() & 0xff);
|
// hr, 8 bits
|
||||||
sample.setStress(buf.get() & 0xff);
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user