Sukrara (alpha)

08 — Glucose on a Garmin watch without Juggluco

2026-10-16 · investigation report · Pixel 10 Pro (Android 17), Garmin Instinct 3, Garmin Connect 5.30.1, Libre 3 Plus

Goal

Feed the Garmin Connect IQ glucose apps the user already runs with our own phone app and no other CGM app (no Juggluco, xDrip+ or GlucoDataHandler). The three targets:

Building our own watchface, app and datafield is the next spike.

Result

All three work with GlucoBridge alone. GlucoBridge is the direct flavor from 02: it reads the sensor itself and now also serves an xDrip+-compatible web service on 127.0.0.1:17580. Juggluco was force-stopped for the whole test (00:44–01:11). No Connect IQ settings were changed: the watch apps stay in their “Juggluco” mode, and our server answers the way Juggluco’s does.

Target Seen on the watch
CGM+ BG app (glance + app) Current value (“74 →, now”) and graph. The 1 h graph showed the gap from the test restarts, then filled in
Instinct3 N - PRO face Value via CGM+ GB’s complication (the face itself makes no network requests)
CGM+ BG Datafield Current value during an activity
CGM Connect (bruder_jakob), also installed Current value (“73 →”), and sensor age via sensor_status
CGM Gauge Widget (Roboleo), unused glance Made no request at all; its data was ~279 days old, so it wasn’t working with Juggluco either. Out of scope

How it works

Libre 3 ──BLE──▶ GlucoBridge (phone) ── 127.0.0.1:17580 HTTP ◀── Garmin Connect app ◀──BLE── watch app
                 serves /sgv.json            (makeWebRequest proxy: the watch asks, the phone app fetches)

A Connect IQ app on the watch can’t open sockets. It calls Communications.makeWebRequest, and the Garmin Connect app on the phone performs the HTTP request and returns the JSON over Bluetooth. Garmin Connect only allows plain HTTP to 127.0.0.1/localhost; anything else must be HTTPS. That is why the whole CGM-on-Garmin ecosystem converged on xDrip+’s “local web service” on port 17580. Juggluco, GlucoDataHandler and AAPS (port 28891) all copy it. None of the three targets uses Connect IQ phone-app messaging: the watch always pulls.

So “no other app” means: our app must own port 17580 and speak that dialect. Garmin Connect itself stays; it is the only bridge to the watch.

What the watch asks for

Every request was logged by the server (evidence). Three fixed patterns repeat every 5 min, a few seconds apart, all with user-agent: Mozilla/5.0 ( compatible ) and x-app-ver: 11657 (Garmin Connect’s build, not the watch app’s):

Request Content-Type sent Likely app Asks for
GET /sgv.json?count=72&brief_mode=true&interval=300 form-urlencoded CGM+ GB (feeds the face and datafield through its complications) 6 h at 5 min
GET /sgv.json?all_data=Y&count=72.0&sensor=Y&interval=600&brief_mode=true application/json CGM Connect 12 h at 10 min + sensor age
GET /sgv.json?all_data=Y&count=12&sensor=Y&interval=300&brief_mode=true application/json CGM Connect 1 h at 5 min + sensor age

The app attribution comes from the documented parameters and the polling cadence. Garmin Connect doesn’t log which app made a request.

What the server answers

XdripServer.kt, ~250 lines, no dependencies. It copies Juggluco’s output field for field (captured samples):

[{"date":1792102244000,"sgv":75,"delta":-0.250,"direction":"Flat","noise":1,"units_hint":"mgdl","sensor_status":"03-10-26 18:03 (12d 7h)"},
{"date":1792101649000,"sgv":72,"delta":-1.300,"direction":"Flat","noise":1}, …]

Findings worth keeping

  1. The only integration surface is HTTP on 127.0.0.1:17580 through Garmin Connect. It’s simple and robust, but the watch polls every 5 min, so the watch lags the 1-minute sensor by up to 5 min (plus the apps’ own offsets). Push needs our own watch app (next spike).
  2. Copy Juggluco’s dialect, not xDrip’s. The user’s apps are set to “Juggluco” and send Juggluco-only parameters (interval). An xDrip-shaped server would ignore them and still mostly work, but would return the wrong spacing or count.
  3. Juggluco’s backfilled entries have "direction":"" (seen right after it reconnected). Ours always carry a direction or "NONE".
  4. Port 17580 is a shared resource. Other local apps poll it too. Our own GlucoStatus (05) and CGM Nostr (03) use Juggluco’s /api/v1/entries.json?interval=0 and its SSE stream /x/live. Our server returns 404 for /x/live, so GlucoStatus fell back to polling every 5 s and CGM Nostr reported “Juggluco unreachable”. The real app should serve all of its own consumers, or implement /x/live.
  5. Garmin Connect is the single point of failure. It must stay alive and unrestricted in the background. CGM Gauge’s developer reports Garmin Connect blocking HTTP after a while (since July 2025), fixed by a sync in Garmin Connect. We didn’t see it in 30 min.

For the next spike (own watchface / app / datafield)

Open questions

Reproducing

cd spikes/glucobridge-android
export JAVA_HOME=$(brew --prefix openjdk@17)/libexec/openjdk.jdk/Contents/Home PATH="$HOME/.local/bin:$PATH"
./gradlew testDirectDebugUnitTest assembleDirectDebug        # XdripServerTest replays the watch's queries
adb install -r -g app/build/outputs/apk/direct/debug/app-direct-debug.apk
adb shell am force-stop tk.glucodata                         # frees the sensor *and* port 17580
adb shell am start -n dev.spike.glucobridge/.MainActivity
adb logcat -s GlucoBridge.Xdrip                              # one line per watch request
adb shell run-as dev.spike.glucobridge cat files/xdrip-requests.log

Watch side: CGM+ BG app → “CGM System” = Juggluco (or xDrip+), “Allow Access from Other Apps” on; face field = “CGM+ GB”. To go back: relaunch Juggluco, then reinstall the juggluco flavor if you used it before.

Sources: watchface.io CGM docs (sources, settings, developer), CGM Connect docs, xDrip local web service, Juggluco watchserver.cpp, AAPS GarminPlugin, Garmin Communications API.