From 715fb678592c2eb8062336b520f594c729e6152e Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Tue, 18 Aug 2020 10:39:44 +0200 Subject: [PATCH] Do summary parsing on the fly if raw binary data is available --- .../activities/ActivitySummaryDetail.java | 41 +-- .../huami/HuamiActivitySummaryParser.java | 276 +++++++++++++++++ .../model/ActivitySummaryParser.java | 23 ++ .../FetchSportsSummaryOperation.java | 286 ++---------------- 4 files changed, 342 insertions(+), 284 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiActivitySummaryParser.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryParser.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummaryDetail.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummaryDetail.java index 1ab55be35..42131f3a8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummaryDetail.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummaryDetail.java @@ -50,10 +50,12 @@ import java.util.Iterator; import java.util.concurrent.TimeUnit; import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiActivitySummaryParser; import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryItems; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser; import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils; import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; @@ -61,7 +63,6 @@ import nodomain.freeyourgadget.gadgetbridge.util.SwipeEvents; public class ActivitySummaryDetail extends AbstractGBActivity { private static final Logger LOG = LoggerFactory.getLogger(ActivitySummaryDetail.class); - private GBDevice mGBDevice; private JSONObject groupData = setGroups(); private boolean show_raw_data = false; BaseActivitySummary currentItem = null; @@ -73,10 +74,10 @@ public class ActivitySummaryDetail extends AbstractGBActivity { super.onCreate(savedInstanceState); setContentView(R.layout.activity_summary_details); Intent intent = getIntent(); - mGBDevice = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE); - final int filter = intent.getIntExtra("filter",0); - final int position = intent.getIntExtra("position",0); - final ActivitySummaryItems items = new ActivitySummaryItems(this, mGBDevice, filter); + GBDevice gbDevice = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE); + final int filter = intent.getIntExtra("filter", 0); + final int position = intent.getIntExtra("position", 0); + final ActivitySummaryItems items = new ActivitySummaryItems(this, gbDevice, filter); final RelativeLayout layout = findViewById(R.id.activity_summary_detail_relative_layout); alternateColor = getAlternateColor(this); @@ -131,7 +132,7 @@ public class ActivitySummaryDetail extends AbstractGBActivity { } //allows long-press.switch of data being in raw form or recalculated - ImageView activity_icon = (ImageView) findViewById(R.id.item_image); + ImageView activity_icon = findViewById(R.id.item_image); activity_icon.setOnLongClickListener(new View.OnLongClickListener() { public boolean onLongClick(View v) { show_raw_data=!show_raw_data; @@ -149,7 +150,7 @@ public class ActivitySummaryDetail extends AbstractGBActivity { private void makeSummaryHeader(BaseActivitySummary item){ //make view of data from main part of item final String gpxTrack = item.getGpxTrack(); - Button show_track_btn = (Button) findViewById(R.id.showTrack); + Button show_track_btn = findViewById(R.id.showTrack); show_track_btn.setVisibility(View.GONE); if (gpxTrack != null) { @@ -165,34 +166,40 @@ public class ActivitySummaryDetail extends AbstractGBActivity { }); } String activitykindname = ActivityKind.asString(item.getActivityKind(), getApplicationContext()); - Date starttime = (Date) item.getStartTime(); - Date endtime = (Date) item.getEndTime(); + Date starttime = item.getStartTime(); + Date endtime = item.getEndTime(); String starttimeS = DateTimeUtils.formatDateTime(starttime); String endtimeS = DateTimeUtils.formatDateTime(endtime); String durationhms = DateTimeUtils.formatDurationHoursMinutes((endtime.getTime() - starttime.getTime()), TimeUnit.MILLISECONDS); - ImageView activity_icon = (ImageView) findViewById(R.id.item_image); + ImageView activity_icon = findViewById(R.id.item_image); activity_icon.setImageResource(ActivityKind.getIconId(item.getActivityKind())); - TextView activity_kind = (TextView) findViewById(R.id.activitykind); + TextView activity_kind = findViewById(R.id.activitykind); activity_kind.setText(activitykindname); - TextView start_time = (TextView) findViewById(R.id.starttime); + TextView start_time = findViewById(R.id.starttime); start_time.setText(starttimeS); - TextView end_time = (TextView) findViewById(R.id.endtime); + TextView end_time = findViewById(R.id.endtime); end_time.setText(endtimeS); - TextView activity_duration = (TextView) findViewById(R.id.duration); + TextView activity_duration = findViewById(R.id.duration); activity_duration.setText(durationhms); } - private void makeSummaryContent (BaseActivitySummary item){ + private void makeSummaryContent(BaseActivitySummary item) { //make view of data from summaryData of item TableLayout fieldLayout = findViewById(R.id.summaryDetails); fieldLayout.removeAllViews(); //remove old widgets JSONObject summarySubdata = null; - JSONObject data = null; + JSONObject data; + + // Parse on the fly if we have binary data available + if (item.getRawSummaryData() != null) { + ActivitySummaryParser parser = new HuamiActivitySummaryParser(); // FIXME: if something else than huami supports that make sure to have the right parser + item = parser.parseBinaryData(item); + } String sumData = item.getSummaryData(); @@ -207,8 +214,6 @@ public class ActivitySummaryDetail extends AbstractGBActivity { if (summarySubdata == null) return; data = makeSummaryList(summarySubdata); //make new list, grouped by groups - if (data == null) return; - Iterator keys = data.keys(); DecimalFormat df = new DecimalFormat("#.##"); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiActivitySummaryParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiActivitySummaryParser.java new file mode 100644 index 000000000..242740ec7 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiActivitySummaryParser.java @@ -0,0 +1,276 @@ +/* Copyright (C) 2020 Andreas Shimokawa + + 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.devices.huami; + +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Date; + +import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser; +import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiActivityDetailsParser; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSportsActivityType; + +public class HuamiActivitySummaryParser implements ActivitySummaryParser { + + private static final Logger LOG = LoggerFactory.getLogger(HuamiActivityDetailsParser.class); + private JSONObject summaryData = new JSONObject(); + + + public BaseActivitySummary parseBinaryData(BaseActivitySummary summary) { + Date startTime = summary.getStartTime(); + if (startTime == null) { + LOG.error("Due to a bug, we can only parse the summary when startTime is already set"); + return null; + } + return parseBinaryData(summary, startTime); + } + + private BaseActivitySummary parseBinaryData(BaseActivitySummary summary, Date startTime) { + summaryData = new JSONObject(); + ByteBuffer buffer = ByteBuffer.wrap(summary.getRawSummaryData()).order(ByteOrder.LITTLE_ENDIAN); + + short version = buffer.getShort(); // version + LOG.debug("Got sport summary version " + version + " total bytes=" + buffer.capacity()); + int activityKind = ActivityKind.TYPE_UNKNOWN; + try { + int rawKind = BLETypeConversions.toUnsigned(buffer.getShort()); + HuamiSportsActivityType activityType = HuamiSportsActivityType.fromCode(rawKind); + activityKind = activityType.toActivityKind(); + } catch (Exception ex) { + LOG.error("Error mapping activity kind: " + ex.getMessage(), ex); + } + summary.setActivityKind(activityKind); + + // FIXME: should honor timezone we were in at that time etc + long timestamp_start = BLETypeConversions.toUnsigned(buffer.getInt()) * 1000; + long timestamp_end = BLETypeConversions.toUnsigned(buffer.getInt()) * 1000; + + + // FIXME: should be done like this but seems to return crap when in DST + //summary.setStartTime(new Date(timestamp_start)); + //summary.setEndTime(new Date(timestamp_end)); + + // FIXME ... so do it like this + long duration = timestamp_end - timestamp_start; + summary.setEndTime(new Date(startTime.getTime() + duration)); + + int baseLongitude = buffer.getInt(); + int baseLatitude = buffer.getInt(); + int baseAltitude = buffer.getInt(); + summary.setBaseLongitude(baseLongitude); + summary.setBaseLatitude(baseLatitude); + summary.setBaseAltitude(baseAltitude); + + int steps; + int activeSeconds; + int maxLatitude; + int minLatitude; + int maxLongitude; + int minLongitude; + float caloriesBurnt; + float distanceMeters; + float ascentMeters = 0; + float descentMeters = 0; + float maxAltitude = 0; + float minAltitude = 0; + float maxSpeed = 0; + float minPace; + float maxPace; + float totalStride = 0; + float averageStride; + short averageHR; + short averageKMPaceSeconds; + int ascentSeconds = 0; + int descentSeconds = 0; + int flatSeconds = 0; + + // Just assuming, Bip has 259 which seems like 256+x + // Bip S now has 518 so assuming 512+x, might be wrong + + if (version >= 512) { + steps = buffer.getInt(); + activeSeconds = buffer.getInt(); + //unknown + buffer.getLong(); + buffer.getLong(); + caloriesBurnt = buffer.getFloat(); + distanceMeters = buffer.getFloat(); + //unknown + buffer.getLong(); + buffer.getLong(); + buffer.getLong(); + buffer.getLong(); + maxPace = buffer.getFloat(); + minPace = buffer.getFloat(); + //unknown + buffer.getLong(); + buffer.getLong(); + buffer.getLong(); + buffer.getLong(); + buffer.getInt(); + averageHR = buffer.getShort(); + averageKMPaceSeconds = buffer.getShort(); + averageStride = buffer.getShort(); + buffer.getShort(); // unknown + + if (activityKind == ActivityKind.TYPE_CYCLING || activityKind == ActivityKind.TYPE_RUNNING) { + // this had nonsense data with treadmill on bip s, need to test it with running + // for cycling it seems to work... hmm... + // 28 bytes + buffer.getInt(); // unknown + buffer.getInt(); // unknown + ascentSeconds = buffer.getInt() / 1000; //ms? + buffer.getInt(); // unknown; + descentSeconds = buffer.getInt() / 1000; //ms? + buffer.getInt(); // unknown; + flatSeconds = buffer.getInt() / 1000; // ms? + } + } else { + distanceMeters = buffer.getFloat(); + ascentMeters = buffer.getFloat(); + descentMeters = buffer.getFloat(); + minAltitude = buffer.getFloat(); + maxAltitude = buffer.getFloat(); + maxLatitude = buffer.getInt(); // format? + minLatitude = buffer.getInt(); // format? + maxLongitude = buffer.getInt(); // format? + minLongitude = buffer.getInt(); // format? + steps = buffer.getInt(); + activeSeconds = buffer.getInt(); + caloriesBurnt = buffer.getFloat(); + maxSpeed = buffer.getFloat(); + minPace = buffer.getFloat(); + maxPace = buffer.getFloat(); + totalStride = buffer.getFloat(); + + buffer.getInt(); // unknown + if (activityKind == ActivityKind.TYPE_SWIMMING) { + // 28 bytes + float averageStrokeDistance = buffer.getFloat(); + float averageStrokesPerSecond = buffer.getFloat(); + float averageLapPace = buffer.getFloat(); + short strokes = buffer.getShort(); + short swolfIndex = buffer.getShort(); + byte swimStyle = buffer.get(); + byte laps = buffer.get(); + buffer.getInt(); // unknown + buffer.getInt(); // unknown + buffer.getShort(); // unknown + + addSummaryData("averageStrokeDistance", averageStrokeDistance, "meters"); + addSummaryData("averageStrokesPerSecond", averageStrokesPerSecond, "strokes_second"); + addSummaryData("averageLapPace", averageLapPace, "second"); + addSummaryData("strokes", strokes, "strokes"); + addSummaryData("swolfIndex", swolfIndex, "swolf_index"); + addSummaryData("swimStyle", swimStyle, "swim_style"); + addSummaryData("laps", laps, "laps"); + + } else { + // 28 bytes + buffer.getInt(); // unknown + buffer.getInt(); // unknown + ascentSeconds = buffer.getInt() / 1000; //ms? + buffer.getInt(); // unknown; + descentSeconds = buffer.getInt() / 1000; //ms? + buffer.getInt(); // unknown; + flatSeconds = buffer.getInt() / 1000; // ms? + + addSummaryData("ascentSeconds", ascentSeconds, "seconds"); + addSummaryData("descentSeconds", descentSeconds, "seconds"); + addSummaryData("flatSeconds", flatSeconds, "seconds"); + } + + averageHR = buffer.getShort(); + + averageKMPaceSeconds = buffer.getShort(); + averageStride = buffer.getShort(); + } + +// summary.setBaseCoordinate(new GPSCoordinate(baseLatitude, baseLongitude, baseAltitude)); +// summary.setDistanceMeters(distanceMeters); +// summary.setAscentMeters(ascentMeters); +// summary.setDescentMeters(descentMeters); +// summary.setMinAltitude(maxAltitude); +// summary.setMaxAltitude(maxAltitude); +// summary.setMinLatitude(minLatitude); +// summary.setMaxLatitude(maxLatitude); +// summary.setMinLongitude(minLatitude); +// summary.setMaxLongitude(maxLatitude); +// summary.setSteps(steps); +// summary.setActiveTimeSeconds(secondsActive); +// summary.setCaloriesBurnt(caloriesBurnt); +// summary.setMaxSpeed(maxSpeed); +// summary.setMinPace(minPace); +// summary.setMaxPace(maxPace); +// summary.setTotalStride(totalStride); +// summary.setTimeAscent(BLETypeConversions.toUnsigned(ascentSeconds); +// summary.setTimeDescent(BLETypeConversions.toUnsigned(descentSeconds); +// summary.setTimeFlat(BLETypeConversions.toUnsigned(flatSeconds); +// summary.setAverageHR(BLETypeConversions.toUnsigned(averageHR); +// summary.setAveragePace(BLETypeConversions.toUnsigned(averagePace); +// summary.setAverageStride(BLETypeConversions.toUnsigned(averageStride); + + addSummaryData("ascentSeconds", ascentSeconds, "seconds"); + addSummaryData("descentSeconds", descentSeconds, "seconds"); + addSummaryData("flatSeconds", flatSeconds, "seconds"); + + addSummaryData("distanceMeters", distanceMeters, "meters"); + addSummaryData("ascentMeters", ascentMeters, "meters"); + addSummaryData("descentMeters", descentMeters, "meters"); + if (maxAltitude != -100000) { + addSummaryData("maxAltitude", maxAltitude, "meters"); + } + if (minAltitude != 100000) { + addSummaryData("minAltitude", minAltitude, "meters"); + } + addSummaryData("steps", steps, "steps_unit"); + addSummaryData("activeSeconds", activeSeconds, "seconds"); + addSummaryData("caloriesBurnt", caloriesBurnt, "calories_unit"); + addSummaryData("maxSpeed", maxSpeed, "meters_second"); + addSummaryData("minPace", minPace, "seconds_m"); + addSummaryData("maxPace", maxPace, "seconds_m"); + addSummaryData("totalStride", totalStride, "meters"); + addSummaryData("averageHR", averageHR, "bpm"); + addSummaryData("averageKMPaceSeconds", averageKMPaceSeconds, "seconds_km"); + addSummaryData("averageStride", averageStride, "cm"); + + summary.setSummaryData(summaryData.toString()); + return summary; + } + + + private void addSummaryData(String key, float value, String unit) { + if (value > 0) { + try { + JSONObject innerData = new JSONObject(); + innerData.put("value", value); + innerData.put("unit", unit); + summaryData.put(key, innerData); + } catch (JSONException ignore) { + } + } + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryParser.java new file mode 100644 index 000000000..49799b8d7 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryParser.java @@ -0,0 +1,23 @@ +/* Copyright (C) 2020 Andreas Shimokawa + + 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.model; + +import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary; + +public interface ActivitySummaryParser { + BaseActivitySummary parseBinaryData(BaseActivitySummary summary); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchSportsSummaryOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchSportsSummaryOperation.java index 3a87ac017..adb164bd4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchSportsSummaryOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchSportsSummaryOperation.java @@ -21,37 +21,25 @@ import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; import android.widget.Toast; -import org.json.JSONException; -import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.File; import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.text.SimpleDateFormat; -import java.util.Date; import java.util.GregorianCalendar; -import java.util.Locale; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.Logging; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiActivitySummaryParser; import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip.AmazfitBipService; import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.entities.User; -import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; -import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSportsActivityType; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport; -import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; /** @@ -62,7 +50,6 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation { private static final Logger LOG = LoggerFactory.getLogger(FetchSportsSummaryOperation.class); private ByteArrayOutputStream buffer = new ByteArrayOutputStream(140); - private JSONObject summaryData = new JSONObject(); public FetchSportsSummaryOperation(HuamiSupport support) { super(support); setName("fetching sport summaries"); @@ -91,17 +78,24 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation { BaseActivitySummary summary = null; if (success) { - summary = parseSummary(buffer); - try (DBHandler dbHandler = GBApplication.acquireDB()) { - DaoSession session = dbHandler.getDaoSession(); - Device device = DBHelper.getDevice(getDevice(), session); - User user = DBHelper.getUser(session); - summary.setDevice(device); - summary.setUser(user); - summary.setRawSummaryData(buffer.toByteArray()); - session.getBaseActivitySummaryDao().insertOrReplace(summary); - } catch (Exception ex) { - GB.toast(getContext(), "Error saving activity summary", Toast.LENGTH_LONG, GB.ERROR, ex); + summary = new BaseActivitySummary(); + summary.setStartTime(getLastStartTimestamp().getTime()); // due to a bug this has to be set + summary.setRawSummaryData(buffer.toByteArray()); + HuamiActivitySummaryParser parser = new HuamiActivitySummaryParser(); + summary = parser.parseBinaryData(summary); + if (summary != null) { + summary.setSummaryData(null); // remove json before saving to database, + try (DBHandler dbHandler = GBApplication.acquireDB()) { + DaoSession session = dbHandler.getDaoSession(); + Device device = DBHelper.getDevice(getDevice(), session); + User user = DBHelper.getUser(session); + summary.setDevice(device); + summary.setUser(user); + summary.setRawSummaryData(buffer.toByteArray()); + session.getBaseActivitySummaryDao().insertOrReplace(summary); + } catch (Exception ex) { + GB.toast(getContext(), "Error saving activity summary", Toast.LENGTH_LONG, GB.ERROR, ex); + } } } @@ -115,6 +109,7 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation { GB.toast(getContext(), "Unable to fetch activity details: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex); } } + } @Override @@ -155,7 +150,6 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation { } else { GB.toast("Error " + getName() + ", invalid package counter: " + value[0] + ", last was: " + lastPacketCounter, Toast.LENGTH_LONG, GB.ERROR); handleActivityFetchFinish(false); - return; } } @@ -170,246 +164,6 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation { buffer.write(value, 1, value.length - 1); // skip the counter } - private BaseActivitySummary parseSummary(ByteArrayOutputStream stream) { - BaseActivitySummary summary = new BaseActivitySummary(); - - boolean dumptofile = false; - if (dumptofile) { - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd-hhmmss", Locale.US); - String filename = "rawsummary_" + dateFormat.format(new Date()) + ".bin"; - try { - File dir = FileUtils.getExternalFilesDir(); - File outputFile = new File(dir, filename); - FileUtils.copyStreamToFile(new ByteArrayInputStream(stream.toByteArray()), outputFile); - } catch (IOException e) { - LOG.warn("could not create file"); - } - } - - ByteBuffer buffer = ByteBuffer.wrap(stream.toByteArray()).order(ByteOrder.LITTLE_ENDIAN); - - short version = buffer.getShort(); // version - LOG.debug("Got sport summary version " + version + " total bytes=" + buffer.capacity()); - int activityKind = ActivityKind.TYPE_UNKNOWN; - try { - int rawKind = BLETypeConversions.toUnsigned(buffer.getShort()); - HuamiSportsActivityType activityType = HuamiSportsActivityType.fromCode(rawKind); - activityKind = activityType.toActivityKind(); - } catch (Exception ex) { - LOG.error("Error mapping activity kind: " + ex.getMessage(), ex); - } - summary.setActivityKind(activityKind); - - // FIXME: should honor timezone we were in at that time etc - long timestamp_start = BLETypeConversions.toUnsigned(buffer.getInt()) * 1000; - long timestamp_end = BLETypeConversions.toUnsigned(buffer.getInt()) * 1000; - - - // FIXME: should be done like this but seems to return crap when in DST - //summary.setStartTime(new Date(timestamp_start)); - //summary.setEndTime(new Date(timestamp_end)); - - // FIXME ... so do it like this - long duration = timestamp_end - timestamp_start; - summary.setStartTime(new Date(getLastStartTimestamp().getTimeInMillis())); - summary.setEndTime(new Date(getLastStartTimestamp().getTimeInMillis() + duration)); - - int baseLongitude = buffer.getInt(); - int baseLatitude = buffer.getInt(); - int baseAltitude = buffer.getInt(); - summary.setBaseLongitude(baseLongitude); - summary.setBaseLatitude(baseLatitude); - summary.setBaseAltitude(baseAltitude); - - int steps; - int activeSeconds; - int maxLatitude; - int minLatitude; - int maxLongitude; - int minLongitude; - float caloriesBurnt; - float distanceMeters; - float ascentMeters = 0; - float descentMeters = 0; - float maxAltitude = 0; - float minAltitude = 0; - float maxSpeed = 0; - float minPace; - float maxPace; - float totalStride = 0; - float averageStride; - short averageHR; - short averageKMPaceSeconds; - int ascentSeconds = 0; - int descentSeconds = 0; - int flatSeconds = 0; - - // Just assuming, Bip has 259 which seems like 256+x - // Bip S now has 518 so assuming 512+x, might be wrong - - if (version >= 512) { - steps = buffer.getInt(); - activeSeconds = buffer.getInt(); - //unknown - buffer.getLong(); - buffer.getLong(); - caloriesBurnt = buffer.getFloat(); - distanceMeters = buffer.getFloat(); - //unknown - buffer.getLong(); - buffer.getLong(); - buffer.getLong(); - buffer.getLong(); - maxPace = buffer.getFloat(); - minPace = buffer.getFloat(); - //unknown - buffer.getLong(); - buffer.getLong(); - buffer.getLong(); - buffer.getLong(); - buffer.getInt(); - averageHR = buffer.getShort(); - averageKMPaceSeconds = buffer.getShort(); - averageStride = buffer.getShort(); - buffer.getShort(); // unknown - - if (activityKind == ActivityKind.TYPE_CYCLING || activityKind == ActivityKind.TYPE_RUNNING) { - // this had nonsense data with treadmill on bip s, need to test it with running - // for cycling it seems to work... hmm... - // 28 bytes - buffer.getInt(); // unknown - buffer.getInt(); // unknown - ascentSeconds = buffer.getInt() / 1000; //ms? - buffer.getInt(); // unknown; - descentSeconds = buffer.getInt() / 1000; //ms? - buffer.getInt(); // unknown; - flatSeconds = buffer.getInt() / 1000; // ms? - } - } else { - distanceMeters = buffer.getFloat(); - ascentMeters = buffer.getFloat(); - descentMeters = buffer.getFloat(); - minAltitude = buffer.getFloat(); - maxAltitude = buffer.getFloat(); - maxLatitude = buffer.getInt(); // format? - minLatitude = buffer.getInt(); // format? - maxLongitude = buffer.getInt(); // format? - minLongitude = buffer.getInt(); // format? - steps = buffer.getInt(); - activeSeconds = buffer.getInt(); - caloriesBurnt = buffer.getFloat(); - maxSpeed = buffer.getFloat(); - minPace = buffer.getFloat(); - maxPace = buffer.getFloat(); - totalStride = buffer.getFloat(); - - buffer.getInt(); // unknown - if (activityKind == ActivityKind.TYPE_SWIMMING) { - // 28 bytes - float averageStrokeDistance = buffer.getFloat(); - float averageStrokesPerSecond = buffer.getFloat(); - float averageLapPace = buffer.getFloat(); - short strokes = buffer.getShort(); - short swolfIndex = buffer.getShort(); - byte swimStyle = buffer.get(); - byte laps = buffer.get(); - buffer.getInt(); // unknown - buffer.getInt(); // unknown - buffer.getShort(); // unknown - - addSummaryData("averageStrokeDistance", averageStrokeDistance, "meters"); - addSummaryData("averageStrokesPerSecond", averageStrokesPerSecond, "strokes_second"); - addSummaryData("averageLapPace", averageLapPace, "second"); - addSummaryData("strokes", strokes, "strokes"); - addSummaryData("swolfIndex", swolfIndex, "swolf_index"); - addSummaryData("swimStyle", swimStyle, "swim_style"); - addSummaryData("laps", laps, "laps"); - - } else { - // 28 bytes - buffer.getInt(); // unknown - buffer.getInt(); // unknown - ascentSeconds = buffer.getInt() / 1000; //ms? - buffer.getInt(); // unknown; - descentSeconds = buffer.getInt() / 1000; //ms? - buffer.getInt(); // unknown; - flatSeconds = buffer.getInt() / 1000; // ms? - - addSummaryData("ascentSeconds", ascentSeconds, "seconds"); - addSummaryData("descentSeconds", descentSeconds, "seconds"); - addSummaryData("flatSeconds", flatSeconds, "seconds"); - } - - averageHR = buffer.getShort(); - - averageKMPaceSeconds = buffer.getShort(); - averageStride = buffer.getShort(); - } - -// summary.setBaseCoordinate(new GPSCoordinate(baseLatitude, baseLongitude, baseAltitude)); -// summary.setDistanceMeters(distanceMeters); -// summary.setAscentMeters(ascentMeters); -// summary.setDescentMeters(descentMeters); -// summary.setMinAltitude(maxAltitude); -// summary.setMaxAltitude(maxAltitude); -// summary.setMinLatitude(minLatitude); -// summary.setMaxLatitude(maxLatitude); -// summary.setMinLongitude(minLatitude); -// summary.setMaxLongitude(maxLatitude); -// summary.setSteps(steps); -// summary.setActiveTimeSeconds(secondsActive); -// summary.setCaloriesBurnt(caloriesBurnt); -// summary.setMaxSpeed(maxSpeed); -// summary.setMinPace(minPace); -// summary.setMaxPace(maxPace); -// summary.setTotalStride(totalStride); -// summary.setTimeAscent(BLETypeConversions.toUnsigned(ascentSeconds); -// summary.setTimeDescent(BLETypeConversions.toUnsigned(descentSeconds); -// summary.setTimeFlat(BLETypeConversions.toUnsigned(flatSeconds); -// summary.setAverageHR(BLETypeConversions.toUnsigned(averageHR); -// summary.setAveragePace(BLETypeConversions.toUnsigned(averagePace); -// summary.setAverageStride(BLETypeConversions.toUnsigned(averageStride); - - addSummaryData("ascentSeconds", ascentSeconds, "seconds"); - addSummaryData("descentSeconds", descentSeconds, "seconds"); - addSummaryData("flatSeconds", flatSeconds, "seconds"); - - addSummaryData("distanceMeters", distanceMeters, "meters"); - addSummaryData("ascentMeters", ascentMeters, "meters"); - addSummaryData("descentMeters", descentMeters, "meters"); - if (maxAltitude != -100000) { - addSummaryData("maxAltitude", maxAltitude, "meters"); - } - if (minAltitude != 100000) { - addSummaryData("minAltitude", minAltitude, "meters"); - } - addSummaryData("steps", steps, "steps_unit"); - addSummaryData("activeSeconds", activeSeconds, "seconds"); - addSummaryData("caloriesBurnt", caloriesBurnt, "calories_unit"); - addSummaryData("maxSpeed", maxSpeed, "meters_second"); - addSummaryData("minPace", minPace, "seconds_m"); - addSummaryData("maxPace", maxPace, "seconds_m"); - addSummaryData("totalStride", totalStride, "meters"); - addSummaryData("averageHR", averageHR, "bpm"); - addSummaryData("averageKMPaceSeconds", averageKMPaceSeconds, "seconds_km"); - addSummaryData("averageStride", averageStride, "cm"); - - summary.setSummaryData(summaryData.toString()); - return summary; - } - - private void addSummaryData(String key, float value, String unit) { - if (value> 0) { - try { - JSONObject innerData= new JSONObject(); - innerData.put("value", value); - innerData.put("unit", unit); - summaryData.put(key, innerData); - } catch (JSONException ignore) { - } - } - } - @Override protected String getLastSyncTimeKey() {