1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-06-02 19:36:14 +02:00
Gadgetbridge/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/fitpro/FitProDeviceSupport.java
José Rebelo 81aef0bf35 Add support for multiple weather locations
Introduce the concept of primary and secondary weathers:

* Primary weather keeps the same behavior as previously across all weather providers, so it's non-breaking. This location is not necessarily the current location, just the primary weather location set by the user.
* The GenericWeatherReceiver now has a new extra WeatherSecondaryJson, that receives a json list with secondary weather locations.

It's guaranteed that the primary weather always exists, so the list of WeatherSpecs provided to devices is never empty. Update all support classes accordingly.
2024-03-29 21:10:40 +00:00

1455 lines
69 KiB
Java

/* Copyright (C) 2021-2024 Arjan Schrijver, Damien Gaignon, Petr Vaněk
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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.fitpro;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_ALARM;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_DND;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_FIND_BAND;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_GET_HW_INFO;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_GROUP_BAND_INFO;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_GROUP_BIND;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_GROUP_GENERAL;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_GROUP_HEARTRATE_SETTINGS;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_GROUP_RECEIVE_BUTTON_DATA;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_GROUP_RECEIVE_SPORTS_DATA;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_GROUP_REQUEST_DATA;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_GROUP_RESET;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_HEART_RATE_MEASUREMENT;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_NOTIFICATIONS_ENABLE;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_NOTIFICATION_CALL;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_NOTIFICATION_MESSAGE;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_REQUEST_STEPS_DATA0x10;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_REQUEST_STEPS_DATA0x7;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_REQUEST_STEPS_DATA0x8;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_REQUEST_STEPS_DATA1;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_RESET;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_RX_BAND_INFO;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_SET_ARM;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_SET_DATE_TIME;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_SET_DEVICE_VIBRATIONS;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_SET_DISPLAY_ON_LIFT;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_SET_LANGUAGE;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_SET_LONG_SIT_REMINDER;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_SET_SLEEP_TIMES;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_SET_STEP_GOAL;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_SET_USER_DATA;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_UNBIND;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.CMD_WEATHER;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.GENDER_FEMALE;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.GENDER_MALE;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.NOTIFICATION_ICON_FACEBOOK;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.NOTIFICATION_ICON_INSTAGRAM;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.NOTIFICATION_ICON_LINE;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.NOTIFICATION_ICON_QQ;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.NOTIFICATION_ICON_SMS;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.NOTIFICATION_ICON_TWITTER;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.NOTIFICATION_ICON_WECHAT;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.NOTIFICATION_ICON_WHATSAPP;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.RX_CAMERA1;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.RX_CAMERA2;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.RX_CAMERA3;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.RX_FIND_PHONE;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.RX_HEART_RATE_DATA;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.RX_MEDIA_BACK;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.RX_MEDIA_FORW;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.RX_MEDIA_PLAY_PAUSE;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.RX_SLEEP_DATA;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.RX_SPORTS_DAY_DATA;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.RX_STEP_DATA;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.UNIT_IMPERIAL;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.UNIT_METRIC;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.UUID_CHARACTERISTIC_RX;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.UUID_CHARACTERISTIC_TX;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.VALUE_OFF;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.VALUE_ON;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.VALUE_SET_ARM_LEFT;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.VALUE_SET_ARM_RIGHT;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.VALUE_SET_DEVICE_VIBRATIONS_DISABLE;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.VALUE_SET_DEVICE_VIBRATIONS_ENABLE;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.VALUE_SET_LONG_SIT_REMINDER_OFF;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.VALUE_SET_LONG_SIT_REMINDER_ON;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.VALUE_SET_NOTIFICATIONS_ENABLE_OFF;
import static nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants.VALUE_SET_NOTIFICATIONS_ENABLE_ON;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.Intent;
import android.net.Uri;
import android.widget.Toast;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
import nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProConstants;
import nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.FitProActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
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.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceBusyAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.IntentListener;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfoProfile;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo;
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile;
import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class FitProDeviceSupport extends AbstractBTLEDeviceSupport {
private static final Logger LOG = LoggerFactory.getLogger(FitProDeviceSupport.class);
public final GBDeviceEventBatteryInfo batteryCmd = new GBDeviceEventBatteryInfo();
public final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo();
public final DeviceInfoProfile<FitProDeviceSupport> deviceInfoProfile;
public final BatteryInfoProfile<FitProDeviceSupport> batteryInfoProfile;
public BluetoothGattCharacteristic readCharacteristic;
public BluetoothGattCharacteristic writeCharacteristic;
private static final boolean debugEnabled = false;
private int mtuSize=20;
public FitProDeviceSupport() {
super(LOG);
addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION);
addSupportedService(GattService.UUID_SERVICE_BATTERY_SERVICE);
IntentListener mListener = new IntentListener() {
@Override
public void notify(Intent intent) {
String action = intent.getAction();
if (DeviceInfoProfile.ACTION_DEVICE_INFO.equals(action)) {
handleDeviceInfo((nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo) intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO));
} else if (BatteryInfoProfile.ACTION_BATTERY_INFO.equals(action)) {
handleBatteryInfo((nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfo) intent.getParcelableExtra(BatteryInfoProfile.EXTRA_BATTERY_INFO));
}
}
};
deviceInfoProfile = new DeviceInfoProfile<>(this);
deviceInfoProfile.addListener(mListener);
addSupportedProfile(deviceInfoProfile);
batteryInfoProfile = new BatteryInfoProfile<>(this);
batteryInfoProfile.addListener(mListener);
addSupportedProfile(batteryInfoProfile);
addSupportedService(FitProConstants.UUID_CHARACTERISTIC_RX);
addSupportedService(FitProConstants.UUID_CHARACTERISTIC_UART);
}
@Override
public TransactionBuilder initializeDevice(TransactionBuilder builder) {
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
readCharacteristic = getCharacteristic(UUID_CHARACTERISTIC_RX);
writeCharacteristic = getCharacteristic(UUID_CHARACTERISTIC_TX);
builder.notify(getCharacteristic(UUID_CHARACTERISTIC_RX), true);
builder.notify(getCharacteristic(GattService.UUID_SERVICE_BATTERY_SERVICE), true);
builder.setCallback(this);
deviceInfoProfile.requestDeviceInfo(builder);
batteryInfoProfile.requestBatteryInfo(builder);
batteryInfoProfile.enableNotify(builder, true);
deviceInfoProfile.enableNotify(builder, true);
// this sequence seems to be important as without it:
// - fetch steps doesn't work
// - band seems to drain battery really fast
// - the wait time is needed as the band must process each command
// - (implementation based on individual requests did not work, the wait is still needed)
builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, FitProConstants.CMD_INIT1, (byte) 0x2));
setTime(builder);
builder.wait(200);
builder.write(writeCharacteristic, craftData(CMD_GROUP_REQUEST_DATA, FitProConstants.CMD_INIT1));
builder.wait(200);
builder.write(writeCharacteristic, craftData(CMD_GROUP_REQUEST_DATA, FitProConstants.CMD_INIT2));
builder.wait(200);
setLanguage(builder);
builder.wait(200);
builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, FitProConstants.CMD_INIT3, VALUE_ON));
builder.wait(200);
builder.write(writeCharacteristic, craftData(CMD_GROUP_REQUEST_DATA, VALUE_ON));
builder.wait(200);
builder.write(writeCharacteristic, craftData(CMD_GROUP_REQUEST_DATA, (byte) 0xf));
builder.wait(200);
builder.write(writeCharacteristic, craftData(CMD_GROUP_REQUEST_DATA, CMD_GET_HW_INFO));
builder.wait(200);
builder.write(writeCharacteristic, craftData(CMD_GROUP_BAND_INFO, CMD_RX_BAND_INFO));
builder.wait(200);
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
return builder;
}
public void handleDeviceInfo(DeviceInfo info) {
LOG.debug("fitpro device info: " + info);
versionCmd.hwVersion = "FitPro";
versionCmd.fwVersion = info.getFirmwareRevision();
handleGBDeviceEvent(versionCmd);
}
public void handleDeviceInfo(byte[] value) {
LOG.debug("FitPro device info2");
//test this 0xCD 0x00 0x11 0x15 0x01 0x02 0x00 0x0C 0x2B 0x27 0x00 0x01 0x33 0xA5 0x02 0x79 0x0A 0x68 0x56 0x06
debugPrintArray(value, "Device info:");
if (value.length < 20) {
return;
}
int start = 14;
int data_len = (int) value[start];
byte[] name = new byte[data_len];
System.arraycopy(value, start + 1, name, 0, data_len);
String sName = new String(name, StandardCharsets.UTF_8); //unused for now
start = start + data_len + 1;
data_len = (int) value[start];
byte[] hwname = new byte[data_len];
System.arraycopy(value, start + 1, hwname, 0, data_len);
String sHWName = new String(hwname, StandardCharsets.UTF_8);
LOG.debug("Device info: " + versionCmd);
versionCmd.hwVersion = sHWName;
handleGBDeviceEvent(versionCmd);
}
@Override
public boolean onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
UUID characteristicUUID = characteristic.getUuid();
byte[] data = characteristic.getValue();
debugPrintArray(data, "FitPro received value");
if (data[0] != FitProConstants.DATA_HEADER) {
if (debugEnabled) {
LOG.info("FitPro, packet not starting with 0xcd: " + data[0]);
debugPrintArray(new byte[]{data[0]}, "first byte");
LOG.info("Characteristic changed UUID: " + characteristicUUID);
LOG.info("Characteristic changed service: " + characteristic.getService().getCharacteristics());
debugPrintArray(data, "value bytes");
}
indicateFinishedFetchingOperation();
return false;
}
if (data != null && data.length > 5) {
byte command = data[3];
byte param = data[5];
switch (command) {
case CMD_GROUP_RECEIVE_BUTTON_DATA:
switch (param) {
case RX_FIND_PHONE:
handleFindPhone();
break;
case RX_MEDIA_BACK:
case RX_MEDIA_FORW:
case RX_MEDIA_PLAY_PAUSE:
handleMediaButton(param);
break;
case RX_CAMERA1:
case RX_CAMERA2:
case RX_CAMERA3:
handleCamera(param);
break;
default:
}
break;
case CMD_GROUP_RECEIVE_SPORTS_DATA:
switch (param) {
case RX_HEART_RATE_DATA:
handleHR(data);
break;
case RX_SPORTS_DAY_DATA:
indicateStartingFetchingOperation();
handleDayTotalsData(data);
indicateFinishedFetchingOperation();
break;
case RX_SLEEP_DATA:
indicateStartingFetchingOperation();
handleSleepData(data);
indicateFinishedFetchingOperation();
break;
case RX_STEP_DATA:
indicateStartingFetchingOperation();
handleStepData(data);
indicateFinishedFetchingOperation();
break;
case CMD_REQUEST_STEPS_DATA0x7:
case CMD_REQUEST_STEPS_DATA0x8:
case CMD_REQUEST_STEPS_DATA0x10:
//acking this makes the band to send data
sendAck(data[3], data[1], data[2], data[5]);
break;
}
break;
case CMD_GROUP_BAND_INFO:
switch (param) {
case CMD_RX_BAND_INFO:
handleDeviceInfo(data);
break;
}
sendAck(data[3], data[1], data[2], data[5]);
break;
case CMD_GROUP_REQUEST_DATA:
switch (param) {
case CMD_GET_HW_INFO:
handleHardwareDetails(data);
break;
}
sendAck(data[3], data[1], data[2], data[5]);
break;
}
LOG.info("Characteristic changed UUID: " + characteristicUUID);
LOG.info("Characteristic changed service: " + characteristic.getService().getCharacteristics());
debugPrintArray(data, "value bytes");
}
return false;
}
public void indicateFinishedFetchingOperation() {
//LOG.debug("download finish announced");
GB.updateTransferNotification(null, "", false, 100, getContext());
GB.signalActivityDataFinish();
unsetBusy();
}
public void indicateStartingFetchingOperation() {
GB.updateTransferNotification(null, getContext().getString(R.string.busy_task_fetch_activity_data), true, 10, getContext());
}
protected void unsetBusy() {
if (getDevice().isBusy()) {
getDevice().unsetBusyTask();
getDevice().sendDeviceUpdateIntent(getContext());
}
}
public void handleHardwareDetails(byte[] value) {
LOG.debug("FitPro hardware details");
debugPrintArray(value, "Device info:");
if (value.length < 20) {
return;
}
int start = 8;
int data_len = (int) value[start];
byte[] led = new byte[data_len];
System.arraycopy(value, start + 1, led, 0, data_len);
String sLED = new String(led, StandardCharsets.UTF_8);
start = start + data_len + 1;
data_len = (int) value[start];
byte[] gsensor = new byte[data_len];
System.arraycopy(value, start + 1, gsensor, 0, data_len);
String sGsensor = new String(gsensor, StandardCharsets.UTF_8);
gbDevice.setFirmwareVersion2(sGsensor + " " + sLED);
//the band does not like to answer when asked together for both hw info, so ask now,
// after data is already received
TransactionBuilder builder = new TransactionBuilder("notification");
builder.write(writeCharacteristic, craftData(CMD_GROUP_BAND_INFO, CMD_RX_BAND_INFO));
builder.queue(getQueue());
}
public void handleHR(byte[] value) {
LOG.debug("FitPro handle heart rate measurement");
debugPrintArray(value, "value");
if (value.length < 17) {
LOG.debug("FitPro heartrate measurement payload too short");
return;
}
int heartRate = (int) value[19];
int pressureLow = (int) value[18];
int pressureHigh = (int) value[17];
int spo2 = (int) value[13];
int seconds = ByteBuffer.wrap(value, 12, 4).getInt();
sendAck(value[3], value[1], value[2], value[5]);
if (!(heartRate > 0)) {
return;
}
handleHR(seconds, heartRate, pressureLow, pressureHigh, spo2);
}
@Override
public void onSetCallState(CallSpec callSpec) {
LOG.debug("FitPro send call notification");
TransactionBuilder builder = new TransactionBuilder("CALL");
if (callSpec.command == CallSpec.CALL_INCOMING) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
outputStream.write(0x1);
outputStream.write(0x0);
outputStream.write(0x0);
if (callSpec.name != null) {
outputStream.write(callSpec.name.getBytes(StandardCharsets.UTF_8));
outputStream.write(0x20);
}
if (callSpec.number != null) {
outputStream.write(callSpec.number.getBytes(StandardCharsets.UTF_8));
outputStream.write(0x20);
}
} catch (IOException e) {
LOG.error("error sending call notification: " + e);
}
debugPrintArray(craftData(CMD_GROUP_GENERAL, CMD_NOTIFICATION_CALL, outputStream.toByteArray()), "crafted call notify");
writeChunkedData(builder, craftData(CMD_GROUP_GENERAL, CMD_NOTIFICATION_CALL, outputStream.toByteArray()));
} else {
builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, CMD_NOTIFICATION_CALL, VALUE_OFF));
}
builder.queue(getQueue());
}
@Override
public void onSendConfiguration(String config) {
LOG.debug("FitPro on send config: " + config);
try {
TransactionBuilder builder = performInitialized("sendConfiguration");
switch (config) {
case DeviceSettingsPreferenceConst.PREF_LANGUAGE:
setLanguage(builder);
break;
case DeviceSettingsPreferenceConst.PREF_INACTIVITY_THRESHOLD_EXTENDED:
case DeviceSettingsPreferenceConst.PREF_INACTIVITY_ENABLE:
case DeviceSettingsPreferenceConst.PREF_INACTIVITY_START:
case DeviceSettingsPreferenceConst.PREF_INACTIVITY_END:
setLongSitReminder(builder);
break;
case DeviceSettingsPreferenceConst.PREF_ACTIVATE_DISPLAY_ON_LIFT:
case DeviceSettingsPreferenceConst.PREF_DISPLAY_ON_LIFT_START:
case DeviceSettingsPreferenceConst.PREF_DISPLAY_ON_LIFT_END:
setDisplayOnLift(builder);
break;
case SettingsActivity.PREF_MEASUREMENT_SYSTEM:
case ActivityUser.PREF_USER_WEIGHT_KG:
case ActivityUser.PREF_USER_GENDER:
case ActivityUser.PREF_USER_HEIGHT_CM:
case ActivityUser.PREF_USER_YEAR_OF_BIRTH:
setUserData(builder);
break;
case ActivityUser.PREF_USER_STEPS_GOAL:
setStepsGoal(builder);
break;
case DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO:
case DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO_START:
case DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO_END:
setDoNotDisturb(builder);
break;
case DeviceSettingsPreferenceConst.PREF_SLEEP_TIME:
case DeviceSettingsPreferenceConst.PREF_SLEEP_TIME_START:
case DeviceSettingsPreferenceConst.PREF_SLEEP_TIME_END:
setSleepTime(builder);
break;
case DeviceSettingsPreferenceConst.PREF_WEARLOCATION:
setWearLocation(builder);
break;
case DeviceSettingsPreferenceConst.PREF_VIBRATION_ENABLE:
setVibrations(builder);
break;
case DeviceSettingsPreferenceConst.PREF_NOTIFICATION_ENABLE:
setNotifications(builder);
break;
case DeviceSettingsPreferenceConst.PREF_AUTOHEARTRATE_SWITCH:
case DeviceSettingsPreferenceConst.PREF_AUTOHEARTRATE_SLEEP:
case DeviceSettingsPreferenceConst.PREF_AUTOHEARTRATE_INTERVAL:
case DeviceSettingsPreferenceConst.PREF_AUTOHEARTRATE_START:
case DeviceSettingsPreferenceConst.PREF_AUTOHEARTRATE_END:
setAutoHeartRate(builder);
break;
}
builder.queue(getQueue());
} catch (IOException e) {
GB.toast(getContext(), "Error sending configuration: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
}
}
public void sendAck(byte command_group, byte length_high, byte length_low, byte command) {
LOG.debug(" ACKing data: " + nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils.arrayToString(new byte[]{command_group}) + " " + nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils.arrayToString(new byte[]{command}));
TransactionBuilder builder = new TransactionBuilder("notification");
short size = (short) (ByteBuffer.wrap(new byte[]{length_high, length_low}).getShort() + 3);
byte[] sizeArray = ByteBuffer.allocate(2).putShort(size).array();
builder.write(writeCharacteristic, new byte[]{FitProConstants.DATA_HEADER_ACK, 0, 5, command_group, 1, sizeArray[0], sizeArray[1], 1});
builder.queue(getQueue());
}
@Override
public void onTestNewFunction() {
LOG.debug("Hello FitPro Test function");
}
@Override
public void onSendWeather(ArrayList<WeatherSpec> weatherSpecs) {
WeatherSpec weatherSpec = weatherSpecs.get(0);
LOG.debug("FitPro send weather");
short todayMax = (short) (weatherSpec.todayMaxTemp - 273);
short todayMin = (short) (weatherSpec.todayMinTemp - 273);
byte weatherUnit = 0;
String units = GBApplication.getPrefs().getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, GBApplication.getContext().getString(R.string.p_unit_metric));
if (units.equals(GBApplication.getContext().getString(R.string.p_unit_imperial))) {
todayMax = (short) (todayMax * 1.8f + 32);
todayMin = (short) (todayMin * 1.8f + 32);
weatherUnit = 1;
}
byte currentConditionCode = Weather.mapToFitProCondition(weatherSpec.currentConditionCode);
TransactionBuilder builder = new TransactionBuilder("weather");
writeChunkedData(builder, craftData(CMD_GROUP_GENERAL, CMD_WEATHER, new byte[]{(byte) todayMin, (byte) todayMax, (byte) currentConditionCode, (byte) weatherUnit}));
builder.queue(getQueue());
}
@Override
public boolean useAutoConnect() {
return true;
}
@Override
public void onNotification(NotificationSpec notificationSpec) {
LOG.debug("FitPro notification: " + notificationSpec.type);
TransactionBuilder builder = new TransactionBuilder("notification");
byte icon = NOTIFICATION_ICON_SMS;
switch (notificationSpec.type) {
case GENERIC_SMS:
icon = NOTIFICATION_ICON_SMS;
break;
case FACEBOOK:
case FACEBOOK_MESSENGER:
icon = NOTIFICATION_ICON_FACEBOOK;
break;
case LINE:
icon = NOTIFICATION_ICON_LINE;
break;
case WHATSAPP:
icon = NOTIFICATION_ICON_WHATSAPP;
break;
case TWITTER:
icon = NOTIFICATION_ICON_TWITTER;
break;
case SIGNAL:
case VIBER:
case CONVERSATIONS:
icon = NOTIFICATION_ICON_QQ;
break;
case WECHAT:
case GMAIL:
icon = NOTIFICATION_ICON_WECHAT;
break;
case INSTAGRAM:
icon = NOTIFICATION_ICON_INSTAGRAM;
break;
default:
icon = NOTIFICATION_ICON_SMS;
break;
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
outputStream.write(icon);
outputStream.write(0x0);
outputStream.write(0x0);
if (notificationSpec.sender != null) {
outputStream.write(notificationSpec.sender.getBytes(StandardCharsets.UTF_8));
outputStream.write(0x20);
} else {
if (notificationSpec.phoneNumber != null) { //use number only if there is no sender
outputStream.write(notificationSpec.phoneNumber.getBytes(StandardCharsets.UTF_8));
outputStream.write(0x20);
}
}
if (notificationSpec.subject != null) {
outputStream.write(notificationSpec.subject.getBytes(StandardCharsets.UTF_8));
outputStream.write(0x20);
}
if (notificationSpec.body != null) {
outputStream.write(notificationSpec.body.getBytes(StandardCharsets.UTF_8));
outputStream.write(0x20);
}
} catch (IOException e) {
LOG.error("FitPro error sending notification: " + e);
}
String output = outputStream.toString();
if (outputStream.toString().length() > 250) {
output = outputStream.toString().substring(0, 250);
}
writeChunkedData(builder, craftData(CMD_GROUP_GENERAL, CMD_NOTIFICATION_MESSAGE, output.getBytes(StandardCharsets.UTF_8)));
builder.queue(getQueue());
}
public FitProDeviceSupport setLanguage(TransactionBuilder builder) {
LOG.debug("FitPro set language");
String localeString = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString("language", "auto");
if (localeString == null || localeString.equals("auto")) {
String language = Locale.getDefault().getLanguage();
String country = Locale.getDefault().getCountry();
if (country == null) {
country = language;
}
localeString = language + "_" + country.toUpperCase();
}
LOG.info("Setting device to locale: " + localeString);
byte languageCode = FitProConstants.LANG_ENGLISH;
switch (localeString.substring(0, 2)) {
case "zh":
languageCode = FitProConstants.LANG_CHINESE;
break;
case "it":
languageCode = FitProConstants.LANG_ITALIAN;
break;
case "cs":
languageCode = FitProConstants.LANG_CZECH;
break;
case "en":
languageCode = FitProConstants.LANG_ENGLISH;
break;
case "tr":
languageCode = FitProConstants.LANG_TURKISH;
break;
case "ru":
languageCode = FitProConstants.LANG_RUSSIAN;
break;
case "pl":
languageCode = FitProConstants.LANG_POLISH;
break;
case "nl":
languageCode = FitProConstants.LANG_NETHERLANDS;
break;
case "fr":
languageCode = FitProConstants.LANG_FRENCH;
break;
case "es":
languageCode = FitProConstants.LANG_SPANISH;
break;
case "de":
languageCode = FitProConstants.LANG_GERMAN;
break;
case "pt":
languageCode = FitProConstants.LANG_PORTUGUESE;
break;
default:
languageCode = FitProConstants.LANG_ENGLISH;
}
builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, CMD_SET_LANGUAGE, languageCode));
return this;
}
public FitProDeviceSupport setUserData(TransactionBuilder builder) {
//0xcd 0x00 0x09 0x12 0x01 0x04 0x00 0x04 0xaf 0x59 0x09 0xe1 FitPro
LOG.debug("FitPro set user data");
ActivityUser activityUser = new ActivityUser();
int age = activityUser.getAge();
int gender = activityUser.getGender();
byte genderUnit = GENDER_FEMALE;
if (gender == ActivityUser.GENDER_MALE) {
genderUnit = GENDER_MALE;
}
int heightCm = activityUser.getHeightCm();
int weightKg = activityUser.getWeightKg();
byte distanceUnit = UNIT_METRIC;
String units = GBApplication.getPrefs().getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, GBApplication.getContext().getString(R.string.p_unit_metric));
if (units.equals(GBApplication.getContext().getString(R.string.p_unit_imperial))) {
distanceUnit = UNIT_IMPERIAL;
}
int userData = genderUnit << 31 | age << 24 | heightCm << 15 | weightKg << 5 | distanceUnit;
byte[] data = craftData(CMD_GROUP_GENERAL, CMD_SET_USER_DATA, ByteBuffer.allocate(4).putInt(userData).array());
builder.write(writeCharacteristic, data);
return this;
}
@Override
public void onFetchRecordedData(int dataTypes) {
indicateFinishedFetchingOperation();
TransactionBuilder builder = new TransactionBuilder("fetch data1");
builder.add(new SetDeviceBusyAction(getDevice(), getContext().getString(R.string.busy_task_fetch_activity_data), getContext()));
builder.write(writeCharacteristic, craftData(CMD_GROUP_RECEIVE_SPORTS_DATA, CMD_REQUEST_STEPS_DATA1, VALUE_ON));
builder.queue(getQueue());
}
public void handleDayTotalsData(byte[] value) {
LOG.debug("FitPro handle day data length: " + value.length);
debugPrintArray(value, "value");
if (value.length < 10) {
LOG.debug("FitPro payload too short");
return;
}
debugPrintArray(value, "processing");
int steps = ByteBuffer.wrap(value, 10, 4).getInt();
int distance = ByteBuffer.wrap(value, 14, 4).getInt();
byte[] caloriesBytes = new byte[3];
System.arraycopy(value, 18, caloriesBytes, 0, 2);
int calories = ByteBuffer.wrap(caloriesBytes, 0, 3).getShort();
LOG.debug("processing day data summary, steps: " + steps + " distance: " + distance + " calories: " + calories);
sendAck(value[3], value[1], value[2], value[5]);
//handleDayTotalsData(steps, distance, calories);
}
public void handleBatteryInfo(nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfo info) {
LOG.debug("FitPro battery info: " + info);
batteryCmd.level = (short) info.getPercentCharged();
handleGBDeviceEvent(batteryCmd);
}
/* public void handleDeviceInfo(nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo info) {
LOG.debug("FitPro device info: " + info);
versionCmd.hwVersion = "+FitPro";
versionCmd.fwVersion = info.getFirmwareRevision();
handleGBDeviceEvent(versionCmd);
}
*/
public void handleCamera(byte command) {
GB.toast(getContext(), "Camera buttons are detected but not further handled.", Toast.LENGTH_SHORT, GB.INFO);
}
public void handleFindPhone() {
LOG.info("FitPro find phone");
GBDeviceEventFindPhone deviceEventFindPhone = new GBDeviceEventFindPhone();
deviceEventFindPhone.event = GBDeviceEventFindPhone.Event.START;
evaluateGBDeviceEvent(deviceEventFindPhone);
}
public void handleMediaButton(byte command) {
GBDeviceEventMusicControl deviceEventMusicControl = new GBDeviceEventMusicControl();
if (command == RX_MEDIA_PLAY_PAUSE) {
deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.PLAYPAUSE;
evaluateGBDeviceEvent(deviceEventMusicControl);
} else if (command == RX_MEDIA_FORW) {
deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.NEXT;
evaluateGBDeviceEvent(deviceEventMusicControl);
} else if (command == RX_MEDIA_BACK) {
deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.PREVIOUS;
evaluateGBDeviceEvent(deviceEventMusicControl);
}
}
public FitProDeviceSupport setVibrations(TransactionBuilder builder) {
LOG.debug("FitPro set enable vibrations");
boolean vibrations = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREF_VIBRATION_ENABLE, false);
byte[] enable = VALUE_SET_DEVICE_VIBRATIONS_ENABLE;
if (!vibrations) {
enable = VALUE_SET_DEVICE_VIBRATIONS_DISABLE;
}
builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, CMD_SET_DEVICE_VIBRATIONS, enable));
return this;
}
public static void debugPrintArray(byte[] bytes, String label) {
if (!debugEnabled) return;
String arrayString = nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils.arrayToString(bytes);
LOG.debug("FitPro debug print " + label + ": " + arrayString);
}
public FitProDeviceSupport setNotifications(TransactionBuilder builder) {
LOG.debug("FitPro set enable notifications");
boolean notifications = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREF_NOTIFICATION_ENABLE, false);
byte[] enable = VALUE_SET_NOTIFICATIONS_ENABLE_ON;
if (!notifications) {
enable = VALUE_SET_NOTIFICATIONS_ENABLE_OFF;
}
builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, CMD_NOTIFICATIONS_ENABLE, enable));
return this;
}
public static byte[] craftData(byte command_group, byte command) {
return craftData(command_group, command, new byte[]{});
}
public static byte[] craftData(byte command_group, byte command, byte value) {
return craftData(command_group, command, new byte[]{value});
}
public static byte[] craftData(byte command_group, byte command, byte[] data) {
//0xCD 0x00 0x09 0x12 0x01 0x01 0x00 0x04 0xA5 0x83 0x73 0xDB
byte[] result = new byte[FitProConstants.DATA_TEMPLATE.length + data.length];
System.arraycopy(FitProConstants.DATA_TEMPLATE, 0, result, 0, FitProConstants.DATA_TEMPLATE.length);
result[1] = (byte) (((FitProConstants.DATA_TEMPLATE.length + data.length - 3) >> 8) & 0xff);
result[2] = (byte) ((FitProConstants.DATA_TEMPLATE.length + data.length - 3) & 0xff);
result[3] = command_group;
result[5] = command;
result[6] = (byte) ((data.length >> 8) & 0xff);
result[7] = (byte) (data.length & 0xff);
System.arraycopy(data, 0, result, 8, data.length);
//debug
debugPrintArray(result, "crafted packet");
return result;
}
// send chucked up data
public void writeChunkedData(TransactionBuilder builder, byte[] data) {
for (int start = 0; start < data.length; start += mtuSize) {
int end = start + mtuSize;
if (end > data.length) end = data.length;
builder.write(writeCharacteristic, Arrays.copyOfRange(data, start, end));
}
}
@Override
public void onSetTime() {
LOG.debug("FitPro set date and time");
TransactionBuilder builder = new TransactionBuilder("Set date and time");
setTime(builder);
builder.queue(getQueue());
}
public FitProDeviceSupport setTime(TransactionBuilder builder) {
LOG.debug("FitPro set time");
Calendar calendar = Calendar.getInstance();
int datetime = calendar.get(Calendar.SECOND) | (
(calendar.get(Calendar.YEAR) - 2000) << 26 | calendar.get(Calendar.MONTH) + 1 << 22 |
calendar.get(Calendar.DAY_OF_MONTH) << 17 |
calendar.get(Calendar.HOUR_OF_DAY) << 12 | calendar.get(Calendar.MINUTE) << 6);
//this is how the values can be re-stored
// result is this
//byte[] array = new byte[]{(byte) (datetime >> 24), (byte) (datetime >> 16), (byte) (datetime >> 8), (byte) (datetime >> 0)};
// int datetime2 = ByteBuffer.wrap(array).getInt();
//byte[] time = craftData(LT716Constants.CMD_SET_DATE_TIME, new byte[]{(byte) (datetime >> 24), (byte) (datetime >> 16), (byte) (datetime >> 8), (byte) (datetime >> 0)});
byte[] time = craftData(CMD_GROUP_GENERAL, CMD_SET_DATE_TIME, (ByteBuffer.allocate(4).putInt(datetime).array()));
builder.write(writeCharacteristic, time);
return this;
}
@Override
public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
LOG.debug("FitPro set alarms");
// handle one-shot alarm from the widget:
// this device doesn't have concept of on-off alarm, so use the last slot for this and store
// this alarm in the database so the user knows what is going on and can disable it
if (alarms.toArray().length == 1 && alarms.get(0).getRepetition() == 0) { //single shot?
Alarm oneshot = alarms.get(0);
alarms = (ArrayList<? extends Alarm>) AlarmUtils.mergeOneshotToDeviceAlarms(gbDevice, (nodomain.freeyourgadget.gadgetbridge.entities.Alarm) oneshot, 7);
}
try {
TransactionBuilder builder = performInitialized("Set alarm");
boolean anyAlarmEnabled = false;
byte[] all_alarms = new byte[]{};
for (Alarm alarm : alarms) {
Calendar calendar = AlarmUtils.toCalendar(alarm);
anyAlarmEnabled |= alarm.getEnabled();
LOG.debug("alarms: " + alarm.getPosition());
int maxAlarms = 8;
if (alarm.getPosition() >= maxAlarms) { //we should never encounter this, but just in case
if (alarm.getEnabled()) {
GB.toast(getContext(), "Only 8 alarms are supported.", Toast.LENGTH_LONG, GB.WARN);
}
return;
}
if (alarm.getEnabled()) {
long datetime = (long) alarm.getRepetition() | (
(long) (calendar.get(Calendar.YEAR) - 2000) << 34 |
(long) (calendar.get(Calendar.MONTH) + 1) << 30 |
(long) (calendar.get(Calendar.DAY_OF_MONTH)) << 25 |
(long) (calendar.get(Calendar.HOUR_OF_DAY)) << 20 |
(long) (calendar.get(Calendar.MINUTE)) << 14 |
1L << 11);
byte[] single_alarm = new byte[]{(byte) (datetime >> 32), (byte) (datetime >> 24), (byte) (datetime >> 16), (byte) (datetime >> 8), (byte) (datetime)};
all_alarms = ArrayUtils.addAll(all_alarms, single_alarm);
}
}
writeChunkedData(builder, craftData(CMD_GROUP_GENERAL, CMD_ALARM, all_alarms));
//builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, CMD_ALARM, all_alarms));
builder.queue(getQueue());
if (anyAlarmEnabled) {
GB.toast(getContext(), getContext().getString(R.string.user_feedback_miband_set_alarms_ok), Toast.LENGTH_SHORT, GB.INFO);
} else {
GB.toast(getContext(), getContext().getString(R.string.user_feedback_all_alarms_disabled), Toast.LENGTH_SHORT, GB.INFO);
}
} catch (IOException ex) {
GB.toast(getContext(), getContext().getString(R.string.user_feedback_miband_set_alarms_failed), Toast.LENGTH_LONG, GB.ERROR, ex);
}
}
@Override
public void onReset(int flags) {
LOG.debug("FitPro reset flags: " + flags);
byte[] command = craftData(CMD_GROUP_RESET, CMD_RESET);
switch (flags) {
case 1:
command = craftData(CMD_GROUP_RESET, CMD_RESET);
break;
case 2:
command = craftData(CMD_GROUP_BIND, CMD_UNBIND);
break;
}
getQueue().clear();
TransactionBuilder builder = new TransactionBuilder("resetting");
builder.write(writeCharacteristic, command);
builder.queue(getQueue());
}
@Override
public void onHeartRateTest() {
TransactionBuilder builder = new TransactionBuilder("notification");
builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, CMD_HEART_RATE_MEASUREMENT, VALUE_ON));
builder.queue(getQueue());
}
@Override
public void onFindDevice(boolean start) {
getQueue().clear();
LOG.debug("FitPro find device");
TransactionBuilder builder = new TransactionBuilder("searching");
builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, CMD_FIND_BAND, start ? VALUE_ON : VALUE_OFF));
builder.queue(getQueue());
}
public FitProDeviceSupport setAutoHeartRate(TransactionBuilder builder) {
LOG.debug("FitPro set automatic heartrate measurements");
boolean prefAutoheartrateSwitch = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean("pref_autoheartrate_switch", false);
LOG.info("Setting autoheartrate to " + prefAutoheartrateSwitch);
boolean sleep = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean("pref_autoheartrate_sleep", false);
String start = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString("pref_autoheartrate_start", "06:00");
String end = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString("pref_autoheartrate_end", "23:00");
String interval = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString("pref_autoheartrate_interval", "2");
int intervalInt = Integer.parseInt(interval);
int sleepInt = sleep ? 1 : 0;
int autoheartrateInt = prefAutoheartrateSwitch ? 1 : 0;
Calendar startCalendar = GregorianCalendar.getInstance();
Calendar endCalendar = GregorianCalendar.getInstance();
DateFormat df = new SimpleDateFormat("HH:mm");
try {
startCalendar.setTime(df.parse(start));
endCalendar.setTime(df.parse(end));
} catch (ParseException e) {
LOG.error("settings error: " + e);
}
int startTime = (startCalendar.get(Calendar.HOUR_OF_DAY) * 60) + startCalendar.get(Calendar.MINUTE);
int endTime = (endCalendar.get(Calendar.HOUR_OF_DAY) * 60) + endCalendar.get(Calendar.MINUTE);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
outputStream.write(autoheartrateInt);
outputStream.write(sleepInt);
outputStream.write(intervalInt >> 8);
outputStream.write(intervalInt);
outputStream.write(startTime >> 8);
outputStream.write(startTime);
outputStream.write(endTime >> 8);
outputStream.write(endTime);
//outputStream.write(0x7F);
builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, CMD_GROUP_HEARTRATE_SETTINGS, outputStream.toByteArray()));
return this;
}
public FitProDeviceSupport setLongSitReminder(TransactionBuilder builder) {
LOG.debug("FitPro set inactivity warning");
boolean prefLongsitSwitch = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_ENABLE, false);
LOG.info("Setting long sit warning to " + prefLongsitSwitch);
if (prefLongsitSwitch) {
String inactivity = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString(DeviceSettingsPreferenceConst.PREF_INACTIVITY_THRESHOLD_EXTENDED, "4");
String start = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString(DeviceSettingsPreferenceConst.PREF_INACTIVITY_START, "08:00");
String end = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString(DeviceSettingsPreferenceConst.PREF_INACTIVITY_END, "16:00");
Calendar startCalendar = GregorianCalendar.getInstance();
Calendar endCalendar = GregorianCalendar.getInstance();
DateFormat df = new SimpleDateFormat("HH:mm");
try {
startCalendar.setTime(df.parse(start));
endCalendar.setTime(df.parse(end));
} catch (ParseException e) {
LOG.debug("settings error: " + e);
}
int inactivityInt = Integer.parseInt(inactivity);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
outputStream.write(VALUE_SET_LONG_SIT_REMINDER_ON);
outputStream.write(inactivityInt);
outputStream.write(startCalendar.get(Calendar.HOUR_OF_DAY));
outputStream.write(endCalendar.get(Calendar.HOUR_OF_DAY));
outputStream.write(0x7F);
} catch (IOException e) {
LOG.error("settings error: " + e);
}
builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, CMD_SET_LONG_SIT_REMINDER, outputStream.toByteArray()));
LOG.info("Setting long sit warning to scheduled");
} else {
builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, CMD_SET_LONG_SIT_REMINDER, VALUE_SET_LONG_SIT_REMINDER_OFF));
LOG.info("Setting long sit warning to OFF");
}
return this;
}
public FitProDeviceSupport setDoNotDisturb(TransactionBuilder builder) {
LOG.debug("FitPro set DND");
String dnd = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString("do_not_disturb_no_auto", "off");
LOG.info("Setting DND to " + dnd);
int dndInt = dnd.equals("scheduled") ? 1 : 0;
String start = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString("do_not_disturb_no_auto_start", "22:00");
String end = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString("do_not_disturb_no_auto_end", "06:00");
Calendar startCalendar = GregorianCalendar.getInstance();
Calendar endCalendar = GregorianCalendar.getInstance();
DateFormat df = new SimpleDateFormat("HH:mm");
try {
startCalendar.setTime(df.parse(start));
endCalendar.setTime(df.parse(end));
} catch (ParseException e) {
LOG.error("settings error: " + e);
}
int startTime = (startCalendar.get(Calendar.HOUR_OF_DAY) * 60) + startCalendar.get(Calendar.MINUTE);
int endTime = (endCalendar.get(Calendar.HOUR_OF_DAY) * 60) + endCalendar.get(Calendar.MINUTE);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
outputStream.write(dndInt);
outputStream.write(startTime >> 8);
outputStream.write(startTime);
outputStream.write(endTime >> 8);
outputStream.write(endTime);
debugPrintArray(craftData(CMD_GROUP_GENERAL, CMD_DND, outputStream.toByteArray()), "enable DND");
debugPrintArray(outputStream.toByteArray(), "payload");
LOG.info("Setting DND to scheduled: " + start + " " + end);
builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, CMD_DND, outputStream.toByteArray()));
LOG.info("Setting DND scheduled");
return this;
}
public FitProDeviceSupport setSleepTime(TransactionBuilder builder) {
LOG.debug("FitPro set sleep times");
String sleepTime = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString("prefs_enable_sleep_time", "off");
LOG.info("Setting sleep times to " + sleepTime);
int sleepTimeInt = sleepTime.equals("scheduled") ? 1 : 0;
String start = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString("prefs_sleep_time_start", "22:00");
String end = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString("prefs_sleep_time_end", "06:00");
Calendar startCalendar = GregorianCalendar.getInstance();
Calendar endCalendar = GregorianCalendar.getInstance();
DateFormat df = new SimpleDateFormat("HH:mm");
try {
startCalendar.setTime(df.parse(start));
endCalendar.setTime(df.parse(end));
} catch (ParseException e) {
LOG.error("settings error: " + e);
}
int startTime = (startCalendar.get(Calendar.HOUR_OF_DAY) * 60) + startCalendar.get(Calendar.MINUTE);
int endTime = (endCalendar.get(Calendar.HOUR_OF_DAY) * 60) + endCalendar.get(Calendar.MINUTE);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
outputStream.write(sleepTimeInt);
outputStream.write(startTime >> 8);
outputStream.write(startTime);
outputStream.write(endTime >> 8);
outputStream.write(endTime);
debugPrintArray(craftData(CMD_GROUP_GENERAL, CMD_SET_SLEEP_TIMES, outputStream.toByteArray()), "enable sleep time");
debugPrintArray(outputStream.toByteArray(), "payload");
LOG.info("Setting sleep times scheduled: " + start + " " + end);
builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, CMD_SET_SLEEP_TIMES, outputStream.toByteArray()));
LOG.info("Setting sleep times scheduled");
return this;
}
public FitProDeviceSupport setWearLocation(TransactionBuilder builder) {
LOG.debug("FitPro set wearing location");
byte location = VALUE_SET_ARM_LEFT;
String setLocation = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString(DeviceSettingsPreferenceConst.PREF_WEARLOCATION, "left");
if ("right".equals(setLocation)) {
location = VALUE_SET_ARM_RIGHT;
}
builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, CMD_SET_ARM, location));
return this;
}
public FitProDeviceSupport setDisplayOnLift(TransactionBuilder builder) {
LOG.debug("FitPro set display on lift");
String displayLift = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString("activate_display_on_lift_wrist", "off");
int displayLiftInt = displayLift.equals("scheduled") ? 1 : 0;
LOG.info("Setting activate display on lift wrist to:" + displayLift + ": " + displayLiftInt);
String start = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString("display_on_lift_start", "08:00");
String end = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString("display_on_lift_end", "16:00");
Calendar startCalendar = GregorianCalendar.getInstance();
Calendar endCalendar = GregorianCalendar.getInstance();
DateFormat df = new SimpleDateFormat("HH:mm");
try {
startCalendar.setTime(df.parse(start));
endCalendar.setTime(df.parse(end));
} catch (ParseException e) {
LOG.error("settings error: " + e);
}
int startTime = (startCalendar.get(Calendar.HOUR_OF_DAY) * 60) + startCalendar.get(Calendar.MINUTE);
int endTime = (endCalendar.get(Calendar.HOUR_OF_DAY) * 60) + endCalendar.get(Calendar.MINUTE);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
outputStream.write(displayLiftInt);
outputStream.write(startTime >> 8);
outputStream.write(startTime);
outputStream.write(endTime >> 8);
outputStream.write(endTime);
debugPrintArray(craftData(CMD_GROUP_GENERAL, CMD_SET_DISPLAY_ON_LIFT, outputStream.toByteArray()), "enable lift display");
debugPrintArray(outputStream.toByteArray(), "payload");
LOG.info("Setting activate display on lift wrist scheduled: " + start + " " + end);
builder.write(writeCharacteristic, craftData(CMD_GROUP_GENERAL, CMD_SET_DISPLAY_ON_LIFT, outputStream.toByteArray()));
LOG.info("Setting activate display on lift wrist scheduled");
return this;
}
public FitProDeviceSupport setStepsGoal(TransactionBuilder builder) {
LOG.debug("FitPro set step goal");
//cd 00 09 12 01 03 00 04 00 00 05 dc
ActivityUser activityUser = new ActivityUser();
int stepGoal = activityUser.getStepsGoal();
byte[] data = craftData(CMD_GROUP_GENERAL, CMD_SET_STEP_GOAL, ByteBuffer.allocate(4).putInt(stepGoal).array());
builder.write(writeCharacteristic, data);
return this;
}
public void handleSleepData(byte[] value) {
debugPrintArray(value, "sleep data value");
// sleep packet consists of: date + list of 4bytes of 15minutes intervals
// these intervals contain seconds offset from the date and type of sleep
byte[] dateArray = new byte[2];
System.arraycopy(value, 8, dateArray, 0, 2);
Calendar date = decodeDateTime(dateArray);
List<FitProActivitySample> samples = new ArrayList<>();
for (int i = 12; i < value.length - 3; i = i + 4) {
byte[] packet = new byte[4];
System.arraycopy(value, i, packet, 0, 4);
int data = ByteBuffer.wrap(packet).getInt();
int activity_kind = (int) (data & 0xff);
int encodedTime = (int) (data >> 16);
int seconds = getSleepSecondsOfDay(encodedTime);
Calendar now = (Calendar) date.clone(); // do not modify the caller's argument
now.add(Calendar.SECOND, seconds);
int timestamp = (int) (now.getTimeInMillis() / 1000L);
debugPrintArray(packet, "processing sleep packet");
LOG.debug("FitPro new sleep: " + activity_kind + " seconds: " + seconds + " ts: " + timestamp + " date: " + DateTimeUtils.formatDateTime(new Date(timestamp * 1000L)));
FitProActivitySample sample = new FitProActivitySample();
sample.setTimestamp(timestamp);
sample.setHeartRate(ActivitySample.NOT_MEASURED);
sample.setActiveTimeMinutes(15);
sample.setRawKind(rawSleepKindToUniqueKind(activity_kind));
samples.add(sample);
}
if (addGBActivitySamples(samples)) {
sendAck(value[3], value[1], value[2], value[5]);
}
}
public int rawSleepKindToUniqueKind(int kind) {
//step and sleep are the same kind so we must distinguish them
return kind + 10;
}
public int rawActivityKindToUniqueKind(int kind) {
return kind;
}
public void handleStepData(byte[] value) {
debugPrintArray(value, "step data value");
// step packet consists of: date + list of 8bytes of (always?) 5minutes intervals
// these intervals contain seconds offset from the date, type of activity, calories,
// steps, distance, duration
byte[] dateArray = new byte[2];
System.arraycopy(value, 8, dateArray, 0, 2);
Calendar date = decodeDateTime(dateArray);
List<FitProActivitySample> samples = new ArrayList<>();
for (int i = 12; i < value.length - 7; i = i + 8) {
byte[] packet = new byte[8];
System.arraycopy(value, i, packet, 0, 8);
long data = ByteBuffer.wrap(packet).getLong();
int steps = (int) Math.abs((data >> 52));
int calories = (int) (data & 0x7ffff);
int activity_kind = (int) ((data >> 19) & 0x1);
int duration = (int) ((data >> 48) & 0xf);
int distance = (int) ((data >> 32) & 0xffff);
int encodedTime = (int) ((data >> 21) & 0x7ff);
int seconds = getSecondsOfDay(encodedTime);
Calendar now = (Calendar) date.clone(); // do not modify the caller's argument
now.add(Calendar.SECOND, seconds);
int timestamp = (int) (now.getTimeInMillis() / 1000L);
debugPrintArray(packet, "processing steps packet");
LOG.debug("FitPro adding new steps: " + steps);
FitProActivitySample sample = new FitProActivitySample();
sample.setTimestamp(timestamp);
sample.setHeartRate(ActivitySample.NOT_MEASURED);
sample.setSteps(steps);
sample.setDistanceMeters(distance);
sample.setCaloriesBurnt(calories);
sample.setActiveTimeMinutes(duration);
sample.setRawKind(rawActivityKindToUniqueKind(activity_kind));
samples.add(sample);
}
if (addGBActivitySamples(samples)) {
sendAck(value[3], value[1], value[2], value[5]);
}
}
public void addGBActivitySample(FitProActivitySample sample) {
List<FitProActivitySample> samples = new ArrayList<>();
samples.add(sample);
addGBActivitySamples(samples);
}
private boolean addGBActivitySamples(List<FitProActivitySample> samples) {
try (DBHandler dbHandler = GBApplication.acquireDB()) {
User user = DBHelper.getUser(dbHandler.getDaoSession());
Device device = DBHelper.getDevice(this.getDevice(), dbHandler.getDaoSession());
FitProSampleProvider provider = new FitProSampleProvider(this.getDevice(), dbHandler.getDaoSession());
for (FitProActivitySample sample : samples) {
sample.setDevice(device);
sample.setUser(user);
sample.setProvider(provider);
provider.addGBActivitySample(sample);
}
} catch (Exception ex) {
LOG.error("Error saving samples: " + ex);
GB.updateTransferNotification(null, "Data transfer failed", false, 0, getContext());
return false;
}
return true;
}
public void broadcastSample(FitProActivitySample sample) {
Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES)
.putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample);
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
}
public void handleHR(int seconds, int heartRate, int pressureLow, int pressureHigh, int spo2) {
LOG.debug("FitPro handle heart rate measurement");
Calendar date = Calendar.getInstance();
date.set(Calendar.HOUR_OF_DAY, 0);
date.set(Calendar.MINUTE, 0);
date.set(Calendar.SECOND, 0);
date.add(Calendar.SECOND, seconds);
LOG.debug("date: " + date);
FitProActivitySample sample = new FitProActivitySample();
sample.setHeartRate(heartRate);
sample.setPressureLowMmHg(pressureLow);
sample.setPressureHighMmHg(pressureHigh);
sample.setSpo2Percent(spo2);
sample.setTimestamp((int) (date.getTimeInMillis() / 1000));
sample.setRawKind(ActivityKind.TYPE_ACTIVITY);
addGBActivitySample(sample);
broadcastSample(sample);
GB.signalActivityDataFinish();
}
public void handleDayTotalsData(int steps, int distance, int calories) {
//this is for day data values, not used in Gb, handleStepData uses the better, 5min data
LOG.debug("FitPro handle day total steps");
LOG.debug("Steps: " + steps);
LOG.debug("Distance: " + distance);
LOG.debug("Calories: " + calories);
Calendar dateStart = Calendar.getInstance();
dateStart.set(Calendar.HOUR_OF_DAY, 0);
dateStart.set(Calendar.MINUTE, 0);
dateStart.set(Calendar.SECOND, 0);
Calendar dateEnd = Calendar.getInstance();
dateEnd.set(Calendar.HOUR_OF_DAY, 23);
dateEnd.set(Calendar.MINUTE, 59);
dateEnd.set(Calendar.SECOND, 59);
int dayStepCount = getStepsOnDay(dateStart, dateEnd);
int newSteps = (steps - dayStepCount);
LOG.debug("FitPro dayStepCount " + dayStepCount);
LOG.debug("FitPro new steps " + newSteps);
/*
if (newSteps > 0) {
LOG.debug("FitPro adding new steps " + newSteps);
ShenTechActivitySample sample = new ShenTechActivitySample();
Calendar date = Calendar.getInstance();
sample.setTimestamp((int) (date.getTimeInMillis() / 1000));
sample.setSteps(newSteps);
sample.setDistanceMeters(distance);
sample.setCaloriesBurnt(calories);
sample.setRawKind(ActivityKind.TYPE_ACTIVITY);
sample.setRawIntensity(1);
addGBActivitySample(sample);
broadcastSample(sample);
}
*/
}
private int getStepsOnDay(Calendar dayStart, Calendar dayEnd) {
//this is for day data values, not used in Gb, handleStepData uses 5min data which is better
try (DBHandler dbHandler = GBApplication.acquireDB()) {
FitProSampleProvider provider = new FitProSampleProvider(this.getDevice(), dbHandler.getDaoSession());
List<FitProActivitySample> samples = provider.getActivitySamples(
(int) (dayStart.getTimeInMillis() / 1000L),
(int) (dayEnd.getTimeInMillis() / 1000L));
int totalSteps = 0;
for (FitProActivitySample sample : samples) {
totalSteps += sample.getSteps();
}
return totalSteps;
} catch (Exception ex) {
LOG.error(ex.getMessage());
return 0;
}
}
public int getSecondsOfDay(int encodedTime) {
int hours = (int) Math.floor((encodedTime * 15) / 60);
int minutes = (encodedTime * 15) % 60;
int seconds = (hours * 3600) + (minutes * 60);
return seconds;
}
public int getSleepSecondsOfDay(int encodedTime) {
int hours = (int) Math.floor(encodedTime / 60);
int minutes = encodedTime % 60;
int seconds = (hours * 3600) + (minutes * 60);
return seconds;
}
public Calendar decodeDateTime(byte[] dateArray) {
debugPrintArray(dateArray, "array to decode to date time");
short dateShort = ByteBuffer.wrap(dateArray).getShort();
int day = (dateShort & 0x1f);
int month = ((dateShort >> 5) & 0xf);
int year = ((dateShort >> 9) + 2000);
Calendar date = GregorianCalendar.getInstance();
date.set(year, month - 1, day, 0, 0, 0);
return date;
}
}