diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index 4e0c91c3a..b4ad0dc7f 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -64,6 +64,7 @@ public class GBDaoGenerator { addPebbleMorpheuzActivitySample(schema, user, device); addHPlusHealthActivityKindOverlay(schema, user, device); addHPlusHealthActivitySample(schema, user, device); + addNo1F1ActivitySample(schema, user, device); addCalendarSyncState(schema, device); @@ -257,6 +258,16 @@ public class GBDaoGenerator { return activityOverlay; } + private static Entity addNo1F1ActivitySample(Schema schema, Entity user, Entity device) { + Entity activitySample = addEntity(schema, "No1F1ActivitySample"); + 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/jyou/JYouConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/JYouConstants.java new file mode 100644 index 000000000..1d87f181e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/JYouConstants.java @@ -0,0 +1,41 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.jyou; + +import java.util.UUID; + +public final class JYouConstants { + public static final UUID UUID_CHARACTERISTIC_CONTROL = UUID.fromString("000033f3-0000-1000-8000-00805f9b34fb"); + public static final UUID UUID_CHARACTERISTIC_MEASURE = UUID.fromString("000033f4-0000-1000-8000-00805f9b34fb"); + public static final UUID UUID_SERVICE_JYOU = UUID.fromString("000056ff-0000-1000-8000-00805f9b34fb"); + + public static final byte CMD_SET_DATE_AND_TIME = 0x08; + public static final byte CMD_SET_HEARTRATE_AUTO = 0x38; + public static final byte CMD_SET_HEARTRATE_WARNING_VALUE = 0x01; + public static final byte CMD_SET_TARGET_STEPS = 0x03; + public static final byte CMD_SET_ALARM_1 = 0x09; + public static final byte CMD_SET_ALARM_2 = 0x22; + public static final byte CMD_SET_ALARM_3 = 0x23; + public static final byte CMD_GET_STEP_COUNT = 0x1D; + public static final byte CMD_GET_SLEEP_TIME = 0x32; + public static final byte CMD_SET_NOON_TIME = 0x26; + 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_DEVICE_INFO = (byte)0xF6; + public static final byte RECEIVE_STEPS_DATA = (byte)0xF9; + public static final byte RECEIVE_HEARTRATE = (byte)0xFC; + + public static final byte ICON_CALL = 0; + public static final byte ICON_SMS = 1; + public static final byte ICON_WECHAT = 2; + public static final byte ICON_QQ = 3; + public static final byte ICON_FACEBOOK = 4; + public static final byte ICON_SKYPE = 5; + public static final byte ICON_TWITTER = 6; + public static final byte ICON_WHATSAPP = 7; + public static final byte ICON_LINE = 8; +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/TeclastH30Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/TeclastH30Coordinator.java new file mode 100644 index 000000000..e170b64c2 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/TeclastH30Coordinator.java @@ -0,0 +1,144 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.jyou; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.bluetooth.le.ScanFilter; +import android.content.Context; +import android.net.Uri; +import android.os.Build; +import android.os.ParcelUuid; +import android.support.annotation.NonNull; + +import de.greenrobot.dao.query.QueryBuilder; +import nodomain.freeyourgadget.gadgetbridge.GBException; +import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsActivity; +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator; +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.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.Collections; + +public class TeclastH30Coordinator extends AbstractDeviceCoordinator { + + protected static final Logger LOG = LoggerFactory.getLogger(TeclastH30Coordinator.class); + + @NonNull + @Override + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public Collection createBLEScanFilters() { + ParcelUuid uuid = new ParcelUuid(JYouConstants.UUID_SERVICE_JYOU); + ScanFilter filter = new ScanFilter.Builder().setServiceUuid(uuid).build(); + return Collections.singletonList(filter); + } + + @NonNull + @Override + public DeviceType getSupportedType(GBDeviceCandidate candidate) { + String name = candidate.getDevice().getName(); + if (name != null && name.startsWith("TECLAST_H30")) { + return DeviceType.TECLASTH30; + } + return DeviceType.UNKNOWN; + } + + @Override + public int getBondingStyle(GBDevice deviceCandidate){ + return BONDING_STYLE_NONE; + } + + @Override + public boolean supportsCalendarEvents() { + return false; + } + + @Override + public boolean supportsRealtimeData() { + return true; + } + + @Override + public DeviceType getDeviceType() { + return DeviceType.TECLASTH30; + } + + @Override + public Class getPairingActivity() { + return null; + } + + @Override + public Class getPrimaryActivity() { + return ChartsActivity.class; + } + + @Override + public InstallHandler findInstallHandler(Uri uri, Context context) { + return null; + } + + @Override + public boolean supportsActivityDataFetching() { + return false; + } + + @Override + public boolean supportsActivityTracking() { + return false; + } + + @Override + public SampleProvider getSampleProvider(GBDevice device, DaoSession session) { + return null; + } + + @Override + public boolean supportsScreenshots() { + return false; + } + + @Override + public boolean supportsAlarmConfiguration() { + return true; + } + + @Override + public boolean supportsSmartWakeup(GBDevice device) { + return false; + } + + @Override + public boolean supportsHeartRateMeasurement(GBDevice device) { + return true; + } + + @Override + public String getManufacturer() { + return "Teclast"; + } + + @Override + public boolean supportsAppsManagement() { + return false; + } + + @Override + public Class getAppsManagementActivity() { + return null; + } + + @Override + protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { + + } +} 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 e839955e0..651fdcb48 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 @@ -15,6 +15,9 @@ public final class No1F1Constants { 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_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_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 bb51683a6..4cd9dcb54 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 @@ -13,12 +13,15 @@ import android.support.annotation.Nullable; import java.util.Collection; import java.util.Collections; +import de.greenrobot.dao.query.QueryBuilder; import nodomain.freeyourgadget.gadgetbridge.GBException; +import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsActivity; import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator; 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.No1F1ActivitySampleDao; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; @@ -65,22 +68,22 @@ public class No1F1Coordinator extends AbstractDeviceCoordinator { @Nullable @Override public Class getPrimaryActivity() { - return null; + return ChartsActivity.class; } @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 No1F1SampleProvider(device, session); } @Override @@ -135,6 +138,8 @@ public class No1F1Coordinator extends AbstractDeviceCoordinator { @Override protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { - + Long deviceId = device.getId(); + QueryBuilder qb = session.getNo1F1ActivitySampleDao().queryBuilder(); + qb.where(No1F1ActivitySampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities(); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/no1f1/No1F1SampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/no1f1/No1F1SampleProvider.java new file mode 100644 index 000000000..e7a615fdb --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/no1f1/No1F1SampleProvider.java @@ -0,0 +1,72 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.no1f1; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; + +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.No1F1ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.No1F1ActivitySampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; + +public class No1F1SampleProvider extends AbstractSampleProvider { + + private GBDevice mDevice; + private DaoSession mSession; + + public No1F1SampleProvider(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 / (float) 8000.0; + } + + @Override + public No1F1ActivitySample createActivitySample() { + return new No1F1ActivitySample(); + } + + @Override + public AbstractDao getSampleDao() { + return getSession().getNo1F1ActivitySampleDao(); + } + + @Nullable + @Override + protected Property getRawKindSampleProperty() { + return No1F1ActivitySampleDao.Properties.RawKind; + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return No1F1ActivitySampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return No1F1ActivitySampleDao.Properties.DeviceId; + } +} 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 d543f913d..e55c1065d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -38,6 +38,7 @@ public enum DeviceType { HPLUS(40, R.drawable.ic_device_hplus, R.drawable.ic_device_hplus_disabled), MAKIBESF68(41, R.drawable.ic_device_hplus, R.drawable.ic_device_hplus_disabled), NO1F1(50, R.drawable.ic_device_hplus, R.drawable.ic_device_hplus_disabled), + TECLASTH30(60, R.drawable.ic_device_h30_h10, R.drawable.ic_device_h30_h10_disabled), TEST(1000, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled); private final int key; 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 f17a5eec8..d085de772 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java @@ -36,6 +36,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.no1f1.No1F1Support; import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.vibratissimo.VibratissimoSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.hplus.HPlusSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.TeclastH30Support; import nodomain.freeyourgadget.gadgetbridge.util.GB; public class DeviceSupportFactory { @@ -129,6 +130,9 @@ public class DeviceSupportFactory { case NO1F1: deviceSupport = new ServiceDeviceSupport(new No1F1Support(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); break; + case TECLASTH30: + deviceSupport = new ServiceDeviceSupport(new TeclastH30Support(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); + break; } if (deviceSupport != null) { deviceSupport.setContext(gbDevice, mBtAdapter, mContext); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/AmazfitBipSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/AmazfitBipSupport.java index 23b2f37bb..95d4288d8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/AmazfitBipSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/amazfitbip/AmazfitBipSupport.java @@ -30,6 +30,7 @@ import java.nio.ByteOrder; import java.util.SimpleTimeZone; import java.util.UUID; +import nodomain.freeyourgadget.gadgetbridge.Logging; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl; import nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip.AmazfitBipIcon; import nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip.AmazfitBipService; @@ -122,6 +123,7 @@ public class AmazfitBipSupport extends MiBand2Support { } else if (value[0] == 0x09) { callCmd.event = GBDeviceEventCallControl.Event.ACCEPT; } else { + LOG.info("Unhandled button press: " + Logging.formatBytes(value)); return; } evaluateGBDeviceEvent(callCmd); @@ -140,11 +142,13 @@ public class AmazfitBipSupport extends MiBand2Support { public void onSendWeather(WeatherSpec weatherSpec) { try { TransactionBuilder builder = performInitialized("Sending weather forecast"); - Version version = new Version(gbDevice.getFirmwareVersion()); - boolean supportsConditionString = false; - if (version.compareTo(new Version("0.0.8.74")) >= 0) { - supportsConditionString = true; + + if (gbDevice.getFirmwareVersion() != null) { + Version version = new Version(gbDevice.getFirmwareVersion()); + if (version.compareTo(new Version("0.0.8.74")) >= 0) { + supportsConditionString = true; + } } final byte NR_DAYS = 2; @@ -186,7 +190,8 @@ public class AmazfitBipSupport extends MiBand2Support { builder.write(getCharacteristic(AmazfitBipService.UUID_CHARACTERISTIC_WEATHER), buf.array()); builder.queue(getQueue()); - } catch (IOException ignore) { + } catch (Exception ex) { + LOG.error("Error sending weather information to the Bip", ex); } } 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/TeclastH30Support.java new file mode 100644 index 000000000..36406f6a0 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/TeclastH30Support.java @@ -0,0 +1,484 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.jyou; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.net.Uri; +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; +import nodomain.freeyourgadget.gadgetbridge.devices.jyou.JYouConstants; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.Alarm; +import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; +import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; +import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; +import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; +import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; + +public class TeclastH30Support extends AbstractBTLEDeviceSupport { + + private static final Logger LOG = LoggerFactory.getLogger(TeclastH30Support.class); + + public BluetoothGattCharacteristic ctrlCharacteristic = null; + public BluetoothGattCharacteristic measureCharacteristic = null; + + private final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo(); + private final GBDeviceEventBatteryInfo batteryCmd = new GBDeviceEventBatteryInfo(); + + public TeclastH30Support() { + super(LOG); + addSupportedService(JYouConstants.UUID_SERVICE_JYOU); + } + + @Override + protected TransactionBuilder initializeDevice(TransactionBuilder builder) { + LOG.info("Initializing"); + + gbDevice.setState(GBDevice.State.INITIALIZING); + gbDevice.sendDeviceUpdateIntent(getContext()); + + measureCharacteristic = getCharacteristic(JYouConstants.UUID_CHARACTERISTIC_MEASURE); + ctrlCharacteristic = getCharacteristic(JYouConstants.UUID_CHARACTERISTIC_CONTROL); + + builder.setGattCallback(this); + builder.notify(measureCharacteristic, true); + + syncSettings(builder); + + gbDevice.setState(GBDevice.State.INITIALIZED); + gbDevice.sendDeviceUpdateIntent(getContext()); + + LOG.info("Initialization Done"); + + return builder; + } + + @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; + } + } + + private 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)); + byte year2 = (byte)Integer.parseInt(strYear.substring(2, 4)); + byte month = (byte)cal.get(Calendar.MONTH); + byte day = (byte)cal.get(Calendar.DAY_OF_MONTH); + byte hour = (byte)cal.get(Calendar.HOUR_OF_DAY); + byte minute = (byte)cal.get(Calendar.MINUTE); + byte second = (byte)cal.get(Calendar.SECOND); + byte weekDay = (byte)cal.get(Calendar.DAY_OF_WEEK); + + builder.write(ctrlCharacteristic, commandWithChecksum( + JYouConstants.CMD_SET_DATE_AND_TIME, + (year1 << 24) | (year2 << 16) | (month << 8) | day, + (hour << 24) | (minute << 16) | (second << 8) | weekDay + )); + } + + 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) + )); + } + + private void showNotification(byte icon, String title, String message) { + try { + TransactionBuilder builder = performInitialized("ShowNotification"); + + byte[] titleBytes = stringToUTF8Bytes(title, 16); + byte[] messageBytes = stringToUTF8Bytes(message, 80); + + for (int i = 1; i <= 7; i++) + { + byte[] currentPacket = new byte[20]; + currentPacket[0] = JYouConstants.CMD_ACTION_SHOW_NOTIFICATION; + currentPacket[1] = 7; + currentPacket[2] = (byte)i; + switch(i) { + case 1: + currentPacket[4] = icon; + break; + case 2: + if (titleBytes != null) { + System.arraycopy(titleBytes, 0, currentPacket, 3, 6); + System.arraycopy(titleBytes, 6, currentPacket, 10, 10); + } + break; + default: + if (messageBytes != null) { + System.arraycopy(messageBytes, 16 * (i - 3), currentPacket, 3, 6); + System.arraycopy(messageBytes, 6 + 16 * (i - 3), currentPacket, 10, 10); + } + break; + } + builder.write(ctrlCharacteristic, currentPacket); + } + performConnected(builder.getTransaction()); + } catch (IOException e) { + LOG.warn(e.getMessage()); + } + } + + @Override + public boolean useAutoConnect() { + return true; + } + + @Override + public void onNotification(NotificationSpec notificationSpec) { + String notificationTitle = StringUtils.getFirstOf(notificationSpec.sender, notificationSpec.title); + byte icon; + switch (notificationSpec.type) { + case GENERIC_SMS: + icon = JYouConstants.ICON_SMS; + break; + case FACEBOOK: + case FACEBOOK_MESSENGER: + icon = JYouConstants.ICON_FACEBOOK; + break; + case TWITTER: + icon = JYouConstants.ICON_TWITTER; + break; + case WHATSAPP: + icon = JYouConstants.ICON_WHATSAPP; + break; + default: + icon = JYouConstants.ICON_LINE; + break; + } + showNotification(icon, notificationTitle, notificationSpec.body); + } + + @Override + public void onDeleteNotification(int id) { + + } + + @Override + public void onSetAlarms(ArrayList alarms) { + try { + TransactionBuilder builder = performInitialized("SetAlarms"); + + for (int i = 0; i < alarms.size(); i++) + { + byte cmd; + switch (i) { + case 0: + cmd = JYouConstants.CMD_SET_ALARM_1; + break; + case 1: + cmd = JYouConstants.CMD_SET_ALARM_2; + break; + case 2: + cmd = JYouConstants.CMD_SET_ALARM_3; + break; + default: + return; + } + Calendar cal = alarms.get(i).getAlarmCal(); + builder.write(ctrlCharacteristic, commandWithChecksum( + cmd, + alarms.get(i).isEnabled() ? cal.get(Calendar.HOUR_OF_DAY) : -1, + alarms.get(i).isEnabled() ? cal.get(Calendar.MINUTE) : -1 + )); + } + 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()); + } + } + + @Override + public void onSetTime() { + try { + TransactionBuilder builder = performInitialized("SetTime"); + syncDateAndTime(builder); + performConnected(builder.getTransaction()); + } catch(IOException e) { + LOG.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; + } + } + + @Override + public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) { + + } + + @Override + public void onSetMusicState(MusicStateSpec stateSpec) { + + } + + @Override + public void onSetMusicInfo(MusicSpec musicSpec) { + + } + + @Override + public void onEnableRealtimeSteps(boolean enable) { + onEnableRealtimeHeartRateMeasurement(enable); + } + + @Override + public void onInstallApp(Uri uri) { + + } + + @Override + public void onAppInfoReq() { + + } + + @Override + public void onAppStart(UUID uuid, boolean start) { + + } + + @Override + public void onAppDelete(UUID uuid) { + + } + + @Override + public void onAppConfiguration(UUID appUuid, String config) { + + } + + @Override + public void onAppReorder(UUID[] uuids) { + + } + + @Override + public void onFetchActivityData() { + + } + + @Override + public void onReboot() { + try { + TransactionBuilder builder = performInitialized("Reboot"); + builder.write(ctrlCharacteristic, commandWithChecksum( + JYouConstants.CMD_ACTION_REBOOT_DEVICE, 0, 0 + )); + performConnected(builder.getTransaction()); + } catch(Exception e) { + LOG.warn(e.getMessage()); + } + } + + @Override + public void onHeartRateTest() { + try { + TransactionBuilder builder = performInitialized("HeartRateTest"); + builder.write(ctrlCharacteristic, commandWithChecksum( + JYouConstants.CMD_ACTION_HEARTRATE_SWITCH, 0, 1 + )); + performConnected(builder.getTransaction()); + } catch(Exception e) { + LOG.warn(e.getMessage()); + } + } + + @Override + public void onEnableRealtimeHeartRateMeasurement(boolean enable) { + // TODO: test + try { + TransactionBuilder builder = performInitialized("RealTimeHeartMeasurement"); + builder.write(ctrlCharacteristic, commandWithChecksum( + JYouConstants.CMD_SET_HEARTRATE_AUTO, 0, enable ? 1 : 0 + )); + performConnected(builder.getTransaction()); + } catch(Exception e) { + LOG.warn(e.getMessage()); + } + } + + @Override + public void onFindDevice(boolean start) { + if (start) { + showNotification(JYouConstants.ICON_QQ, "Gadgetbridge", "Bzzt! Bzzt!"); + GB.toast(getContext(), "As your device doesn't have sound, it will only vibrate 3 times consecutively", Toast.LENGTH_LONG, GB.INFO); + } + } + + @Override + public void onSetConstantVibration(int integer) { + + } + + @Override + public void onScreenshotReq() { + + } + + @Override + public void onEnableHeartRateSleepSupport(boolean enable) { + + } + + @Override + public void onAddCalendarEvent(CalendarEventSpec calendarEventSpec) { + + } + + @Override + public void onDeleteCalendarEvent(byte type, long id) { + + } + + @Override + public void onSendConfiguration(String config) { + + } + + @Override + public void onTestNewFunction() { + + } + + @Override + public void onSendWeather(WeatherSpec weatherSpec) { + + } + + private byte[] commandWithChecksum(byte cmd, int argSlot1, int argSlot2) + { + ByteBuffer buf = ByteBuffer.allocate(10); + buf.put(cmd); + buf.putInt(argSlot1); + buf.putInt(argSlot2); + + byte[] bytesToWrite = buf.array(); + + byte checksum = 0; + for (byte b : bytesToWrite) { + checksum += b; + } + + bytesToWrite[9] = checksum; + + return bytesToWrite; + } + + private byte[] stringToUTF8Bytes(String src, int byteCount) { + try { + if (src == null) + return null; + + for (int i = src.length(); i > 0; i--) { + String sub = src.substring(0, i); + byte[] subUTF8 = sub.getBytes("UTF-8"); + + if (subUTF8.length == byteCount) { + return subUTF8; + } + + if (subUTF8.length < byteCount) { + byte[] largerSubUTF8 = new byte[byteCount]; + System.arraycopy(subUTF8, 0, largerSubUTF8, 0, subUTF8.length); + return largerSubUTF8; + } + } + } catch (UnsupportedEncodingException e) { + LOG.warn(e.getMessage()); + } + return null; + } +} 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 f8f3d9a80..85c99041c 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 @@ -3,6 +3,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.no1f1; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; import android.net.Uri; +import android.widget.Toast; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -12,12 +13,20 @@ 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.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.no1f1.No1F1Constants; +import nodomain.freeyourgadget.gadgetbridge.devices.no1f1.No1F1SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.No1F1ActivitySample; 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; @@ -29,6 +38,8 @@ import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceBusyAction; +import nodomain.freeyourgadget.gadgetbridge.util.GB; import static org.apache.commons.lang3.math.NumberUtils.min; @@ -38,6 +49,7 @@ public class No1F1Support extends AbstractBTLEDeviceSupport { private final GBDeviceEventBatteryInfo batteryCmd = new GBDeviceEventBatteryInfo(); public BluetoothGattCharacteristic ctrlCharacteristic = null; public BluetoothGattCharacteristic measureCharacteristic = null; + private List samples = new ArrayList<>(); public No1F1Support() { super(LOG); @@ -99,6 +111,9 @@ public class No1F1Support extends AbstractBTLEDeviceSupport { case No1F1Constants.CMD_USER_DATA: LOG.info("User data updated"); return true; + case No1F1Constants.CMD_FETCH_STEPS: + handleStepData(data); + return true; case No1F1Constants.CMD_NOTIFICATION: case No1F1Constants.CMD_ICON: case No1F1Constants.CMD_DEVICE_SETTINGS: @@ -201,22 +216,25 @@ public class No1F1Support extends AbstractBTLEDeviceSupport { @Override public void onFetchActivityData() { - - } - - @Override - public void onReboot() { try { - TransactionBuilder builder = performInitialized("clearNotification"); + samples.clear(); + TransactionBuilder builder = performInitialized("fetchSteps"); + builder.add(new SetDeviceBusyAction(getDevice(), getContext().getString(R.string.busy_task_fetch_activity_data), getContext())); byte[] msg = new byte[]{ - (byte) 0xad + No1F1Constants.CMD_FETCH_STEPS, + (byte) 0xfa }; builder.write(ctrlCharacteristic, msg); performConnected(builder.getTransaction()); } catch (IOException e) { + GB.toast(getContext(), "Error fetching activity data: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); } } + @Override + public void onReboot() { + } + @Override public void onHeartRateTest() { @@ -371,6 +389,7 @@ public class No1F1Support extends AbstractBTLEDeviceSupport { builder.write(ctrlCharacteristic, msg); performConnected(builder.getTransaction()); } catch (IOException e) { + LOG.warn("Unable to set vibration", e); } } @@ -384,6 +403,7 @@ public class No1F1Support extends AbstractBTLEDeviceSupport { builder.write(ctrlCharacteristic, msg); performConnected(builder.getTransaction()); } catch (IOException e) { + GB.toast(getContext(), "Error showing icon: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); } } @@ -415,6 +435,7 @@ public class No1F1Support extends AbstractBTLEDeviceSupport { performConnected(builder.getTransaction()); } catch (IOException e) { + GB.toast(getContext(), "Error showing notificaton: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); } } @@ -428,6 +449,53 @@ public class No1F1Support extends AbstractBTLEDeviceSupport { builder.write(ctrlCharacteristic, msg); performConnected(builder.getTransaction()); } catch (IOException e) { + LOG.warn("Unable to stop notification", e); + } + } + + private void handleStepData(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); + samples.get(i).setRawKind(ActivityKind.TYPE_ACTIVITY); + samples.get(i).setRawIntensity(samples.get(i).getSteps()); + provider.addGBActivitySample(samples.get(i)); + } + samples.clear(); + LOG.info("Steps data saved"); + if (getDevice().isBusy()) { + getDevice().unsetBusyTask(); + getDevice().sendDeviceUpdateIntent(getContext()); + } + } catch (Exception ex) { + GB.toast(getContext(), "Error saving step 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, 0); + timestamp.set(Calendar.SECOND, 0); + + sample.setTimestamp((int) (timestamp.getTimeInMillis() / 1000L)); + sample.setSteps(data[6] * 256 + (data[7] & 0xff)); + + samples.add(sample); + LOG.info("Received steps data for " + String.format("%1$TD %1$TT", timestamp) + ": " + + sample.getSteps() + " steps" + ); } } } 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 e3241d6a6..ab5f8c812 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java @@ -42,6 +42,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.UnknownDeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip.AmazfitBipCooordinator; import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.hplus.MakibesF68Coordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.jyou.TeclastH30Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.liveview.LiveviewCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; @@ -195,6 +196,7 @@ public class DeviceHelper { result.add(new HPlusCoordinator()); result.add(new No1F1Coordinator()); result.add(new MakibesF68Coordinator()); + result.add(new TeclastH30Coordinator()); return result; } diff --git a/app/src/main/res/drawable-hdpi/ic_device_h30_h10.png b/app/src/main/res/drawable-hdpi/ic_device_h30_h10.png new file mode 100644 index 000000000..4a599d7b8 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_device_h30_h10.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_device_h30_h10_disabled.png b/app/src/main/res/drawable-hdpi/ic_device_h30_h10_disabled.png new file mode 100644 index 000000000..f3da742ca Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_device_h30_h10_disabled.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_device_h30_h10.png b/app/src/main/res/drawable-mdpi/ic_device_h30_h10.png new file mode 100644 index 000000000..729cbd624 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_device_h30_h10.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_device_h30_h10_disabled.png b/app/src/main/res/drawable-mdpi/ic_device_h30_h10_disabled.png new file mode 100644 index 000000000..72840f433 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_device_h30_h10_disabled.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_device_h30_h10.png b/app/src/main/res/drawable-xhdpi/ic_device_h30_h10.png new file mode 100644 index 000000000..5ce9b58e4 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_device_h30_h10.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_device_h30_h10_disabled.png b/app/src/main/res/drawable-xhdpi/ic_device_h30_h10_disabled.png new file mode 100644 index 000000000..a3a55f5c4 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_device_h30_h10_disabled.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_device_h30_h10.png b/app/src/main/res/drawable-xxhdpi/ic_device_h30_h10.png new file mode 100644 index 000000000..08f72ddbe Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_device_h30_h10.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_device_h30_h10_disabled.png b/app/src/main/res/drawable-xxhdpi/ic_device_h30_h10_disabled.png new file mode 100644 index 000000000..2d1d4eff1 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_device_h30_h10_disabled.png differ diff --git a/app/src/main/res/drawable/level_list_device.xml b/app/src/main/res/drawable/level_list_device.xml index eb68abe6e..5e67a7675 100644 --- a/app/src/main/res/drawable/level_list_device.xml +++ b/app/src/main/res/drawable/level_list_device.xml @@ -10,6 +10,7 @@ + @@ -21,5 +22,6 @@ + \ No newline at end of file