mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-06-02 19:36:14 +02:00
81aef0bf35
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.
1455 lines
69 KiB
Java
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;
|
|
}
|
|
} |