2019-02-13 20:43:30 +01:00
|
|
|
/* Copyright (C) 2017-2019 Andreas Shimokawa, AndrewH, Carsten Pfeiffer,
|
2018-08-29 21:30:23 +02:00
|
|
|
szilardx
|
2018-06-25 18:35:46 +02:00
|
|
|
|
|
|
|
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 <http://www.gnu.org/licenses/>. */
|
2018-06-14 18:16:49 +02:00
|
|
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip;
|
2017-10-19 21:52:38 +02:00
|
|
|
|
|
|
|
import org.slf4j.Logger;
|
|
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
|
|
|
|
import java.math.BigDecimal;
|
|
|
|
import java.math.RoundingMode;
|
2018-07-29 12:27:27 +02:00
|
|
|
import java.util.ArrayList;
|
2017-10-19 21:52:38 +02:00
|
|
|
import java.util.Date;
|
2018-07-29 12:27:27 +02:00
|
|
|
import java.util.List;
|
|
|
|
import java.util.concurrent.TimeUnit;
|
2017-10-19 21:52:38 +02:00
|
|
|
|
|
|
|
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
|
|
|
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
|
|
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
|
|
|
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
|
|
|
|
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
|
|
|
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
|
|
|
|
|
|
|
public class ActivityDetailsParser {
|
|
|
|
private static final Logger LOG = LoggerFactory.getLogger(ActivityDetailsParser.class);
|
|
|
|
|
|
|
|
private static final byte TYPE_GPS = 0;
|
|
|
|
private static final byte TYPE_HR = 1;
|
|
|
|
private static final byte TYPE_UNKNOWN2 = 2;
|
|
|
|
private static final byte TYPE_PAUSE = 3;
|
|
|
|
private static final byte TYPE_SPEED4 = 4;
|
|
|
|
private static final byte TYPE_SPEED5 = 5;
|
|
|
|
private static final byte TYPE_GPS_SPEED6 = 6;
|
|
|
|
|
|
|
|
public static final BigDecimal HUAMI_TO_DECIMAL_DEGREES_DIVISOR = new BigDecimal(3000000.0);
|
|
|
|
private final BaseActivitySummary summary;
|
|
|
|
private final ActivityTrack activityTrack;
|
2018-08-01 00:38:45 +02:00
|
|
|
// private final int version;
|
2017-10-19 21:52:38 +02:00
|
|
|
private final Date baseDate;
|
|
|
|
private long baseLongitude;
|
|
|
|
private long baseLatitude;
|
|
|
|
private int baseAltitude;
|
|
|
|
private ActivityPoint lastActivityPoint;
|
|
|
|
|
|
|
|
public boolean getSkipCounterByte() {
|
|
|
|
return skipCounterByte;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void setSkipCounterByte(boolean skipCounterByte) {
|
|
|
|
this.skipCounterByte = skipCounterByte;
|
|
|
|
}
|
|
|
|
|
|
|
|
private boolean skipCounterByte;
|
|
|
|
|
|
|
|
public ActivityDetailsParser(BaseActivitySummary summary) {
|
|
|
|
this.summary = summary;
|
|
|
|
// this.version = version;
|
|
|
|
// this.baseDate = baseDate;
|
|
|
|
//
|
|
|
|
this.baseLongitude = summary.getBaseLongitude();
|
|
|
|
this.baseLatitude = summary.getBaseLatitude();
|
|
|
|
this.baseAltitude = summary.getBaseAltitude();
|
|
|
|
this.baseDate = summary.getStartTime();
|
|
|
|
|
|
|
|
this.activityTrack = new ActivityTrack();
|
|
|
|
activityTrack.setUser(summary.getUser());
|
|
|
|
activityTrack.setDevice(summary.getDevice());
|
|
|
|
activityTrack.setName(summary.getName() + "-" + summary.getId());
|
|
|
|
}
|
|
|
|
|
|
|
|
public ActivityTrack parse(byte[] bytes) throws GBException {
|
|
|
|
int i = 0;
|
|
|
|
try {
|
|
|
|
long totalTimeOffset = 0;
|
|
|
|
int lastTimeOffset = 0;
|
|
|
|
while (i < bytes.length) {
|
|
|
|
if (skipCounterByte && (i % 17) == 0) {
|
|
|
|
i++;
|
|
|
|
}
|
|
|
|
|
2019-02-27 20:52:16 +01:00
|
|
|
byte type = bytes[i++]; // lgtm [java/index-out-of-bounds]]
|
|
|
|
int timeOffset = BLETypeConversions.toUnsigned(bytes[i++]); // lgtm [java/index-out-of-bounds]
|
2017-10-19 21:52:38 +02:00
|
|
|
// handle timeOffset overflows (1 byte, always increasing, relative to base)
|
|
|
|
if (lastTimeOffset <= timeOffset) {
|
|
|
|
timeOffset = timeOffset - lastTimeOffset;
|
|
|
|
lastTimeOffset += timeOffset;
|
|
|
|
} else {
|
|
|
|
lastTimeOffset = timeOffset;
|
|
|
|
}
|
|
|
|
totalTimeOffset += timeOffset;
|
|
|
|
|
|
|
|
switch (type) {
|
|
|
|
case TYPE_GPS:
|
|
|
|
i += consumeGPSAndUpdateBaseLocation(bytes, i, totalTimeOffset);
|
|
|
|
break;
|
|
|
|
case TYPE_HR:
|
|
|
|
i += consumeHeartRate(bytes, i, totalTimeOffset);
|
|
|
|
break;
|
|
|
|
case TYPE_UNKNOWN2:
|
|
|
|
i += consumeUnknown2(bytes, i);
|
|
|
|
break;
|
|
|
|
case TYPE_PAUSE:
|
|
|
|
i += consumePause(bytes, i);
|
|
|
|
break;
|
|
|
|
case TYPE_SPEED4:
|
|
|
|
i += consumeSpeed4(bytes, i);
|
|
|
|
break;
|
|
|
|
case TYPE_SPEED5:
|
|
|
|
i += consumeSpeed5(bytes, i);
|
|
|
|
break;
|
|
|
|
case TYPE_GPS_SPEED6:
|
|
|
|
i += consumeSpeed6(bytes, i);
|
|
|
|
break;
|
2018-11-08 17:13:00 +01:00
|
|
|
default:
|
|
|
|
LOG.warn("unknown packet type" + type);
|
|
|
|
i+=6;
|
2017-10-19 21:52:38 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (IndexOutOfBoundsException ex) {
|
|
|
|
throw new GBException("Error parsing activity details: " + ex.getMessage(), ex);
|
|
|
|
}
|
|
|
|
|
2018-08-01 00:38:45 +02:00
|
|
|
fixupMissingTimestamps(activityTrack);
|
|
|
|
|
|
|
|
return activityTrack;
|
|
|
|
}
|
|
|
|
|
2018-08-01 19:48:05 +02:00
|
|
|
private void fixupMissingTimestamps(ActivityTrack activityTrack) {
|
2018-08-01 00:38:45 +02:00
|
|
|
try {
|
2018-07-29 12:27:27 +02:00
|
|
|
int pointer = 0;
|
|
|
|
List<ActivityPoint> activityPointList = activityTrack.getTrackPoints();
|
|
|
|
Date gpsStartTime = null;
|
2018-08-01 00:38:45 +02:00
|
|
|
List<ActivityPoint> entriesToFixUp = new ArrayList<>();
|
2018-08-04 23:10:11 +02:00
|
|
|
while (pointer < activityPointList.size() - 1) {
|
2018-08-01 00:38:45 +02:00
|
|
|
ActivityPoint activityPoint = activityPointList.get(pointer);
|
|
|
|
if (activityPoint.getLocation() == null) {
|
2018-07-29 12:27:27 +02:00
|
|
|
pointer++;
|
|
|
|
continue;
|
|
|
|
}
|
2018-08-01 00:38:45 +02:00
|
|
|
if (activityPoint.getTime().equals(activityPointList.get(pointer + 1).getTime())) {
|
|
|
|
entriesToFixUp.add(activityPoint);
|
|
|
|
} else {
|
|
|
|
// found the first activity point with a proper timestamp
|
|
|
|
entriesToFixUp.add(activityPoint);
|
|
|
|
gpsStartTime = activityPointList.get(pointer + 1).getTime();
|
2018-07-29 12:27:27 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
pointer++;
|
|
|
|
}
|
2018-08-01 00:38:45 +02:00
|
|
|
if (gpsStartTime != null) {
|
|
|
|
// now adjust those entries without a timestamp
|
|
|
|
long differenceInSec = TimeUnit.SECONDS.convert(Math.abs(gpsStartTime.getTime() - baseDate.getTime()), TimeUnit.MILLISECONDS);
|
2018-07-29 12:27:27 +02:00
|
|
|
|
2018-08-01 00:38:45 +02:00
|
|
|
double multiplier = (double) differenceInSec / (double) (entriesToFixUp.size());
|
2018-07-29 12:27:27 +02:00
|
|
|
|
2018-08-01 00:38:45 +02:00
|
|
|
for (int j = 0; j < entriesToFixUp.size(); j++) {
|
|
|
|
long timeOffsetSeconds = Math.round(j * multiplier);
|
|
|
|
entriesToFixUp.get(j).setTime(makeAbsolute(timeOffsetSeconds));
|
|
|
|
}
|
2018-07-29 12:27:27 +02:00
|
|
|
}
|
2018-08-01 00:38:45 +02:00
|
|
|
} catch (Exception ex) {
|
2018-08-01 19:48:05 +02:00
|
|
|
LOG.warn("Error cleaning activity details", ex);
|
2018-07-29 12:27:27 +02:00
|
|
|
}
|
2017-10-19 21:52:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private int consumeGPSAndUpdateBaseLocation(byte[] bytes, int offset, long timeOffset) {
|
|
|
|
int i = 0;
|
|
|
|
int longitudeDelta = BLETypeConversions.toInt16(bytes[offset + i++], bytes[offset + i++]);
|
|
|
|
int latitudeDelta = BLETypeConversions.toInt16(bytes[offset + i++], bytes[offset + i++]);
|
|
|
|
int altitudeDelta = BLETypeConversions.toInt16(bytes[offset + i++], bytes[offset + i++]);
|
|
|
|
|
|
|
|
baseLongitude += longitudeDelta;
|
|
|
|
baseLatitude += latitudeDelta;
|
|
|
|
baseAltitude += altitudeDelta;
|
|
|
|
|
|
|
|
GPSCoordinate coordinate = new GPSCoordinate(
|
|
|
|
convertHuamiValueToDecimalDegrees(baseLongitude),
|
|
|
|
convertHuamiValueToDecimalDegrees(baseLatitude),
|
|
|
|
baseAltitude);
|
|
|
|
|
2018-07-29 12:27:27 +02:00
|
|
|
ActivityPoint ap = getActivityPointFor(timeOffset, coordinate);
|
2017-10-19 21:52:38 +02:00
|
|
|
ap.setLocation(coordinate);
|
|
|
|
add(ap);
|
|
|
|
|
|
|
|
return i;
|
|
|
|
}
|
|
|
|
|
|
|
|
private double convertHuamiValueToDecimalDegrees(long huamiValue) {
|
|
|
|
BigDecimal result = new BigDecimal(huamiValue).divide(HUAMI_TO_DECIMAL_DEGREES_DIVISOR, GPSCoordinate.GPS_DECIMAL_DEGREES_SCALE, RoundingMode.HALF_UP);
|
|
|
|
return result.doubleValue();
|
|
|
|
}
|
|
|
|
|
|
|
|
private int consumeHeartRate(byte[] bytes, int offset, long timeOffsetSeconds) {
|
|
|
|
int v1 = BLETypeConversions.toUint16(bytes[offset]);
|
|
|
|
int v2 = BLETypeConversions.toUint16(bytes[offset + 1]);
|
|
|
|
int v3 = BLETypeConversions.toUint16(bytes[offset + 2]);
|
|
|
|
int v4 = BLETypeConversions.toUint16(bytes[offset + 3]);
|
|
|
|
int v5 = BLETypeConversions.toUint16(bytes[offset + 4]);
|
|
|
|
int v6 = BLETypeConversions.toUint16(bytes[offset + 5]);
|
|
|
|
|
|
|
|
if (v2 == 0 && v3 == 0 && v4 == 0 && v5 == 0 && v6 == 0) {
|
|
|
|
// new version
|
|
|
|
// LOG.info("detected heart rate in 'new' version, where version is: " + summary.getVersion());
|
|
|
|
LOG.info("detected heart rate in 'new' version format");
|
|
|
|
ActivityPoint ap = getActivityPointFor(timeOffsetSeconds);
|
|
|
|
ap.setHeartRate(v1);
|
|
|
|
add(ap);
|
|
|
|
} else {
|
|
|
|
ActivityPoint ap = getActivityPointFor(v1);
|
|
|
|
ap.setHeartRate(v2);
|
|
|
|
add(ap);
|
|
|
|
|
|
|
|
ap = getActivityPointFor(v3);
|
|
|
|
ap.setHeartRate(v4);
|
|
|
|
add(ap);
|
|
|
|
|
|
|
|
ap = getActivityPointFor(v5);
|
|
|
|
ap.setHeartRate(v6);
|
|
|
|
add(ap);
|
|
|
|
}
|
|
|
|
return 6;
|
|
|
|
}
|
|
|
|
|
|
|
|
private ActivityPoint getActivityPointFor(long timeOffsetSeconds) {
|
|
|
|
Date time = makeAbsolute(timeOffsetSeconds);
|
2018-04-13 00:50:47 +02:00
|
|
|
if (lastActivityPoint != null) {
|
|
|
|
if (lastActivityPoint.getTime().equals(time)) {
|
|
|
|
return lastActivityPoint;
|
|
|
|
}
|
|
|
|
}
|
2017-10-19 21:52:38 +02:00
|
|
|
return new ActivityPoint(time);
|
|
|
|
}
|
|
|
|
|
2018-07-29 12:27:27 +02:00
|
|
|
private ActivityPoint getActivityPointFor(long timeOffsetSeconds, GPSCoordinate gpsCoordinate) {
|
|
|
|
Date time = makeAbsolute(timeOffsetSeconds);
|
|
|
|
if (lastActivityPoint != null) {
|
|
|
|
if (lastActivityPoint.getTime().equals(time)) {
|
2018-08-01 00:38:45 +02:00
|
|
|
if (lastActivityPoint.getLocation() != null && !lastActivityPoint.getLocation().equals(gpsCoordinate)) {
|
2018-07-29 12:27:27 +02:00
|
|
|
return new ActivityPoint(time);
|
|
|
|
}
|
|
|
|
return lastActivityPoint;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return new ActivityPoint(time);
|
|
|
|
}
|
|
|
|
|
2017-10-19 21:52:38 +02:00
|
|
|
private Date makeAbsolute(long timeOffsetSeconds) {
|
|
|
|
return new Date(baseDate.getTime() + timeOffsetSeconds * 1000);
|
|
|
|
}
|
|
|
|
|
|
|
|
private void add(ActivityPoint ap) {
|
|
|
|
if (ap != lastActivityPoint) {
|
|
|
|
lastActivityPoint = ap;
|
|
|
|
activityTrack.addTrackPoint(ap);
|
|
|
|
} else {
|
|
|
|
LOG.info("skipping point!");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private int consumeUnknown2(byte[] bytes, int offset) {
|
|
|
|
return 6; // just guessing...
|
|
|
|
}
|
|
|
|
|
|
|
|
private int consumePause(byte[] bytes, int i) {
|
|
|
|
return 6; // just guessing...
|
|
|
|
}
|
|
|
|
|
|
|
|
private int consumeSpeed4(byte[] bytes, int offset) {
|
|
|
|
return 6;
|
|
|
|
}
|
|
|
|
|
|
|
|
private int consumeSpeed5(byte[] bytes, int offset) {
|
|
|
|
return 6;
|
|
|
|
}
|
|
|
|
|
|
|
|
private int consumeSpeed6(byte[] bytes, int offset) {
|
|
|
|
return 6;
|
|
|
|
}
|
|
|
|
}
|