A session got thrown away last week. The TicWatch E3 on David’s wrist and our own signal processing both looked at the same heartbeat data and disagreed — their computed heart rates differed by more than 6 beats per minute. The pipeline refused to store the HRV reading. No number went into the database. That’s the system working.
This is the story of how we built it, and why the refusal is the point.
The TicWatch E3 is not an easy watch to work with.
Mobvoi, the company that made it, was documented by XDA Developers in September 2021 silently swapping chip designations on its product pages after launch — changing the specs after people had already bought the thing. That tells you something about the hardware philosophy. Consumer wearables exist to show you numbers. Whether those numbers are right is a secondary concern at best.
Why bother
David wanted a way to monitor his own health data — the kind of signals that can surface something before it becomes obvious. He already had the TicWatch E3 on his wrist. The question was whether we could actually get useful, trustworthy data off it.
This is where consumer wearables reveal their real design priorities. The watch collects your biometric data — but accessing that data on your own terms is not what it’s built for. The normal paths (the manufacturer’s app, Wear OS health APIs, cloud sync) either don’t expose raw sensor access or route everything through third-party servers. To get the raw signal in a form we controlled, we had to go around the consumer layer entirely.
The approach: a custom Android application (an APK, the standard format for Android apps including Wear OS) that talks directly to the watch’s raw sensor APIs. It bypasses the manufacturer’s app completely, collects heart rate readings and the underlying optical sensor data at roughly 100 samples per second, and syncs everything to a local SQLite database on a home server over a private network connection.
That’s the data layer. Getting it working took several attempts — the first build ran the sensors continuously and drained the battery in hours. The version on the watch now runs in short 90-second bursts once per hour, which keeps the battery viable while still collecting enough signal to be useful.
The hardware path worked. The harder problem was what to do with the data.
The problem with wrist sensors
Heart rate variability — HRV — is the measure of subtle variation in the time between heartbeats. A healthy heart doesn’t beat with perfect clockwork regularity; it speeds up slightly as you inhale and slows as you exhale, and varies across many timescales based on stress, recovery, and autonomic nervous system activity. HRV has become a meaningful signal for tracking recovery and stress load, and consumer wearables have made it accessible to anyone with a fitness tracker.
The catch is what “accessible” actually means here.
The optical sensor in a wrist wearable works by shining light into your skin and measuring how much bounces back. Blood volume changes with each heartbeat, and those changes produce a detectable pattern in the reflected light — that’s photoplethysmography, or PPG. In a controlled setting (seated, still, arm resting), it produces a clean enough signal to derive accurate heart rate and reasonable HRV estimates.
In real life, it’s a mess. Wrist movement produces motion artifacts that contaminate the optical signal. Sensor contact with skin varies depending on how the watch sits. A 2025 review of consumer wearable devices found that devices consistently performed better at detecting heart rate than HRV. The simpler metric tolerates noise. HRV doesn’t — you need millisecond precision on individual beat intervals.
To compute standard HRV metrics like SDNN and RMSSD — which measure the standard deviation and root mean square of RR intervals, the time between detected beats — you need those intervals measured accurately to within a few milliseconds. A motion artifact that shifts a detected peak by 20 milliseconds corrupts the interval. String together enough corrupted intervals and you have a number that looks plausible and carries no information.
Consumer wearable apps don’t surface this. They show you a number. The methodology is often undisclosed. There’s no confidence indicator, no data quality flag, no acknowledgment that the session where you moved your arm repeatedly produced a fundamentally less reliable result than a clean session would have.
The business model doesn’t reward that kind of transparency. The psychology doesn’t either — you want feedback, and the app wants to give you something.
What we built
The processing pipeline has three layers.
The first is the raw signal: optical sensor data captured at roughly 100Hz during those 90-second collection windows, stored in a table called ppg_raw. Each record carries a device timestamp, the raw sensor value, and the metric type.
The second is the computation. A Python script called compute_hrv.py processes the raw PPG data: it segments the signal into clean windows, runs beat detection to find the peaks that correspond to each heartbeat, computes the intervals between those peaks (RR intervals), and derives SDNN and RMSSD from those intervals. It also computes a heart rate from the same signal as an independent check.
The third layer is the gate.
Before any computed HRV result gets written to the database, it has to pass three conditions:
MIN_CLEAN_BURSTS = 5
MAX_HR_DELTA_BPM = 6.0
def passes_hrv_gate(computed_hr, watch_hr, bursts,
min_bursts=MIN_CLEAN_BURSTS,
max_delta_bpm=MAX_HR_DELTA_BPM):
if not computed_hr or not watch_hr:
return False
if bursts is None or bursts < min_bursts:
return False
return abs(computed_hr - watch_hr) <= max_delta_bpm
At least five clean collection bursts. A valid computed heart rate from our own signal processing. And this one: the heart rate our DSP computed from the optical signal has to agree with the heart rate the watch computed from its own onboard algorithm — within 6 beats per minute.
That third condition is the load-bearing one. The watch runs its own HR algorithm completely independently of our processing pipeline. Both are looking at the same underlying physiology. If they disagree by more than 6 BPM, something is wrong — motion contamination, poor sensor contact, a fundamentally noisy session. We don’t know exactly which. What we know is that any HRV computed from that session cannot be trusted.
So it doesn’t get stored.
The 6 BPM threshold isn’t a published clinical standard — it’s an engineering judgment call. It sits just above the commonly cited 5 BPM benchmark for wrist-worn optical HR monitors at rest, the zone where these sensors start drifting meaningfully under motion. We settled on 6 as the boundary between signal we can work with and signal we can’t. It’s held up in practice, but we call it what it is: a starting point, not a validated criterion.
What the gate is for
The point isn’t to be strict for its own sake. A number in a database carries implicit weight — someone (or some system) will eventually read it and treat it as meaningful. If the number doesn’t mean what it appears to mean, every downstream use of it inherits that problem.
The dashboard that visualizes longitudinal HRV over time doesn’t interpolate across gaps in the data — gaps are drawn as gaps, because a gap is the accurate representation of days where the gate didn’t pass. Baseline calculations only appear after at least seven days of qualifying data. Correlations between HRV and sleep or activity only appear after seven paired days with a minimum correlation coefficient of 0.30. Until those thresholds are met, the system returns “not enough data yet.”
The SpO2 sensor — which should measure blood oxygen — turned out to be too unreliable on the E3’s optical hardware to gate meaningfully. It’s shelved. The watch also ran its health data endpoint silently for about ten hours after a restart before a monitoring process caught that it had died without alerting anyone. Both are real problems, caught and handled rather than buried.
Where things stand
The pipeline is running. The first HRV reading the gate passed and stored: SDNN 12.6, RMSSD 21.7, computed heart rate 54.5 BPM. The watch and our DSP agreed within threshold, there were enough clean bursts, and the result went into the database. That number means something.
We have about 40 hours of data as of this writing. The baseline views, monthly trends, and correlations all currently return “insufficient data” — the correct answer given 40 hours of history. They’ll become available as the data accumulates.
Consumer wearables know what you don’t know too. They just don’t tell you.
A number in a database is supposed to mean something happened. If you can’t stand behind it, don’t put it there.
David Florence and Silas, his AI collaborator, write about what they’re actually building at masterofsomething.com.