From 41f40802646c55080c558e21480a7c6d275fcfd6 Mon Sep 17 00:00:00 2001 From: Yudi Xiao <463708580@qq.com> Date: Wed, 10 Dec 2025 10:29:50 +0800 Subject: [PATCH] =?UTF-8?q?expo-ble=E6=A8=A1=E5=9D=97=E6=B5=8B=E8=AF=95dem?= =?UTF-8?q?o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...@shuohigh__duooomi-keystore-credentials.md | 10 + README.md | 301 ++++++++-- app.json | 33 +- app/(tabs)/explore.tsx | 513 ++++++++++++++---- ble/core/BleClient.ts | 222 ++++++++ ble/core/types.ts | 34 ++ ble/example/BleExampleV2.tsx | 372 +++++++++++++ ble/index.ts | 8 + ble/manager/BlePeripheralManager.ts | 472 ++++++++++++++++ ble/package.json | 43 ++ ble/protocol/Constants.ts | 52 ++ ble/protocol/ProtocolManager.ts | 155 ++++++ ble/protocol/types.ts | 89 +++ ble/services/BleProtocolService.ts | 165 ++++++ ble/services/DeviceInfoService.ts | 76 +++ ble/services/FileTransferService.ts | 51 ++ ble/utils/CryptoUtils.ts | 80 +++ ble/utils/ProtocolUtilsV2.ts | 306 +++++++++++ bun.lock | 38 ++ eas.json | 37 ++ hooks/useBleExplorer.ts | 493 +++++++++++++++++ hooks/useBlePeripheral.ts | 323 +++++++++++ package.json | 17 +- 23 files changed, 3749 insertions(+), 141 deletions(-) create mode 100644 @shuohigh__duooomi-keystore-backup/@shuohigh__duooomi-keystore-credentials.md create mode 100644 ble/core/BleClient.ts create mode 100644 ble/core/types.ts create mode 100644 ble/example/BleExampleV2.tsx create mode 100644 ble/index.ts create mode 100644 ble/manager/BlePeripheralManager.ts create mode 100644 ble/package.json create mode 100644 ble/protocol/Constants.ts create mode 100644 ble/protocol/ProtocolManager.ts create mode 100644 ble/protocol/types.ts create mode 100644 ble/services/BleProtocolService.ts create mode 100644 ble/services/DeviceInfoService.ts create mode 100644 ble/services/FileTransferService.ts create mode 100644 ble/utils/CryptoUtils.ts create mode 100644 ble/utils/ProtocolUtilsV2.ts create mode 100644 eas.json create mode 100644 hooks/useBleExplorer.ts create mode 100644 hooks/useBlePeripheral.ts diff --git a/@shuohigh__duooomi-keystore-backup/@shuohigh__duooomi-keystore-credentials.md b/@shuohigh__duooomi-keystore-backup/@shuohigh__duooomi-keystore-credentials.md new file mode 100644 index 0000000..bf61938 --- /dev/null +++ b/@shuohigh__duooomi-keystore-backup/@shuohigh__duooomi-keystore-credentials.md @@ -0,0 +1,10 @@ +# Android Upload Keystore Credentials + +These credentials are used in conjunction with your Android upload keystore file to sign your app for distribution. + +## Credential Values + +- Android upload keystore password: 33ded76b859c481bdbcb2759429ffbd2 +- Android key alias: 803a70040d2ea760c77979a931829601 +- Android key password: b509128efd5326930a1fe943f3f02890 + \ No newline at end of file diff --git a/README.md b/README.md index 48dd63f..eb8e7c3 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,289 @@ -# Welcome to your Expo app 👋 +# 协议三端通信说明 -This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app). +## 1. 数据格式 -## Get started +### 1.1 基本帧结构 -1. Install dependencies +所有数据包遵循以下格式(通常为大端序): - ```bash - npm install - ``` +| 字段 (Field) | 长度 (Bytes) | 值 / 说明 | +| :--- | :--- | :--- | +| **HEADER** | 3 | `FEDCBA` | +| **CMD** | 2 | 命令字 (如 `E100`) | +| **DATA** | n | 数据内容 (0 ~ n 字节) | +| **CHECKSUM** | 1 | 校验和 | +| **TAIL** | 2 | `00EF` | -2. Start the app +### 1.2 错误反馈 (系统级) - ```bash - npx expo start - ``` +当接收到的数据包格式错误时,设备会返回以下错误码: -In the output, you'll find options to open the app in a +| 错误类型 | CMD (上报) | 参数 (DATA) | 完整 HEX 示例 | +| :--- | :--- | :--- | :--- | +| HEADER 错误 | `E0E0` | `C0` | `FEDCBA E0E0 C0 00EF` | +| TAIL 错误 | `E0E0` | `C1` | `FEDCBA E0E0 C1 00EF` | +| CHECKSUM 错误 | `E0E2` | `C0` | `FEDCBA E0E2 C0 00EF` | +| CMD 不支持 | `E0E3` | `[CMD]` | `FEDCBA E0E3 [CMD] XX 00EF` | -- [development build](https://docs.expo.dev/develop/development-builds/introduction/) -- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/) -- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/) -- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo +--- -You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction). +## 2. 蓝牙通道说明 -## Get a fresh project +* **OTA 通道**: + * Write: `ae01` + * Notify: `ai02` (或 `ae02`) +* **APP 通信通道**: + * Write: `ae10` (MTU 建议设置为 517) + * Notify: `ae02` -When you're ready, run: +--- -```bash -npm run reset-project -``` +## 3. 命令详解 -This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing. +### 3.1 绑定 (0xE100) +**方向**: APP -> 设备 -## Learn more +* **发送**: `FEDCBA E100 E1 00EF` +* **返回**: -To learn more about developing your project with Expo, look at the following resources: +| 状态 | CMD | 参数 | 完整 HEX 示例 | +| :--- | :--- | :--- | :--- | +| 成功 | `E1A0` | `81` | `FEDCBA E1A0 81 00EF` | +| 失败 | `E1A1` | `82` | `FEDCBA E1A1 82 00EF` | +| 异常 | `E1A2` | `83` | `FEDCBA E1A2 83 00EF` | -- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides). -- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web. +### 3.2 绑定失败上报 (0xE200) +**方向**: APP -> 设备 (通常用于APP端绑定失败后通知设备) -## Join the community +* **发送**: `FEDCBA E200 XX 00EF` +* **返回**: -Join our community of developers creating universal apps. +| 状态 | CMD | 参数 | 完整 HEX 示例 | +| :--- | :--- | :--- | :--- | +| 成功 | `E2A0` | `XX` | `FEDCBA E2A0 XX 00EF` | +| 失败 | `E2A1` | `XX` | `FEDCBA E2A1 XX 00EF` | +| 异常 | `E2A2` | `XX` | `FEDCBA E2A2 XX 00EF` | -- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute. -- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions. +### 3.3 写用户 ID (0xE300) +**方向**: APP -> 设备 + +* **发送**: `FEDCBA E300 [UserID] XX 00EF` + * `UserID`: 用户ID数据 +* **返回**: + +| 状态 | CMD | 参数 | 完整 HEX 示例 | +| :--- | :--- | :--- | :--- | +| 成功 | `E3A0` | `83` | `FEDCBA E3A0 83 00EF` | +| 失败 | `E3A1` | `84` | `FEDCBA E3A1 84 00EF` | +| 异常(空) | `E3A3` | `85` | `FEDCBA E3A3 85 00EF` | + +### 3.4 查询密锁 (0xE400) +**方向**: APP -> 设备 + +* **发送**: `FEDCBA E400 E4 00EF` +* **返回**: `FEDCBA E4A0 [TOKEN] XX 00EF` + * `TOKEN`: 32位 (4字节) 密锁数据。 + +### 3.5 设置时间 (0xE500) +**方向**: APP -> 设备 + +* **发送**: `FEDCBA E500 [Time] XX 00EF` + * `Time`: 格式 `yyyymmddhhmmss` (14字节 ASCII) +* **返回**: + +| 状态 | CMD | 参数 | 完整 HEX 示例 | +| :--- | :--- | :--- | :--- | +| 成功 | `E5A0` | `85` | `FEDCBA E5A0 85 00EF` | +| 失败 | `E5A1` | `86` | `FEDCBA E5A1 86 00EF` | +| 异常 | `E5A2` | `87` | `FEDCBA E5A2 87 00EF` | + +### 3.6 查询固件版本号 (0xE600) +**方向**: APP -> 设备 + +* **发送**: `FEDCBA E600 E6 00EF` +* **返回**: `FEDCBA E6A0 [Version] XX 00EF` + * `Version`: 格式 `yyyymmddhh` (10字节 ASCII) + +### 3.7 查询资源版本号 (0xE700 / 0xD700) +**方向**: APP -> 设备 + +* **版本 < V4.0**: + * 发送: `FEDCBA E700 E7 00EF` + * 返回: `FEDCBA E7A0 [ResVersion] XX 00EF` +* **版本 >= V4.0**: + * 发送: `FEDCBA D700 E7 00EF` + * 返回: `FEDCBA D7A0 [ResVersion] XX 00EF` + +### 3.8 设置资源版本号 (0xE800) +**方向**: APP -> 设备 + +* **发送**: `FEDCBA E800 [TT] [ResVersion] XX 00EF` +* **返回**: + +| 状态 | CMD | 参数 | 完整 HEX 示例 | +| :--- | :--- | :--- | :--- | +| 成功 | `E8A0` | `88` | `FEDCBA E8A0 88 00EF` | +| 失败 | `E8A1` | `89` | `FEDCBA E8A1 89 00EF` | +| 异常 | `E8A2` | `8A` | `FEDCBA E8A2 8A 00EF` | + +### 3.9 解绑 (0xE900) +**方向**: APP -> 设备 + +* **发送**: `FEDCBA E900 E0 00EF` +* **返回**: `FEDCBA E9A0 F3 00EF` (解绑成功) + +### 3.10 下载文件 / OTA (0xEA00) +**方向**: APP -> 设备 +此命令流程包含请求传输、数据传输、传输结束、取消传输等状态。 + +#### A. 启动传输请求 +* **发送**: `FEDCBA EA00 [Type] [NameLen] [Name] [MD5] [Size] XX 00EF` + * `Type`: 文件类型 (1字节) + * `NameLen`: 文件名长度 (1字节) + * `Name`: 文件名 (N字节) + * `MD5`: 文件校验 (8字节) + * `Size`: 文件大小 (4字节, 16进制) +* **返回**: + +| 状态 | CMD | 说明 | 附加数据 | +| :--- | :--- | :--- | :--- | +| 同意传输 | `EAA0` | 准备接收 | `8A` | +| 失败 | `EAA1` | 拒绝/错误 | `8B` | +| 异常 | `EAA2` | 参数错误 | `8C` | +| 文件相同 | `EAA3` | 无需传输 | `8D` | +| 断点续传 | `EAA4` | 从Offset开始 | `[Offset(4B)]` | +| OTA回复 | `EAA5` | OTA 错误码 | `[ErrCode]` | + +#### B. 文件数据传输 (Packet) +* **发送**: `FEDCBA EA01 [Seq] [Len] [Data] XX 00EF` + * `Seq`: 序列号 (2字节) + * `Len`: 数据长度 (2字节) + * `Data`: 数据内容 +* **返回 (每包/批量确认)**: `FEDCBA EAA6 [ErrCode] [Seq] [TotalLen] XX 00EF` + +#### C. 传输完成 +* **发送**: `FEDCBA EA02 EC 00EF` +* **返回**: + * 成功: `FEDCBA EAA7 91 00EF` + * 失败: `FEDCBA EAA8 [ErrCode] XX 00EF` + +#### D. 取消传输 +* **发送**: `FEDCBA EA03 ED 00EF` +* **返回**: `FEDCBA EAA9 [ErrCode] 8D 00EF` + +### 3.11 查询设备 OTA 信息 (0xEB00) +**方向**: APP -> 设备 + +* **查询 OTA 信息**: + * 发送: `FEDCBA EB00 E0 00EF` + * 返回: `FEDCBA EBA0 [Err] [Ver] XX 00EF` +* **清除 OTA 状态**: + * 发送: `FEDCBA EB01 EC 00EF` + * 返回: `FEDCBA EBA7 92 00EF` + +### 3.12 设备信息查询 (0xED00) +**方向**: APP -> 设备 + +* **发送**: `FEDCBA ED00 [TypeMask] XX 00EF` + * `TypeMask`: 2字节位掩码,每一位代表请求的信息类型。 +* **返回**: + * 成功: `FEDCBA EDA0 [Data] XX 00EF` + * 失败: `FEDCBA EDA1 ...` + * 异常: `FEDCBA EDA2 ...` + +**支持的信息类型 (Bit定义)**: + +| Bit | 信息内容 | 长度 (Bytes) | 说明 | +| :--- | :--- | :--- | :--- | +| 0 | 当前电量 | 1 | 0~100 | +| 1 | 当前音量 | 1 | 0~3 | +| 2 | SD卡挂载状态 | 1 | 0:未挂载, 1:挂载 | +| 3 | SD卡总容量 | 4 | 单位 KB | +| 4 | SD卡剩余容量 | 4 | 单位 KB | +| 5 | BLE MAC 地址 | 6 | Hex format | + +--- + +## 4. JSON 协议 (V2) + +新版协议在原有基础上增加了 JSON 数据传输模式,支持更丰富的信息交互。 + +### 4.1 帧结构 (Frame Structure) + +V2 协议使用特定的帧头 `0xC7` (App发) / `0xB0` (设备发) 包裹 JSON 数据。 + +| 字段 | 长度 (Bytes) | 说明 | +| :--- | :--- | :--- | +| **HEAD** | 1 | `0xC7` (App->Dev) / `0xB0` (Dev->App) | +| **TYPE** | 1 | 命令字 (如 `0x0D`) | +| **Subpage Total** | 2 | 分包总数 (大端) | +| **Current Page** | 2 | 当前包序号 (大端) | +| **Data Len** | 2 | 数据长度 (大端) | +| **DATA** | N | JSON 字符串 (UTF-8) | +| **CHECKSUM** | 1 | 校验和 (0 - Sum(0...End-1)) | + +### 4.2 命令定义 (JSON Payload) + +以下为 Data 字段中的 JSON 内容示例。 + +#### 4.2.1 激活状态 (0x01) +* **APP -> 设备**: (查询) +* **设备 -> APP**: + ```json + { + "type": 0x01, + "state": 0x00 // 0:未激活, 1:已激活 + } + ``` + +#### 4.2.2 协议版本 (0x07) +* **设备 -> APP**: + ```json + { + "type": 0x07, + "version": "2024.01" + } + ``` + +#### 4.2.3 时间同步 (0x08) +* **APP -> 设备**: + ```json + { + "type": 0x08, + "year": 2024, + "mon": 1, + "day": 1, + "hour": 12, + "min": 0, + "mes": 0 + } + ``` + +#### 4.2.4 设备信息上报 (0x0D) +* **设备 -> APP**: + ```json + { + "type": 0x0d, + "allspace": 1000, + "freespace": 500, + "devname": "Loom", + "size": 0, // 0:圆屏, 1:方屏 + "brand": 5 + } + ``` + +#### 4.2.5 身份核对 (0x0E) +* **APP -> 设备** (发送 12位码): + ```json + { + "type": 0x0e, + "IdCheck": "xxxxxxxxxxxx" + } + ``` +* **设备 -> APP** (返回结果): + ```json + { + "type": 0x0e, + "Ret": 1 // 0:错误, 1:正确 + } + ``` diff --git a/app.json b/app.json index 2fc0a0b..029ee9e 100644 --- a/app.json +++ b/app.json @@ -1,6 +1,7 @@ { "expo": { "name": "expo-ble-app", + "owner": "bowong", "slug": "expo-ble-app", "version": "1.0.0", "orientation": "portrait", @@ -19,7 +20,14 @@ "monochromeImage": "./assets/images/android-icon-monochrome.png" }, "edgeToEdgeEnabled": true, - "predictiveBackGestureEnabled": false + "predictiveBackGestureEnabled": false, + "permissions": [ + "android.permission.BLUETOOTH", + "android.permission.BLUETOOTH_ADMIN", + "android.permission.BLUETOOTH_CONNECT", + "android.permission.RECORD_AUDIO" + ], + "package": "com.shuohigh.expobleapp" }, "web": { "output": "static", @@ -38,11 +46,34 @@ "backgroundColor": "#000000" } } + ], + [ + "react-native-ble-plx", + { + "isBackgroundEnabled": true, + "modes": [ + "peripheral", + "central" + ], + "bluetoothAlwaysPermission": "Allow $(PRODUCT_NAME) to connect to bluetooth devices" + } + ], + [ + "expo-image-picker", + { + "photosPermission": "The app accesses your photos to let you share them with your friends." + } ] ], "experiments": { "typedRoutes": true, "reactCompiler": true + }, + "extra": { + "router": {}, + "eas": { + "projectId": "9adec1c5-cf51-4d78-8d4c-ca38a80334e1" + } } } } diff --git a/app/(tabs)/explore.tsx b/app/(tabs)/explore.tsx index 71518f9..18280aa 100644 --- a/app/(tabs)/explore.tsx +++ b/app/(tabs)/explore.tsx @@ -1,112 +1,415 @@ -import { Image } from 'expo-image'; -import { Platform, StyleSheet } from 'react-native'; - -import { Collapsible } from '@/components/ui/collapsible'; -import { ExternalLink } from '@/components/external-link'; +import React from 'react'; +import {Alert, StyleSheet, Button, Switch, ScrollView} from 'react-native'; +import {ThemedText} from '@/components/themed-text'; +import {ThemedView} from '@/components/themed-view'; +import {useBleExplorer} from '@/hooks/useBleExplorer'; +import {useBlePeripheral} from '@/hooks/useBlePeripheral'; +import {BLE_UUIDS, PROTOCOL_VERSION} from '@/ble'; import ParallaxScrollView from '@/components/parallax-scroll-view'; -import { ThemedText } from '@/components/themed-text'; -import { ThemedView } from '@/components/themed-view'; -import { IconSymbol } from '@/components/ui/icon-symbol'; -import { Fonts } from '@/constants/theme'; +import {IconSymbol} from '@/components/ui/icon-symbol'; + export default function TabTwoScreen() { - return ( - - }> - - - Explore - - - This app includes example code to help you get started. - - - This app has two screens:{' '} - app/(tabs)/index.tsx and{' '} - app/(tabs)/explore.tsx - - - The layout file in app/(tabs)/_layout.tsx{' '} - sets up the tab navigator. - - - Learn more - - - - - You can open this project on Android, iOS, and the web. To open the web version, press{' '} - w in the terminal running this project. - - - - - For static images, you can use the @2x and{' '} - @3x suffixes to provide files for - different screen densities - - - - Learn more - - - - - This template has light and dark mode support. The{' '} - useColorScheme() hook lets you inspect - what the user's current color scheme is, and so you can adjust UI colors accordingly. - - - Learn more - - - - - This template includes an example of an animated component. The{' '} - components/HelloWave.tsx component uses - the powerful{' '} - - react-native-reanimated - {' '} - library to create a waving hand animation. - - {Platform.select({ - ios: ( - - The components/ParallaxScrollView.tsx{' '} - component provides a parallax effect for the header image. - - ), - })} - - - ); + const [deviceMode, setDeviceMode] = React.useState<'central' | 'peripheral'>('central'); + + + const { + isScanning, + isConnected, + connectedDevice, + deviceInfo, + version, + isActivated, + transferProgress, + isTransferring, + discoveredDevices, + loading, + error, + startScan, + stopScan, + connectToDevice, + disconnectDevice, + queryActivationStatus, + queryDeviceVersion, + requestDeviceInfo, + updateActivationTime, + sendIdentityCheck, + transferSampleFile, + clearLogs, + } = useBleExplorer(); + + const { + isAdvertising, + connectedCentralCount, + logs: peripheralLogs, + deviceInfo: peripheralDeviceInfo, + loading: peripheralLoading, + error: peripheralError, + startAdvertising, + stopAdvertising, + getCharacteristicReadValue, + updateDeviceInfo, + resetDeviceInfo, + clearLogs: clearPeripheralLogs, + } = useBlePeripheral(); + + + return ( + + }> + + BLE Explorer + + + {/* Device Mode Selection */} + + Device Mode + + Central + { + if (value) { + // Switching to peripheral mode - stop central operations first + console.log('Switching to peripheral mode - stopping central operations'); + if (isScanning) { + await stopScan(); + // Add small delay to ensure cleanup + await new Promise(resolve => setTimeout(resolve, 500)); + } + if (isConnected) { + await disconnectDevice(); + // Add small delay to ensure cleanup + await new Promise(resolve => setTimeout(resolve, 500)); + } + } else { + // Switching to central mode - stop peripheral operations first + console.log('Switching to central mode - stopping peripheral operations'); + if (isAdvertising) { + await stopAdvertising(); + // Add small delay to ensure cleanup + await new Promise(resolve => setTimeout(resolve, 500)); + } + } + setDeviceMode(value ? 'peripheral' : 'central'); + }} + disabled={isScanning || isConnected || isAdvertising || peripheralLoading.advertising} + /> + Peripheral + + + {deviceMode === 'central' + ? 'Central mode: Scan for and connect to BLE peripherals' + : `Peripheral mode: Act as BLE receiver`} + + + + {deviceMode === 'central' ? ( + <> + {/* Connection Status */} + + Connection Status + Scanning: {isScanning ? 'Yes' : 'No'} + Connected: {isConnected ? 'Yes' : 'No'} + Device: {connectedDevice?.name || 'None'} + {error && Error: {error}} + + + {/* Discovered Devices */} + + Discovered Devices + Total devices + found: {discoveredDevices.length} + {discoveredDevices.length === 0 ? ( + No devices discovered yet. Start scanning to find devices. + ) : ( + + {discoveredDevices.map((item) => ( + + + + {item.name || 'Unknown Device'} + + {item.id} + {item.serviceUUIDs && item.serviceUUIDs.length > 0 && ( + + Services: {item.serviceUUIDs.join(', ')} + + )} + {item.connected && ( + Connected + )} + +