Compare commits

...

4 Commits

Author SHA1 Message Date
José Rebelo
3902db5abd Huami: Keep track of last debug logs titmestamp 2024-02-25 13:25:12 +00:00
José Rebelo
58d4ebf509 Huami: Refactor activity data fetching
Activity data fetching on Huami devices was filled with duplicated code,
and the handleActivityFetchFinish was called from multiple places where
it did not make sense. This made us signal to the band that activity
fetch was finished when it sometimes was not, causing some race
condititions that would make activity fetch fail or get stuck.

This refactor defines a clear "processBufferedData" that is called
upstream, signaling to the fetch operation that we have received all
data and the buffer can be processed. All handling of metadata and ack
messages is also delegated to the upstream class.
2024-02-25 13:10:25 +00:00
José Rebelo
9b0229cdf0 Huami: Split fetch, init and update operations 2024-02-25 13:10:25 +00:00
José Rebelo
5e068ee4ca Huami: Extract activity fetch data types to enum 2024-02-25 13:10:25 +00:00
44 changed files with 619 additions and 866 deletions

View File

@ -221,18 +221,7 @@ public class HuamiService {
public static final byte SUCCESS = 0x01;
public static final byte COMMAND_ACTIVITY_DATA_START_DATE = 0x01;
public static final byte COMMAND_ACTIVITY_DATA_TYPE_ACTIVTY = 0x01;
public static final byte COMMAND_ACTIVITY_DATA_TYPE_MANUAL_HEART_RATE = 0x02;
public static final byte COMMAND_ACTIVITY_DATA_XXX_DATE = 0x02; // issued on first connect, followd by COMMAND_XXXX_ACTIVITY_DATA instead of COMMAND_FETCH_DATA
public static final byte COMMAND_ACTIVITY_DATA_TYPE_PAI = 0x0d;
public static final byte COMMAND_ACTIVITY_DATA_TYPE_STRESS_MANUAL = 0x12;
public static final byte COMMAND_ACTIVITY_DATA_TYPE_STRESS_AUTOMATIC = 0x13;
public static final byte COMMAND_ACTIVITY_DATA_TYPE_SPO2_NORMAL = 0x25;
public static final byte COMMAND_ACTIVITY_DATA_TYPE_SPO2_SLEEP = 0x26;
public static final byte COMMAND_ACTIVITY_DATA_TYPE_STATISTICS = 0x2c;
public static final byte COMMAND_ACTIVITY_DATA_TYPE_TEMPERATURE = 0x2e;
public static final byte COMMAND_ACTIVITY_DATA_TYPE_SLEEP_RESPIRATORY_RATE = 0x38;
public static final byte COMMAND_ACTIVITY_DATA_TYPE_RESTING_HEART_RATE = 0x3a;
public static final byte COMMAND_ACTIVITY_DATA_TYPE_MAX_HEART_RATE = 0x3d;
public static final byte COMMAND_FIRMWARE_INIT = 0x01; // to UUID_CHARACTERISTIC_FIRMWARE, followed by fw file size in bytes
public static final byte COMMAND_FIRMWARE_START_DATA = 0x03; // to UUID_CHARACTERISTIC_FIRMWARE
@ -240,8 +229,6 @@ public class HuamiService {
public static final byte COMMAND_FIRMWARE_CHECKSUM = 0x04; // to UUID_CHARACTERISTIC_FIRMWARE
public static final byte COMMAND_FIRMWARE_REBOOT = 0x05; // to UUID_CHARACTERISTIC_FIRMWARE
public static final byte[] RESPONSE_ACTIVITY_DATA_START_DATE_SUCCESS = new byte[] { RESPONSE, COMMAND_ACTIVITY_DATA_START_DATE, SUCCESS};
public static final byte[] WEAR_LOCATION_LEFT_WRIST = new byte[] { 0x20, 0x00, 0x00, 0x02 };
public static final byte[] WEAR_LOCATION_RIGHT_WRIST = new byte[] { 0x20, 0x00, 0x00, (byte) 0x82};

View File

@ -19,22 +19,15 @@ package nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip;
import java.util.UUID;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService.DISPLAY_ITEM_BIT_CLOCK;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService.ENDPOINT_DISPLAY;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService.ENDPOINT_DISPLAY_ITEMS;
public class AmazfitBipService {
public static final UUID UUID_CHARACTERISTIC_WEATHER = UUID.fromString("0000000e-0000-3512-2118-0009af100700");
public static final byte COMMAND_ACTIVITY_DATA_TYPE_DEBUGLOGS = 0x07;
public static final byte[] COMMAND_SET_LANGUAGE_SIMPLIFIED_CHINESE = new byte[]{ENDPOINT_DISPLAY, 0x13, 0x00, 0x00};
public static final byte[] COMMAND_SET_LANGUAGE_TRADITIONAL_CHINESE = new byte[]{ENDPOINT_DISPLAY, 0x13, 0x00, 0x01};
public static final byte[] COMMAND_SET_LANGUAGE_ENGLISH = new byte[]{ENDPOINT_DISPLAY, 0x13, 0x00, 0x02};
public static final byte[] COMMAND_SET_LANGUAGE_SPANISH = new byte[]{ENDPOINT_DISPLAY, 0x13, 0x00, 0x03};
public static final byte COMMAND_ACTIVITY_DATA_TYPE_SPORTS_SUMMARIES = 0x05;
public static final byte COMMAND_ACTIVITY_DATA_TYPE_SPORTS_DETAILS = 0x06;
public static final byte[] COMMAND_ACK_FIND_PHONE_IN_PROGRESS = new byte[]{ENDPOINT_DISPLAY, 0x14, 0x00, 0x00};
}

View File

@ -128,19 +128,19 @@ import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes;
import nodomain.freeyourgadget.gadgetbridge.model.SleepState;
import nodomain.freeyourgadget.gadgetbridge.model.WearingState;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.AbstractFetchOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchStatisticsOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchTemperatureOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchHeartRateManualOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchHeartRateMaxOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchHeartRateRestingOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchPaiOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchSleepRespiratoryRateOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchSpo2NormalOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchSportsSummaryOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchStressAutoOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchStressManualOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.HuamiFetchDebugLogsOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch.AbstractFetchOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch.FetchStatisticsOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch.FetchTemperatureOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch.FetchHeartRateManualOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch.FetchHeartRateMaxOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch.FetchHeartRateRestingOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch.FetchPaiOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch.FetchSleepRespiratoryRateOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch.FetchSpo2NormalOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch.FetchSportsSummaryOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch.FetchStressAutoOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch.FetchStressManualOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch.FetchDebugLogsOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsCannedMessagesService;
import nodomain.freeyourgadget.gadgetbridge.util.MediaManager;
import nodomain.freeyourgadget.gadgetbridge.util.SilentMode;
@ -176,10 +176,10 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.common.SimpleNotific
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.actions.StopNotificationAction;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2.Mi2NotificationStrategy;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2.Mi2TextNotificationStrategy;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchActivityOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.InitOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.InitOperation2021;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch.FetchActivityOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.init.InitOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.init.InitOperation2021;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.NotificationStrategy;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.RealtimeSamplesSupport;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
@ -344,7 +344,7 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
protected Huami2021ChunkedEncoder huami2021ChunkedEncoder;
protected Huami2021ChunkedDecoder huami2021ChunkedDecoder;
private final Queue<AbstractFetchOperation> fetchOperationQueue = new LinkedList<>();
private final LinkedList<AbstractFetchOperation> fetchOperationQueue = new LinkedList<>();
public HuamiSupport() {
this(LOG);
@ -1675,7 +1675,7 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
}
if ((dataTypes & RecordedDataTypes.TYPE_DEBUGLOGS) != 0 && coordinator.supportsDebugLogs()) {
this.fetchOperationQueue.add(new HuamiFetchDebugLogsOperation(this));
this.fetchOperationQueue.add(new FetchDebugLogsOperation(this));
}
if ((dataTypes & RecordedDataTypes.TYPE_STRESS) != 0 && coordinator.supportsStressMeasurement()) {
@ -1725,6 +1725,10 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
return fetchOperationQueue.poll();
}
public LinkedList<AbstractFetchOperation> getFetchOperationQueue() {
return fetchOperationQueue;
}
@Override
public void onEnableRealtimeSteps(boolean enable) {
try {

View File

@ -31,14 +31,8 @@ import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip.AmazfitBipFWHelper;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchActivityOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchSportsSummaryOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.HuamiFetchDebugLogsOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.NotificationStrategy;
public class AmazfitBipSupport extends HuamiSupport {

View File

@ -27,8 +27,8 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip3pro.Amazfit
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip.AmazfitBipSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation2020;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperation2020;
public class AmazfitBip3ProSupport extends AmazfitBipSupport {
@Override

View File

@ -29,13 +29,12 @@ import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbips.AmazfitBipSFWHelper;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip.AmazfitBipSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation2020;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperationNew;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperation2020;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperationNew;
import nodomain.freeyourgadget.gadgetbridge.util.Version;
public class AmazfitBipSSupport extends AmazfitBipSupport {

View File

@ -26,8 +26,8 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbipu.AmazfitBip
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbips.AmazfitBipSSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation2020;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperation2020;
public class AmazfitBipUSupport extends AmazfitBipSSupport {

View File

@ -26,8 +26,8 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitgtr.AmazfitGTRLiteFWHelper;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitgts.AmazfitGTSSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperationNew;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperationNew;
import nodomain.freeyourgadget.gadgetbridge.util.Version;
public class AmazfitGTRLiteSupport extends AmazfitGTSSupport {

View File

@ -24,9 +24,9 @@ import java.io.IOException;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitgtr.AmazfitGTRFWHelper;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitgts.AmazfitGTSSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation2020;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperationNew;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperation2020;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperationNew;
import nodomain.freeyourgadget.gadgetbridge.util.Version;
public class AmazfitGTRSupport extends AmazfitGTSSupport {

View File

@ -28,8 +28,8 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitgtr2.AmazfitGTR
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitgtr.AmazfitGTRSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation2020;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperation2020;
public class AmazfitGTR2Support extends AmazfitGTRSupport {

View File

@ -27,12 +27,11 @@ import java.io.IOException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitgts.AmazfitGTSFWHelper;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip.AmazfitBipSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation2020;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperationNew;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperation2020;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperationNew;
import nodomain.freeyourgadget.gadgetbridge.util.Version;
public class AmazfitGTSSupport extends AmazfitBipSupport {

View File

@ -28,8 +28,8 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitgts2.AmazfitGTS
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitgts.AmazfitGTSSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation2020;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperation2020;
public class AmazfitGTS2Support extends AmazfitGTSSupport {

View File

@ -35,12 +35,11 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitneo.AmazfitNeoF
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband5.MiBand5Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation2020;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperation2020;
public class AmazfitNeoSupport extends MiBand5Support {

View File

@ -26,12 +26,11 @@ import java.io.IOException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip.AmazfitBipSupport;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitvergel.AmazfitVergeLFWHelper;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation2020;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperation2020;
public class AmazfitVergeLSupport extends AmazfitBipSupport {
private static final Logger LOG = LoggerFactory.getLogger(AmazfitVergeLSupport.class);

View File

@ -29,8 +29,8 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband4.MiBand4FWHelper;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband3.MiBand3Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperationNew;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperationNew;
public class MiBand4Support extends MiBand3Support {
private static final Logger LOG = LoggerFactory.getLogger(MiBand4Support.class);

View File

@ -30,8 +30,8 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband6.MiBand6FWHelpe
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband5.MiBand5Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation2020;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperation2020;
public class MiBand6Support extends MiBand5Support {
private static final Logger LOG = LoggerFactory.getLogger(MiBand6Support.class);

View File

@ -1,281 +0,0 @@
/* Copyright (C) 2018-2024 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, José Rebelo, Oleg Vasilev, Sebastian Krey, Your Name
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.huami.operations;
import android.text.format.DateUtils;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.GregorianCalendar;
import androidx.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.Logging;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip.AmazfitBipService;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.export.ActivityTrackExporter;
import nodomain.freeyourgadget.gadgetbridge.export.GPXExporter;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuamiActivityDetailsParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiActivityDetailsParser;
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
* An operation that fetches activity data. For every fetch, a new operation must
* be created, i.e. an operation may not be reused for multiple fetches.
*/
public class FetchSportsDetailsOperation extends AbstractFetchOperation {
private static final Logger LOG = LoggerFactory.getLogger(FetchSportsDetailsOperation.class);
private final AbstractHuamiActivityDetailsParser detailsParser;
private final BaseActivitySummary summary;
private final String lastSyncTimeKey;
private ByteArrayOutputStream buffer;
FetchSportsDetailsOperation(@NonNull BaseActivitySummary summary,
@NonNull AbstractHuamiActivityDetailsParser detailsParser,
@NonNull HuamiSupport support,
@NonNull String lastSyncTimeKey,
int fetchCount) {
super(support);
setName("fetching sport details");
this.summary = summary;
this.detailsParser = detailsParser;
this.lastSyncTimeKey = lastSyncTimeKey;
this.fetchCount = fetchCount;
}
@Override
protected String taskDescription() {
return getContext().getString(R.string.busy_task_fetch_sports_details);
}
@Override
protected void startFetching(TransactionBuilder builder) {
LOG.info("start " + getName());
buffer = new ByteArrayOutputStream(1024);
GregorianCalendar sinceWhen = getLastSuccessfulSyncTime();
startFetching(builder, AmazfitBipService.COMMAND_ACTIVITY_DATA_TYPE_SPORTS_DETAILS, sinceWhen);
}
@Override
protected boolean handleActivityFetchFinish(boolean success) {
LOG.info(getName() + " has finished round " + fetchCount);
boolean parseSuccess = true;
if (success && buffer.size() > 0) {
if (detailsParser instanceof HuamiActivityDetailsParser) {
((HuamiActivityDetailsParser) detailsParser).setSkipCounterByte(false); // is already stripped
}
try {
ActivityTrack track = detailsParser.parse(buffer.toByteArray());
ActivityTrackExporter exporter = createExporter();
String trackType = "track";
switch (summary.getActivityKind()) {
case ActivityKind.TYPE_CYCLING:
trackType = getContext().getString(R.string.activity_type_biking);
break;
case ActivityKind.TYPE_RUNNING:
trackType = getContext().getString(R.string.activity_type_running);
break;
case ActivityKind.TYPE_WALKING:
trackType = getContext().getString(R.string.activity_type_walking);
break;
case ActivityKind.TYPE_HIKING:
trackType = getContext().getString(R.string.activity_type_hiking);
break;
case ActivityKind.TYPE_CLIMBING:
trackType = getContext().getString(R.string.activity_type_climbing);
break;
case ActivityKind.TYPE_SWIMMING:
trackType = getContext().getString(R.string.activity_type_swimming);
break;
}
final String rawBytesPath = saveRawBytes();
String fileName = FileUtils.makeValidFileName("gadgetbridge-" + trackType.toLowerCase() + "-" + DateTimeUtils.formatIso8601(summary.getStartTime()) + ".gpx");
File targetFile = new File(FileUtils.getExternalFilesDir(), fileName);
boolean exportGpxSuccess = true;
try {
exporter.performExport(track, targetFile);
} catch (ActivityTrackExporter.GPXTrackEmptyException ex) {
exportGpxSuccess = false;
GB.toast(getContext(), "This activity does not contain GPX tracks.", Toast.LENGTH_LONG, GB.ERROR, ex);
}
try (DBHandler dbHandler = GBApplication.acquireDB()) {
if (exportGpxSuccess) {
summary.setGpxTrack(targetFile.getAbsolutePath());
}
if (rawBytesPath != null) {
summary.setRawDetailsPath(rawBytesPath);
}
dbHandler.getDaoSession().getBaseActivitySummaryDao().update(summary);
}
} catch (Exception ex) {
GB.toast(getContext(), "Error getting activity details: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
parseSuccess = false;
}
}
final boolean superSuccess = super.handleActivityFetchFinish(success);
if (success && parseSuccess) {
// Always increment the sync timestamp on success, even if we did not get data
GregorianCalendar endTime = BLETypeConversions.createCalendar();
endTime.setTime(summary.getEndTime());
saveLastSyncTimestamp(endTime);
if (needsAnotherFetch(endTime)) {
FetchSportsSummaryOperation nextOperation = new FetchSportsSummaryOperation(getSupport(), fetchCount);
try {
nextOperation.perform();
} catch (IOException ex) {
LOG.error("Error starting another round of fetching activity data", ex);
}
}
}
return superSuccess && parseSuccess;
}
private boolean needsAnotherFetch(GregorianCalendar lastSyncTimestamp) {
// We have 2 operations per fetch round: summary + details
if (fetchCount > 10) {
LOG.warn("Already have 5 fetch rounds, not doing another one.");
return false;
}
if (DateUtils.isToday(lastSyncTimestamp.getTimeInMillis())) {
LOG.info("Hopefully no further fetch needed, last synced timestamp is from today.");
return false;
}
if (lastSyncTimestamp.getTimeInMillis() > System.currentTimeMillis()) {
LOG.warn("Not doing another fetch since last synced timestamp is in the future: {}", DateTimeUtils.formatDateTime(lastSyncTimestamp.getTime()));
return false;
}
LOG.info("Doing another fetch since last sync timestamp is still too old: {}", DateTimeUtils.formatDateTime(lastSyncTimestamp.getTime()));
return true;
}
@Override
protected boolean validChecksum(int crc32) {
return crc32 == CheckSums.getCRC32(buffer.toByteArray());
}
private ActivityTrackExporter createExporter() {
GPXExporter exporter = new GPXExporter();
exporter.setCreator(GBApplication.app().getNameAndVersion());
return exporter;
}
/**
* Method to handle the incoming activity data.
* There are two kind of messages we currently know:
* - the first one is 11 bytes long and contains metadata (how many bytes to expect, when the data starts, etc.)
* - the second one is 20 bytes long and contains the actual activity data
* <p/>
* The first message type is parsed by this method, for every other length of the value param, bufferActivityData is called.
*
* @param value
*/
@Override
protected void handleActivityNotif(byte[] value) {
LOG.warn("sports details: " + Logging.formatBytes(value));
if (!isOperationRunning()) {
LOG.error("ignoring sports details notification because operation is not running. Data length: " + value.length);
getSupport().logMessageContent(value);
return;
}
if (value.length < 2) {
LOG.error("unexpected sports details data length: " + value.length);
getSupport().logMessageContent(value);
return;
}
if ((byte) (lastPacketCounter + 1) == value[0]) {
lastPacketCounter++;
bufferActivityData(value);
} else {
GB.toast("Error " + getName() + ", invalid package counter: " + value[0] + ", last was: " + lastPacketCounter, Toast.LENGTH_LONG, GB.ERROR);
handleActivityFetchFinish(false);
}
}
/**
* Buffers the given activity summary data. If the total size is reached,
* it is converted to an object and saved in the database.
*
* @param value
*/
@Override
protected void bufferActivityData(byte[] value) {
buffer.write(value, 1, value.length - 1); // skip the counter
}
@Override
protected String getLastSyncTimeKey() {
return lastSyncTimeKey;
}
protected GregorianCalendar getLastSuccessfulSyncTime() {
GregorianCalendar calendar = BLETypeConversions.createCalendar();
calendar.setTime(summary.getStartTime());
return calendar;
}
private String saveRawBytes() {
final String fileName = FileUtils.makeValidFileName(String.format("%s.bin", DateTimeUtils.formatIso8601(summary.getStartTime())));
FileOutputStream outputStream = null;
try {
final File targetFolder = new File(FileUtils.getExternalFilesDir(), "rawDetails");
targetFolder.mkdirs();
final File targetFile = new File(targetFolder, fileName);
outputStream = new FileOutputStream(targetFile);
outputStream.write(buffer.toByteArray());
outputStream.close();
return targetFile.getAbsolutePath();
} catch (final IOException e) {
LOG.error("Failed to save raw bytes", e);
}
return null;
}
}

View File

@ -1,196 +0,0 @@
/* Copyright (C) 2018-2024 Andreas Shimokawa, Carsten Pfeiffer, Daniel
Dakhno, Daniele Gobbetti, José Rebelo, Oleg Vasilev, Petr Vaněk
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.huami.operations;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.GregorianCalendar;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.Logging;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
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.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuamiActivityDetailsParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
* An operation that fetches activity data. For every fetch, a new operation must
* be created, i.e. an operation may not be reused for multiple fetches.
*/
public class FetchSportsSummaryOperation extends AbstractFetchOperation {
private static final Logger LOG = LoggerFactory.getLogger(FetchSportsSummaryOperation.class);
private ByteArrayOutputStream buffer = new ByteArrayOutputStream(140);
public FetchSportsSummaryOperation(HuamiSupport support, int fetchCount) {
super(support);
setName("fetching sport summaries");
this.fetchCount = fetchCount;
}
@Override
protected String taskDescription() {
return getContext().getString(R.string.busy_task_fetch_sports_summaries);
}
@Override
protected void startFetching(TransactionBuilder builder) {
LOG.info("start" + getName());
GregorianCalendar sinceWhen = getLastSuccessfulSyncTime();
startFetching(builder, AmazfitBipService.COMMAND_ACTIVITY_DATA_TYPE_SPORTS_SUMMARIES, sinceWhen);
}
@Override
protected boolean handleActivityFetchFinish(boolean success) {
LOG.info(getName() + " has finished round " + fetchCount);
BaseActivitySummary summary = null;
final DeviceCoordinator coordinator = getDevice().getDeviceCoordinator();
final ActivitySummaryParser summaryParser = coordinator.getActivitySummaryParser(getDevice());
boolean parseSummarySuccess = true;
if (success && buffer.size() > 0) {
summary = new BaseActivitySummary();
summary.setStartTime(getLastStartTimestamp().getTime()); // due to a bug this has to be set
summary.setRawSummaryData(buffer.toByteArray());
try {
summary = summaryParser.parseBinaryData(summary);
} catch (final Exception e) {
GB.toast(getContext(), "Failed to parse activity summary", Toast.LENGTH_LONG, GB.ERROR, e);
summary = null;
parseSummarySuccess = false;
}
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);
parseSummarySuccess = false;
}
}
}
final boolean superSuccess = super.handleActivityFetchFinish(success);
boolean getDetailsSuccess = true;
if (summary != null) {
final AbstractHuamiActivityDetailsParser detailsParser = ((HuamiActivitySummaryParser) summaryParser).getDetailsParser(summary);
FetchSportsDetailsOperation nextOperation = new FetchSportsDetailsOperation(summary, detailsParser, getSupport(), getLastSyncTimeKey(), fetchCount);
try {
nextOperation.perform();
} catch (IOException ex) {
GB.toast(getContext(), "Unable to fetch activity details: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
getDetailsSuccess = false;
}
}
return parseSummarySuccess && superSuccess && getDetailsSuccess;
}
@Override
protected boolean validChecksum(int crc32) {
return crc32 == CheckSums.getCRC32(buffer.toByteArray());
}
@Override
public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
LOG.warn("characteristic read: " + characteristic.getUuid() + ": " + Logging.formatBytes(characteristic.getValue()));
return super.onCharacteristicRead(gatt, characteristic, status);
}
/**
* Method to handle the incoming activity data.
* There are two kind of messages we currently know:
* - the first one is 11 bytes long and contains metadata (how many bytes to expect, when the data starts, etc.)
* - the second one is 20 bytes long and contains the actual activity data
* <p/>
* The first message type is parsed by this method, for every other length of the value param, bufferActivityData is called.
*
* @param value
*/
@Override
protected void handleActivityNotif(byte[] value) {
LOG.warn("sports summary data: " + Logging.formatBytes(value));
if (!isOperationRunning()) {
LOG.error("ignoring activity data notification because operation is not running. Data length: " + value.length);
getSupport().logMessageContent(value);
return;
}
if (value.length < 2) {
LOG.error("unexpected sports summary data length: " + value.length);
getSupport().logMessageContent(value);
return;
}
if ((byte) (lastPacketCounter + 1) == value[0]) {
lastPacketCounter++;
bufferActivityData(value);
} else {
GB.toast("Error " + getName() + ", invalid package counter: " + value[0] + ", last was: " + lastPacketCounter, Toast.LENGTH_LONG, GB.ERROR);
handleActivityFetchFinish(false);
}
}
/**
* Buffers the given activity summary data. If the total size is reached,
* it is converted to an object and saved in the database.
*
* @param value
*/
@Override
protected void bufferActivityData(byte[] value) {
buffer.write(value, 1, value.length - 1); // skip the counter
}
@Override
protected String getLastSyncTimeKey() {
return "lastSportsActivityTimeMillis";
}
}

View File

@ -15,7 +15,7 @@
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.huami.operations;
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
@ -28,6 +28,7 @@ import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.text.DateFormat;
import java.util.Arrays;
@ -42,39 +43,41 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.AbstractGattListenerWriteAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceBusyAction;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuamiOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsSupport;
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
/**
* An operation that fetches activity data. For every fetch, a new operation must
* be created, i.e. an operation may not be reused for multiple fetches.
* An operation that fetches activity data.
*/
public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
private static final Logger LOG = LoggerFactory.getLogger(AbstractFetchOperation.class);
protected byte lastPacketCounter;
int fetchCount;
protected BluetoothGattCharacteristic characteristicActivityData;
protected BluetoothGattCharacteristic characteristicFetch;
Calendar startTimestamp;
int expectedDataLength = 0;
public AbstractFetchOperation(HuamiSupport support) {
protected Calendar startTimestamp;
protected int fetchCount;
protected byte lastPacketCounter;
protected int expectedDataLength = 0;
protected final ByteArrayOutputStream buffer = new ByteArrayOutputStream(1024);
protected boolean operationValid = true; // to mark operation failed midway (eg. out of sync)
public AbstractFetchOperation(final HuamiSupport support) {
super(support);
}
@Override
protected void enableNeededNotifications(TransactionBuilder builder, boolean enable) {
protected void enableNeededNotifications(final TransactionBuilder builder, final boolean enable) {
if (!enable) {
// dynamically enabled, but always disabled on finish
builder.notify(characteristicFetch, enable);
builder.notify(characteristicActivityData, enable);
builder.notify(characteristicFetch, false);
builder.notify(characteristicActivityData, false);
}
}
@ -87,7 +90,7 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
expectedDataLength = 0;
lastPacketCounter = -1;
TransactionBuilder builder = performInitialized(getName());
final TransactionBuilder builder = performInitialized(getName());
if (fetchCount == 0) {
builder.add(new SetDeviceBusyAction(getDevice(), taskDescription(), getContext()));
}
@ -114,11 +117,11 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
protected abstract String getLastSyncTimeKey();
@Override
public boolean onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
UUID characteristicUUID = characteristic.getUuid();
public boolean onCharacteristicChanged(final BluetoothGatt gatt,
final BluetoothGattCharacteristic characteristic) {
final UUID characteristicUUID = characteristic.getUuid();
if (HuamiService.UUID_CHARACTERISTIC_5_ACTIVITY_DATA.equals(characteristicUUID)) {
handleActivityNotif(characteristic.getValue());
handleActivityData(characteristic.getValue());
return true;
} else if (HuamiService.UUID_UNKNOWN_CHARACTERISTIC4.equals(characteristicUUID)) {
handleActivityMetadata(characteristic.getValue());
@ -129,18 +132,15 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
}
/**
* Handles the finishing of fetching the activity.
* @param success whether fetching was successful
* @return whether handling the activity fetch finish was successful
* Handles the finishing of fetching the activity. This signals the actual end of this operation.
*/
@CallSuper
protected boolean handleActivityFetchFinish(boolean success) {
protected final void onOperationFinished() {
final AbstractFetchOperation nextFetchOperation = getSupport().getNextFetchOperation();
if (nextFetchOperation != null) {
LOG.debug("Performing next operation {}", nextFetchOperation.getName());
try {
nextFetchOperation.perform();
return true;
return;
} catch (final IOException e) {
GB.toast(
getContext(),
@ -148,17 +148,16 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
Toast.LENGTH_SHORT,
GB.ERROR, e
);
return false;
return;
}
}
LOG.debug("All operations finished");
GB.updateTransferNotification(null, "", false, 100, getContext());
GB.signalActivityDataFinish();
operationFinished();
unsetBusy();
return true;
}
/**
@ -168,72 +167,54 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
* @param crc32 the expected checksum
* @return whether the checksum was valid
*/
protected abstract boolean validChecksum(int crc32);
protected boolean validChecksum(int crc32) {
return crc32 == CheckSums.getCRC32(buffer.toByteArray());
}
/**
* Method to handle the incoming activity data.
* There are two kind of messages we currently know:
* - the first one is 11 bytes long and contains metadata (how many bytes to expect, when the data starts, etc.)
* - the second one is 20 bytes long and contains the actual activity data
* <p/>
* The first message type is parsed by this method, for every other length of the value param, bufferActivityData is called.
*
* @param value
*/
protected abstract void handleActivityNotif(byte[] value);
protected abstract boolean processBufferedData();
protected abstract void bufferActivityData(byte[] value);
protected void handleActivityData(final byte[] value) {
LOG.debug("{} data: {}", getName(), Logging.formatBytes(value));
protected void startFetching(TransactionBuilder builder, byte fetchType, GregorianCalendar sinceWhen) {
final String taskName = StringUtils.ensureNotNull(builder.getTaskName());
if (!isOperationRunning()) {
LOG.error("ignoring {} notification because operation is not running. Data length: {}", getName(), value.length);
getSupport().logMessageContent(value);
return;
}
if ((byte) (lastPacketCounter + 1) == value[0]) {
// TODO we should handle skipped or repeated bytes more gracefully
lastPacketCounter++;
bufferActivityData(value);
} else {
GB.toast("Error " + getName() + ", invalid package counter: " + value[0] + ", last was: " + lastPacketCounter, Toast.LENGTH_LONG, GB.ERROR);
operationValid = false;
}
}
protected void bufferActivityData(byte[] value) {
buffer.write(value, 1, value.length - 1); // skip the counter
}
protected void startFetching(final TransactionBuilder builder, final byte fetchType, final GregorianCalendar sinceWhen) {
final HuamiSupport support = getSupport();
final boolean isZeppOs = support instanceof ZeppOsSupport;
byte[] fetchBytes = BLETypeConversions.join(new byte[]{
HuamiService.COMMAND_ACTIVITY_DATA_START_DATE,
fetchType},
support.getTimeBytes(sinceWhen, support.getFetchOperationsTimeUnit()));
builder.add(new AbstractGattListenerWriteAction(getQueue(), characteristicFetch, fetchBytes) {
@Override
protected boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
UUID characteristicUUID = characteristic.getUuid();
if (HuamiService.UUID_UNKNOWN_CHARACTERISTIC4.equals(characteristicUUID)) {
byte[] value = characteristic.getValue();
if (ArrayUtils.equals(value, HuamiService.RESPONSE_ACTIVITY_DATA_START_DATE_SUCCESS, 0)) {
handleActivityMetadata(value);
if (expectedDataLength == 0 && isZeppOs) {
// Nothing to receive, if we try to fetch data it will fail
sendAckZeppOs(true);
} else if (expectedDataLength != 0) {
TransactionBuilder newBuilder = createTransactionBuilder(taskName + " Step 2");
newBuilder.notify(characteristicActivityData, true);
newBuilder.write(characteristicFetch, new byte[]{HuamiService.COMMAND_FETCH_DATA});
try {
performImmediately(newBuilder);
} catch (IOException ex) {
GB.toast(getContext(), "Error fetching debug logs: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
}
}
return true;
} else {
handleActivityMetadata(value);
}
}
return false;
}
});
builder.write(characteristicFetch, fetchBytes);
}
private void handleActivityMetadata(byte[] value) {
if (value.length < 3) {
LOG.warn("Activity metadata too short: {}", Logging.formatBytes(value));
handleActivityFetchFinish(false);
onOperationFinished();
return;
}
if (value[0] != HuamiService.RESPONSE) {
LOG.warn("Activity metadata not a response: {}", Logging.formatBytes(value));
handleActivityFetchFinish(false);
onOperationFinished();
return;
}
@ -245,26 +226,26 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
handleFetchDataResponse(value);
return;
case HuamiService.COMMAND_ACK_ACTIVITY_DATA:
// ignore, this is just the reply to the COMMAND_ACK_ACTIVITY_DATA
LOG.info("Got reply to COMMAND_ACK_ACTIVITY_DATA");
onOperationFinished();
return;
default:
LOG.warn("Unexpected activity metadata: {}", Logging.formatBytes(value));
handleActivityFetchFinish(false);
onOperationFinished();
}
}
private void handleStartDateResponse(final byte[] value) {
if (value[2] != HuamiService.SUCCESS) {
LOG.warn("Start date unsuccessful response: {}", Logging.formatBytes(value));
handleActivityFetchFinish(false);
onOperationFinished();
return;
}
// it's 16 on the MB7, with a 0 at the end
if (value.length != 15 && (value.length != 16 && value[15] != 0x00)) {
LOG.warn("Start date response length: {}", Logging.formatBytes(value));
handleActivityFetchFinish(false);
onOperationFinished();
return;
}
@ -277,7 +258,8 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
if (expectedDataLength == 0) {
LOG.info("No data to fetch since {}", startTimestamp.getTime());
handleActivityFetchFinish(true);
sendAck(true);
// do not finish the operation - do it in the ack response
return;
}
@ -287,85 +269,108 @@ public abstract class AbstractFetchOperation extends AbstractHuamiOperation {
GB.updateTransferNotification(taskDescription(),
getContext().getString(R.string.FetchActivityOperation_about_to_transfer_since,
DateFormat.getDateTimeInstance().format(startTimestamp.getTime())), true, 0, getContext());
// Trigger the actual data fetch
final TransactionBuilder step2builder = createTransactionBuilder(getName() + " Step 2");
step2builder.notify(characteristicActivityData, true);
step2builder.write(characteristicFetch, new byte[]{HuamiService.COMMAND_FETCH_DATA});
try {
performImmediately(step2builder);
} catch (final IOException e) {
GB.toast(getContext(), "Error starting fetch step 2: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
onOperationFinished();
}
}
private void handleFetchDataResponse(final byte[] value) {
if (value[2] != HuamiService.SUCCESS) {
LOG.warn("Fetch data unsuccessful response: {}", Logging.formatBytes(value));
handleActivityFetchFinish(false);
onOperationFinished();
return;
}
if (value.length != 3 && value.length != 7) {
LOG.warn("Fetch data unexpected metadata length: {}", Logging.formatBytes(value));
handleActivityFetchFinish(false);
onOperationFinished();
return;
}
if (value.length == 7 && !validChecksum(BLETypeConversions.toUint32(value, 3))) {
LOG.warn("Data checksum invalid");
handleActivityFetchFinish(false);
sendAckZeppOs(true);
// If we're on Zepp OS, ack but keep data on device
if (isZeppOs()) {
sendAck(true);
// do not finish the operation - do it in the ack response
return;
}
onOperationFinished();
return;
}
boolean handleFinishSuccess;
try {
handleFinishSuccess = handleActivityFetchFinish(true);
} catch (final Exception e) {
LOG.warn("Failed to handle activity fetch finish", e);
handleFinishSuccess = false;
final boolean success = operationValid && processBufferedData();
final boolean keepActivityDataOnDevice = !success || HuamiCoordinator.getKeepActivityDataOnDevice(getDevice().getAddress());
if (isZeppOs() || !keepActivityDataOnDevice) {
sendAck(keepActivityDataOnDevice);
// do not finish the operation - do it in the ack response
return;
}
final boolean keepActivityDataOnDevice = HuamiCoordinator.getKeepActivityDataOnDevice(getDevice().getAddress());
sendAckZeppOs(keepActivityDataOnDevice || !handleFinishSuccess);
onOperationFinished();
}
protected void sendAckZeppOs(final boolean keepDataOnDevice) {
if (!(getSupport() instanceof ZeppOsSupport)) {
return;
protected void sendAck(final boolean keepDataOnDevice) {
final byte[] ackBytes;
if (isZeppOs()) {
LOG.debug("Sending ack, keepDataOnDevice = {}", keepDataOnDevice);
// 0x01 to ACK, mark as saved on phone (drop from band)
// 0x09 to ACK, but keep it marked as not saved
// If 0x01 is sent, detailed information seems to be discarded, and is not sent again anymore
final byte ackByte = (byte) (keepDataOnDevice ? 0x09 : 0x01);
ackBytes = new byte[]{HuamiService.COMMAND_ACK_ACTIVITY_DATA, ackByte};
} else {
LOG.debug("Sending ack, simple");
ackBytes = new byte[]{HuamiService.COMMAND_ACK_ACTIVITY_DATA};
}
LOG.debug("Sending Zepp OS ack, keepDataOnDevice = {}", keepDataOnDevice);
// 0x01 to ACK, mark as saved on phone (drop from band)
// 0x09 to ACK, but keep it marked as not saved
// If 0x01 is sent, detailed information seems to be discarded, and is not sent again anymore
final byte ackByte = (byte) (keepDataOnDevice ? 0x09 : 0x01);
try {
final TransactionBuilder builder = performInitialized(getName() + " end");
builder.write(characteristicFetch, new byte[]{HuamiService.COMMAND_ACK_ACTIVITY_DATA, ackByte});
final TransactionBuilder builder = createTransactionBuilder(getName() + " end");
builder.write(characteristicFetch, ackBytes);
performImmediately(builder);
} catch (final IOException e) {
LOG.error("Ending failed", e);
LOG.error("Failed to send ack", e);
}
}
private void setStartTimestamp(Calendar startTimestamp) {
private void setStartTimestamp(final Calendar startTimestamp) {
this.startTimestamp = startTimestamp;
}
Calendar getLastStartTimestamp() {
protected Calendar getLastStartTimestamp() {
return startTimestamp;
}
void saveLastSyncTimestamp(@NonNull GregorianCalendar timestamp) {
SharedPreferences.Editor editor = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()).edit();
protected void saveLastSyncTimestamp(@NonNull final GregorianCalendar timestamp) {
final SharedPreferences.Editor editor = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()).edit();
editor.putLong(getLastSyncTimeKey(), timestamp.getTimeInMillis());
editor.apply();
}
protected GregorianCalendar getLastSuccessfulSyncTime() {
long timeStampMillis = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()).getLong(getLastSyncTimeKey(), 0);
final long timeStampMillis = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()).getLong(getLastSyncTimeKey(), 0);
if (timeStampMillis != 0) {
GregorianCalendar calendar = BLETypeConversions.createCalendar();
calendar.setTimeInMillis(timeStampMillis);
return calendar;
}
GregorianCalendar calendar = BLETypeConversions.createCalendar();
final GregorianCalendar calendar = BLETypeConversions.createCalendar();
calendar.add(Calendar.DAY_OF_MONTH, -100);
return calendar;
}
protected boolean isZeppOs() {
return getSupport() instanceof ZeppOsSupport;
}
}

View File

@ -14,32 +14,22 @@
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.huami.operations;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.widget.Toast;
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.Logging;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
* A repeating fetch operation. This operation repeats the fetch up to a certain number of times, or
@ -49,21 +39,19 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB;
public abstract class AbstractRepeatingFetchOperation extends AbstractFetchOperation {
private static final Logger LOG = LoggerFactory.getLogger(AbstractRepeatingFetchOperation.class);
private final ByteArrayOutputStream byteStreamBuffer = new ByteArrayOutputStream(140);
protected final HuamiFetchDataType dataType;
protected final byte dataType;
public AbstractRepeatingFetchOperation(final HuamiSupport support, final byte dataType, final String dataName) {
public AbstractRepeatingFetchOperation(final HuamiSupport support, final HuamiFetchDataType dataType) {
super(support);
this.dataType = dataType;
setName("fetching " + dataName);
setName("fetching " + dataType.name());
}
@Override
protected void startFetching(final TransactionBuilder builder) {
final GregorianCalendar sinceWhen = getLastSuccessfulSyncTime();
LOG.info("start {} since {}", getName(), sinceWhen.getTime());
startFetching(builder, dataType, sinceWhen);
startFetching(builder, dataType.getCode(), sinceWhen);
}
/**
@ -77,21 +65,14 @@ public abstract class AbstractRepeatingFetchOperation extends AbstractFetchOpera
protected abstract boolean handleActivityData(GregorianCalendar timestamp, byte[] bytes);
@Override
protected boolean handleActivityFetchFinish(final boolean success) {
LOG.info("{} has finished round {}: {}, got {} bytes in buffer", getName(), fetchCount, success, byteStreamBuffer.size());
protected boolean processBufferedData() {
LOG.info("{} has finished round {}, got {} bytes in buffer", getName(), fetchCount, buffer.size());
if (!success) {
// We need to explicitly ack this, or the next operation will fail fetch will become stuck
sendAckZeppOs(true);
super.handleActivityFetchFinish(false);
return false;
if (buffer.size() == 0) {
return true;
}
if (byteStreamBuffer.size() == 0) {
return super.handleActivityFetchFinish(true);
}
final byte[] bytes = byteStreamBuffer.toByteArray();
final byte[] bytes = buffer.toByteArray();
final GregorianCalendar timestamp = (GregorianCalendar) this.startTimestamp.clone();
// Uncomment to dump the bytes to external storage for debugging
@ -100,7 +81,6 @@ public abstract class AbstractRepeatingFetchOperation extends AbstractFetchOpera
final boolean handleSuccess = handleActivityData(timestamp, bytes);
if (!handleSuccess) {
super.handleActivityFetchFinish(false);
return false;
}
@ -108,63 +88,12 @@ public abstract class AbstractRepeatingFetchOperation extends AbstractFetchOpera
saveLastSyncTimestamp(timestamp);
if (needsAnotherFetch(timestamp)) {
byteStreamBuffer.reset();
buffer.reset();
try {
final boolean keepActivityDataOnDevice = HuamiCoordinator.getKeepActivityDataOnDevice(getDevice().getAddress());
sendAckZeppOs(keepActivityDataOnDevice);
startFetching();
return true;
} catch (final IOException ex) {
LOG.error("Error starting another round of " + getName(), ex);
super.handleActivityFetchFinish(false);
return false;
}
getSupport().getFetchOperationQueue().add(0, this);
}
final boolean superSuccess = super.handleActivityFetchFinish(true);
postActivityFetchFinish(superSuccess);
return superSuccess;
}
protected void postActivityFetchFinish(final boolean success) {
}
@Override
protected boolean validChecksum(final int crc32) {
return crc32 == CheckSums.getCRC32(byteStreamBuffer.toByteArray());
}
@Override
public boolean onCharacteristicRead(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) {
LOG.debug("characteristic read: {}: {}", characteristic.getUuid(), Logging.formatBytes(characteristic.getValue()));
return super.onCharacteristicRead(gatt, characteristic, status);
}
@Override
protected void handleActivityNotif(final byte[] value) {
LOG.debug("{} data: {}", getName(), Logging.formatBytes(value));
if (!isOperationRunning()) {
LOG.error("ignoring {} notification because operation is not running. Data length: {}", getName(), value.length);
getSupport().logMessageContent(value);
return;
}
if ((byte) (lastPacketCounter + 1) == value[0]) {
// TODO we should handle skipped or repeated bytes more gracefully
lastPacketCounter++;
bufferActivityData(value);
} else {
GB.toast("Error " + getName() + ", invalid package counter: " + value[0] + ", last was: " + lastPacketCounter, Toast.LENGTH_LONG, GB.ERROR);
handleActivityFetchFinish(false);
}
}
@Override
protected void bufferActivityData(final byte[] value) {
byteStreamBuffer.write(value, 1, value.length - 1); // skip the counter
return true;
}
private boolean needsAnotherFetch(final GregorianCalendar lastSyncTimestamp) {

View File

@ -15,7 +15,7 @@
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.huami.operations;
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch;
import android.widget.Toast;
@ -36,7 +36,6 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.HuamiExtendedActivitySample;
@ -44,7 +43,6 @@ import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
@ -57,7 +55,7 @@ public class FetchActivityOperation extends AbstractRepeatingFetchOperation {
private final int sampleSize;
public FetchActivityOperation(final HuamiSupport support) {
super(support, HuamiService.COMMAND_ACTIVITY_DATA_TYPE_ACTIVTY, "activity data");
super(support, HuamiFetchDataType.ACTIVITY);
this.sampleSize = support.getActivitySampleSize();
}
@ -131,11 +129,6 @@ public class FetchActivityOperation extends AbstractRepeatingFetchOperation {
}
}
@Override
protected void postActivityFetchFinish(final boolean success) {
GB.signalActivityDataFinish();
}
@Override
protected boolean validChecksum(final int crc32) {
// TODO actually check it

View File

@ -14,9 +14,9 @@
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.huami.operations;
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch;
import android.widget.Toast;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -25,27 +25,21 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import androidx.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip.AmazfitBipService;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class HuamiFetchDebugLogsOperation extends AbstractFetchOperation {
private static final Logger LOG = LoggerFactory.getLogger(HuamiFetchDebugLogsOperation.class);
public class FetchDebugLogsOperation extends AbstractFetchOperation {
private static final Logger LOG = LoggerFactory.getLogger(FetchDebugLogsOperation.class);
private FileOutputStream logOutputStream;
public HuamiFetchDebugLogsOperation(HuamiSupport support) {
public FetchDebugLogsOperation(final HuamiSupport support) {
super(support);
setName("fetch debug logs");
}
@ -56,7 +50,7 @@ public class HuamiFetchDebugLogsOperation extends AbstractFetchOperation {
}
@Override
protected void startFetching(TransactionBuilder builder) {
protected void startFetching(final TransactionBuilder builder) {
File dir;
try {
dir = FileUtils.getExternalFilesDir();
@ -64,39 +58,37 @@ public class HuamiFetchDebugLogsOperation extends AbstractFetchOperation {
return;
}
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US);
String filename = "huamidebug_" + dateFormat.format(new Date()) + ".log";
final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US);
final String filename = "huamidebug_" + dateFormat.format(new Date()) + ".log";
File outputFile = new File(dir, filename );
File outputFile = new File(dir, filename);
try {
logOutputStream = new FileOutputStream(outputFile);
} catch (IOException e) {
LOG.warn("could not create file " + outputFile, e);
return;
}
GregorianCalendar sinceWhen = BLETypeConversions.createCalendar();
sinceWhen.add(Calendar.DAY_OF_MONTH, -10);
startFetching(builder, AmazfitBipService.COMMAND_ACTIVITY_DATA_TYPE_DEBUGLOGS, sinceWhen);
final GregorianCalendar sinceWhen = getLastSuccessfulSyncTime();
startFetching(builder, HuamiFetchDataType.DEBUG_LOGS.getCode(), sinceWhen);
}
@Override
protected String getLastSyncTimeKey() {
return null;
return "lastDebugTimeMillis";
}
@Override
protected boolean handleActivityFetchFinish(boolean success) {
protected boolean processBufferedData() {
LOG.info("{} data has finished", getName());
try {
logOutputStream.close();
logOutputStream = null;
} catch (IOException e) {
LOG.warn("could not close output stream", e);
} catch (final IOException e) {
LOG.error("could not close output stream", e);
return false;
}
return super.handleActivityFetchFinish(success);
return true;
}
@Override
@ -106,30 +98,12 @@ public class HuamiFetchDebugLogsOperation extends AbstractFetchOperation {
return true;
}
@Override
protected void handleActivityNotif(byte[] value) {
if (!isOperationRunning()) {
LOG.error("ignoring notification because operation is not running. Data length: " + value.length);
getSupport().logMessageContent(value);
return;
}
if ((byte) (lastPacketCounter + 1) == value[0]) {
lastPacketCounter++;
bufferActivityData(value);
} else {
GB.toast("Error " + getName() + " invalid package counter: " + value[0], Toast.LENGTH_LONG, GB.ERROR);
handleActivityFetchFinish(false);
}
}
@Override
protected void bufferActivityData(@NonNull byte[] value) {
try {
logOutputStream.write(value, 1, value.length - 1);
} catch (final IOException e) {
LOG.warn("could not write to output stream", e);
handleActivityFetchFinish(false);
LOG.error("could not write to output stream", e);
}
}
}

View File

@ -14,7 +14,7 @@
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.huami.operations;
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch;
import android.widget.Toast;
@ -33,15 +33,12 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiHeartRateManualSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.HuamiHeartRateManualSample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuamiHeartRateManualSample;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
@ -51,7 +48,7 @@ public class FetchHeartRateManualOperation extends AbstractRepeatingFetchOperati
private static final Logger LOG = LoggerFactory.getLogger(FetchHeartRateManualOperation.class);
public FetchHeartRateManualOperation(final HuamiSupport support) {
super(support, HuamiService.COMMAND_ACTIVITY_DATA_TYPE_MANUAL_HEART_RATE, "manual hr data");
super(support, HuamiFetchDataType.MANUAL_HEART_RATE);
}
@Override

View File

@ -14,7 +14,7 @@
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.huami.operations;
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch;
import android.widget.Toast;
@ -32,7 +32,6 @@ import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiHeartRateMaxSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
@ -40,7 +39,6 @@ import nodomain.freeyourgadget.gadgetbridge.entities.HuamiHeartRateMaxSample;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
@ -50,7 +48,7 @@ public class FetchHeartRateMaxOperation extends AbstractRepeatingFetchOperation
private static final Logger LOG = LoggerFactory.getLogger(FetchHeartRateMaxOperation.class);
public FetchHeartRateMaxOperation(final HuamiSupport support) {
super(support, HuamiService.COMMAND_ACTIVITY_DATA_TYPE_MAX_HEART_RATE, "max hr data");
super(support, HuamiFetchDataType.MAX_HEART_RATE);
}
@Override

View File

@ -14,7 +14,7 @@
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.huami.operations;
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch;
import android.widget.Toast;
@ -33,14 +33,12 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiHeartRateRestingSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.HuamiHeartRateRestingSample;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
@ -50,7 +48,7 @@ public class FetchHeartRateRestingOperation extends AbstractRepeatingFetchOperat
private static final Logger LOG = LoggerFactory.getLogger(FetchHeartRateRestingOperation.class);
public FetchHeartRateRestingOperation(final HuamiSupport support) {
super(support, HuamiService.COMMAND_ACTIVITY_DATA_TYPE_RESTING_HEART_RATE, "resting hr data");
super(support, HuamiFetchDataType.RESTING_HEART_RATE);
}
@Override

View File

@ -14,7 +14,7 @@
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.huami.operations;
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch;
import android.widget.Toast;
@ -32,14 +32,12 @@ import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiPaiSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.HuamiPaiSample;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
@ -49,7 +47,7 @@ public class FetchPaiOperation extends AbstractRepeatingFetchOperation {
private static final Logger LOG = LoggerFactory.getLogger(FetchPaiOperation.class);
public FetchPaiOperation(final HuamiSupport support) {
super(support, HuamiService.COMMAND_ACTIVITY_DATA_TYPE_PAI, "pai data");
super(support, HuamiFetchDataType.PAI);
}
@Override

View File

@ -14,7 +14,7 @@
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.huami.operations;
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch;
import android.widget.Toast;
@ -32,14 +32,12 @@ import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiSleepRespiratoryRateSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.HuamiSleepRespiratoryRateSample;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
@ -49,7 +47,7 @@ public class FetchSleepRespiratoryRateOperation extends AbstractRepeatingFetchOp
private static final Logger LOG = LoggerFactory.getLogger(FetchSleepRespiratoryRateOperation.class);
public FetchSleepRespiratoryRateOperation(final HuamiSupport support) {
super(support, HuamiService.COMMAND_ACTIVITY_DATA_TYPE_SLEEP_RESPIRATORY_RATE, "sleep respiratory rate data");
super(support, HuamiFetchDataType.SLEEP_RESPIRATORY_RATE);
}
@Override

View File

@ -14,7 +14,7 @@
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.huami.operations;
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch;
import android.widget.Toast;
@ -32,7 +32,6 @@ import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiSpo2SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
@ -40,7 +39,6 @@ import nodomain.freeyourgadget.gadgetbridge.entities.HuamiSpo2Sample;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
@ -50,7 +48,7 @@ public class FetchSpo2NormalOperation extends AbstractRepeatingFetchOperation {
private static final Logger LOG = LoggerFactory.getLogger(FetchSpo2NormalOperation.class);
public FetchSpo2NormalOperation(final HuamiSupport support) {
super(support, HuamiService.COMMAND_ACTIVITY_DATA_TYPE_SPO2_NORMAL, "spo2 normal data");
super(support, HuamiFetchDataType.SPO2_NORMAL);
}
@Override

View File

@ -14,7 +14,7 @@
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.huami.operations;
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -24,9 +24,7 @@ import java.nio.ByteOrder;
import java.util.GregorianCalendar;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
* An operation that fetches SPO2 data for sleep measurements (this requires sleep breathing quality enabled).
@ -35,7 +33,7 @@ public class FetchSpo2SleepOperation extends AbstractRepeatingFetchOperation {
private static final Logger LOG = LoggerFactory.getLogger(FetchSpo2SleepOperation.class);
public FetchSpo2SleepOperation(final HuamiSupport support) {
super(support, HuamiService.COMMAND_ACTIVITY_DATA_TYPE_SPO2_SLEEP, "spo2 sleep data");
super(support, HuamiFetchDataType.SPO2_SLEEP);
}
@Override

View File

@ -0,0 +1,222 @@
/* Copyright (C) 2018-2024 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, José Rebelo, Oleg Vasilev, Sebastian Krey, Your Name
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.huami.operations.fetch;
import android.text.format.DateUtils;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.GregorianCalendar;
import androidx.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.export.ActivityTrackExporter;
import nodomain.freeyourgadget.gadgetbridge.export.GPXExporter;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuamiActivityDetailsParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiActivityDetailsParser;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
* An operation that fetches activity data. For every fetch, a new operation must
* be created, i.e. an operation may not be reused for multiple fetches.
*/
public class FetchSportsDetailsOperation extends AbstractFetchOperation {
private static final Logger LOG = LoggerFactory.getLogger(FetchSportsDetailsOperation.class);
private final AbstractHuamiActivityDetailsParser detailsParser;
private final BaseActivitySummary summary;
private final String lastSyncTimeKey;
FetchSportsDetailsOperation(@NonNull BaseActivitySummary summary,
@NonNull AbstractHuamiActivityDetailsParser detailsParser,
@NonNull HuamiSupport support,
@NonNull String lastSyncTimeKey,
int fetchCount) {
super(support);
setName("fetching sport details");
this.summary = summary;
this.detailsParser = detailsParser;
this.lastSyncTimeKey = lastSyncTimeKey;
this.fetchCount = fetchCount;
}
@Override
protected String taskDescription() {
return getContext().getString(R.string.busy_task_fetch_sports_details);
}
@Override
protected void startFetching(TransactionBuilder builder) {
LOG.info("start " + getName());
final GregorianCalendar sinceWhen = getLastSuccessfulSyncTime();
startFetching(builder, HuamiFetchDataType.SPORTS_DETAILS.getCode(), sinceWhen);
}
@Override
protected boolean processBufferedData() {
LOG.info("{} has finished round {}", getName(), fetchCount);
if (buffer.size() == 0) {
LOG.warn("Buffer is empty");
return false;
}
if (detailsParser instanceof HuamiActivityDetailsParser) {
((HuamiActivityDetailsParser) detailsParser).setSkipCounterByte(false); // is already stripped
}
try {
final ActivityTrack track = detailsParser.parse(buffer.toByteArray());
final ActivityTrackExporter exporter = createExporter();
final String trackType;
switch (summary.getActivityKind()) {
case ActivityKind.TYPE_CYCLING:
trackType = getContext().getString(R.string.activity_type_biking);
break;
case ActivityKind.TYPE_RUNNING:
trackType = getContext().getString(R.string.activity_type_running);
break;
case ActivityKind.TYPE_WALKING:
trackType = getContext().getString(R.string.activity_type_walking);
break;
case ActivityKind.TYPE_HIKING:
trackType = getContext().getString(R.string.activity_type_hiking);
break;
case ActivityKind.TYPE_CLIMBING:
trackType = getContext().getString(R.string.activity_type_climbing);
break;
case ActivityKind.TYPE_SWIMMING:
trackType = getContext().getString(R.string.activity_type_swimming);
break;
default:
trackType = "track";
break;
}
final String rawBytesPath = saveRawBytes();
final String fileName = FileUtils.makeValidFileName("gadgetbridge-" + trackType.toLowerCase() + "-" + DateTimeUtils.formatIso8601(summary.getStartTime()) + ".gpx");
final File targetFile = new File(FileUtils.getExternalFilesDir(), fileName);
boolean exportGpxSuccess = true;
try {
exporter.performExport(track, targetFile);
} catch (final ActivityTrackExporter.GPXTrackEmptyException ex) {
exportGpxSuccess = false;
}
try (DBHandler dbHandler = GBApplication.acquireDB()) {
if (exportGpxSuccess) {
summary.setGpxTrack(targetFile.getAbsolutePath());
}
if (rawBytesPath != null) {
summary.setRawDetailsPath(rawBytesPath);
}
dbHandler.getDaoSession().getBaseActivitySummaryDao().update(summary);
}
} catch (final Exception e) {
GB.toast(getContext(), "Error saving activity details: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
return false;
}
// Always increment the sync timestamp on success, even if we did not get data
final GregorianCalendar endTime = BLETypeConversions.createCalendar();
endTime.setTime(summary.getEndTime());
saveLastSyncTimestamp(endTime);
if (needsAnotherFetch(endTime)) {
final FetchSportsSummaryOperation nextOperation = new FetchSportsSummaryOperation(getSupport(), fetchCount);
getSupport().getFetchOperationQueue().add(0, nextOperation);
}
return true;
}
private boolean needsAnotherFetch(GregorianCalendar lastSyncTimestamp) {
// We have 2 operations per fetch round: summary + details
if (fetchCount > 10) {
LOG.warn("Already have 5 fetch rounds, not doing another one.");
return false;
}
if (DateUtils.isToday(lastSyncTimestamp.getTimeInMillis())) {
LOG.info("Hopefully no further fetch needed, last synced timestamp is from today.");
return false;
}
if (lastSyncTimestamp.getTimeInMillis() > System.currentTimeMillis()) {
LOG.warn("Not doing another fetch since last synced timestamp is in the future: {}", DateTimeUtils.formatDateTime(lastSyncTimestamp.getTime()));
return false;
}
LOG.info("Doing another fetch since last sync timestamp is still too old: {}", DateTimeUtils.formatDateTime(lastSyncTimestamp.getTime()));
return true;
}
private ActivityTrackExporter createExporter() {
final GPXExporter exporter = new GPXExporter();
exporter.setCreator(GBApplication.app().getNameAndVersion());
return exporter;
}
@Override
protected String getLastSyncTimeKey() {
return lastSyncTimeKey;
}
@Override
protected GregorianCalendar getLastSuccessfulSyncTime() {
final GregorianCalendar calendar = BLETypeConversions.createCalendar();
calendar.setTime(summary.getStartTime());
return calendar;
}
private String saveRawBytes() {
final String fileName = FileUtils.makeValidFileName(String.format("%s.bin", DateTimeUtils.formatIso8601(summary.getStartTime())));
FileOutputStream outputStream = null;
try {
final File targetFolder = new File(FileUtils.getExternalFilesDir(), "rawDetails");
targetFolder.mkdirs();
final File targetFile = new File(targetFolder, fileName);
outputStream = new FileOutputStream(targetFile);
outputStream.write(buffer.toByteArray());
outputStream.close();
return targetFile.getAbsolutePath();
} catch (final IOException e) {
LOG.error("Failed to save raw bytes", e);
}
return null;
}
}

View File

@ -0,0 +1,119 @@
/* Copyright (C) 2018-2024 Andreas Shimokawa, Carsten Pfeiffer, Daniel
Dakhno, Daniele Gobbetti, José Rebelo, Oleg Vasilev, Petr Vaněk
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.huami.operations.fetch;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.GregorianCalendar;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiActivitySummaryParser;
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.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuamiActivityDetailsParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
* An operation that fetches activity data. For every fetch, a new operation must
* be created, i.e. an operation may not be reused for multiple fetches.
*/
public class FetchSportsSummaryOperation extends AbstractFetchOperation {
private static final Logger LOG = LoggerFactory.getLogger(FetchSportsSummaryOperation.class);
public FetchSportsSummaryOperation(HuamiSupport support, int fetchCount) {
super(support);
setName("fetching sport summaries");
this.fetchCount = fetchCount;
}
@Override
protected String taskDescription() {
return getContext().getString(R.string.busy_task_fetch_sports_summaries);
}
@Override
protected void startFetching(TransactionBuilder builder) {
LOG.info("start" + getName());
final GregorianCalendar sinceWhen = getLastSuccessfulSyncTime();
startFetching(builder, HuamiFetchDataType.SPORTS_SUMMARIES.getCode(), sinceWhen);
}
@Override
protected boolean processBufferedData() {
LOG.info("{} has finished round {}", getName(), fetchCount);
if (buffer.size() < 2) {
LOG.warn("Buffer size {} too small for activity summary", buffer.size());
return false;
}
final DeviceCoordinator coordinator = getDevice().getDeviceCoordinator();
final ActivitySummaryParser summaryParser = coordinator.getActivitySummaryParser(getDevice());
BaseActivitySummary summary = new BaseActivitySummary();
summary.setStartTime(getLastStartTimestamp().getTime()); // due to a bug this has to be set
summary.setRawSummaryData(buffer.toByteArray());
try {
summary = summaryParser.parseBinaryData(summary);
} catch (final Exception e) {
GB.toast(getContext(), "Failed to parse activity summary", Toast.LENGTH_LONG, GB.ERROR, e);
return false;
}
if (summary == null) {
return false;
}
summary.setSummaryData(null); // remove json before saving to database,
try (DBHandler dbHandler = GBApplication.acquireDB()) {
final DaoSession session = dbHandler.getDaoSession();
final Device device = DBHelper.getDevice(getDevice(), session);
final User user = DBHelper.getUser(session);
summary.setDevice(device);
summary.setUser(user);
summary.setRawSummaryData(buffer.toByteArray());
session.getBaseActivitySummaryDao().insertOrReplace(summary);
} catch (final Exception ex) {
GB.toast(getContext(), "Error saving activity summary", Toast.LENGTH_LONG, GB.ERROR, ex);
return false;
}
final AbstractHuamiActivityDetailsParser detailsParser = ((HuamiActivitySummaryParser) summaryParser).getDetailsParser(summary);
final FetchSportsDetailsOperation nextOperation = new FetchSportsDetailsOperation(summary, detailsParser, getSupport(), getLastSyncTimeKey(), fetchCount);
getSupport().getFetchOperationQueue().add(0, nextOperation);
return true;
}
@Override
protected String getLastSyncTimeKey() {
return "lastSportsActivityTimeMillis";
}
}

View File

@ -14,18 +14,15 @@
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.huami.operations;
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
/**
@ -36,7 +33,7 @@ public class FetchStatisticsOperation extends AbstractRepeatingFetchOperation {
private static final Logger LOG = LoggerFactory.getLogger(FetchStatisticsOperation.class);
public FetchStatisticsOperation(final HuamiSupport support) {
super(support, HuamiService.COMMAND_ACTIVITY_DATA_TYPE_STATISTICS, "statistics data");
super(support, HuamiFetchDataType.STATISTICS);
}
@Override
@ -53,14 +50,6 @@ public class FetchStatisticsOperation extends AbstractRepeatingFetchOperation {
return true;
}
@Override
protected GregorianCalendar getLastSuccessfulSyncTime() {
// One minute ago - we don't really care about this data, and we don't want to fetch it all
final GregorianCalendar sinceWhen = BLETypeConversions.createCalendar();
sinceWhen.add(Calendar.MINUTE, -1);
return sinceWhen;
}
@Override
protected String getLastSyncTimeKey() {
return "lastStatisticsTimeMillis";

View File

@ -14,7 +14,7 @@
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.huami.operations;
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch;
import android.widget.Toast;
@ -31,7 +31,6 @@ import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiStressSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
@ -39,7 +38,6 @@ import nodomain.freeyourgadget.gadgetbridge.entities.HuamiStressSample;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
@ -49,7 +47,7 @@ public class FetchStressAutoOperation extends AbstractRepeatingFetchOperation {
private static final Logger LOG = LoggerFactory.getLogger(FetchStressAutoOperation.class);
public FetchStressAutoOperation(final HuamiSupport support) {
super(support, HuamiService.COMMAND_ACTIVITY_DATA_TYPE_STRESS_AUTOMATIC, "auto stress data");
super(support, HuamiFetchDataType.STRESS_AUTOMATIC);
}
@Override

View File

@ -14,7 +14,7 @@
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.huami.operations;
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch;
import android.widget.Toast;
@ -32,7 +32,6 @@ import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiStressSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
@ -41,7 +40,6 @@ import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
@ -51,7 +49,7 @@ public class FetchStressManualOperation extends AbstractRepeatingFetchOperation
private static final Logger LOG = LoggerFactory.getLogger(FetchStressManualOperation.class);
public FetchStressManualOperation(final HuamiSupport support) {
super(support, HuamiService.COMMAND_ACTIVITY_DATA_TYPE_STRESS_MANUAL, "manual stress data");
super(support, HuamiFetchDataType.STRESS_MANUAL);
}
@Override

View File

@ -14,7 +14,7 @@
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.huami.operations;
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.fetch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -24,7 +24,6 @@ import java.nio.ByteOrder;
import java.util.GregorianCalendar;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@ -35,7 +34,7 @@ public class FetchTemperatureOperation extends AbstractRepeatingFetchOperation {
private static final Logger LOG = LoggerFactory.getLogger(FetchTemperatureOperation.class);
public FetchTemperatureOperation(final HuamiSupport support) {
super(support, HuamiService.COMMAND_ACTIVITY_DATA_TYPE_TEMPERATURE, "body temperature data");
super(support, HuamiFetchDataType.TEMPERATURE);
}
@Override

View File

@ -0,0 +1,46 @@
/* 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.huami.operations.fetch;
public enum HuamiFetchDataType {
ACTIVITY(0x01),
MANUAL_HEART_RATE(0x02),
SPORTS_SUMMARIES(0x05),
SPORTS_DETAILS(0x06),
DEBUG_LOGS(0x07),
PAI(0x0d),
STRESS_MANUAL(0x12),
STRESS_AUTOMATIC(0x13),
SPO2_NORMAL(0x25),
SPO2_SLEEP(0x26),
STATISTICS(0x2c),
TEMPERATURE(0x2e),
SLEEP_RESPIRATORY_RATE(0x38),
RESTING_HEART_RATE(0x3a),
MAX_HEART_RATE(0x3d),
;
private final byte code;
HuamiFetchDataType(final int code) {
this.code = (byte) code;
}
public byte getCode() {
return code;
}
}

View File

@ -15,7 +15,7 @@
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.huami.operations;
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.init;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothGatt;

View File

@ -14,7 +14,7 @@
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.huami.operations;
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.init;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService.RESPONSE;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService.SUCCESS;

View File

@ -14,7 +14,7 @@
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.huami.operations;
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;

View File

@ -14,7 +14,7 @@
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.huami.operations;
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update;
import android.net.Uri;
import android.widget.Toast;

View File

@ -14,7 +14,7 @@
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.huami.operations;
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update;
import android.net.Uri;
@ -29,7 +29,6 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceBusyAction;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuamiFirmwareInfo;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiFirmwareInfo;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiFirmwareType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;

View File

@ -25,8 +25,8 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppe.ZeppEFWHelper;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitgtr.AmazfitGTRSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation2020;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperation2020;
public class ZeppESupport extends AmazfitGTRSupport {

View File

@ -112,7 +112,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiDeviceEve
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiPhoneGpsStatus;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiVibrationPatternNotificationType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations.ZeppOsFirmwareUpdateOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations.ZeppOsAgpsUpdateOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations.ZeppOsGpxRouteUploadOperation;

View File

@ -40,7 +40,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceBusyAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetProgressAction;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiFirmwareType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation2020;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.update.UpdateFirmwareOperation2020;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.AbstractMiBandOperation;
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;