Pebble: Make connection to new Pebble 2 Duo work in recovery mode

This is just a first hack, it is unclear how to deal with MTU etc yet.
I was able to pair and connect after some tries, which is better than nothing
This commit is contained in:
Andreas Shimokawa
2025-10-30 15:03:50 +01:00
parent eced8996a4
commit 9c5980617d
3 changed files with 41 additions and 36 deletions
@@ -188,7 +188,7 @@ class PebbleIoThread extends GBDeviceIoThread {
deviceAddress = gbDevice.getVolatileAddress(); deviceAddress = gbDevice.getVolatileAddress();
} }
BluetoothDevice btDevice = mBtAdapter.getRemoteDevice(deviceAddress); BluetoothDevice btDevice = mBtAdapter.getRemoteDevice(deviceAddress);
if (btDevice.getType() == BluetoothDevice.DEVICE_TYPE_LE) { if (btDevice.getType() == BluetoothDevice.DEVICE_TYPE_LE || btDevice.getType() == BluetoothDevice.DEVICE_TYPE_DUAL) {
LOG.info("This is a Pebble 2 or Pebble-LE/Pebble Time LE, will use BLE"); LOG.info("This is a Pebble 2 or Pebble-LE/Pebble Time LE, will use BLE");
mInStream = new PipedInputStream(); mInStream = new PipedInputStream();
mOutStream = new PipedOutputStream(); mOutStream = new PipedOutputStream();
@@ -16,6 +16,9 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */ along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.ble; 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.annotation.SuppressLint;
import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGatt;
@@ -36,9 +39,6 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WriteAction;
import nodomain.freeyourgadget.gadgetbridge.util.BondingUtil; import nodomain.freeyourgadget.gadgetbridge.util.BondingUtil;
import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.GB;
import static android.bluetooth.BluetoothGattCharacteristic.FORMAT_UINT16;
import static android.bluetooth.BluetoothGattCharacteristic.PROPERTY_WRITE;
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
class PebbleGATTClient extends BluetoothGattCallback { class PebbleGATTClient extends BluetoothGattCallback {
@@ -61,7 +61,7 @@ class PebbleGATTClient extends BluetoothGattCallback {
private final Context mContext; private final Context mContext;
private final PebbleLESupport mPebbleLESupport; private final PebbleLESupport mPebbleLESupport;
private boolean oldPebble = false; private boolean hasConnectivityCharacteristics = false;
private final boolean doPairing = true; private final boolean doPairing = true;
private final boolean removeBond = false; private final boolean removeBond = false;
private BluetoothGatt mBluetoothGatt; private BluetoothGatt mBluetoothGatt;
@@ -82,12 +82,12 @@ class PebbleGATTClient extends BluetoothGattCallback {
if (characteristic.getUuid().equals(MTU_CHARACTERISTIC)) { if (characteristic.getUuid().equals(MTU_CHARACTERISTIC)) {
int newMTU = characteristic.getIntValue(FORMAT_UINT16, 0); int newMTU = characteristic.getIntValue(FORMAT_UINT16, 0);
LOG.info("Pebble requested MTU: " + newMTU); LOG.info("Pebble requested MTU: {}", newMTU);
mPebbleLESupport.setMTU(newMTU); mPebbleLESupport.setMTU(newMTU);
} else if (characteristic.getUuid().equals(PPOGATT_CHARACTERISTIC_READ)) { } else if (characteristic.getUuid().equals(PPOGATT_CHARACTERISTIC_READ)) {
mPebbleLESupport.handlePPoGATTPacket(characteristic.getValue().clone()); mPebbleLESupport.handlePPoGATTPacket(characteristic.getValue().clone());
} else { } else {
LOG.info("onCharacteristicChanged() " + characteristic.getUuid().toString() + " " + GB.hexdump(characteristic.getValue(), 0, -1)); LOG.info("onCharacteristicChanged() {} {}", characteristic.getUuid().toString(), GB.hexdump(characteristic.getValue(), 0, -1));
} }
} }
@@ -97,11 +97,11 @@ class PebbleGATTClient extends BluetoothGattCallback {
return; return;
} }
LOG.info("onCharacteristicRead() status = " + status); LOG.info("onCharacteristicRead() status = {}", status);
if (status == BluetoothGatt.GATT_SUCCESS) { if (status == BluetoothGatt.GATT_SUCCESS) {
LOG.info("onCharacteristicRead() " + characteristic.getUuid().toString() + " " + GB.hexdump(characteristic.getValue(), 0, -1)); LOG.info("onCharacteristicRead() {} {}", characteristic.getUuid().toString(), GB.hexdump(characteristic.getValue(), 0, -1));
if (oldPebble) { if (hasConnectivityCharacteristics) {
subscribeToConnectivity(gatt); subscribeToConnectivity(gatt);
} else { } else {
subscribeToConnectionParams(gatt); subscribeToConnectionParams(gatt);
@@ -141,7 +141,7 @@ class PebbleGATTClient extends BluetoothGattCallback {
} else if (characteristic.getUuid().equals(PAIRING_TRIGGER_CHARACTERISTIC) || characteristic.getUuid().equals(CONNECTIVITY_CHARACTERISTIC)) { } else if (characteristic.getUuid().equals(PAIRING_TRIGGER_CHARACTERISTIC) || characteristic.getUuid().equals(CONNECTIVITY_CHARACTERISTIC)) {
//mBtDevice.createBond(); // did not work when last tried //mBtDevice.createBond(); // did not work when last tried
if (oldPebble) { if (hasConnectivityCharacteristics) {
subscribeToConnectivity(gatt); subscribeToConnectivity(gatt);
} else { } else {
subscribeToConnectionParams(gatt); subscribeToConnectionParams(gatt);
@@ -182,13 +182,13 @@ class PebbleGATTClient extends BluetoothGattCallback {
return; return;
} }
LOG.info("onServicesDiscovered() status = " + status); LOG.info("onServicesDiscovered() status = {}", status);
if (status == BluetoothGatt.GATT_SUCCESS) { if (status == BluetoothGatt.GATT_SUCCESS) {
BluetoothGattCharacteristic connectionPararmharacteristic = gatt.getService(SERVICE_UUID).getCharacteristic(CONNECTION_PARAMETERS_CHARACTERISTIC); BluetoothGattCharacteristic connectionParamCharacteristic = gatt.getService(SERVICE_UUID).getCharacteristic(CONNECTION_PARAMETERS_CHARACTERISTIC);
oldPebble = connectionPararmharacteristic == null; hasConnectivityCharacteristics = connectionParamCharacteristic == null;
if (oldPebble) { if (hasConnectivityCharacteristics) {
LOG.info("This seems to be an older le enabled pebble"); LOG.info("This seems to be an older le enabled Pebble (Pebble Time), or a 2025 Pebble");
} }
if (doPairing) { if (doPairing) {
@@ -213,7 +213,7 @@ class PebbleGATTClient extends BluetoothGattCallback {
gatt.readCharacteristic(characteristic); gatt.readCharacteristic(characteristic);
} }
} else { } else {
if (oldPebble) { if (hasConnectivityCharacteristics) {
subscribeToConnectivity(gatt); subscribeToConnectivity(gatt);
} else { } else {
subscribeToConnectionParams(gatt); subscribeToConnectionParams(gatt);
@@ -240,10 +240,15 @@ class PebbleGATTClient extends BluetoothGattCallback {
} }
private void subscribeToMTU(BluetoothGatt gatt) { private void subscribeToMTU(BluetoothGatt gatt) {
LOG.info("subscribing to mtu characteristic"); BluetoothGattCharacteristic characteristic = gatt.getService(SERVICE_UUID).getCharacteristic(MTU_CHARACTERISTIC);
BluetoothGattDescriptor descriptor = gatt.getService(SERVICE_UUID).getCharacteristic(MTU_CHARACTERISTIC).getDescriptor(CHARACTERISTIC_CONFIGURATION_DESCRIPTOR); if (characteristic != null) {
NotifyAction.writeDescriptor(gatt, descriptor, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); LOG.info("subscribing to mtu characteristic");
gatt.setCharacteristicNotification(gatt.getService(SERVICE_UUID).getCharacteristic(MTU_CHARACTERISTIC), true); BluetoothGattDescriptor descriptor = gatt.getService(SERVICE_UUID).getCharacteristic(MTU_CHARACTERISTIC).getDescriptor(CHARACTERISTIC_CONFIGURATION_DESCRIPTOR);
NotifyAction.writeDescriptor(gatt, descriptor, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
gatt.setCharacteristicNotification(gatt.getService(SERVICE_UUID).getCharacteristic(MTU_CHARACTERISTIC), true);
} else {
LOG.info("Could not find MTU Characteristic. This seems to be a 2025 Pebble");
}
} }
private void subscribeToConnectionParams(BluetoothGatt gatt) { private void subscribeToConnectionParams(BluetoothGatt gatt) {
@@ -38,22 +38,20 @@ public class PebbleLESupport {
private static final Logger LOG = LoggerFactory.getLogger(PebbleLESupport.class); private static final Logger LOG = LoggerFactory.getLogger(PebbleLESupport.class);
private static final AtomicLong THREAD_COUNTER = new AtomicLong(0L); private static final AtomicLong THREAD_COUNTER = new AtomicLong(0L);
private final GBDevice mgbDevice;
private final BluetoothDevice mBtDevice; private final BluetoothDevice mBtDevice;
private PipeReader mPipeReader; private PipeReader mPipeReader;
private PebbleGATTServer mPebbleGATTServer; private PebbleGATTServer mPebbleGATTServer;
private PebbleGATTClient mPebbleGATTClient; private PebbleGATTClient mPebbleGATTClient;
private PipedInputStream mPipedInputStream; private final PipedInputStream mPipedInputStream;
private PipedOutputStream mPipedOutputStream; private final PipedOutputStream mPipedOutputStream;
private int mMTU = 20; private int mMTU = 20;
private int mMTULimit = Integer.MAX_VALUE; private int mMTULimit;
public boolean clientOnly = false; // currently experimental, and only possible for Pebble 2 public boolean clientOnly; // currently experimental, and only possible for Pebble 2
private boolean mIsConnected = false; private boolean mIsConnected = false;
private HandlerThread mWriteHandlerThread; private final HandlerThread mWriteHandlerThread;
private Handler mWriteHandler; private final Handler mWriteHandler;
public PebbleLESupport(Context context, GBDevice gbDevice, final BluetoothDevice btDevice, PipedInputStream pipedInputStream, PipedOutputStream pipedOutputStream) throws IOException { public PebbleLESupport(Context context, GBDevice gbDevice, final BluetoothDevice btDevice, PipedInputStream pipedInputStream, PipedOutputStream pipedOutputStream) throws IOException {
mgbDevice = gbDevice;
mBtDevice = btDevice; mBtDevice = btDevice;
mPipedInputStream = new PipedInputStream(); mPipedInputStream = new PipedInputStream();
mPipedOutputStream = new PipedOutputStream(); mPipedOutputStream = new PipedOutputStream();
@@ -69,14 +67,16 @@ public class PebbleLESupport {
mWriteHandler = new Handler(mWriteHandlerThread.getLooper()); mWriteHandler = new Handler(mWriteHandlerThread.getLooper());
mWriteHandler.post(() -> LOG.debug("started thread {}", Thread.currentThread().getName())); mWriteHandler.post(() -> LOG.debug("started thread {}", Thread.currentThread().getName()));
mMTULimit = GBApplication.getDevicePrefs(mgbDevice).getInt("pebble_mtu_limit", 512); mMTULimit = GBApplication.getDevicePrefs(gbDevice).getInt("pebble_mtu_limit", 512);
mMTULimit = Math.max(mMTULimit, 20); mMTULimit = Math.max(mMTULimit, 20);
mMTULimit = Math.min(mMTULimit, 512); mMTULimit = Math.min(mMTULimit, 512);
clientOnly = GBApplication.getDevicePrefs(mgbDevice).getBoolean("pebble_gatt_clientonly", false); clientOnly = GBApplication.getDevicePrefs(gbDevice).getBoolean("pebble_gatt_clientonly", false);
if (!clientOnly) { if (!clientOnly) {
mPebbleGATTServer = new PebbleGATTServer(this, context, mBtDevice); mPebbleGATTServer = new PebbleGATTServer(this, context, mBtDevice);
} else {
LOG.info ("using client only mode");
} }
if (clientOnly || mPebbleGATTServer.initialize()) { if (clientOnly || mPebbleGATTServer.initialize()) {
mPebbleGATTClient = new PebbleGATTClient(this, context, mBtDevice); mPebbleGATTClient = new PebbleGATTClient(this, context, mBtDevice);
@@ -94,9 +94,9 @@ public class PebbleLESupport {
throw new IOException("connection failed"); throw new IOException("connection failed");
} }
private void writeToPipedOutputStream(byte[] value, int offset, int count) { private void writeToPipedOutputStream(byte[] value, int count) {
try { try {
mPipedOutputStream.write(value, offset, count); mPipedOutputStream.write(value, 1, count);
} catch (IOException e) { } catch (IOException e) {
LOG.warn("error writing to output stream", e); LOG.warn("error writing to output stream", e);
} }
@@ -163,7 +163,7 @@ public class PebbleLESupport {
int command = header & 7; int command = header & 7;
int serial = header >> 3; int serial = header >> 3;
if (command == 0x01) { if (command == 0x01) {
LOG.info("got ACK for serial = " + serial); LOG.info("got ACK for serial = {}", serial);
} }
if (command == 0x02) { // some request? if (command == 0x02) { // some request?
LOG.info("got command 0x02"); LOG.info("got command 0x02");
@@ -178,7 +178,7 @@ public class PebbleLESupport {
sendAckToPebble(serial); sendAckToPebble(serial);
writeToPipedOutputStream(value, 1, value.length - 1); writeToPipedOutputStream(value, value.length - 1);
} }
} }
@@ -238,7 +238,7 @@ public class PebbleLESupport {
int srcPos = 0; int srcPos = 0;
int maxChunkSize = calcMaxWriteChunk(mMTU) - 1; int maxChunkSize = calcMaxWriteChunk(mMTU) - 1;
while (payloadToSend > 0) { while (payloadToSend > 0) {
int chunkSize = (payloadToSend < maxChunkSize) ? payloadToSend : maxChunkSize; int chunkSize = Math.min(payloadToSend, maxChunkSize);
byte[] outBuf = new byte[chunkSize + 1]; byte[] outBuf = new byte[chunkSize + 1];
outBuf[0] = (byte) ((mmSequence++ << 3) & 0xff); outBuf[0] = (byte) ((mmSequence++ << 3) & 0xff);
System.arraycopy(buf, srcPos, outBuf, 1, chunkSize); System.arraycopy(buf, srcPos, outBuf, 1, chunkSize);