Expo(React Native)集成蓝牙功能

2,399 阅读10分钟

作者: p0ssword

编写于2024年11月04日

0. 附录

  • 项目目录
├── app
|  ├── (tabs)
|  |  ├── explore.jsx
|  |  ├── index.jsx
|  |  └── _layout.jsx
|  ├── +html.jsx
|  ├── +not-found.jsx
|  └── _layout.jsx
├── app.json
├── babel.config.js
├── components
|  ├── Collapsible.jsx
|  ├── ExternalLink.jsx
|  ├── HelloWave.jsx
|  ├── navigation
|  |  └── TabBarIcon.jsx
|  ├── ParallaxScrollView.jsx
|  ├── ThemedText.jsx
|  └── ThemedView.jsx
├── constants
|  └── Colors.js
├── expo-env.d.ts
├── hooks
|  ├── useColorScheme.js
|  ├── useColorScheme.web.js
|  └── useThemeColor.js
├── jsconfig.json
├── package.json
└── README.md
  • 所用包

    react-native-ble-manager @craftzdog/react-native-buffer expo-device
    

简单讲讲流程, 手机使用蓝牙功能进行扫描设备, 然后连接对应的设备并读取设备提供的服务, 接着使用服务进行对应的操作(CRUD)

扫描设备

[扫描设备]

连接并使用设备

[连接并使用设备]

1. 安装蓝牙所需的包以及进行预编译

  1. 打开项目终端, 在项目中安装react-native-ble-manager@craftzdog/react-native-buffer

    • react-native-ble-manager: 用于蓝牙通信的包
    • @craftzdog/react-native-buffer: 用于处理蓝牙通信数据处理(可选用其他包, 不一定要一样)
    • expo-device: 可访问物理设备的系统信息,如制造商和型号
    npx expo add react-native-ble-manager @craftzdog/react-native-buffer expo-device
    
  2. 终端输入命令对Expo项目进行预编译, 注意: iOS平台只能在Mac电脑上进行预编译, Windows仅能预编译Android平台

    npx expo prebuild
    

    根据实际情况进行填写内容

    $ npx expo prebuild
    
    📝  Android package Learn more: https://expo.fyi/android-package
    
    √ What would you like your Android package name to be? » #[根据实际进行填写]
    
    ✔ Created native directory
    ✔ Updated package.json
    ✔ Finished prebuild
    

    在预编译完成后, 即可以在项目中看到androidiOS目录

2. 编写代码

注意: 模拟器没有蓝牙功能, 请实际使用时, 使用真机进行测试

本次项目为演示, 主要代码将编写在app > index.jsx , 实际情况请自行判断

0. 运行项目

npx expo run:android

运行之前请先连接真机并开启adb调试, 详情请进行互联网或者Ai查询

在终端中显示BUILD SUCCESSFUL则编译成功, 期间真机会弹出安装App的弹窗, 请进行确认安装

安装后则终端会自动打开App, 如图所示

image-20241105104934340

1. 编写蓝牙权限申请的Hook函数

安卓部分

import { PermissionsAndroid, Platform } from 'react-native';

/**
 * 请求蓝牙相关权限。
 * 
 * 该函数会根据 Android 版本,请求不同的权限:
 * - Android 31 及以上版本:请求 `BLUETOOTH_SCAN` 和 `BLUETOOTH_CONNECT` 权限。
 * - Android 23 及以上版本(低于 31):请求 `ACCESS_FINE_LOCATION` 权限。
 * 
 * @returns {Promise<boolean>} 如果所有权限都被授予,返回 true;否则返回 false。
 */
