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
This commit is contained in:
Andreas Shimokawa
2025-11-02 12:27:15 +01:00
parent d0c9878934
commit 876f5c8050
5 changed files with 101 additions and 57 deletions
+1
View File
@@ -2,6 +2,7 @@
#### Next
* Initial support for Pebble 2 Duo (Experimental)
* Pebble 2/2 Duo: Fix random crashes on disconnect
#### 0.87.1
@@ -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);
}
}
}
@@ -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) {
@@ -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) {
@@ -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;
}
}