diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index f9a0a697e..3f07e9250 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -70,7 +70,7 @@ public class GBDaoGenerator { addXWatchActivitySample(schema, user, device); addZeTimeActivitySample(schema, user, device); addID115ActivitySample(schema, user, device); - + addJYouActivitySample(schema, user, device); addCalendarSyncState(schema, device); addAlarms(schema, user, device); @@ -328,6 +328,19 @@ public class GBDaoGenerator { return activitySample; } + private static Entity addJYouActivitySample(Schema schema, Entity user, Entity device) { + Entity activitySample = addEntity(schema, "JYouActivitySample"); + activitySample.implementsSerializable(); + addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device); + activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty("caloriesBurnt"); + activitySample.addIntProperty("distanceMeters"); + activitySample.addIntProperty("activeTimeMinutes"); + addHeartRateProperties(activitySample); + 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/jyou/JYouConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/JYouConstants.java index b18c65d85..2aac395a3 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/JYouConstants.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/JYouConstants.java @@ -36,12 +36,17 @@ public final class JYouConstants { public static final byte CMD_SET_SLEEP_TIME = 0x27; public static final byte CMD_SET_DND_SETTINGS = 0x39; public static final byte CMD_SET_INACTIVITY_WARNING_TIME = 0x24; + public static final byte CMD_ACTION_HEARTRATE_SWITCH = 0x0D; public static final byte CMD_ACTION_SHOW_NOTIFICATION = 0x2C; public static final byte CMD_ACTION_REBOOT_DEVICE = 0x0E; - public static final byte RECEIVE_BATTERY_LEVEL = (byte)0xF7; + public static final byte RECEIVE_HISTORY_SLEEP_COUNT = 0x32; + public static final byte RECEIVE_BLOOD_PRESSURE = (byte) 0xE8; + public static final byte RECEIVE_WATCH_MAC = (byte)0xEC; + public static final byte RECEIVE_GET_PHOTO = (byte)0xF3; public static final byte RECEIVE_DEVICE_INFO = (byte)0xF6; + public static final byte RECEIVE_BATTERY_LEVEL = (byte)0xF7; public static final byte RECEIVE_STEPS_DATA = (byte)0xF9; public static final byte RECEIVE_HEARTRATE = (byte)0xFC; @@ -54,4 +59,4 @@ public final class JYouConstants { public static final byte ICON_TWITTER = 6; public static final byte ICON_WHATSAPP = 7; public static final byte ICON_LINE = 8; -} +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/JYouSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/JYouSampleProvider.java new file mode 100644 index 000000000..b29c02b51 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/JYouSampleProvider.java @@ -0,0 +1,70 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.jyou; + +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.JYouActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.JYouActivitySampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class JYouSampleProvider extends AbstractSampleProvider { + + public static final int TYPE_ACTIVITY = -1; + private final float movementDivisor = 6000.0f; + private GBDevice mDevice; + private DaoSession mSession; + + public JYouSampleProvider(GBDevice device, DaoSession session) { + super(device, session); + + mSession = session; + mDevice = device; + } + + @Override + public int normalizeType(int rawType) { + return rawType; + } + + @Override + public int toRawActivityKind(int activityKind) { + return activityKind; + } + + @Override + public float normalizeIntensity(int rawIntensity) { + return rawIntensity/movementDivisor; + } + + @Override + public JYouActivitySample createActivitySample() { + return new JYouActivitySample(); + } + + @Override + public AbstractDao getSampleDao() { + return getSession().getJYouActivitySampleDao(); + } + + @Nullable + @Override + protected Property getRawKindSampleProperty() { + return JYouActivitySampleDao.Properties.RawKind; + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return JYouActivitySampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return JYouActivitySampleDao.Properties.DeviceId; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/TeclastH30Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/TeclastH30/TeclastH30Coordinator.java similarity index 97% rename from app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/TeclastH30Coordinator.java rename to app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/TeclastH30/TeclastH30Coordinator.java index 68a464af4..6b87a661e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/TeclastH30Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/TeclastH30/TeclastH30Coordinator.java @@ -15,7 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -package nodomain.freeyourgadget.gadgetbridge.devices.jyou; +package nodomain.freeyourgadget.gadgetbridge.devices.jyou.TeclastH30; import android.annotation.TargetApi; import android.app.Activity; @@ -38,6 +38,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBException; import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.jyou.JYouConstants; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/y5/Y5Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/y5/Y5Coordinator.java new file mode 100644 index 000000000..af3f262dc --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/y5/Y5Coordinator.java @@ -0,0 +1,132 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.jyou.y5; + +import android.app.Activity; +import android.content.Context; +import android.net.Uri; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import de.greenrobot.dao.query.QueryBuilder; +import nodomain.freeyourgadget.gadgetbridge.GBException; +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; +import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.jyou.JYouSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.JYouActivitySampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; + +public class Y5Coordinator extends AbstractDeviceCoordinator { + @Override + protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { + Long deviceId = device.getId(); + QueryBuilder qb = session.getJYouActivitySampleDao().queryBuilder(); + qb.where(JYouActivitySampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities(); + } + + @NonNull + @Override + public DeviceType getSupportedType(GBDeviceCandidate candidate) { + try { + String name = candidate.getDevice().getName(); + if (name != null) { + if (name.contains("Y5")) { + return DeviceType.Y5; + } + } + } catch (Exception ex) { + ex.getLocalizedMessage(); + } + return DeviceType.UNKNOWN; + } + + @Override + public DeviceType getDeviceType() { + return DeviceType.Y5; + } + + @Nullable + @Override + public Class getPairingActivity() { + return null; + } + + @Override + public boolean supportsActivityDataFetching() { + return true; + } + + @Override + public boolean supportsActivityTracking() { + return true; + } + + @Override + public SampleProvider getSampleProvider(GBDevice device, DaoSession session) { + return new JYouSampleProvider(device, session); + } + + @Override + public InstallHandler findInstallHandler(Uri uri, Context context) { + return null; + } + + @Override + public boolean supportsScreenshots() { + return false; + } + + @Override + public int getAlarmSlotCount() { + return 3; + } + + @Override + public boolean supportsSmartWakeup(GBDevice device) { + return true; + } + + @Override + public boolean supportsHeartRateMeasurement(GBDevice device) { + return true; + } + + @Override + public String getManufacturer() { + return "Y5"; + } + + @Override + public boolean supportsAppsManagement() { + return false; + } + + @Override + public Class getAppsManagementActivity() { + return null; + } + + @Override + public boolean supportsCalendarEvents() { + return false; + } + + @Override + public boolean supportsRealtimeData() { + return true; + } + + @Override + public boolean supportsWeather() { + return false; + } + + @Override + public boolean supportsFindDevice() { + return true; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java index 9625f0db8..17334d6b6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -58,6 +58,7 @@ public enum DeviceType { WATCH9(100, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_watch9), ROIDMI(110, R.drawable.ic_device_roidmi, R.drawable.ic_device_roidmi_disabled, R.string.devicetype_roidmi), ROIDMI3(112, R.drawable.ic_device_roidmi, R.drawable.ic_device_roidmi_disabled, R.string.devicetype_roidmi3), + Y5(113, R.drawable.ic_device_h30_h10, R.drawable.ic_device_roidmi_disabled, R.string.devicetype_y5), CASIOGB6900(120, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_casiogb6900), MISCALE2(131, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_miscale2), BFH16(140, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_bfh16), diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java index b30bf3db8..95ac4695d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java @@ -45,7 +45,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband3.MiBand import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband4.MiBand4Support; import nodomain.freeyourgadget.gadgetbridge.service.devices.id115.ID115Support; import nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.BFH16DeviceSupport; -import nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.TeclastH30Support; +import nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.TeclastH30.TeclastH30Support; import nodomain.freeyourgadget.gadgetbridge.service.devices.liveview.LiveviewSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.makibeshr3.MakibesHR3DeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport; @@ -57,6 +57,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSuppo import nodomain.freeyourgadget.gadgetbridge.service.devices.roidmi.RoidmiSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.vibratissimo.VibratissimoSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.watch9.Watch9DeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.y5.Y5Support; import nodomain.freeyourgadget.gadgetbridge.service.devices.xwatch.XWatchSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.zetime.ZeTimeDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.util.GB; @@ -203,6 +204,9 @@ public class DeviceSupportFactory { case ROIDMI3: deviceSupport = new ServiceDeviceSupport(new RoidmiSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); break; + case Y5: + deviceSupport = new ServiceDeviceSupport(new Y5Support(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); + break; case CASIOGB6900: deviceSupport = new ServiceDeviceSupport(new CasioGB6900DeviceSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); break; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/JYouDataRecord.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/JYouDataRecord.java new file mode 100644 index 000000000..ec9f08999 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/JYouDataRecord.java @@ -0,0 +1,65 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.jyou; + +/* + * @author Pavel Elagin <elagin.pasha@gmail.com> + */ + +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; + +public class JYouDataRecord { + public final static int TYPE_UNKNOWN = 0; + public final static int TYPE_SLEEP = 100; + public final static int TYPE_DAY_SUMMARY = 101; + public final static int TYPE_DAY_SLOT = 102; + public final static int TYPE_REALTIME = 103; + + public int type = TYPE_UNKNOWN; + public int activityKind = ActivityKind.TYPE_UNKNOWN; + + /** + * Time of this record in seconds + */ + public int timestamp; + + /** + * Raw data as sent from the device + */ + public byte[] rawData; + + protected JYouDataRecord(){ + + } + + protected JYouDataRecord(byte[] data, int type){ + this.rawData = data; + this.type = type; + } + + public byte[] getRawData() { + + return rawData; + } + + public class RecordInterval { + /** + * Start time of this interval in seconds + */ + public int timestampFrom; + + /** + * End time of this interval in seconds + */ + public int timestampTo; + + /** + * Type of activity {@link ActivityKind} + */ + public int activityKind; + + RecordInterval(int timestampFrom, int timestampTo, int activityKind) { + this.timestampFrom = timestampFrom; + this.timestampTo = timestampTo; + this.activityKind = activityKind; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/TeclastH30Support.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/JYouSupport.java similarity index 72% rename from app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/TeclastH30Support.java rename to app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/JYouSupport.java index 368966e15..5f83a4c52 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/TeclastH30Support.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/JYouSupport.java @@ -23,7 +23,6 @@ import android.net.Uri; import android.widget.Toast; import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.UnsupportedEncodingException; @@ -50,29 +49,32 @@ import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; -public class TeclastH30Support extends AbstractBTLEDeviceSupport { +public class JYouSupport extends AbstractBTLEDeviceSupport { - private static final Logger LOG = LoggerFactory.getLogger(TeclastH30Support.class); + private Logger logger; - public BluetoothGattCharacteristic ctrlCharacteristic = null; - public BluetoothGattCharacteristic measureCharacteristic = null; + protected BluetoothGattCharacteristic ctrlCharacteristic = null; - private final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo(); - private final GBDeviceEventBatteryInfo batteryCmd = new GBDeviceEventBatteryInfo(); + protected final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo(); + protected final GBDeviceEventBatteryInfo batteryCmd = new GBDeviceEventBatteryInfo(); - public TeclastH30Support() { - super(LOG); + public JYouSupport(Logger logger) { + super(logger); + this.logger = logger; + if (logger == null) { + throw new IllegalArgumentException("logger must not be null"); + } addSupportedService(JYouConstants.UUID_SERVICE_JYOU); } @Override protected TransactionBuilder initializeDevice(TransactionBuilder builder) { - LOG.info("Initializing"); + logger.info("Initializing"); gbDevice.setState(GBDevice.State.INITIALIZING); gbDevice.sendDeviceUpdateIntent(getContext()); - measureCharacteristic = getCharacteristic(JYouConstants.UUID_CHARACTERISTIC_MEASURE); + BluetoothGattCharacteristic measureCharacteristic = getCharacteristic(JYouConstants.UUID_CHARACTERISTIC_MEASURE); ctrlCharacteristic = getCharacteristic(JYouConstants.UUID_CHARACTERISTIC_CONTROL); builder.setGattCallback(this); @@ -83,7 +85,7 @@ public class TeclastH30Support extends AbstractBTLEDeviceSupport { gbDevice.setState(GBDevice.State.INITIALIZED); gbDevice.sendDeviceUpdateIntent(getContext()); - LOG.info("Initialization Done"); + logger.info("Initialization Done"); return builder; } @@ -91,41 +93,10 @@ public class TeclastH30Support extends AbstractBTLEDeviceSupport { @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_DEVICE_INFO: - 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); - return true; - case JYouConstants.RECEIVE_HEARTRATE: - LOG.info("Current heart rate: " + data[8]); - return true; - default: - LOG.info("Unhandled characteristic change: " + characteristicUUID + " code: " + String.format("0x%1x ...", data[0])); - return true; - } + return super.onCharacteristicChanged(gatt, characteristic); } - private void syncDateAndTime(TransactionBuilder builder) { + protected void syncDateAndTime(TransactionBuilder builder) { Calendar cal = Calendar.getInstance(); String strYear = String.valueOf(cal.get(Calendar.YEAR)); byte year1 = (byte)Integer.parseInt(strYear.substring(0, 2)); @@ -144,45 +115,7 @@ public class TeclastH30Support extends AbstractBTLEDeviceSupport { )); } - private void syncSettings(TransactionBuilder builder) { - syncDateAndTime(builder); - - // TODO: unhardcode and separate stuff - builder.write(ctrlCharacteristic, commandWithChecksum( - JYouConstants.CMD_SET_HEARTRATE_WARNING_VALUE, 0, 152 - )); - builder.write(ctrlCharacteristic, commandWithChecksum( - JYouConstants.CMD_SET_TARGET_STEPS, 0, 10000 - )); - builder.write(ctrlCharacteristic, commandWithChecksum( - JYouConstants.CMD_GET_STEP_COUNT, 0, 0 - )); - builder.write(ctrlCharacteristic, commandWithChecksum( - JYouConstants.CMD_GET_SLEEP_TIME, 0, 0 - )); - builder.write(ctrlCharacteristic, commandWithChecksum( - JYouConstants.CMD_SET_NOON_TIME, 12 * 60 * 60, 14 * 60 * 60 // 12:00 - 14:00 - )); - builder.write(ctrlCharacteristic, commandWithChecksum( - JYouConstants.CMD_SET_SLEEP_TIME, 21 * 60 * 60, 8 * 60 * 60 // 21:00 - 08:00 - )); - builder.write(ctrlCharacteristic, commandWithChecksum( - JYouConstants.CMD_SET_INACTIVITY_WARNING_TIME, 0, 0 - )); - - // do not disturb and a couple more features - byte dndStartHour = 22; - byte dndStartMin = 0; - byte dndEndHour = 8; - byte dndEndMin = 0; - boolean dndToggle = false; - boolean vibrationToggle = true; - boolean wakeOnRaiseToggle = true; - builder.write(ctrlCharacteristic, commandWithChecksum( - JYouConstants.CMD_SET_DND_SETTINGS, - (dndStartHour << 24) | (dndStartMin << 16) | (dndEndHour << 8) | dndEndMin, - ((dndToggle ? 0 : 1) << 2) | ((vibrationToggle ? 1 : 0) << 1) | (wakeOnRaiseToggle ? 1 : 0) - )); + protected void syncSettings(TransactionBuilder builder) { } private void showNotification(byte icon, String title, String message) { @@ -217,9 +150,9 @@ public class TeclastH30Support extends AbstractBTLEDeviceSupport { } builder.write(ctrlCharacteristic, currentPacket); } - builder.queue(getQueue()); + performConnected(builder.getTransaction()); } catch (IOException e) { - LOG.warn(e.getMessage()); + logger.warn(e.getMessage()); } } @@ -286,10 +219,10 @@ public class TeclastH30Support extends AbstractBTLEDeviceSupport { alarms.get(i).getEnabled() ? cal.get(Calendar.MINUTE) : -1 )); } - builder.queue(getQueue()); + performConnected(builder.getTransaction()); GB.toast(getContext(), "Alarm settings applied - do note that the current device does not support day specification", Toast.LENGTH_LONG, GB.INFO); } catch(IOException e) { - LOG.warn(e.getMessage()); + logger.warn(e.getMessage()); } } @@ -298,18 +231,16 @@ public class TeclastH30Support extends AbstractBTLEDeviceSupport { try { TransactionBuilder builder = performInitialized("SetTime"); syncDateAndTime(builder); - builder.queue(getQueue()); + performConnected(builder.getTransaction()); } catch(IOException e) { - LOG.warn(e.getMessage()); + logger.warn(e.getMessage()); } } @Override public void onSetCallState(CallSpec callSpec) { - switch (callSpec.command) { - case CallSpec.CALL_INCOMING: - showNotification(JYouConstants.ICON_CALL, callSpec.name, callSpec.number); - break; + if(callSpec.command == CallSpec.CALL_INCOMING) { + showNotification(JYouConstants.ICON_CALL, callSpec.name, callSpec.number); } } @@ -375,9 +306,9 @@ public class TeclastH30Support extends AbstractBTLEDeviceSupport { builder.write(ctrlCharacteristic, commandWithChecksum( JYouConstants.CMD_ACTION_REBOOT_DEVICE, 0, 0 )); - builder.queue(getQueue()); + performConnected(builder.getTransaction()); } catch(Exception e) { - LOG.warn(e.getMessage()); + logger.warn(e.getMessage()); } } @@ -388,15 +319,14 @@ public class TeclastH30Support extends AbstractBTLEDeviceSupport { builder.write(ctrlCharacteristic, commandWithChecksum( JYouConstants.CMD_ACTION_HEARTRATE_SWITCH, 0, 1 )); - builder.queue(getQueue()); + performConnected(builder.getTransaction()); } catch(Exception e) { - LOG.warn(e.getMessage()); + logger.warn(e.getMessage()); } } @Override public void onEnableRealtimeHeartRateMeasurement(boolean enable) { - // TODO: test try { TransactionBuilder builder = performInitialized("RealTimeHeartMeasurement"); builder.write(ctrlCharacteristic, commandWithChecksum( @@ -404,7 +334,7 @@ public class TeclastH30Support extends AbstractBTLEDeviceSupport { )); builder.queue(getQueue()); } catch(Exception e) { - LOG.warn(e.getMessage()); + logger.warn(e.getMessage()); } } @@ -466,7 +396,7 @@ public class TeclastH30Support extends AbstractBTLEDeviceSupport { } - private byte[] commandWithChecksum(byte cmd, int argSlot1, int argSlot2) + protected byte[] commandWithChecksum(byte cmd, int argSlot1, int argSlot2) { ByteBuffer buf = ByteBuffer.allocate(10); buf.put(cmd); @@ -505,7 +435,7 @@ public class TeclastH30Support extends AbstractBTLEDeviceSupport { } } } catch (UnsupportedEncodingException e) { - LOG.warn(e.getMessage()); + logger.warn(e.getMessage()); } return null; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/RealtimeSamplesSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/RealtimeSamplesSupport.java new file mode 100644 index 000000000..c75d0d19f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/RealtimeSamplesSupport.java @@ -0,0 +1,95 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.jyou; + +import java.util.Timer; +import java.util.TimerTask; + +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; + +public abstract class RealtimeSamplesSupport { + private final long delay; + private final long period; + + protected int steps; + protected int heartrateBpm; + private int lastSteps; + // subclasses may add more + + private Timer realtimeStorageTimer; + + public RealtimeSamplesSupport(long delay, long period) { + this.delay = delay; + this.period = period; + } + + public synchronized void start() { + if (isRunning()) { + return; // already running + } + realtimeStorageTimer = new Timer("JYou Realtime Storage Timer"); + realtimeStorageTimer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + triggerCurrentSample(); + } + }, delay, period); + } + + public synchronized void stop() { + if (realtimeStorageTimer != null) { + realtimeStorageTimer.cancel(); + realtimeStorageTimer.purge(); + realtimeStorageTimer = null; + } + } + + public synchronized boolean isRunning() { + return realtimeStorageTimer != null; + } + + public synchronized void setSteps(int stepsPerMinute) { + this.steps = stepsPerMinute; + } + + /** + * Returns the number of steps recorded since the last measurements. If no + * steps are available yet, ActivitySample.NOT_MEASURED is returned. + * @return + */ + public synchronized int getSteps() { + if (steps == ActivitySample.NOT_MEASURED) { + return ActivitySample.NOT_MEASURED; + } + if (lastSteps == 0) { + return ActivitySample.NOT_MEASURED; // wait until we have a delta between two samples + } + int delta = steps - lastSteps; + if (delta < 0) { + return 0; + } + return delta; + } + + public void setHeartrateBpm(int hrBpm) { + this.heartrateBpm = hrBpm; + } + + public int getHeartrateBpm() { + return heartrateBpm; + } + + public void triggerCurrentSample() { + doCurrentSample(); + resetCurrentValues(); + } + + protected synchronized void resetCurrentValues() { + if (steps >= lastSteps) { + lastSteps = steps; + } + steps = ActivitySample.NOT_MEASURED; + heartrateBpm = ActivitySample.NOT_MEASURED; + } + + protected abstract void doCurrentSample(); +} + diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/TeclastH30/TeclastH30Support.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/TeclastH30/TeclastH30Support.java new file mode 100644 index 000000000..69c6f942f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/TeclastH30/TeclastH30Support.java @@ -0,0 +1,121 @@ +/* Copyright (C) 2017-2019 Andreas Shimokawa, Carsten Pfeiffer, Sami Alaoui, + Sebastian Kranz + + 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.TeclastH30; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.devices.jyou.JYouConstants; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.JYouSupport; + +public class TeclastH30Support extends JYouSupport { + + private static final Logger LOG = LoggerFactory.getLogger(TeclastH30Support.class); + + public TeclastH30Support() { + 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_DEVICE_INFO: + 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); + return true; + case JYouConstants.RECEIVE_HEARTRATE: + LOG.info("Current heart rate: " + data[8]); + return true; + default: + LOG.info("Unhandled characteristic change: " + characteristicUUID + " code: " + String.format("0x%1x ...", data[0])); + return true; + } + } + + @Override + protected void syncSettings(TransactionBuilder builder) { + syncDateAndTime(builder); + + // TODO: unhardcode and separate stuff + builder.write(ctrlCharacteristic, commandWithChecksum( + JYouConstants.CMD_SET_HEARTRATE_WARNING_VALUE, 0, 152 + )); + builder.write(ctrlCharacteristic, commandWithChecksum( + JYouConstants.CMD_SET_TARGET_STEPS, 0, 10000 + )); + builder.write(ctrlCharacteristic, commandWithChecksum( + JYouConstants.CMD_GET_STEP_COUNT, 0, 0 + )); + builder.write(ctrlCharacteristic, commandWithChecksum( + JYouConstants.CMD_GET_SLEEP_TIME, 0, 0 + )); + builder.write(ctrlCharacteristic, commandWithChecksum( + JYouConstants.CMD_SET_NOON_TIME, 12 * 60 * 60, 14 * 60 * 60 // 12:00 - 14:00 + )); + builder.write(ctrlCharacteristic, commandWithChecksum( + JYouConstants.CMD_SET_SLEEP_TIME, 21 * 60 * 60, 8 * 60 * 60 // 21:00 - 08:00 + )); + builder.write(ctrlCharacteristic, commandWithChecksum( + JYouConstants.CMD_SET_INACTIVITY_WARNING_TIME, 0, 0 + )); + + // do not disturb and a couple more features + byte dndStartHour = 22; + byte dndStartMin = 0; + byte dndEndHour = 8; + byte dndEndMin = 0; + boolean dndToggle = false; + boolean vibrationToggle = true; + boolean wakeOnRaiseToggle = true; + builder.write(ctrlCharacteristic, commandWithChecksum( + JYouConstants.CMD_SET_DND_SETTINGS, + (dndStartHour << 24) | (dndStartMin << 16) | (dndEndHour << 8) | dndEndMin, + ((dndToggle ? 0 : 1) << 2) | ((vibrationToggle ? 1 : 0) << 1) | (wakeOnRaiseToggle ? 1 : 0) + )); + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/y5/Y5Support.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/y5/Y5Support.java new file mode 100644 index 000000000..96ef0d41a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/y5/Y5Support.java @@ -0,0 +1,233 @@ +/* Copyright (C) 2017-2019 Andreas Shimokawa, Carsten Pfeiffer, Sami Alaoui, + Sebastian Kranz + + 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.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(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()); + } + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java index 4ac3037d4..05d6562b8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java @@ -60,8 +60,9 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband2.MiBand2HRXCoor import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband3.MiBand3Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband4.MiBand4Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.id115.ID115Coordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.jyou.TeclastH30.TeclastH30Coordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.jyou.y5.Y5Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.jyou.BFH16DeviceCoordinator; -import nodomain.freeyourgadget.gadgetbridge.devices.jyou.TeclastH30Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.liveview.LiveviewCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator; @@ -235,6 +236,7 @@ public class DeviceHelper { result.add(new Watch9DeviceCoordinator()); result.add(new Roidmi1Coordinator()); result.add(new Roidmi3Coordinator()); + result.add(new Y5Coordinator()); result.add(new CasioGB6900DeviceCoordinator()); result.add(new BFH16DeviceCoordinator()); result.add(new MijiaLywsd02Coordinator()); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b8530ecf8..be131d35a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -684,6 +684,7 @@ Watch 9 Roidmi Roidmi 3 + Y5 Casio GB-6900 Mi Scale 2 BFH-16