Lefun: Implement activity data sampling

This commit is contained in:
Yukai Li 2020-10-05 03:13:22 -06:00 committed by Gitea
parent 6c32a3a99d
commit d161415046
9 changed files with 725 additions and 4 deletions

View File

@ -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");

View File

@ -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;
}

View File

@ -92,7 +92,7 @@ public class LefunDeviceCoordinator extends AbstractDeviceCoordinator {
@Override
public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
return null;
return new LefunSampleProvider(device, session);
}
@Override

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<LefunActivitySample> {
public LefunSampleProvider(GBDevice device, DaoSession session) {
super(device, session);
}
@Override
public AbstractDao<LefunActivitySample, ?> 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();
}
}

View File

@ -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<Request> inProgressRequests = Collections.synchronizedList(new ArrayList<Request>());
private final Queue<Request> 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<LefunActivitySample> 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);
}
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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";
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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";
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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";
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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();
}