const requestPermissions = async () => {
  // 检查当前平台是否为 Android
  if (Platform.OS === 'android') {
    // 检查 Android 版本是否大于等于 31
    if (Platform.Version >= 31) {
      // 请求 `BLUETOOTH_SCAN` 和 `BLUETOOTH_CONNECT` 权限
      const result = await PermissionsAndroid.requestMultiple([
        PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
        PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
      ]);
      // 检查权限请求结果
      if (result && result['android.permission.BLUETOOTH_SCAN'] === 'granted' && result['android.permission.BLUETOOTH_CONNECT'] === 'granted') {
        console.debug(
          '[handleAndroidPermissions] User accepts runtime permissions android 12+',
        );
        return true;
      } else {
        console.error(
          '[handleAndroidPermissions] User refuses runtime permissions android 12+',
        );
        return false;
      }
    } else if (Platform.Version >= 23) { // 检查 Android 版本是否大于等于 23
      // 请求 `ACCESS_FINE_LOCATION` 权限
      const granted = await PermissionsAndroid.request(
        PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
      );
      // 检查权限请求结果
      if (granted === PermissionsAndroid.RESULTS.GRANTED) {
        console.debug(
          '[handleAndroidPermissions] User accepts runtime permission android <12',
        );
        return true;
      } else {
        console.error(
          '[handleAndroidPermissions] User refuses runtime permission android <12',
        );
        return false;
      }
    }
  }
  // 非 Android 平台默认返回 true
  return true;
};

// 导出权限请求函数
export default requestPermissions;

iOS部分

iOS需要在info.plist声明对蓝牙的应用的场景说明, 如果不进行声明, 系统在运行时有权对权限进行拒绝

info.plist文件一般在ios > [项目名] > Supporting > Info.plist

<key>NSBluetoothPeripheralUsageDescription</key>
<string>我们需要访问蓝牙来扫描和连接设备。</string>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>我们需要访问蓝牙来提供持续的设备连接。</string>

2. 权限获取

使用useEffect进行处理蓝牙的权限获取, 当拿到权限时并进行监听蓝牙相关事件

  useEffect(() => {
    // 权限申请
    const blePermission = requestPermissions();
    if (!blePermission) return console.log('获取权限被拒绝');

    // 权限申请通过
    // ... 
  }, []);

image-20241105105036293

打开时会有权限请求, 请进行允许

3. 初始化蓝牙与扫描蓝牙设备

在权限获取成功后, 进行蓝牙的初始化和创建监听器

export default function HomeScreen() {
  // 扫描到的设备
  const [peripherals, setPeripherals] useState(new Map());

  useEffect(() => {
    // 权限申请
    const blePermission = requestPermissions();
    if (!blePermission) return console.log('[ERROR CODE](16) 获取权限被拒绝');

    // 权限申请通过
    try {
      // 激活蓝牙, 即让用户开启蓝牙
      bleManager.enableBluetooth().then(_ => {console.log('[DEBUG CODE](21) 蓝牙已开启')}).catch(err => {console.log('[ERROR CODE](21) 蓝牙激活错误: ', err)});

      // 初始化蓝牙模块
      bleManager.start({showAlert: false}).then(() => {console.log('[DEBUG CODE](20) 蓝牙初始化成功')})
      .catch(err => {
        console.error('[ERROR CODE](22) 蓝牙初始化错误: ', err);
      })
    } catch (err) {
      console.log('[ERROR CODE](25) 蓝牙初始化错误: ', err)
    }

    // 创建监听器
    const listeners = [
      // 蓝牙状态监听
      bleManager.addListener('BleManagerDidUpdateState', state => {
        /*
         共4种状态:turning_off, off, turning_on, on
         */
        console.log('[DEBUG CODE](32) 蓝牙状态发生变化: ', state);
      }),

      // 扫描蓝牙设备
      bleManager.addListener('BleManagerDiscoverPeripheral', peripheral => {
        console.log('[DEBUG CODE](40) 发现设备: ', peripheral);
        // 可在此处对扫描到的设备进行保存处理...
        setPeripherals(map=>{return new Map(peripheral.id, peripheral)})
      }),

      // 停止扫描设备
      bleManager.addListener('BleManagerStopScan', event => {
        console.log('[DEBUG CODE](49) 停止扫描: ', event);

        // 获取扫描到的所有的设备
        bleManager.getDiscoveredPeripherals().then(peripherals => {
          console.log('[DEBUG CODE](54) 获取到的设备: ', JSON.stringify(peripherals));
        })
      })
    ];

    // 离开时移除监听器
    return () => {
      for (const listenersKey in listeners) {
        listeners[listenersKey].remove();
      }
    }
  }, []);

  /**
   * 扫描蓝牙设备
   */
  const handlerScanBle = () => {
    try {
      bleManager.scan([], 5, false)
      .then(_ => {console.log('[DEBUG CODE](57) 开启扫描')})
    } catch (err) {
      console.log('[ERROR CODE](58) 扫描蓝牙错误: ', err)
    }
  }

  return (
    <>
      <Button title={'扫描蓝牙'} onPress={handlerScanBle}/>
    </>
  );
}

