I wear a RingConn Gen 3 every day. The hardware is great. The app is not. The UI is horrible, the insights are pretty lackluster, and RingConn has no API, so nothing else can even read the ring. Bevel is the closest thing out there, but it's so cluttered.
All I wanted was one clean, minimal screen that tells me how I slept and how ready I am, plus an AI that actually reads my data. So I built my own. It's called Halo, a native iPhone app that talks to the ring over Bluetooth and turns its raw data into WHOOP and Oura style scores. The first version was on my phone about three hours after I started.

Starting from OpenCircuit
I didn't crack the ring's protocol myself. Halo is built on top of OpenCircuit, an open source project that already reverse engineered how RingConn talks over Bluetooth. They found some wild stuff. The data isn't encrypted at all, the login is a challenge-response keyed on the ring's MAC address, and there's a hidden 100Hz raw optical sensor stream in there.
The catch is that OpenCircuit is a whole app, and its Bluetooth layer alone is 6,838 lines. I only needed the part that understands the ring.
So I kept OpenCircuitKit, the core that decodes the ring's messages, and wrote my own Bluetooth layer around it. It's one Swift file, 523 lines, about 13x smaller. It connects, logs in, pulls the history and streams live heart rate, and that's it.
fig 01
I only kept the part that understands the ring
Lines of Swift in each app's Bluetooth layer
- OpenCircuit6,838
- Halo523
13× smaller
show datahide data
| Bluetooth layer | Lines |
|---|---|
| OpenCircuit | 6,838 |
| Halo | 523 |
Most of the early bugs were me reading the wrong bytes. My favourite was in OpenCircuit's scripts, which found records by checking for a first byte of 0x0c. That's actually the top byte of the timestamp, and it flips to 0x0d on November 28, 2026, so they'd have silently stopped reading history in about a month.
Staying awake all night
Once the ring was talking, I hit the real problem. iOS suspends apps overnight, and overnight is the whole reason I wear the thing.
The fix came from the ring. It sends a small heartbeat signal about once a minute, and with Bluetooth background mode on, iOS wakes the app for each one. So Halo just rides the ring's heartbeat, pulling down the night so far every 15 minutes. I was dreading a redesign around background tasks, and this let me skip all of it.
fig 02illustrative
The ring keeps waking the app up
iOS suspends Halo overnight, but each ring heartbeat wakes it for a moment. Every 15th one, Halo pulls the night so far.
- ring heartbeat · ~every minute
- Halo pulls history · every 15 min
zoomed in · one 15-minute window · the app wakes for each beat, then sleeps
On first launch Halo also backfilled 60 days from Apple Health, so my baselines existed on day one. That matters more than it sounds, you'll see in a sec.
The ring stores one record every 150 seconds with heart rate, HRV, blood oxygen, breathing rate and motion. RingConn computes sleep stages and scores on its own servers, so if I wanted scores, I had to make them.
How the scores actually work
WHOOP and Oura are the industry leaders in recovery and sleep tracking, so I wanted my scores to work like theirs. Neither one publishes its formulas, but companies patent what they won't publish. So I researched it properly. I read 18 WHOOP patents, papers by both companies' own scientists, reverse engineering repos, and a project that had decompiled the scoring library inside Oura's app.
The biggest idea is that everything is compared to you. A resting heart rate of 50 means nothing by itself, only next to your normal. So Halo keeps a baseline for each metric and measures how far tonight is from your usual night to night swing.
fig 03illustrative
Same number, very different night
Both people wake up with HRV 20 ms above their average. What matters is how much they normally swing.
Person A · usually swings ±10 ms
2.0 usual swings above normalbetter than 98% of their nights
Person B · usually swings ±30 ms
0.7 usual swings above normalbetter than 75% of their nights
x axis: HRV vs personal average, ms · curves: each person's usual night-to-night spread
The sleep score turned out to be simpler than I expected. Oura's is a weighted average, so each part of your night counts for a fixed share of the score. How long you slept counts the most, then how restless you were, and then efficiency, REM, deep sleep, how fast you fell asleep and your timing all count equally.
fig 04
Oura's sleep score is a weighted average
Each part of your night counts for a fixed share of the score
- Total sleep35%
- Restfulness15%
- Efficiency10%
- REM sleep10%
- Deep sleep10%
- Time to fall asleep10%
- Timing10%
Weights from a decompiled copy of Oura's scoring library; checked against 336 days of one person's scores (R² 0.9987).
show datahide data
| Part | Share of score |
|---|---|
| Total sleep | 35% |
| Restfulness | 15% |
| Efficiency | 10% |
| REM sleep | 10% |
| Deep sleep | 10% |
| Time to fall asleep | 10% |
| Timing | 10% |
To check it, someone took over 336 days of their own Oura scores and compared them to this formula. It matched almost perfectly (R² of 0.9987, where 1 would be a perfect match). It also showed I was being way too nice to myself. My first version gave full marks for total sleep at about 7.75 hours, but Oura only gives 100 at 9 hours.
fig 05
I was being way too nice to myself
Sleep duration score by hours slept
- Oura
- my first version
Oura's anchors from patent US11793454B2. Hover for exact scores.
show datahide data
| Hours slept | My first version | Oura |
|---|---|---|
| 4h | 18 | 58 |
| 5h | 40 | 67 |
| 6h | 63 | 75 |
| 7h | 85 | 82 |
| 7h 45m | 100 | 88 |
| 8h | 100 | 90 |
| 9h | 100 | 100 |
| 10h | 100 | 100 |
Recovery is WHOOP's morning score, from 0 to 100%, for how ready your body is to take on a hard day. HRV, or heart rate variability, is the tiny variation in time between your heartbeats. When it's higher than your normal, your body's usually well rested.
Recovery is mostly HRV. Someone ran the numbers on 411 days of their own WHOOP data to see what actually moves the score. HRV did about half the work, how much you slept compared to what you needed did about a quarter, and resting heart rate barely mattered.
fig 06
Recovery is mostly HRV
Share of the day-to-day change in WHOOP recovery each input explains
- HRV48–50%
- Sleep vs need23–29%
- Resting heart rate3–5%
One person, 411 days of WHOOP data. Ranges span two models fit to the same data (Shapley shares).
show datahide data
| Input | Share |
|---|---|
| HRV | 48–50% |
| Sleep vs need | 23–29% |
| Resting heart rate | 3–5% |
The shape matters too. Recovery follows an S-curve, which means it changes fast when you're close to your normal and flattens out at the extremes. A really great night and an insanely great night end up scoring about the same. On days the model hadn't seen, an S-curve explained 82% of the swings in their recovery, compared to 59% for a straight line.
fig 07
Recovery follows an S-curve
Fast change near your normal, flat at the extremes. A great night and an insanely great night score about the same.
- S-curve · explains 82% of the variation
- straight line · 59%
S-curve is Halo's recovery map. Fit quality is hold-out R² from one person's 411 days of WHOOP data.
show datahide data
| Tonight vs normal | S-curve | Straight line |
|---|---|---|
| -3 | 2 | 2 |
| -2 | 6 | 19 |
| -1 | 24 | 36 |
| 0 | 58 | 53 |
| +1 | 86 | 70 |
| +2 | 97 | 87 |
| +3 | 99 | 100 |
Strain is WHOOP's number for how hard your day was on your body, on a scale from 0 to 21. It adds up how long your heart rate stayed up and how high it went. The scale gets harder to climb the higher you go, so 18 to 19 takes way more than 4 to 5. A chill day lands under 10, and a hard workout pushes 14 to 17.
Strain needed a minimum. Your heart rate reserve is the gap between your resting and max heart rate, and Halo only counts time spent above 30% of it, which is about 84 bpm for me. An open source WHOOP clone that counted everything gave a day of doing nothing a strain of 11 to 12. With the minimum, a day sitting at my desk barely registers.
fig 08illustrative
Strain only counts above a minimum
Halo only counts time above 30% of your heart-rate reserve, about 84 bpm for me. Sitting at a desk barely registers.
- heart rate
- counts as strain
- 84 bpm minimum
Heart-rate reserve = max HR − resting HR. Without a minimum, an open-source WHOOP clone gave a day of doing nothing a strain of 11–12.

