Skip to content

ounsal/ouroboros

Repository files navigation

Ouroboros

A ring that listens. Privacy by architecture. Truth as a feature.

Ouroboros is an Android app that turns a generic low-cost smart ring into a Fitbit Premium equivalent for senior citizens, families in the developing world, and anyone for whom data sovereignty is non-negotiable. Gemma 4 runs entirely on the phone. No subscription. No cloud. No data ever leaves the device.

Built for the Google Gemma 4 Impact Challenge (Digital Equity & Inclusivity track).


The pitch

This week, Google launches Fitbit Air with Health Pro. A hundred dollar device and a hundred and twenty dollar a year subscription. By year one the subscription costs more than the hardware. The intelligence lives in Google's cloud. The data lives there too. For the people who can afford it, it will be excellent.

There are four billion people who will not buy that.

The exact same sensor hardware (a single photoplethysmography sensor plus a three-axis accelerometer) sits inside a ten dollar smart ring on AliExpress. We bought one. We threw away its app. We rebuilt it with Gemma 4 running locally on the user's phone, and an explicit refusal to fabricate the metrics the hardware cannot actually measure.

Architecture

+----------------+      +-----------------+      +---------------------+
| Generic ring   | BLE  | Foreground sync | Room SQLite (8 tables)   |
| ($10 hardware) | <--> | service (5min)  +-----> Heart rate, sleep, |
+----------------+      +-----------------+       HRV, SpO2, journal, |
                                                  context, sessions   |
                                                  +-+-----------------+
                                                    |
                          +-------------------------+
                          |
                +---------v---------+      +---------------------+
                | StatsBuilder      |----->| Gemma 4 E4B via     |
                | (7-day baselines, |      | LiteRT-LM (3.66 GB) |
                |  anomaly p10/p90) |      | streaming + JSON    |
                +---------+---------+      +----------+----------+
                          |                           |
                          v                           v
                +---------------------+      +-----------------+
                | Anomaly notifier    |      | Offline TTS     |
                | (chip-action tags)  |      | (STREAM_MUSIC)  |
                +---------------------+      +-----------------+

Every layer is local. There is no server. There is no required network call. The app works fully offline in airplane mode once the model is on disk.

How Gemma 4 is used

Three places, three different facets of the model:

  1. Streaming narrative summarization. The morning readout is the emotional core. We feed Gemma 4 a structured prompt with direction-stated facts ("sleep last night was 28 minutes ABOVE the seven-night median"), a one-shot example, and a ban-list of clinical vocabulary. Output streams token by token via LlmInferenceSession.generateResponseAsync wrapped in a Kotlin Flow. First token in under two seconds on a Samsung S24 Ultra.

  2. Native JSON extraction. Voice journal entries go through a two-stage parser. A regex pass catches the obvious cases (LDL 140, weight 75 kg); anything regex can't fill goes to Gemma 4 with a strict JSON-only prompt that infers reasonable units from personal-intro context. Returns a typed profile that backfills without overwriting.

  3. Personal causal-map narration. The morning prompt includes the last five (anomaly, context_tag) pairs from the local journal. Gemma weaves them in as recall, never as advice.


Technical challenges overcome

This project is real engineering, not a demo. The hard parts:

Hardware sourcing without IP exposure

The premise of the project rests on finding a ring whose hardware is sufficient (PPG + accelerometer + thermistor) and whose Bluetooth Low Energy protocol could be reverse engineered cleanly enough to write an independent implementation.

Most popular smart rings ship locked: proprietary BLE protocols, signed firmware, account-bound pairing, and companion apps that refuse to function without their cloud. We deliberately avoided those. The hardware we settled on is a generic family of inexpensive rings sold under dozens of OEM rebrands on AliExpress, Amazon, and Alibaba. They share a common BLE protocol because they share a common reference SDK; what they DON'T share is brand loyalty, account binding, or cloud dependency. A ring purchased under one brand pairs identically to a ring sold under another. That common substrate is what makes this project portable across the price tier.

Clean-room BLE protocol port

The vendor SDK is a Java archive shipped to OEMs for embedding in their companion apps. We decompiled it locally with the standard cfr.jar Java decompiler to study the wire format -- frame structure, command IDs, CRC16 polynomial, history-stream chunking, time encoding (UTC seconds since a custom epoch), and the multi-part defragmentation for BLE packets over 512 bytes. The decompiled tree is gitignored and never shipped.

