← All work Hardware · BLE · Overnight apnea monitor

ApneaWatch — overnight apnea monitoring.

Consumer pulse oximeters show a number and store nothing. Clinical gear is expensive and gives caregivers no live view and no usable record. ApneaWatch turns an off-the-shelf oximeter into an overnight monitor: it streams blood-oxygen over Bluetooth, watches through the night, and turns it into clinical-style analytics a doctor can actually read.

RoleSolo — product, design & engineering
PlatformWeb + mobile
StackFlutter · Kotlin/Ktor · Postgres · BLE
StatusDeployed · runs nightly

The problem

Picture someone who needs their blood-oxygen watched overnight — for suspected sleep apnea or another condition that causes oxygen to dip during sleep — and a caregiver who wants to keep an eye on it from another room and bring a clear record to the next appointment.

The off-the-shelf options fail this person at both ends. A consumer fingertip oximeter shows a live number and remembers nothing — no history, no trend, no export. Clinical equipment is expensive, episodic, and gives a caregiver no live view and no record they can take home. A full sleep study is the gold standard for diagnosis, but it isn't something you run at home every night. There was a gap between "a number on a screen" and "something a doctor can act on." ApneaWatch fills it.

Note: ApneaWatch uses consumer-grade sensors and is not an FDA-cleared medical device; it's a logging and visualization tool to inform a conversation with a physician. All screens and data shown here are synthetic samples — no real person's data is displayed.

Who it's for

The caregiver is the primary operator: they pick a profile, pair the device, start a session, and can watch live from another room. The person being monitored just wears the oximeter overnight — the app applies age-appropriate thresholds (it supports pediatric as well as adult ranges). The clinician is the audience for the output: a clear overnight summary they can read in seconds.

Product & journey

One night, end to end

  • Pair — Bluetooth pairing on mobile, or the browser's Web Bluetooth picker on the web build; two oximeter families are supported, and can even run in parallel.
  • Monitor — a live session opens a WebSocket and streams oxygen saturation, heart rate, perfusion index, and the pulse waveform, with a live dashboard a caregiver can watch from another room.
  • Persist — every session is stored: high-frequency raw samples plus one-second aggregates, so nothing is lost across an eight-hour night.
  • Review — a session detail view with timeline charts and a clinical-style summary; a home dashboard rolls multiple nights into trends.
  • Share — a one-tap text summary (ODI, time below 90%, lowest and average SpO₂, events) copied to the clipboard for a doctor's visit.

Architecture

The hard part is the front of the pipe: decoding a proprietary Bluetooth stream reliably for hours. The Flutter client decodes and aggregates the BLE feed, streams it over a WebSocket to a Kotlin/Ktor API, which persists raw and one-second data to PostgreSQL and computes the overnight analytics. The same Flutter codebase ships to Android and the web.

Oximeter BLE ~50 Hz Flutter client decode · aggregate Ktor API WebSocket + REST PostgreSQL raw + 1 s + analytics
From a reverse-engineered Bluetooth stream to overnight analytics.

Craft & challenges

  • Reverse-engineered two BLE protocols — one device speaks a Nordic UART service with ~36 Hz waveform frames and 13-byte vitals packets; the other needs a custom CRC-8 (extracted from a decompiled vendor library) and a handshake to unlock a 50 Hz pulse waveform. Get the polynomial wrong and the device silently ignores you.
  • Resilient decode & reconnect — a rolling frame parser that survives split and concatenated Bluetooth notifications, plus per-device reconnect loops so an eight-hour overnight session survives Wi-Fi and BLE flaps.
  • A data-integrity fix that mattered — switching aggregation to fixed wall-clock one-second buckets recovered roughly a quarter of samples that arrival-timed bucketing had been dropping.
  • Sleep-oximetry analytics — ODI (3% desaturation index), time below 90%, hypoxic burden, nadir and mean SpO₂, with age-adjusted pediatric vs adult thresholds drawn from the clinical literature.

Honest scope: the doctor-visit export is a structured text summary copied to the clipboard — not a generated PDF. It's framed as "discuss with your physician," never as diagnosis.

FlutterDartWeb Bluetoothflutter_blue_plusKotlin 1.9KtorWebSocketsPostgreSQLExposed ORMRailway

Selected screens

All screens below use synthetic sample data.