diff --git a/README.md b/README.md index c0e6c2d..2012f76 100644 --- a/README.md +++ b/README.md @@ -1,104 +1,63 @@
-FightRate logo +FightRate: heart-rate capture for fight training from a BLE chest strap, with a live BPM readout over the BASE, PRESSÃO and REDLINE zones -# FightRate (HealthTracking) +![React Native 0.86.0](https://img.shields.io/badge/React_Native-0.86.0-E23B2E?style=flat-square) +![Android minSdk 24](https://img.shields.io/badge/Android-minSdk_24-E23B2E?style=flat-square) +![Node ≥ 22.11](https://img.shields.io/badge/Node-%E2%89%A5_22.11-E23B2E?style=flat-square) +![License: proprietary](https://img.shields.io/badge/license-proprietary-E23B2E?style=flat-square) +![Status: work in progress](https://img.shields.io/badge/status-work_in_progress-E23B2E?style=flat-square) -**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.** - -![React Native](https://img.shields.io/badge/React_Native-0.86.0-informational) -![Android](https://img.shields.io/badge/Android-minSdk_24-informational) -![Node](https://img.shields.io/badge/Node-%E2%89%A5_22.11-informational) - -[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/). + +
+ +Made by Evandro Fonseca Junior · git.evandro.dev.br + +
diff --git a/assets/readme/hero.svg b/assets/readme/hero.svg new file mode 100644 index 0000000..6a1f24a --- /dev/null +++ b/assets/readme/hero.svg @@ -0,0 +1,39 @@ + + FightRate + FightRate records heart rate from a BLE chest strap during fight training. On the right, a live readout of 160 BPM in the PRESSÃO zone over a session trace that crosses the 150 and 170 BPM thresholds, decoded from the Heart Rate Measurement packet 16 A0 6C 01. + + + + + HealthTracking · Android + FightRate + Heart-rate capture for fight training + from a BLE chest strap. Real data only. + React Native 0.86 · Kotlin · SQLite + + + + + + + LIVE + 160 + BPM + + PRESSÃO + + + + + + 170 + 150 + + + + + + 2A37 · 16 A0 6C 01 → 160 bpm + +