1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-26 03:46:49 +01:00

Display calibration button for Watch X Plus

Use calibration activity from Watch9 for now
Partial heart rate history handling (WIP)
This commit is contained in:
mkusnierz 2019-10-20 10:34:29 +02:00
parent 34d9bccb86
commit 4601d827ae
3 changed files with 112 additions and 26 deletions

View File

@ -59,6 +59,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSett
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.devices.watch9.Watch9CalibrationActivity; import nodomain.freeyourgadget.gadgetbridge.devices.watch9.Watch9CalibrationActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.watchxplus.WatchXPlusCalibrationActivity;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState; import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
@ -320,11 +321,17 @@ public class GBDeviceAdapterv2 extends RecyclerView.Adapter<GBDeviceAdapterv2.Vi
); );
holder.calibrateDevice.setVisibility(device.isInitialized() && device.getType() == DeviceType.WATCH9 ? View.VISIBLE : View.GONE); holder.calibrateDevice.setVisibility(device.isInitialized() && (device.getType() == DeviceType.WATCH9 || device.getType() == DeviceType.WATCHXPLUS) ? View.VISIBLE : View.GONE);
holder.calibrateDevice.setOnClickListener(new View.OnClickListener() { holder.calibrateDevice.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
Intent startIntent = new Intent(context, Watch9CalibrationActivity.class); Intent startIntent;
if(device.getType() == DeviceType.WATCHXPLUS) {
startIntent = new Intent(context, WatchXPlusCalibrationActivity.class);
} else {
startIntent = new Intent(context, Watch9CalibrationActivity.class);
}
startIntent.putExtra(GBDevice.EXTRA_DEVICE, device); startIntent.putExtra(GBDevice.EXTRA_DEVICE, device);
context.startActivity(startIntent); context.startActivity(startIntent);
} }

View File

