Deliver Android phone notifications to a Pixel Watch Gen-1 and let the user dismiss/reply, over direct BLE only — no Google Play Services, no Wearable Data Layer, no companion app.
Android phone (unmodified Gadgetbridge) Pixel Watch (PixelBridge app)
┌───────────────────────────────────────────┐ ┌────────────────────────────────────────┐
│ NotificationListenerService │ │ MainActivity (Wear Compose UI) │
│ captures + filters notifications │ │ renders the notification list │
│ │ │ │ ▲ │
│ ▼ │ │ │ │
│ Bangle.js DeviceSupport (BLE CENTRAL) │ │ DieselBridgeService (connectedDevice FGS)│
│ connects to the advertising watch │ │ owns the BLE stack │
│ │ │ │ │ │
└────────────────┼───────────────────────────┘ └────────────────┼───────────────────────┘
│ BLE GATT · Nordic UART Service (NUS) │
│ │
phone→watch RX ──┤ GB({"t":"notify","title":…,"body":…})\n ──────────▶│ NusGattServer (GATT SERVER)
watch→phone TX ◀─┤ {"t":"notify","id":…,"n":"DISMISS"}\n ──────────┤ RX write / TX notify
│ │ NusAdvertiser advertises NUS
└───────────────────────────────────────────────────────┘ name "Bangle.js PixelBridge"
Why this topology:
- It's the proven AsteroidOS model (watch = advertising GATT server, phone = central that writes).
- It co-locates notification capture + connection management + the foreground service on the phone, where background execution and battery-optimization exemptions are mature — the watch's role stays simple and low-power (advertise + serve GATT).
- By speaking the Bangle.js JSON-over-NUS dialect and advertising a
Bangle.js…name, an unmodified Gadgetbridge drives it as a Bangle.js device — zero new phone-side code (verified against Gadgetbridge's published Bangle.js protocol) and no AGPL obligation.
| Component | Where | Role | Tech |
|---|---|---|---|
| PixelBridge watch app | this repo (:watch) |
BLE peripheral: NUS GATT server + advertiser; parse GB({...}); render UI; send action back-channel; foreground service |
Kotlin, Jetpack Compose for Wear OS, android.bluetooth, no native libs |
| Gadgetbridge (unmodified) | phone (F-Droid) | BLE central + notification source; NotificationListenerService; maps actions → cancelNotification / RemoteInput |
Existing Bangle.js Coordinator/Support (used as-is — never forked or modified) |
- Phone app posts a notification → Gadgetbridge
NotificationListener.onNotificationPostedbuilds aNotificationSpec→ routed to the Bangle.jsDeviceSupport.onNotification. - Gadgetbridge writes
GB({"t":"notify","id":…,"src":…,"title":…,"body":…})\nto the watch's RX. - Watch
NusGattServerreassembles bytes on\n, strips the0x10DLE, parses JSON, shows it. - User acts on the watch → watch writes
{"t":"notify","id":…,"n":"DISMISS"}(or"REPLY",reply) to TX (notify) → Gadgetbridge maps DISMISS →NotificationListenerService.cancelNotification, REPLY → the notification'sNotification.Action+RemoteInput.
Full wire details: ble-protocol.md. Gadgetbridge internals: gadgetbridge-integration.md.
Long-lived BLE dies when the process is killed, and Doze defers BLE even for foreground services. The layered mitigation (applies to whichever device is central — here, the phone):
- Foreground service,
foregroundServiceType="connectedDevice"+FOREGROUND_SERVICE_CONNECTED_DEVICE(mandatory API 34+). The watch app also runs one (DieselBridgeService) for its GATT server. - Battery-optimization exemption (
REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) — a foreground service alone does not beat Doze. CompanionDeviceManagerassociation — grantsREQUEST_COMPANION_RUN_IN_BACKGROUNDand, on Android 15, restores access to "sensitive" notifications (OTP codes) otherwise hidden fromNotificationListenerService. This is how Gadgetbridge already does it.- Serialize all GATT operations through a single queue (Gadgetbridge's
BtLEQueue; on the watch our server is inherently single-threaded via its callback) — unordered ops causestatus 133. - Reconnection: bond (
createBond, waitBOND_BONDED); directconnectGatt(autoConnect=false)for speed,autoConnect=trueas passive fallback; alwaysclose()the oldBluetoothGattfirst and inspect thestatusarg inonConnectionStateChange.