-
+
-# 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.**
-
-
-
-
-
-[Features](#features) · [How it works](#how-it-works) · [Run locally](#run-locally) · [Known limitations](#known-limitations)
+[Design system](FIGHTRATE_DESIGN_SYSTEM.md) · [Literature notes](docs/data/pesquisa.md) · [Issues](https://git.evandro.dev.br/evandro/HealthTracking/issues)
-> [!NOTE]
-> **Project status:** work in progress (`package.json` is `0.0.1`, Android `versionName` `1.0`). The
-> `applicationId` is `com.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.md`](.impeccable.md) lists `npm run typecheck` with 0
-> errors and `npm test -- --runInBand` with 74/74 passing for that release.
+The BLE parser decoding one Heart Rate Measurement packet (flags `0x16`: skin contact detected, one RR interval). It is plain TypeScript, so Node runs it straight from a fresh clone, no install:
-> [!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/`); the `ios/` project has no
-> native capture module.
+```console
+$ node --experimental-strip-types --no-warnings --input-type=module -e "import { parseHeartRateMeasurement as parse } from './src/ble/heartRateParser.ts'; console.log(parse([0x16, 0xA0, 0x6C, 0x01]))"
+{
+ bpm: 160,
+ contactSupported: true,
+ contactDetected: true,
+ energyExpended: null,
+ rrIntervalsMs: [ 355.46875 ]
+}
+```
-## Features
+## What it is
+
+FightRate (repository `HealthTracking`) is an Android app that records heart rate continuously from a Bluetooth Low Energy chest strap during combat-sport training. It is a bare React Native app (no Expo): a Kotlin foreground service owns the BLE connection and stores every timestamped BPM sample in SQLite, and the React Native UI reads those samples. UI copy is PT-BR; code identifiers are English.
| 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 |
+| **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) |
+| **Insights** | 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.
+## Why use it
-> [!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_OPTIMIZATIONS` is declared) so the background capture isn't
-> throttled.
+- **Real data only.** Every number on screen is computed from samples a real sensor sent. With no samples the app shows an empty state instead of inventing a peak, zone, load or HRV. Samples outside 30–240 bpm, or reported without skin contact, are discarded.
+- **Capture survives the screen turning off.** It runs in a foreground service of type `connectedDevice`, restarts as `START_STICKY`, reconnects with backoff (2 s → 5 s → 10 s → 30 s) and restores the last device after a restart.
+- **Built for rounds, not steps.** Fixed zones `BASE <150`, `PRESSÃO 150–169`, `REDLINE 170+`, and a zoomable chart (pinch zoom, pan, tap-to-pin) whose ticks follow the visible span, from half-second to one-minute steps.
+
+Do not use it if you need iOS (there is no native capture module there), zones from your own HRmax or resting heart rate (there is no athlete profile yet), or a signed build you can distribute (see [Project status](#project-status)).
## How it works
-```mermaid
-flowchart LR
- A["Chest strap (BLE) Heart Rate 0x180D · 2A37"] --> B["FightRateCaptureService Kotlin foreground service"]
- B --> C["FightRateCaptureStore SQLite"]
- B --> D["FightRateCaptureModule native module · events"]
- D --> E["useHeartRateMonitor React hook"]
- C -. "samples / sessions" .-> D
- E --> F["trainingMetrics · continuousUsage chartViewport"]
- F --> G["Capture · History · Sensor · Insights"]
-```
+1. **BLE client**: `FightRateCaptureService.kt` scans, connects by known device id, subscribes to Heart Rate Measurement `2A37` (service `180D`), reads Battery Level `2A19` and RSSI. After 12 s without a valid sample it reports a stale state and waits for new real data.
+2. **Parser**: `HeartRateMeasurementParser.kt` and `src/ble/heartRateParser.ts` read UInt8/UInt16 BPM, skin contact and RR intervals (converted from 1/1024 s to ms).
+3. **Storage**: `FightRateCaptureStore.kt` writes `fightrate_rn_capture.db` (schema version 2) with training windows and continuous samples (timestamp, BPM, contact, RR intervals, device).
+4. **Bridge**: `FightRateCaptureModule.kt` and `src/native/FightRateCaptureNative.ts` expose start/stop, connect, snapshots and samples, and turn service broadcasts into JS events.
+5. **Domain**: pure TypeScript in `src/training/` computes metrics, zones, drift, recovery, usage blocks (a 5-minute gap starts a new block) and the chart viewport.
+6. **UI**: a state-driven tab shell in `src/app/FightRateApp.tsx` (no navigation library), Reanimated 4 + Worklets animations and design tokens in `src/theme/`.
-
-Technical details
+Design direction: [`FIGHTRATE_DESIGN_SYSTEM.md`](FIGHTRATE_DESIGN_SYSTEM.md) (**01 — Corner Instrument**, the official one). [`FIGHTRATE_COMBAT_LAB_DESIGN_SYSTEM.md`](FIGHTRATE_COMBAT_LAB_DESIGN_SYSTEM.md) is a deprecated direction kept for history. Specs and plans live in [`docs/superpowers/`](docs/superpowers).
-| 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 |
+## Quick start
-Design references: [`FIGHTRATE_DESIGN_SYSTEM.md`](FIGHTRATE_DESIGN_SYSTEM.md) (direction **01 — Corner
-Instrument**, the official one) and, for history only,
-[`FIGHTRATE_COMBAT_LAB_DESIGN_SYSTEM.md`](FIGHTRATE_COMBAT_LAB_DESIGN_SYSTEM.md) (deprecated direction — do
-not reuse). Specs and plans live in [`docs/superpowers/`](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; `%HRmax` zones, 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 `release` build 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`](.impeccable.md), `npm run lint` fails on a pre-existing
- `jest/globals` configuration 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.
+Requirements: Node ≥ 22.11 (`engines` in `package.json`), the Android SDK and a JDK.
```bash
git clone https://git.evandro.dev.br/evandro/HealthTracking.git
@@ -106,7 +65,6 @@ cd HealthTracking
npm install
npm run typecheck
npm test -- --runInBand
-npm run lint
```
```bash
@@ -122,6 +80,9 @@ cd android
# output: android/app/build/outputs/apk/debug/app-debug.apk
```
+> [!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`. The **Bateria** button on the Sensor tab opens Android's battery-optimization settings (`REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` is declared) so background capture isn't throttled.
+
If Gradle reports `No Java compiler found`, point `JAVA_HOME` to the JBR bundled with Android Studio:
```bash
@@ -129,19 +90,34 @@ export JAVA_HOME="/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:
+If React Native autolinking keeps a stale package after renaming Android package paths, clean the generated Gradle caches before rebuilding:
```bash
rm -rf android/.gradle android/app/build android/build .gradle
cd android && ./gradlew assembleDebug
```
-## Notices
+## Project status
-- The literature notes in [`docs/data/pesquisa.md`](docs/data/pesquisa.md) state that population tables and
- formula-based estimates are not a substitute for measured testing.
-- Built on [React Native](https://reactnative.dev/), [Reanimated](https://docs.swmansion.com/react-native-reanimated/),
- [Gesture Handler](https://docs.swmansion.com/react-native-gesture-handler/),
- [react-native-svg](https://github.com/software-mansion/react-native-svg),
- [Lucide](https://lucide.dev/) icons and [JetBrains Mono](https://www.jetbrains.com/lp/mono/).
+Work in progress: `package.json` is `0.0.1`, Android `versionName` is `1.0`, `applicationId` is `com.healthtracking.fightrate`. The last feature work (2026-06-16) shipped the "Instrumento Forte" UI refinement; its record in [`.impeccable.md`](.impeccable.md) lists `npm run typecheck` with 0 errors, `npm test -- --runInBand` with 74/74 passing, and `npm run lint` failing on a pre-existing `jest/globals` configuration error.
+
+Known limits:
+
+- **Android only**: capture exists only in the Kotlin native module; the `ios/` project has no equivalent.
+- **No athlete profile yet**: zones use the fixed thresholds above; `%HRmax` zones, Karvonen and full TRIMP are shown as needing 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.
+- **Release signing**: the `release` build type uses the **debug keystore** (`android/app/build.gradle`); generate your own keystore before distributing.
+
+The literature notes in [`docs/data/pesquisa.md`](docs/data/pesquisa.md) state that population tables and formula-based estimates are not a substitute for measured testing.
+
+## License
+
+Proprietary license in [`LICENSE`](LICENSE): personal and study use only, with visible credit to the author. Redistribution, resale, sublicensing and commercial use need written authorization; authorized commercial use is by contract, with 50% of gross revenue paid to the author. Provided "as is", without warranty.
+
+Built on [React Native](https://reactnative.dev/), [Reanimated](https://docs.swmansion.com/react-native-reanimated/), [Gesture Handler](https://docs.swmansion.com/react-native-gesture-handler/), [react-native-svg](https://github.com/software-mansion/react-native-svg), [Lucide](https://lucide.dev/) icons and [JetBrains Mono](https://www.jetbrains.com/lp/mono/).
+
+