mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Pebble: Rewrite pairing flow to be more cleanly structured
This commit is contained in:
+35
@@ -0,0 +1,35 @@
|
||||
/* Copyright (C) 2024 Gadgetbridge contributors
|
||||
|
||||
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.pebble.ble;
|
||||
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
|
||||
/**
|
||||
* Callback interface for pairing flow completion.
|
||||
* Allows pairing flows to notify PebbleGATTClient when pairing is complete
|
||||
* and the appropriate subscription chain should begin.
|
||||
*/
|
||||
interface PairingCallback {
|
||||
/**
|
||||
* Called when pairing is complete (or skipped) and subscription chain should start.
|
||||
*
|
||||
* @param gatt The GATT connection
|
||||
* @param hasConnectivityCharacteristics true if device has connectivity characteristic (modern),
|
||||
* false if using connection parameters characteristic (legacy)
|
||||
*/
|
||||
void onPairingComplete(BluetoothGatt gatt, boolean hasConnectivityCharacteristics);
|
||||
}
|
||||
+63
-319
@@ -17,7 +17,6 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.ble;
|
||||
|
||||
import static android.bluetooth.BluetoothGattCharacteristic.FORMAT_UINT16;
|
||||
import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_WRITE;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
@@ -26,14 +25,7 @@ import android.bluetooth.BluetoothGattCallback;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.bluetooth.BluetoothGattDescriptor;
|
||||
import android.bluetooth.BluetoothGattService;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -51,21 +43,10 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.ValueDecoder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
class PebbleGATTClient extends BluetoothGattCallback {
|
||||
class PebbleGATTClient extends BluetoothGattCallback implements PairingCallback {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PebbleGATTClient.class);
|
||||
|
||||
private static final UUID SERVICE_UUID = UUID.fromString("0000fed9-0000-1000-8000-00805f9b34fb");
|
||||
private static final UUID CONNECTIVITY_CHARACTERISTIC = UUID.fromString("00000001-328E-0FBB-C642-1AA6699BDADA");
|
||||
private static final UUID PAIRING_TRIGGER_CHARACTERISTIC = UUID.fromString("00000002-328E-0FBB-C642-1AA6699BDADA");
|
||||
private static final UUID MTU_CHARACTERISTIC = UUID.fromString("00000003-328e-0fbb-c642-1aa6699bdada");
|
||||
private static final UUID CONNECTION_PARAMETERS_CHARACTERISTIC = UUID.fromString("00000005-328E-0FBB-C642-1AA6699BDADA");
|
||||
private static final UUID CHARACTERISTIC_CONFIGURATION_DESCRIPTOR = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
|
||||
|
||||
//PPoGATT service (Pebble side)
|
||||
private static final UUID PPOGATT_SERVICE_UUID = UUID.fromString("30000003-328E-0FBB-C642-1AA6699BDADA");
|
||||
private static final UUID PPOGATT_CHARACTERISTIC_READ = UUID.fromString("30000004-328E-0FBB-C642-1AA6699BDADA");
|
||||
private static final UUID PPOGATT_CHARACTERISTIC_WRITE = UUID.fromString("30000006-328E-0FBB-C642-1AA6699BDADA");
|
||||
|
||||
private BluetoothGattCharacteristic writeCharacteristics;
|
||||
|
||||
@@ -73,16 +54,12 @@ class PebbleGATTClient extends BluetoothGattCallback {
|
||||
private final PebbleLESupport mPebbleLESupport;
|
||||
|
||||
private boolean hasConnectivityCharacteristics = false;
|
||||
private final boolean doPairing = true;
|
||||
private BluetoothGatt mBluetoothGatt;
|
||||
|
||||
private CountDownLatch mWaitWriteCompleteLatch;
|
||||
|
||||
// New pairing state management
|
||||
private ConnectivityStatus mConnectivityStatus;
|
||||
private CountDownLatch mBondingLatch;
|
||||
private BroadcastReceiver mBondingReceiver;
|
||||
private static final long BONDING_TIMEOUT_MS = 60000; // 60 seconds
|
||||
// Pairing flow strategy
|
||||
private PebblePairingFlow mPairingFlow;
|
||||
|
||||
PebbleGATTClient(PebbleLESupport pebbleLESupport, Context context, BluetoothDevice btDevice) {
|
||||
mContext = context;
|
||||
@@ -96,11 +73,11 @@ class PebbleGATTClient extends BluetoothGattCallback {
|
||||
return;
|
||||
}
|
||||
|
||||
if (characteristic.getUuid().equals(MTU_CHARACTERISTIC)) {
|
||||
if (characteristic.getUuid().equals(PebbleGATTConstants.MTU_CHARACTERISTIC)) {
|
||||
int newMTU = characteristic.getIntValue(FORMAT_UINT16, 0);
|
||||
LOG.info("Pebble requested MTU: {}", newMTU);
|
||||
mPebbleLESupport.setMTU(newMTU);
|
||||
} else if (characteristic.getUuid().equals(PPOGATT_CHARACTERISTIC_READ)) {
|
||||
} else if (characteristic.getUuid().equals(PebbleGATTConstants.PPOGATT_CHARACTERISTIC_READ)) {
|
||||
mPebbleLESupport.handlePPoGATTPacket(characteristic.getValue().clone());
|
||||
} else if (characteristic.getUuid().equals(GattCharacteristic.UUID_CHARACTERISTIC_BATTERY_LEVEL)) {
|
||||
int battery_percent = ValueDecoder.decodePercent(characteristic, characteristic.getValue());
|
||||
@@ -120,24 +97,19 @@ class PebbleGATTClient extends BluetoothGattCallback {
|
||||
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||
LOG.info("onCharacteristicRead() {} {}", characteristic.getUuid().toString(), GB.hexdump(characteristic.getValue(), 0, -1));
|
||||
|
||||
if (characteristic.getUuid().equals(CONNECTIVITY_CHARACTERISTIC)) {
|
||||
// This is required for Pebble Time 2 (EMERY) pairing
|
||||
handleConnectivityRead(gatt, characteristic.getValue());
|
||||
} else if (characteristic.getUuid().equals(GattCharacteristic.UUID_CHARACTERISTIC_BATTERY_LEVEL)) {
|
||||
// Delegate to pairing flow if applicable
|
||||
if (mPairingFlow != null && mPairingFlow.onCharacteristicRead(gatt, characteristic, characteristic.getValue())) {
|
||||
return; // Pairing flow handled it
|
||||
}
|
||||
|
||||
// Handle non-pairing characteristics
|
||||
if (characteristic.getUuid().equals(GattCharacteristic.UUID_CHARACTERISTIC_BATTERY_LEVEL)) {
|
||||
int battery_percent = ValueDecoder.decodePercent(characteristic, characteristic.getValue());
|
||||
LOG.info("Got battery level through read, is at {}%", battery_percent);
|
||||
GBDeviceEventBatteryInfo gbDeviceEventBatteryInfo = new GBDeviceEventBatteryInfo();
|
||||
gbDeviceEventBatteryInfo.level = battery_percent;
|
||||
gbDeviceEventBatteryInfo.state = BatteryState.BATTERY_NORMAL;
|
||||
mPebbleLESupport.getPebbleSupport().evaluateGBDeviceEvent(gbDeviceEventBatteryInfo);
|
||||
} else if (characteristic.getUuid().equals(PAIRING_TRIGGER_CHARACTERISTIC)) {
|
||||
// Legacy path: this is just a hack to force sequential ble commands for initialization
|
||||
// Only happens when READING the pairing trigger for old pebbles running fw 3.x
|
||||
if (hasConnectivityCharacteristics) {
|
||||
subscribeToConnectivity(gatt);
|
||||
} else {
|
||||
subscribeToConnectionParams(gatt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,7 +134,14 @@ class PebbleGATTClient extends BluetoothGattCallback {
|
||||
if (mPebbleLESupport.isUnexpectedDevice(gatt.getDevice())) {
|
||||
return;
|
||||
}
|
||||
if (characteristic.getUuid().equals(PPOGATT_CHARACTERISTIC_WRITE)) {
|
||||
|
||||
// Delegate to pairing flow if applicable
|
||||
if (mPairingFlow != null && mPairingFlow.onCharacteristicWrite(gatt, characteristic, status)) {
|
||||
return; // Pairing flow handled it
|
||||
}
|
||||
|
||||
// Handle non-pairing characteristics
|
||||
if (characteristic.getUuid().equals(PebbleGATTConstants.PPOGATT_CHARACTERISTIC_WRITE)) {
|
||||
if (status != BluetoothGatt.GATT_SUCCESS) {
|
||||
LOG.error("something went wrong when writing to PPoGATT characteristics");
|
||||
}
|
||||
@@ -171,19 +150,7 @@ class PebbleGATTClient extends BluetoothGattCallback {
|
||||
} else {
|
||||
LOG.warn("mWaitWriteCompleteLatch is null!");
|
||||
}
|
||||
} else if (characteristic.getUuid().equals(PAIRING_TRIGGER_CHARACTERISTIC)) {
|
||||
// Pairing trigger written - now initiate Bluetooth bonding
|
||||
// This is required for Pebble Time 2 (EMERY) pairing
|
||||
LOG.info("Pairing trigger written successfully (status={}), initiating bond", status);
|
||||
initiateBluetoothBond(gatt);
|
||||
} else if (characteristic.getUuid().equals(CONNECTIVITY_CHARACTERISTIC)) {
|
||||
// Legacy path for connectivity characteristic write
|
||||
if (hasConnectivityCharacteristics) {
|
||||
subscribeToConnectivity(gatt);
|
||||
} else {
|
||||
subscribeToConnectionParams(gatt);
|
||||
}
|
||||
} else if (characteristic.getUuid().equals(MTU_CHARACTERISTIC)) {
|
||||
} else if (characteristic.getUuid().equals(PebbleGATTConstants.MTU_CHARACTERISTIC)) {
|
||||
gatt.requestMtu(339);
|
||||
}
|
||||
}
|
||||
@@ -200,17 +167,17 @@ class PebbleGATTClient extends BluetoothGattCallback {
|
||||
|
||||
// this is just a hack to force sequential ble commands for initialization
|
||||
// kind of event driven
|
||||
if (CHARACTERISTICUUID.equals(CONNECTION_PARAMETERS_CHARACTERISTIC)) {
|
||||
if (CHARACTERISTICUUID.equals(PebbleGATTConstants.CONNECTION_PARAMETERS_CHARACTERISTIC)) {
|
||||
subscribeToConnectivity(gatt);
|
||||
} else if (CHARACTERISTICUUID.equals(CONNECTIVITY_CHARACTERISTIC)) {
|
||||
} else if (CHARACTERISTICUUID.equals(PebbleGATTConstants.CONNECTIVITY_CHARACTERISTIC)) {
|
||||
subscribeToMTUOrBattery(gatt);
|
||||
} else if (CHARACTERISTICUUID.equals(MTU_CHARACTERISTIC) || CHARACTERISTICUUID.equals(GattCharacteristic.UUID_CHARACTERISTIC_BATTERY_LEVEL)) {
|
||||
} else if (CHARACTERISTICUUID.equals(PebbleGATTConstants.MTU_CHARACTERISTIC) || CHARACTERISTICUUID.equals(GattCharacteristic.UUID_CHARACTERISTIC_BATTERY_LEVEL)) {
|
||||
if (mPebbleLESupport.clientOnly) {
|
||||
subscribeToPPoGATT(gatt);
|
||||
} else {
|
||||
setMTU(gatt);
|
||||
}
|
||||
} else if (CHARACTERISTICUUID.equals(PPOGATT_CHARACTERISTIC_READ)) {
|
||||
} else if (CHARACTERISTICUUID.equals(PebbleGATTConstants.PPOGATT_CHARACTERISTIC_READ)) {
|
||||
setMTU(gatt);
|
||||
}
|
||||
}
|
||||
@@ -227,41 +194,35 @@ class PebbleGATTClient extends BluetoothGattCallback {
|
||||
return;
|
||||
}
|
||||
|
||||
BluetoothGattService pairingService = gatt.getService(SERVICE_UUID);
|
||||
BluetoothGattService pairingService = gatt.getService(PebbleGATTConstants.SERVICE_UUID);
|
||||
if (pairingService == null) {
|
||||
LOG.error("Pairing service not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Detect device characteristics for subscription chain selection
|
||||
BluetoothGattCharacteristic connectionParamCharacteristic =
|
||||
pairingService.getCharacteristic(CONNECTION_PARAMETERS_CHARACTERISTIC);
|
||||
pairingService.getCharacteristic(PebbleGATTConstants.CONNECTION_PARAMETERS_CHARACTERISTIC);
|
||||
hasConnectivityCharacteristics = connectionParamCharacteristic == null;
|
||||
|
||||
if (hasConnectivityCharacteristics) {
|
||||
LOG.info("This seems to be an older LE Pebble (Pebble Time), or a 2025 Pebble");
|
||||
}
|
||||
|
||||
if (doPairing) {
|
||||
// Read Connectivity characteristic FIRST to determine pairing state
|
||||
// This is required for Pebble Time 2 (EMERY) pairing
|
||||
BluetoothGattCharacteristic connectivityChar =
|
||||
pairingService.getCharacteristic(CONNECTIVITY_CHARACTERISTIC);
|
||||
if (connectivityChar != null) {
|
||||
LOG.info("Reading connectivity characteristic to check pairing state");
|
||||
gatt.readCharacteristic(connectivityChar);
|
||||
// Continue in onCharacteristicRead() -> handleConnectivityRead()
|
||||
} else {
|
||||
// Fallback for very old firmware - use legacy method
|
||||
LOG.warn("Connectivity characteristic not found, using legacy pairing");
|
||||
proceedWithLegacyPairing(gatt);
|
||||
}
|
||||
} else {
|
||||
if (hasConnectivityCharacteristics) {
|
||||
subscribeToConnectivity(gatt);
|
||||
} else {
|
||||
subscribeToConnectionParams(gatt);
|
||||
}
|
||||
// Get device model for hardware-based pairing flow detection
|
||||
String deviceModel = null;
|
||||
try {
|
||||
deviceModel = mPebbleLESupport.getPebbleSupport().getDevice().getModel();
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Could not get device model: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// Create appropriate pairing flow
|
||||
mPairingFlow = PebblePairingFlowFactory.createPairingFlow(
|
||||
mContext, deviceModel, pairingService, mPebbleLESupport.clientOnly, this);
|
||||
|
||||
// Start pairing
|
||||
mPairingFlow.startPairing(gatt, pairingService);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -293,18 +254,18 @@ class PebbleGATTClient extends BluetoothGattCallback {
|
||||
|
||||
private void subscribeToConnectivity(BluetoothGatt gatt) {
|
||||
LOG.info("subscribing to connectivity characteristic");
|
||||
BluetoothGattDescriptor descriptor = gatt.getService(SERVICE_UUID).getCharacteristic(CONNECTIVITY_CHARACTERISTIC).getDescriptor(CHARACTERISTIC_CONFIGURATION_DESCRIPTOR);
|
||||
BluetoothGattDescriptor descriptor = gatt.getService(PebbleGATTConstants.SERVICE_UUID).getCharacteristic(PebbleGATTConstants.CONNECTIVITY_CHARACTERISTIC).getDescriptor(PebbleGATTConstants.CHARACTERISTIC_CONFIGURATION_DESCRIPTOR);
|
||||
NotifyAction.writeDescriptor(gatt, descriptor, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
|
||||
gatt.setCharacteristicNotification(gatt.getService(SERVICE_UUID).getCharacteristic(CONNECTIVITY_CHARACTERISTIC), true);
|
||||
gatt.setCharacteristicNotification(gatt.getService(PebbleGATTConstants.SERVICE_UUID).getCharacteristic(PebbleGATTConstants.CONNECTIVITY_CHARACTERISTIC), true);
|
||||
}
|
||||
|
||||
private void subscribeToMTU(BluetoothGatt gatt) {
|
||||
BluetoothGattCharacteristic characteristic = gatt.getService(SERVICE_UUID).getCharacteristic(MTU_CHARACTERISTIC);
|
||||
BluetoothGattCharacteristic characteristic = gatt.getService(PebbleGATTConstants.SERVICE_UUID).getCharacteristic(PebbleGATTConstants.MTU_CHARACTERISTIC);
|
||||
if (characteristic != null) {
|
||||
LOG.info("subscribing to mtu characteristic");
|
||||
BluetoothGattDescriptor descriptor = gatt.getService(SERVICE_UUID).getCharacteristic(MTU_CHARACTERISTIC).getDescriptor(CHARACTERISTIC_CONFIGURATION_DESCRIPTOR);
|
||||
BluetoothGattDescriptor descriptor = gatt.getService(PebbleGATTConstants.SERVICE_UUID).getCharacteristic(PebbleGATTConstants.MTU_CHARACTERISTIC).getDescriptor(PebbleGATTConstants.CHARACTERISTIC_CONFIGURATION_DESCRIPTOR);
|
||||
NotifyAction.writeDescriptor(gatt, descriptor, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
|
||||
gatt.setCharacteristicNotification(gatt.getService(SERVICE_UUID).getCharacteristic(MTU_CHARACTERISTIC), true);
|
||||
gatt.setCharacteristicNotification(gatt.getService(PebbleGATTConstants.SERVICE_UUID).getCharacteristic(PebbleGATTConstants.MTU_CHARACTERISTIC), true);
|
||||
} else {
|
||||
LOG.info("Could not find MTU Characteristic. This seems to be a 2025 Pebble");
|
||||
}
|
||||
@@ -312,14 +273,14 @@ class PebbleGATTClient extends BluetoothGattCallback {
|
||||
|
||||
private void subscribeToConnectionParams(BluetoothGatt gatt) {
|
||||
LOG.info("subscribing to connection parameters characteristic");
|
||||
BluetoothGattDescriptor descriptor = gatt.getService(SERVICE_UUID).getCharacteristic(CONNECTION_PARAMETERS_CHARACTERISTIC).getDescriptor(CHARACTERISTIC_CONFIGURATION_DESCRIPTOR);
|
||||
BluetoothGattDescriptor descriptor = gatt.getService(PebbleGATTConstants.SERVICE_UUID).getCharacteristic(PebbleGATTConstants.CONNECTION_PARAMETERS_CHARACTERISTIC).getDescriptor(PebbleGATTConstants.CHARACTERISTIC_CONFIGURATION_DESCRIPTOR);
|
||||
NotifyAction.writeDescriptor(gatt, descriptor, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
|
||||
gatt.setCharacteristicNotification(gatt.getService(SERVICE_UUID).getCharacteristic(CONNECTION_PARAMETERS_CHARACTERISTIC), true);
|
||||
gatt.setCharacteristicNotification(gatt.getService(PebbleGATTConstants.SERVICE_UUID).getCharacteristic(PebbleGATTConstants.CONNECTION_PARAMETERS_CHARACTERISTIC), true);
|
||||
}
|
||||
|
||||
private void subscribeToMTUOrBattery(BluetoothGatt gatt) {
|
||||
// This is dumb, right now there is only one of them present in all pebbles
|
||||
BluetoothGattCharacteristic characteristic = gatt.getService(SERVICE_UUID).getCharacteristic(MTU_CHARACTERISTIC);
|
||||
BluetoothGattCharacteristic characteristic = gatt.getService(PebbleGATTConstants.SERVICE_UUID).getCharacteristic(PebbleGATTConstants.MTU_CHARACTERISTIC);
|
||||
if (characteristic != null) {
|
||||
subscribeToMTU(gatt);
|
||||
} else {
|
||||
@@ -331,7 +292,7 @@ class PebbleGATTClient extends BluetoothGattCallback {
|
||||
BluetoothGattCharacteristic characteristic = gatt.getService(GattService.UUID_SERVICE_BATTERY_SERVICE).getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_BATTERY_LEVEL);
|
||||
if (characteristic != null) {
|
||||
LOG.info("subscribing to battery characteristic");
|
||||
BluetoothGattDescriptor descriptor = gatt.getService(GattService.UUID_SERVICE_BATTERY_SERVICE).getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_BATTERY_LEVEL).getDescriptor(CHARACTERISTIC_CONFIGURATION_DESCRIPTOR);
|
||||
BluetoothGattDescriptor descriptor = gatt.getService(GattService.UUID_SERVICE_BATTERY_SERVICE).getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_BATTERY_LEVEL).getDescriptor(PebbleGATTConstants.CHARACTERISTIC_CONFIGURATION_DESCRIPTOR);
|
||||
NotifyAction.writeDescriptor(gatt, descriptor, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
|
||||
gatt.setCharacteristicNotification(gatt.getService(GattService.UUID_SERVICE_BATTERY_SERVICE).getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_BATTERY_LEVEL), true);
|
||||
} else {
|
||||
@@ -341,9 +302,9 @@ class PebbleGATTClient extends BluetoothGattCallback {
|
||||
|
||||
private void setMTU(BluetoothGatt gatt) {
|
||||
LOG.info("setting MTU");
|
||||
BluetoothGattCharacteristic characteristic = gatt.getService(SERVICE_UUID).getCharacteristic(MTU_CHARACTERISTIC);
|
||||
BluetoothGattCharacteristic characteristic = gatt.getService(PebbleGATTConstants.SERVICE_UUID).getCharacteristic(PebbleGATTConstants.MTU_CHARACTERISTIC);
|
||||
if (characteristic != null) {
|
||||
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(CHARACTERISTIC_CONFIGURATION_DESCRIPTOR);
|
||||
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(PebbleGATTConstants.CHARACTERISTIC_CONFIGURATION_DESCRIPTOR);
|
||||
descriptor.setValue(new byte[]{0x0b, 0x01}); // unknown
|
||||
// descriptor is not wrote back to the device, but the characteristic is.
|
||||
// Reason is unclear but writing back the descriptor instead of the characteristic breaks the connection.
|
||||
@@ -355,10 +316,10 @@ class PebbleGATTClient extends BluetoothGattCallback {
|
||||
|
||||
private void subscribeToPPoGATT(BluetoothGatt gatt) {
|
||||
LOG.info("subscribing to PPoGATT read characteristic");
|
||||
BluetoothGattDescriptor descriptor = gatt.getService(PPOGATT_SERVICE_UUID).getCharacteristic(PPOGATT_CHARACTERISTIC_READ).getDescriptor(CHARACTERISTIC_CONFIGURATION_DESCRIPTOR);
|
||||
BluetoothGattDescriptor descriptor = gatt.getService(PebbleGATTConstants.PPOGATT_SERVICE_UUID).getCharacteristic(PebbleGATTConstants.PPOGATT_CHARACTERISTIC_READ).getDescriptor(PebbleGATTConstants.CHARACTERISTIC_CONFIGURATION_DESCRIPTOR);
|
||||
NotifyAction.writeDescriptor(gatt, descriptor, new byte[]{1, 0});
|
||||
gatt.setCharacteristicNotification(gatt.getService(PPOGATT_SERVICE_UUID).getCharacteristic(PPOGATT_CHARACTERISTIC_READ), true);
|
||||
writeCharacteristics = gatt.getService(PPOGATT_SERVICE_UUID).getCharacteristic(PPOGATT_CHARACTERISTIC_WRITE);
|
||||
gatt.setCharacteristicNotification(gatt.getService(PebbleGATTConstants.PPOGATT_SERVICE_UUID).getCharacteristic(PebbleGATTConstants.PPOGATT_CHARACTERISTIC_READ), true);
|
||||
writeCharacteristics = gatt.getService(PebbleGATTConstants.PPOGATT_SERVICE_UUID).getCharacteristic(PebbleGATTConstants.PPOGATT_CHARACTERISTIC_WRITE);
|
||||
}
|
||||
|
||||
synchronized void sendDataToPebble(byte[] data) {
|
||||
@@ -377,169 +338,11 @@ class PebbleGATTClient extends BluetoothGattCallback {
|
||||
mWaitWriteCompleteLatch = null;
|
||||
}
|
||||
|
||||
// ================== New Pairing Methods (based on libpebble3) ==================
|
||||
// ================== Pairing Callback Implementation ==================
|
||||
|
||||
/**
|
||||
* Handle the connectivity characteristic read to determine pairing state.
|
||||
* Based on libpebble3 ConnectivityWatcher and PebblePairing.
|
||||
*/
|
||||
private void handleConnectivityRead(BluetoothGatt gatt, byte[] value) {
|
||||
mConnectivityStatus = new ConnectivityStatus(value);
|
||||
LOG.info("Connectivity status: {}", mConnectivityStatus);
|
||||
|
||||
BluetoothDevice device = gatt.getDevice();
|
||||
int bondState = device.getBondState();
|
||||
boolean phoneBonded = bondState == BluetoothDevice.BOND_BONDED;
|
||||
LOG.info("Phone bond state: {} (BONDED={})", bondState, BluetoothDevice.BOND_BONDED);
|
||||
|
||||
// If BOTH sides are already paired, skip pairing trigger and bonding entirely
|
||||
if (mConnectivityStatus.paired && phoneBonded) {
|
||||
LOG.info("Already paired on both sides - skipping pairing trigger, proceeding with subscriptions");
|
||||
proceedAfterPairing(gatt);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.info("Proceeding with pairing trigger write (watch.paired={}, phone.bonded={})",
|
||||
mConnectivityStatus.paired, phoneBonded);
|
||||
|
||||
BluetoothGattCharacteristic pairingTrigger =
|
||||
gatt.getService(SERVICE_UUID).getCharacteristic(PAIRING_TRIGGER_CHARACTERISTIC);
|
||||
|
||||
if (pairingTrigger == null) {
|
||||
LOG.error("Pairing trigger characteristic not found");
|
||||
proceedAfterPairing(gatt);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((pairingTrigger.getProperties() & PROPERTY_WRITE) != 0) {
|
||||
LOG.info("Writing pairing trigger for Pebble Time 2 / modern Pebble");
|
||||
|
||||
// Use the original working values, but we'll add createBond() after
|
||||
// 0x09 = pinAddress(1) + autoAccept(8) - worked for normal mode
|
||||
// 0x11 = pinAddress(1) + watchAsServer(16) - for clientOnly mode
|
||||
byte[] triggerValue;
|
||||
if (mPebbleLESupport.clientOnly) {
|
||||
triggerValue = new byte[]{0x11};
|
||||
LOG.info("Using clientOnly pairing trigger: 0x11");
|
||||
} else {
|
||||
triggerValue = new byte[]{0x09};
|
||||
LOG.info("Using normal pairing trigger: 0x09");
|
||||
}
|
||||
WriteAction.writeCharacteristic(gatt, pairingTrigger, triggerValue);
|
||||
// Continue in onCharacteristicWrite() -> initiateBluetoothBond()
|
||||
} else {
|
||||
// Old firmware - just read the characteristic (legacy path)
|
||||
LOG.info("Pairing trigger not writable, using legacy read");
|
||||
gatt.readCharacteristic(pairingTrigger);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate Bluetooth bonding after writing the pairing trigger.
|
||||
* Based on libpebble3 PebblePairing.requestBlePairing().
|
||||
*/
|
||||
private void initiateBluetoothBond(BluetoothGatt gatt) {
|
||||
BluetoothDevice device = gatt.getDevice();
|
||||
int currentBondState = device.getBondState();
|
||||
LOG.info("Current bond state before pairing: {} (NONE={}, BONDING={}, BONDED={})",
|
||||
currentBondState, BluetoothDevice.BOND_NONE,
|
||||
BluetoothDevice.BOND_BONDING, BluetoothDevice.BOND_BONDED);
|
||||
|
||||
// If already bonded, just proceed - don't try to re-bond
|
||||
if (currentBondState == BluetoothDevice.BOND_BONDED) {
|
||||
LOG.info("Device already bonded, proceeding with subscriptions");
|
||||
proceedAfterPairing(gatt);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.info("Initiating Bluetooth bond with {}ms timeout", BONDING_TIMEOUT_MS);
|
||||
|
||||
// Register bond state receiver
|
||||
mBondingLatch = new CountDownLatch(1);
|
||||
mBondingReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
BluetoothDevice bondDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
|
||||
if (bondDevice == null || !bondDevice.getAddress().equals(device.getAddress())) {
|
||||
return;
|
||||
}
|
||||
int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
|
||||
LOG.info("Bond state changed: {}", bondState);
|
||||
|
||||
if (bondState == BluetoothDevice.BOND_BONDED) {
|
||||
LOG.info("Bonding succeeded!");
|
||||
mBondingLatch.countDown();
|
||||
} else if (bondState == BluetoothDevice.BOND_NONE) {
|
||||
int reason = intent.getIntExtra("android.bluetooth.device.extra.REASON", -1);
|
||||
LOG.error("Bonding failed with reason: {}", reason);
|
||||
mBondingLatch.countDown();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
|
||||
ContextCompat.registerReceiver(mContext, mBondingReceiver, filter, ContextCompat.RECEIVER_EXPORTED);
|
||||
|
||||
// Start bonding on background thread to avoid blocking GATT callbacks
|
||||
new Thread(() -> {
|
||||
try {
|
||||
LOG.info("Calling createBond() on device {}", device.getAddress());
|
||||
boolean bondInitiated = device.createBond();
|
||||
LOG.info("createBond() returned: {}", bondInitiated);
|
||||
|
||||
if (!bondInitiated) {
|
||||
LOG.error("Failed to initiate bonding - createBond() returned false");
|
||||
LOG.info("Current bond state after failed createBond: {}", device.getBondState());
|
||||
cleanupBondingReceiver();
|
||||
// Still proceed to try connection
|
||||
new Handler(Looper.getMainLooper()).post(() -> proceedAfterPairing(gatt));
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.info("Waiting for bond state change (timeout: {}ms)...", BONDING_TIMEOUT_MS);
|
||||
boolean bonded = mBondingLatch.await(BONDING_TIMEOUT_MS, java.util.concurrent.TimeUnit.MILLISECONDS);
|
||||
int finalBondState = device.getBondState();
|
||||
LOG.info("Bond wait completed: latch={}, finalState={}", bonded, finalBondState);
|
||||
|
||||
if (bonded && finalBondState == BluetoothDevice.BOND_BONDED) {
|
||||
LOG.info("Bonding completed successfully!");
|
||||
} else {
|
||||
LOG.warn("Bonding timed out or failed (latch={}, state={}), proceeding anyway",
|
||||
bonded, finalBondState);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
LOG.error("Bonding interrupted", e);
|
||||
} catch (SecurityException e) {
|
||||
LOG.error("SecurityException during createBond - missing BLUETOOTH_CONNECT permission?", e);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Unexpected error during bonding", e);
|
||||
} finally {
|
||||
cleanupBondingReceiver();
|
||||
// Continue with subscription chain on main thread
|
||||
new Handler(Looper.getMainLooper()).post(() -> proceedAfterPairing(gatt));
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up the bonding broadcast receiver.
|
||||
*/
|
||||
private void cleanupBondingReceiver() {
|
||||
if (mBondingReceiver != null) {
|
||||
try {
|
||||
mContext.unregisterReceiver(mBondingReceiver);
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Error unregistering bonding receiver: {}", e.getMessage());
|
||||
}
|
||||
mBondingReceiver = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Continue with the subscription chain after pairing is complete or skipped.
|
||||
*/
|
||||
private void proceedAfterPairing(BluetoothGatt gatt) {
|
||||
LOG.info("Proceeding after pairing to subscription chain");
|
||||
@Override
|
||||
public void onPairingComplete(BluetoothGatt gatt, boolean hasConnectivityCharacteristics) {
|
||||
LOG.info("Pairing complete, starting subscription chain (hasConnectivityChar={})", hasConnectivityCharacteristics);
|
||||
if (hasConnectivityCharacteristics) {
|
||||
subscribeToConnectivity(gatt);
|
||||
} else {
|
||||
@@ -547,70 +350,11 @@ class PebbleGATTClient extends BluetoothGattCallback {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy pairing path for backward compatibility with older firmwares.
|
||||
*/
|
||||
private void proceedWithLegacyPairing(BluetoothGatt gatt) {
|
||||
LOG.info("Using legacy pairing method");
|
||||
BluetoothGattCharacteristic characteristic =
|
||||
gatt.getService(SERVICE_UUID).getCharacteristic(PAIRING_TRIGGER_CHARACTERISTIC);
|
||||
if ((characteristic.getProperties() & PROPERTY_WRITE) != 0) {
|
||||
byte[] value = mPebbleLESupport.clientOnly ? new byte[]{0x11} : new byte[]{0x09};
|
||||
WriteAction.writeCharacteristic(gatt, characteristic, value);
|
||||
} else {
|
||||
gatt.readCharacteristic(characteristic);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if we need to pair based on connectivity status and bond state.
|
||||
* Based on libpebble3 PebbleBle.kt pairing logic.
|
||||
*/
|
||||
private boolean shouldPair(ConnectivityStatus status, boolean phoneBonded) {
|
||||
if (status.paired && phoneBonded) {
|
||||
LOG.info("Already paired on both sides, skipping pairing");
|
||||
return false;
|
||||
}
|
||||
if (status.paired && !phoneBonded) {
|
||||
LOG.info("Watch thinks it's paired, phone does not - need to re-pair");
|
||||
return true;
|
||||
}
|
||||
if (!status.paired && phoneBonded) {
|
||||
LOG.info("Phone thinks it's paired, watch does not - need to re-pair");
|
||||
return true;
|
||||
}
|
||||
LOG.info("Neither side paired - need full pairing");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the pairing trigger value with proper bit flags.
|
||||
* Based on libpebble3 PebblePairing.makePairingTriggerValue()
|
||||
*
|
||||
* Bit 0: pinAddress - pin BLE address to prevent MAC rotation
|
||||
* Bit 1: noSecurityRequest - if true, watch won't request security (phone will via createBond)
|
||||
* Bit 2: forceSecurityRequest - if true, watch will request security (inverse of bit 1)
|
||||
* Bit 3: autoAcceptFuturePairing
|
||||
* Bit 4: watchAsGattServer - for reversed PPoG mode
|
||||
*
|
||||
* When phone calls createBond(), set noSecurityRequest=true so watch doesn't also request.
|
||||
*/
|
||||
private byte buildPairingTriggerValue(boolean pinAddress, boolean noSecurityRequest,
|
||||
boolean autoAcceptFuture, boolean watchAsGattServer) {
|
||||
byte value = 0;
|
||||
if (pinAddress) value |= 0b00001; // Bit 0
|
||||
if (noSecurityRequest) value |= 0b00010; // Bit 1
|
||||
if (!noSecurityRequest) value |= 0b00100; // Bit 2: force security (only if bit 1 is 0)
|
||||
if (autoAcceptFuture) value |= 0b01000; // Bit 3
|
||||
if (watchAsGattServer) value |= 0b10000; // Bit 4
|
||||
LOG.debug("buildPairingTriggerValue: pin={}, noSec={}, auto={}, server={} -> 0x{}",
|
||||
pinAddress, noSecurityRequest, autoAcceptFuture, watchAsGattServer,
|
||||
String.format("%02X", value));
|
||||
return value;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
cleanupBondingReceiver();
|
||||
if (mPairingFlow != null) {
|
||||
mPairingFlow.close();
|
||||
mPairingFlow = null;
|
||||
}
|
||||
if (mBluetoothGatt != null) {
|
||||
mBluetoothGatt.disconnect();
|
||||
mBluetoothGatt.close();
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/* Copyright (C) 2024 Gadgetbridge contributors
|
||||
|
||||
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.pebble.ble;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Shared GATT constants for Pebble BLE pairing flows.
|
||||
*/
|
||||
class PebbleGATTConstants {
|
||||
static final UUID SERVICE_UUID = UUID.fromString("0000fed9-0000-1000-8000-00805f9b34fb");
|
||||
static final UUID CONNECTIVITY_CHARACTERISTIC = UUID.fromString("00000001-328E-0FBB-C642-1AA6699BDADA");
|
||||
static final UUID PAIRING_TRIGGER_CHARACTERISTIC = UUID.fromString("00000002-328E-0FBB-C642-1AA6699BDADA");
|
||||
static final UUID MTU_CHARACTERISTIC = UUID.fromString("00000003-328e-0fbb-c642-1aa6699bdada");
|
||||
static final UUID CONNECTION_PARAMETERS_CHARACTERISTIC = UUID.fromString("00000005-328E-0FBB-C642-1AA6699BDADA");
|
||||
static final UUID CHARACTERISTIC_CONFIGURATION_DESCRIPTOR = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
|
||||
|
||||
// PPoGATT service (Pebble side)
|
||||
static final UUID PPOGATT_SERVICE_UUID = UUID.fromString("30000003-328E-0FBB-C642-1AA6699BDADA");
|
||||
static final UUID PPOGATT_CHARACTERISTIC_READ = UUID.fromString("30000004-328E-0FBB-C642-1AA6699BDADA");
|
||||
static final UUID PPOGATT_CHARACTERISTIC_WRITE = UUID.fromString("30000006-328E-0FBB-C642-1AA6699BDADA");
|
||||
|
||||
private PebbleGATTConstants() {
|
||||
// Utility class
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/* Copyright (C) 2024 Gadgetbridge contributors
|
||||
|
||||
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.pebble.ble;
|
||||
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.bluetooth.BluetoothGattService;
|
||||
|
||||
/**
|
||||
* Strategy interface for different Pebble BLE pairing flows.
|
||||
* Different Pebble hardware versions require different pairing protocols.
|
||||
*/
|
||||
interface PebblePairingFlow {
|
||||
/**
|
||||
* Start the pairing sequence after service discovery.
|
||||
*
|
||||
* @param gatt The GATT connection
|
||||
* @param pairingService The Pebble pairing service (0000fed9)
|
||||
*/
|
||||
void startPairing(BluetoothGatt gatt, BluetoothGattService pairingService);
|
||||
|
||||
/**
|
||||
* Handle characteristic read callback during pairing.
|
||||
*
|
||||
* @param gatt The GATT connection
|
||||
* @param characteristic The characteristic that was read
|
||||
* @param value The value that was read
|
||||
* @return true if this flow handled the read, false otherwise
|
||||
*/
|
||||
boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] value);
|
||||
|
||||
/**
|
||||
* Handle characteristic write callback during pairing.
|
||||
*
|
||||
* @param gatt The GATT connection
|
||||
* @param characteristic The characteristic that was written
|
||||
* @param status The write status (GATT_SUCCESS, etc.)
|
||||
* @return true if this flow handled the write, false otherwise
|
||||
*/
|
||||
boolean onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status);
|
||||
|
||||
/**
|
||||
* Clean up any resources held by this pairing flow.
|
||||
* Called when the GATT client is closed.
|
||||
*/
|
||||
void close();
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
/* Copyright (C) 2024 Gadgetbridge contributors
|
||||
|
||||
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.pebble.ble;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.bluetooth.BluetoothGattService;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleHardware;
|
||||
|
||||
/**
|
||||
* Factory for creating the appropriate Pebble pairing flow based on device hardware and characteristics.
|
||||
* <p>
|
||||
* Device detection strategy:
|
||||
* 1. Primary: Use hardware platform (via PebbleHardware.isBleOnlyByModel) to determine expected flow
|
||||
* 2. Validation: Verify device has expected GATT characteristics
|
||||
* 3. Defensive: If mismatch, adapt based on actual characteristics (e.g., firmware upgrade scenario)
|
||||
* <p>
|
||||
* Expected characteristics by hardware:
|
||||
* - BLE-only Pebbles (Pebble 2, Time 2, 2 Duo): CONNECTIVITY_CHARACTERISTIC, no CONNECTION_PARAMETERS → ModernPebblePairingFlow
|
||||
* - Dual-mode Pebbles (Pebble Time, Time Round): CONNECTION_PARAMETERS_CHARACTERISTIC → LegacyPebblePairingFlow
|
||||
* - Classic Pebbles: No BLE pairing needed
|
||||
*/
|
||||
class PebblePairingFlowFactory {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PebblePairingFlowFactory.class);
|
||||
|
||||
/**
|
||||
* Create the appropriate pairing flow for the given device.
|
||||
*
|
||||
* @param context Android context for bonding operations
|
||||
* @param deviceModel Device model string from GBDevice.getModel() (may be null)
|
||||
* @param pairingService The Pebble pairing service (0000fed9)
|
||||
* @param clientOnly Whether to use clientOnly mode (affects pairing trigger value)
|
||||
* @param callback Callback to invoke when pairing is complete
|
||||
* @return The appropriate pairing flow implementation
|
||||
*/
|
||||
static PebblePairingFlow createPairingFlow(
|
||||
Context context,
|
||||
@Nullable String deviceModel,
|
||||
BluetoothGattService pairingService,
|
||||
boolean clientOnly,
|
||||
PairingCallback callback) {
|
||||
|
||||
// Detect GATT characteristics
|
||||
BluetoothGattCharacteristic connectivityChar =
|
||||
pairingService.getCharacteristic(PebbleGATTConstants.CONNECTIVITY_CHARACTERISTIC);
|
||||
BluetoothGattCharacteristic connectionParamChar =
|
||||
pairingService.getCharacteristic(PebbleGATTConstants.CONNECTION_PARAMETERS_CHARACTERISTIC);
|
||||
|
||||
boolean hasConnectivityChar = (connectivityChar != null);
|
||||
boolean hasConnectionParamChar = (connectionParamChar != null);
|
||||
|
||||
// Hardware-based expectation (if model is known)
|
||||
Boolean hardwareExpectsModern = null; // null = unknown
|
||||
if (deviceModel != null && !deviceModel.isEmpty()) {
|
||||
hardwareExpectsModern = PebbleHardware.isBleOnlyByModel(deviceModel);
|
||||
LOG.info("Hardware detection: model='{}', BLE-only={}", deviceModel, hardwareExpectsModern);
|
||||
}
|
||||
|
||||
if (hardwareExpectsModern != null) {
|
||||
if (hardwareExpectsModern && !hasConnectivityChar) {
|
||||
LOG.warn("Mismatch: Hardware expects modern flow but CONNECTIVITY_CHARACTERISTIC missing (model={})", deviceModel);
|
||||
}
|
||||
|
||||
if (!hardwareExpectsModern && hasConnectivityChar && !hasConnectionParamChar) {
|
||||
LOG.warn("Mismatch: Hardware expects legacy flow but has modern characteristics (model={}) - possible firmware upgrade", deviceModel);
|
||||
}
|
||||
}
|
||||
|
||||
boolean useModernFlow;
|
||||
if (hasConnectivityChar) {
|
||||
// Modern characteristic present - use V2 flow
|
||||
useModernFlow = true;
|
||||
LOG.info("Using PebblePairingFlowV2 (CONNECTIVITY_CHARACTERISTIC present)");
|
||||
} else if (hasConnectionParamChar) {
|
||||
// Legacy characteristic present - use V1 flow
|
||||
useModernFlow = false;
|
||||
LOG.info("Using PebblePairingFlowV1 (CONNECTION_PARAMETERS_CHARACTERISTIC present)");
|
||||
} else {
|
||||
// No characteristics - fall back to hardware expectation or default to V1
|
||||
useModernFlow = (hardwareExpectsModern != null && hardwareExpectsModern);
|
||||
LOG.warn("No pairing characteristics found - using {} flow based on {}",
|
||||
useModernFlow ? "V2" : "V1",
|
||||
hardwareExpectsModern != null ? "hardware expectation" : "default");
|
||||
}
|
||||
|
||||
if (useModernFlow) {
|
||||
return new PebblePairingFlowV2(context, clientOnly, callback);
|
||||
} else {
|
||||
return new PebblePairingFlowV1(clientOnly, callback);
|
||||
}
|
||||
}
|
||||
|
||||
private PebblePairingFlowFactory() {
|
||||
// Utility class
|
||||
}
|
||||
}
|
||||
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
/* Copyright (C) 2024 Gadgetbridge contributors
|
||||
|
||||
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.pebble.ble;
|
||||
|
||||
import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_WRITE;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.bluetooth.BluetoothGattService;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WriteAction;
|
||||
|
||||
/**
|
||||
* V1 Pebble pairing flow for older BLE-enabled devices (Pebble Time, Pebble Time Round).
|
||||
* These devices use a simpler pairing protocol without connectivity status checking
|
||||
* or explicit bonding management.
|
||||
* <p>
|
||||
* This is the original pairing flow from before commit 945e06946.
|
||||
*/
|
||||
@SuppressLint("MissingPermission")
|
||||
class PebblePairingFlowV1 implements PebblePairingFlow {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PebblePairingFlowV1.class);
|
||||
|
||||
private final boolean mClientOnly;
|
||||
private final PairingCallback mCallback;
|
||||
|
||||
PebblePairingFlowV1(boolean clientOnly, PairingCallback callback) {
|
||||
mClientOnly = clientOnly;
|
||||
mCallback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startPairing(BluetoothGatt gatt, BluetoothGattService pairingService) {
|
||||
LOG.info("PebblePairingFlowV1: Starting pairing for Pebble Time/Time Round");
|
||||
|
||||
BluetoothGattCharacteristic characteristic =
|
||||
pairingService.getCharacteristic(PebbleGATTConstants.PAIRING_TRIGGER_CHARACTERISTIC);
|
||||
|
||||
if (characteristic == null) {
|
||||
LOG.error("Pairing trigger characteristic not found");
|
||||
// Proceed anyway to subscription chain
|
||||
mCallback.onPairingComplete(gatt, false);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((characteristic.getProperties() & PROPERTY_WRITE) != 0) {
|
||||
LOG.info("This seems to be a >=4.0 FW Pebble, writing to pairing trigger");
|
||||
// flags:
|
||||
// 0 - always 1
|
||||
// 1 - unknown
|
||||
// 2 - always 0
|
||||
// 3 - unknown, set on kitkat (seems to help to get a "better" pairing)
|
||||
// 4 - unknown, set on some phones
|
||||
byte[] value;
|
||||
if (mClientOnly) {
|
||||
value = new byte[]{0x11}; // needed in clientOnly mode
|
||||
} else {
|
||||
value = new byte[]{0x09}; // I just keep this, because it worked
|
||||
}
|
||||
WriteAction.writeCharacteristic(gatt, characteristic, value);
|
||||
// Continue in onCharacteristicWrite()
|
||||
} else {
|
||||
LOG.info("This seems to be some <4.0 FW Pebble, reading pairing trigger");
|
||||
gatt.readCharacteristic(characteristic);
|
||||
// Continue in onCharacteristicRead()
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] value) {
|
||||
if (!characteristic.getUuid().equals(PebbleGATTConstants.PAIRING_TRIGGER_CHARACTERISTIC)) {
|
||||
return false; // Not handled by this flow
|
||||
}
|
||||
|
||||
// Legacy pairing trigger read complete - proceed to subscriptions
|
||||
LOG.info("V1 pairing trigger read complete, proceeding to subscriptions");
|
||||
proceedAfterPairing(gatt);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
|
||||
if (!characteristic.getUuid().equals(PebbleGATTConstants.PAIRING_TRIGGER_CHARACTERISTIC)) {
|
||||
return false; // Not handled by this flow
|
||||
}
|
||||
|
||||
// Legacy pairing trigger write complete - proceed to subscriptions
|
||||
LOG.info("V1 pairing trigger write complete (status={}), proceeding to subscriptions", status);
|
||||
proceedAfterPairing(gatt);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// No resources to clean up for V1 flow
|
||||
}
|
||||
|
||||
/**
|
||||
* Continue with the subscription chain after pairing trigger is complete.
|
||||
*/
|
||||
private void proceedAfterPairing(BluetoothGatt gatt) {
|
||||
LOG.info("PebblePairingFlowV1: Pairing complete, proceeding to subscriptions");
|
||||
mCallback.onPairingComplete(gatt, false); // false = NO hasConnectivityCharacteristics (V1)
|
||||
}
|
||||
}
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
/* Copyright (C) 2024 Gadgetbridge contributors
|
||||
|
||||
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.pebble.ble;
|
||||
|
||||
import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_WRITE;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.bluetooth.BluetoothGattService;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WriteAction;
|
||||
|
||||
/**
|
||||
* V2 Pebble pairing flow for BLE-only devices (Pebble 2, Time 2, 2 Duo).
|
||||
* These devices require reading connectivity status, writing a pairing trigger,
|
||||
* and initiating Bluetooth bonding before the connection can complete.
|
||||
* <p>
|
||||
* Based on the PebbleOS open source implementation and libpebble3.
|
||||
*/
|
||||
@SuppressLint("MissingPermission")
|
||||
class PebblePairingFlowV2 implements PebblePairingFlow {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PebblePairingFlowV2.class);
|
||||
|
||||
private static final long BONDING_TIMEOUT_MS = 60000; // 60 seconds
|
||||
|
||||
private final Context mContext;
|
||||
private final boolean mClientOnly;
|
||||
private final PairingCallback mCallback;
|
||||
|
||||
private ConnectivityStatus mConnectivityStatus;
|
||||
private CountDownLatch mBondingLatch;
|
||||
private BroadcastReceiver mBondingReceiver;
|
||||
|
||||
PebblePairingFlowV2(Context context, boolean clientOnly, PairingCallback callback) {
|
||||
mContext = context;
|
||||
mClientOnly = clientOnly;
|
||||
mCallback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startPairing(BluetoothGatt gatt, BluetoothGattService pairingService) {
|
||||
LOG.info("PebblePairingFlowV2: Starting pairing for Pebble 2/Time 2/2 Duo");
|
||||
|
||||
// Read Connectivity characteristic FIRST to determine pairing state
|
||||
BluetoothGattCharacteristic connectivityChar =
|
||||
pairingService.getCharacteristic(PebbleGATTConstants.CONNECTIVITY_CHARACTERISTIC);
|
||||
|
||||
if (connectivityChar != null) {
|
||||
LOG.info("Reading connectivity characteristic to check pairing state");
|
||||
gatt.readCharacteristic(connectivityChar);
|
||||
// Continue in onCharacteristicRead() -> handleConnectivityRead()
|
||||
} else {
|
||||
LOG.error("Connectivity characteristic not found - cannot proceed with modern pairing");
|
||||
// Proceed anyway to subscription chain
|
||||
mCallback.onPairingComplete(gatt, true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] value) {
|
||||
if (!characteristic.getUuid().equals(PebbleGATTConstants.CONNECTIVITY_CHARACTERISTIC)) {
|
||||
return false; // Not handled by this flow
|
||||
}
|
||||
|
||||
handleConnectivityRead(gatt, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
|
||||
if (!characteristic.getUuid().equals(PebbleGATTConstants.PAIRING_TRIGGER_CHARACTERISTIC)) {
|
||||
return false; // Not handled by this flow
|
||||
}
|
||||
|
||||
// Pairing trigger written - now initiate Bluetooth bonding
|
||||
LOG.info("Pairing trigger written successfully (status={}), initiating bond", status);
|
||||
initiateBluetoothBond(gatt);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
cleanupBondingReceiver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the connectivity characteristic read to determine pairing state.
|
||||
* Based on libpebble3 ConnectivityWatcher and PebblePairing.
|
||||
*/
|
||||
private void handleConnectivityRead(BluetoothGatt gatt, byte[] value) {
|
||||
mConnectivityStatus = new ConnectivityStatus(value);
|
||||
LOG.info("Connectivity status: {}", mConnectivityStatus);
|
||||
|
||||
BluetoothDevice device = gatt.getDevice();
|
||||
int bondState = device.getBondState();
|
||||
boolean phoneBonded = bondState == BluetoothDevice.BOND_BONDED;
|
||||
LOG.info("Phone bond state: {} (BONDED={})", bondState, BluetoothDevice.BOND_BONDED);
|
||||
|
||||
// If BOTH sides are already paired, skip pairing trigger and bonding entirely
|
||||
if (mConnectivityStatus.paired && phoneBonded) {
|
||||
LOG.info("Already paired on both sides - skipping pairing trigger, proceeding with subscriptions");
|
||||
proceedAfterPairing(gatt);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.info("Proceeding with pairing trigger write (watch.paired={}, phone.bonded={})",
|
||||
mConnectivityStatus.paired, phoneBonded);
|
||||
|
||||
BluetoothGattCharacteristic pairingTrigger =
|
||||
gatt.getService(PebbleGATTConstants.SERVICE_UUID).getCharacteristic(PebbleGATTConstants.PAIRING_TRIGGER_CHARACTERISTIC);
|
||||
|
||||
if (pairingTrigger == null) {
|
||||
LOG.error("Pairing trigger characteristic not found");
|
||||
proceedAfterPairing(gatt);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((pairingTrigger.getProperties() & PROPERTY_WRITE) != 0) {
|
||||
LOG.info("Writing pairing trigger for Pebble Time 2 / modern Pebble");
|
||||
|
||||
// Use the original working values
|
||||
// 0x09 = pinAddress(1) + autoAccept(8) - worked for normal mode
|
||||
// 0x11 = pinAddress(1) + watchAsServer(16) - for clientOnly mode
|
||||
byte[] triggerValue;
|
||||
if (mClientOnly) {
|
||||
triggerValue = new byte[]{0x11};
|
||||
LOG.info("Using clientOnly pairing trigger: 0x11");
|
||||
} else {
|
||||
triggerValue = new byte[]{0x09};
|
||||
LOG.info("Using normal pairing trigger: 0x09");
|
||||
}
|
||||
WriteAction.writeCharacteristic(gatt, pairingTrigger, triggerValue);
|
||||
// Continue in onCharacteristicWrite() -> initiateBluetoothBond()
|
||||
} else {
|
||||
// Old firmware - just read the characteristic (legacy path - shouldn't happen in modern flow)
|
||||
LOG.warn("Pairing trigger not writable in modern flow - unexpected");
|
||||
gatt.readCharacteristic(pairingTrigger);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate Bluetooth bonding after writing the pairing trigger.
|
||||
* Based on libpebble3 PebblePairing.requestBlePairing().
|
||||
*/
|
||||
private void initiateBluetoothBond(BluetoothGatt gatt) {
|
||||
BluetoothDevice device = gatt.getDevice();
|
||||
int currentBondState = device.getBondState();
|
||||
LOG.info("Current bond state before pairing: {} (NONE={}, BONDING={}, BONDED={})",
|
||||
currentBondState, BluetoothDevice.BOND_NONE,
|
||||
BluetoothDevice.BOND_BONDING, BluetoothDevice.BOND_BONDED);
|
||||
|
||||
// If already bonded, just proceed - don't try to re-bond
|
||||
if (currentBondState == BluetoothDevice.BOND_BONDED) {
|
||||
LOG.info("Device already bonded, proceeding with subscriptions");
|
||||
proceedAfterPairing(gatt);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.info("Initiating Bluetooth bond with {}ms timeout", BONDING_TIMEOUT_MS);
|
||||
|
||||
// Register bond state receiver
|
||||
mBondingLatch = new CountDownLatch(1);
|
||||
mBondingReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
BluetoothDevice bondDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
|
||||
if (bondDevice == null || !bondDevice.getAddress().equals(device.getAddress())) {
|
||||
return;
|
||||
}
|
||||
int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
|
||||
LOG.info("Bond state changed: {}", bondState);
|
||||
|
||||
if (bondState == BluetoothDevice.BOND_BONDED) {
|
||||
LOG.info("Bonding succeeded!");
|
||||
mBondingLatch.countDown();
|
||||
} else if (bondState == BluetoothDevice.BOND_NONE) {
|
||||
int reason = intent.getIntExtra("android.bluetooth.device.extra.REASON", -1);
|
||||
LOG.error("Bonding failed with reason: {}", reason);
|
||||
mBondingLatch.countDown();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
|
||||
ContextCompat.registerReceiver(mContext, mBondingReceiver, filter, ContextCompat.RECEIVER_EXPORTED);
|
||||
|
||||
// Start bonding on background thread to avoid blocking GATT callbacks
|
||||
new Thread(() -> {
|
||||
try {
|
||||
LOG.info("Calling createBond() on device {}", device.getAddress());
|
||||
boolean bondInitiated = device.createBond();
|
||||
LOG.info("createBond() returned: {}", bondInitiated);
|
||||
|
||||
if (!bondInitiated) {
|
||||
LOG.error("Failed to initiate bonding - createBond() returned false");
|
||||
LOG.info("Current bond state after failed createBond: {}", device.getBondState());
|
||||
cleanupBondingReceiver();
|
||||
// Still proceed to try connection
|
||||
new Handler(Looper.getMainLooper()).post(() -> proceedAfterPairing(gatt));
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.info("Waiting for bond state change (timeout: {}ms)...", BONDING_TIMEOUT_MS);
|
||||
boolean bonded = mBondingLatch.await(BONDING_TIMEOUT_MS, java.util.concurrent.TimeUnit.MILLISECONDS);
|
||||
int finalBondState = device.getBondState();
|
||||
LOG.info("Bond wait completed: latch={}, finalState={}", bonded, finalBondState);
|
||||
|
||||
if (bonded && finalBondState == BluetoothDevice.BOND_BONDED) {
|
||||
LOG.info("Bonding completed successfully!");
|
||||
} else {
|
||||
LOG.warn("Bonding timed out or failed (latch={}, state={}), proceeding anyway",
|
||||
bonded, finalBondState);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
LOG.error("Bonding interrupted", e);
|
||||
} catch (SecurityException e) {
|
||||
LOG.error("SecurityException during createBond - missing BLUETOOTH_CONNECT permission?", e);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Unexpected error during bonding", e);
|
||||
} finally {
|
||||
cleanupBondingReceiver();
|
||||
// Continue with subscription chain on main thread
|
||||
new Handler(Looper.getMainLooper()).post(() -> proceedAfterPairing(gatt));
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up the bonding broadcast receiver.
|
||||
*/
|
||||
private void cleanupBondingReceiver() {
|
||||
if (mBondingReceiver != null) {
|
||||
try {
|
||||
mContext.unregisterReceiver(mBondingReceiver);
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Error unregistering bonding receiver: {}", e.getMessage());
|
||||
}
|
||||
mBondingReceiver = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Continue with the subscription chain after pairing is complete or skipped.
|
||||
*/
|
||||
private void proceedAfterPairing(BluetoothGatt gatt) {
|
||||
LOG.info("PebblePairingFlowV2: Pairing complete, proceeding to subscriptions");
|
||||
mCallback.onPairingComplete(gatt, true); // true = hasConnectivityCharacteristics
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user