From 876f5c805097825162322fb3b6a79b74f0826f22 Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Sun, 2 Nov 2025 12:27:15 +0100 Subject: [PATCH] Pebble 2 Duo: request battery level though battery service when connecting Also cleanup code a bit. Problem is that the battery level vanishes for some reason after being displayed for a split second --- CHANGELOG.md | 1 + .../devices/pebble/PebbleIoThread.java | 4 +- .../devices/pebble/ble/PebbleGATTClient.java | 102 +++++++++++++----- .../devices/pebble/ble/PebbleGATTServer.java | 12 +-- .../devices/pebble/ble/PebbleLESupport.java | 39 ++++--- 5 files changed, 101 insertions(+), 57 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8e80acefa..7ddb938b41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ #### Next * Initial support for Pebble 2 Duo (Experimental) +* Pebble 2/2 Duo: Fix random crashes on disconnect #### 0.87.1 diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java index ffe40ae39c..58bc5977db 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java @@ -192,7 +192,7 @@ class PebbleIoThread extends GBDeviceIoThread { LOG.info("This is a Pebble 2 or Pebble-LE/Pebble Time LE, will use BLE"); mInStream = new PipedInputStream(); mOutStream = new PipedOutputStream(); - mPebbleLESupport = new PebbleLESupport(this.getContext(), gbDevice, btDevice, (PipedInputStream) mInStream, (PipedOutputStream) mOutStream); + mPebbleLESupport = new PebbleLESupport(this.getContext(), mPebbleSupport, gbDevice, btDevice, (PipedInputStream) mInStream, (PipedOutputStream) mOutStream); } else { ParcelUuid[] uuids = btDevice.getUuids(); if (uuids == null) { @@ -214,7 +214,7 @@ class PebbleIoThread extends GBDeviceIoThread { LOG.info("This seems to be a 2025 Pebble will use BLE"); mInStream = new PipedInputStream(); mOutStream = new PipedOutputStream(); - mPebbleLESupport = new PebbleLESupport(this.getContext(), gbDevice, btDevice, (PipedInputStream) mInStream, (PipedOutputStream) mOutStream); + mPebbleLESupport = new PebbleLESupport(this.getContext(), mPebbleSupport, gbDevice, btDevice, (PipedInputStream) mInStream, (PipedOutputStream) mOutStream); } } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/ble/PebbleGATTClient.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/ble/PebbleGATTClient.java index 7316b2d618..835f7f64b3 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/ble/PebbleGATTClient.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/ble/PebbleGATTClient.java @@ -33,10 +33,13 @@ import org.slf4j.LoggerFactory; import java.util.UUID; import java.util.concurrent.CountDownLatch; -import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; +import nodomain.freeyourgadget.gadgetbridge.model.BatteryState; +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic; +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.NotifyAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WriteAction; -import nodomain.freeyourgadget.gadgetbridge.util.BondingUtil; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.ValueDecoder; import nodomain.freeyourgadget.gadgetbridge.util.GB; @SuppressLint("MissingPermission") @@ -63,7 +66,6 @@ class PebbleGATTClient extends BluetoothGattCallback { private boolean hasConnectivityCharacteristics = false; private final boolean doPairing = true; - private final boolean removeBond = false; private BluetoothGatt mBluetoothGatt; private CountDownLatch mWaitWriteCompleteLatch; @@ -76,7 +78,7 @@ class PebbleGATTClient extends BluetoothGattCallback { @Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { - if (!mPebbleLESupport.isExpectedDevice(gatt.getDevice())) { + if (mPebbleLESupport.isUnexpectedDevice(gatt.getDevice())) { return; } @@ -86,6 +88,9 @@ class PebbleGATTClient extends BluetoothGattCallback { mPebbleLESupport.setMTU(newMTU); } else if (characteristic.getUuid().equals(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()); + LOG.info("Got battery level through notification, is at {}%", battery_percent); } else { LOG.info("onCharacteristicChanged() {} {}", characteristic.getUuid().toString(), GB.hexdump(characteristic.getValue(), 0, -1)); } @@ -93,29 +98,40 @@ class PebbleGATTClient extends BluetoothGattCallback { @Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { - if (!mPebbleLESupport.isExpectedDevice(gatt.getDevice())) { + if (mPebbleLESupport.isUnexpectedDevice(gatt.getDevice())) { return; } LOG.info("onCharacteristicRead() status = {}", status); if (status == BluetoothGatt.GATT_SUCCESS) { LOG.info("onCharacteristicRead() {} {}", characteristic.getUuid().toString(), GB.hexdump(characteristic.getValue(), 0, -1)); - - if (hasConnectivityCharacteristics) { - subscribeToConnectivity(gatt); - } else { - subscribeToConnectionParams(gatt); + 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 + if (hasConnectivityCharacteristics) { + subscribeToConnectivity(gatt); + } else { + subscribeToConnectionParams(gatt); + } } } } @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { - if (!mPebbleLESupport.isExpectedDevice(gatt.getDevice())) { + if (mPebbleLESupport.isUnexpectedDevice(gatt.getDevice())) { return; } - LOG.info("onConnectionStateChange() status = " + status + " newState = " + newState); + LOG.info("onConnectionStateChange() status = {} newState = {}", status, newState); if (newState == BluetoothGatt.STATE_CONNECTED) { LOG.info("calling discoverServices()"); gatt.discoverServices(); @@ -126,7 +142,7 @@ class PebbleGATTClient extends BluetoothGattCallback { @Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { - if (!mPebbleLESupport.isExpectedDevice(gatt.getDevice())) { + if (mPebbleLESupport.isUnexpectedDevice(gatt.getDevice())) { return; } if (characteristic.getUuid().equals(PPOGATT_CHARACTERISTIC_WRITE)) { @@ -153,19 +169,21 @@ class PebbleGATTClient extends BluetoothGattCallback { @Override public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor bluetoothGattDescriptor, int status) { - if (!mPebbleLESupport.isExpectedDevice(gatt.getDevice())) { + if (mPebbleLESupport.isUnexpectedDevice(gatt.getDevice())) { return; } - LOG.info("onDescriptorWrite() status=" + status); + LOG.info("onDescriptorWrite() status={}", status); UUID CHARACTERISTICUUID = bluetoothGattDescriptor.getCharacteristic().getUuid(); + // this is just a hack to force sequential ble commands for initialization + // kind of event driven if (CHARACTERISTICUUID.equals(CONNECTION_PARAMETERS_CHARACTERISTIC)) { subscribeToConnectivity(gatt); } else if (CHARACTERISTICUUID.equals(CONNECTIVITY_CHARACTERISTIC)) { - subscribeToMTU(gatt); - } else if (CHARACTERISTICUUID.equals(MTU_CHARACTERISTIC)) { + subscribeToMTUOrBattery(gatt); + } else if (CHARACTERISTICUUID.equals(MTU_CHARACTERISTIC) || CHARACTERISTICUUID.equals(GattCharacteristic.UUID_CHARACTERISTIC_BATTERY_LEVEL)) { if (mPebbleLESupport.clientOnly) { subscribeToPPoGATT(gatt); } else { @@ -178,7 +196,7 @@ class PebbleGATTClient extends BluetoothGattCallback { @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { - if (!mPebbleLESupport.isExpectedDevice(gatt.getDevice())) { + if (mPebbleLESupport.isUnexpectedDevice(gatt.getDevice())) { return; } @@ -227,13 +245,16 @@ class PebbleGATTClient extends BluetoothGattCallback { if (status == BluetoothGatt.GATT_SUCCESS) { LOG.info("MTU changed to {}", mtu); mPebbleLESupport.setMTU(mtu); + + // read battery + BluetoothGattCharacteristic characteristic = gatt.getService(GattService.UUID_SERVICE_BATTERY_SERVICE).getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_BATTERY_LEVEL); + if (characteristic != null) { + gatt.readCharacteristic(characteristic); + } } } private void connectToPebble(BluetoothDevice btDevice) { - if (removeBond) { - BondingUtil.Unpair(GBApplication.getContext(), btDevice.getAddress()); - } if (mBluetoothGatt != null) { this.close(); } @@ -255,8 +276,7 @@ class PebbleGATTClient extends BluetoothGattCallback { 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, requesting MTU via gatt"); - gatt.requestMtu(339); + LOG.info("Could not find MTU Characteristic. This seems to be a 2025 Pebble"); } } @@ -267,14 +287,40 @@ class PebbleGATTClient extends BluetoothGattCallback { gatt.setCharacteristicNotification(gatt.getService(SERVICE_UUID).getCharacteristic(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); + if (characteristic != null) { + subscribeToMTU(gatt); + } else { + subscribeToBattery(gatt); + } + } + + private void subscribeToBattery(BluetoothGatt gatt) { + 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); + 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 { + LOG.info("Could not find Battery Characteristic. This is normal on pre-2025 pebbles."); + } + } + private void setMTU(BluetoothGatt gatt) { LOG.info("setting MTU"); BluetoothGattCharacteristic characteristic = gatt.getService(SERVICE_UUID).getCharacteristic(MTU_CHARACTERISTIC); - BluetoothGattDescriptor descriptor = characteristic.getDescriptor(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. - WriteAction.writeCharacteristic(gatt,characteristic, characteristic.getValue()); + if (characteristic != null) { + BluetoothGattDescriptor descriptor = characteristic.getDescriptor(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. + WriteAction.writeCharacteristic(gatt, characteristic, characteristic.getValue()); + } else { + gatt.requestMtu(339); + } } private void subscribeToPPoGATT(BluetoothGatt gatt) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/ble/PebbleGATTServer.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/ble/PebbleGATTServer.java index 5e2c81fd7f..16265aac1d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/ble/PebbleGATTServer.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/ble/PebbleGATTServer.java @@ -93,7 +93,7 @@ class PebbleGATTServer extends BluetoothGattServerCallback { @Override public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) { - if (!mPebbleLESupport.isExpectedDevice(device)) { + if (mPebbleLESupport.isUnexpectedDevice(device)) { return; } @@ -112,7 +112,7 @@ class PebbleGATTServer extends BluetoothGattServerCallback { @Override public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) { - if (!mPebbleLESupport.isExpectedDevice(device)) { + if (mPebbleLESupport.isUnexpectedDevice(device)) { return; } @@ -125,7 +125,7 @@ class PebbleGATTServer extends BluetoothGattServerCallback { @Override public void onConnectionStateChange(BluetoothDevice device, int status, int newState) { - if (!mPebbleLESupport.isExpectedDevice(device)) { + if (mPebbleLESupport.isUnexpectedDevice(device)) { return; } @@ -139,7 +139,7 @@ class PebbleGATTServer extends BluetoothGattServerCallback { public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) { - if (!mPebbleLESupport.isExpectedDevice(device)) { + if (mPebbleLESupport.isUnexpectedDevice(device)) { return; } @@ -166,7 +166,7 @@ class PebbleGATTServer extends BluetoothGattServerCallback { @Override public void onMtuChanged(BluetoothDevice device, int mtu) { - if (!mPebbleLESupport.isExpectedDevice(device)) { + if (mPebbleLESupport.isUnexpectedDevice(device)) { return; } @@ -177,7 +177,7 @@ class PebbleGATTServer extends BluetoothGattServerCallback { @Override public void onNotificationSent(BluetoothDevice bluetoothDevice, int status) { - if (!mPebbleLESupport.isExpectedDevice(bluetoothDevice)) { + if (mPebbleLESupport.isUnexpectedDevice(bluetoothDevice)) { return; } if (status != BluetoothGatt.GATT_SUCCESS) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/ble/PebbleLESupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/ble/PebbleLESupport.java index ae6127a160..8e5ebc5340 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/ble/PebbleLESupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/ble/PebbleLESupport.java @@ -33,6 +33,7 @@ import java.util.concurrent.atomic.AtomicLong; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleSupport; public class PebbleLESupport { private static final Logger LOG = LoggerFactory.getLogger(PebbleLESupport.class); @@ -50,8 +51,10 @@ public class PebbleLESupport { private boolean mIsConnected = false; private final HandlerThread mWriteHandlerThread; private final Handler mWriteHandler; + private final PebbleSupport mPebbleSupport; - public PebbleLESupport(Context context, GBDevice gbDevice, final BluetoothDevice btDevice, PipedInputStream pipedInputStream, PipedOutputStream pipedOutputStream) throws IOException { + public PebbleLESupport(Context context, PebbleSupport pebbleSupport, GBDevice gbDevice, final BluetoothDevice btDevice, PipedInputStream pipedInputStream, PipedOutputStream pipedOutputStream) throws IOException { + mPebbleSupport = pebbleSupport; mBtDevice = btDevice; mPipedInputStream = new PipedInputStream(); mPipedOutputStream = new PipedOutputStream(); @@ -169,12 +172,12 @@ public class PebbleLESupport { LOG.info("got command 0x02"); if (value.length > 1) { sendDataToPebble(new byte[]{0x03, 0x19, 0x19}); // no we don't know what that means - createPipedInputReader(); // FIXME: maybe not here + createPipedInputReader(); // maybe better not here? } else { sendDataToPebble(new byte[]{0x03}); // no we don't know what that means } } else if (command == 0) { // normal package - LOG.info("got PPoGATT package serial = " + serial + " sending ACK"); + LOG.info("got PPoGATT package serial = {} sending ACK", serial); sendAckToPebble(serial); @@ -188,23 +191,17 @@ public class PebbleLESupport { private synchronized void sendDataToPebble(final byte[] bytes) { if (mPebbleGATTServer != null) { - mWriteHandler.post(new Runnable() { - @Override - public void run() { - mPebbleGATTServer.sendDataToPebble(bytes); - } - }); + mWriteHandler.post(() -> mPebbleGATTServer.sendDataToPebble(bytes)); } else { // For now only in experimental client only code - mWriteHandler.post(new Runnable() { - @Override - public void run() { - mPebbleGATTClient.sendDataToPebble(bytes); - } - }); + mWriteHandler.post(() -> mPebbleGATTClient.sendDataToPebble(bytes)); } } + public PebbleSupport getPebbleSupport() { + return mPebbleSupport; + } + private class PipeReader extends Thread { int mmSequence = 0; @@ -256,26 +253,26 @@ public class PebbleLESupport { break; } } - LOG.info("Pipereader thread shut down"); + LOG.info("PipeReader thread shut down"); } @Override public void interrupt() { super.interrupt(); try { - LOG.info("closing piped inputstream"); + LOG.info("closing piped InputStream"); mPipedInputStream.close(); } catch (IOException ignore) { } } } - boolean isExpectedDevice(BluetoothDevice device) { + boolean isUnexpectedDevice(BluetoothDevice device) { if (!device.getAddress().equals(mBtDevice.getAddress())) { - LOG.info("unhandled device: " + device.getAddress() + " , ignoring, will only talk to " + mBtDevice.getAddress()); - return false; + LOG.info("unhandled device: {} , ignoring, will only talk to {}", device.getAddress(), mBtDevice.getAddress()); + return true; } - return true; + return false; } }