@ -56,7 +56,9 @@ public final class WatchXPlusConstants {
public static final byte[] CMD_ALARM_SETTINGS = new byte[]{0x01, 0x0A}; public static final byte[] CMD_ALARM_SETTINGS = new byte[]{0x01, 0x0A};
public static final byte[] CMD_WEATHER_SET = new byte[]{0x01, 0x10}; public static final byte[] CMD_WEATHER_SET = new byte[]{0x01, 0x10};
public static final byte[] CMD_BATTERY_INFO = new byte[]{0x01, 0x14}; public static final byte[] CMD_BATTERY_INFO = new byte[]{0x01, 0x14};
public static final byte[] CMD_HEARTRATE_INFO = new byte[]{0x15, 0x03}; public static final byte[] CMD_RETRIEVE_DATA = new byte[]{(byte)0xF0, 0x10};
public static final byte[] CMD_RETRIEVE_DATA_DETAILS = new byte[]{(byte)0xF0, 0x11};
public static final byte[] HEART_RATE_DATA_TYPE = new byte[]{0x00, 0x02};
public static final byte[] CMD_NOTIFICATION_TASK = new byte[]{0x03, 0x01}; public static final byte[] CMD_NOTIFICATION_TASK = new byte[]{0x03, 0x01};
public static final byte[] CMD_NOTIFICATION_TEXT_TASK = new byte[]{0x03, 0x06}; public static final byte[] CMD_NOTIFICATION_TEXT_TASK = new byte[]{0x03, 0x06};
@ -73,12 +75,14 @@ public final class WatchXPlusConstants {
public static final byte[] RESP_BUTTON_INDICATOR = new byte[]{0x04, 0x03, 0x11}; public static final byte[] RESP_BUTTON_INDICATOR = new byte[]{0x04, 0x03, 0x11};
public static final byte[] RESP_ALARM_INDICATOR = new byte[]{-0x80, 0x01, 0x0A}; public static final byte[] RESP_ALARM_INDICATOR = new byte[]{-0x80, 0x01, 0x0A};
public static final byte[] RESP_DAY_STEPS_INDICATOR = new byte[]{0x08, 0x10, 0x03}; public static final byte[] RESP_DAY_STEPS_INDICATOR = new byte[]{0x08, 0x10, 0x03};
public static final byte[] RESP_HEARTRATE = new byte[]{0x08, 0x15, 0x02}; public static final byte[] RESP_HEARTRATE = new byte[]{-0x80, 0x15, 0x03};
public static final byte[] RESP_FIRMWARE_INFO = new byte[]{0x08, 0x01, 0x02}; public static final byte[] RESP_FIRMWARE_INFO = new byte[]{0x08, 0x01, 0x02};
public static final byte[] RESP_TIME_SETTINGS = new byte[]{0x08, 0x01, 0x08}; public static final byte[] RESP_TIME_SETTINGS = new byte[]{0x08, 0x01, 0x08};
public static final byte[] RESP_BATTERY_INFO = new byte[]{0x08, 0x01, 0x14}; public static final byte[] RESP_BATTERY_INFO = new byte[]{0x08, 0x01, 0x14};
public static final byte[] RESP_NOTIFICATION_SETTINGS = new byte[]{0x01, 0x03, 0x02}; public static final byte[] RESP_NOTIFICATION_SETTINGS = new byte[]{0x01, 0x03, 0x02};
public static final byte[] RESP_HEART_RATE_DATA = new byte[]{0x08, (byte)0xF0, 0x10};
public static final byte[] RESP_HEART_RATE_DATA_DETAILS = new byte[]{0x08, (byte)0xF0, 0x11};
public static final String ACTION_ENABLE = "action.watch9.enable"; public static final String ACTION_ENABLE = "action.watch9.enable";

View File

@ -35,6 +35,7 @@ import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
import java.util.Locale; import java.util.Locale;
import java.util.UUID; import java.util.UUID;
@ -485,10 +486,12 @@ public class WatchXPlusDeviceSupport extends AbstractBTLEDeviceSupport {
builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
buildCommand(WatchXPlusConstants.CMD_DAY_STEPS_INFO, buildCommand(WatchXPlusConstants.CMD_DAY_STEPS_INFO,
WatchXPlusConstants.READ_VALUE)); WatchXPlusConstants.READ_VALUE));
// TODO: Watch does not return heart rate data after this command. Check why
// builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), // Fetch heart rate data samples count
// buildCommand(WatchXPlusConstants.CMD_HEARTRATE_INFO, builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
// WatchXPlusConstants.READ_VALUE)); buildCommand(WatchXPlusConstants.CMD_RETRIEVE_DATA,
WatchXPlusConstants.READ_VALUE,
WatchXPlusConstants.HEART_RATE_DATA_TYPE));
performImmediately(builder); performImmediately(builder);
} catch (IOException e) { } catch (IOException e) {
@ -577,22 +580,22 @@ public class WatchXPlusDeviceSupport extends AbstractBTLEDeviceSupport {
try { try {
TransactionBuilder builder = performInitialized("setWeather"); TransactionBuilder builder = performInitialized("setWeather");
byte[] command = WatchXPlusConstants.CMD_WEATHER_SET; byte[] command = WatchXPlusConstants.CMD_WEATHER_SET;
byte[] weatherInfo = new byte[5]; byte[] weatherInfo = new byte[5];
// bArr[8] = (byte) (this.mWeatherType >> 8); // First two bytes are controlling the icon
// bArr[9] = (byte) this.mWeatherType; weatherInfo[0] = 0x00;
// bArr[10] = (byte) this.mLowTemp; weatherInfo[1] = 0x00;
// bArr[11] = (byte) this.mHighTemp; weatherInfo[2] = (byte) weatherSpec.todayMinTemp;
// bArr[12] = (byte) this.mCurrentTemp; weatherInfo[3] = (byte) weatherSpec.todayMaxTemp;
weatherInfo[4] = (byte) weatherSpec.currentTemp;
builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
buildCommand(command, buildCommand(command,
WatchXPlusConstants.KEEP_ALIVE, WatchXPlusConstants.KEEP_ALIVE,
weatherInfo)); weatherInfo));
performImmediately(builder); performImmediately(builder);
} catch (IOException e) { } catch (IOException e) {
LOG.warn("Unable to set time", e); LOG.warn("Unable to set weather", e);
} }
} }
@ -620,11 +623,15 @@ public class WatchXPlusDeviceSupport extends AbstractBTLEDeviceSupport {
isCalibrationActive = false; isCalibrationActive = false;
} else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_DAY_STEPS_INDICATOR, 5)) { } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_DAY_STEPS_INDICATOR, 5)) {
handleStepsInfo(value); handleStepsInfo(value);
} else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_HEARTRATE, 5)) { } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_HEART_RATE_DATA, 5)) {
LOG.info(" Received Heart rate history"); 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 (value.length == 7 && value[5] == 0) { } else if (value.length == 7 && value[5] == 0) {
// Not sure if that's necessary // Not sure if that's necessary. There is no response for ACK in original app logs
handleAck(); // handleAck();
} else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_NOTIFICATION_SETTINGS, 5)) { } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_NOTIFICATION_SETTINGS, 5)) {
LOG.info(" Received notification settings status"); LOG.info(" Received notification settings status");
} else { } else {
@ -641,6 +648,36 @@ public class WatchXPlusDeviceSupport extends AbstractBTLEDeviceSupport {
return false; return false;
} }
private void handleHeartRateDetails(byte[] value) {
calculateHeartRateDetails(value);
LOG.info("Got Heart rate details");
}
private void handleHeartRateDataCount(byte[] value) {
int dataCount = Conversion.fromByteArr16(value[10], value[11]);
try {
TransactionBuilder builder = performInitialized("requestHeartRate");
byte[] heartRateDataType = WatchXPlusConstants.HEART_RATE_DATA_TYPE;
LOG.info("Watch contains " + dataCount + " heart rate entries");
// Request all data samples
for (int i = 0; i < dataCount; i++) {
byte[] index = Conversion.toByteArr16(i);
byte[] req = BLETypeConversions.join(heartRateDataType, index);
builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
buildCommand(WatchXPlusConstants.CMD_RETRIEVE_DATA_DETAILS,
WatchXPlusConstants.READ_VALUE,
req));
}
performImmediately(builder);
} catch (IOException e) {
LOG.warn("Unable to response to ACK", e);
}
}
private void handleAck() { private void handleAck() {
try { try {
TransactionBuilder builder = performInitialized("handleAck"); TransactionBuilder builder = performInitialized("handleAck");
@ -723,6 +760,44 @@ public class WatchXPlusDeviceSupport extends AbstractBTLEDeviceSupport {
super.dispose(); super.dispose();
} }
private static void calculateHeartRateDetails(byte[] bArr) {
int timestamp = Conversion.fromByteArr16(bArr[8], bArr[9], bArr[10], bArr[11]);
int dataLength = Conversion.fromByteArr16(bArr[12], bArr[13]);
int samplingInterval = (int) onSamplingInterval(bArr[14] >> 4, Conversion.fromByteArr16((byte) (bArr[14] & 15), bArr[15]));
int mtu = Conversion.fromByteArr16(bArr[16]);
int parts = dataLength / 16;
if (dataLength % 16 > 0) {
parts++;
}
LOG.info("timestamp (UTC): " + timestamp);
LOG.info("timestamp (UTC): " + new Date(timestamp));
LOG.info("dataLength (data length): " + dataLength);
LOG.info("samplingInterval (per time): " + samplingInterval);
LOG.info("mtu (mtu): " + mtu);
LOG.info("parts: " + parts);
}
private static double onSamplingInterval(int i, int i2) {
switch (i) {
case 1:
return 1.0d * Math.pow(10.0d, -6.0d) * ((double) i2);
case 2:
return 1.0d * Math.pow(10.0d, -3.0d) * ((double) i2);
case 3:
return (double) (1 * i2);
case 4:
return 10.0d * Math.pow(10.0d, -6.0d) * ((double) i2);
case 5:
return 10.0d * Math.pow(10.0d, -3.0d) * ((double) i2);
case 6:
return (double) (10 * i2);
default:
return (double) (10 * i2);
}
}
private static class Conversion { private static class Conversion {
static byte toBcd8(@IntRange(from = 0, to = 99) int value) { static byte toBcd8(@IntRange(from = 0, to = 99) int value) {
int high = (value / 10) << 4; int high = (value / 10) << 4;