This fork delivers insulin. It contains a pump driver and a dosing algorithm that are not part of upstream AndroidAPS, have not been through the AAPS review process, and are not clinically validated. They were built and tested by one person, on one pump — not with anything like the multi-user testing an insulin-dosing system needs.
Not affiliated with or endorsed by the AndroidAPS project, the Nightscout Foundation, Ypsomed, or CamDiab.
If you want a loop that works and is supported, use upstream AndroidAPS. This fork exists to explore three things upstream does not do, and is shared in case the work is useful to someone — not as a product.
Forked from nightscout/AndroidAPS at 43cc754 (2026-06-04). Seven workstreams:
| # | Area | What it is | Status |
|---|---|---|---|
| 1 | YpsoPump driver | Loops a pump AAPS lists as "Not Loopable" | Alpha, dosing-capable |
| 2 | HovorkaMPC | A nonlinear-MPC APS algorithm alongside oref1 | Experimental, runs live |
| 3 | Infusion-site handling | Treats a fresh cannula as a distinct physiological state, across the algorithm, wizard and careportal | Running |
| 4 | Compose UI redesign | Full-app Material 3 rewrite of the interface, plus skins loadable from a file | Running |
| 5 | Slim loop build | Strips the app to the one pump and one algorithm it runs, and lets Android AOT-compile it | Running |
| 6 | Delivery the pump cannot make | Stopped pump, empty cartridge: say so, refuse the dose, and never book insulin that did not go in | Running |
| 7 | Native Libre 3 / 3+ CGM | Talks to the sensor directly over BLE — no Juggluco, no xDrip in the glucose path | Live on hardware |
Plus a number of smaller changes — Nightscout over a private network, wizard fields the redesign had dropped, and pump-driver reliability fixes.
All of it lives on main — the workstreams are separable as ideas, not as code (the fresh-site
work is algorithm and wizard; the slim build is gradle config and workers and UI), so a single
branch is the honest representation.
| Branch | What it is |
|---|---|
main |
Everything above. This is the build that runs on hardware. |
master |
An untouched mirror of nightscout/AndroidAPS at the fork point, kept so upstream can be merged and diffed cleanly. No work of this fork's is on it. |
| others | Short-lived feature branches, merged and deleted. |
An earlier layout split the algorithm and the UI across two long-lived branches. It was retired in
August 2026 after the loop branch drifted to 0 commits ahead and 74 behind, six commit subjects were
applied twice, and a repair commit had to be written to carry algorithm fixes back across. Its tip
survives as the tag loop-only-3.4.2.3.
pump/ypsopump/ — full README: protocol, safety notes, credits
AndroidAPS officially lists the Ypsomed mylife YpsoPump as "Not Loopable — Ypso added very heavy 3rd-party encryption." This driver is a proof that it can be.
- Full BLE transport: XChaCha20-Poly1305, multiframe assembly, persistent connection model
- Read path (status, basal, boluses, events) and write path (TBR, bolus, cancel)
- Separate read/write counter model, with self-healing resync after a dropped write-ack
- Recovery for the pump's real failure modes:
0x82bad param,0x86TBR-already-active,0x8Amalformed,0x8Bcounter-behind - Confirm-by-read bolus delivery, and a pump-side suspend reflected as a 0-rate TBR
- Reservoir surveillance, and a refusal to book insulin the pump cannot have delivered — see delivery the pump cannot make
This stands entirely on other people's reverse-engineering, principally SandraK82 / ypsopump-research and vicktor / ypsomed-pump. Proper credit is in the driver README.
Hard requirement: the pump's per-pairing session key, which you must extract yourself from the genuine app on a rooted device. There is no supported path to obtain it — no key, no function. Pairing a pump from scratch is not possible without the vendor backend, as the pairing challenge is validated server-side.
plugins/aps/src/main/kotlin/app/aaps/plugins/aps/hovorka/ ·
validation harness: hovorka-mpc/
An alternative APS algorithm, selectable alongside oref1. Where oref1 is a rule-based system refined over years of community use, this is a receding-horizon nonlinear model-predictive controller built on the published Hovorka 2004 glucose-insulin model.
Clean-room — reimplemented from the published model plus independent reverse-engineering of CamAPS FX. Not a port of anyone's binary.
- State estimation — an Extended Kalman Filter replays the last 6 hours of glucose, insulin and carbs every tick to estimate the current physiological state. Stateless by design: there is no persisted filter state that can silently corrupt.
- Control — optimises a piecewise-constant basal sequence over a 3-hour horizon against an exponential reference trajectory, and enacts its first step.
- Output law — graduated basal floor, current-glucose safety damper, deadband, hard hypo suspend.
| Feature | What it does |
|---|---|
| Adaptive TDD | Scales the profile basal curve by a gain learned from total daily dose (see below) |
| IMM Kalman bank | Parallel submodels identifying the current carb-absorption regime |
| SMB microbolus | Part of the short-horizon correction as a bolus, behind five independent gates |
| Meal detection | Bayesian inference of unannounced carbs from the innovation sequence |
| Re-identification | Daily re-fit of insulin sensitivity / EGP / absorption from your own logs |
| Time-of-day carb absorption | Scales absorption by meal time. Built, then rejected when fitted against real data — left in place, off, and not recommended |
Worth describing in detail, because it is the clearest example of what this fork tries to do: build the mechanism, then hold it against the user's own recorded history rather than against a simulator.
The first implementation followed the decoded CamAPS shape only loosely, and a 60-day replay of real history found four deviations that compounded into one failure — the operating point walked from 97% of the titrated profile to 54% over three weeks, which removed 63% of the dawn basal and made the 30-min forecast measurably worse. What went wrong, in descending order of damage:
- A binary daily hypo flag applied as a fixed ×0.90. On this user's data
tbrFrac > 0.01 || min < 3.5fires on 42 of 58 days. A fixed multiplicative cut three days in four is a ratchet, not a safety response — and it sat first in the branch, so a day averaging 9.1 mmol/L with one compression low was cut 10% and the "persistently high" arm never ran. The response is now continuous in time-below-range with a deadband at the consensus <4% target, and the two arms add instead of one vetoing the other. - It learned from basal alone. Basal is 30% of this user's insulin and correlates −0.01 with their daily total, so the layer was learning from the loop's own output — a feedback loop with no external reference. The ledger is now total delivered insulin, basal and boluses, which is what "TDD" means.
- It replaced the profile instead of scaling it, deleting the circadian basal shape for all 24 hours.
It now applies a dimensionless gain to
profile.getBasal(now), so the titrated shape survives. - It compared a block against daily means, which reads low by construction, and cached on a UTC day index, so the "daily" value rolled over at local noon — the number governing a dawn rise had been computed at midday the day before.
Two findings from the rebuild are worth carrying to any similar layer. The window length was tuned on real data rather than assumed: the performance term, not the dose window, was the dominant source of churn, and reading glucose from a single day rather than the window costs an 11× increase in it. And because the per-update clamp is asymmetric (−20% down against +12% up), estimator noise does not average out — it ratchets down; reducing noise removed a systematic 6% bias, not just variance.
An EGP/disturbance state in the Kalman filter was also built and A/B'd as a candidate fix for the same symptom, and rejected: it improved the dawn dose barely, degraded forecast error, and spent most of its time pinned against its own bound. The replay that produced that negative result is kept in the project's analysis tree (outside this repo) so it is not quietly re-proposed.
TddAdapterV2 carries the full derivation in its KDoc, and TddAdapterV2Test pins each property as a
contract — written against realistic days from the recorded history, because the defect was invisible to
round numbers.
Each of these exists because the failure it prevents happened first:
- Hard hypo suspend — 0 U/hr at or below 3.9 mmol/L on the raw sensor value, overriding the model
- Descent guard — tapers basal toward zero on the stricter of two arms, a mass-balance projection and a raw-CGM taper, so no forecast can license dosing into an observed fall
- Current-glucose damper — scales the above-nominal portion of the dose by how far glucose sits above target, so a glucose climbing back out of a low can never be met with near-maximum basal. It is released for one case, and only when both conditions hold: glucose rising and no reading below 4.5 mmol/L in the last four hours. Below target the damper is identically zero, which pinned the controller at nominal through the first 30 minutes of a dawn rise even after it had correctly predicted the climb — 82% of that morning's shortfall. The damper still guards the case it was built for, since a post-hypo recovery and a dawn rise are indistinguishable by level and nearly so by direction; only the preceding low separates them. Measured over 60 days, rising below target: 21% reach <4.0 mmol/L within two hours with a preceding low, 8% without
- IOB divergence detector — when glucose rises while the model insists it is falling, booked IOB is discounted, so a failed infusion site cannot silence the correction path at a high
- High-glucose correction floor — ramps basal while the mass-balance eventual stays above target
- SMB gates — fed-state, rising, post-hypo lockout, rolling stack cap, maxIOB/maxBolus ceilings
- SITE-GUARD — a fresh cannula suppresses SMB and stands the divergence detector down; see section 3
Validated in-silico only, plus replay against recorded logs. Not clinically validated. Note that several changes which passed the virtual-patient cohort were later rejected when fitted against real data — where that happened, the code comments record what was tried and why it was dropped.
plugins/aps/.../hovorka/ (SITE-GUARD) · ui/.../dialogs/ (wizard advisory, back-dated recording)
The largest single effect found in this user's own data, and it is not an algorithm parameter — it is the cannula. Across 17 days: for roughly six hours after a site change, glucose averaged 13.5 mmol/L against 7.6 in the same clock hours on non-change days, while taking four times the bolus insulin (2.15 vs 0.51 U/h). Six changes out of six, paired sign test p=0.016. The cleanest case involved zero carbs, ~13 U of bolus over six hours, and still averaged 11.8.
The important part is what that implies. A normal loop response there is not merely ineffective, it is actively harmful: the insulin is not absorbed, it accumulates, and when the site opens it all arrives at once. The change where the loop pushed hardest ended at 2.2 mmol/L.
So a fresh site is handled as its own state, in three places:
- SITE-GUARD — for the first 24 hours (configurable), SMB is suppressed (irreversible dosing has no place in a window where the high is caused by insulin not arriving), and the IOB-divergence detector is stood down. That detector fires on exactly this signature — glucose rising while booked IOB says it should fall — and responds by discounting IOB and adding correction. It was written for a site that has failed; a fresh site is late, and the two need opposite responses. An earlier version also capped basal; that was removed as the wrong instrument on the wrong timescale. The guard fails open: with no cannula change ever recorded it stays off rather than clamping forever on an empty history.
- Fresh-site bolus advisory — insulin peaks about twice as slowly at a day-1 site (time-to-peak 110 min vs 56 min, Hildebrandt 1991), and a large single bolus compounds it. Splitting a dose is the user's call and the loop cannot do it for them, so the wizard carries the advisory: amber, informational, and only for boluses of 1.5 U or more, since a basal trickle is handled fine.
- Back-dated site recording — a cannula change is routinely made away from the phone. Prime/Fill used to stamp the current time with no way to correct it, so changes went unrecorded and were invisible to both the analysis and the guard. There is now a "changed N minutes ago" field with quick chips. It back-dates the therapy event only — the prime bolus is still delivered now, because insulin cannot be given retroactively and a fictional delivery time would corrupt IOB.
Armed on CANNULA_CHANGE, so a tubing- or reservoir-only fill does not trigger it.
core/compose/ (design system) + Compose screens throughout plugins/main/
A full-app rewrite of the interface in Jetpack Compose and Material 3, on a shared design-system module.
It reuses the existing dosing logic, constraints and protection paths underneath — every delivery still
goes through BolusWizard.confirmAndExecute and the same constraint chain.
Rewritten: Home (hero card, graph, actions), Bolus/Carb wizard, Loop control, Temp target, Profile switch, Actions & Careportal, Statistics, History timeline, Profile view and editor, Config Builder, preference screens, YpsoPump and Medtrum pump status (the live pump's tab was the last screen still rendering as stock AAPS), and around a dozen legacy dialogs.
The charts are drawn rather than plotted: the Home glucose graph and the Statistics "typical day" are
Compose Canvas code, not a charting library. That was not a purity exercise — it is what allows the
graph to share the app's colour tokens, so the band around target and the tint on the trace agree with
the hero BG by construction instead of by coincidence. The Home chart draws the same 5-minute bucketed
series the loop computes on, with the raw sensor scatter faint underneath, so a surprising dose can be
read off the picture the algorithm actually saw. Statistics shows a median with 25–75% and 10–90% bands
by hour of day — an AGP, the view a clinic reads — because "85% in range" hides whether a flat hour sits
at 4 or at 10.
The shared confirmation dialog went with them. OKDialog no longer builds a MaterialAlertDialog — it
renders the design system's alert surface — so roughly forty-six call sites across the app moved over
without being touched. That needed the module dependency between core:ui and core:compose reversed:
the design system never actually used core:ui, so that edge was dropped and core:ui now depends on
core:compose. Password and PIN prompts are deliberately still the old dialog: they carry autofill hints
and IME actions on the protection path, and a botched conversion there locks you out of your own app.
Not purely cosmetic, despite the above. Some inputs that affect dosing changed:
- Pre-bolus (carb time) — restored after the first pass of the redesign dropped it
- Extended carbs — an absorption-duration field, with a
carbDurationHoursparameter threaded through the wizard calculation - Record-only insulin entry — logs a bolus delivered by pump or pen into IOB without re-delivering it, reachable from the Home "+" menu
- Recent-insulin undo — the IOB tap removes a bolus the pump never delivered; see section 6
- Removing a mis-entered treatment — long-press any History row to select and delete. Records are invalidated rather than erased, so the audit trail and Nightscout sync still see them
- Fresh-site advisory — see section 3
- The second confirmation popup was removed; the hold-to-deliver control is the confirmation
Captured on the phone that runs the loop, so every number is live data rather than a mock-up.
The design is mmol/L-first, and the accent colour is reserved for "this is tappable" while greens, ambers and reds mean glucose or loop state and nothing else.
Every colour, the font and the corner radii resolve through one seam at runtime, so the look is data rather than something compiled in. Changing it needs no rebuild.
![]() |
![]() |
![]() |
| Light — the built-in light ground | Game Boy — one hue, square corners, 8-bit font | Kawaii Neon — neon rainbow, round corners, bubble font |
The two skin files pull the rules in opposite directions, which is why both are kept as tests: a single-hue palette makes the glucose bands hard to separate, while a saturated one makes text contrast hard. Neon is instructive about the second — bright colours only clear contrast against a dark ground, which is why the first attempt at this skin put them on white, failed, and came back muted. A skin whose background reads as light also flips the app's remaining XML screens to light, so the half that cannot be skinned does not fight the half that can.
Choosing one. Settings → General → Theme. The list is flat — Follow system, Light, Dark, Midnight, then one entry per installed skin file. It is deliberately not two settings. A palette picker plus a separate light/dark switch produces combinations that quietly do nothing (a dark-only palette set to "light"), and most skins are a single look anyway. Changing skin repaints immediately; changing light/dark still recreates the activities, because the app's remaining XML screens have no other way to follow.
Managing them. Settings → Skins. Import a .aapsskin bundle, export one to send to someone,
remove one, or export a starter template seeded from the palette currently on screen.
Writing one. A .aapsskin file is a zip holding skin.json and, optionally, a .ttf. Every
colour is optional and inherits from the default skin, so a skin says only what it cares about — this
is the entire Game Boy skin above:
{
"formatVersion": 1,
"id": "gameboy",
"label": "Game Boy",
"author": "alex",
"cornerRadius": 0,
"font": { "file": "dmg.ttf", "singleWeight": true, "scale": 0.6 },
"dark": {
"background": "#1B2300", "surface": "#222E00", "surface2": "#293800", "bar": "#161B00",
"textPrimary": "#C5DB7A", "textSecondary": "#B5CA6B", "textTertiary": "#7B8E3C",
"inRange": "#698023", "high": "#A4BA5B", "low": "#E8FD9A",
"veryHigh": "#7DA300", "veryLow": "#FAFFE2",
"accent": "#C5DB7A", "onAccent": "#1B2300"
}
}cornerRadius is one number for the whole shape language — 0 squares every corner including the
pills. The font above is Press Start 2P (SIL Open Font License); a bundle may carry its LICENSE or
OFL.txt alongside, which the importer keeps so the skin stays redistributable. A skin with one look declares palette instead of dark/light, and keeps it whichever mode is
set; anything it leaves out is inherited from whichever default ground its background resembles, so a
pale skin does not pick up dark surfaces. dark and light still exist for a skin that wants two. Colour names match the
tokens in core/compose/theme/Color.kt one for one. singleWeight stops the type scale asking a one-weight font for bolds it cannot
draw, which would otherwise be synthesised and smear the edges a pixel font exists to keep sharp, and
scale compensates for a face far wider per character than the one the layouts were drawn against.
Numeric readouts set their unit much smaller than the number, and shrink to fit rather than truncating. That is worth knowing when writing a skin: a pixel face is roughly twice the width per character, and without it "0.45 U/h" renders as "0.45" — a dose readout quietly losing its unit while still looking like a complete number.
A skin has to be legible before it is allowed on screen. This app decides insulin and the number on the hero is the number you act on, so a palette is checked on import and on every load — the rules can tighten in a later build, and a skin accepted under looser ones must not keep rendering. Text clears WCAG 4.5:1 on every surface it appears on, status colours clear 3:1, and the glucose bands must be at least ΔE 20 apart from each other. A rejected file says which token failed, what it measured and what it needed. Built-in skins go through the identical code path, so they cannot hold themselves to a lower bar than a file someone sends you.
That ΔE floor is perceptual distance, not a contrast ratio, because contrast answers the wrong question here: amber and green sit at a ratio of 1.09 while being nothing alike. It looks at first as though a single-hue palette cannot satisfy five bands at that distance and still be readable — the Game Boy skin above was nearly the reason to lower it — but that turns out to be an artefact of choosing grounds and inks first and fitting the bands into what is left. Solved together, one hue clears the same floor with room to spare.
Sharing is file-based: export a bundle and send it however you like. There is no skin repository yet.
Not yet skinnable: the app's remaining XML screens (the tab bar, the preference tree, the system
bars) follow the old AppTheme, so they stay dark-blue under any skin; and about thirty hard-coded
circular shapes — status dots, the "+" button — stay round whatever cornerRadius says. Spacing is
deliberately excluded: padding decides whether a dose stepper or a hold-to-confirm button can be hit,
which is not a knob to hand to a theme file.
loop build type in buildSrc/, plus removals across the module tree
Upstream ships every pump driver, every algorithm and every integration, because upstream serves everyone. This fork serves one pump and one algorithm, so it carries what it uses. Measured on device before and after:
| Before | After | |
|---|---|---|
| APK on disk | 189 MB | 67 MB |
| Dex files | 34 | 7 |
resources.arsc |
10.2 MB | 1.2 MB |
| Native ABIs | 7 | 1 (arm64-v8a) |
| Dexopt state | run-from-apk |
speed |
| Idle CPU, screen off | 4.72%* | 2.59% of a core |
* The before figure is a 56-hour mixed foreground/background average rather than a matched screen-off window, so treat that one pair as indicative. The protocol-matched measurement is 3.04% → 2.59% across the legacy-overview change alone, over identical 15-minute screen-off windows.
The important part is not the size. Android refuses to ahead-of-time compile a DEBUGGABLE package —
cmd package compile -m speed reports success and silently downgrades to verify, so a debug build runs
status=run-from-apk and JIT-compiles every dex file, forever.
The loop build type is the debug build with isDebuggable = false, signed with the same debug
keystore. That signing choice is deliberate and load-bearing: the signature still matches what is
installed, so adb install -r remains an update rather than a reinstall, /data survives, and the pump
driver keeps its session key. Give it a different signingConfig and every install becomes an uninstall.
./gradlew :app:assembleFullLoop
adb install -r app/build/outputs/apk/full/loop/app-full-loop.apk
adb shell cmd package compile -m speed -f info.nightscout.androidaps # not automatic after install
adb shell cmd package dump info.nightscout.androidaps | grep -A3 'Dexopt state' # want status=speedBuild assembleFullDebug instead when you need a debugger, and rebuild loop afterwards.
Twelve unused pump drivers and the RileyLink radio only Medtronic/Omnipod need (YpsoPump plus
Medtrum, virtual and pump:common are kept); Firebase Analytics,
Crashlytics and Remote Config (FabricPrivacy now logs to the AAPS log instead, so its call sites are
unchanged); LeakCanary; Wear support and fourteen bundled watchfaces; all locales but en; all ABIs but
arm64-v8a. material-icons-extended was 19.7 MB of dex to draw 33 icons — the 25 that only exist there
are vendored into core/compose as path data extracted from the library itself, so they render identically.
Only two touch behaviour, and neither is on the dosing path:
- Graph work is gated on UI visibility. Every CGM tick ran a 16-worker chain, of which 14 built
overview graph series by querying the database — with the screen off, and with the dosing decision
queued thirteenth. The chain now runs IOB/COB and the loop first, and prepares graph series only while
a screen is visible;
OverviewFragmentrebuilds them on resume. Hidden cycles run 5 workers in under a second against 17 taking several. - The legacy overview is gone, not hidden. For a while the Compose home sat as an opaque overlay
over the original layout, which was still inflated, still being fed ~120 values per refresh and still
redrawing its own GraphViews underneath.
overview_fragment.xmland the sixteen update functions that filled it have since been deleted; the fragment inflates a bareComposeView.
Plus: Nightscout upload/download failures back off and log one stack per distinct error rather than one per
record, and deviceStatus — write-only rows that existed to be uploaded — is kept for 7 days instead of 186.
pump/ypsopump/ (pre-flight, reservoir watch, recording rules) · ui/.../dialogs/compose/PumpReadyGate.kt
(the workflow) · plugins/main/.../overview/ (the alert, the pill, the undo)
Two failures that look nothing alike from the outside turn out to be the same bug: the app assumed the pump was delivering, and had no idea what to do when it was not.
A bolus into a stopped pump looked like it was working. The wizard did its maths, the progress dialog opened, and it sat at 0% until the driver's five-minute confirm window expired. The pump had refused the very first write; nothing in the app knew, because the driver polled for the delivery regardless.
A cartridge that ran dry produced no feedback at all. There is no reservoir alert anywhere in AndroidAPS — a pump that empties simply stops. Worse, the reservoir pill on the redesigned Home was drawn only when the level was above zero, so it vanished at exactly the moment it mattered: an empty cartridge rendered identically to a screen that had never shown one. The loop kept commanding basal and boluses into a pump that could not take them, and because an unconfirmed bolus is deliberately recorded as delivered (over-stating IOB is the safe side of that guess), it kept booking insulin that did not exist. The IOB the loop was reasoning against was wrong for hours.
So:
- Pre-flight before anything is queued. Every route that delivers — wizard, manual bolus, insulin, prime/fill — checks the pump first. Stopped or empty, and you get a sheet that says which, with Check again: it reconnects, re-reads the pump, and delivers the same dose if it comes back healthy, so nothing has to be re-entered. There is no BLE command to restart a YpsoPump — starting a pump is a physical act, deliberately — so the honest workflow asks rather than pretends. A merely suspended loop is different: that is reversible from the sheet, and it is not a reason to refuse a meal bolus, so "Resume loop and bolus" and "Bolus anyway" are both offered.
- The driver fails fast instead of polling a dead pump. The BLE layer now distinguishes never sent
from sent from ack lost. Only the first is a certain no-op, and only it skips confirmation — a lost
ack can still mean the pump delivered, so that path confirms by read exactly as before. The stuck-at-0%
dialog was never a UI bug: the progress dialog closes when
deliverTreatmentreturns. - Reservoir surveillance. Urgent alert below the critical threshold, alarm at empty — Home alert, Android notification and sound. The pill is drawn whenever the pump has been read, turns amber below the warning threshold and says Empty in red. Both thresholds are the app's existing reservoir preferences (Overview → status lights), which the redesign had left unread when it dropped the status-lights row — so this puts a setting that was already there, and already translated, back to work rather than inventing numbers nobody can find.
- Empty is a suspended pump. It reports as suspended, so the loop drops to
SUSPENDED_BY_PUMP, and it records the same 0-ratePUMP_SUSPENDtemp basal a pump-side stop does — which is what stops basal IOB accruing against insulin that never left the cartridge. - Never book a dose the pump was known not to take. An unconfirmed bolus is still recorded when the pump is healthy, because under-counting IOB is the dangerous direction — but it now raises an urgent notification telling you to verify it. When a fresh status read shows the pump stopped or empty, that is not ambiguity, and nothing is recorded at all.
- A way to repair IOB. Tapping IOB opens the bolus/basal split and the last six hours of boluses,
each removable behind a confirmation and the same audited
invalidateBoluspath the old Treatments screen used. Six hours is the point: it is longer than any sane DIA, so every dose still contributing to the IOB on screen is in that list.
libre3/ — full README: architecture, protocol notes, credits
xDrip cannot start a Libre 3 sensor, so the usual setup runs Juggluco to activate and stream, then bridges into AAPS. That bridge decimates the data — ~1 reading per 5 minutes reaches AAPS out of the sensor's 1-per-minute — and loses the sensor's own gap backfill. This module removes the middle apps: AAPS performs the sensor authorization handshake and talks to the patch directly over BLE.
- Sensor authorization handshake, then the full 1-minute glucose stream — the rate HovorkaMPC's minute-stepping EKF is built for, feeding it raw readings instead of 1-of-5
- Gap backfill from the sensor's own retained history when the phone was out of range
- Sensor lifecycle (warm-up / active / expiring / failed) surfaced in the UI, not inferred from silence
- Dense-aware bucketing in core AAPS: 1-minute samples are averaged into 5-minute buckets for the legacy consumers, so the extra data is a benefit rather than a jagged chart; the loop still reads raw
- A
Libre3glucose source plugin with range/rate gating andlifeCountde-duplication
This stands on Juggluco by Jaap Korthals Altes (j-kaltes),
GPL-3.0. The sensor-authorization / challenge-cipher native core is Juggluco's C, vendored verbatim
with its GPL headers intact; every wire-format decoder transcribes formats Juggluco worked out first.
Full provenance is in the module README and
libre3/src/main/cpp/VENDOR.md.
Not yet: starting a fresh sensor from AAPS over NFC. The activation command is written, but the NFC response parser is not, so a new sensor is still activated with Juggluco and its per-sensor credentials imported. This is a private single-device build — per-sensor credentials never leave the device, and no APK built from it should be distributed.
Things that do not warrant a section but are still differences from upstream.
Nightscout over a private network. NSClient v3 is HTTPS-only at four separate gates — the URL
validator, the plugin's scheme handling, the nssdk request builder, and Android's own
network_security_config. The last is the quiet one: it fails with UnknownServiceException: CLEARTEXT ... not permitted and only in AAPS's internal log. All four are patched so a Nightscout on a Tailscale
or LAN address works. For private networks only — do not point this at anything reachable from the
internet over plain HTTP.
Nightscout failure handling. Upload and download failures back off (30 s doubling to 30 min) and log one stack per distinct error instead of one per record. With the mesh down, the previous behaviour was 416 failures in three hours and 55% of every line the app wrote.
Pump-driver reliability. A BLE watchdog that unwedges stalled operations (the recurring "pump
unreachable"), a fix for delivered boluses being discarded by an AAPS one-minute freshness gate, bolus
under-recording corrected by decoding pump status and reconciling against pump history, and the pump's
own 28-day session-key expiry identified and surfaced (0x8C NO_SHARED_KEY) rather than presenting as a
generic connection failure.
Two variants matter:
./gradlew :app:assembleFullLoop # the build that drives the pump — non-debuggable, AOT-eligible
./gradlew :app:assembleFullDebug # debuggable, for attaching a debuggerBoth are signed with the debug keystore, so install -r between them preserves app data.
If the build fails inside KSP with PROCESSING_ERROR, run ./gradlew :app:clean first — the annotation
processor caches stale generated types across large refactors.
The YpsoPump driver reads its session key from device preferences at runtime. It is never committed to this repository.
Please do not raise issues from this fork against upstream AndroidAPS. None of the above is their work and none of it has had their review. Upstream bugs belong at nightscout/AndroidAPS.
AGPL-3.0, same as upstream AndroidAPS. See LICENSE.txt.
The libre3/ module vendors native code from Juggluco
(Jaap Korthals Altes), which is GPL-3.0-or-later. GPL-3.0 combines upward into AGPL-3.0; the vendored
files keep their original GPL headers, and provenance is recorded in
libre3/src/main/cpp/VENDOR.md.
Everything below is the upstream AndroidAPS README, unchanged.
ℹ️ Fork change — NSClientv3 accepts
http://Nightscout URLs. Upstream AAPS is HTTPS-only for Nightscout sync, enforced at four independent layers: the URL input validator, the NSClientv3 plugin's scheme handling, thenssdknetwork builder, and the app's Android network-security config (app/src/main/res/xml/network_security_config.xml). This fork loosens all four so a plain-HTTP Nightscout can be used. This is intended specifically for VPN / mesh setups (e.g. Tailscale, WireGuard, ZeroTier) where the transport is already end-to-end encrypted and TLS on Nightscout is redundant. Cleartext is permitted only for the explicitly listed hosts innetwork_security_config.xml— everything else stays HTTPS-only. Do not point NSClientv3 at a plain-HTTP endpoint over the public internet or an untrusted LAN: your API token and health data would travel unencrypted. Add or remove allowed hosts by editing the<domain>entries in that file.
- Check the wiki: https://wiki.aaps.app
- Everyone who’s been looping with AAPS needs to fill out the form after 3 days of looping https://docs.google.com/forms/d/14KcMjlINPMJHVt28MDRupa4sz4DDIooI4SrW0P3HSN8/viewform?c=0&w=1
3KawK8aQe48478s6fxJ8Ms6VTWkwjgr9f2














