Read this in Russian / по-русски.
An Android phone app that monitors up to three standard BLE heart rate sensors at once — chest straps, or a Wear OS watch emulating a strap (see the companion project HeartRate_Strap_Emulator). A test tool that makes it easy to compare the heart rate reported by different sensors side by side, live and on a common graph.
Chest strap #1 ---+
| BLE (GATT client, +-------------------------------+
Chest strap #2 ---+ HRS 0x180D / 0x2A37) -> | 3 live tiles + common history |
| | graph (zoom / scroll) |
Watch as strap ---+ +-------------------------------+
- Three source slots. Tap a tile to scan (filtered by the 16-bit Heart Rate Service UUID 0x180D in the ADV packet) and assign a sensor to the slot, or clear it. A sensor can occupy only one slot at a time.
- Foreground service (
HrCollectService) keeps the GATT connections alive with the app in the background; slot assignment survives app and process restarts (SharedPreferences). - Auto-reconnect (optional, 3 s retry) after connection drops.
- Live tiles: per-slot heart rate in the slot color; values older than 10 s are
shown as
--. - History graph (
HrGraphView): the three sources as colored lines on a fixed scale from 40 bpm to 10 bpm above the configured max HR, up to 4 hours of samples at 1 Hz. The max HR is drawn as a highlighted red "max HR" line, with the 10 bpm of headroom above it left clear so an overshoot past the maximum stays visible. The bpm axis is labeled at the zone boundaries plus 20-bpm fill marks (skipping any that would crowd a boundary). The time axis runs forward from the workout start (0:00 at Start). Pinch horizontally to change the time window (10 s … 3 h), drag to scroll back, double-tap to return to live follow mode. The Clear button wipes the history. - Measuring cursor: long-press the graph to summon a vertical cursor that follows the finger; the tiles above switch to the values under it and the status line shows its time position. The cursor is pinned to its moment in time (during a live recording it drifts left with the history); double-tap removes it.
- Workout tracking: Start/Stop frame a workout. Large readouts above the graph show the current heart rate averaged across the live sources ("average"), the elapsed workout time ("time") and the workout step count ("steps") received from the watch strap emulator over a custom GATT characteristic ("--" when only plain straps are connected).
- Heart rate zones: five translucent bands behind the graph (warm-up / fat burn / aerobic / anaerobic / maximum at 50/60/70/80/90 % of the maximum heart rate). The max HR and the bands' visibility are set in Settings.
- Statistics (options menu): workout start time and duration, min / average / max heart rate and the sample count for each source, plus the time each source spent in every heart rate zone.
- HRV / Poincaré (options menu): for every source that transmits RR intervals (Garmin HRM-Pro, Polar H10, …, detected automatically from the HR packets) a Poincaré plot (RR(n) vs RR(n+1)) overlays all such sources in their slot colors. Above it a per-source legend gives, on its own lines, RMSSD and SDNN — over the whole recording and a rolling 60 s window — plus a colored switch that shows or hides that source's points individually. Physiologically impossible intervals and ectopic beats are filtered out of the metrics first. Each source also carries a rhythm screen line — regular / irregular beats / possible AFib, in green / orange / red — derived from the beat-to-beat irregularity (the Poincaré SD1 / SD2 axes, the coefficient of variation and the share of beats jumping more than 10 % of the mean). It is a rough screen the way a cuff AFib monitor works, not a diagnosis. Works live and on a loaded recording.
- Battery (options menu): a table of every assigned sensor with its connection
state and battery level (%), read once per connection from the standard Battery
Service (0x180F / 0x2A19). The level is color-coded (green > 50 %, orange 20–50 %,
red < 20 %); sensors that do not expose the Battery Service show
--. - Recovery drops (options menu, toggle): marks heart-rate recovery — the steepest one-minute fall (i.e. HRR) after each effort above the fat burn zone — and writes that fall in bpm/min next to the mark. The HR is median-smoothed and a drop is kept only when every source present at that moment confirms the same fall, so single-sensor glitches are ignored. Turning it off erases the marks. Works live and on a loaded recording.
- Load segment analysis (options menu → Analyze load): drag out a band over a stretch of constant effort and its HR-response parameters are written right on the band — the steady-state (plateau) HR, the on-kinetics time constant (tau) of the rise and the cardiovascular drift over the plateau. Drag either edge (or the whole band) to adjust; the readout updates live. Set the power via "Analysis power (W)" in the menu to also get efficiency (W per bpm, bpm per 100 W). Uses the source with the most samples in the band; a two-finger pinch still zooms.
- Line styles: one dialog sets the color (8-swatch palette) and width (1–8 dp) of all three source lines, persisted per slot; open it from the options menu or by long-pressing a tile.
- CSV export: "Save data" in the options menu writes the recorded data to
Downloads/HR_YYYY-MM-DD_HH-MM-SS.csv(long format: wall-clock time, series, name, value; series 1–3 are the HR sources in bpm, series 0 is the cumulative workout step count from the watch; series 11–13 are the RR intervals of the sources in ms, for HRV). - CSV import: "Load data" opens a saved CSV back onto the graph for review — the time axis freezes at the end of the recording, the statistics (min / avg / max, time in zones) are recomputed from the file, the tiles show the sensor names recorded in it, and the status line shows "Viewing saved data". Clear or starting a new workout (which wipes the graph and restores the live sensor names) leaves review mode.
- Send by email: "Send by email" shares the current graph history as a CSV attachment through the system chooser (email, messengers, etc.).
- HRV trend panel: an optional RMSSD graph in its own panel below the main graph (dashed line per RR-capable source; shares the time axis and the cursor). The panel's ms axis is auto-scaled — the baseline stays at 0 and the top fits the largest value in view, labeled on the left. RMSSD is computed over a rolling 30 s window; toggle it in Settings. When off, the main graph uses the full height.
- Settings (options menu): keep screen on, auto-reconnect, heart rate zones on/off, HRV trend on/off, maximum heart rate, and the load-analysis source (Auto = the source with the most data, or a fixed slot).
- Android 8.0+ (minSdk 26, targetSdk 36); Bluetooth LE required.
- Runtime permissions: Bluetooth scan/connect (Android 12+) or location (Android 11-), notifications (Android 13+).
Open the folder in Android Studio (Gradle 9.4.1 / AGP 9.2.1 / compileSdk 36.1) or from the command line:
$env:JAVA_HOME = "C:\Program Files\Android\Android Studio\jbr"
.\gradlew.bat :mobile:assembleDebugAPK: mobile\build\outputs\apk\debug\mobile-debug.apk.
adb install -r mobile\build\outputs\apk\debug\mobile-debug.apk- Open "HR Monitor", grant the permissions.
- Tap a tile, pick a heart rate sensor from the scan list (repeat for up to three). A long press on a tile opens the line styles dialog (colors, widths).
- Tap Start to begin a workout — the service connects to the assigned sensors and the graph starts filling; the same button turns into Stop. The large readouts show the cross-source average and the workout time; Clear wipes the graph history.
- The action bar menu holds Statistics (duration, min/avg/max per source), HRV / Poincaré (per-source RMSSD/SDNN, point toggles and a rhythm screen), Battery (per-sensor charge level), Save data (CSV export to Downloads, date and time in the file name), Load data (a saved CSV back onto the graph), Send by email (the current data as a CSV attachment), Recovery drops (toggle the drop marks), Analyze load (drag out a constant-effort segment), Line styles (one dialog for all sources) and Settings (keep screen on, auto-reconnect).
- Any sensor implementing the standard Heart Rate Profile works; the HR Measurement characteristic (0x2A37) is consumed, and the Battery Level (0x2A19) is read once per connection when the sensor exposes the Battery Service.
- The scan list shows the advertised device name when present, otherwise the MAC address. Sensors advertising with a resolvable private address (e.g. Android/Wear devices emulating a strap) change address between sessions — reassign the slot if the connection cannot be restored.
Split out of the HardRate_Translator repository (which contained the phone and the
watch app together) after the two apps became independent.