The hardest bugs in a camera heart-rate app are not always in the math. Some of them live in the gaps between user behavior and algorithm timing. A user lifts the finger for half a second. The camera warms up. A result appears, then disappears. The UI says measuring while the signal is clearly gone.

The fix was to stop thinking of measurement as a button action. It is a state machine.
This diagram could not be rendered. Its source is still available below.
View diagram source
stateDiagram-v2
[*] --> initial
initial --> preparing: startDetection
preparing --> detectingFinger: camera ready
detectingFinger --> measuring: good coverage stays stable
measuring --> calculating: duration and quality pass
calculating --> completed: result accepted
calculating --> failed: result rejected
measuring --> detectingFinger: finger lost beyond grace period
failed --> initial: reset
completed --> initial: stop or new measurement
Preparing is a real state
Camera permission, session setup, torch warm-up, and exposure settling are not instant. If the UI jumps straight into measurement, the first seconds are polluted by setup behavior. MonoPump keeps a preparing phase, then moves into finger detection only after the camera path is ready.
Finger detection needs patience
A single good frame does not mean the finger is placed well. MonoPump waits for good coverage to remain stable before starting the timer. This protects the first part of the signal window, where early noise otherwise creates bad peaks later.
This diagram could not be rendered. Its source is still available below.
View diagram source
flowchart TD
A[Coverage becomes good] --> B[Start stability timer]
B --> C{Still good after delay?}
C -- no --> D[Keep guiding]
C -- yes --> E[Reset analyzer and start measuring]
Resetting the analyzer at measurement start is intentional. Finger search frames are useful for UX, but they are not clean measurement samples.
Grace period beats panic resets
The first version reset too quickly when the finger moved. It was technically correct and practically annoying. In the real world, people adjust their grip. A short grace period lets the app tolerate a brief bad patch without pretending the entire attempt is invalid.
This diagram could not be rendered. Its source is still available below.
View diagram source
sequenceDiagram
participant F as Frame analyzer
participant D as Detector
participant UI as UI
F-->>D: coverage becomes bad
D->>D: start grace timer
D-->>UI: keep measurement context
F-->>D: coverage recovers
D->>D: cancel grace timer
D-->>UI: continue measuring
Extending measurement is better than failing fast
If the planned duration is complete but confidence or quality is weak, MonoPump can extend measurement up to a maximum duration. The user does not have to restart immediately, and the app still has a hard upper bound.
This diagram could not be rendered. Its source is still available below.
View diagram source
flowchart LR
A[Planned duration reached] --> B{Quality and confidence pass?}
B -- yes --> C[Finish]
B -- no --> D{Below maximum duration?}
D -- yes --> E[Extend collection]
D -- no --> F[Fail with clear reason]
A measurement state machine decides what the user sees, when haptics fire, when the torch turns off, when the result is saved, and when the app should be honest about failure. Never let a raw frame directly control the user experience. Frames feed state. State drives the app.