2026-10-15 · investigation report · code in spikes/
Replace the current setup — an xbar plugin polling LibreLinkUp over the internet — with a macOS menubar item showing the glucose number and a precise trend arrow, with data that never leaves the user’s devices.
Questions:
| Sensor | FreeStyle Libre 3 Plus (15-day wear), started 2026-10-03 |
| Phone | Pixel 10 Pro, Android 17 (SDK 37), rooted (KernelSU Next); Juggluco 11.4.1; GlucoDataHandler |
| Mac | macOS 27.0.1, Apple Silicon; Swift 6.4 (Command Line Tools only, no Xcode) |
| Units | mg/dL; personal thresholds low 65 / slightly high 145 / high 185 |
| Path | Works? | Latency | Notes |
|---|---|---|---|
| Mac ↔ sensor directly (BLE) | ✅ proven | — | Phone must release the sensor; per-sensor pairing data from the phone; NFC still needed to start a sensor |
Juggluco web server → Mac over USB (adb forward) |
✅ | instant | Not practical (cable) |
| Juggluco web server → Mac over Wi-Fi | ✅ | ~0.3 s | Needs a shared network and Juggluco “Local only” off |
| Juggluco → GlucoBridge (phone) → Mac over BLE | ✅ | ~0.2–1 s | Works with Mac offline / phone on mobile data; 17/17 readings with phone unplugged + screen off |
| Bluetooth PAN tethering | ❌ | — | macOS 27 has no Bluetooth PAN support |
| Juggluco’s built-in “BLE mirror” | ⚠️ not built | — | Feasible but large, undocumented, moving target |
Yes — but not as the everyday path.
spikes/ble-scan) sees no Libre: the sensor stops
advertising once a device is connected to it.17310/17311: 182, 182 — backfilled rows
carry no rate). Live data resumed with no loss.Constraints that rule it out as the main path:
Possible future use: an emergency fallback when the phone is gone (dead battery, left at home).
Juggluco has a built-in, Nightscout-compatible web server (Settings → Web server). Facts confirmed from its source and live:
adb forward tcp:17580 tcp:17580) it can stay on.api-secret: <sha1 hex of secret>, or ?token=<secret>.GET /api/v1/entries.json?count=N — newest first. Default output is thinned to ~5 min; add interval=0 to
get every 1-minute Libre 3 value.delta is the sensor’s rate × 5, not a difference between readings → rate = delta / 5 (mg/dL/min).
A missing rate is reported as delta: 0 with direction: "".GET /x/live — Server-Sent Events: one glucose event per new reading, pushed as soon as the phone gets it,
with glucoseMgDl and rateMgDlPerMinute. Heartbeat every 20 s./x/stream — TSV with the per-minute Rate column (handy for analysis).Rate precision: the sensor reports the rate with 0.01 mg/dL/min resolution; in a 5 h sample 98 % of values were
non-integer (short runs of exact 0 occur). Enough to draw a continuous arrow.
Wi-Fi results (spikes/menubar, “auto” mode):
:17580 and checks
/api/v1/status.json → found the phone in ~1 s on a /24.entries.json?count=180&interval=0, then live via /x/live: ~0.3 s from the phone receiving a
reading to the Mac showing it..app needs the Local Network permission for this.| Option | Mac offline | Mac Wi-Fi + phone mobile data | Verdict |
|---|---|---|---|
| Phone hotspot | ✓ (Mac joins it) | ✗ (one Wi-Fi radio) | Partial |
| Bluetooth PAN tethering | — | — | macOS 27 dropped it (no “Bluetooth PAN” port, no driver) |
| Tailscale / VPN | ✗ | ✓ | Needs internet |
| Juggluco “BLE mirror” | ✓ | ✓ | Mac would have to speak Juggluco’s mirror sync protocol (handshake + session crypto + acking its internal file-sync stream). ~1–1.4k lines of Swift against an undocumented protocol that changed several times in 2026. Rejected. |
| Companion app + BLE GATT | ✓ | ✓ | Chosen |
spikes/glucobridge-android, Kotlin)A foreground service on the phone that:
127.0.0.1:17580 (so “Local only” can stay on), and| Characteristic | UUID | Properties | Value (little-endian) |
|---|---|---|---|
| service | 6c1f0001-5b8a-4d8e-9a3c-676c75636f42 |
||
| READING | 6c1f0002-… |
read, notify | u32 epoch s ‖ u16 mg/dL ‖ i16 rate×100 (0x8000 = unknown) |
| HISTORY | 6c1f0003-… |
read (long) | u32 newest epoch s ‖ u16 mg/dL per minute, newest first (180 slots, 0 = gap) |
| Test | Result |
|---|---|
BLE only (GlucoseBar -lan NO) |
Found GlucoBridge in 3 s; every reading ~0.3 s after the phone |
| Phone Wi-Fi off (phone on mobile data, Mac on home Wi-Fi) | Wi-Fi transport dropped; BLE delivered every minute (19:03–19:05, ≤ 1.2 s) |
| Phone unplugged + screen off, 19:08–19:25 | BLE 17/17 readings, 0 missed, worst 0.2 s, never disconnected |
| Both transports at once | Same reading arrives twice (BLE ~1 s earlier); the app merges them |
Raw log: evidence/01-transport-tests.log. (Wi-Fi readings stop at 19:12 because “Local only” was switched back on — not a failure.)
Not yet validated: deep Doze (phone unplugged and still > 30–60 min), Mac sleep/wake, Mac with Wi-Fi truly off
(simulated with -lan NO), range, long-term reconnect behaviour.
Gotchas:
BluetoothGattServerCallback.onConnectionStateChange also fires for the phone’s own outgoing links (the
sensor, a watch), so “connected centrals” is noisy — count subscriptions instead.

brew install --cask android-platform-tools.curl -fsSL https://dl.google.com/android/cli/latest/darwin_arm64/install.sh | bash,
then android init. Used for android create empty-activity, android run --apks … --install-options=-g,
android screen capture, android docs search. It installs SDK packages on demand into ~/Library/Android/sdk.brew install openjdk@17, then
export JAVA_HOME=$(brew --prefix openjdk@17)/libexec/openjdk.jdk/Contents/Home.swift build); bundle.sh wraps GlucoseBar into an ad-hoc-signed
.app (LSUIElement, Bluetooth + Local Network usage strings).U+202F) — remember it when parsing logs.