解析扫描到的数据:

{
  "advertising": {
    "manufacturerData": {
      "3534": {
        "bytes": [
          99,
          49,
          45,
          49,
          49,
          49,
          50,
          48,
          52,
          48,
          51,
          49,
          56,
          78
        ],
        "data": "YzEtMTExMjA0MDMxOE4=",
        "CDVType": "ArrayBuffer"
      }
    },
    "txPowerLevel": -2147483648,
    "isConnectable": true,
    "serviceData": {},
    "localName": "tcl_AC_t*ap_cabinet",
    "serviceUUIDs": [
      "f100",
      "f400"
    ],
    "manufacturerRawData": {
      "bytes": [
        0,
        0,
        53,
        52,
        99,
        49,
        45,
        49,
        49,
        49,
        50,
        48,
        52,
        48,
        51,
        49,
        56,
        78
      ],
      "data": "AAA1NGMxLTExMTIwNDAzMThO",
      "CDVType": "ArrayBuffer"
    },
    "rawData": {
      "bytes": [
        2,
        1,
        6,
        3,
        3,
        0,
        241,
        20,
        9,
        116,
        99,
        108,
        95,
        65,
        67,
        95,
        116,
        42,
        97,
        112,
        95,
        99,
        97,
        98,
        105,
        110,
        101,
        116,
        3,
        3,
        0,
        244,
        3,
        25,
        0,
        0,
        17,
        255,
        52,
        53,
        99,
        49,
        45,
        49,
        49,
        49,
        50,
        48,
        52,
        48,
        51,
        49,
        56,
        78,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0
      ],
      "data": "AgEGAwMA8RQJdGNsX0FDX3QqYXBfY2FiaW5ldAMDAPQDGQAAEf80NWMxLTExMTIwNDAzMThOAAAAAAAAAAA=",
      "CDVType": "ArrayBuffer"
    }
  },
  "rssi": -71,
  "id": "D0:76:02:9C:45:C2",
  "name": "tcl_AC_t*ap_cabinet"
}
  • name: 设备名
  • id: 设备蓝牙MAC地址
  • rssi: 信号强度
  • advertising: 蓝牙广播
    • isConnectable: 是否可以连接
    • servicesUUIDs: 服务uuid

其中, id, servicesUUIDs较为重要, 这两个参数连接时进行获取蓝牙服务

4. 连接蓝牙设备并获取蓝牙设备服务

当我们获取到我们要的蓝牙设备时, 可以对其进行连接并获取所提供的服务