The shipped Kotlin BLE stack is a clean-room reimplementation. No vendor code is included or derived line-by-line. The implementation lives entirely in android/app/src/main/java/com/ouroboros/ring/ble/protocol/. Twenty-four Kotlin unit tests lock byte parity against Python-generated golden vectors from our reference Python pipeline (ring_collector.py), which itself was written by reading the protocol off the wire with a BLE sniffer and cross-validating against the decompiled spec.

Two distinct truth oracles

The project carries two complete BLE implementations, in two different languages, that agree byte-for-byte:

  1. Python pipeline (ring_collector.py, morning.py, sync_all.py). Originally the daily-driver running on a desktop with scheduled syncs three times daily. Pulls from BLE, decodes, persists to PostgreSQL, produces a terminal morning readout.

  2. Kotlin pipeline (under android/app/src/main/java/com/ouroboros/ring/). The phone-native peer-node. Same protocol, same parsing, same byte order. Adds Room SQLite persistence, foreground service sync, Gemma 4 narration, voice journaling, anomaly detection.

When the two disagree on a single byte, one of them has a bug. The unit test suite uses traces dumped from the Python implementation as golden vectors -- if the Kotlin parser produces a different SensorRecord for the same input bytes, the test fails. This is how we know the clean-room port is correct.

Hardware-honest UX

The biggest non-technical challenge is what we deliberately DON'T ship.

The vendor companion apps proudly display blood pressure, blood glucose, body fat, uric acid, and blood lipid numbers. None of these can be measured by a single PPG sensor and an accelerometer. They are firmware fiction.

We refuse to display any of them. The What this ring doesn't measure (and why) page in Settings names each fabricated metric explicitly and explains why the hardware cannot produce it. For a target user already inclined to trust a screen, this honesty IS the feature.

Android 16 foreground service hardening

Android 16 (API 36) tightened foreground-service requirements. The connectedDevice foreground service type now requires that BLUETOOTH_CONNECT (or another listed permission) be runtime-granted before startForeground() can be called -- not just declared in the manifest.

The first time we ran the app on Android 16 against the BLE-permission-less demo path, SyncService.onCreate() threw SecurityException, the OS auto-restarted the service due to START_STICKY, and the process spun in a crash loop the user couldn't escape. The fix is in sync/SyncService.kt: wrap startForegroundCompat() in a try/catch that flips a disabled flag, calls stopSelf(), and returns START_NOT_STICKY so the OS doesn't try again. The service starts cleanly once permissions are granted, and bails safely when they aren't.

Samsung TTS audio routing

TextToSpeech.speak() returns SUCCESS on Samsung even when the media volume rocker is at zero and the user hears nothing. We chased a "read aloud doesn't work" bug for an hour before logcat revealed the engine was firing onStart and onDone events perfectly; only the audio was silent.

Two fixes layered: force STREAM_MUSIC and KEY_PARAM_VOLUME=1.0 in every speak() call to bypass the system stream defaulting; expose engine state in Settings -> Voice with a Test voice button so users can diagnose without us.

Also: Samsung's tts_default_synth secure setting is per-Android-user. The app may run as a secondary user (UID 95xxxxx) where the primary user's chosen engine is invisible. The fix is to pin to a specific engine (com.samsung.SMT or com.google.android.tts) via the three-arg TextToSpeech(context, listener, engine) constructor, with manifest <queries> declaring the TTS_SERVICE intent + explicit package names so Android 11+ package visibility lets us see those engines.

MediaPipe to LiteRT-LM migration

We started on MediaPipe's tasks-genai runtime with Gemma 3 1B in INT4. The 1B model copied facts back to the user verbatim rather than paraphrasing -- it could read the data, but it couldn't narrate.

Mid-build we discovered Google AI Edge had shipped com.google.ai.edge.litertlm with a saner format (.litertlm), ~30% faster generation than MediaPipe, and -- crucially -- the same model format that AI Edge Gallery itself uses. That meant users who already had Gemma 4 downloaded for testing could adb shell cp the model into our app's external files directory instead of fetching a separately-packaged variant.

One-line dependency swap; eighty lines of session-management code deleted. The 4B model paraphrases naturally where the 1B copied verbatim. The leap in narrative quality between sizes is the difference between "looks like AI" and "feels like a coach."

