All notes

【TECH】The state machine behind a calm heart-rate measurement

Why MonoPump treats measurement as a staged state machine instead of a single start-and-stop camera action.

state machineUXSwiftUIPPG
【TECH】The state machine behind a calm heart-rate measurement

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.

A notebook state-machine sketch beside a phone running a pulse measurement
The user experience is driven by stable states, not by single raw frames.

The fix was to stop thinking of measurement as a button action. It is a state machine.

State diagram Preparing diagram
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.

Flow diagram Preparing diagram
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.

Sequence diagram Preparing diagram
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.

Flow diagram Preparing diagram
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.

From MonoWare

MonoPump has more context behind this note.

Open the product site or keep reading notes filtered to MonoPump.

Newsletter

Privacy and product engineering notes, sent occasionally.