export default function HomeScreen() {
  // .....

  /**
   *  连接设备
   */
  const handlerConnectPeripheral = async ({item}) => {
    try {
      // 连接蓝牙设备
      if (!isConnect) {
        console.log('[DEBUG CODE](89) 正在连接: ', item);
        await bleManager.connect(item.id);
      }

      // 获取设备的服务
      const peripheralServices = await bleManager.retrieveServices(item.id);
      console.log('[DEBUG CODE](110) 发现服务:', JSON.stringify(peripheralServices));

      // 筛出设备服务
      const characteristicsWithMatchingService = peripheralServices.characteristics.slice().map(t => {
        const temp = {
          service: t.service,
          characteristic: t.characteristic,
          properties: Object.keys(t.properties)
        }
        if (t.descriptors && t.descriptors > 0) temp.descriptors = t.descriptors
        return temp;
      })

      // 保存服务
      setPeripheralData(characteristicsWithMatchingService);
      console.log('[DEBUG CODE] 123 设备服务: ',characteristicsWithMatchingService);
    } catch (err) {
      console.log('[ERROR CODE](93) 连接错误: ', err);
    }
  }

  return (
    <View style={{top: useSafeAreaInsets().top}}>
      <Button title={'扫描蓝牙'} onPress={handlerScanBle}/>
      <FlatList style={{marginTop: 40}} data={Array.from(peripherals.values())} renderItem={(item) => {
        return (
          <TouchableOpacity onPress={() => {handlerConnectPeripheral({...item})}}>
            <View style={{flex: 1, marginBottom: 16, borderBottomWidth: 1}}>
              <Text>{item.item.name}</Text>
              <Text style={{fontSize: 12}}>{item.item.id}</Text>
            </View>
          </TouchableOpacity>
        );
      }}/>
    </View>
  );
}

当连接上蓝牙设备后, 读取对应的蓝牙服务并进行保存, 便于接下来的数据收发

5. 数据收发

目前仅研究了蓝牙数据的读取以及发送, react-native-ble-manager中还有一个Notice也就是通知, 但示例中并没有展示, 因为实际用途感觉不是很大

蓝牙的数据传输一般为HEX数据, 也就是16进制的数字数组, 在前文中提到的包: @craftzdog/react-native-buffer, 就是用来解析以及创建数据用的

下面请看函数方法

 /**
   * 处理服务
   */
  const handleService = (item, t) => {
    // pid: 设备MAC id, characteristic: 特征id, service: 服务id, t: 服务方法
    const pid = connectDevice.peripheral;
    const {characteristic, service} = item;

    
    switch (t) {
      case 'Read':
        // 读取服务
        bleManager.read(pid, service, characteristic).then(data => {
          const buffer = Buffer.from(data);
          const stringData = buffer.toString();
          console.log('[DEBUG CODE](145) 读取服务: ', stringData);
        })
        .catch(err=>{
          console.log('[ERROR CODE](148) 读取失败: ', err)
        })
        break;
        
      case 'Write':
        // 写入服务

        // 根据官方文档提示, 将要传输数据进行转换为数字数组(Number[]), 并将数据使用Buffer.toJSON().data转换
        
        // 这里使用时间代替, 相关的数据格式请根据实际项目进行处理
        const data = new Date().getTime(); 
        
        // 使用TextEncoder进行编码, 再将数据传输到Buffer.from()进行转换为数字数组
        const dataBuffer = Buffer.from(new TextEncoder().encode(data)); 

        bleManager.write(pid, service, characteristic, dataBuffer.toJSON().data).then(()=>{
          console.log('[DEBUG CODE](159) 写入成功')
        })
        .catch(err=>{
          console.log('[ERROR CODE](162) 写入失败: ', err)
        })
        break;
        
      default:
        break;
    }
  }

看看示例输出

读取服务

 LOG  [DEBUG CODE](145) 读取服务:  test-read

写入服务

 LOG  [DEBUG CODE](159) 写入成功

提示写入成功后, 基本就可以在蓝牙端方面进行接收数据了

写入: <Buffer 31 37 33 30 38 36 30 31 36 38 33 35 34> {
  type: 'Buffer',
  data: [
    49, 55, 51, 48, 56, 54,
    48, 49, 54, 56, 51, 53,
    52
  ]
} 1730860168354

完整示例代码

import {
    Button,
    Dimensions,
    FlatList,
    PermissionsAndroid,
    Platform,
    StyleSheet,
    Text,
    TouchableHighlight,
    View
} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {useEffect, useState} from 'react';
import BleManager from 'react-native-ble-manager';
import {Buffer} from '@craftzdog/react-native-buffer';
import requestPermissions from '@/hooks/blePermissions';
import bleManager from 'react-native-ble-manager';