One of my favourite finds was a WHOOP patent that just prints the formula for how much extra sleep you need after a hard day.
extra sleep (hours) = 1.7 / (1 + e^((17 − strain) / 3.5))
At a strain of 5.3 it gives exactly the 3.5 minutes from WHOOP's own API example. Patents come with worked examples like that, so I turned them into tests. Halo's engine tests are pinned to numbers from WHOOP's and Oura's own documents, so if my math drifts from theirs, a test fails.
fig 09
How much extra sleep a hard day costs
extra sleep (h) = 1.7 / (1 + e(17 − strain) / 3.5)
Formula printed in WHOOP patent US 12,318,226 B2. Strain 5.3 → 3.5 min matches WHOOP's own API example.
show datahide data
| Strain | Extra sleep |
|---|---|
| 0 | 0.8 min |
| 5.3 | 3.5 min |
| 8 | 7.2 min |
| 10 | 12.2 min |
| 12 | 19.7 min |
| 14 | 30.4 min |
| 17 | 51.0 min |
| 19 | 65.2 min |
| 21 | 77.3 min |

Letting my AI fill in the context
The other thing I wanted was for my AI agents to be part of it. My Grok bot has way more context about my life than any app does. It knows when I had drinks, a late meal or a rough day because I already tell it. So Halo syncs to a small server, Grok connects over MCP, and it logs that context into Halo on its own. Halo only calls something a real effect once it has 5 nights with it and 5 without, and the difference beats my normal noise.
fig 10
My AI fills in the context
I tell Grok about my day like I already do. It logs the context into Halo on its own.
- 1Mehad 3 drinks
- 2Grokalready knows my day, calls Halo over MCP
- 3Halo servertag · alcohol · 3 · fri night
- 4Halo appfri night · 3 drinks
Halo only calls a tag a real effect after 5 nights with it and 5 without, and only if the difference beats my normal noise.


Every morning Claude writes me a brief from the same math the app uses. Today's noticed my HRV averages 74.9 ms on weekends vs 89.4 on weekdays, so this Saturday's experiment is strain under 14 and in bed before 1:30.

With AI you can just build the thing you want now. I was stuck with an app I didn't like for a ring I wear every day. Now I have my own app, scored the way I want, talking to my own agents.
I think this is the first of many. There's a lot of stuff I use every day that I'd rather just build myself.