2026-10-15 · investigation report · follows 02 — direct sensor
The doctor reads LibreView, so whatever replaces Juggluco must keep feeding it: glucose, and the insulin / carbs / notes the user logs. Juggluco does this today. Questions:
| Question | Answer |
|---|---|
| Phone → LibreView | Yes, proven. GlucoBridge uploads treatments and (direct flavor) sensor glucose; both appear in LibreView’s export under the spike device. |
| Mac → LibreView | Yes, proven. GlucoseBar uploads treatments with the same client logic, byte-identical payload. |
| Sync phone ↔ Mac | Yes, proven over the existing BLE link: either direction in ~1–2 s, edits/deletes on both sides, and changes made on both sides while disconnected all converge. |
| No duplicates in LibreView | Yes, proven with LibreView’s CSV export: the same entry posted by phone, Mac and a test tool is one row; edits replace, deletes remove. |
| Nostr-ready | By design. A treatment is one JSON object with a stable id and a last-writer-wins version — a parameterized-replaceable Nostr event as-is. Not implemented here. |
┌──────── treatments (BLE GATT, JSON) ────────┐
Libre 3 ──BLE──▶ GlucoBridge (Android) GlucoseBar (Mac)
│ glucose + treatments │ treatments
└────────────▶ LibreView ◀──────────────────────┘
one LibreView "device": same DeviceId, same record numbers
Juggluco impersonates Abbott’s FreeStyle Libre 3 Android app: it fetches Abbott’s public app config (API base
URL + API key), logs in with the patient’s email/password and a per-install DeviceId, then POSTs one JSON
document per upload — device settings plus currentGlucoseEntries, scheduledContinuousGlucoseEntries (5-min
history), insulinEntries (rapid/long units), foodEntries (grams), genericEntries (notes, sensor start).
Every entry carries a recordNumber; an edit or a delete ("action":"deleted") is the same record sent again.
The exact bytes are in spikes/libreview/SPEC.md; lv.py there is an executable
reference that generates a golden fixture both apps must reproduce byte for byte (they do).
Key design decisions:
DeviceId, and LibreView lists each
DeviceId as a separate device. The phone generates the id and hands it to the Mac over BLE, so phone, Mac
(and the lv.py test tool) are one device. That also makes the spike removable in one click (LibreView →
My devices).recordNumber = SHA-256("id:version")[0..5] << 8 | type;
glucose uses Juggluco’s scheme (sensor serial hash | lifeCount), checked against Juggluco’s own C++. Either app
uploading the same version produces the same record, so re-uploads are harmless. LibreView keeps the first
value of a record number, so an edit is “delete the uploaded version + add the new one” (as Juggluco does).{id, kind, amount, time, note, version, deleted, origin, uploaded-version};
merge = newer version wins, upload watermark only moves forward. The phone numbers every accepted change, the Mac
follows that sequence with a cursor — so sync never depends on the two clocks agreeing.| Time | Step | Outcome |
|---|---|---|
| 21:16–21:25 | BLE sync, no LibreView: phone→Mac, Mac→phone, cross edit/delete, edits on both sides while disconnected | all converged (Mac cursor = phone seq, nothing unacked) |
| 23:01 | login with SetDevice:false |
status 20 wrongDeviceForUser — the account was bound to Juggluco’s device |
| 23:04 | phone: login with SetDevice:true (Juggluco’s upload switched off first), upload 2 own entries |
status 0 |
| 23:09 | phone uploads the Mac’s 2 entries after the 2-min grace (one is a delete of a never-uploaded row) | status 0 |
| 23:11 | Mac logs in (SetDevice:false now succeeds — same DeviceId), uploads its own entry |
status 0; phone’s token kept working |
| 23:12 | edit-after-upload (Mac edits a phone entry) — uploaded by both Mac and phone 3 s apart | status 0 ×2 |
| 23:12 | delete-after-upload (phone deletes a Mac entry) | status 0 |
| 23:12 | deliberate duplicate: lv.py re-posts two unchanged entries as the same device |
status 0 |
| 23:13–23:26 | Juggluco force-stopped, direct flavor holds the sensor, glucose upload on | only 1-minute “current” readings went up — LibreView doesn’t keep those as rows |
| 23:22 | CSV #1 | dedupe ✅, delete ✅, edit ignored ❌ (0.5 U edited to 0.7 U still showed 0.5) → record number per version |
| 23:30–23:34 | direct flavor again, uploading the sensor’s backfilled 5-min history from 22:45 (what Juggluco hadn’t uploaded) | status 0 |
| 23:35 | edit test (1.0 → 1.5 U, edited on the Mac) and delete test (5 g, deleted on the phone), new scheme | status 0 |
| 23:49 | CSV #2 | edit ✅ (one row, 1.5 U), delete ✅, glucose ✅ (6 rows 22:49–23:14, continuing Juggluco’s 22:44) |
LibreView’s upload API answers status 0 even when it later discards data (ingestion is asynchronous), so success
codes prove nothing about duplicates. The only raw read-back is the web app’s Download glucose data CSV (behind
a captcha, so it’s a manual step): it has one row per stored record with Device, Serial Number, record type and
insulin/carb/note columns.
Spike-device rows in the final export (device = “FreeStyle Libre 3”, serial = the shared DeviceId):
| What was sent | Rows in LibreView |
|---|---|
note: uploaded by the phone, re-posted unchanged by lv.py |
1 |
0.3 U: uploaded by the Mac, re-posted unchanged by lv.py |
1 |
| 0.5 U edited to 0.7 U under the same record number, uploaded by Mac and phone | 1 — still 0.5 (edit ignored) |
| 1.0 U edited to 1.5 U with the per-version scheme | 1 — 1.5 |
| 1 U long and 5 g carbs, deleted after upload; 12 g deleted before upload | 0 |
| sensor glucose, 5-min history 22:49–23:14 | 6, values identical to the sensor’s |
| sensor-start record | 1 empty “note” row — Juggluco’s device has the same |
| 1-minute “current” readings (uploaded every minute 23:14–23:26) | 0 — they feed LibreLinkUp’s live view, not the stored history |
So: re-uploading is idempotent per (device, record number), deletes work, edits need a new record number, and the glucose that reports are built from is the 5-minute history (which trails the live reading by ~16 min).
Dalvik/2.1.0 … User-Agent the phone sends anyway.SetDevice:true, which logs the
previous one (Juggluco) out of LibreView. Juggluco then re-takes it on its next upload, so two uploaders with
different DeviceIds fight. Exactly one app may own the LibreView upload.PUT nisperson (active-sensor registration) after login answered status 20 invalid session; it
succeeded on the next cycle a minute later.swift run binary reads UserDefaults domain GlucoseBar, not dev.spike.GlucoseBar.Android/16 headers from the Mac) is what Juggluco does but is against
Abbott’s terms.03 streams glucose from one uploader to any number of encrypted followers and lists treatments as out of scope. They fit without changing anything 03 built — design in SPEC §5:
d,
content = this log’s JSON encrypted to 03’s read key. Volume is a few events per day.treatments; it could serve this log instead (rapid → Correction Bolus,
carbs → Carb Correction, note → Note), which is what xDrip+, Nightguard and LoopFollow read.Code check (2026-10-16): this investigation’s uncommitted changes replay onto origin/main (with 03 and 05) with
one conflict — main.swift, where both added wiring; resolution = keep both, and ble?.treatmentSync because 03
made the BLE transport optional. Mac app builds with 03’s Nostr SDK and passes the fixture + merge tests; Android
juggluco flavor builds and passes its unit tests.
What building it would take: the phone’s TreatmentStore.onChange becomes a listener list (BLE + Nostr), 03’s
NostrPublisher (moving into GlucoBridge anyway) gains a treatment publish + subscribe; on the Mac, 03’s
read-only NostrSource gains a device key (Keychain) and event publishing with 03’s pacing rules.
Done 2026-10-16: spike uploads switched off in both apps, spike device removed in LibreView, Juggluco’s upload back on. Verified with a final export: 0 rows from the spike device, and Juggluco’s own history continuous (every 5 min, no gaps, no duplicates) through the spike window — it re-uploaded its backlog after being re-enabled.
lv.py cleanup.