Kable disconnect hang on Samsung S24

Kotlin's Kable BLE library peripheral.disconnect() blocks indefinitely on Samsung S24 + the active ring family after a half-completed GATT teardown. Symptom: manual sync hangs the UI on "Connecting..." even though logcat shows BLE writes succeeding. recordsChannel.close() never runs, so the caller job.join() waits forever for a flow that never completes.

Fix in ble/RealBleSession.close(): wrap peripheral.disconnect() in withTimeoutOrNull(2_500) so the rest of cleanup always runs downstream, even when disconnect hangs.


Quick start

Try the demo (no ring required)

The fastest path is to install the APK from the latest GitHub release and tap Try with demo data on the pairing screen. The app pre-seeds two weeks of realistic data for a synthetic user (Alex Kim, 42, mild borderline labs) so the entire product surface -- streaming readout, patterns, voice journal, take-a-moment, settings -- is explorable in one tap. The model file isn't bundled in the APK (3.66 GB); see "Run with the LLM" below to get the full narrating experience.

Run with a real ring

  1. Clone this repo.
  2. Set up a PostgreSQL database for the Python side and copy .env.example to .env with credentials.
  3. Pair the ring once with its OEM companion app to wake it, then uninstall the companion app.
  4. Run python sync_patient.py <MAC> for a manual sync, or python sync_all.py for the multi-ring loop.
  5. python morning.py for the terminal readout, or build the Android app and pair via the in-app scan.

Run with the LLM

The model file isn't bundled (3.66 GB). Two options:

  • Easiest: install AI Edge Gallery from Play Store, download Gemma 4 E4B inside it, then copy the file:
    adb shell cp /sdcard/Android/data/com.google.ai.edge.gallery/files/Gemma_4_E4B_it/<hash>/gemma-4-E4B-it.litertlm /sdcard/Android/data/com.ouroboros.ring/files/models/
  • Or download the LiteRT-LM variant directly from Google AI Edge and adb push it to the same path.

Once present, the Tell me button on the Today tab will stream Gemma's narration token by token instead of falling back to the deterministic template.

Build from source

cd android
./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk

Requires Android Studio Iguana+, AGP 9.2+, Kotlin 2.2, JDK 17. Runs on any Android 13+ device with 6 GB RAM; tested on Samsung Galaxy S24 Ultra.


Repository layout

ring_collector.py       -- Python BLE pipeline (truth oracle)
morning.py              -- terminal morning readout
sync_all.py, sync_patient.py  -- scheduled-sync entry points
capabilities.py         -- vendor `getSupportFunction` decoder
db.py                   -- PostgreSQL config
dev/                    -- dev DB seeder, trace dumper
tests/                  -- pytest harness for Python pipeline
android/                -- Android app (Kotlin, Compose, Room, Kable, LiteRT-LM)
android/app/src/test/   -- Kotlin unit tests (BLE protocol parity)
register_tasks.ps1      -- Windows Task Scheduler hookup for periodic sync
run_sync.bat            -- batch wrapper used by Task Scheduler

The Android codebase is organized by feature, not by layer:

android/app/src/main/java/com/ouroboros/ring/
  audio/        -- TTS engine wrapper
  ble/          -- BleSource interface + recorded/real implementations
  ble/protocol/ -- Frame, defragmenter, history reader, data unpack
  db/           -- Room entities, DAOs, database
  demo/         -- no-ring evaluation path (seeded synthetic user)
  journal/      -- voice journal, parser, onboarding
  notif/        -- anomaly notifications + chip-tap receiver
  prefs/        -- DataStore wrapper
  readout/      -- stats builder, template fallback, Gemma inference
  sync/         -- foreground sync service + status
  ui/           -- bottom-nav tabs (Today / Talk / Patterns / Settings)
  wellbeing/    -- breathing + squats scripted exercises

License

MIT. See LICENSE.


Acknowledgments

This project would not be possible without the work of:

  • Google AI Edge team for LiteRT-LM and the open-source Gemma 4 model family
  • The Kable maintainers for an actually-Kotlin-coroutines-native BLE library
  • The OEM ring manufacturers who, by adopting a common reference SDK, inadvertently enabled an aftermarket open-source software stack for an entire price tier of wearables they themselves never imagined supporting

Submitted in good faith to the Gemma 4 Impact Challenge.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages