康心伴Logo
康心伴WellAlly
Health

React Native BLE心率监测器完整教程:蓝牙设备集成 | WellAlly康心伴

5 分钟阅读

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}>休息 (&lt;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}>极限 (&gt;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

关键要点

  1. react-native-ble-plx是首选:功能完整,文档详细
  2. Heart Rate Service是标准:UUID统一
  3. 位置权限必需:Android需要
  4. 后台模式需配置:iOS设置UIBackgroundModes
  5. 数据解析有规范:8位/16位格式

常见问题

扫描不到设备?

可能原因:

  1. 设备未开启
  2. 设备已连接其他应用
  3. 权限未授予
  4. 设备不支持标准HR服务

数据不稳定?

优化方法:

  1. 增加数据缓冲
  2. 使用滑动平均
  3. 检查连接状态
  4. 实现自动重连

iOS后台问题?

解决方法:

  1. 添加UIBackgroundModes
  2. 使用BackgroundTask
  3. 保持连接活跃

参考资料

  • Bluetooth Heart Rate Profile规范
  • react-native-ble-plx文档
  • Apple Core Bluetooth指南
  • Android蓝牙文档

发布日期:2026年3月8日 最后更新:2026年3月8日

免责声明: 本内容仅供教育参考,不能替代专业医疗建议。请咨询医生获取个性化诊断和治疗方案。

#

文章标签

React Native
BLE
心率监测
蓝牙设备
健康应用

觉得这篇文章有帮助?

立即体验康心伴,开始您的健康管理之旅