React Native BLE心率监测器完整教程:蓝牙设备集成
概述
蓝牙低功耗(BLE)心率监测器是最常见的健康穿戴设备。本文将教你使用React Native和react-native-ble-plx库连接这些设备。
支持的品牌:
- Polar (H7/H10)
- Garmin (心率带)
- Fitbit (部分型号)
- 通用BLE心率带
技术栈
code
React Native App
├── react-native-ble-plx # BLE通信
├── react-native-permissions # 权限管理
├── react-native-chart-kit # 图表
└── @react-native-async-storage/async-storage # 数据存储
Code collapsed
环境设置
安装依赖
code
# BLE核心库
npm install react-native-ble-plx
# 权限管理
npm install react-native-permissions
# 图表
npm install react-native-chart-kit
npm install react-native-svg
# iOS pod install
cd ios && pod install
Code collapsed
Android配置
code
<!-- android/app/src/main/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.heartmonitor">
<!-- BLE权限 -->
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
<application
...>
</application>
</manifest>
Code collapsed
iOS配置
code
<!-- ios/YourApp/Info.plist -->
<key>NSBluetoothAlwaysUsageDescription</key>
<string>需要蓝牙权限来连接心率监测设备</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>需要蓝牙权限来连接心率监测设备</string>
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
</array>
Code collapsed
BLE服务配置
Heart Rate Service UUID
code
// constants/HeartRateService.ts
/**
* 蓝牙心率服务标准UUID
* 参考: Bluetooth Heart Rate Profile
*/
export const HEART_RATE_SERVICE_UUID = '0000180d-0000-1000-800000805f9b34fb';
export const HEART_RATE_MEASUREMENT_UUID = '00002a37-0000-1000-800000805f9b34fb';
export const BODY_SENSOR_LOCATION_UUID = '00002a38-0000-1000-800000805f9b34fb';
export const HEART_RATE_CONTROL_POINT_UUID = '00002a39-0000-1000-800000805f9b34fb';
/**
* 身体传感器位置
*/
export enum BodySensorLocation {
Other = 0,
Chest = 1,
Wrist = 2,
Finger = 3,
Hand = 4,
EarLobe = 5,
Foot = 6
}
export const BODY_SENSOR_LOCATION_NAMES = {
[BodySensorLocation.Other]: '其他',
[BodySensorLocation.Chest]: '胸部',
[BodySensorLocation.Wrist]: '手腕',
[BodySensorLocation.Finger]: '手指',
[BodySensorLocation.Hand]: '手部',
[BodySensorLocation.EarLobe]: '耳垂',
[BodySensorLocation.Foot]: '脚部'
};
Code collapsed
BLE连接管理
1. 设备扫描和连接
code
// services/HeartRateDeviceManager.ts
import { BleManager, Device } from 'react-native-ble-plx';
import { PermissionsAndroid, Platform } from 'react-native';
export class HeartRateDeviceManager {
private device: Device | null = null;
private connection: any = null;
private notificationListener: any = null;
/**
* 初始化并请求权限
*/
async initialize(): Promise<boolean> {
try {
if (Platform.OS === 'android') {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION
);
if (!granted) {
console.error('位置权限被拒绝');
return false;
}
}
// 启动BleManager
await BleManager.start({ showAlert: false });
console.log('BleManager已启动');
return true;
} catch (error) {
console.error('初始化失败:', error);
return false;
}
}
/**
* 扫描并连接心率设备
*/
async scanAndConnect(): Promise<boolean> {
try {
console.log('开始扫描心率设备...');
// 扫描设备(扫描10秒)
const devices = await BleManager.scanDeviceServices(
[HEART_RATE_SERVICE_UUID],
10 // 扫描10秒
);
console.log(`找到${devices.length}个设备`);
if (devices.length === 0) {
throw new Error('未找到心率设备');
}
// 连接第一个设备
const device = devices[0];
console.log('连接到:', device.name);
// 连接设备
this.connection = await BleManager.connectToDevice(device.id);
// 保存设备引用
this.device = device;
console.log('设备已连接');
return true;
} catch (error) {
console.error('连接失败:', error);
throw error;
}
}
/**
* 断开连接
*/
async disconnect(): Promise<void> {
try {
if (this.device) {
await BleManager.disconnect(this.device.id);
this.device = null;
}
if (this.notificationListener) {
this.notificationListener.remove();
this.notificationListener = null;
}
console.log('设备已断开');
} catch (error) {
console.error('断开连接失败:', error);
}
}
/**
* 获取服务
*/
async discoverServices(): Promise<void> {
if (!this.device) {
throw new Error('设备未连接');
}
try {
// 发现服务
const services = await BleManager.retrieveServices(
this.device.id,
[HEART_RATE_SERVICE_UUID]
);
console.log('服务已发现:', services);
} catch (error) {
console.error('发现服务失败:', error);
throw error;
}
}
/**
* 启用心率通知
*/
async startHeartRateNotification(callback: (heartRate: number) => void): Promise<void> {
if (!this.device) {
throw new Error('设备未连接');
}
try {
// 监听通知
this.notificationListener = BleManager.addListener(
'BleManagerDidUpdateValueForCharacteristic',
({ value, characteristic }) => {
if (characteristic === HEART_RATE_MEASUREMENT_UUID) {
const heartRate = this.parseHeartRate(value);
callback(heartRate);
}
}
);
// 启用通知
await BleManager.startNotification(
this.device.id,
HEART_RATE_SERVICE_UUID,
HEART_RATE_MEASUREMENT_UUID
);
console.log('心率通知已启用');
} catch (error) {
console.error('启用通知失败:', error);
throw error;
}
}
/**
* 停止通知
*/
async stopHeartRateNotification(): Promise<void> {
if (this.device) {
await BleManager.stopNotification(
this.device.id,
HEART_RATE_SERVICE_UUID,
HEART_RATE_MEASUREMENT_UUID
);
}
}
/**
* 解析心率数据
*/
private parseHeartRate(data: number[]): number {
if (data.length === 0) return 0;
const flags = data[0];
const is16Bit = (flags & 0x01) === 0x01;
if (is16Bit) {
// 16位格式
const value16 = (data[1] << 8) | data[2];
return value16;
} else {
// 8位格式
return data[1];
}
}
/**
* 读取传感器位置
*/
async getSensorLocation(): Promise<BodySensorLocation> {
if (!this.device) {
throw new Error('设备未连接');
}
try {
const characteristic = await BleManager.readCharacteristic(
this.device.id,
HEART_RATE_SERVICE_UUID,
BODY_SENSOR_LOCATION_UUID
);
if (characteristic.value) {
const location = characteristic.value[0];
return location;
}
return BodySensorLocation.Other;
} catch (error) {
console.error('读取传感器位置失败:', error);
return BodySensorLocation.Other;
}
}
}
Code collapsed
React Hook
使用心率数据Hook
code
// hooks/useHeartRateMonitor.ts
import { useState, useEffect, useCallback } from 'react';
import { Platform } from 'react-native';
import { HeartRateDeviceManager } from '../services/HeartRateDeviceManager';
interface HeartRateData {
current: number | null;
history: Array<{
value: number;
timestamp: number;
}>;
average: number;
}
export function useHeartRateMonitor() {
const [isConnected, setIsConnected] = useState(false);
const [isScanning, setIsScanning] = useState(false);
const [error, setError] = useState<string | null>(null);
const [heartRateData, setHeartRateData] = useState<HeartRateData>({
current: null,
history: [],
average: 0
});
const managerRef = useRef<HeartRateDeviceManager | null>(null);
// 初始化管理器
useEffect(() => {
const manager = new HeartRateDeviceManager();
managerRef.current = manager;
manager.initialize().catch(err => {
setError('初始化失败: ' + err.message);
});
return () => {
// 清理
manager.disconnect();
};
}, []);
/**
* 连接设备
*/
const connect = useCallback(async () => {
if (!managerRef.current) return;
setIsScanning(true);
setError(null);
try {
const manager = managerRef.current;
// 扫描并连接
await manager.scanAndConnect();
// 发现服务
await manager.discoverServices();
// 启用心率通知
await manager.startHeartRateNotification((heartRate) => {
setHeartRateData(prev => {
const newHistory = [
...prev.history,
{
value: heartRate,
timestamp: Date.now()
}
].slice(-100); // 只保留最近100个数据点
// 计算平均心率
const average = newHistory.reduce((sum, h) => sum + h.value, 0) / newHistory.length;
return {
current: heartRate,
history: newHistory,
average
};
});
});
setIsConnected(true);
} catch (err: any) {
setError(err.message || '连接失败');
} finally {
setIsScanning(false);
}
}, []);
/**
* 断开连接
*/
const disconnect = useCallback(async () => {
if (managerRef.current) {
await managerRef.current.disconnect();
setIsConnected(false);
setHeartRateData({
current: null,
history: [],
average: 0
});
}
}, []);
return {
isConnected,
isScanning,
error,
heartRateData,
connect,
disconnect
};
}
Code collapsed
UI组件
心率监测界面
code
// screens/HeartRateMonitorScreen.tsx
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, ActivityIndicator } from 'react-native';
import { LineChart } from 'react-native-chart-kit';
import { Dimensions } from 'react-native';
import { useHeartRateMonitor } from '../hooks/useHeartRateMonitor';
import { HeartRateDeviceManager } from '../services/HeartRateDeviceManager';
export function HeartRateMonitorScreen() {
const {
isConnected,
isScanning,
error,
heartRateData,
connect,
disconnect
} = useHeartRateMonitor();
const getHeartRateZone = (hr: number) => {
if (hr < 60) return { zone: '休息', color: '#3498db' };
if (hr < 70) return { zone: '燃脂', color: '#2ecc71' };
if (hr < 80) return { zone: '有氧', color: '#f39c12' };
if (hr < 90) return { zone: '无氧', color: '#e67e22' };
return { zone: '极限', color: '#e74c3c' };
};
const currentZone = heartRateData.current
? getHeartRateZone(heartRateData.current)
: null;
return (
<ScrollView style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>❤️ 心率监测</Text>
<Text style={styles.subtitle}>蓝牙心率设备连接</Text>
</View>
{/* 连接状态 */}
<View style={styles.connectionSection}>
<TouchableOpacity
style={[
styles.connectButton,
isConnected && styles.connectedButton
]}
onPress={isConnected ? disconnect : connect}
disabled={isScanning}
>
{isScanning ? (
<ActivityIndicator color="#fff" />
) : isConnected ? (
<Text style={styles.connectButtonText}>断开连接</Text>
) : (
<Text style={styles.connectButtonText}>连接设备</Text>
)}
</TouchableOpacity>
{isConnected && (
<View style={styles.statusIndicator}>
<Text style={styles.statusText}>✓ 已连接</Text>
</View>
)}
</View>
{/* 错误信息 */}
{error && (
<View style={styles.errorContainer}>
<Text style={styles.errorText}>❌ {error}</Text>
</View>
)}
{/* 心率显示 */}
{isConnected && (
<>
{/* 当前心率 */}
<View style={styles.currentHeartRateContainer}>
<Text style={styles.label}>当前心率</Text>
<View style={styles.heartRateDisplay}>
<Text style={[
styles.heartRateValue,
{ color: currentZone?.color || '#333' }
]}>
{heartRateData.current || '--'}
</Text>
<Text style={styles.heartRateUnit}>BPM</Text>
</View>
{currentZone && (
<View style={[styles.zoneBadge, { backgroundColor: currentZone.color }]}>
<Text style={styles.zoneText}>{currentZone.zone}</Text>
</View>
)}
</View>
{/* 统计信息 */}
<View style={styles.statsContainer}>
<View style={styles.statItem}>
<Text style={styles.statLabel}>平均心率</Text>
<Text style={styles.statValue}>
{Math.round(heartRateData.average)} BPM
</Text>
</View>
<View style={styles.statItem}>
<Text style={styles.statLabel}>数据点数</Text>
<Text style={styles.statValue}>
{heartRateData.history.length}
</Text>
</View>
</View>
{/* 心率图表 */}
{heartRateData.history.length > 0 && (
<View style={styles.chartContainer}>
<Text style={styles.chartTitle}>心率趋势</Text>
<LineChart
data={{
labels: heartRateData.history.map((_, i) => i),
datasets: [{
data: heartRateData.history.map(h => h.value),
color: (opacity = 1) => `rgba(231, 76, 60, ${opacity})`,
strokeWidth: 2,
}]
}}
width={Dimensions.get('window').width - 40}
height={220}
chartConfig={{
backgroundColor: '#fff',
backgroundGradientFrom: '#fff',
backgroundGradientTo: '#fff',
decimalPlaces: 0,
color: (opacity = 1) => `rgba(0, 0, 0, ${opacity})`,
style: {
borderRadius: 16
},
props: {
...chartConfig.props,
pointerEvents: 'none'
},
}}
bezier
style={styles.chart}
/>
</View>
)}
{/* 心率区间说明 */}
<View style={styles.zonesContainer}>
<Text style={styles.zonesTitle}>心率区间说明</Text>
<View style={styles.zoneList}>
<View style={styles.zoneItem}>
<View style={[styles.zoneDot, { backgroundColor: '#3498db' }]} />
<Text style={styles.zoneText}>休息 (<60 BPM)</Text>
</View>
<View style={styles.zoneItem}>
<View style={[styles.zoneDot, { backgroundColor: '#2ecc71' }]} />
<Text style={styles.zoneText}>燃脂 (60-70 BPM)</Text>
</View>
<View style={styles.zoneItem}>
<View style={[styles.zoneDot, { backgroundColor: '#f39c12' }]} />
<Text style={styles.zoneText}>有氧 (70-80 BPM)</Text>
</View>
<View style={styles.zoneItem}>
<View style={[styles.zoneDot, { backgroundColor: '#e67e22' }]} />
<Text style={styles.zoneText}>无氧 (80-90 BPM)</Text>
</View>
<View style={styles.zoneItem}>
<View style={[styles.zoneDot, { backgroundColor: '#e74c3c' }]} />
<Text style={styles.zoneText}>极限 (>90 BPM)</Text>
</View>
</View>
</View>
</>
)}
{/* 使用说明 */}
{!isConnected && (
<View style={styles.instructions}>
<Text style={styles.instructionsTitle}>📋 使用说明</Text>
<Text style={styles.instruction}>
1. 确保蓝牙心率设备已开启且可被发现
</Text>
<Text style={styles.instruction}>
2. 点击"连接设备"按钮开始扫描
</Text>
<Text style={styles.instruction}>
3. 在设备列表中选择您的心率设备
</Text>
<Text style={styles.instruction}>
4. 配对后即可看到实时心率数据
</Text>
<View style={styles.supportedDevices}>
<Text style={styles.supportedDevicesTitle}>✅ 支持的设备品牌</Text>
<Text style={styles.supportedDevice}>• Polar (H7, H10)</Text>
<Text style={styles.supportedDevice}>• Garmin (心率带)</Text>
<Text style={styles.supportedDevice}>• Fitbit (部分型号)</Text>
<Text style={styles.supportedDevice}>• 通用BLE心率带</Text>
</View>
</View>
)}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
header: {
padding: 20,
backgroundColor: '#fff',
alignItems: 'center',
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
title: {
fontSize: 28,
fontWeight: 'bold',
marginBottom: 5,
},
subtitle: {
fontSize: 14,
color: '#666',
},
connectionSection: {
flexDirection: 'row',
padding: 15,
justifyContent: 'center',
alignItems: 'center',
gap: 15,
},
connectButton: {
backgroundColor: '#667eea',
paddingHorizontal: 40,
paddingVertical: 15,
borderRadius: 25,
},
connectedButton: {
backgroundColor: '#f093fb',
},
connectButtonText: {
color: '#fff',
fontSize: 16,
fontWeight: 'bold',
},
statusIndicator: {
backgroundColor: '#2ecc71',
paddingHorizontal: 15,
paddingVertical: 7,
borderRadius: 15,
},
statusText: {
color: '#fff',
fontWeight: 'bold',
},
errorContainer: {
margin: 15,
padding: 15,
backgroundColor: '#fee',
borderRadius: 10,
borderLeftWidth: 4,
borderLeftColor: '#fcc',
},
errorText: {
color: '#c00',
},
currentHeartRateContainer: {
backgroundColor: '#fff',
margin: 15,
borderRadius: 15,
padding: 20,
alignItems: 'center',
},
label: {
fontSize: 16,
color: '#666',
marginBottom: 10,
},
heartRateDisplay: {
flexDirection: 'row',
alignItems: 'center',
},
heartRateValue: {
fontSize: 72,
fontWeight: 'bold',
marginRight: 10,
},
heartRateUnit: {
fontSize: 24,
color: '#999',
},
zoneBadge: {
paddingHorizontal: 15,
paddingVertical: 7,
borderRadius: 15,
marginTop: 10,
},
zoneText: {
color: '#fff',
fontWeight: 'bold',
},
statsContainer: {
flexDirection: 'row',
margin: 15,
gap: 10,
},
statItem: {
flex: 1,
backgroundColor: '#fff',
borderRadius: 15,
padding: 20,
alignItems: 'center',
},
statLabel: {
fontSize: 14,
color: '#666',
marginBottom: 5,
},
statValue: {
fontSize: 24,
fontWeight: 'bold',
color: '#333',
},
chartContainer: {
backgroundColor: '#fff',
margin: 15,
borderRadius: 15,
padding: 15,
},
chartTitle: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 10,
},
chart: {
borderRadius: 15,
},
zonesContainer: {
backgroundColor: '#fff',
margin: 15,
borderRadius: 15,
padding: 15,
},
zonesTitle: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 15,
},
zoneList: {
gap: 10,
},
zoneItem: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
},
zoneDot: {
width: 12,
height: 12,
borderRadius: 6,
},
instructions: {
margin: 15,
padding: 20,
backgroundColor: '#fff',
borderRadius: 15,
},
instructionsTitle: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 15,
},
instruction: {
fontSize: 14,
lineHeight: 24,
marginBottom: 5,
},
supportedDevices: {
marginTop: 20,
paddingTop: 20,
borderTopWidth: 1,
borderTopColor: '#e0e0e0',
},
supportedDevicesTitle: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 10,
},
supportedDevice: {
fontSize: 14,
color: '#666',
marginLeft: 10,
marginBottom: 5,
},
});
Code collapsed
关键要点
- react-native-ble-plx是首选:功能完整,文档详细
- Heart Rate Service是标准:UUID统一
- 位置权限必需:Android需要
- 后台模式需配置:iOS设置UIBackgroundModes
- 数据解析有规范:8位/16位格式
常见问题
扫描不到设备?
可能原因:
- 设备未开启
- 设备已连接其他应用
- 权限未授予
- 设备不支持标准HR服务
数据不稳定?
优化方法:
- 增加数据缓冲
- 使用滑动平均
- 检查连接状态
- 实现自动重连
iOS后台问题?
解决方法:
- 添加UIBackgroundModes
- 使用BackgroundTask
- 保持连接活跃
参考资料
- Bluetooth Heart Rate Profile规范
- react-native-ble-plx文档
- Apple Core Bluetooth指南
- Android蓝牙文档
发布日期:2026年3月8日 最后更新:2026年3月8日