diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index b4ad0dc7f..d15e3f9af 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -265,6 +265,7 @@ public class GBDaoGenerator { activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE); activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE); activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE); + addHeartRateProperties(activitySample); return activitySample; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/no1f1/No1F1Constants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/no1f1/No1F1Constants.java index 651fdcb48..516f5d583 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/no1f1/No1F1Constants.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/no1f1/No1F1Constants.java @@ -18,6 +18,8 @@ public final class No1F1Constants { public static final byte CMD_REALTIME_STEPS = (byte) 0xb1; public static final byte CMD_FETCH_STEPS = (byte) 0xb2; public static final byte CMD_FETCH_SLEEP = (byte) 0xb3; + public static final byte CMD_REALTIME_HEARTRATE = (byte) 0xe5; + public static final byte CMD_FETCH_HEARTRATE = (byte) 0xe6; 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/no1f1/No1F1Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/no1f1/No1F1Coordinator.java index 4cd9dcb54..fae0b9c6a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/no1f1/No1F1Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/no1f1/No1F1Coordinator.java @@ -108,7 +108,7 @@ public class No1F1Coordinator extends AbstractDeviceCoordinator { @Override public boolean supportsHeartRateMeasurement(GBDevice device) { - return false; + return true; } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/no1f1/No1F1Support.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/no1f1/No1F1Support.java index 27f6e39d0..df079a800 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/no1f1/No1F1Support.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/no1f1/No1F1Support.java @@ -117,6 +117,12 @@ public class No1F1Support extends AbstractBTLEDeviceSupport { case No1F1Constants.CMD_FETCH_SLEEP: handleSleepData(data); return true; + case No1F1Constants.CMD_FETCH_HEARTRATE: + handleHeartRateData(data); + return true; + case No1F1Constants.CMD_REALTIME_HEARTRATE: + handleRealtimeHeartRateData(data); + return true; case No1F1Constants.CMD_NOTIFICATION: case No1F1Constants.CMD_ICON: case No1F1Constants.CMD_DEVICE_SETTINGS: @@ -240,7 +246,17 @@ public class No1F1Support extends AbstractBTLEDeviceSupport { @Override public void onHeartRateTest() { - + try { + TransactionBuilder builder = performInitialized("heartRateTest"); + byte[] msg = new byte[]{ + No1F1Constants.CMD_REALTIME_HEARTRATE, + (byte) 0x11 + }; + builder.write(ctrlCharacteristic, msg); + performConnected(builder.getTransaction()); + } catch (IOException e) { + GB.toast(getContext(), "Error starting heart rate measurement: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + } } @Override @@ -529,10 +545,18 @@ public class No1F1Support extends AbstractBTLEDeviceSupport { } samples.clear(); LOG.info("Sleep data saved"); - if (getDevice().isBusy()) { - getDevice().unsetBusyTask(); - getDevice().sendDeviceUpdateIntent(getContext()); + try { + TransactionBuilder builder = performInitialized("fetchHeartRate"); + byte[] msg = new byte[]{ + No1F1Constants.CMD_FETCH_HEARTRATE, + (byte) 0xfa + }; + builder.write(ctrlCharacteristic, msg); + performConnected(builder.getTransaction()); + } catch (IOException e) { + GB.toast(getContext(), "Error fetching heart rate data: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); } + } catch (Exception ex) { GB.toast(getContext(), "Error saving sleep data: " + ex.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); } @@ -557,4 +581,76 @@ public class No1F1Support extends AbstractBTLEDeviceSupport { ); } } + + private void handleHeartRateData(byte[] data) { + if (data[1] == (byte) 0xfd) { + // TODO Check CRC + if (samples.size() > 0) { + try (DBHandler dbHandler = GBApplication.acquireDB()) { + Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId(); + Long deviceId = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()).getId(); + No1F1SampleProvider provider = new No1F1SampleProvider(getDevice(), dbHandler.getDaoSession()); + for (int i = 0; i < samples.size(); i++) { + samples.get(i).setDeviceId(deviceId); + samples.get(i).setUserId(userId); + provider.addGBActivitySample(samples.get(i)); + } + samples.clear(); + LOG.info("Heart rate data saved"); + if (getDevice().isBusy()) { + getDevice().unsetBusyTask(); + getDevice().sendDeviceUpdateIntent(getContext()); + } + } catch (Exception ex) { + GB.toast(getContext(), "Error saving heart rate data: " + ex.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + } + } + } else { + No1F1ActivitySample sample = new No1F1ActivitySample(); + + 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.MINUTE, data[6] & 0xff); + timestamp.set(Calendar.SECOND, 0); + + sample.setTimestamp((int) (timestamp.getTimeInMillis() / 1000L)); + sample.setHeartRate(data[7] & 0xff); + + samples.add(sample); + LOG.info("Received heart rate data for " + String.format("%1$TD %1$TT", timestamp) + ": " + + sample.getHeartRate() + " BPM" + ); + } + } + + private void handleRealtimeHeartRateData(byte[] data) { + if (data.length==2) + { + if (data[1]==(byte) 0x11) + LOG.info("Heart rate measurement started."); + else + LOG.info("Heart rate measurement stopped."); + return; + } + // Check if data is valid. Otherwise ignore sample. + if (data[2]==0) { + No1F1ActivitySample sample = new No1F1ActivitySample(); + sample.setTimestamp((int) (GregorianCalendar.getInstance().getTimeInMillis() / 1000L)); + sample.setHeartRate(data[3] & 0xff); + LOG.info("Current heart rate is: " + sample.getHeartRate() + " BPM"); + try (DBHandler dbHandler = GBApplication.acquireDB()) { + Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId(); + Long deviceId = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()).getId(); + No1F1SampleProvider provider = new No1F1SampleProvider(getDevice(), dbHandler.getDaoSession()); + sample.setDeviceId(deviceId); + sample.setUserId(userId); + provider.addGBActivitySample(sample); + } catch (Exception ex) { + LOG.warn("Error saving current heart rate: " + ex.getLocalizedMessage()); + } + } + } }