FlowRunner is a mobile running companion that estimates cadence from a phone's accelerometer and gives real-time haptic pacing feedback. Runners choose a target cadence and acceptable range; while a run is active, the app detects steps, calculates steps per minute, and signals when the runner should speed up or slow down.
The project is built with Expo SDK 54, React Native, TypeScript, and Expo Router. It also records run telemetry locally, exports sessions as JSON, and includes Python scripts for producing evaluation charts and metrics.
- Live step counting and cadence estimation from accelerometer readings
- Four-sample moving-average filtering and calibrated peak detection
- Configurable target cadence and feedback buffer
- 30-second, device-specific step-detection calibration
- Distinct haptic cues for speeding up and slowing down, with a five-second cooldown
- Run timer, step count, cadence, target range, and phone-position status
- Screen wake lock, portrait orientation lock, and automatic brightness dimming during a run
- Local JSON logging for accelerometer samples, cadence/haptic events, battery level, and step-to-haptic latency
- Run history plus individual and combined session export
- Python analysis tools for accuracy, battery, latency, and signal-filtering charts
![]() |
![]() |
![]() |
![]() |
![]() |
- Node.js and npm
- A physical iOS or Android device for accelerometer, vibration, and brightness testing
- Expo Go for basic testing, or an Expo development build for the full native haptic experience
- Xcode or Android Studio when building locally for a simulator/emulator or device
- Python 3 with
pandas,matplotlib, andnumpyto run the analysis scripts
FlowRunner can open on web or a simulator, but its core sensor and haptic behavior is intended for a physical phone. iOS AHAP patterns require a development build; Expo Go falls back to the standard vibration API.
Install the JavaScript dependencies:
npm installStart the Expo development server:
npm startThen scan the QR code with Expo Go, or use one of the terminal shortcuts shown by Expo. Other available commands are:
npm run android # Build and run the native Android app
npm run ios # Build and run the native iOS app
npm run web # Start the web version
npm run lint # Run Expo ESLint checksThe native build commands may install generated native projects and require the corresponding platform toolchain.
- Open Settings and choose a target cadence (120–200 steps per minute) and buffer range (±5–30 spm).
- Optionally run calibration. After a five-second preparation delay, run with the phone in your pocket for 30 seconds; FlowRunner uses the collected motion magnitude to set the peak-detection threshold.
- Return home and select Start Run.
- Keep the phone on your body while running. The app samples acceleration every 20 ms, updates cadence as steps are detected, and provides pacing cues outside the selected range.
- Hold Stop Run for 2.5 seconds. The session is saved locally and the results screen shows duration and average cadence.
- Export the run from the results screen, or open Analytics to inspect saved runs and export all sessions together.
Settings currently live in React state, so they reset to their defaults (150 spm, ±20 spm, threshold 1.2) after the app process restarts.
The run pipeline is:
Accelerometer (20 ms)
→ vector magnitude
→ 4-sample moving average
→ calibrated peak detector (300 ms cooldown)
→ rolling cadence from the last 4 steps
→ target-range comparison
→ haptic cue and session logs
The app records at most the first 500 raw/filtered accelerometer samples per run. Battery level is captured when a run begins and every five minutes afterward. Haptic feedback waits for at least three detected steps and is rate-limited to once every five seconds.
Each completed run is stored in the app's document directory as run-session-<UTC timestamp>.json:
{
"session_id": "run-2026-07-07-22-46-18-982",
"target_cadence": 150,
"battery_logs": [
{ "timestamp": 1783464378982, "battery": 0.84 }
],
"step_logs": [
{
"timestamp": 1783464379182,
"step_count": 4,
"live_cadence": 148,
"vibration_triggered": "none"
}
],
"latency_logs": [12],
"acc_logs": [
{ "time_ms": 1783464378982, "raw_acc": 1.45, "filtered_acc": 1.12 }
]
}Timestamps are Unix time in milliseconds, battery values range from 0 to 1, cadence is measured in steps per minute, and latency values are milliseconds. Export All Runs creates a JSON array containing every locally saved session.
Install the Python dependencies in your preferred virtual environment:
python3 -m pip install pandas matplotlib numpyGenerate the four basic charts from the included sample session:
python3 analysis_scripts/generate_charts.py \
--input analysis_scripts/sample.json \
--output charts \
--chart allGenerate annotated charts plus metrics.json and metrics_summary.txt:
python3 analysis_scripts/generate_charts_metrics.py \
--input analysis_scripts/sample.json \
--output charts \
--chart allValid --chart values are accuracy, battery, latency, filtered, and all. The accuracy scripts use a hard-coded ground-truth step count; update ACTUAL_STEPS_5_MIN in the selected script to match the manual or pedometer count for the evaluated run.
app/ Expo Router screens (home, running, results, analytics, settings)
contexts/ Shared cadence, buffer, and calibration settings
hooks/ Sensors, calibration, step detection, haptics, battery, and storage hooks
utils/ Filtering, peak detection, cadence, session save, and export helpers
assets/haptics/ Apple haptic pattern assets
analysis_scripts/ JSON analysis and chart-generation scripts
charts/ Generated evaluation charts and metric reports
- iOS development build: uses Core Haptics/AHAP patterns when available.
- iOS Expo Go: uses a standard vibration fallback because AHAP is unavailable.
- Android: uses timed vibration patterns and requires vibration permission (configured in
app.json). - Web: useful for inspecting UI, but does not represent the physical sensor/haptic workflow.




