1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-10-02 03:07:09 +02:00

Fixed battery level parsing from response

Added basic notification support
This commit is contained in:
mkusnierz 2019-10-18 00:02:53 +02:00
parent 2acb65b745
commit cf61ab9d38
8 changed files with 1107 additions and 12 deletions

View File

@ -406,6 +406,12 @@
<activity
android:name=".devices.watch9.Watch9CalibrationActivity"
android:label="@string/title_activity_watch9_calibration" />
<activity
android:name=".devices.watchxplus.WatchXPlusPairingActivity"
android:label="@string/title_activity_watch9_pairing" />
<activity
android:name=".devices.watchxplus.WatchXPlusCalibrationActivity"
android:label="@string/title_activity_watch9_calibration" />
<activity
android:name=".activities.charts.ChartsActivity"
android:label="@string/title_activity_charts"

View File

@ -0,0 +1,123 @@
/* Copyright (C) 2018-2019 Daniele Gobbetti, maxirnilian
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.watchxplus;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.widget.Button;
import android.widget.NumberPicker;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class WatchXPlusCalibrationActivity extends AbstractGBActivity {
private static final String STATE_DEVICE = "stateDevice";
GBDevice device;
NumberPicker pickerHour, pickerMinute, pickerSecond;
Handler handler;
Runnable holdCalibration;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_watch9_calibration);
pickerHour = findViewById(R.id.np_hour);
pickerMinute = findViewById(R.id.np_minute);
pickerSecond = findViewById(R.id.np_second);
pickerHour.setMinValue(1);
pickerHour.setMaxValue(12);
pickerHour.setValue(12);
pickerMinute.setMinValue(0);
pickerMinute.setMaxValue(59);
pickerMinute.setValue(0);
pickerSecond.setMinValue(0);
pickerSecond.setMaxValue(59);
pickerSecond.setValue(0);
handler = new Handler();
holdCalibration = new Runnable() {
@Override
public void run() {
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(new Intent(WatchXPlusConstants.ACTION_CALIBRATION_HOLD));
handler.postDelayed(this, 10000);
}
};
Intent intent = getIntent();
device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
if (device == null && savedInstanceState != null) {
device = savedInstanceState.getParcelable(STATE_DEVICE);
}
if (device == null) {
finish();
}
final Button btCalibrate = findViewById(R.id.watch9_bt_calibrate);
btCalibrate.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
btCalibrate.setEnabled(false);
handler.removeCallbacks(holdCalibration);
Intent calibrationData = new Intent(WatchXPlusConstants.ACTION_CALIBRATION_SEND);
calibrationData.putExtra(WatchXPlusConstants.VALUE_CALIBRATION_HOUR, pickerHour.getValue());
calibrationData.putExtra(WatchXPlusConstants.VALUE_CALIBRATION_MINUTE, pickerMinute.getValue());
calibrationData.putExtra(WatchXPlusConstants.VALUE_CALIBRATION_SECOND, pickerSecond.getValue());
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(calibrationData);
finish();
}
});
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(STATE_DEVICE, device);
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
device = savedInstanceState.getParcelable(STATE_DEVICE);
}
@Override
protected void onStart() {
super.onStart();
Intent calibration = new Intent(WatchXPlusConstants.ACTION_CALIBRATION);
calibration.putExtra(WatchXPlusConstants.ACTION_ENABLE, true);
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(calibration);
handler.postDelayed(holdCalibration, 1000);
}
@Override
protected void onStop() {
super.onStop();
Intent calibration = new Intent(WatchXPlusConstants.ACTION_CALIBRATION);
calibration.putExtra(WatchXPlusConstants.ACTION_ENABLE, false);
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(calibration);
handler.removeCallbacks(holdCalibration);
}
}

View File

@ -0,0 +1,93 @@
/* Copyright (C) 2018-2019 maxirnilian
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.watchxplus;
import java.util.UUID;
public final class WatchXPlusConstants {
public static final UUID UUID_SERVICE_WATCHXPLUS = UUID.fromString("0000a800-0000-1000-8000-00805f9b34fb");
public static final UUID UUID_UNKNOWN_DESCRIPTOR = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
public static final UUID UUID_CHARACTERISTIC_WRITE = UUID.fromString("0000a801-0000-1000-8000-00805f9b34fb");
public static final UUID UUID_CHARACTERISTIC_UNKNOWN_2 = UUID.fromString("0000a802-0000-1000-8000-00805f9b34fb");
public static final UUID UUID_CHARACTERISTIC_UNKNOWN_3 = UUID.fromString("0000a803-0000-1000-8000-00805f9b34fb");
public static final UUID UUID_CHARACTERISTIC_UNKNOWN_4 = UUID.fromString("0000a804-0000-1000-8000-00805f9b34fb");
public static final int NOTIFICATION_CHANNEL_DEFAULT = 7;
public static final int NOTIFICATION_CHANNEL_PHONE_CALL = 1024;
public static final byte RESPONSE = 0x13;
public static final byte REQUEST = 0x31;
public static final byte WRITE_VALUE = 0x01;
public static final byte READ_VALUE = 0x02;
public static final byte TASK = 0x04;
public static final byte KEEP_ALIVE = -0x80;
public static final byte[] CMD_HEADER = new byte[]{0x23, 0x01, 0x00, 0x00, 0x00};
// byte[] COMMAND = new byte[]{0x23, 0x01, 0x00, 0x31, 0x00, ... , 0x00}
// | | | | | | Checksum
// | | | | | Command + value
// | | | | Sequence number
// | | | Response/Request indicator
// | | Value length
// | |
// ----- Header
public static final byte[] CMD_FIRMWARE_INFO = new byte[]{0x01, 0x02};
public static final byte[] CMD_AUTHORIZATION_TASK = new byte[]{0x01, 0x05};
public static final byte[] CMD_TIME_SETTINGS = new byte[]{0x01, 0x08};
public static final byte[] CMD_ALARM_SETTINGS = new byte[]{0x01, 0x0A};
public static final byte[] CMD_BATTERY_INFO = new byte[]{0x01, 0x14};
public static final byte[] CMD_NOTIFICATION_TASK = new byte[]{0x03, 0x01};
public static final byte[] CMD_NOTIFICATION_TEXT_TASK = new byte[]{0x03, 0x06};
public static final byte[] CMD_NOTIFICATION_SETTINGS = new byte[]{0x03, 0x02};
public static final byte[] CMD_CALIBRATION_INIT_TASK = new byte[]{0x03, 0x31};
public static final byte[] CMD_CALIBRATION_TASK = new byte[]{0x03, 0x33, 0x01};
public static final byte[] CMD_CALIBRATION_KEEP_ALIVE = new byte[]{0x03, 0x34};
public static final byte[] CMD_DO_NOT_DISTURB_SETTINGS = new byte[]{0x03, 0x61};
public static final byte[] CMD_FITNESS_GOAL_SETTINGS = new byte[]{0x10, 0x02};
public static final byte[] RESP_AUTHORIZATION_TASK = new byte[]{0x01, 0x01, 0x05};
public static final byte[] RESP_BUTTON_INDICATOR = new byte[]{0x04, 0x03, 0x11};
public static final byte[] RESP_ALARM_INDICATOR = new byte[]{-0x80, 0x01, 0x0A};
public static final byte[] RESP_FIRMWARE_INFO = new byte[]{0x08, 0x01, 0x02};
public static final byte[] RESP_TIME_SETTINGS = new byte[]{0x08, 0x01, 0x08};
public static final byte[] RESP_BATTERY_INFO = new byte[]{0x08, 0x01, 0x14};
public static final byte[] RESP_NOTIFICATION_SETTINGS = new byte[]{0x08, 0x03, 0x02};
public static final String ACTION_ENABLE = "action.watch9.enable";
public static final String ACTION_CALIBRATION
= "nodomain.freeyourgadget.gadgetbridge.devices.action.watchxplus.start_calibration";
public static final String ACTION_CALIBRATION_SEND
= "nodomain.freeyourgadget.gadgetbridge.devices.action.watchxplus.send_calibration";
public static final String ACTION_CALIBRATION_HOLD
= "nodomain.freeyourgadget.gadgetbridge.devices.action.watchxplus.keep_calibrating";
public static final String VALUE_CALIBRATION_HOUR
= "value.watch9.calibration_hour";
public static final String VALUE_CALIBRATION_MINUTE
= "value.watch9.calibration_minute";
public static final String VALUE_CALIBRATION_SECOND
= "value.watch9.calibration_second";
}

View File

@ -17,8 +17,8 @@ import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.watch9.Watch9Constants;
import nodomain.freeyourgadget.gadgetbridge.devices.watch9.Watch9PairingActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.watchxplus.WatchXPlusConstants;
import nodomain.freeyourgadget.gadgetbridge.devices.watchxplus.WatchXPlusPairingActivity;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@ -33,9 +33,8 @@ public class WatchXPlusDeviceCoordinator extends AbstractDeviceCoordinator {
@Override
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public Collection<? extends ScanFilter> createBLEScanFilters() {
// TODO constants for watch x plus
//ParcelUuid watchXpService = new ParcelUuid(Watch9Constants.UUID_SERVICE_WATCH9);
ScanFilter filter = new ScanFilter.Builder().setDeviceName("Watch XPlus").build();
ParcelUuid watchXpService = new ParcelUuid(WatchXPlusConstants.UUID_SERVICE_WATCHXPLUS);
ScanFilter filter = new ScanFilter.Builder().setServiceUuid(watchXpService).build();
return Collections.singletonList(filter);
}
@ -45,21 +44,20 @@ public class WatchXPlusDeviceCoordinator extends AbstractDeviceCoordinator {
}
@Override
public int getBondingStyle(GBDevice device) {
public int getBondingStyle() {
return BONDING_STYLE_NONE;
}
@NonNull
@Override
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
// TODO constants and mac for watch x plus
String macAddress = candidate.getMacAddress().toUpperCase();
String deviceName = candidate.getName().toUpperCase();
if (candidate.supportsService(Watch9Constants.UUID_SERVICE_WATCH9)) {
if (candidate.supportsService(WatchXPlusConstants.UUID_SERVICE_WATCHXPLUS)) {
return DeviceType.WATCHXPLUS;
} else if (macAddress.startsWith("1C:87:79")) {
} else if (macAddress.startsWith("DC:41:E5")) {
return DeviceType.WATCHXPLUS;
} else if (deviceName.equals("WATCH XPLUS")) {
} else if (deviceName.equalsIgnoreCase("WATCH XPLUS")) {
return DeviceType.WATCHXPLUS;
}
return DeviceType.UNKNOWN;
@ -73,8 +71,7 @@ public class WatchXPlusDeviceCoordinator extends AbstractDeviceCoordinator {
@Nullable
@Override
public Class<? extends Activity> getPairingActivity() {
// TODO watch X plus!
return Watch9PairingActivity.class;
return WatchXPlusPairingActivity.class;
}
@Override

View File

@ -0,0 +1,129 @@
/* Copyright (C) 2018-2019 Daniele Gobbetti, maxirnilian
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.watchxplus;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.widget.TextView;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2;
import nodomain.freeyourgadget.gadgetbridge.activities.DiscoveryActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class WatchXPlusPairingActivity extends AbstractGBActivity {
private static final Logger LOG = LoggerFactory.getLogger(WatchXPlusPairingActivity.class);
private static final String STATE_DEVICE_CANDIDATE = "stateDeviceCandidate";
private TextView message;
private GBDeviceCandidate deviceCandidate;
private final BroadcastReceiver mPairingReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (GBDevice.ACTION_DEVICE_CHANGED.equals(intent.getAction())) {
GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
LOG.debug("pairing activity: device changed: " + device);
if (deviceCandidate.getMacAddress().equals(device.getAddress())) {
if (device.isInitialized()) {
pairingFinished();
} else if (device.isConnecting() || device.isInitializing()) {
LOG.info("still connecting/initializing device...");
}
}
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_watch9_pairing);
message = findViewById(R.id.watch9_pair_message);
Intent intent = getIntent();
deviceCandidate = intent.getParcelableExtra(DeviceCoordinator.EXTRA_DEVICE_CANDIDATE);
if (deviceCandidate == null && savedInstanceState != null) {
deviceCandidate = savedInstanceState.getParcelable(STATE_DEVICE_CANDIDATE);
}
if (deviceCandidate == null) {
Toast.makeText(this, getString(R.string.message_cannot_pair_no_mac), Toast.LENGTH_SHORT).show();
startActivity(new Intent(this, DiscoveryActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
finish();
return;
}
startPairing();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(STATE_DEVICE_CANDIDATE, deviceCandidate);
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
deviceCandidate = savedInstanceState.getParcelable(STATE_DEVICE_CANDIDATE);
}
@Override
protected void onDestroy() {
AndroidUtils.safeUnregisterBroadcastReceiver(LocalBroadcastManager.getInstance(this), mPairingReceiver);
super.onDestroy();
}
private void startPairing() {
message.setText(getString(R.string.pairing, deviceCandidate));
IntentFilter filter = new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(mPairingReceiver, filter);
GBApplication.deviceService().disconnect();
GBDevice device = DeviceHelper.getInstance().toSupportedDevice(deviceCandidate);
if (device != null) {
GBApplication.deviceService().connect(device, true);
} else {
GB.toast(this, "Unable to connect, can't recognize the device type: " + deviceCandidate, Toast.LENGTH_LONG, GB.ERROR);
}
}
private void pairingFinished() {
AndroidUtils.safeUnregisterBroadcastReceiver(LocalBroadcastManager.getInstance(this), mPairingReceiver);
Intent intent = new Intent(this, ControlCenterv2.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
finish();
}
}

View File

@ -52,6 +52,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleSupport
import nodomain.freeyourgadget.gadgetbridge.service.devices.roidmi.RoidmiSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vibratissimo.VibratissimoSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.watch9.Watch9DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.watchxplus.WatchXPlusDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xwatch.XWatchSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.zetime.ZeTimeDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@ -183,6 +184,9 @@ public class DeviceSupportFactory {
case WATCH9:
deviceSupport = new ServiceDeviceSupport(new Watch9DeviceSupport(), EnumSet.of(ServiceDeviceSupport.Flags.THROTTLING, ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;
case WATCHXPLUS:
deviceSupport = new ServiceDeviceSupport(new WatchXPlusDeviceSupport(), EnumSet.of(ServiceDeviceSupport.Flags.THROTTLING, ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;
case ROIDMI:
deviceSupport = new ServiceDeviceSupport(new RoidmiSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;

View File

@ -0,0 +1,649 @@
/* Copyright (C) 2018-2019 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, maxirnilian, Sebastian Kranz
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.watchxplus;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import androidx.annotation.IntRange;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
import nodomain.freeyourgadget.gadgetbridge.devices.watchxplus.WatchXPlusConstants;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
import nodomain.freeyourgadget.gadgetbridge.service.devices.watchxplus.operations.InitOperation;
import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils;
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
public class WatchXPlusDeviceSupport extends AbstractBTLEDeviceSupport {
private boolean needsAuth;
private int sequenceNumber = 0;
private boolean isCalibrationActive = false;
private byte ACK_CALIBRATION = 0;
private final GBDeviceEventVersionInfo versionInfo = new GBDeviceEventVersionInfo();
private final GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo();
private static final Logger LOG = LoggerFactory.getLogger(WatchXPlusDeviceSupport.class);
private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String broadcastAction = intent.getAction();
switch (broadcastAction) {
case WatchXPlusConstants.ACTION_CALIBRATION:
enableCalibration(intent.getBooleanExtra(WatchXPlusConstants.ACTION_ENABLE, false));
break;
case WatchXPlusConstants.ACTION_CALIBRATION_SEND:
int hour = intent.getIntExtra(WatchXPlusConstants.VALUE_CALIBRATION_HOUR, -1);
int minute = intent.getIntExtra(WatchXPlusConstants.VALUE_CALIBRATION_MINUTE, -1);
int second = intent.getIntExtra(WatchXPlusConstants.VALUE_CALIBRATION_SECOND, -1);
if (hour != -1 && minute != -1 && second != -1) {
sendCalibrationData(hour, minute, second);
}
break;
case WatchXPlusConstants.ACTION_CALIBRATION_HOLD:
holdCalibration();
break;
}
}
};
public WatchXPlusDeviceSupport() {
super(LOG);
addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS);
addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE);
addSupportedService(WatchXPlusConstants.UUID_SERVICE_WATCHXPLUS);
LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext());
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(WatchXPlusConstants.ACTION_CALIBRATION);
intentFilter.addAction(WatchXPlusConstants.ACTION_CALIBRATION_SEND);
intentFilter.addAction(WatchXPlusConstants.ACTION_CALIBRATION_HOLD);
broadcastManager.registerReceiver(broadcastReceiver, intentFilter);
}
@Override
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
try {
boolean auth = needsAuth;
needsAuth = false;
new InitOperation(auth, this, builder).perform();
} catch (IOException e) {
e.printStackTrace();
}
return builder;
}
@Override
public boolean connectFirstTime() {
needsAuth = true;
return super.connect();
}
@Override
public boolean useAutoConnect() {
return true;
}
@Override
public void onNotification(NotificationSpec notificationSpec) {
String senderOrTitle = StringUtils.getFirstOf(notificationSpec.sender, notificationSpec.title);
String message = StringUtils.truncate(senderOrTitle, 32) + "\0";
// TODO: Commented out to simplify testing
// if (notificationSpec.subject != null) {
// message += StringUtils.truncate(notificationSpec.subject, 128) + "\n\n";
// }
// if (notificationSpec.body != null) {
// message += StringUtils.truncate(notificationSpec.body, 128);
// }
sendNotification(WatchXPlusConstants.NOTIFICATION_CHANNEL_DEFAULT, message);
}
private void sendNotification(int notificationChannel, String notificationText) {
try {
TransactionBuilder builder = performInitialized("showNotification");
byte[] command = WatchXPlusConstants.CMD_NOTIFICATION_TEXT_TASK;
byte[] text = notificationText.getBytes("UTF-8");
byte[] value = new byte[text.length + 2];
value[0] = (byte)(notificationChannel);
// TODO: Split message into 9-byte arrays and send them one by one.
// Set the message index to FF to indicate end of message
value[1] = (byte) 0xFF;
System.arraycopy(text, 0, value, 2, text.length);
builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
buildCommand(command,
WatchXPlusConstants.KEEP_ALIVE,
value));
performImmediately(builder);
} catch (IOException e) {
LOG.warn("Unable to send notification", e);
}
}
private WatchXPlusDeviceSupport enableNotificationChannels(TransactionBuilder builder) {
builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
buildCommand(WatchXPlusConstants.CMD_NOTIFICATION_SETTINGS,
WatchXPlusConstants.WRITE_VALUE,
new byte[]{(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF}));
return this;
}
public WatchXPlusDeviceSupport authorizationRequest(TransactionBuilder builder, boolean firstConnect) {
builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
buildCommand(WatchXPlusConstants.CMD_AUTHORIZATION_TASK,
WatchXPlusConstants.TASK,
new byte[]{(byte) (firstConnect ? 0x00 : 0x01)})); //possibly not the correct meaning
return this;
}
private WatchXPlusDeviceSupport enableDoNotDisturb(TransactionBuilder builder, boolean active) {
builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
buildCommand(WatchXPlusConstants.CMD_DO_NOT_DISTURB_SETTINGS,
WatchXPlusConstants.WRITE_VALUE,
new byte[]{(byte) (active ? 0x01 : 0x00)}));
return this;
}
private void enableCalibration(boolean enable) {
try {
TransactionBuilder builder = performInitialized("enableCalibration");
builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
buildCommand(WatchXPlusConstants.CMD_CALIBRATION_INIT_TASK,
WatchXPlusConstants.TASK,
new byte[]{(byte) (enable ? 0x01 : 0x00)}));
performImmediately(builder);
} catch (IOException e) {
LOG.warn("Unable to start/stop calibration mode", e);
}
}
private void holdCalibration() {
try {
TransactionBuilder builder = performInitialized("holdCalibration");
builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
buildCommand(WatchXPlusConstants.CMD_CALIBRATION_KEEP_ALIVE,
WatchXPlusConstants.KEEP_ALIVE));
performImmediately(builder);
} catch (IOException e) {
LOG.warn("Unable to keep calibration mode alive", e);
}
}
private void sendCalibrationData(@IntRange(from=0,to=23)int hour, @IntRange(from=0,to=59)int minute, @IntRange(from=0,to=59)int second) {
try {
isCalibrationActive = true;
TransactionBuilder builder = performInitialized("calibrate");
int handsPosition = ((hour % 12) * 60 + minute) * 60 + second;
builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
buildCommand(WatchXPlusConstants.CMD_CALIBRATION_TASK,
WatchXPlusConstants.TASK,
Conversion.toByteArr16(handsPosition)));
performImmediately(builder);
} catch (IOException e) {
isCalibrationActive = false;
LOG.warn("Unable to send calibration data", e);
}
}
private void getTime() {
try {
TransactionBuilder builder = performInitialized("getTime");
builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
buildCommand(WatchXPlusConstants.CMD_TIME_SETTINGS,
WatchXPlusConstants.READ_VALUE));
performImmediately(builder);
} catch (IOException e) {
LOG.warn("Unable to get device time", e);
}
}
private void handleTime(byte[] time) {
GregorianCalendar now = BLETypeConversions.createCalendar();
GregorianCalendar nowDevice = BLETypeConversions.createCalendar();
int year = (nowDevice.get(Calendar.YEAR) / 100) * 100 + Conversion.fromBcd8(time[8]);
nowDevice.set(year,
Conversion.fromBcd8(time[9]) - 1,
Conversion.fromBcd8(time[10]),
Conversion.fromBcd8(time[11]),
Conversion.fromBcd8(time[12]),
Conversion.fromBcd8(time[13]));
nowDevice.set(Calendar.DAY_OF_WEEK, Conversion.fromBcd8(time[16]) + 1);
long timeDiff = (Math.abs(now.getTimeInMillis() - nowDevice.getTimeInMillis())) / 1000;
if (10 < timeDiff && timeDiff < 120) {
enableCalibration(true);
setTime(BLETypeConversions.createCalendar());
enableCalibration(false);
}
}
private void setTime(Calendar calendar) {
try {
TransactionBuilder builder = performInitialized("setTime");
int timezoneOffsetMinutes = (calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET)) / (60 * 1000);
int timezoneOffsetIndustrialMinutes = Math.round((Math.abs(timezoneOffsetMinutes) % 60) * 100f / 60f);
byte[] time = new byte[]{Conversion.toBcd8(calendar.get(Calendar.YEAR) % 100),
Conversion.toBcd8(calendar.get(Calendar.MONTH) + 1),
Conversion.toBcd8(calendar.get(Calendar.DAY_OF_MONTH)),
Conversion.toBcd8(calendar.get(Calendar.HOUR_OF_DAY)),
Conversion.toBcd8(calendar.get(Calendar.MINUTE)),
Conversion.toBcd8(calendar.get(Calendar.SECOND)),
(byte) (timezoneOffsetMinutes / 60),
(byte) timezoneOffsetIndustrialMinutes,
(byte) (calendar.get(Calendar.DAY_OF_WEEK) - 1)
};
builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
buildCommand(WatchXPlusConstants.CMD_TIME_SETTINGS,
WatchXPlusConstants.WRITE_VALUE,
time));
performImmediately(builder);
} catch (IOException e) {
LOG.warn("Unable to set time", e);
}
}
public WatchXPlusDeviceSupport getFirmwareVersion(TransactionBuilder builder) {
builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
buildCommand(WatchXPlusConstants.CMD_FIRMWARE_INFO,
WatchXPlusConstants.READ_VALUE));
return this;
}
private WatchXPlusDeviceSupport getBatteryState(TransactionBuilder builder) {
builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
buildCommand(WatchXPlusConstants.CMD_BATTERY_INFO,
WatchXPlusConstants.READ_VALUE));
return this;
}
private WatchXPlusDeviceSupport setFitnessGoal(TransactionBuilder builder) {
int fitnessGoal = new ActivityUser().getStepsGoal();
builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
buildCommand(WatchXPlusConstants.CMD_FITNESS_GOAL_SETTINGS,
WatchXPlusConstants.WRITE_VALUE,
Conversion.toByteArr16(fitnessGoal)));
return this;
}
public WatchXPlusDeviceSupport initialize(TransactionBuilder builder) {
getFirmwareVersion(builder)
.getBatteryState(builder)
.enableNotificationChannels(builder)
.enableDoNotDisturb(builder, false)
.setFitnessGoal(builder);
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
builder.setGattCallback(this);
return this;
}
@Override
public void onDeleteNotification(int id) {
}
@Override
public void onSetTime() {
getTime();
}
@Override
public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
try {
TransactionBuilder builder = performInitialized("setAlarms");
for (Alarm alarm : alarms) {
setAlarm(alarm, alarm.getPosition() + 1, builder);
}
builder.queue(getQueue());
} catch (IOException e) {
LOG.warn("Unable to set alarms", e);
}
}
// No useful use case at the moment, used to clear alarm slots for testing.
private void deleteAlarm(TransactionBuilder builder, int index) {
if (0 < index && index < 4) {
byte[] alarmValue = new byte[]{(byte) index, 0x00, 0x00, 0x00, 0x00, 0x00};
builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
buildCommand(WatchXPlusConstants.CMD_ALARM_SETTINGS,
WatchXPlusConstants.WRITE_VALUE,
alarmValue));
}
}
private void setAlarm(Alarm alarm, int index, TransactionBuilder builder) {
// Shift the GB internal repetition mask to match the device specific one.
byte repetitionMask = (byte) ((alarm.getRepetition() << 1) | (alarm.isRepetitive() ? 0x80 : 0x00));
repetitionMask |= (alarm.getRepetition(Alarm.ALARM_SUN) ? 0x01 : 0x00);
if (0 < index && index < 4) {
byte[] alarmValue = new byte[]{(byte) index,
Conversion.toBcd8(AlarmUtils.toCalendar(alarm).get(Calendar.HOUR_OF_DAY)),
Conversion.toBcd8(AlarmUtils.toCalendar(alarm).get(Calendar.MINUTE)),
repetitionMask,
(byte) (alarm.getEnabled() ? 0x01 : 0x00),
0x00 // TODO: Unknown
};
builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE),
buildCommand(WatchXPlusConstants.CMD_ALARM_SETTINGS,
WatchXPlusConstants.WRITE_VALUE,
alarmValue));
}
}
@Override
public void onSetCallState(CallSpec callSpec) {
switch (callSpec.command) {
case CallSpec.CALL_INCOMING:
sendNotification(WatchXPlusConstants.NOTIFICATION_CHANNEL_PHONE_CALL, callSpec.name);
break;
case CallSpec.CALL_START:
case CallSpec.CALL_END:
// sendNotification(WatchXPlusConstants.NOTIFICATION_CHANNEL_PHONE_CALL, true);
// break;
default:
break;
}
}
@Override
public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) {
}
@Override
public void onSetMusicState(MusicStateSpec stateSpec) {
}
@Override
public void onSetMusicInfo(MusicSpec musicSpec) {
}
@Override
public void onEnableRealtimeSteps(boolean enable) {
}
@Override
public void onInstallApp(Uri uri) {
}
@Override
public void onAppInfoReq() {
}
@Override
public void onAppStart(UUID uuid, boolean start) {
}
@Override
public void onAppDelete(UUID uuid) {
}
@Override
public void onAppConfiguration(UUID appUuid, String config, Integer id) {
}
@Override
public void onAppReorder(UUID[] uuids) {
}
@Override
public void onFetchRecordedData(int dataTypes) {
}
@Override
public void onReset(int flags) {
}
@Override
public void onHeartRateTest() {
}
@Override
public void onEnableRealtimeHeartRateMeasurement(boolean enable) {
}
@Override
public void onFindDevice(boolean start) {
}
@Override
public void onSetConstantVibration(int integer) {
}
@Override
public void onScreenshotReq() {
}
@Override
public void onEnableHeartRateSleepSupport(boolean enable) {
}
@Override
public void onSetHeartRateMeasurementInterval(int seconds) {
}
@Override
public void onAddCalendarEvent(CalendarEventSpec calendarEventSpec) {
}
@Override
public void onDeleteCalendarEvent(byte type, long id) {
}
@Override
public void onSendConfiguration(String config) {
TransactionBuilder builder;
try {
builder = performInitialized("sendConfig: " + config);
switch (config) {
case ActivityUser.PREF_USER_STEPS_GOAL:
setFitnessGoal(builder);
break;
}
builder.queue(getQueue());
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void onReadConfiguration(String config) {
}
@Override
public void onTestNewFunction() {
}
@Override
public void onSendWeather(WeatherSpec weatherSpec) {
}
@Override
public boolean onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
UUID characteristicUUID = characteristic.getUuid();
if (WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE.equals(characteristicUUID)) {
byte[] value = characteristic.getValue();
if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_FIRMWARE_INFO, 5)) {
handleFirmwareInfo(value);
} else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_BATTERY_INFO, 5)) {
handleBatteryState(value);
} else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_TIME_SETTINGS, 5)) {
handleTime(value);
} else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_BUTTON_INDICATOR, 5)) {
LOG.info("Unhandled action: Button pressed");
} else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_ALARM_INDICATOR, 5)) {
LOG.info("Alarm active: id=" + value[8]);
} else if (isCalibrationActive && value.length == 7 && value[4] == ACK_CALIBRATION) {
setTime(BLETypeConversions.createCalendar());
isCalibrationActive = false;
}
return true;
} else {
LOG.info("Unhandled characteristic changed: " + characteristicUUID);
logMessageContent(characteristic.getValue());
}
return false;
}
private byte[] buildCommand(byte[] command, byte action) {
return buildCommand(command, action, null);
}
private byte[] buildCommand(byte[] command, byte action, byte[] value) {
if (Arrays.equals(command, WatchXPlusConstants.CMD_CALIBRATION_TASK)) {
ACK_CALIBRATION = (byte) sequenceNumber;
}
command = BLETypeConversions.join(command, value);
byte[] result = new byte[7 + command.length];
System.arraycopy(WatchXPlusConstants.CMD_HEADER, 0, result, 0, 5);
System.arraycopy(command, 0, result, 6, command.length);
result[2] = (byte) (command.length + 1);
result[3] = WatchXPlusConstants.REQUEST;
result[4] = (byte) sequenceNumber++;
result[5] = action;
result[result.length - 1] = calculateChecksum(result);
return result;
}
private byte calculateChecksum(byte[] bytes) {
byte checksum = 0x00;
for (int i = 0; i < bytes.length - 1; i++) {
checksum += (bytes[i] ^ i) & 0xFF;
}
return (byte) (checksum & 0xFF);
}
private void handleFirmwareInfo(byte[] value) {
versionInfo.fwVersion = String.format(Locale.US,"%d.%d.%d", value[8], value[9], value[10]);
handleGBDeviceEvent(versionInfo);
}
private void handleBatteryState(byte[] value) {
batteryInfo.state = value[8] == 1 ? BatteryState.BATTERY_NORMAL : BatteryState.BATTERY_LOW;
batteryInfo.level = value[9];
handleGBDeviceEvent(batteryInfo);
}
@Override
public void dispose() {
LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext());
broadcastManager.unregisterReceiver(broadcastReceiver);
super.dispose();
}
private static class Conversion {
static byte toBcd8(@IntRange(from = 0, to = 99) int value) {
int high = (value / 10) << 4;
int low = value % 10;
return (byte) (high | low);
}
static int fromBcd8(byte value) {
int high = ((value & 0xF0) >> 4) * 10;
int low = value & 0x0F;
return high + low;
}
static byte[] toByteArr16(int value) {
return new byte[]{(byte) (value >> 8), (byte) value};
}
static byte[] toByteArr32(int value) {
return new byte[]{(byte) (value >> 24),
(byte) (value >> 16),
(byte) (value >> 8),
(byte) value};
}
}
}

View File

@ -0,0 +1,94 @@
/* Copyright (C) 2018-2019 maxirnilian
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.watchxplus.operations;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.devices.watch9.Watch9Constants;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEOperation;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
import nodomain.freeyourgadget.gadgetbridge.service.devices.watch9.Watch9DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.watchxplus.WatchXPlusDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class InitOperation extends AbstractBTLEOperation<WatchXPlusDeviceSupport>{
private static final Logger LOG = LoggerFactory.getLogger(InitOperation.class);
private final TransactionBuilder builder;
private final boolean needsAuth;
private final BluetoothGattCharacteristic cmdCharacteristic = getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE);
public InitOperation(boolean needsAuth, WatchXPlusDeviceSupport support, TransactionBuilder builder) {
super(support);
this.needsAuth = needsAuth;
this.builder = builder;
builder.setGattCallback(this);
}
@Override
protected void doPerform() throws IOException {
builder.notify(cmdCharacteristic, true);
if (needsAuth) {
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.AUTHENTICATING, getContext()));
getSupport().authorizationRequest(builder, needsAuth);
} else {
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
getSupport().initialize(builder);
getSupport().performImmediately(builder);
}
}
@Override
public boolean onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
UUID characteristicUUID = characteristic.getUuid();
if (Watch9Constants.UUID_CHARACTERISTIC_WRITE.equals(characteristicUUID) && needsAuth) {
try {
byte[] value = characteristic.getValue();
getSupport().logMessageContent(value);
if (ArrayUtils.equals(value, Watch9Constants.RESP_AUTHORIZATION_TASK, 5) && value[8] == 0x01) {
TransactionBuilder builder = getSupport().createTransactionBuilder("authInit");
builder.setGattCallback(this);
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
getSupport().initialize(builder).performImmediately(builder);
} else {
return super.onCharacteristicChanged(gatt, characteristic);
}
} catch (Exception e) {
GB.toast(getContext(), "Error authenticating Watch 9", Toast.LENGTH_LONG, GB.ERROR, e);
}
return true;
} else {
LOG.info("Unhandled characteristic changed: " + characteristicUUID);
return super.onCharacteristicChanged(gatt, characteristic);
}
}
}