docs(readme): redesenha o README com hero em SVG [skip ci]
This commit is contained in:
1 parent
f377fa63b4
commit
b7569e610f
2 files changed
+104
-89
No files matched your search
@@ -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)
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
**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.**
|
|
||||||
|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
[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>
|
||||||
@@ -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 |
Reference in new issue
Block a user