/* Copyright (C) 2018-2024 Da Pa, Pavel Elagin 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.jyou.y5; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; import android.content.Intent; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.ByteBuffer; import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.jyou.JYouConstants; import nodomain.freeyourgadget.gadgetbridge.devices.jyou.JYouSampleProvider; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.entities.JYouActivitySample; import nodomain.freeyourgadget.gadgetbridge.entities.User; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.DeviceService; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.JYouSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.RealtimeSamplesSupport; public class Y5Support extends JYouSupport { private static final Logger LOG = LoggerFactory.getLogger(Y5Support.class); private RealtimeSamplesSupport realtimeSamplesSupport; public Y5Support() { super(LOG); addSupportedService(JYouConstants.UUID_SERVICE_JYOU); } @Override public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { if (super.onCharacteristicChanged(gatt, characteristic)) { return true; } UUID characteristicUUID = characteristic.getUuid(); byte[] data = characteristic.getValue(); if (data.length == 0) return true; switch (data[0]) { case JYouConstants.RECEIVE_HISTORY_SLEEP_COUNT: LOG.info("onCharacteristicChanged: " + data[0]); return true; case JYouConstants.RECEIVE_BLOOD_PRESSURE: int heartRate = data[2]; int bloodPressureHigh = data[3]; int bloodPressureLow = data[4]; int bloodOxygen = data[5]; int Fatigue = data[6]; LOG.info("RECEIVE_BLOOD_PRESSURE: Heart rate: " + heartRate + " Pressure high: " + bloodPressureHigh+ " pressure low: " + bloodPressureLow); return true; case JYouConstants.RECEIVE_DEVICE_INFO: int model = data[7]; int fwVerNum = data[4] & 0xFF; versionCmd.fwVersion = (fwVerNum / 100) + "." + ((fwVerNum % 100) / 10) + "." + ((fwVerNum % 100) % 10); handleGBDeviceEvent(versionCmd); LOG.info("Firmware version is: " + versionCmd.fwVersion); return true; case JYouConstants.RECEIVE_BATTERY_LEVEL: batteryCmd.level = data[8]; handleGBDeviceEvent(batteryCmd); LOG.info("Battery level is: " + batteryCmd.level); return true; case JYouConstants.RECEIVE_STEPS_DATA: int steps = ByteBuffer.wrap(data, 5, 4).getInt(); LOG.info("Number of walked steps: " + steps); handleRealtimeSteps(steps); return true; case JYouConstants.RECEIVE_HEARTRATE: handleHeartrate(data[8]); return true; case JYouConstants.RECEIVE_WATCH_MAC: return true; case JYouConstants.RECEIVE_GET_PHOTO: return true; default: LOG.info("Unhandled characteristic change: " + characteristicUUID + " code: " + String.format("0x%1x ...", data[0])); return true; } } private void handleRealtimeSteps(int value) { //todo Call on connect the device if (LOG.isDebugEnabled()) { LOG.debug("realtime steps: " + value); } getRealtimeSamplesSupport().setSteps(value); } private void handleHeartrate(int value) { if (LOG.isDebugEnabled()) { LOG.debug("heart rate: " + value); } RealtimeSamplesSupport realtimeSamplesSupport = getRealtimeSamplesSupport(); realtimeSamplesSupport.setHeartrateBpm(value); if (!realtimeSamplesSupport.isRunning()) { // single shot measurement, manually invoke storage and result publishing realtimeSamplesSupport.triggerCurrentSample(); } } public JYouActivitySample createActivitySample(Device device, User user, int timestampInSeconds, SampleProvider provider) { JYouActivitySample sample = new JYouActivitySample(); sample.setDevice(device); sample.setUser(user); sample.setTimestamp(timestampInSeconds); sample.setProvider(provider); return sample; } private void enableRealtimeSamplesTimer(boolean enable) { if (enable) { getRealtimeSamplesSupport().start(); } else { if (realtimeSamplesSupport != null) { realtimeSamplesSupport.stop(); } } } private RealtimeSamplesSupport getRealtimeSamplesSupport() { if (realtimeSamplesSupport == null) { realtimeSamplesSupport = new RealtimeSamplesSupport(1000, 1000) { @Override public void doCurrentSample() { try (DBHandler handler = GBApplication.acquireDB()) { DaoSession session = handler.getDaoSession(); int ts = (int) (System.currentTimeMillis() / 1000); JYouSampleProvider provider = new JYouSampleProvider(gbDevice, session); JYouActivitySample sample = createActivitySample(DBHelper.getDevice(getDevice(), session), DBHelper.getUser(session), ts, provider); sample.setHeartRate(getHeartrateBpm()); sample.setRawIntensity(ActivitySample.NOT_MEASURED); sample.setRawKind(JYouSampleProvider.TYPE_ACTIVITY); // to make it visible in the charts TODO: add a MANUAL kind for that? provider.addGBActivitySample(sample); // set the steps only afterwards, since realtime steps are also recorded // in the regular samples and we must not count them twice // Note: we know that the DAO sample is never committed again, so we simply // change the value here in memory. sample.setSteps(getSteps()); if(steps > 1){ LOG.debug("Have steps: " + getSteps()); } if (LOG.isDebugEnabled()) { LOG.debug("realtime sample: " + sample); } Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES) .putExtra(GBDevice.EXTRA_DEVICE, getDevice()) .putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample); LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); } catch (Exception e) { LOG.warn("Unable to acquire db for saving realtime samples", e); } } }; } return realtimeSamplesSupport; } @Override protected void syncSettings(TransactionBuilder builder) { syncDateAndTime(builder); } @Override public void dispose() { LOG.info("Dispose"); super.dispose(); } @Override public void onHeartRateTest() { try { TransactionBuilder builder = performInitialized("HeartRateTest"); builder.write(ctrlCharacteristic, commandWithChecksum( JYouConstants.CMD_SET_HEARTRATE_AUTO, 0, 0 )); performConnected(builder.getTransaction()); } catch(Exception e) { LOG.warn(e.getMessage()); } } @Override public void onEnableRealtimeHeartRateMeasurement(boolean enable) { try { TransactionBuilder builder = performInitialized("RealTimeHeartMeasurement"); builder.write(ctrlCharacteristic, commandWithChecksum( JYouConstants.CMD_ACTION_HEARTRATE_SWITCH, 0, enable ? 1 : 0 )); performConnected(builder.getTransaction()); enableRealtimeSamplesTimer(enable); } catch (Exception e) { LOG.warn(e.getMessage()); } } }