const {width: sWidth, height: sHeight} = Dimensions.get('screen');
export default function BleManagerPage() {
    const [peripherals, setPeripherals] = useState(new Map());
    const [isConnect, setIsConnect] = useState(false);
    const [peripheralData, setPeripheralData] = useState(null);
    const [connectDevice, setConnectDevice] = useState(null);

    useEffect(() => {
        // 权限申请
        const blePermission = requestPermissions();
        if (!blePermission) return console.log('[ERROR CODE](16) 获取权限被拒绝');

        // 权限申请通过
        try {
            // 激活蓝牙, 即让用户开启蓝牙
            bleManager.enableBluetooth().then(_ => {
                console.log('[DEBUG CODE](21) 蓝牙已开启')
            }).catch(err => {
                console.log('[ERROR CODE](21) 蓝牙激活错误: ', err)
            });

            // 初始化蓝牙模块
            bleManager.start({showAlert: false}).then(() => {
                console.log('[DEBUG CODE](20) 蓝牙初始化成功')
            })
            .catch(err => {
                console.error('[ERROR CODE](22) 蓝牙初始化错误: ', err);
            })
        } catch (err) {
            console.log('[ERROR CODE](25) 蓝牙初始化错误: ', err)
        }

        // 创建监听器
        const listeners = [
            // 蓝牙状态监听
            bleManager.addListener('BleManagerDidUpdateState', state => {
                /*
                 共4种状态:turning_off, off, turning_on, on
                 */
                console.log('[DEBUG CODE](32) 蓝牙状态发生变化: ', state);
            }),

            // 扫描蓝牙设备
            bleManager.addListener('BleManagerDiscoverPeripheral', peripheral => {
                console.log('[DEBUG CODE](40) 发现设备: ', JSON.stringify(peripheral));
                // 可在此处对扫描到的设备进行保存等处理...
                setPeripherals(map => {
                    return new Map(map.set(peripheral.id, peripheral))
                })
            }),

            // 停止扫描设备
            bleManager.addListener('BleManagerStopScan', event => {
                console.log('[DEBUG CODE](49) 停止扫描: ', event);

                // 获取扫描到的所有的设备
                bleManager.getDiscoveredPeripherals().then(peripherals => {
                    console.log('[DEBUG CODE](54) 获取到的设备: ', JSON.stringify(peripherals));
                })
            }),

            // 连接设备
            bleManager.addListener('BleManagerConnectPeripheral', event => {
                console.log('[DEBUG CODE](66) 已连接设备: ', event);
                setIsConnect(true);
                setConnectDevice(event);
            }),

            // 断开连接
            bleManager.addListener('BleManagerDisconnectPeripheral', event => {
                console.log('[DEBUG CODE](71) 断开连接: ', event);
                setIsConnect(false);
                setPeripheralData(null)
            })
        ];

        return () => {
            for (const listenersKey in listeners) {
                listeners[listenersKey].remove();
            }
        }
    }, []);

    /**
     * 扫描蓝牙设备
     */
    const handlerScanBle = () => {
        try {
            setPeripherals(new Map());
            bleManager.scan([], 3, false)
            .then(_ => {
                console.log('[DEBUG CODE](57) 开启扫描')
            })
        } catch (err) {
            console.log('[ERROR CODE](58) 扫描蓝牙错误: ', err)
        }
    }

    /**
     *  连接设备
     */
    const handlerConnectPeripheral = async (item) => {
        try {
            // 连接蓝牙设备
            if (!isConnect) {
                console.log('[DEBUG CODE](89) 正在连接: ', item);
                await bleManager.connect(item.id);
            }

            // 获取设备的服务
            const peripheralServices = await bleManager.retrieveServices(item.id);
            console.log('[DEBUG CODE](110) 发现服务:', JSON.stringify(peripheralServices));

            // 筛出设备服务
            const characteristicsWithMatchingService = peripheralServices.characteristics.slice().map(t => {
                const temp = {
                    service: t.service,
                    characteristic: t.characteristic,
                    properties: Object.keys(t.properties)
                }
                if (t.descriptors && t.descriptors > 0) temp.descriptors = t.descriptors
                return temp;
            })

            setPeripheralData(characteristicsWithMatchingService);
            console.log('[DEBUG CODE] 123 设备服务: ', characteristicsWithMatchingService);
        } catch (err) {
            console.log('[ERROR CODE](93) 连接错误: ', err);
        }
    }

    /**
     * 处理服务
     */
    const handleService = (item, t) => {
        // pid: 设备MAC id, characteristic: 特征id, service: 服务id
        const pid = connectDevice.peripheral;
        const {characteristic, service} = item;
        console.log(t, pid, characteristic, service);
        switch (t) {
            case 'Read':
                // 读取服务
                bleManager.read(pid, service, characteristic).then(data => {
                    const buffer = Buffer.from(data);
                    const stringData = buffer.toString();
                    console.log('[DEBUG CODE](145) 读取服务: ', stringData);
                })
                .catch(err => {
                    console.log('[ERROR CODE](148) 读取失败: ', err)
                })
                break;
            case 'Write':
                // 写入服务

                // 根据官方文档提示, 将要传输数据进行转换为数字数组(Number[]), 并将数据使用Buffer.toJSON().data转换
                // 这里使用时间代替, 相关的数据格式请根据实际项目进行处理
                const data = new Date().getTime(); 

                // 使用TextEncoder进行编码, 再将数据传输到Buffer.from()进行转换为数字数组
                const dataBuffer = Buffer.from(new TextEncoder().encode(data)); 
                bleManager.write(pid, service, characteristic, dataBuffer.toJSON().data).then(() => {
                    console.log('[DEBUG CODE](159) 写入成功')
                })
                .catch(err => {
                    console.log('[ERROR CODE](162) 写入失败: ', err)
                })
                break;
            default:
                console.log('NOTHING HERE')
                break;
        }
    }

    /**
     * renderItem
     */
    const renderItem = ({item}) => {
        return (
            <TouchableHighlight style={{
                padding: 10,
                marginBottom: 12,
                backgroundColor: '#efefff'
            }} onPress={() => {
                handlerConnectPeripheral(item)
            }}>
                <View>
                    <Text>{item.name ?? 'NoName'}</Text>
                    <Text>{item.id}</Text>
                </View>
            </TouchableHighlight>
        )
    }

    return (
        <View style={[styles.container, {marginTop: useSafeAreaInsets().top}]}>
            <Text>BleManager</Text>
            <Button title={'开始扫描'} onPress={handlerScanBle}/>
            <Button title={'断开蓝牙'} onPress={() => {bleManager.disconnect(connectDevice.peripheral)}} disabled={!isConnect}/>

            {!isConnect ?
                <FlatList
                    style={{flex: 1}}
                    data={Array.from(peripherals.values())}
                    renderItem={renderItem}
                />
                :
                <FlatList
                    data={peripheralData}
                    keyExtractor={(item, index) => index.toString()}
                    renderItem={({item, index}) => (
                        <View style={[styles.view, {marginBottom: 10, backgroundColor: '#c0c0c4'}]} key={index}>
                            <Text>{item.characteristic}</Text>
                            {item.properties.map((t, i) => (
                                <Button
                                    title={t}
                                    onPress={() => {
                                        handleService(item, t)
                                    }}
                                    styles={{
                                        backgroundColor: '#67dc5e',
                                        color: '#fcfcfc'
                                    }}
                                    key={i}
                                />
                            ))}
                        </View>
                    )}
                />
            }
        </View>
    );
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
    },
})

完整项目示例请前往Github进行查看

3. 简单总结

本文简述使用Expo项目进行集成蓝牙功能, 实际还有react-native-ble-plx, 这个包API要比react-native-ble-manager多, 但是文档阅读有点难度, 感兴趣的可以去看看

附录

Github 完整示例代码

react-native-ble-manager