8.0 KiB
FightRate (HealthTracking)
Continuous heart-rate capture for combat-sport training from a BLE chest strap — a bare React Native app (no Expo) with a native Android foreground service, local SQLite storage and metrics computed only from real timestamped BPM.
Note
Project status: work in progress (
package.jsonis0.0.1, AndroidversionName1.0). TheapplicationIdiscom.healthtracking.fightrate. The latest commits (2026-06-16) ship the "Instrumento Forte" UI refinement (v2): tabular JetBrains Mono numerics, animated live indicators and a zoomable chart. The record in.impeccable.mdlistsnpm run typecheckwith 0 errors andnpm test -- --runInBandwith 74/74 passing for that release.
Important
Real data only, Android only. Every number on screen is computed from BPM samples received from a real sensor; with no samples the app shows an empty state instead of inventing a peak, zone, load or HRV. The BLE capture module is written in Kotlin (
android/…/capture/); theios/project has no native capture module.
Features
| Tab | What it does |
|---|---|
| Capture | Live BPM, average / peak / sample-count tiles, BLE, battery and background status, and the training chart. Start/stop marks a training window inside the continuous stream — monitoring keeps storing samples either way |
| History | Saved training windows and continuous-usage blocks (split by gaps), each with average / peak / min BPM; open one to inspect the timestamped data; custom time range |
| Sensor | BLE scan, device list, connect, connection phase, signal (RSSI), battery level, BLE services/characteristics diagnostics and a shortcut to Android battery-optimization settings |
| Insights | Calculation matrix: current/average BPM, range, variability, time in zones, load, heart-rate drift, post-session recovery, HRmax/HRR (marked as needing a profile) and HRV (only when the strap sends RR intervals) |
- Zoomable chart (
TrainingChart): pinch zoom, pan and a tap-to-pin marker, with tick granularity that follows the visible span (half-second up to one-minute steps). - Zones by fixed BPM thresholds:
BASE <150,PRESSÃO 150–169,REDLINE 170+. - Samples outside 30–240 bpm, or reported without skin contact, are discarded from metrics; a sample is considered fresh for 12 s.
- UI copy is PT-BR; code identifiers are English.
Warning
Grant the permissions and exempt the app from battery optimization. The manifest requests Bluetooth scan/connect, fine location, notifications and a foreground service of type
connectedDevice. Capture runs in that service; the Bateria button on the Sensor tab opens Android's battery-optimization settings (REQUEST_IGNORE_BATTERY_OPTIMIZATIONSis declared) so the background capture isn't throttled.
How it works
flowchart LR
A["Chest strap (BLE)<br/>Heart Rate 0x180D · 2A37"] --> B["FightRateCaptureService<br/>Kotlin foreground service"]
B --> C["FightRateCaptureStore<br/>SQLite"]
B --> D["FightRateCaptureModule<br/>native module · events"]
D --> E["useHeartRateMonitor<br/>React hook"]
C -. "samples / sessions" .-> D
E --> F["trainingMetrics · continuousUsage<br/>chartViewport"]
F --> G["Capture · History · Sensor · Insights"]
Technical details
| Piece | Where | Notes |
|---|---|---|
| BLE client | FightRateCaptureService.kt |
Scans, connects by known device id, enables notifications on Heart Rate Measurement 2A37 (CCCD 2902), reads Battery Level 2A19 from service 180F, reads RSSI |
| Reconnect | same file | START_STICKY service; automatic reconnect with backoff 2 s → 5 s → 10 s → 30 s unless stopped manually; restores the last device after a restart; after 12 s without a valid sample it reports the stale state and waits for new real data |
| Parser | HeartRateMeasurementParser.kt, src/ble/heartRateParser.ts |
Flags: UInt8/UInt16 BPM, contact detection, RR intervals (converted from 1/1024 s to ms) |
| Storage | FightRateCaptureStore.kt |
fightrate_rn_capture.db (version 2): workout_sessions (training windows with device id/name), continuous_heart_rate_samples (timestamp, BPM, contact, RR intervals, device) and the legacy heart_rate_samples, migrated on upgrade |
| Bridge | FightRateCaptureModule.kt, src/native/FightRateCaptureNative.ts |
Start/stop monitoring and capture window, connect, snapshot, session and continuous samples, battery-optimization settings; service broadcasts become JS events |
| Domain (pure TS) | src/training/ |
Live metrics, variability, drift, recovery, zones, insight dashboard, continuous-usage blocks (gap of 5 min starts a new block), chart viewport math |
| UI | src/screens/, src/components/, src/theme/ |
Tab shell in src/app/FightRateApp.tsx (state-driven; no navigation library), Reanimated 4 + Worklets animations, design tokens |
Design references: FIGHTRATE_DESIGN_SYSTEM.md (direction 01 — Corner
Instrument, the official one) and, for history only,
FIGHTRATE_COMBAT_LAB_DESIGN_SYSTEM.md (deprecated direction — do
not reuse). Specs and plans live in docs/superpowers/.
Known limitations
- Android only: capture exists only in the Kotlin native module; there is no equivalent on iOS.
- No athlete profile yet: zones use the fixed BPM thresholds above;
%HRmaxzones, Karvonen and full TRIMP are shown as requiring a profile — the app doesn't invent HRmax or resting heart rate. - HRV needs RR intervals: without them the dashboard says so instead of estimating.
- No navigation library: tabs are driven by component state.
- Release signing: the
releasebuild type is configured with the debug keystore (android/app/build.gradle); generate your own keystore before distributing. - Lint: at the last record in
.impeccable.md,npm run lintfails on a pre-existingjest/globalsconfiguration error (not blocking typecheck or tests).
Run locally
Requirements: Node ≥ 22.11 (engines in package.json), the Android SDK and a JDK. The project doesn't use Expo.
git clone https://git.evandro.dev.br/evandro/HealthTracking.git
cd HealthTracking
npm install
npm run typecheck
npm test -- --runInBand
npm run lint
npm run start # Metro
npm run android # run on a device/emulator
Debug APK:
cd android
./gradlew assembleDebug
# output: android/app/build/outputs/apk/debug/app-debug.apk
If Gradle reports No Java compiler found, point JAVA_HOME to the JBR bundled with Android Studio:
export JAVA_HOME="<Android Studio install dir>/jbr"
export PATH="$JAVA_HOME/bin:$PATH"
If React Native autolinking keeps a stale package after renaming Android package paths, clean the generated Gradle caches before rebuilding:
rm -rf android/.gradle android/app/build android/build .gradle
cd android && ./gradlew assembleDebug
Notices
- The literature notes in
docs/data/pesquisa.mdstate that population tables and formula-based estimates are not a substitute for measured testing. - Built on React Native, Reanimated, Gesture Handler, react-native-svg, Lucide icons and JetBrains Mono.