From d161415046df399a293b1c51bd42dfc1269613f8 Mon Sep 17 00:00:00 2001 From: Yukai Li Date: Mon, 5 Oct 2020 03:13:22 -0600 Subject: [PATCH] Lefun: Implement activity data sampling --- .../gadgetbridge/daogen/GBDaoGenerator.java | 47 ++++- .../devices/lefun/LefunConstants.java | 20 ++ .../devices/lefun/LefunDeviceCoordinator.java | 2 +- .../devices/lefun/LefunSampleProvider.java | 101 +++++++++ .../devices/lefun/LefunDeviceSupport.java | 197 +++++++++++++++++- .../requests/GetActivityDataRequest.java | 87 ++++++++ .../lefun/requests/GetPpgDataRequest.java | 87 ++++++++ .../lefun/requests/GetSleepDataRequest.java | 87 ++++++++ .../lefun/requests/MultiFetchRequest.java | 101 +++++++++ 9 files changed, 725 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/LefunSampleProvider.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetActivityDataRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetPpgDataRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetSleepDataRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/MultiFetchRequest.java diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index ff9fe81a6..1b7c08465 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -43,7 +43,7 @@ public class GBDaoGenerator { public static void main(String[] args) throws Exception { - Schema schema = new Schema(30, MAIN_PACKAGE + ".entities"); + Schema schema = new Schema(31, MAIN_PACKAGE + ".entities"); Entity userAttributes = addUserAttributes(schema); Entity user = addUserInfo(schema, userAttributes); @@ -74,6 +74,9 @@ public class GBDaoGenerator { addWatchXPlusHealthActivitySample(schema, user, device); addWatchXPlusHealthActivityKindOverlay(schema, user, device); addTLW64ActivitySample(schema, user, device); + addLefunActivitySample(schema, user, device); + addLefunBiometricSample(schema,user,device); + addLefunSleepSample(schema, user, device); addHybridHRActivitySample(schema, user, device); addCalendarSyncState(schema, device); @@ -404,6 +407,48 @@ public class GBDaoGenerator { return activitySample; } + private static Entity addLefunActivitySample(Schema schema, Entity user, Entity device) { + Entity activitySample = addEntity(schema, "LefunActivitySample"); + activitySample.implementsSerializable(); + addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device); + activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty("distance").notNull(); + activitySample.addIntProperty("calories").notNull(); + addHeartRateProperties(activitySample); + return activitySample; + } + + private static Entity addLefunBiometricSample(Schema schema, Entity user, Entity device) { + Entity biometricSample = addEntity(schema, "LefunBiometricSample"); + biometricSample.implementsSerializable(); + + biometricSample.addIntProperty("timestamp").notNull().primaryKey(); + Property deviceId = biometricSample.addLongProperty("deviceId").primaryKey().notNull().getProperty(); + biometricSample.addToOne(device, deviceId); + Property userId = biometricSample.addLongProperty("userId").notNull().getProperty(); + biometricSample.addToOne(user, userId); + + biometricSample.addIntProperty("type").notNull(); + biometricSample.addIntProperty("value1").notNull(); + biometricSample.addIntProperty("value2"); + return biometricSample; + } + + private static Entity addLefunSleepSample(Schema schema, Entity user, Entity device) { + Entity sleepSample = addEntity(schema, "LefunSleepSample"); + sleepSample.implementsSerializable(); + + sleepSample.addIntProperty("timestamp").notNull().primaryKey(); + Property deviceId = sleepSample.addLongProperty("deviceId").primaryKey().notNull().getProperty(); + sleepSample.addToOne(device, deviceId); + Property userId = sleepSample.addLongProperty("userId").notNull().getProperty(); + sleepSample.addToOne(user, userId); + + sleepSample.addIntProperty("type").notNull(); + return sleepSample; + } + private static void addCommonActivitySampleProperties(String superClass, Entity activitySample, Entity user, Entity device) { activitySample.setSuperclass(superClass); activitySample.addImport(MAIN_PACKAGE + ".devices.SampleProvider"); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/LefunConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/LefunConstants.java index a4b972634..0f98fa608 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/LefunConstants.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/LefunConstants.java @@ -73,4 +73,24 @@ public class LefunConstants { public static final int PPG_TYPE_HEART_RATE = 0; public static final int PPG_TYPE_BLOOD_PRESSURE = 1; public static final int PPG_TYPE_BLOOD_OXYGEN = 2; + public static final int PPG_TYPE_COUNT = 3; + + // Extended DB types + public static final int DB_SAMPLE_TYPE_PPG = 1; + public static final int DB_SAMPLE_TYPE_SLEEP_DAY = 2; + public static final int DB_SAMPLE_TYPE_SLEEP_PERIODS = 3; + + // DB activity kinds + public static final int DB_ACTIVITY_KIND_UNKNOWN = 0; + public static final int DB_ACTIVITY_KIND_ACTIVITY = 1; + public static final int DB_ACTIVITY_KIND_HEART_RATE = 2; + public static final int DB_ACTIVITY_KIND_LIGHT_SLEEP = 3; + public static final int DB_ACTIVITY_KIND_DEEP_SLEEP = 4; + + // Pseudo-intensity + public static final int INTENSITY_MIN = 0; + public static final int INTENSITY_DEEP_SLEEP = 1; + public static final int INTENSITY_LIGHT_SLEEP = 2; + public static final int INTENSITY_AWAKE = 3; + public static final int INTENSITY_MAX = 4; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/LefunDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/LefunDeviceCoordinator.java index fe9aee215..9e501b765 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/LefunDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/LefunDeviceCoordinator.java @@ -92,7 +92,7 @@ public class LefunDeviceCoordinator extends AbstractDeviceCoordinator { @Override public SampleProvider getSampleProvider(GBDevice device, DaoSession session) { - return null; + return new LefunSampleProvider(device, session); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/LefunSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/LefunSampleProvider.java new file mode 100644 index 000000000..861cd089d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/LefunSampleProvider.java @@ -0,0 +1,101 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import de.greenrobot.dao.AbstractDao; +import de.greenrobot.dao.Property; +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.LefunActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.LefunActivitySampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; + +public class LefunSampleProvider extends AbstractSampleProvider { + public LefunSampleProvider(GBDevice device, DaoSession session) { + super(device, session); + } + + @Override + public AbstractDao getSampleDao() { + return getSession().getLefunActivitySampleDao(); + } + + @Nullable + @Override + protected Property getRawKindSampleProperty() { + return LefunActivitySampleDao.Properties.RawKind; + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return LefunActivitySampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return LefunActivitySampleDao.Properties.DeviceId; + } + + @Override + public int normalizeType(int rawType) { + switch (rawType) { + case LefunConstants.DB_ACTIVITY_KIND_ACTIVITY: + case LefunConstants.DB_ACTIVITY_KIND_HEART_RATE: + return ActivityKind.TYPE_ACTIVITY; + case LefunConstants.DB_ACTIVITY_KIND_LIGHT_SLEEP: + return ActivityKind.TYPE_LIGHT_SLEEP; + case LefunConstants.DB_ACTIVITY_KIND_DEEP_SLEEP: + return ActivityKind.TYPE_DEEP_SLEEP; + default: + return ActivityKind.TYPE_UNKNOWN; + } + } + + @Override + public int toRawActivityKind(int activityKind) { + switch (activityKind) { + case ActivityKind.TYPE_ACTIVITY: + return LefunConstants.DB_ACTIVITY_KIND_ACTIVITY; + case ActivityKind.TYPE_LIGHT_SLEEP: + return LefunConstants.DB_ACTIVITY_KIND_LIGHT_SLEEP; + case ActivityKind.TYPE_DEEP_SLEEP: + return LefunConstants.DB_ACTIVITY_KIND_DEEP_SLEEP; + default: + return LefunConstants.DB_ACTIVITY_KIND_UNKNOWN; + } + } + + @Override + public float normalizeIntensity(int rawIntensity) { + return rawIntensity / (float)LefunConstants.INTENSITY_MAX; + } + + @Override + public LefunActivitySample createActivitySample() { + return new LefunActivitySample(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/LefunDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/LefunDeviceSupport.java index f5ed6ee54..80753ecce 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/LefunDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/LefunDeviceSupport.java @@ -28,11 +28,27 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.ArrayList; +import java.util.Calendar; import java.util.Collections; import java.util.List; +import java.util.Queue; import java.util.UUID; +import java.util.concurrent.ConcurrentLinkedQueue; +import de.greenrobot.dao.query.Query; +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.GetActivityDataCommand; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.GetPpgDataCommand; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.GetSleepDataCommand; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.LefunActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.LefunActivitySampleDao; +import nodomain.freeyourgadget.gadgetbridge.entities.LefunBiometricSample; +import nodomain.freeyourgadget.gadgetbridge.entities.LefunBiometricSampleDao; +import nodomain.freeyourgadget.gadgetbridge.entities.LefunSleepSample; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.Alarm; import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; @@ -41,14 +57,18 @@ import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; +import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.FindDeviceRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.GetActivityDataRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.GetBatteryLevelRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.GetFirmwareInfoRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.GetPpgDataRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.GetSleepDataRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.Request; import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.SetTimeRequest; import nodomain.freeyourgadget.gadgetbridge.util.GB; @@ -57,6 +77,7 @@ public class LefunDeviceSupport extends AbstractBTLEDeviceSupport { private static final Logger LOG = LoggerFactory.getLogger(LefunDeviceSupport.class); private final List inProgressRequests = Collections.synchronizedList(new ArrayList()); + private final Queue queuedRequests = new ConcurrentLinkedQueue<>(); public LefunDeviceSupport() { super(LOG); @@ -113,7 +134,7 @@ public class LefunDeviceSupport extends AbstractBTLEDeviceSupport { @Override public void onSetTime() { try { - TransactionBuilder builder = createTransactionBuilder(SetTimeRequest.class.getSimpleName()); + TransactionBuilder builder = performInitialized(SetTimeRequest.class.getSimpleName()); SetTimeRequest request = new SetTimeRequest(this, builder); request.perform(); inProgressRequests.add(request); @@ -186,7 +207,27 @@ public class LefunDeviceSupport extends AbstractBTLEDeviceSupport { @Override public void onFetchRecordedData(int dataTypes) { + if ((dataTypes & RecordedDataTypes.TYPE_ACTIVITY) != 0) { + for (int i = 0; i < 7; ++i) { + GetActivityDataRequest req = new GetActivityDataRequest(this); + req.setDaysAgo(i); + queuedRequests.add(req); + } + for (int i = 0; i < LefunConstants.PPG_TYPE_COUNT; ++i) { + GetPpgDataRequest req = new GetPpgDataRequest(this); + req.setPpgType(i); + queuedRequests.add(req); + } + + for (int i = 0; i < 7; ++i) { + GetSleepDataRequest req = new GetSleepDataRequest(this); + req.setDaysAgo(i); + queuedRequests.add(req); + } + + runNextQueuedRequest(); + } } @Override @@ -208,7 +249,7 @@ public class LefunDeviceSupport extends AbstractBTLEDeviceSupport { public void onFindDevice(boolean start) { if (start) { try { - TransactionBuilder builder = createTransactionBuilder(FindDeviceRequest.class.getSimpleName()); + TransactionBuilder builder = performInitialized(FindDeviceRequest.class.getSimpleName()); FindDeviceRequest request = new FindDeviceRequest(this, builder); request.perform(); inProgressRequests.add(request); @@ -296,10 +337,12 @@ public class LefunDeviceSupport extends AbstractBTLEDeviceSupport { if (handleAsynchronousResponse(data)) return true; + logMessageContent(data); LOG.error(String.format("No handler for response 0x%02x", commandId)); return false; } + logMessageContent(data); LOG.error("Invalid response received"); return false; } @@ -316,4 +359,154 @@ public class LefunDeviceSupport extends AbstractBTLEDeviceSupport { gbDevice.setState(GBDevice.State.INITIALIZED); gbDevice.sendDeviceUpdateIntent(getContext()); } + + private int dateToTimestamp(byte year, byte month, byte day, byte hour, byte minute, byte second) { + Calendar calendar = Calendar.getInstance(); + calendar.set( + ((int) year & 0xff) + 2000, + ((int) month & 0xff) - 1, + (int) day, + (int) hour, + (int) minute, + (int) second + ); + return (int) (calendar.getTimeInMillis() / 1000); + } + + private LefunActivitySample getActivitySample(DaoSession session, int timestamp) { + LefunActivitySampleDao dao = session.getLefunActivitySampleDao(); + Long userId = DBHelper.getUser(session).getId(); + Long deviceId = DBHelper.getDevice(getDevice(), session).getId(); + Query q = dao.queryBuilder() + .where(LefunActivitySampleDao.Properties.Timestamp.eq(timestamp)) + .where(LefunActivitySampleDao.Properties.DeviceId.eq(deviceId)) + .where(LefunActivitySampleDao.Properties.UserId.eq(userId)) + .build(); + return q.unique(); + } + + public void handleActivityData(GetActivityDataCommand command) { + try (DBHandler handler = GBApplication.acquireDB()) { + DaoSession session = handler.getDaoSession(); + int timestamp = dateToTimestamp(command.getYear(), command.getMonth(), command.getDay(), + command.getHour(), command.getMinute(), (byte) 0); + // For the most part I'm ignoring the sample provider, because it doesn't really help + // when I need to combine sample data instead of replacing + LefunActivitySample sample = getActivitySample(session, timestamp); + if (sample == null) { + sample = new LefunActivitySample(timestamp, + DBHelper.getDevice(getDevice(), session).getId()); + sample.setUserId(DBHelper.getUser(session).getId()); + sample.setRawKind(LefunConstants.DB_ACTIVITY_KIND_ACTIVITY); + } + + sample.setSteps(command.getSteps()); + sample.setDistance(command.getDistance()); + sample.setCalories(command.getCalories()); + sample.setRawIntensity(LefunConstants.INTENSITY_AWAKE); + + session.getLefunActivitySampleDao().insertOrReplace(sample); + } catch (Exception e) { + LOG.error("Error handling activity data", e); + } + } + + public void handlePpgData(GetPpgDataCommand command) { + byte[] ppgData = command.getPpgData(); + int ppgData0 = ppgData[0] & 0xff; + int ppgData1 = ppgData.length > 1 ? ppgData[1] & 0xff : 0; + + try (DBHandler handler = GBApplication.acquireDB()) { + DaoSession session = handler.getDaoSession(); + int timestamp = dateToTimestamp(command.getYear(), command.getMonth(), command.getDay(), + command.getHour(), command.getMinute(), command.getSecond()); + + if (command.getPpgType() == LefunConstants.PPG_TYPE_HEART_RATE) { + LefunActivitySample sample = getActivitySample(session, timestamp); + if (sample == null) { + sample = new LefunActivitySample(timestamp, + DBHelper.getDevice(getDevice(), session).getId()); + sample.setUserId(DBHelper.getUser(session).getId()); + sample.setRawKind(LefunConstants.DB_ACTIVITY_KIND_HEART_RATE); + } + + sample.setHeartRate(ppgData0); + + session.getLefunActivitySampleDao().insertOrReplace(sample); + } + + LefunBiometricSample bioSample = new LefunBiometricSample(timestamp, + DBHelper.getDevice(getDevice(), session).getId()); + bioSample.setUserId(DBHelper.getUser(session).getId()); + bioSample.setType(command.getPpgType()); + bioSample.setValue1(ppgData0); + bioSample.setValue2(ppgData1); + session.getLefunBiometricSampleDao().insertOrReplace(bioSample); + } catch (Exception e) { + LOG.error("Error handling PPG data", e); + } + } + + public void handleSleepData(GetSleepDataCommand command) { + try (DBHandler handler = GBApplication.acquireDB()) { + DaoSession session = handler.getDaoSession(); + int timestamp = dateToTimestamp(command.getYear(), command.getMonth(), command.getDay(), + command.getHour(), command.getMinute(), (byte) 0); + + LefunActivitySample sample = getActivitySample(session, timestamp); + if (sample == null) { + sample = new LefunActivitySample(timestamp, + DBHelper.getDevice(getDevice(), session).getId()); + sample.setUserId(DBHelper.getUser(session).getId()); + } + + int rawKind; + int intensity; + switch (command.getSleepType()) { + case GetSleepDataCommand.SLEEP_TYPE_AWAKE: + rawKind = LefunConstants.DB_ACTIVITY_KIND_ACTIVITY; + intensity = LefunConstants.INTENSITY_AWAKE; + break; + case GetSleepDataCommand.SLEEP_TYPE_LIGHT_SLEEP: + rawKind = LefunConstants.DB_ACTIVITY_KIND_LIGHT_SLEEP; + intensity = LefunConstants.INTENSITY_LIGHT_SLEEP; + break; + case GetSleepDataCommand.SLEEP_TYPE_DEEP_SLEEP: + rawKind = LefunConstants.DB_ACTIVITY_KIND_DEEP_SLEEP; + intensity = LefunConstants.INTENSITY_DEEP_SLEEP; + break; + default: + rawKind = LefunConstants.DB_ACTIVITY_KIND_UNKNOWN; + intensity = LefunConstants.INTENSITY_AWAKE; + break; + } + + sample.setRawKind(rawKind); + sample.setRawIntensity(intensity); + + session.getLefunActivitySampleDao().insertOrReplace(sample); + + LefunSleepSample sleepSample = new LefunSleepSample(timestamp, + DBHelper.getDevice(getDevice(), session).getId()); + sleepSample.setUserId(DBHelper.getUser(session).getId()); + sleepSample.setType(command.getSleepType()); + session.getLefunSleepSampleDao().insertOrReplace(sleepSample); + } catch (Exception e) { + LOG.error("Error handling sleep data", e); + } + } + + public void runNextQueuedRequest() { + Request request = queuedRequests.poll(); + if (request != null) { + try { + request.perform(); + if (!request.isSelfQueue()) + performConnected(request.getTransactionBuilder().getTransaction()); + } catch (IOException e) { + GB.toast(getContext(), "Failed to run next queued request", Toast.LENGTH_SHORT, + GB.ERROR, e); + } + } + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetActivityDataRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetActivityDataRequest.java new file mode 100644 index 000000000..2c1adb8c8 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetActivityDataRequest.java @@ -0,0 +1,87 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.GetActivityDataCommand; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; + +public class GetActivityDataRequest extends MultiFetchRequest { + private int daysAgo; + + public GetActivityDataRequest(LefunDeviceSupport support) { + super(support); + } + + public int getDaysAgo() { + return daysAgo; + } + + public void setDaysAgo(int daysAgo) { + this.daysAgo = daysAgo; + } + + @Override + public byte[] createRequest() { + GetActivityDataCommand cmd = new GetActivityDataCommand(); + cmd.setDaysAgo((byte) daysAgo); + return cmd.serialize(); + } + + @Override + public void handleResponse(byte[] data) { + GetActivityDataCommand cmd = new GetActivityDataCommand(); + cmd.deserialize(data); + + if (daysAgo != (cmd.getDaysAgo() & 0xff)) { + throw new IllegalArgumentException("Mismatching days ago"); + } + + if (totalRecords == -1) { + totalRecords = cmd.getTotalRecords() & 0xff; + } else if (totalRecords != (cmd.getTotalRecords() & 0xff)) { + throw new IllegalArgumentException("Total records mismatch"); + } + + if (totalRecords != 0) { + int currentRecord = cmd.getCurrentRecord() & 0xff; + if (lastRecord + 1 != currentRecord) { + throw new IllegalArgumentException("Records received out of sequence"); + } + lastRecord = currentRecord; + + getSupport().handleActivityData(cmd); + } else { + lastRecord = totalRecords; + } + + if (lastRecord == totalRecords) + operationFinished(); + } + + @Override + public int getCommandId() { + return LefunConstants.CMD_ACTIVITY_DATA; + } + + @Override + protected String getOperationName() { + return "Getting activity data"; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetPpgDataRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetPpgDataRequest.java new file mode 100644 index 000000000..eb7c4a9f0 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetPpgDataRequest.java @@ -0,0 +1,87 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.GetPpgDataCommand; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; + +public class GetPpgDataRequest extends MultiFetchRequest { + private int ppgType; + + public GetPpgDataRequest(LefunDeviceSupport support) { + super(support); + } + + public int getPpgType() { + return ppgType; + } + + public void setPpgType(int ppgType) { + this.ppgType = ppgType; + } + + @Override + public byte[] createRequest() { + GetPpgDataCommand cmd = new GetPpgDataCommand(); + cmd.setPpgType(ppgType); + return cmd.serialize(); + } + + @Override + public void handleResponse(byte[] data) { + GetPpgDataCommand cmd = new GetPpgDataCommand(); + cmd.deserialize(data); + + if (cmd.getPpgType() != ppgType) { + throw new IllegalArgumentException("Mismatching PPG type"); + } + + if (totalRecords == -1) { + totalRecords = cmd.getTotalRecords() & 0xffff; + } else if (totalRecords != (cmd.getTotalRecords() & 0xffff)) { + throw new IllegalArgumentException("Total records mismatch"); + } + + if (totalRecords != 0) { + int currentRecord = cmd.getCurrentRecord() & 0xffff; + if (lastRecord + 1 != currentRecord) { + throw new IllegalArgumentException("Records received out of sequence"); + } + lastRecord = currentRecord; + + getSupport().handlePpgData(cmd); + } else { + lastRecord = totalRecords; + } + + if (lastRecord == totalRecords) + operationFinished(); + } + + @Override + public int getCommandId() { + return LefunConstants.CMD_PPG_DATA; + } + + @Override + protected String getOperationName() { + return "Getting PPG data"; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetSleepDataRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetSleepDataRequest.java new file mode 100644 index 000000000..ab53855a3 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetSleepDataRequest.java @@ -0,0 +1,87 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.GetSleepDataCommand; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; + +public class GetSleepDataRequest extends MultiFetchRequest { + private int daysAgo; + + public GetSleepDataRequest(LefunDeviceSupport support) { + super(support); + } + + public int getDaysAgo() { + return daysAgo; + } + + public void setDaysAgo(int daysAgo) { + this.daysAgo = daysAgo; + } + + @Override + public byte[] createRequest() { + GetSleepDataCommand cmd = new GetSleepDataCommand(); + cmd.setDaysAgo((byte) daysAgo); + return cmd.serialize(); + } + + @Override + public void handleResponse(byte[] data) { + GetSleepDataCommand cmd = new GetSleepDataCommand(); + cmd.deserialize(data); + + if (daysAgo != (cmd.getDaysAgo() & 0xff)) { + throw new IllegalArgumentException("Mismatching days ago"); + } + + if (totalRecords == -1) { + totalRecords = cmd.getTotalRecords() & 0xff; + } else if (totalRecords != (cmd.getTotalRecords() & 0xff)) { + throw new IllegalArgumentException("Total records mismatch"); + } + + if (totalRecords != 0) { + int currentRecord = cmd.getCurrentRecord() & 0xff; + if (lastRecord + 1 != currentRecord) { + throw new IllegalArgumentException("Records received out of sequence"); + } + lastRecord = currentRecord; + + getSupport().handleSleepData(cmd); + } else { + lastRecord = totalRecords; + } + + if (lastRecord == totalRecords) + operationFinished(); + } + + @Override + public int getCommandId() { + return LefunConstants.CMD_SLEEP_DATA; + } + + @Override + protected String getOperationName() { + return "Getting sleep data"; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/MultiFetchRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/MultiFetchRequest.java new file mode 100644 index 000000000..031d8f6bd --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/MultiFetchRequest.java @@ -0,0 +1,101 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.widget.Toast; + +import java.io.IOException; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceBusyAction; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public abstract class MultiFetchRequest extends Request { + protected MultiFetchRequest(LefunDeviceSupport support) { + super(support, null); + removeAfterHandling = false; + } + + protected int lastRecord = 0; + protected int totalRecords = -1; + + @Override + protected void prePerform() throws IOException { + super.prePerform(); + builder = performInitialized(getClass().getSimpleName()); + if (getDevice().isBusy()) { + throw new IllegalStateException("Device is busy"); + } + builder.add(new SetDeviceBusyAction(getDevice(), getOperationName(), getContext())); + builder.wait(1000); // Wait a bit (after previous operation), or device sometimes won't respond + } + + @Override + protected void operationFinished() { + if (lastRecord == totalRecords) + removeAfterHandling = true; + try { + super.operationFinished(); + TransactionBuilder builder = performInitialized("Finishing operation"); + builder.setGattCallback(null); + builder.queue(getQueue()); + } catch (IOException e) { + GB.toast(getContext(), "Failed to reset callback", Toast.LENGTH_SHORT, + GB.ERROR, e); + } + unsetBusy(); + operationStatus = OperationStatus.FINISHED; + getSupport().runNextQueuedRequest(); + } + + @Override + public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + if (characteristic.getUuid().equals(LefunConstants.UUID_CHARACTERISTIC_LEFUN_NOTIFY)) { + byte[] data = characteristic.getValue(); + // Parse response + if (data.length >= LefunConstants.CMD_HEADER_LENGTH && data[0] == LefunConstants.CMD_RESPONSE_ID) { + try { + handleResponse(data); + return true; + } catch (IllegalArgumentException e) { + log("Failed to handle response"); + operationFinished(); + } + } + + getSupport().logMessageContent(data); + log("Invalid response received"); + return false; + } + + return super.onCharacteristicChanged(gatt, characteristic); + } + + @Override + public boolean isSelfQueue() { + return true; + } + + protected abstract String getOperationName(); +}