mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Pebble: Fix Pebble [Core 2 Duo / Time 2 / 2 Round] BLE pairing
Changes: - BondingUtil: Use PebbleHardware.isBleOnly() for accurate BLE detection - PebbleIoThread: Detect BLE-only devices using hardware revision/model - PebblePairingActivity: Use manufacturer data for device identification, refresh device list on pairing complete - PebbleGATTClient: Fixed connection handling when pairing with new pebble watches based on open source PebbleOS New status and pairing codes which match PebbleOS implementation. - ConnectivityStatus: BLE connectivity state enum - PairingErrorCode: Pairing error codes for better debugging
This commit is contained in:
+35
-2
@@ -46,6 +46,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryActivityV2;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DeviceDao;
|
||||
@@ -56,6 +57,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.BondingInterface;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.BondingUtil;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleHardware;
|
||||
|
||||
|
||||
public class PebblePairingActivity extends AbstractGBActivity implements BondingInterface {
|
||||
@@ -104,7 +106,7 @@ public class PebblePairingActivity extends AbstractGBActivity implements Bonding
|
||||
message.setText(getString(R.string.pairing, btDevice.getAddress()));
|
||||
|
||||
GBDevice device;
|
||||
if (BondingUtil.isLePebble(btDevice)) {
|
||||
if (PebbleHardware.isLePebbleCompanion(btDevice)) {
|
||||
if (!GBApplication.getPrefs().getBoolean("pebble_force_le", false)) {
|
||||
GB.toast(this, "Please switch on \"Always prefer BLE\" option in Pebble settings before pairing you Pebble LE", Toast.LENGTH_LONG, GB.ERROR);
|
||||
onBondingComplete(false);
|
||||
@@ -122,6 +124,30 @@ public class PebblePairingActivity extends AbstractGBActivity implements Bonding
|
||||
return;
|
||||
}
|
||||
|
||||
// Pebble 2, Pebble Time 2, and Pebble 2 Duo are BLE-only and need GATT connection first.
|
||||
// They require writing to a pairing trigger characteristic before createBond() works.
|
||||
// PebbleGATTClient handles the pairing trigger write and initiates bonding.
|
||||
// Note: These devices are BLE-only, so we don't need the "pebble_force_le" setting -
|
||||
// PebbleIoThread automatically uses BLE based on isPebble2() detection.
|
||||
// Use deviceCandidate to leverage manufacturer data for reliable device identification.
|
||||
if (BondingUtil.needsConnectFirstPairing(deviceCandidate)) {
|
||||
LOG.info("Pebble 2/Time 2/2 Duo detected via manufacturer data - using connect-first pairing (BLE-only device)");
|
||||
|
||||
registerBroadcastReceivers();
|
||||
|
||||
// Create device and initiate connection - BLE mode will be auto-detected in PebbleIoThread
|
||||
GBDevice gbDevice = DeviceHelper.getInstance().toSupportedDevice(deviceCandidate);
|
||||
if (gbDevice != null) {
|
||||
GB.toast(this, getString(R.string.discovery_trying_to_connect_to, gbDevice.getName()), Toast.LENGTH_SHORT, GB.INFO);
|
||||
GBApplication.deviceService(gbDevice).connect(true);
|
||||
// Don't call onBondingComplete() here - wait for bonding via broadcast receiver
|
||||
} else {
|
||||
LOG.error("Failed to create GBDevice for Pebble 2/Time 2/2 Duo");
|
||||
onBondingComplete(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (btDevice.getBondState() == BluetoothDevice.BOND_BONDED ||
|
||||
btDevice.getBondState() == BluetoothDevice.BOND_BONDING) {
|
||||
BondingUtil.connectThenComplete(this, deviceCandidate);
|
||||
@@ -171,6 +197,13 @@ public class PebblePairingActivity extends AbstractGBActivity implements Bonding
|
||||
public void onBondingComplete(boolean success) {
|
||||
LOG.debug("ONBONDINGCOMPLETE");
|
||||
unregisterBroadcastReceivers();
|
||||
|
||||
// Trigger device list refresh so the new device appears immediately
|
||||
if (success) {
|
||||
LocalBroadcastManager.getInstance(this).sendBroadcast(
|
||||
new Intent(DeviceManager.ACTION_REFRESH_DEVICELIST));
|
||||
}
|
||||
|
||||
if (success) {
|
||||
startActivity(new Intent(this, ControlCenterv2.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
|
||||
} else {
|
||||
@@ -178,7 +211,7 @@ public class PebblePairingActivity extends AbstractGBActivity implements Bonding
|
||||
}
|
||||
|
||||
// If it's not a LE Pebble, initiate a connection when bonding is complete
|
||||
if (!BondingUtil.isLePebble(getCurrentTarget().getDevice()) && success) {
|
||||
if (!PebbleHardware.isLePebbleCompanion(getCurrentTarget().getDevice()) && success) {
|
||||
BondingUtil.attemptToFirstConnect(getCurrentTarget().getDevice());
|
||||
}
|
||||
finish();
|
||||
|
||||
+6
-2
@@ -64,6 +64,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.Pebbl
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleHardware;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.BondingUtil;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils;
|
||||
@@ -182,8 +183,11 @@ class PebbleIoThread extends GBDeviceIoThread {
|
||||
deviceAddress = gbDevice.getVolatileAddress();
|
||||
}
|
||||
BluetoothDevice btDevice = mBtAdapter.getRemoteDevice(deviceAddress);
|
||||
if (btDevice.getType() == BluetoothDevice.DEVICE_TYPE_LE) {
|
||||
LOG.info("This is a Pebble 2 or Pebble-LE/Pebble Time LE, will use BLE");
|
||||
// Use BLE for DEVICE_TYPE_LE or for Pebble 2/Time 2/2 Duo which are DEVICE_TYPE_DUAL but BLE-only
|
||||
// Check both btDevice and gbDevice (btDevice.getName() can be null during reconnection)
|
||||
boolean isPebble2 = PebbleHardware.isBleOnly(gbDevice, btDevice);
|
||||
if (btDevice.getType() == BluetoothDevice.DEVICE_TYPE_LE || isPebble2) {
|
||||
LOG.info("This is a Pebble 2/Time 2/2 Duo or Pebble-LE/Pebble Time LE, will use BLE (isPebble2={})", isPebble2);
|
||||
mInStream = new PipedInputStream();
|
||||
mOutStream = new PipedOutputStream();
|
||||
mPebbleLESupport = new PebbleLESupport(this.getContext(), mPebbleSupport, gbDevice, btDevice, (PipedInputStream) mInStream, (PipedOutputStream) mOutStream);
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
/* 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;
|
||||
|
||||
/**
|
||||
* Parses the Pebble Connectivity characteristic to determine pairing state.
|
||||
* Based on libpebble3 ConnectivityWatcher.kt
|
||||
*
|
||||
* The connectivity characteristic contains status flags indicating the current
|
||||
* connection and pairing state from the watch's perspective.
|
||||
*/
|
||||
public class ConnectivityStatus {
|
||||
public final boolean connected;
|
||||
public final boolean paired;
|
||||
public final boolean encrypted;
|
||||
public final boolean hasBondedGateway;
|
||||
public final boolean supportsPinningWithoutSlaveSecurity;
|
||||
public final boolean hasRemoteAttemptedToUseStalePairing;
|
||||
public final PairingErrorCode pairingErrorCode;
|
||||
|
||||
/**
|
||||
* Parse a connectivity characteristic value.
|
||||
*
|
||||
* @param characteristicValue Raw bytes from the connectivity characteristic.
|
||||
* Byte 0: Status flags
|
||||
* Byte 3: Pairing error code (if present)
|
||||
*/
|
||||
public ConnectivityStatus(byte[] characteristicValue) {
|
||||
if (characteristicValue == null || characteristicValue.length == 0) {
|
||||
// Default to safe values if no data
|
||||
connected = false;
|
||||
paired = false;
|
||||
encrypted = false;
|
||||
hasBondedGateway = false;
|
||||
supportsPinningWithoutSlaveSecurity = false;
|
||||
hasRemoteAttemptedToUseStalePairing = false;
|
||||
pairingErrorCode = PairingErrorCode.UNKNOWN_ERROR;
|
||||
return;
|
||||
}
|
||||
|
||||
byte flags = characteristicValue[0];
|
||||
connected = (flags & 0b000001) != 0;
|
||||
paired = (flags & 0b000010) != 0;
|
||||
encrypted = (flags & 0b000100) != 0;
|
||||
hasBondedGateway = (flags & 0b001000) != 0;
|
||||
supportsPinningWithoutSlaveSecurity = (flags & 0b010000) != 0;
|
||||
hasRemoteAttemptedToUseStalePairing = (flags & 0b100000) != 0;
|
||||
|
||||
if (characteristicValue.length > 3) {
|
||||
pairingErrorCode = PairingErrorCode.fromValue(characteristicValue[3]);
|
||||
} else {
|
||||
pairingErrorCode = PairingErrorCode.NO_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format(
|
||||
"ConnectivityStatus{connected=%b, paired=%b, encrypted=%b, " +
|
||||
"hasBondedGateway=%b, supportsPinning=%b, stalePairing=%b, errorCode=%s}",
|
||||
connected, paired, encrypted, hasBondedGateway,
|
||||
supportsPinningWithoutSlaveSecurity, hasRemoteAttemptedToUseStalePairing,
|
||||
pairingErrorCode);
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/* 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;
|
||||
|
||||
/**
|
||||
* Pairing error codes from the Pebble Connectivity characteristic.
|
||||
* Based on libpebble3 PairingErrorCode enum.
|
||||
*/
|
||||
public enum PairingErrorCode {
|
||||
NO_ERROR((byte) 0),
|
||||
PASSKEY_ENTRY_FAILED((byte) 1),
|
||||
OOB_NOT_AVAILABLE((byte) 2),
|
||||
AUTHENTICATION_REQUIREMENTS((byte) 3),
|
||||
CONFIRM_VALUE_FAILED((byte) 4),
|
||||
PAIRING_NOT_SUPPORTED((byte) 5),
|
||||
ENCRYPTION_KEY_SIZE((byte) 6),
|
||||
COMMAND_NOT_SUPPORTED((byte) 7),
|
||||
UNSPECIFIED_REASON((byte) 8),
|
||||
REPEATED_ATTEMPTS((byte) 9),
|
||||
INVALID_PARAMETERS((byte) 10),
|
||||
DHKEY_CHECK_FAILED((byte) 11),
|
||||
NUMERIC_COMPARISON_FAILED((byte) 12),
|
||||
BR_EDR_PAIRING_IN_PROGRESS((byte) 13),
|
||||
CROSS_TRANSPORT_KEY_DERIVATION_NOT_ALLOWED((byte) 14),
|
||||
UNKNOWN_ERROR((byte) -1);
|
||||
|
||||
private final byte value;
|
||||
|
||||
PairingErrorCode(byte value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public byte getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public static PairingErrorCode fromValue(byte value) {
|
||||
for (PairingErrorCode code : values()) {
|
||||
if (code.value == value) {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
return UNKNOWN_ERROR;
|
||||
}
|
||||
}
|
||||
+296
-40
@@ -26,7 +26,14 @@ 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;
|
||||
@@ -71,6 +78,12 @@ class PebbleGATTClient extends BluetoothGattCallback {
|
||||
|
||||
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
|
||||
|
||||
PebbleGATTClient(PebbleLESupport pebbleLESupport, Context context, BluetoothDevice btDevice) {
|
||||
mContext = context;
|
||||
mPebbleLESupport = pebbleLESupport;
|
||||
@@ -106,17 +119,20 @@ class PebbleGATTClient extends BluetoothGattCallback {
|
||||
LOG.info("onCharacteristicRead() status = {}", status);
|
||||
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||
LOG.info("onCharacteristicRead() {} {}", characteristic.getUuid().toString(), GB.hexdump(characteristic.getValue(), 0, -1));
|
||||
if (characteristic.getUuid().equals(GattCharacteristic.UUID_CHARACTERISTIC_BATTERY_LEVEL)) {
|
||||
|
||||
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)) {
|
||||
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))) {
|
||||
// this is just a hack to force sequential ble commands for initialization
|
||||
// kind of event driven
|
||||
// And this never happens when not READING the pairing trigger which is only done for old pebbles running fw 3.x
|
||||
} 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 {
|
||||
@@ -155,9 +171,13 @@ class PebbleGATTClient extends BluetoothGattCallback {
|
||||
} else {
|
||||
LOG.warn("mWaitWriteCompleteLatch is null!");
|
||||
}
|
||||
} else if (characteristic.getUuid().equals(PAIRING_TRIGGER_CHARACTERISTIC) || characteristic.getUuid().equals(CONNECTIVITY_CHARACTERISTIC)) {
|
||||
//mBtDevice.createBond(); // did not work when last tried
|
||||
|
||||
} 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 {
|
||||
@@ -202,41 +222,44 @@ class PebbleGATTClient extends BluetoothGattCallback {
|
||||
}
|
||||
|
||||
LOG.info("onServicesDiscovered() status = {}", status);
|
||||
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||
BluetoothGattCharacteristic connectionParamCharacteristic = gatt.getService(SERVICE_UUID).getCharacteristic(CONNECTION_PARAMETERS_CHARACTERISTIC);
|
||||
hasConnectivityCharacteristics = connectionParamCharacteristic == null;
|
||||
if (status != BluetoothGatt.GATT_SUCCESS) {
|
||||
LOG.error("Service discovery failed with status {}", status);
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasConnectivityCharacteristics) {
|
||||
LOG.info("This seems to be an older le enabled Pebble (Pebble Time), or a 2025 Pebble");
|
||||
}
|
||||
BluetoothGattService pairingService = gatt.getService(SERVICE_UUID);
|
||||
if (pairingService == null) {
|
||||
LOG.error("Pairing service not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (doPairing) {
|
||||
BluetoothGattCharacteristic characteristic = gatt.getService(SERVICE_UUID).getCharacteristic(PAIRING_TRIGGER_CHARACTERISTIC);
|
||||
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 (mPebbleLESupport.clientOnly) {
|
||||
value = new byte[]{0x11}; // needed in clientOnly mode (TODO: try 0x19)
|
||||
} else {
|
||||
value = new byte[]{0x09}; // I just keep this, because it worked
|
||||
}
|
||||
WriteAction.writeCharacteristic(gatt, characteristic, value);
|
||||
} else {
|
||||
LOG.info("This seems to be some <4.0 FW Pebble, reading pairing trigger");
|
||||
gatt.readCharacteristic(characteristic);
|
||||
}
|
||||
BluetoothGattCharacteristic connectionParamCharacteristic =
|
||||
pairingService.getCharacteristic(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 {
|
||||
if (hasConnectivityCharacteristics) {
|
||||
subscribeToConnectivity(gatt);
|
||||
} else {
|
||||
subscribeToConnectionParams(gatt);
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -354,7 +377,240 @@ class PebbleGATTClient extends BluetoothGattCallback {
|
||||
mWaitWriteCompleteLatch = null;
|
||||
}
|
||||
|
||||
// ================== New Pairing Methods (based on libpebble3) ==================
|
||||
|
||||
/**
|
||||
* 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");
|
||||
if (hasConnectivityCharacteristics) {
|
||||
subscribeToConnectivity(gatt);
|
||||
} else {
|
||||
subscribeToConnectionParams(gatt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (mBluetoothGatt != null) {
|
||||
mBluetoothGatt.disconnect();
|
||||
mBluetoothGatt.close();
|
||||
|
||||
@@ -67,6 +67,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BleNamesResolver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleHardware;
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
public class BondingUtil {
|
||||
@@ -134,9 +135,12 @@ public class BondingUtil {
|
||||
switch (bondState) {
|
||||
case BluetoothDevice.BOND_BONDED: {
|
||||
LOG.info("Bonded with {}", device.getAddress());
|
||||
//noinspection StatementWithEmptyBody
|
||||
if (isLePebble(device) || isPebble2(device) || !bondingInterface.getAttemptToConnect()) {
|
||||
// Do not initiate connection to LE Pebble and some others!
|
||||
if (PebbleHardware.isLePebbleCompanion(device) || PebbleHardware.isBleOnly(device) || !bondingInterface.getAttemptToConnect()) {
|
||||
// Do not initiate connection to LE Pebble and Pebble 2/Time 2/2 Duo
|
||||
// For Pebble 2/Time 2/2 Duo, the connection was already started before bonding
|
||||
// Just signal that bonding is complete
|
||||
LOG.info("LE/Pebble2/Pebble2Duo device bonded - signaling completion");
|
||||
bondingInterface.onBondingComplete(true);
|
||||
} else {
|
||||
attemptToFirstConnect(device);
|
||||
}
|
||||
@@ -331,20 +335,31 @@ public class BondingUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if device is LE Pebble
|
||||
* Checks if device needs connect-first pairing (GATT connection before createBond).
|
||||
* Pebble 2, Pebble Time 2, and Pebble 2 Duo require writing to a pairing trigger
|
||||
* characteristic before Android's createBond() will succeed.
|
||||
*
|
||||
* @param candidate Device candidate with manufacturer data from BLE scan
|
||||
* @return true if this device needs connect-first pairing
|
||||
*/
|
||||
public static boolean isLePebble(BluetoothDevice device) {
|
||||
return (device.getType() == BluetoothDevice.DEVICE_TYPE_DUAL || device.getType() == BluetoothDevice.DEVICE_TYPE_LE) &&
|
||||
(device.getName().startsWith("Pebble-LE ") || device.getName().startsWith("Pebble Time LE "));
|
||||
public static boolean needsConnectFirstPairing(GBDeviceCandidate candidate) {
|
||||
if (candidate == null) {
|
||||
return false;
|
||||
}
|
||||
// First try manufacturer data - this is the most reliable method
|
||||
if (PebbleHardware.isBleOnly(candidate.getManufacturerSpecificData())) {
|
||||
return true;
|
||||
}
|
||||
// Fall back to name-based detection
|
||||
return PebbleHardware.isBleOnly(candidate.getDevice());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if device is Pebble 2
|
||||
* Checks if device needs connect-first pairing (GATT connection before createBond).
|
||||
* Uses name-based detection when GBDeviceCandidate is not available.
|
||||
*/
|
||||
public static boolean isPebble2(BluetoothDevice device) {
|
||||
return device.getType() == BluetoothDevice.DEVICE_TYPE_LE &&
|
||||
device.getName().startsWith("Pebble ") &&
|
||||
!device.getName().startsWith("Pebble Time LE ");
|
||||
public static boolean needsConnectFirstPairing(BluetoothDevice device) {
|
||||
return PebbleHardware.isBleOnly(device);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -445,7 +460,7 @@ public class BondingUtil {
|
||||
|
||||
if (bondState == BluetoothDevice.BOND_BONDED) {
|
||||
GB.toast(bondingInterface.getContext().getString(R.string.pairing_already_bonded, device.getName(), device.getAddress()), Toast.LENGTH_SHORT, GB.INFO);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !isPebble2(device) && contextIsActivity) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !PebbleHardware.isBleOnly(device) && contextIsActivity) {
|
||||
// If CompanionDeviceManager is available, skip connection and go bond
|
||||
// TODO: It would theoretically be nice to check if it's already been granted,
|
||||
// but re-bond works
|
||||
@@ -458,9 +473,9 @@ public class BondingUtil {
|
||||
|
||||
GB.toast(bondingInterface.getContext(), bondingInterface.getContext().getString(R.string.pairing_creating_bond_with, device.getName(), device.getAddress()), Toast.LENGTH_LONG, GB.INFO);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !isPebble2(device) && contextIsActivity) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !PebbleHardware.isBleOnly(device) && contextIsActivity) {
|
||||
askCompanionPairing(bondingInterface, device);
|
||||
} else if (isPebble2(device)) {
|
||||
} else if (PebbleHardware.isBleOnly(device)) {
|
||||
// TODO: start companionDevicePairing after connecting to Pebble 2 but before writing to pairing trigger
|
||||
attemptToFirstConnect(device);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user