A personal Android app for Type 1 Diabetes that passively reads a Microtech/Ottai AiDEX X / LinX continuous glucose monitor over Bluetooth-LE advertisements and runs on-device glucose forecasting. It is advisory-only — it never actuates insulin delivery — and is built for a single arm64 device rather than for general distribution: it is sideloaded, not published to any app store.
Designed by a T1DM patient, informed by lived experience.
Caution
Personal project, research use only. T1DMDROID solves one patient's niche problem — one sensor, one phone — and is published for reference, not for anyone else to install or depend on. It is not a medical device, not clinically validated, and unsupported. Its forecasts and calculators may be wrong and must not be used for medical, diagnostic, or dosing decisions, nor to replace a real CGM, its official app, or professional care. Provided "as is", without warranty; the authors accept no liability.
- What it is
- Features
- Architecture
- Module map
- Building
- Running on Xiaomi HyperOS / MIUI
- Target device
- Related projects
- License
The AiDEX X broadcasts its current reading roughly once per minute and the app listens passively — no pairing, no bond, no GATT connection. Activation, calibration and warmup stay with the sensor's official app on a separate phone. Each reading is stamped with the phone's receive time snapped to a 5-minute grid.
A small transformer runs over that feed entirely on the device. It predicts any withheld stretch of glucose rather than only the next two hours, so one artifact fills a gap the sensor left as well as it forecasts, and a low-rank adapter can personalise it from the wearer's own matured forecasts while the exported weights stay frozen. Around it sit meal and insulin logs, advisory statistics, and a deterministic, model-free alarm path for out-of-range and loss-of-signal. Optional integrations add a self-hosted sync server and an encrypted BLE watch accessory.
The Bluetooth, inference, and watch protocols are documented under docs/: CGM.md, INFERENCE.md, WATCH_BLE.md, and T1DMSERVER_API.md.
tall_blood_sugar_graph_that_shows_iob_and_cob.mp4Blood glucose graph |
read_from_multiple_sensors_at_once.mp4Multiple sensors |
paint_on_the_blood_sugar_graph.mp4Paint |
drive_on_your_blood_sugar_curve.mp4Drive |
autoregressive_rolling_for_long_horizon_predictions.mp4Autoregressive rolling |
see_previous_predictions_with_hindsight_mode.mp4Hindsight |
test_infilling_capabilities_of_prediction_models.mp4Infilling |
visualize_model_time_output.mp4Circadian clock |
advanced_model_evaluation.mp4Model evaluation |
lora_finetuning.mp4LoRA adapters |
log_carbohydrates.mp4Log carbohydrates |
log_insulin.mp4Log insulin |
advanced_blood_sugar_statistics.mp4Statistics |
advanced_settings_panel.mp4Settings |
Bouts run from a manual start to a manual stop, following GPS during walks and runs and drawing the route on OpenStreetMap tiles. Distance and pace come from accepted fixes; energy expenditure from the ACSM walking and running equations at zero grade, shown only once a body mass has been entered. Each bout's per-five-minute magnitude folds into the same wide sensor series that carries glucose, heart rate, steps, sleep and mood.
Every bout opens its own review: the route, the glucose trace from 30 minutes before to 2 hours after, and a slider sweeping the forecast issued at any instant in that window across the trace that followed it. Where no forecast was issued, the slider shows nothing rather than the nearest one. Tracks are never uploaded; map tiles are fetched for the surrounding area and cached on the device.
One gzipped, line-delimited JSON file holds the whole local record — every glucose reading, the wide sensor series, meals, doses, basal schedules, custom foods, saved meals, insulin types, exercise bouts and their tracks, the graph's freehand drawings, and every setting. Automatic backups run on a chosen cadence into a folder outside app storage, so they survive an uninstall, with a configurable number of older archives retained.
Restore merges: a record already present is kept, so importing the same file twice changes nothing and an older archive can never roll back newer data. The server token is never written to a backup — it lives in the Android Keystore rather than in the database.
- UI: Jetpack Compose, organized as a multi-module Gradle build so the CGM-source and model-backend seams stay pluggable.
- Rust core (
t1dm-core, via JNI/NDK): the correctness-critical, hot numerics — AiDEX frame decode and its CRCs, session crypto, the model pre/post pipeline (causal Savitzky-Golay smoothing, normalize/denormalize, the Kovatchev risk transform, quantile assembly), and the watch AES-128-GCM. Kotlin keeps the UI, BLE plumbing, storage, and orchestration. The core is tested bit-for-bit against golden vectors in CI. - On-device inference: ExecuTorch. One exported model runs on two backends behind a clean seam — CPU (XNNPACK, fp32) as the reference authority, and the GPU (Vulkan compute delegate, fp16) as a measured shadow whose agreement with the CPU path is measured before it may inform anything. The Vulkan delegate comes from a custom ExecuTorch build vendored under
third_party/; the stock runtime registers XNNPACK only. - Storage & orchestration: Room on the bundled SQLite driver; an always-on foreground service plus WorkManager run the passive scan, the 5-minute grid, inference, sync, and the alarm path off the main thread.
| Module | Responsibility |
|---|---|
:app |
Composition root, the always-on foreground service, notifications, widgets, navigation |
:cgm |
Passive AiDEX X advertisement scan, recognition, and the CGM-source registry |
:inference |
The forecasting cycle: context build, backend dispatch, decode, degeneracy gating |
:sensors |
Step counter, GPS track recording, and other phone sensors |
:calc |
Advisory bolus/basal and statistics calculators |
:alerts |
The deterministic, model-free alarm engine (out-of-range, loss-of-signal, device temperature) |
:sync |
Durable-outbox sync with the self-hosted server |
:watch |
Encrypted BLE link to the optional ESP32-C3 watch |
:data |
Room database, repositories, curve reconstruction, the backup archive codec |
:core:common, :core:model, :core:design, :core:native |
Shared dispatchers, domain types, theming, and the Rust-core JNI bindings |
:ui:graph |
The custom Compose blood-glucose graph |
:feature:* |
Screen features — dashboard, stats, models, hardware, network, meals, insulin, security, settings, logs, backup |
- Android SDK 36 and the NDK, JDK 21.
- A Rust toolchain with the
aarch64-linux-androidtarget andcargo-ndkinstalled and onPATH— the native build silently skips ifcargo-ndkis missing.
The app targets arm64-v8a only, minSdk 34, targetSdk 36.
./gradlew :app:assemblePersonalReleaseTwo product flavors: personal (the daily build) and public (installs under a .pub application id). Release builds are R8-minified and resource-shrunk; without a keystore.properties they fall back to the debug signing key, so a fresh checkout still produces an installable APK.
HyperOS manages background apps far more aggressively than stock Android, and an always-on passive CGM reader is exactly the kind of app it curtails. The setup below is required for reliable operation, and a system update or reboot can silently reset parts of it.
In Settings → Apps, for T1DMDROID:
- Autostart on — this also lets it start on boot.
- Battery mode No restrictions; the default "Battery saver" level throttles background work.
- Pause app activity if unused off.
- The standard Android battery-optimization exemption granted as well.
Also set the system Bluetooth app to Unrestricted (Settings → Apps → show system apps → Bluetooth → battery usage) — easy to miss, and the scan depends on it. Lock the app in Recents (drag its card down until it shows a padlock) so "clear all" and the memory cleaner cannot evict it. Performance power mode helps as well.
On Android 14+, and especially on HyperOS, the system suspends a background app's Bluetooth-LE scan when the screen turns off. To keep collecting, the app uses offloaded batch scanning: the Bluetooth controller buffers the sensor's advertisements in hardware regardless of screen state, and HyperOS flushes those batches on roughly a five-minute timer.
So while the phone is locked, new readings — and therefore any alarms — can lag by up to about five minutes. This is an OS-imposed floor for a passive-advertisement sensor, and does not apply while the screen is on. Each batched reading is timestamped at its true capture instant, so no 5-minute grid slot is lost.
The app posts a persistent, silent notification with the current glucose value and trend. HyperOS hides silent notifications from the lock screen by default and removes the corresponding control from Settings, so the notification appears in the shade but not on the lock screen until that control is re-enabled once:
adb shell settings put secure lock_screen_show_silent_notifications 1An "Activity Launcher"-type app reaches the same control: com.android.settings.Settings$ConfigureNotificationSettingsActivity → Notifications on lock screen → Show conversations, default and silent.
The setting is device-wide, persists across reboots, and is cleared by a factory reset — no app can set it on the user's behalf.
The build targets a single phone: a Redmi K90 Max (MediaTek Dimensity 9500 / MT6993) running Android 16 / HyperOS, arm64-v8a. Accelerated inference runs on the GPU through Vulkan; the SoC's APU is not used, since the MediaTek NeuroPilot runtime ships through Play feature delivery and a sideloaded build cannot fetch it. Other devices are untested and unsupported.
- T1DMSIM — the behavioral simulator whose synthetic traces pretrain the model this app runs.
- T1DMAI — the training and ExecuTorch export pipeline that produces the artifact and descriptor loaded here.
- T1DMSERVER — the optional sync backend this app pushes to, phone-authoritative (protocol in
docs/T1DMSERVER_API.md).
MIT — see LICENSE.