From 552547ec9ec8c49d745fe9a38b9789318eac4fa8 Mon Sep 17 00:00:00 2001 From: Benjamin Temple Date: Sat, 24 Jan 2026 15:01:53 -0800 Subject: [PATCH] Pebble: Add support for pull to refresh for pebble health data Only signalActivityDataFinish on error as datalog will invoke once health sync data finishes syncing --- .../devices/pebble/PebbleCoordinator.java | 5 +++ .../devices/pebble/PebbleProtocol.java | 44 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleCoordinator.java index f4e3fdc409..ef1ddcccca 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleCoordinator.java @@ -114,6 +114,11 @@ public class PebbleCoordinator extends AbstractBLClassicDeviceCoordinator { return true; } + @Override + public boolean supportsDataFetching(@NonNull GBDevice device) { + return true; + } + @Override public boolean supportsScreenshots(final GBDevice device) { return true; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java index 9e98ec1359..08edcb3490 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java @@ -109,6 +109,7 @@ public class PebbleProtocol extends GBDeviceProtocol { private static final short ENDPOINT_APPREORDER = (short) 0xabcd; // FW >=3.x private static final short ENDPOINT_BLOBDB = (short) 0xb1db; // FW >=3.x private static final short ENDPOINT_PUTBYTES = (short) 0xbeef; + private static final short ENDPOINT_HEALTH_SYNC = 911; private static final byte APPRUNSTATE_START = 1; private static final byte APPRUNSTATE_STOP = 2; @@ -201,6 +202,9 @@ public class PebbleProtocol extends GBDeviceProtocol { private static final byte DATALOG_ACK = (byte) 0x85; private static final byte DATALOG_NACK = (byte) 0x86; + private static final byte HEALTH_SYNC_CMD_SYNC = 0x01; + private static final byte HEALTH_SYNC_CMD_ACK = 0x11; + private static final byte PING_PING = 0; private static final byte PING_PONG = 1; @@ -735,9 +739,30 @@ public class PebbleProtocol extends GBDeviceProtocol { if (dataTypes == RecordedDataTypes.TYPE_DEBUGLOGS) { return encodeRequestLogDump(0, 0); } + if ((dataTypes & RecordedDataTypes.TYPE_ACTIVITY) != 0) { + return encodeHealthSync(); + } return null; } + private byte[] encodeHealthSync() { + // Health sync message format (from PebbleOS health_sync_endpoint.c): + // - 1 byte: command (0x01 = sync) + // - 4 bytes: seconds_since_sync (uint32_t, little-endian) + // Sending 0 requests all queued health data from the DLS + final short LENGTH_HEALTH_SYNC = 5; + ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + LENGTH_HEALTH_SYNC); + buf.order(ByteOrder.BIG_ENDIAN); + buf.putShort(LENGTH_HEALTH_SYNC); + buf.putShort(ENDPOINT_HEALTH_SYNC); + buf.put(HEALTH_SYNC_CMD_SYNC); + + // Request all queued data (0 = send everything in DLS queue) + buf.order(ByteOrder.LITTLE_ENDIAN); + buf.putInt(0); + return buf.array(); + } + byte[] encodeRequestLogDump(int generation, int cookie) { final short LENGTH_REQUEST_LOGDUMP = 5; ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + LENGTH_REQUEST_LOGDUMP); @@ -2253,6 +2278,7 @@ public class PebbleProtocol extends GBDeviceProtocol { devEvtsDataLogging = new GBDeviceEvent[]{dataLogging, null}; } if (datalogSession.uuid.equals(UUID_ZERO) && (datalogSession.tag == 81 || datalogSession.tag == 83 || datalogSession.tag == 84)) { + // Tag 81 = activity minute data, Tag 84 = activity sessions GB.signalActivityDataFinish(getDevice()); } mDatalogSessions.remove(id); @@ -2611,6 +2637,24 @@ public class PebbleProtocol extends GBDeviceProtocol { devEvts = new GBDeviceEvent[]{decodeAudioStream(buf)}; // LOG.debug("AUDIOSTREAM DATA: " + GB.hexdump(responseData, 4, length)); break; + case ENDPOINT_HEALTH_SYNC: + pebbleCmd = buf.get(); + if (pebbleCmd == HEALTH_SYNC_CMD_ACK) { + // ACK response format: cmd (0x11) + ack_nack (0x01=ok, 0x02=fail) + byte ackNack = buf.get(); + if (ackNack == 0x01) { + // Success: actual data will arrive via DATALOG (tags 81/83/84), which + // already calls signalActivityDataFinish. Leave the spinner running. + LOG.info("Health sync ACK received (success), data will arrive via data logging"); + } else { + // Failure: no DATALOG data is coming, so dismiss the spinner now. + LOG.warn("Health sync ACK received but watch reported failure (ack_nack={}), signalling finish", ackNack); + GB.signalActivityDataFinish(getDevice()); + } + } else { + LOG.warn("Unknown health sync response: 0x{}", Integer.toHexString(pebbleCmd & 0xff)); + } + break; default: break; }