diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/DataType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/DataType.java new file mode 100644 index 000000000..ce4d4cc73 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/DataType.java @@ -0,0 +1,31 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.lenovo; + +public enum DataType { + STEPS(new byte[]{0x00, 0x00}), + SLEEP(new byte[]{0x00, 0x01}), + HEART_RATE(new byte[]{0x00, 0x02}), + BLOOD_PRESSURE(new byte[]{0x00, 0x06}), + INFRARED_TEMPERATURE(new byte[]{0x00, 0x08}), + ENVIRONMENT_TEMPERATURE(new byte[]{0x00, 0x09}), + AIR_PRESSURE(new byte[]{0x00, 0x0A}); + + private byte[] value; + + DataType(byte[] value) { + this.value = value; + } + + public byte[] getValue() { + return value; + } + + public static DataType getType(int value) { + for(DataType type : values()) { + int intVal = (type.getValue()[1] & 0xff) | ((type.getValue()[0] & 0xff) << 8); + if(intVal == value) { + return type; + } + } + throw new RuntimeException("No value defined for " + value); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/watchxplus/WatchXPlusConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/watchxplus/WatchXPlusConstants.java index 4bf09b94d..0a08fb10d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/watchxplus/WatchXPlusConstants.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/watchxplus/WatchXPlusConstants.java @@ -26,7 +26,7 @@ public final class WatchXPlusConstants extends LenovoWatchConstants { public static final UUID UUID_UNKNOWN_DESCRIPTOR = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); public static final UUID UUID_CHARACTERISTIC_WRITE = UUID.fromString("0000a801-0000-1000-8000-00805f9b34fb"); - public static final UUID UUID_CHARACTERISTIC_UNKNOWN_2 = UUID.fromString("0000a802-0000-1000-8000-00805f9b34fb"); + public static final UUID UUID_CHARACTERISTIC_DATABASE_READ = UUID.fromString("0000a802-0000-1000-8000-00805f9b34fb"); public static final UUID UUID_CHARACTERISTIC_UNKNOWN_3 = UUID.fromString("0000a803-0000-1000-8000-00805f9b34fb"); public static final UUID UUID_CHARACTERISTIC_UNKNOWN_4 = UUID.fromString("0000a804-0000-1000-8000-00805f9b34fb"); @@ -37,9 +37,10 @@ public final class WatchXPlusConstants extends LenovoWatchConstants { public static final byte[] CMD_RETRIEVE_DATA_COUNT = new byte[]{(byte)0xF0, 0x10}; public static final byte[] CMD_RETRIEVE_DATA_DETAILS = new byte[]{(byte)0xF0, 0x11}; public static final byte[] CMD_RETRIEVE_DATA_CONTENT = new byte[]{(byte)0xF0, 0x12}; - public static final byte[] HEART_RATE_DATA_TYPE = new byte[]{0x00, 0x02}; + public static final byte[] CMD_REMOVE_DATA_CONTENT = new byte[]{(byte)0xF0, 0x32}; public static final byte[] CMD_BLOOD_PRESSURE_MEASURE = new byte[]{0x05, 0x0D}; + public static final byte[] CMD_NOTIFICATION_TEXT_TASK = new byte[]{0x03, 0x06}; public static final byte[] CMD_NOTIFICATION_SETTINGS = new byte[]{0x03, 0x02}; public static final byte[] CMD_DO_NOT_DISTURB_SETTINGS = new byte[]{0x03, 0x61}; @@ -51,9 +52,10 @@ public final class WatchXPlusConstants extends LenovoWatchConstants { public static final byte[] RESP_DAY_STEPS_INDICATOR = new byte[]{0x08, 0x10, 0x03}; public static final byte[] RESP_HEARTRATE = new byte[]{-0x80, 0x15, 0x03}; - public static final byte[] RESP_HEART_RATE_DATA_COUNT = new byte[]{0x08, (byte)0xF0, 0x10}; - public static final byte[] RESP_HEART_RATE_DATA_DETAILS = new byte[]{0x08, (byte)0xF0, 0x11}; - public static final byte[] RESP_HEART_RATE_DATA_CONTENT = new byte[]{0x08, (byte)0xF0, 0x12}; + public static final byte[] RESP_DATA_COUNT = new byte[]{0x08, (byte)0xF0, 0x10}; + public static final byte[] RESP_DATA_DETAILS = new byte[]{0x08, (byte)0xF0, 0x11}; + public static final byte[] RESP_DATA_CONTENT = new byte[]{0x08, (byte)0xF0, 0x12}; + public static final byte[] RESP_DATA_CONTENT_REMOVE = new byte[]{-0x80, (byte)0xF0, 0x32}; public static final byte[] RESP_BP_MEASURE_STARTED = new byte[]{0x08, 0x05, 0x0D}; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lenovo/operations/InitOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lenovo/operations/InitOperation.java index c83cadbee..c161e7f50 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lenovo/operations/InitOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lenovo/operations/InitOperation.java @@ -26,6 +26,7 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.UUID; +import nodomain.freeyourgadget.gadgetbridge.devices.lenovo.watchxplus.WatchXPlusConstants; import nodomain.freeyourgadget.gadgetbridge.devices.watch9.Watch9Constants; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEOperation; @@ -41,7 +42,8 @@ public class InitOperation extends AbstractBTLEOperation heartRateDataToFetch = new ArrayList<>(); - private int requestedHeartRateTimestamp; - private int heartRateDataSlots; + private Map dataToFetch = new LinkedHashMap<>(); + private int requestedDataTimestamp; + private Map dataSlots = new HashMap<>(); + private DataType currentDataType; private byte ACK_CALIBRATION = 0; @@ -502,7 +507,7 @@ public class WatchXPlusDeviceSupport extends AbstractBTLEDeviceSupport { WatchXPlusConstants.READ_VALUE)); // Fetch heart rate data samples count - requestHeartRateDataCount(builder); + requestDataCount(builder, DataType.HEART_RATE); builder.queue(getQueue()); } catch (IOException e) { @@ -631,11 +636,9 @@ public class WatchXPlusDeviceSupport extends AbstractBTLEDeviceSupport { super.onCharacteristicChanged(gatt, characteristic); UUID characteristicUUID = characteristic.getUuid(); + byte[] value = characteristic.getValue(); if (WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE.equals(characteristicUUID)) { - byte[] value = characteristic.getValue(); - if (value[0] != 0x23) { - handleHeartRateContentDataChunk(value); - } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_FIRMWARE_INFO, 5)) { + if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_FIRMWARE_INFO, 5)) { handleFirmwareInfo(value); } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_BATTERY_INFO, 5)) { handleBatteryState(value); @@ -651,17 +654,19 @@ public class WatchXPlusDeviceSupport extends AbstractBTLEDeviceSupport { isCalibrationActive = false; } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_DAY_STEPS_INDICATOR, 5)) { handleStepsInfo(value); - } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_HEART_RATE_DATA_COUNT, 5)) { - LOG.info(" Received Heart rate data count"); - handleHeartRateDataCount(value); - } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_HEART_RATE_DATA_DETAILS, 5)) { - LOG.info(" Received Heart rate data details"); - handleHeartRateDetails(value); - } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_HEART_RATE_DATA_CONTENT, 5)) { - LOG.info(" Received Heart rate data content"); - handleHeartRateContentAck(value); + } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_DATA_COUNT, 5)) { + LOG.info(" Received data count"); + handleDataCount(value); + } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_DATA_DETAILS, 5)) { + LOG.info(" Received data details"); + handleDataDetails(value); + } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_DATA_CONTENT, 5)) { + LOG.info(" Received data content"); + handleDataContentAck(value); } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_BP_MEASURE_STARTED, 5)) { handleBpMeasureResult(value); + } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_DATA_CONTENT_REMOVE, 5)) { + handleDataContentRemove(value); } else if (value.length == 7 && value[5] == 0) { LOG.info(" Received ACK"); // Not sure if that's necessary. There is no response for ACK in original app logs @@ -673,6 +678,10 @@ public class WatchXPlusDeviceSupport extends AbstractBTLEDeviceSupport { logMessageContent(characteristic.getValue()); } + return true; + } else if (WatchXPlusConstants.UUID_CHARACTERISTIC_DATABASE_READ.equals(characteristicUUID)) { + + handleContentDataChunk(value); return true; } else { LOG.info(" Unhandled characteristic changed: " + characteristicUUID); @@ -682,44 +691,59 @@ public class WatchXPlusDeviceSupport extends AbstractBTLEDeviceSupport { return false; } - /** - * Heart rate history retrieve flow: - * 1. Request for heart rate data slots count. CMD_RETRIEVE_DATA_COUNT, {@link WatchXPlusDeviceSupport#requestHeartRateDataCount} - * 2. Extract data count from response. RESP_HEART_RATE_DATA_COUNT, {@link WatchXPlusDeviceSupport#handleHeartRateDataCount} - * 3. Request for N data slot details. CMD_RETRIEVE_DATA_DETAILS, {@link WatchXPlusDeviceSupport#requestHeartRateDetails} - * 4. Timestamp of slot is returned, save it for later use. RESP_HEART_RATE_DATA_DETAILS, {@link WatchXPlusDeviceSupport#handleHeartRateDetails} - * 5. Repeat step 3-4 until all slots details retrieved. - * 6. Request for M data content by timestamp. CMD_RETRIEVE_DATA_CONTENT, {@link WatchXPlusDeviceSupport#requestHeartRateContentForTimestamp} - * 7. Receive kind of pre-flight response. RESP_HEART_RATE_DATA_CONTENT, {@link WatchXPlusDeviceSupport#handleHeartRateContentAck} - * 8. Receive frames with content. They are different than other frames, {@link WatchXPlusDeviceSupport#handleHeartRateContentDataChunk} - * ie. 0000000255-4F4C48-434241434444454648474747, 0001000247-474645-434240FFFFFFFFFFFFFFFFFF - */ - private void requestHeartRateDataCount(TransactionBuilder builder) { - builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), - buildCommand(WatchXPlusConstants.CMD_RETRIEVE_DATA_COUNT, - WatchXPlusConstants.READ_VALUE, - WatchXPlusConstants.HEART_RATE_DATA_TYPE)); - } - - private void handleHeartRateDataCount(byte[] value) { - - int dataCount = Conversion.fromByteArr16(value[10], value[11]); - LOG.info("Watch contains " + dataCount + " heart rate entries"); - this.heartRateDataSlots = dataCount; - heartRateDataToFetch.clear(); - if (dataCount != 0) { - requestHeartRateDetails(heartRateDataToFetch.size()); + private void handleDataContentRemove(byte[] value) { + int dataType = Conversion.fromByteArr16(value[8], value[9]); + int timestamp = Conversion.fromByteArr16(value[10], value[11], value[12], value[13]); + int removed = value[14]; + DataType type = DataType.getType(dataType); + if( removed == 0) { + LOG.info(" Removed " + type + " data for timestamp " + timestamp); + } else { + LOG.info(" Unsuccessful removal of " + type + " data for timestamp " + timestamp); } } - private void requestHeartRateDetails(int i) { + /** + * Heart rate history retrieve flow: + * 1. Request for heart rate data slots count. CMD_RETRIEVE_DATA_COUNT, {@link WatchXPlusDeviceSupport#requestDataCount} + * 2. Extract data count from response. RESP_DATA_COUNT, {@link WatchXPlusDeviceSupport#handleDataCount} + * 3. Request for N data slot details. CMD_RETRIEVE_DATA_DETAILS, {@link WatchXPlusDeviceSupport#requestDataDetails} + * 4. Timestamp of slot is returned, save it for later use. RESP_DATA_DETAILS, {@link WatchXPlusDeviceSupport#handleDataDetails} + * 5. Repeat step 3-4 until all slots details retrieved. + * 6. Request for M data content by timestamp. CMD_RETRIEVE_DATA_CONTENT, {@link WatchXPlusDeviceSupport#requestDataContentForTimestamp} + * 7. Receive kind of pre-flight response. RESP_DATA_CONTENT, {@link WatchXPlusDeviceSupport#handleDataContentAck} + * 8. Receive frames with content. They are different than other frames, {@link WatchXPlusDeviceSupport#handleContentDataChunk} + * ie. 0000000255-4F4C48-434241434444454648474747, 0001000247-474645-434240FFFFFFFFFFFFFFFFFF + */ + private void requestDataCount(TransactionBuilder builder, DataType dataType) { + builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), + buildCommand(WatchXPlusConstants.CMD_RETRIEVE_DATA_COUNT, + WatchXPlusConstants.READ_VALUE, + dataType.getValue())); + } + + private void handleDataCount(byte[] value) { + + int dataType = Conversion.fromByteArr16(value[8], value[9]); + int dataCount = Conversion.fromByteArr16(value[10], value[11]); + + DataType type = DataType.getType(dataType); + LOG.info("Watch contains " + dataCount + " " + type + " entries"); + dataSlots.put(type, dataCount); + dataToFetch.clear(); + if (dataCount != 0) { + requestDataDetails(dataToFetch.size(), type); + } + } + + private void requestDataDetails(int i, DataType dataType) { + LOG.info(" Requesting " + dataType + " details"); try { - TransactionBuilder builder = performInitialized("requestHeartRate"); - byte[] heartRateDataType = WatchXPlusConstants.HEART_RATE_DATA_TYPE; + TransactionBuilder builder = performInitialized("requestDataDetails"); byte[] index = Conversion.toByteArr16(i); - byte[] req = BLETypeConversions.join(heartRateDataType, index); - + byte[] req = BLETypeConversions.join(dataType.getValue(), index); + currentDataType = dataType; builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(WatchXPlusConstants.CMD_RETRIEVE_DATA_DETAILS, WatchXPlusConstants.READ_VALUE, @@ -727,12 +751,12 @@ public class WatchXPlusDeviceSupport extends AbstractBTLEDeviceSupport { builder.queue(getQueue()); } catch (IOException e) { - LOG.warn("Unable to request data", e); + LOG.warn("Unable to request data details", e); } } - private void handleHeartRateDetails(byte[] value) { - LOG.info("Got Heart rate details"); + private void handleDataDetails(byte[] value) { + LOG.info("Got data details"); int timestamp = Conversion.fromByteArr16(value[8], value[9], value[10], value[11]); int dataLength = Conversion.fromByteArr16(value[12], value[13]); int samplingInterval = (int) onSamplingInterval(value[14] >> 4, Conversion.fromByteArr16((byte) (value[14] & 15), value[15])); @@ -749,63 +773,105 @@ public class WatchXPlusDeviceSupport extends AbstractBTLEDeviceSupport { LOG.info("mtu (mtu): " + mtu); LOG.info("parts: " + parts); - heartRateDataToFetch.add(timestamp); + dataToFetch.put(timestamp, parts); - if (heartRateDataToFetch.size() == heartRateDataSlots) { - requestHeartRateContentForTimestamp(heartRateDataToFetch.get(0)); + if (dataToFetch.size() == dataSlots.get(currentDataType)) { + Map.Entry currentValue = dataToFetch.entrySet().iterator().next(); + requestedDataTimestamp = currentValue.getKey(); + requestDataContentForTimestamp(requestedDataTimestamp, currentDataType); } else { - requestHeartRateDetails(heartRateDataToFetch.size()); + requestDataDetails(dataToFetch.size(), currentDataType); } } - private void requestHeartRateContentForTimestamp(int timestamp) { - byte[] heartRateDataType = WatchXPlusConstants.HEART_RATE_DATA_TYPE; + private void requestDataContentForTimestamp(int timestamp, DataType dataType) { byte[] command = WatchXPlusConstants.CMD_RETRIEVE_DATA_CONTENT; try { - TransactionBuilder builder = performInitialized("content"); + TransactionBuilder builder = performInitialized("requestDataContentForTimestamp"); byte[] ts = Conversion.toByteArr32(timestamp); - byte[] req = BLETypeConversions.join(heartRateDataType, ts); + byte[] req = BLETypeConversions.join(dataType.getValue(), ts); req = BLETypeConversions.join(req, Conversion.toByteArr16(0)); - requestedHeartRateTimestamp = timestamp; + requestedDataTimestamp = timestamp; builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(command, WatchXPlusConstants.READ_VALUE, req)); builder.queue(getQueue()); } catch (IOException e) { - LOG.warn("Unable to request heart rate content", e); + LOG.warn("Unable to request data content", e); } } + private void removeDataContentForTimestamp(int timestamp, DataType dataType) { + byte[] command = WatchXPlusConstants.CMD_REMOVE_DATA_CONTENT; - private void handleHeartRateContentAck(byte[] value) { - LOG.info(" Received heart rate data content start"); + try { + TransactionBuilder builder = performInitialized("removeDataContentForTimestamp"); + byte[] ts = Conversion.toByteArr32(timestamp); + byte[] req = BLETypeConversions.join(dataType.getValue(), ts); + builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), + buildCommand(command, + WatchXPlusConstants.TASK, + req)); + builder.queue(getQueue()); + } catch (IOException e) { + LOG.warn("Unable to remove data content", e); + } } - private void handleHeartRateContentDataChunk(byte[] value) { + private void handleDataContentAck(byte[] value) { + LOG.info(" Received data content start"); +// To verify: Chunks are sent if value[8] == 0, if value[8] == 1 they are not sent by watch + } + + private void handleContentDataChunk(byte[] value) { int chunkNo = Conversion.fromByteArr16(value[0], value[1]); - int dataType = Conversion.fromByteArr16(value[2], value[2]); - int timezoneOffset = TimeZone.getDefault().getOffset(System.currentTimeMillis()); - if (dataType != 2) { - LOG.warn(" Got unsupported data package type: " + dataType); - } else { - for (int i = 4; i < value.length; i++) { + int dataType = Conversion.fromByteArr16(value[2], value[3]); + int timezoneOffset = TimeZone.getDefault().getOffset(System.currentTimeMillis())/1000; + DataType type = DataType.getType(dataType); + if (type == DataType.HEART_RATE) { + try (DBHandler dbHandler = GBApplication.acquireDB()) { + WatchXPlusSampleProvider provider = new WatchXPlusSampleProvider(getDevice(), dbHandler.getDaoSession()); + List samples = new ArrayList<>(); - int val = Conversion.fromByteArr16(value[i]); - if (255 == val) { - break; + for (int i = 4; i < value.length; i++) { + + int val = Conversion.fromByteArr16(value[i]); + if (255 == val) { + break; + } + int tsWithOffset = requestedDataTimestamp + (((((chunkNo * 16) + i) - 4) * 2) * 60) - timezoneOffset; +// LOG.debug(" requested timestamp " + requestedDataTimestamp + " chunkNo " + chunkNo + " Got data: " + new Date((long) tsWithOffset * 1000) + ", value: " + val); + WatchXPlusActivitySample sample = createSample(dbHandler, tsWithOffset); + sample.setTimestamp(tsWithOffset); + sample.setHeartRate(val); + sample.setProvider(provider); + sample.setRawKind(ActivityKind.TYPE_ACTIVITY); + samples.add(sample); } - int tsWithOffset = requestedHeartRateTimestamp + (((((chunkNo * 16) + i) - 4) * 2) * 60) - timezoneOffset; - LOG.info(" Got HR data: " + new Date(tsWithOffset) + ", value: " + val); + provider.addGBActivitySamples(samples.toArray(new WatchXPlusActivitySample[0])); + } catch (GBException ex) { + LOG.info((ex.getMessage())); + } catch (Exception ex) { + LOG.info(ex.getMessage()); } - heartRateDataToFetch.remove(0); - if (!heartRateDataToFetch.isEmpty()) { - requestHeartRateContentForTimestamp(heartRateDataToFetch.get(0)); - } else { - heartRateDataSlots = 0; + if(!dataToFetch.isEmpty() && chunkNo == dataToFetch.get(requestedDataTimestamp) - 1) { + dataToFetch.remove(requestedDataTimestamp); + removeDataContentForTimestamp(requestedDataTimestamp, currentDataType); + if (!dataToFetch.isEmpty()) { + Map.Entry currentValue = dataToFetch.entrySet().iterator().next(); + requestedDataTimestamp = currentValue.getKey(); + requestDataContentForTimestamp(requestedDataTimestamp, type); + } else { + dataSlots.put(type,0); + } + } else if (dataToFetch.isEmpty()) { + dataSlots.put(type,0); } + } else { + LOG.warn(" Got unsupported data package type: " + type); } } @@ -872,6 +938,7 @@ public class WatchXPlusDeviceSupport extends AbstractBTLEDeviceSupport { WatchXPlusActivitySample sample = createSample(dbHandler, timestamp); sample.setTimestamp(timestamp); // sample.setRawKind(record.type); + sample.setRawKind(ActivityKind.TYPE_ACTIVITY); sample.setSteps(newSteps); // sample.setDistance(record.distance); // sample.setCalories(record.calories);