docs(readme): redesenha o README com hero em SVG [skip ci]

This commit is contained in:
evandro committed 2026-10-24 04:58:04 -03:00
1 parent f377fa63b4
commit b7569e610f
2 files changed
+104 -89

No files matched your search

+65 -89
View File
@@ -1,104 +1,63 @@
<div align="center"> <div align="center">
<img src="src/assets/fightrate-logo.png" width="110" alt="FightRate logo"> <img src="assets/readme/hero.svg" width="100%" alt="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 [Design system](FIGHTRATE_DESIGN_SYSTEM.md) · [Literature notes](docs/data/pesquisa.md) · [Issues](https://git.evandro.dev.br/evandro/HealthTracking/issues)
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)
</div> </div>
> [!NOTE] 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:
> **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.
> [!IMPORTANT] ```console
> **Real data only, Android only.** Every number on screen is computed from BPM samples received from a $ 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]))"
> 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 bpm: 160,
> native capture module. 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 | | 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 | | **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 | | **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 ## Why use it
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] - **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.
> **Grant the permissions and exempt the app from battery optimization.** The manifest requests Bluetooth - **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.
> scan/connect, fine location, notifications and a foreground service of type `connectedDevice`. Capture - **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.
> 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 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)).
> throttled.
## How it works ## How it works
```mermaid 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.
flowchart LR 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).
A["Chest strap (BLE)<br/>Heart Rate 0x180D · 2A37"] --> B["FightRateCaptureService<br/>Kotlin foreground service"] 3. **Storage**: `FightRateCaptureStore.kt` writes `fightrate_rn_capture.db` (schema version 2) with training windows and continuous samples (timestamp, BPM, contact, RR intervals, device).
B --> C["FightRateCaptureStore<br/>SQLite"] 4. **Bridge**: `FightRateCaptureModule.kt` and `src/native/FightRateCaptureNative.ts` expose start/stop, connect, snapshots and samples, and turn service broadcasts into JS events.
B --> D["FightRateCaptureModule<br/>native module · 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.
D --> E["useHeartRateMonitor<br/>React hook"] 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/`.
C -. "samples / sessions" .-> D
E --> F["trainingMetrics · continuousUsage<br/>chartViewport"]
F --> G["Capture · History · Sensor · Insights"]
```
<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).
<summary>Technical details</summary>
| Piece | Where | Notes | ## Quick start
|---|---|---|
| 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`](FIGHTRATE_DESIGN_SYSTEM.md) (direction **01 — Corner Requirements: Node ≥ 22.11 (`engines` in `package.json`), the Android SDK and a JDK.
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).
</details>
## 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.
```bash ```bash
git clone https://git.evandro.dev.br/evandro/HealthTracking.git git clone https://git.evandro.dev.br/evandro/HealthTracking.git
@@ -106,7 +65,6 @@ cd HealthTracking
npm install npm install
npm run typecheck npm run typecheck
npm test -- --runInBand npm test -- --runInBand
npm run lint
``` ```
```bash ```bash
@@ -122,6 +80,9 @@ cd android
# output: android/app/build/outputs/apk/debug/app-debug.apk # 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: If Gradle reports `No Java compiler found`, point `JAVA_HOME` to the JBR bundled with Android Studio:
```bash ```bash
@@ -129,19 +90,34 @@ export JAVA_HOME="<Android Studio install dir>/jbr"
export PATH="$JAVA_HOME/bin:$PATH" export PATH="$JAVA_HOME/bin:$PATH"
``` ```
If React Native autolinking keeps a stale package after renaming Android package paths, clean the generated If React Native autolinking keeps a stale package after renaming Android package paths, clean the generated Gradle caches before rebuilding:
Gradle caches before rebuilding:
```bash ```bash
rm -rf android/.gradle android/app/build android/build .gradle rm -rf android/.gradle android/app/build android/build .gradle
cd android && ./gradlew assembleDebug cd android && ./gradlew assembleDebug
``` ```
## Notices ## Project status
- The literature notes in [`docs/data/pesquisa.md`](docs/data/pesquisa.md) state that population tables and 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.
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/), Known limits:
[Gesture Handler](https://docs.swmansion.com/react-native-gesture-handler/),
[react-native-svg](https://github.com/software-mansion/react-native-svg), - **Android only**: capture exists only in the Kotlin native module; the `ios/` project has no equivalent.
[Lucide](https://lucide.dev/) icons and [JetBrains Mono](https://www.jetbrains.com/lp/mono/). - **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/).
<div align="center">
<sub>Made by <a href="https://evandro.dev.br">Evandro Fonseca Junior</a> · <a href="https://git.evandro.dev.br/evandro">git.evandro.dev.br</a></sub>
</div>
+39
View File
@@ -0,0 +1,39 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="420" viewBox="0 0 1200 420" role="img" aria-labelledby="title desc">
<title id="title">FightRate</title>
<desc id="desc">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.</desc>
<rect width="1200" height="420" rx="24" fill="#070605"/>
<g id="title-block" font-family="system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif">
<text x="64" y="104" font-family="ui-monospace, 'SF Mono', Consolas, monospace" font-size="20" fill="#B9A98E">HealthTracking · Android</text>
<text x="64" y="186" font-size="72" font-weight="700" fill="#F8EAD8">FightRate</text>
<text x="64" y="246" font-size="28" fill="#F8EAD8">Heart-rate capture for fight training</text>
<text x="64" y="282" font-size="28" fill="#F8EAD8">from a BLE chest strap. Real data only.</text>
<text x="64" y="344" font-family="ui-monospace, 'SF Mono', Consolas, monospace" font-size="20" fill="#B9A98E">React Native 0.86 · Kotlin · SQLite</text>
</g>
<g id="project-proof" font-family="ui-monospace, 'SF Mono', Consolas, monospace">
<rect x="632" y="40" width="512" height="340" rx="16" fill="#12100C" stroke="#392D1E" stroke-width="2"/>
<circle cx="672" cy="80" r="7" fill="#E23B2E"/>
<text x="688" y="86" font-size="18" fill="#B9A98E">LIVE</text>
<text x="664" y="150" font-size="60" font-weight="700" fill="#F8EAD8">160</text>
<text x="784" y="150" font-size="20" fill="#B9A98E">BPM</text>
<rect x="976" y="62" width="136" height="36" rx="18" fill="#FFB43D"/>
<text x="1044" y="87" font-size="18" font-weight="700" fill="#070605" text-anchor="middle">PRESSÃO</text>
<g id="zones" font-size="18">
<line x1="664" y1="216" x2="1068" y2="216" stroke="#E23B2E" stroke-width="2" stroke-dasharray="6 6"/>
<line x1="664" y1="256" x2="1068" y2="256" stroke="#FFB43D" stroke-width="2" stroke-dasharray="6 6"/>
<line x1="664" y1="316" x2="1068" y2="316" stroke="#392D1E" stroke-width="2"/>
<text x="1112" y="222" fill="#E23B2E" text-anchor="end">170</text>
<text x="1112" y="262" fill="#FFB43D" text-anchor="end">150</text>
</g>
<polyline fill="none" stroke="#F8EAD8" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"
points="664,304 700,294 736,276 772,252 808,258 844,234 880,208 916,198 952,224 988,246 1024,240 1060,236"/>
<circle cx="1060" cy="236" r="7" fill="#FFB43D"/>
<text x="664" y="354" font-size="20" fill="#B9A98E">2A37 · 16 A0 6C 01 → 160 bpm</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB