From 943728c35c8c3ed144f03e5785f9d75d285899e5 Mon Sep 17 00:00:00 2001 From: 115ek Date: Wed, 1 Jul 2020 18:54:31 +0200 Subject: [PATCH] TLW64: Support fetching activity data (steps + sleep) --- .../gadgetbridge/daogen/GBDaoGenerator.java | 13 +- .../devices/tlw64/TLW64Constants.java | 2 + .../devices/tlw64/TLW64Coordinator.java | 6 +- .../devices/tlw64/TLW64SampleProvider.java | 68 +++++++++++ .../service/devices/tlw64/TLW64Support.java | 111 +++++++++++++++++- 5 files changed, 195 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/tlw64/TLW64SampleProvider.java diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index 1ad1c75a4..b65a26472 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(27, MAIN_PACKAGE + ".entities"); + Schema schema = new Schema(28, MAIN_PACKAGE + ".entities"); Entity userAttributes = addUserAttributes(schema); Entity user = addUserInfo(schema, userAttributes); @@ -73,6 +73,7 @@ public class GBDaoGenerator { addJYouActivitySample(schema, user, device); addWatchXPlusHealthActivitySample(schema, user, device); addWatchXPlusHealthActivityKindOverlay(schema, user, device); + addTLW64ActivitySample(schema, user, device); addHybridHRActivitySample(schema, user, device); addCalendarSyncState(schema, device); @@ -393,6 +394,16 @@ public class GBDaoGenerator { return activityOverlay; } + private static Entity addTLW64ActivitySample(Schema schema, Entity user, Entity device) { + Entity activitySample = addEntity(schema, "TLW64ActivitySample"); + activitySample.implementsSerializable(); + addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device); + activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE); + return activitySample; + } + 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/tlw64/TLW64Constants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/tlw64/TLW64Constants.java index c4b355565..301a12f09 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/tlw64/TLW64Constants.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/tlw64/TLW64Constants.java @@ -33,6 +33,8 @@ public final class TLW64Constants { public static final byte CMD_USER_DATA = (byte) 0xa9; public static final byte CMD_ALARM = (byte) 0xab; public static final byte CMD_FACTORY_RESET = (byte) 0xad; + public static final byte CMD_FETCH_STEPS = (byte) 0xb2; + public static final byte CMD_FETCH_SLEEP = (byte) 0xb3; public static final byte CMD_NOTIFICATION = (byte) 0xc1; public static final byte CMD_ICON = (byte) 0xc3; public static final byte CMD_DEVICE_SETTINGS = (byte) 0xd3; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/tlw64/TLW64Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/tlw64/TLW64Coordinator.java index 6ce26b085..fb057f51b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/tlw64/TLW64Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/tlw64/TLW64Coordinator.java @@ -66,17 +66,17 @@ public class TLW64Coordinator extends AbstractDeviceCoordinator { @Override public boolean supportsActivityDataFetching() { - return false; + return true; } @Override public boolean supportsActivityTracking() { - return false; + return true; } @Override public SampleProvider getSampleProvider(GBDevice device, DaoSession session) { - return null; + return new TLW64SampleProvider(device, session); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/tlw64/TLW64SampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/tlw64/TLW64SampleProvider.java new file mode 100644 index 000000000..b07a50eaf --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/tlw64/TLW64SampleProvider.java @@ -0,0 +1,68 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.tlw64; + +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.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.TLW64ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.TLW64ActivitySampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class TLW64SampleProvider extends AbstractSampleProvider { + + private GBDevice mDevice; + private DaoSession mSession; + + public TLW64SampleProvider(GBDevice device, DaoSession session) { + super(device, session); + + mSession = session; + mDevice = device; + } + + @Override + public AbstractDao getSampleDao() { + return getSession().getTLW64ActivitySampleDao(); + } + + @Nullable + @Override + protected Property getRawKindSampleProperty() { + return TLW64ActivitySampleDao.Properties.RawKind; + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return TLW64ActivitySampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return TLW64ActivitySampleDao.Properties.DeviceId; + } + + @Override + public int normalizeType(int rawType) { + return rawType; + } + + @Override + public int toRawActivityKind(int activityKind) { + return activityKind; + } + + @Override + public float normalizeIntensity(int rawIntensity) { + return rawIntensity / (float) 4000.0; + } + + @Override + public TLW64ActivitySample createActivitySample() { + return new TLW64ActivitySample(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/tlw64/TLW64Support.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/tlw64/TLW64Support.java index e35ddfc82..9ffae5a7e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/tlw64/TLW64Support.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/tlw64/TLW64Support.java @@ -34,15 +34,21 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.GregorianCalendar; +import java.util.List; import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; import nodomain.freeyourgadget.gadgetbridge.devices.tlw64.TLW64Constants; +import nodomain.freeyourgadget.gadgetbridge.devices.tlw64.TLW64SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.TLW64ActivitySample; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; import nodomain.freeyourgadget.gadgetbridge.model.Alarm; import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; @@ -55,6 +61,7 @@ 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.SetDeviceBusyAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.IntentListener; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfoProfile; @@ -73,6 +80,9 @@ public class TLW64Support extends AbstractBTLEDeviceSupport { private final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo(); public BluetoothGattCharacteristic ctrlCharacteristic = null; public BluetoothGattCharacteristic notifyCharacteristic = null; + private List samples = new ArrayList<>(); + private byte crc = 0; + private int firstTimestamp = 0; private final IntentListener mListener = new IntentListener() { @Override @@ -167,6 +177,10 @@ public class TLW64Support extends AbstractBTLEDeviceSupport { case TLW64Constants.CMD_FACTORY_RESET: LOG.info("Factory reset requested"); return true; + case TLW64Constants.CMD_FETCH_STEPS: + case TLW64Constants.CMD_FETCH_SLEEP: + handleActivityData(data); + return true; case TLW64Constants.CMD_NOTIFICATION: LOG.info("Notification is displayed"); return true; @@ -345,7 +359,7 @@ public class TLW64Support extends AbstractBTLEDeviceSupport { @Override public void onFetchRecordedData(int dataTypes) { - + sendFetchCommand(TLW64Constants.CMD_FETCH_STEPS); } @Override @@ -596,4 +610,99 @@ public class TLW64Support extends AbstractBTLEDeviceSupport { LOG.warn("Unable to stop notification", e); } } + + private void sendFetchCommand(byte type) { + samples.clear(); + crc = 0; + firstTimestamp = 0; + try { + TransactionBuilder builder = performInitialized("fetchActivityData"); + builder.add(new SetDeviceBusyAction(getDevice(), getContext().getString(R.string.busy_task_fetch_activity_data), getContext())); + byte[] msg = new byte[]{ + type, + (byte) 0xfa + }; + builder.write(ctrlCharacteristic, msg); + builder.queue(getQueue()); + } catch (IOException e) { + GB.toast(getContext(), "Error fetching activity data: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + } + } + + private void handleActivityData(byte[] data) { + if (data[1] == (byte) 0xfd) { + LOG.info("CRC received: " + (data[2] & 0xff) + ", calculated: " + (crc & 0xff)); + if (data[2] != crc) { + GB.toast(getContext(), "Incorrect CRC. Try fetching data again.", Toast.LENGTH_LONG, GB.ERROR); + GB.updateTransferNotification(null, "Data transfer failed", false, 0, getContext()); + if (getDevice().isBusy()) { + getDevice().unsetBusyTask(); + getDevice().sendDeviceUpdateIntent(getContext()); + } + } else if (samples.size() > 0) { + try (DBHandler dbHandler = GBApplication.acquireDB()) { + Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId(); + Long deviceId = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()).getId(); + TLW64SampleProvider provider = new TLW64SampleProvider(getDevice(), dbHandler.getDaoSession()); + for (int i = 0; i < samples.size(); i++) { + samples.get(i).setDeviceId(deviceId); + samples.get(i).setUserId(userId); + if (data[0] == TLW64Constants.CMD_FETCH_STEPS) { + samples.get(i).setRawKind(ActivityKind.TYPE_ACTIVITY); + samples.get(i).setRawIntensity(samples.get(i).getSteps()); + } else if (data[0] == TLW64Constants.CMD_FETCH_SLEEP) { + if (samples.get(i).getRawIntensity() < 7) { + samples.get(i).setRawKind(ActivityKind.TYPE_DEEP_SLEEP); + } else + samples.get(i).setRawKind(ActivityKind.TYPE_LIGHT_SLEEP); + } + provider.addGBActivitySample(samples.get(i)); + } + LOG.info("Activity data saved"); + if (data[0] == TLW64Constants.CMD_FETCH_STEPS) { + sendFetchCommand(TLW64Constants.CMD_FETCH_SLEEP); + } else { + GB.updateTransferNotification(null, "", false, 100, getContext()); + if (getDevice().isBusy()) { + getDevice().unsetBusyTask(); + GB.signalActivityDataFinish(); + } + } + } catch (Exception ex) { + GB.toast(getContext(), "Error saving activity data: " + ex.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + GB.updateTransferNotification(null, "Data transfer failed", false, 0, getContext()); + } + } + } else { + TLW64ActivitySample sample = new TLW64ActivitySample(); + + Calendar timestamp = GregorianCalendar.getInstance(); + timestamp.set(Calendar.YEAR, data[1] * 256 + (data[2] & 0xff)); + timestamp.set(Calendar.MONTH, (data[3] - 1) & 0xff); + timestamp.set(Calendar.DAY_OF_MONTH, data[4] & 0xff); + timestamp.set(Calendar.HOUR_OF_DAY, data[5] & 0xff); + timestamp.set(Calendar.SECOND, 0); + + int startProgress = 0; + if (data[0] == TLW64Constants.CMD_FETCH_STEPS) { + timestamp.set(Calendar.MINUTE, 0); + sample.setSteps(data[6] * 256 + (data[7] & 0xff)); + crc ^= (data[6] ^ data[7]); + } else if (data[0] == TLW64Constants.CMD_FETCH_SLEEP) { + timestamp.set(Calendar.MINUTE, data[6] & 0xff); + sample.setRawIntensity(data[7] * 256 + (data[8] & 0xff)); + crc ^= (data[7] ^ data[8]); + startProgress = 33; + } + + sample.setTimestamp((int) (timestamp.getTimeInMillis() / 1000L)); + samples.add(sample); + + if (firstTimestamp == 0) + firstTimestamp = sample.getTimestamp(); + int progress = startProgress + 33 * (sample.getTimestamp() - firstTimestamp) / + ((int) (Calendar.getInstance().getTimeInMillis() / 1000L) - firstTimestamp); + GB.updateTransferNotification(null, getContext().getString(R.string.busy_task_fetch_activity_data), true, progress, getContext()); + } + } }