From da1a72c6c6a0f9ea44058a9fc88f44e83f3d7af9 Mon Sep 17 00:00:00 2001 From: Cre3per Date: Mon, 7 Oct 2019 15:28:54 +0200 Subject: [PATCH] makibes hr3. implemented deleteDevice. implemented heart rate history download. cleaned up sample handling. --- .../makibeshr3/MakibesHR3Constants.java | 59 +++++--- .../makibeshr3/MakibesHR3Coordinator.java | 13 +- .../makibeshr3/MakibesHR3DeviceSupport.java | 131 +++++++++++++----- 3 files changed, 145 insertions(+), 58 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/makibeshr3/MakibesHR3Constants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/makibeshr3/MakibesHR3Constants.java index d585d7754..1981bef1f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/makibeshr3/MakibesHR3Constants.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/makibeshr3/MakibesHR3Constants.java @@ -72,7 +72,6 @@ public final class MakibesHR3Constants { public static final byte[] RPRT_SINGLE_BLOOD_OXYGEN = new byte[]{ (byte) 0x31, (byte) 0x11 }; - // This is also used with different parameters. // steps take up more bytes. I don't know which ones yet. // Only sent after we send CMD_51 // 00 (maybe also used for steps) @@ -86,7 +85,32 @@ public final class MakibesHR3Constants { // 00 // 00 // 00 - public static final byte RPRT_FITNESS = (byte) 0x51; + public static final byte[] RPRT_FITNESS = new byte[]{ (byte) 0x51, 0x08 }; + + // year (+2000) + // month + // day + // hour + // minute + // heart rate + // heart rate + public static final byte[] RPRT_HEART_RATE_SAMPLE = new byte[]{ (byte) 0x51, (byte) 0x11 }; + + // year + // month + // day + // hour? + // 00 + // 01 + // 39 + // 00 + // 00 + // 0d + // 00 + // 00 + // 00 + // 00 + public static final byte[] RPRT_51_20 = new byte[]{ (byte) 0x51, (byte) 0x20 }; // enable (00/01) @@ -123,34 +147,35 @@ public final class MakibesHR3Constants { public static final byte CMD_FACTORY_RESET = (byte) 0x23; + // enable (00/01) + public static final byte[] CMD_SET_REAL_TIME_BLOOD_OXYGEN = new byte[]{ (byte) 0x31, (byte) 0x12 }; + + + // After disabling, the watch replies with RPRT_SINGLE_BLOOD_OXYGEN + // enable (00/01) + public static final byte[] CMD_SET_SINGLE_BLOOD_OXYGEN = new byte[]{ (byte) 0x31, (byte) 0x11 }; + + + // The times in here are probably 'from' and 'until' // 00 // year (+2000) - // month (not current! but close) - // day (not current! but close) + // month (not current! but close. Multiple of 5) + // day (not current! but close. Multiple of 5) // 0b (A) // 00 (B) // year (+2000) // month (not current! but close) // day (not current! but close) - // 0b (this is >= (A)) - // 19 (this is >= (B)) + // 0b (this is >= (A). Multiple of 5) + // 19 (this is >= (B). Multiple of 5) public static final byte CMD_REQUEST_FITNESS = (byte) 0x51; - // I don't think the watch can do this, but it replies. - // enable (00/01) - public static final byte[] CMD_SET_REAL_TIME_BLOOD_OXYGEN = new byte[]{ (byte) 0x31, (byte) 0x12 }; - - // When disabling, the watch replies with RPRT_SINGLE_BLOOD_OXYGEN - // enable (00/01) - public static final byte[] CMD_SET_SINGLE_BLOOD_OXYGEN = new byte[]{ (byte) 0x31, (byte) 0x11 }; - - - // this is the last command sent on sync + // this looks like a request for the heart rate history // 00 // year (+2000) // month (not current!) - // 14 this isn't the current day + // day (not current!) // hour (current) // minute (current) public static final byte CMD_52 = (byte) 0x52; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/makibeshr3/MakibesHR3Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/makibeshr3/MakibesHR3Coordinator.java index 6d1ab96fb..e5ddbc4a7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/makibeshr3/MakibesHR3Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/makibeshr3/MakibesHR3Coordinator.java @@ -28,6 +28,7 @@ import androidx.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import de.greenrobot.dao.query.QueryBuilder; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.GBException; import nodomain.freeyourgadget.gadgetbridge.R; @@ -37,6 +38,8 @@ import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.MakibesHR3ActivitySampleDao; +import nodomain.freeyourgadget.gadgetbridge.entities.No1F1ActivitySampleDao; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; @@ -50,11 +53,9 @@ public class MakibesHR3Coordinator extends AbstractDeviceCoordinator { private static final Logger LOG = LoggerFactory.getLogger(MakibesHR3Coordinator.class); public static byte getTimeMode(SharedPreferences sharedPrefs) { - String tmode = sharedPrefs.getString(DeviceSettingsPreferenceConst.PREF_TIMEFORMAT, getContext().getString(R.string.p_timeformat_24h)); + String timeMode = sharedPrefs.getString(DeviceSettingsPreferenceConst.PREF_TIMEFORMAT, getContext().getString(R.string.p_timeformat_24h)); - LOG.debug("tmode is " + tmode); - - if (tmode.equals(getContext().getString(R.string.p_timeformat_24h))) { + if (timeMode.equals(getContext().getString(R.string.p_timeformat_24h))) { return MakibesHR3Constants.ARG_SET_TIMEMODE_24H; } else { return MakibesHR3Constants.ARG_SET_TIMEMODE_12H; @@ -82,7 +83,9 @@ public class MakibesHR3Coordinator extends AbstractDeviceCoordinator { @Override protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { - + Long deviceId = device.getId(); + QueryBuilder qb = session.getMakibesHR3ActivitySampleDao().queryBuilder(); + qb.where(MakibesHR3ActivitySampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities(); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/makibeshr3/MakibesHR3DeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/makibeshr3/MakibesHR3DeviceSupport.java index ac42f106d..d3271f566 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/makibeshr3/MakibesHR3DeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/makibeshr3/MakibesHR3DeviceSupport.java @@ -1,8 +1,11 @@ // TODO: WearFit sometimes resets today's step count when it's used after GB. // TODO: Where can I view today's steps in GB? -// TODO: GB adds the step count to the week's total every time we broadcast a sample. +// TODO: GB accumulates all step samples, even if they're part of the same day. -// TODO: Read activity history from device +// TODO: Activity history download progress. +// TODO: Remove downloaded history from the device. + +// TODO: Request and handle step history from the device. // TODO: All the commands that aren't supported by GB should be added to device specific settings. @@ -27,8 +30,11 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.sql.Date; import java.util.ArrayList; +import java.util.Arrays; import java.util.Calendar; +import java.util.GregorianCalendar; import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.GBApplication; @@ -43,6 +49,8 @@ import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.entities.MakibesHR3ActivitySample; import nodomain.freeyourgadget.gadgetbridge.entities.User; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; import nodomain.freeyourgadget.gadgetbridge.model.Alarm; import nodomain.freeyourgadget.gadgetbridge.model.BatteryState; @@ -485,7 +493,7 @@ public class MakibesHR3DeviceSupport extends AbstractBTLEDeviceSupport implement return builder; } - private void broadcastActivity(Integer heartRate, Integer steps) { + private void addGBActivitySamples(MakibesHR3ActivitySample[] samples) { try (DBHandler dbHandler = GBApplication.acquireDB()) { User user = DBHelper.getUser(dbHandler.getDaoSession()); @@ -493,45 +501,93 @@ public class MakibesHR3DeviceSupport extends AbstractBTLEDeviceSupport implement MakibesHR3SampleProvider provider = new MakibesHR3SampleProvider(this.getDevice(), dbHandler.getDaoSession()); - int timeStamp = (int) (System.currentTimeMillis() / 1000); + for (MakibesHR3ActivitySample sample : samples) { + sample.setDevice(device); + sample.setUser(user); + sample.setProvider(provider); - MakibesHR3ActivitySample sample = this.createActivitySample(device, user, timeStamp, provider); + sample.setRawIntensity(ActivitySample.NOT_MEASURED); - if (heartRate != null) { - sample.setHeartRate(heartRate); + provider.addGBActivitySample(sample); } - if (steps != null) { - sample.setSteps(steps); - } - - // I saw this somewhere else and it works. - sample.setRawKind(-1); - - provider.addGBActivitySample(sample); - - // TODO: steps aren't real time. - Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES) - .putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample) - .putExtra(DeviceService.EXTRA_TIMESTAMP, timeStamp); - LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); - } catch (Exception ex) { - GB.toast(getContext(), "Error saving steps data: " + ex.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + // Why is this a toast? The user doesn't care about the error. + GB.toast(getContext(), "Error saving samples: " + ex.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); GB.updateTransferNotification(null, "Data transfer failed", false, 0, getContext()); + + LOG.error(ex.getMessage()); } } + private void addGBActivitySample(MakibesHR3ActivitySample sample) { + this.addGBActivitySamples(new MakibesHR3ActivitySample[]{ sample }); + } + + /** + * Should only be called after the sample has been populated by + * {@link MakibesHR3DeviceSupport#addGBActivitySample} or + * {@link MakibesHR3DeviceSupport#addGBActivitySamples} + * @param sample + */ + private void broadcastSample(MakibesHR3ActivitySample sample) { + Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES) + .putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample) + .putExtra(DeviceService.EXTRA_TIMESTAMP, sample.getTimestamp()); + LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); + } + private void onReceiveFitness(int steps) { LOG.info("steps: " + steps); - this.broadcastActivity(null, steps); + MakibesHR3ActivitySample sample = new MakibesHR3ActivitySample(); + + sample.setSteps(steps); + sample.setTimestamp((int) (System.currentTimeMillis() / 1000)); + + sample.setRawKind(ActivityKind.TYPE_ACTIVITY); + this.addGBActivitySample(sample); } private void onReceiveHeartRate(int heartRate) { LOG.info("heart rate: " + heartRate); - this.broadcastActivity(heartRate, null); + MakibesHR3ActivitySample sample = new MakibesHR3ActivitySample(); + + if (heartRate > 0) { + sample.setHeartRate(heartRate); + sample.setTimestamp((int) (System.currentTimeMillis() / 1000)); + sample.setRawKind(ActivityKind.TYPE_ACTIVITY); + } else { + if (heartRate == MakibesHR3Constants.ARG_HEARTRATE_NO_TARGET) { + sample.setRawKind(ActivityKind.TYPE_NOT_WORN); + } else if (heartRate == MakibesHR3Constants.ARG_HEARTRATE_NO_READING) { + sample.setRawKind(ActivityKind.TYPE_NOT_MEASURED); + } else { + LOG.warn("invalid heart rate reading: " + heartRate); + return; + } + } + + this.addGBActivitySample(sample); + this.broadcastSample(sample); + } + + private void onReceiveHeartRateSample(int year, int month, int day, int hour, int minute, int heartRate) { + LOG.debug("received heart rate sample " + year + "-" + month + "-" + day + " " + hour + ":" + minute + " " + heartRate); + + MakibesHR3ActivitySample sample = new MakibesHR3ActivitySample(); + + Calendar calendar = new GregorianCalendar(year, month - 1, day, hour, minute); + + int timeStamp = (int) (calendar.getTimeInMillis() / 1000); + + sample.setHeartRate(heartRate); + sample.setTimestamp(timeStamp); + + sample.setRawKind(ActivityKind.TYPE_ACTIVITY); + + this.addGBActivitySample(sample); } @Override @@ -555,18 +611,9 @@ public class MakibesHR3DeviceSupport extends AbstractBTLEDeviceSupport implement arguments[i] = value[i + 6]; } - byte report = value[4]; + byte[] report = new byte[]{ value[4], value[5] }; - LOG.debug("report: " + Integer.toHexString((int) report)); - - switch (report) { - case MakibesHR3Constants.RPRT_FITNESS: - if (value.length == 17) { - this.onReceiveFitness( - (int) arguments[1] * 0xff + arguments[2] - ); - } - break; + switch (report[0]) { case MakibesHR3Constants.RPRT_REVERSE_FIND_DEVICE: this.onReverseFindDevice(arguments[0] == 0x01); break; @@ -590,6 +637,18 @@ public class MakibesHR3DeviceSupport extends AbstractBTLEDeviceSupport implement this.getDevice().setFirmwareVersion(((int) arguments[0]) + "." + ((int) arguments[1])); } break; + default: // Non-80 reports + if (Arrays.equals(report, MakibesHR3Constants.RPRT_FITNESS)) { + this.onReceiveFitness( + (int) arguments[1] * 0xff + arguments[2] + ); + } else if (Arrays.equals(report, MakibesHR3Constants.RPRT_HEART_RATE_SAMPLE)) { + this.onReceiveHeartRateSample( + arguments[0] + 2000, arguments[1], arguments[2], + arguments[3], arguments[4], + arguments[5]); + } + break; } }