expo-ble模块测试demo 联调BLE模块

This commit is contained in:
Yudi Xiao
2025-12-10 12:39:41 +08:00
parent 41f4080264
commit d2dcd39411
17 changed files with 391 additions and 2069 deletions

View File

@@ -1,3 +1,23 @@
# 项目初始化
安装 bun https://bun.com/docs/installation#windows
```bash
powershell -c "irm bun.sh/install.ps1|iex"
```
安装项目依赖
```bash
bun install
```
运行 dev项目
```bash
bun run start
`````
# 协议三端通信说明
## 1. 数据格式
@@ -7,7 +27,7 @@
所有数据包遵循以下格式(通常为大端序):
| 字段 (Field) | 长度 (Bytes) | 值 / 说明 |
| :--- | :--- | :--- |
|:-------------|:-----------|:----------------|
| **HEADER** | 3 | `FEDCBA` |
| **CMD** | 2 | 命令字 (如 `E100`) |
| **DATA** | n | 数据内容 (0 ~ n 字节) |
@@ -19,7 +39,7 @@
当接收到的数据包格式错误时,设备会返回以下错误码:
| 错误类型 | CMD (上报) | 参数 (DATA) | 完整 HEX 示例 |
| :--- | :--- | :--- | :--- |
|:------------|:---------|:----------|:----------------------------|
| HEADER 错误 | `E0E0` | `C0` | `FEDCBA E0E0 C0 00EF` |
| TAIL 错误 | `E0E0` | `C1` | `FEDCBA E0E0 C1 00EF` |
| CHECKSUM 错误 | `E0E2` | `C0` | `FEDCBA E0E2 C0 00EF` |
@@ -41,30 +61,33 @@
## 3. 命令详解
### 3.1 绑定 (0xE100)
**方向**: APP -> 设备
* **发送**: `FEDCBA E100 E1 00EF`
* **返回**:
| 状态 | CMD | 参数 | 完整 HEX 示例 |
| :--- | :--- | :--- | :--- |
|:---|:-------|:-----|:----------------------|
| 成功 | `E1A0` | `81` | `FEDCBA E1A0 81 00EF` |
| 失败 | `E1A1` | `82` | `FEDCBA E1A1 82 00EF` |
| 异常 | `E1A2` | `83` | `FEDCBA E1A2 83 00EF` |
### 3.2 绑定失败上报 (0xE200)
**方向**: APP -> 设备 (通常用于APP端绑定失败后通知设备)
* **发送**: `FEDCBA E200 XX 00EF`
* **返回**:
| 状态 | CMD | 参数 | 完整 HEX 示例 |
| :--- | :--- | :--- | :--- |
|:---|:-------|:-----|:----------------------|
| 成功 | `E2A0` | `XX` | `FEDCBA E2A0 XX 00EF` |
| 失败 | `E2A1` | `XX` | `FEDCBA E2A1 XX 00EF` |
| 异常 | `E2A2` | `XX` | `FEDCBA E2A2 XX 00EF` |
### 3.3 写用户 ID (0xE300)
**方向**: APP -> 设备
* **发送**: `FEDCBA E300 [UserID] XX 00EF`
@@ -72,12 +95,13 @@
* **返回**:
| 状态 | 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`
@@ -85,6 +109,7 @@
* `TOKEN`: 32位 (4字节) 密锁数据。
### 3.5 设置时间 (0xE500)
**方向**: APP -> 设备
* **发送**: `FEDCBA E500 [Time] XX 00EF`
@@ -92,12 +117,13 @@
* **返回**:
| 状态 | 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`
@@ -105,6 +131,7 @@
* `Version`: 格式 `yyyymmddhh` (10字节 ASCII)
### 3.7 查询资源版本号 (0xE700 / 0xD700)
**方向**: APP -> 设备
* **版本 < V4.0**:
@@ -115,28 +142,32 @@
* 返回: `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字节)
@@ -146,7 +177,7 @@
* **返回**:
| 状态 | CMD | 说明 | 附加数据 |
| :--- | :--- | :--- | :--- |
|:------|:-------|:----------|:---------------|
| 同意传输 | `EAA0` | 准备接收 | `8A` |
| 失败 | `EAA1` | 拒绝/错误 | `8B` |
| 异常 | `EAA2` | 参数错误 | `8C` |
@@ -155,6 +186,7 @@
| OTA回复 | `EAA5` | OTA 错误码 | `[ErrCode]` |
#### B. 文件数据传输 (Packet)
* **发送**: `FEDCBA EA01 [Seq] [Len] [Data] XX 00EF`
* `Seq`: 序列号 (2字节)
* `Len`: 数据长度 (2字节)
@@ -162,16 +194,19 @@
* **返回 (每包/批量确认)**: `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 信息**:
@@ -182,6 +217,7 @@
* 返回: `FEDCBA EBA7 92 00EF`
### 3.12 设备信息查询 (0xED00)
**方向**: APP -> 设备
* **发送**: `FEDCBA ED00 [TypeMask] XX 00EF`
@@ -194,7 +230,7 @@
**支持的信息类型 (Bit定义)**:
| Bit | 信息内容 | 长度 (Bytes) | 说明 |
| :--- | :--- | :--- | :--- |
|:----|:-----------|:-----------|:------------|
| 0 | 当前电量 | 1 | 0~100 |
| 1 | 当前音量 | 1 | 0~3 |
| 2 | SD卡挂载状态 | 1 | 0:未挂载, 1:挂载 |
@@ -213,7 +249,7 @@
V2 协议使用特定的帧头 `0xC7` (App发) / `0xB0` (设备发) 包裹 JSON 数据。
| 字段 | 长度 (Bytes) | 说明 |
| :--- | :--- | :--- |
|:------------------|:-----------|:--------------------------------------|
| **HEAD** | 1 | `0xC7` (App->Dev) / `0xB0` (Dev->App) |
| **TYPE** | 1 | 命令字 (如 `0x0D`) |
| **Subpage Total** | 2 | 分包总数 (大端) |
@@ -227,6 +263,7 @@ V2 协议使用特定的帧头 `0xC7` (App发) / `0xB0` (设备发) 包裹 JSON
以下为 Data 字段中的 JSON 内容示例。
#### 4.2.1 激活状态 (0x01)
* **APP -> 设备**: (查询)
* **设备 -> APP**:
```json
@@ -237,6 +274,7 @@ V2 协议使用特定的帧头 `0xC7` (App发) / `0xB0` (设备发) 包裹 JSON
```
#### 4.2.2 协议版本 (0x07)
* **设备 -> APP**:
```json
{
@@ -246,6 +284,7 @@ V2 协议使用特定的帧头 `0xC7` (App发) / `0xB0` (设备发) 包裹 JSON
```
#### 4.2.3 时间同步 (0x08)
* **APP -> 设备**:
```json
{
@@ -260,6 +299,7 @@ V2 协议使用特定的帧头 `0xC7` (App发) / `0xB0` (设备发) 包裹 JSON
```
#### 4.2.4 设备信息上报 (0x0D)
* **设备 -> APP**:
```json
{
@@ -273,6 +313,7 @@ V2 协议使用特定的帧头 `0xC7` (App发) / `0xB0` (设备发) 包裹 JSON
```
#### 4.2.5 身份核对 (0x0E)
* **APP -> 设备** (发送 12位码):
```json
{

View File

@@ -1,18 +1,13 @@
import React from 'react';
import {Alert, StyleSheet, Button, Switch, ScrollView} from 'react-native';
import {StyleSheet, Button} 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 {BLE_UUIDS, PROTOCOL_VERSION, useBleExplorer} from '@/ble';
import ParallaxScrollView from '@/components/parallax-scroll-view';
import {IconSymbol} from '@/components/ui/icon-symbol';
export default function TabTwoScreen() {
const [deviceMode, setDeviceMode] = React.useState<'central' | 'peripheral'>('central');
const {
isScanning,
isConnected,
@@ -35,24 +30,8 @@ export default function TabTwoScreen() {
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 (
<ParallaxScrollView
@@ -69,51 +48,6 @@ export default function TabTwoScreen() {
<ThemedText type="title">BLE Explorer</ThemedText>
</ThemedView>
{/* Device Mode Selection */}
<ThemedView style={styles.section}>
<ThemedText type="subtitle">Device Mode</ThemedText>
<ThemedView style={styles.modeSwitchContainer}>
<ThemedText style={styles.modeLabel}>Central</ThemedText>
<Switch
value={deviceMode === 'peripheral'}
onValueChange={async (value) => {
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}
/>
<ThemedText style={styles.modeLabel}>Peripheral</ThemedText>
</ThemedView>
<ThemedText style={styles.modeDescription}>
{deviceMode === 'central'
? 'Central mode: Scan for and connect to BLE peripherals'
: `Peripheral mode: Act as BLE receiver`}
</ThemedText>
</ThemedView>
{deviceMode === 'central' ? (
<>
{/* Connection Status */}
<ThemedView style={styles.section}>
<ThemedText type="subtitle">Connection Status</ThemedText>
@@ -234,75 +168,6 @@ export default function TabTwoScreen() {
/>
</ThemedView>
</ThemedView>
</>
) : (
<>
{/* Peripheral Status */}
<ThemedView style={styles.section}>
<ThemedText type="subtitle">Peripheral Status</ThemedText>
<ThemedText>Advertising: {isAdvertising ? 'Yes' : 'No'}</ThemedText>
<ThemedText>Connected Centrals: {connectedCentralCount}</ThemedText>
{peripheralError && <ThemedText style={styles.errorText}>Error: {peripheralError}</ThemedText>}
</ThemedView>
{/* Device Information */}
<ThemedView style={styles.section}>
<ThemedText type="subtitle">Device Information</ThemedText>
<ThemedText>Activated: {peripheralDeviceInfo.activated ? 'Yes' : 'No'}</ThemedText>
<ThemedText>Version: {peripheralDeviceInfo.version || 'Unknown'}</ThemedText>
<ThemedText>Name: {peripheralDeviceInfo.devname || 'Unknown Device'}</ThemedText>
<ThemedText>Total Space: {peripheralDeviceInfo.allspace || 0} KB</ThemedText>
<ThemedText>Free Space: {peripheralDeviceInfo.freespace || 0} KB</ThemedText>
<ThemedText>Brand: {peripheralDeviceInfo.brand || 'Unknown'}</ThemedText>
</ThemedView>
{/* Peripheral Controls */}
<ThemedView style={styles.section}>
<ThemedText type="subtitle">Peripheral Controls</ThemedText>
<ThemedView style={styles.buttonRow}>
<Button
title={peripheralLoading.advertising ? 'Starting...' : 'Start Advertising'}
onPress={startAdvertising}
disabled={isAdvertising || peripheralLoading.advertising}
/>
<Button title="Stop Advertising" onPress={stopAdvertising} disabled={!isAdvertising}/>
</ThemedView>
<ThemedView style={styles.buttonRow}>
<Button
title="Test Read Response"
onPress={() => getCharacteristicReadValue(BLE_UUIDS.READ_CHARACTERISTIC)}
disabled={!isAdvertising}
/>
<Button title="Reset Device Info" onPress={resetDeviceInfo}/>
</ThemedView>
<ThemedView style={styles.buttonRow}>
<Button title="Toggle Activation"
onPress={() => updateDeviceInfo({activated: !(peripheralDeviceInfo.activated ?? false)})}/>
<Button title="Update Free Space"
onPress={() => updateDeviceInfo({freespace: Math.floor(Math.random() * 1024)})}/>
</ThemedView>
</ThemedView>
{/* Peripheral Logs */}
<ThemedView style={styles.section}>
<ThemedView style={styles.logHeader}>
<ThemedText type="subtitle">Peripheral Logs</ThemedText>
<Button title="Clear" onPress={clearPeripheralLogs}/>
</ThemedView>
<ThemedView style={styles.logContainer} lightColor="#eee" darkColor="#2a2a2a">
<ScrollView nestedScrollEnabled={false} showsVerticalScrollIndicator={true}>
{peripheralLogs.map((log, index) => (
<ThemedText key={index} style={styles.logText}>
{log}
</ThemedText>
))}
</ScrollView>
</ThemedView>
</ThemedView>
</>
)}
{/* Protocol Info - Available in both modes */}
<ThemedView style={styles.section}>

View File

@@ -1,372 +0,0 @@
import React, { useState, useEffect } from 'react';
import { View, Text, Button, Alert, ScrollView, StyleSheet } from 'react-native';
import {
BleManager,
DeviceInfoManagerV2,
FileTransferManager,
CommandUtils,
PROTOCOL_UUIDS,
APP_COMMAND_TYPES,
DEVICE_RESPONSE_TYPES,
BleEventCallback,
DeviceInfoListener,
FileTransferListener,
DeviceInfo,
ActivationStatus,
VersionInfo,
BleDevice,
} from '../index';
/**
* Example component demonstrating BLE V2.1.1 protocol usage
* This component shows how to use the new JSON-based BLE protocol
*/
const BleExampleV2: React.FC = () => {
const [isScanning, setIsScanning] = useState(false);
const [isConnected, setIsConnected] = useState(false);
const [connectedDevice, setConnectedDevice] = useState<BleDevice | null>(null);
const [deviceInfo, setDeviceInfo] = useState<DeviceInfo | null>(null);
const [version, setVersion] = useState<string>('');
const [isActivated, setIsActivated] = useState<boolean>(false);
const [transferProgress, setTransferProgress] = useState<number>(0);
const [isTransferring, setIsTransferring] = useState<boolean>(false);
const [logs, setLogs] = useState<string[]>([]);
// Managers
const bleManager = BleManager.getInstance();
const deviceInfoManager = DeviceInfoManagerV2.getInstance();
const fileTransferManager = FileTransferManager.getInstance();
// Add log message
const addLog = (message: string) => {
const timestamp = new Date().toLocaleTimeString();
setLogs(prev => [...prev, `[${timestamp}] ${message}`]);
};
// Setup event listeners
useEffect(() => {
const bleCallback: BleEventCallback = {
onDiscoveryStateChange: (scanning: boolean) => {
setIsScanning(scanning);
addLog(`Scanning: ${scanning ? 'Started' : 'Stopped'}`);
},
onDeviceDiscovered: (device) => {
addLog(`Device discovered: ${device.name || 'Unknown'} (${device.id})`);
},
onConnectionStateChange: (deviceId, status) => {
setIsConnected(status === 'Connected');
addLog(`Connection ${status}: ${deviceId}`);
if (status === 'Disconnected') {
setConnectedDevice(null);
}
},
onDataReceived: (deviceId, serviceUuid, characteristicUuid, data) => {
console.log('onDataReceived', deviceId, serviceUuid, characteristicUuid, data);
addLog(`Data received from ${deviceId} (${data.byteLength} bytes)`);
},
};
const deviceInfoCallback: DeviceInfoListener = {
onDeviceInfoReceived: (info) => {
setDeviceInfo(info);
addLog(`Device info received: ${info.devname}`);
},
onVersionReceived: (version) => {
setVersion(version);
addLog(`Version: ${version}`);
},
onActivationStatusReceived: (activated) => {
setIsActivated(activated);
addLog(`Activation status: ${activated ? 'Activated' : 'Not activated'}`);
},
onIdentityCheckReceived: (isValid) => {
addLog(`Identity check: ${isValid ? 'Valid' : 'Invalid'}`);
},
};
const fileTransferCallback: FileTransferListener = {
onStart: () => {
setIsTransferring(true);
setTransferProgress(0);
addLog('File transfer started');
},
onProgress: (progress) => {
setTransferProgress(progress);
addLog(`Transfer progress: ${progress}%`);
},
onComplete: () => {
setIsTransferring(false);
setTransferProgress(0);
addLog('File transfer completed');
},
onFail: (code, message) => {
setIsTransferring(false);
setTransferProgress(0);
addLog(`Transfer failed: ${message} (${code})`);
},
};
bleManager.registerCallback(bleCallback);
deviceInfoManager.addListener(deviceInfoCallback);
fileTransferManager.addListener(fileTransferCallback);
return () => {
bleManager.unregisterCallback(bleCallback);
deviceInfoManager.removeListener(deviceInfoCallback);
fileTransferManager.removeListener(fileTransferCallback);
};
}, []);
// Start scanning for devices
const startScan = async () => {
try {
addLog('Starting BLE scan...');
await bleManager.startScan();
} catch (error) {
addLog(`Scan failed: ${error}`);
}
};
// Stop scanning
const stopScan = async () => {
try {
addLog('Stopping BLE scan...');
await bleManager.stopScan();
} catch (error) {
addLog(`Stop scan failed: ${error}`);
}
};
// Connect to device (for demo, connect to first discovered device)
const connectToDevice = async () => {
try {
addLog('Connecting to device...');
// In real app, you would select a specific device
// For demo, we'll show the connection flow
Alert.alert('Connect', 'Please select a device from the scan results first');
} catch (error) {
addLog(`Connection failed: ${error}`);
}
};
// Disconnect from device
const disconnectDevice = async () => {
try {
addLog('Disconnecting from device...');
await bleManager.disconnectDevice();
} catch (error) {
addLog(`Disconnect failed: ${error}`);
}
};
// Query activation status
const queryActivationStatus = async () => {
try {
addLog('Querying activation status...');
await deviceInfoManager.queryActivationStatus();
} catch (error) {
addLog(`Activation query failed: ${error}`);
}
};
// Query version
const queryVersion = async () => {
try {
addLog('Querying device version...');
await deviceInfoManager.queryVersion();
} catch (error) {
addLog(`Version query failed: ${error}`);
}
};
// Request device info
const requestDeviceInfo = async () => {
try {
addLog('Requesting device info...');
await deviceInfoManager.requestDeviceInfoReport();
} catch (error) {
addLog(`Device info request failed: ${error}`);
}
};
// Update activation time to current time
const updateActivationTime = async () => {
try {
addLog('Updating activation time...');
await CommandUtils.sendCurrentTimeUpdate();
} catch (error) {
addLog(`Time update failed: ${error}`);
}
};
// Send identity check response
const sendIdentityCheck = (isValid: boolean) => {
try {
addLog(`Sending identity check response: ${isValid}`);
deviceInfoManager.sendIdentityCheckResponse(isValid);
} catch (error) {
addLog(`Identity check failed: ${error}`);
}
};
// Transfer sample file (demo)
const transferSampleFile = async () => {
try {
addLog('Starting sample file transfer...');
// In real app, you would provide actual file path
Alert.alert('File Transfer', 'Please provide a valid file path for transfer');
} catch (error) {
addLog(`File transfer failed: ${error}`);
}
};
// Clear logs
const clearLogs = () => {
setLogs([]);
};
return (
<ScrollView style={styles.container}>
<Text style={styles.title}>BLE V2.1.1 Protocol Example</Text>
{/* Connection Status */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Connection Status</Text>
<Text>Scanning: {isScanning ? 'Yes' : 'No'}</Text>
<Text>Connected: {isConnected ? 'Yes' : 'No'}</Text>
<Text>Device: {connectedDevice?.name || 'None'}</Text>
</View>
{/* Device Info */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Device Information</Text>
<Text>Activated: {isActivated ? 'Yes' : 'No'}</Text>
<Text>Version: {version || 'Unknown'}</Text>
{deviceInfo && (
<>
<Text>Name: {deviceInfo.devname}</Text>
<Text>Total Space: {deviceInfo.allspace} KB</Text>
<Text>Free Space: {deviceInfo.freespace} KB</Text>
<Text>Brand: {deviceInfo.brand}</Text>
</>
)}
</View>
{/* Transfer Status */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>File Transfer</Text>
<Text>Transferring: {isTransferring ? 'Yes' : 'No'}</Text>
<Text>Progress: {transferProgress}%</Text>
</View>
{/* Control Buttons */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Controls</Text>
<View style={styles.buttonRow}>
<Button
title={isScanning ? "Stop Scan" : "Start Scan"}
onPress={isScanning ? stopScan : startScan}
/>
<Button
title={isConnected ? "Disconnect" : "Connect"}
onPress={isConnected ? disconnectDevice : connectToDevice}
disabled={!isConnected && !isScanning}
/>
</View>
<View style={styles.buttonRow}>
<Button title="Query Activation" onPress={queryActivationStatus} disabled={!isConnected} />
<Button title="Query Version" onPress={queryVersion} disabled={!isConnected} />
</View>
<View style={styles.buttonRow}>
<Button title="Device Info" onPress={requestDeviceInfo} disabled={!isConnected} />
<Button title="Update Time" onPress={updateActivationTime} disabled={!isConnected} />
</View>
<View style={styles.buttonRow}>
<Button title="Identity Check (Valid)" onPress={() => sendIdentityCheck(true)} disabled={!isConnected} />
<Button title="Identity Check (Invalid)" onPress={() => sendIdentityCheck(false)} disabled={!isConnected} />
</View>
<View style={styles.buttonRow}>
<Button title="Transfer File" onPress={transferSampleFile} disabled={!isConnected} />
<Button title="Clear Logs" onPress={clearLogs} />
</View>
</View>
{/* Protocol Info */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Protocol Information</Text>
<Text>Version: 2.1.1</Text>
<Text>Service UUID: {PROTOCOL_UUIDS.SERVICE}</Text>
<Text>Write UUID: {PROTOCOL_UUIDS.WRITE_CHARACTERISTIC}</Text>
<Text>Read UUID: {PROTOCOL_UUIDS.READ_CHARACTERISTIC}</Text>
</View>
{/* Logs */}
<View style={styles.section}>
<View style={styles.logHeader}>
<Text style={styles.sectionTitle}>Logs</Text>
<Button title="Clear" onPress={clearLogs} />
</View>
<ScrollView style={styles.logContainer} nestedScrollEnabled={true}>
{logs.map((log, index) => (
<Text key={index} style={styles.logText}>{log}</Text>
))}
</ScrollView>
</View>
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
backgroundColor: '#f5f5f5',
},
title: {
fontSize: 20,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 20,
},
section: {
backgroundColor: 'white',
padding: 16,
marginBottom: 16,
borderRadius: 8,
elevation: 2,
},
sectionTitle: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 8,
},
buttonRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 8,
},
logHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
},
logContainer: {
height: 200,
backgroundColor: '#f8f8f8',
padding: 8,
borderRadius: 4,
},
logText: {
fontSize: 12,
fontFamily: 'monospace',
marginBottom: 2,
},
});
export default BleExampleV2;

View File

@@ -9,9 +9,9 @@ import {
BleProtocolService, COMMAND_TYPES,
ConnectionState,
DeviceInfo,
DeviceInfoService,
FileTransferService
} from '../ble/index';
} from '../index';
import {DeviceInfoService} from '../services/DeviceInfoService'
import {FileTransferService} from "../services/FileTransferService";
import {ScanMode} from "react-native-ble-plx";
import * as ImagePicker from 'expo-image-picker';
import {File, Directory, Paths} from 'expo-file-system';
@@ -26,7 +26,6 @@ interface BleState {
isActivated: boolean;
transferProgress: number;
isTransferring: boolean;
logs: string[];
discoveredDevices: BleDevice[];
loading: {
connecting: boolean;
@@ -37,15 +36,12 @@ interface BleState {
}
const MAX_LOGS = 100;
export const useBleExplorer = () => {
const bleClient = BleClient.getInstance();
const deviceInfoService = DeviceInfoService.getInstance();
const fileTransferService = FileTransferService.getInstance();
const protocolService = BleProtocolService.getInstance();
const [state, setState] = useState<BleState>({
isScanning: false,
isConnected: false,
@@ -55,7 +51,6 @@ export const useBleExplorer = () => {
isActivated: false,
transferProgress: 0,
isTransferring: false,
logs: [],
discoveredDevices: [],
loading: {
connecting: false,
@@ -65,25 +60,13 @@ export const useBleExplorer = () => {
error: null,
});
const addLog = useCallback((message: string) => {
const timestamp = new Date().toLocaleTimeString();
console.log(message);
setState(prev => {
const newLogs = [...prev.logs, `[${timestamp}] ${message}`];
const trimmedLogs = newLogs.length > MAX_LOGS ? newLogs.slice(-MAX_LOGS) : newLogs;
return {...prev, logs: trimmedLogs};
});
}, []);
const setError = useCallback(
(error: string | null) => {
setState(prev => ({...prev, error}));
if (error) {
addLog(`ERROR: ${error}`);
console.error(`ERROR: ${error}`);
}
},
[addLog]
);
}, []);
const requestBluetoothPermissions = useCallback(async (): Promise<boolean> => {
if (Platform.OS !== 'android') return true;
@@ -111,7 +94,7 @@ export const useBleExplorer = () => {
useEffect(() => {
if (Platform.OS === 'web') {
addLog('BLE not supported on web platform');
console.log('BLE not supported on web platform');
return;
}
@@ -137,7 +120,7 @@ export const useBleExplorer = () => {
discoveredDevices: updatedDevices,
};
});
addLog(`Connection state ${deviceId}: ${connState}`);
console.log(`Connection state ${deviceId}: ${connState}`);
};
const onScanError = (error: BleError) => {
@@ -150,13 +133,13 @@ export const useBleExplorer = () => {
const onDeviceInfo = (info: DeviceInfo) => {
setState(prev => ({...prev, deviceInfo: info}));
addLog(`Device info received: ${info.devname}`);
console.log(`Device info received: ${info.devname}`);
};
const onActivationStatus = (status: ActivationStatus) => {
const activated = status.state === 1;
setState(prev => ({...prev, isActivated: activated}));
addLog(`Activation status: ${activated ? 'Activated' : 'Not activated'}`);
console.log(`Activation status: ${activated ? 'Activated' : 'Not activated'}`);
};
deviceInfoService.addListener('deviceInfo', onDeviceInfo);
@@ -168,13 +151,13 @@ export const useBleExplorer = () => {
deviceInfoService.removeListener('deviceInfo', onDeviceInfo);
deviceInfoService.removeListener('activationStatus', onActivationStatus);
};
}, [bleClient, deviceInfoService, addLog, setError]);
}, [bleClient, deviceInfoService, setError]);
const startScan = useCallback(async () => {
if (Platform.OS === 'web') return;
try {
setError(null);
addLog('Starting scan...');
console.log('Starting scan...');
const hasPerms = await requestBluetoothPermissions();
if (!hasPerms) return;
@@ -185,7 +168,7 @@ export const useBleExplorer = () => {
const relevantDevices = await bleClient.getConnectedDevices([BLE_UUIDS.SERVICE]);
if (relevantDevices.length > 0) {
addLog(`Found ${relevantDevices.length} system-connected devices`);
console.log(`Found ${relevantDevices.length} system-connected devices`);
setState(prev => {
// Avoid duplicates if any
const newDevices = relevantDevices.filter(
@@ -222,26 +205,26 @@ export const useBleExplorer = () => {
device.connected = false;
addLog(`Device found: ${device.name} (${device.id}), serviceUUIDs: ${device.serviceUUIDs}`);
console.debug(`Device found: ${device.name} (${device.id}), serviceUUIDs: ${device.serviceUUIDs}`);
return {...prev, discoveredDevices: [...prev.discoveredDevices, device]};
});
});
} catch (e: any) {
setError(`Start scan failed: ${e.message}`);
}
}, [bleClient, requestBluetoothPermissions, addLog, setError]);
}, [bleClient, requestBluetoothPermissions, setError]);
const stopScan = useCallback(() => {
bleClient.stopScan();
setState(prev => ({...prev, isScanning: false}));
addLog('Scan stopped');
}, [bleClient, addLog]);
console.log('Scan stopped');
}, [bleClient]);
const connectToDevice = useCallback(async (device: BleDevice) => {
try {
stopScan();
setState(prev => ({...prev, loading: {...prev.loading, connecting: true}}));
addLog(`Connecting to ${device.name}...`);
console.log(`Connecting to ${device.name}...`);
const connectedDevice = await bleClient.connect(device.id) as BleDevice;
connectedDevice.connected = true;
@@ -250,7 +233,7 @@ export const useBleExplorer = () => {
try {
const services = await connectedDevice.services();
for (const service of services) {
addLog(`[Service] ${service.uuid}`);
console.log(`[Service] ${service.uuid}`);
const characteristics = await service.characteristics();
for (const char of characteristics) {
const props = [
@@ -260,11 +243,11 @@ export const useBleExplorer = () => {
char.isNotifiable ? 'Notify' : '',
char.isIndicatable ? 'Indicate' : ''
].filter(Boolean).join(',');
addLog(` -> [Char] ${char.uuid} (${props})`);
console.log(` -> [Char] ${char.uuid} (${props})`);
}
}
} catch (scanError) {
addLog(`Error scanning services: ${scanError}`);
console.error(`Error scanning services: ${scanError}`);
}
await protocolService.initialize(device.id);
@@ -275,54 +258,54 @@ export const useBleExplorer = () => {
isConnected: true,
loading: {...prev.loading, connecting: false}
}));
addLog('Connected and Protocol initialized');
console.log('Connected and Protocol initialized');
} catch (e: any) {
setError(`Connection failed: ${e.message}`);
setState(prev => ({...prev, loading: {...prev.loading, connecting: false}}));
}
}, [bleClient, protocolService, stopScan, addLog, setError]);
}, [bleClient, protocolService, stopScan, setError]);
const disconnectDevice = useCallback(async () => {
try {
addLog('Disconnecting...');
console.log('Disconnecting...');
await bleClient.disconnect();
protocolService.disconnect();
} catch (e: any) {
addLog(`Disconnect failed: ${e.message}`);
console.error(`Disconnect failed: ${e.message}`);
}
}, [bleClient, protocolService, addLog]);
}, [bleClient, protocolService]);
const requestDeviceInfo = useCallback(async () => {
if (!state.connectedDevice) return;
try {
addLog(`[${state.connectedDevice.id}] Requesting Device Info...`);
console.log(`[${state.connectedDevice.id}] Requesting Device Info...`);
await deviceInfoService.queryDeviceInfo(state.connectedDevice.id);
addLog(`[${state.connectedDevice.id}] Device Info query request sent.`);
console.log(`[${state.connectedDevice.id}] Device Info query request sent.`);
} catch (e: any) {
setError(`Request failed: ${e.message}`);
}
}, [deviceInfoService, state.connectedDevice, addLog, setError]);
}, [deviceInfoService, state.connectedDevice, setError]);
const queryActivationStatus = useCallback(async () => {
if (!state.connectedDevice) return;
try {
addLog('Querying Activation Status...');
console.log('Querying Activation Status...');
await deviceInfoService.queryActivationStatus(state.connectedDevice.id);
} catch (e: any) {
setError(`Query failed: ${e.message}`);
}
}, [deviceInfoService, state.connectedDevice, addLog, setError]);
}, [deviceInfoService, state.connectedDevice, setError]);
const queryDeviceVersion = useCallback(async () => {
if (!state.connectedDevice) return;
try {
addLog(`[${state.connectedDevice.id}] Requesting Device Version...`);
console.log(`[${state.connectedDevice.id}] Requesting Device Version...`);
await deviceInfoService.queryDeviceVersion(state.connectedDevice.id);
addLog(`[${state.connectedDevice.id}] Device Version query request sent`);
console.log(`[${state.connectedDevice.id}] Device Version query request sent`);
} catch (e: any) {
setError(`Request failed: ${e.message}`);
}
}, [state.connectedDevice, deviceInfoService, addLog, setError])
}, [state.connectedDevice, deviceInfoService, setError])
const pickImage = async () => {
// No permissions request is necessary for launching the image library.
@@ -366,11 +349,12 @@ export const useBleExplorer = () => {
if (!response.ok) {
throw new Error(`Conversion failed with status ${response.status}`);
}
tempFile.write(await response.bytes());
const content = await response.arrayBuffer()
tempFile.write(new Uint8Array(content));
const tempFileInfo = tempFile.info()
addLog(`Converted video saved to ${tempFile.uri}, size : ${tempFileInfo.size} bytes`);
console.debug(`Converted video saved to ${tempFile.uri}, size : ${tempFileInfo.size} bytes`);
return tempFile;
}, [addLog]);
}, []);
const transferSampleFile = useCallback(async () => {
if (!state.connectedDevice) {
@@ -381,13 +365,13 @@ export const useBleExplorer = () => {
if (!tempDir.exists) tempDir.create();
const medias = await pickImage();
if (medias.length === 0) return;
addLog(`[${state.connectedDevice.id}] processing ${medias.length} files...`);
console.debug(`[${state.connectedDevice.id}] processing ${medias.length} files...`);
for (const media of medias) {
if (media.type === 'video') {
let tempFile: File;
addLog(`Converting video: ${media.fileName || 'video'}...`);
console.debug(`Converting video: ${media.fileName || 'video'}...`);
tempFile = await convertToANI(tempDir, media)
addLog(`Transferring converted file to device...`);
console.log(`Transferring converted file to device...`);
await fileTransferService.transferFile(
state.connectedDevice.id,
tempFile.uri,
@@ -401,18 +385,18 @@ export const useBleExplorer = () => {
}
);
tempFile.delete();
addLog(`Transfer successful`);
console.log(`Transfer successful`);
} else if (media.type === 'image') {
try {
addLog(`Processing image: ${media.fileName || 'image'}...`);
console.debug(`Processing image: ${media.fileName || 'image'}...`);
// Check if it is a GIF
const isGif = media.mimeType === 'image/gif' || media.uri.toLowerCase().endsWith('.gif');
if (isGif) {
addLog(`Converting GIF to ANI: ${media.fileName || 'gif'}...`);
console.debug(`Converting GIF to ANI: ${media.fileName || 'gif'}...`);
const tempFile = await convertToANI(tempDir, media)
addLog(`Transferring converted file to device...`);
console.log(`Transferring converted file to device...`);
await fileTransferService.transferFile(
state.connectedDevice.id,
tempFile.uri,
@@ -421,9 +405,9 @@ export const useBleExplorer = () => {
setState(prev => ({...prev, transferProgress: progress * 100}));
}
);
addLog(`Transfer successful`);
console.log(`Transfer successful`);
tempFile.delete();
addLog(`Cleaned up temp file`);
console.log(`Cleaned up temp file`);
return;
}
@@ -436,7 +420,7 @@ export const useBleExplorer = () => {
const isJpeg = media.mimeType === 'image/jpeg' || media.mimeType === 'image/jpg' || media.uri.toLowerCase().endsWith('.jpg') || media.uri.toLowerCase().endsWith('.jpeg');
if (!isJpeg) {
addLog(`Converting image to JPEG...`);
console.debug(`Converting image to JPEG...`);
const context = ImageManipulator.ImageManipulator.manipulate(media.uri);
const imageRef = await context.renderAsync();
const result = await imageRef.saveAsync({
@@ -444,10 +428,10 @@ export const useBleExplorer = () => {
format: ImageManipulator.SaveFormat.JPEG,
});
imageUri = result.uri;
addLog(`Conversion successful: ${imageUri}`);
console.debug(`Conversion successful: ${imageUri}`);
}
addLog(`Transferring image to device...`);
console.log(`Transferring image to device...`);
await fileTransferService.transferFile(
state.connectedDevice.id,
imageUri,
@@ -461,16 +445,16 @@ export const useBleExplorer = () => {
// ImageManipulator results are usually in cache, which OS cleans up, but we can't easily delete if we don't own it.
// For this flow, we just leave it.
addLog(`Transfer successful`);
console.log(`Transfer successful`);
} catch (e: any) {
addLog(`Error: ${e.message}`);
console.error(`Error: ${e.message}`);
setError(e.message);
}
} else {
console.log(`Unsupported media type: ${media.type}`);
}
}
}, [state.connectedDevice, fileTransferService, convertToANI, setError, addLog]);
}, [state.connectedDevice, fileTransferService, convertToANI, setError]);
const clearLogs = useCallback(() => setState(prev => ({...prev, logs: []})), []);

View File

@@ -6,3 +6,4 @@ export * from './protocol/ProtocolManager';
export * from './services/BleProtocolService';
export * from './services/DeviceInfoService';
export * from './services/FileTransferService';
export * from './hooks/useBleExplorer';

View File

@@ -1,472 +0,0 @@
/**
* BLE Peripheral Manager for real peripheral functionality
* Handles advertising, characteristic setup, and communication with central devices
*
* SETUP REQUIREMENTS:
* 1. npm install buffer react-native-multi-ble-peripheral
* 2. npx expo prebuild (to generate native code)
* 3. Test on physical device (simulator doesn't support peripheral mode)
* 4. Android 12+ requires BLUETOOTH_ADVERTISE permission (handled in app.json)
* 5. iOS requires NSBluetoothPeripheralUsageDescription (handled in app.json)
*/
import {Platform} from 'react-native';
import Peripheral, {Permission, Property} from 'react-native-multi-ble-peripheral';
import {
BLE_UUIDS as PROTOCOL_UUIDS,
COMMAND_TYPES as APP_COMMAND_TYPES,
DeviceInfo,
FRAME_CONSTANTS as PROTOCOL_FRAME,
PeripheralConnectionEvent,
PeripheralReadRequest,
PeripheralWriteRequest,
RESPONSE_TYPES as DEVICE_RESPONSE_TYPES
} from '../index';
import {ProtocolUtilsV2} from '../utils/ProtocolUtilsV2';
import {Buffer} from 'buffer';
// Re-export DeviceInfo for useBlePeripheral hook
export type {DeviceInfo};
export interface PeripheralEventCallback {
onAdvertisingStateChange?: (isAdvertising: boolean, error?: string) => void;
onCentralConnected?: (centralId: string) => void;
onCentralDisconnected?: (centralId: string) => void;
onCharacteristicRead?: (centralId: string, characteristicUuid: string, data: ArrayBuffer) => void;
onCharacteristicWrite?: (centralId: string, characteristicUuid: string, data: ArrayBuffer) => void;
}
export class BlePeripheralManager {
private static instance: BlePeripheralManager;
private callbacks: Set<PeripheralEventCallback> = new Set();
private peripheral: Peripheral | null = null;
private isAdvertising = false;
private isReady = false;
private connectedCentrals = new Set<string>();
private deviceInfo: DeviceInfo;
private operationQueue: Array<() => Promise<void>> = [];
// Default device info - matches the DeviceInfo interface from BleTypesV2
private readonly DEFAULT_DEVICE_INFO: DeviceInfo = {
devname: 'BLE Test Device',
allspace: 1024,
freespace: 512,
size: 1, // Square screen
brand: 1, // Vendor
version: '2.1.1', // Firmware version
activated: true, // Activation status
};
private constructor() {
this.deviceInfo = {...this.DEFAULT_DEVICE_INFO};
this.initializePeripheral();
}
public static getInstance(): BlePeripheralManager {
if (!BlePeripheralManager.instance) {
BlePeripheralManager.instance = new BlePeripheralManager();
}
return BlePeripheralManager.instance;
}
private async initializePeripheral(): Promise<void> {
try {
if (Platform.OS === 'web') {
console.log('BlePeripheralManager: BLE peripheral not supported on web platform');
return;
}
this.peripheral = new Peripheral();
console.log('BlePeripheralManager: Peripheral instance created');
// Set up event listeners
this.peripheral.on('ready', this.handleReady.bind(this));
this.peripheral.on('connect', this.handleCentralConnected.bind(this));
this.peripheral.on('disconnect', this.handleCentralDisconnected.bind(this));
this.peripheral.on('read', this.handleCharacteristicRead.bind(this));
this.peripheral.on('write', this.handleCharacteristicWrite.bind(this));
console.log('BlePeripheralManager: Event listeners set up');
} catch (error) {
console.error('BlePeripheralManager: Failed to initialize peripheral', error);
this.notifyAdvertisingStateChange(false, error instanceof Error ? error.message : 'Unknown error');
}
}
private async handleReady(): Promise<void> {
console.log('BlePeripheralManager: Peripheral ready');
this.isReady = true;
// Execute queued operations
while (this.operationQueue.length > 0) {
const operation = this.operationQueue.shift();
if (operation) {
try {
await operation();
} catch (error) {
console.error('BlePeripheralManager: Queued operation failed', error);
}
}
}
}
private handleCentralConnected(central: PeripheralConnectionEvent): void {
console.log('BlePeripheralManager: Central connected', central);
const centralId = central.id || 'unknown';
this.connectedCentrals.add(centralId);
this.notifyCentralConnected(centralId);
}
private handleCentralDisconnected(central: PeripheralConnectionEvent): void {
console.log('BlePeripheralManager: Central disconnected', central);
const centralId = central.id || 'unknown';
this.connectedCentrals.delete(centralId);
this.notifyCentralDisconnected(centralId);
}
private handleCharacteristicRead(request: PeripheralReadRequest): void {
console.log('BlePeripheralManager: Characteristic read request', request);
if (request.characteristicUuid === PROTOCOL_UUIDS.READ_CHARACTERISTIC) {
// Return device info as JSON
const response = {
type: 0x02, // Device info response type
data: this.deviceInfo,
};
const jsonString = JSON.stringify(response);
// Update the characteristic value with the response
if (this.peripheral) {
this.peripheral.updateValue(
PROTOCOL_UUIDS.SERVICE,
PROTOCOL_UUIDS.READ_CHARACTERISTIC,
Buffer.from(jsonString, 'utf-8')
);
}
}
}
private handleCharacteristicWrite(request: PeripheralWriteRequest): void {
console.log('BlePeripheralManager: Characteristic write request', request);
if (request.characteristicUuid === PROTOCOL_UUIDS.WRITE_CHARACTERISTIC) {
let buffer: Buffer;
try {
buffer = Buffer.from(request.value);
} catch (e) {
console.error('BlePeripheralManager: Failed to convert value to buffer', e);
return;
}
const arrayBuffer = ProtocolUtilsV2.uint8ArrayToArrayBuffer(buffer);
// Check for V2 Binary Protocol (starts with HEAD_APP_TO_DEVICE = 0xc7)
if (buffer.length > 0 && buffer[0] === PROTOCOL_FRAME.HEAD_APP_TO_DEVICE) {
console.log('BlePeripheralManager: Received V2 binary frame');
try {
const frame = ProtocolUtilsV2.parseFrame(arrayBuffer);
if (frame && frame.data) {
const jsonData = ProtocolUtilsV2.parseJsonData(frame.data);
if (jsonData) {
console.log('BlePeripheralManager: Parsed V2 command:', jsonData);
// Handle Version Query (0x07)
if (jsonData.type === APP_COMMAND_TYPES.VERSION_QUERY) {
console.log('BlePeripheralManager: Handling VERSION_QUERY');
const response = {
type: DEVICE_RESPONSE_TYPES.VERSION_INFO,
version: this.deviceInfo.version || '2.1.1'
};
const jsonString = JSON.stringify(response);
// Use HEAD_DEVICE_TO_APP (0xB0) for response
const frames = ProtocolUtilsV2.createFrame(
DEVICE_RESPONSE_TYPES.VERSION_INFO,
jsonString,
false,
PROTOCOL_FRAME.HEAD_DEVICE_TO_APP
);
if (this.peripheral) {
for (const f of frames) {
this.peripheral.updateValue(
PROTOCOL_UUIDS.SERVICE,
PROTOCOL_UUIDS.READ_CHARACTERISTIC,
Buffer.from(f)
);
}
console.log('BlePeripheralManager: Sent VERSION_INFO response');
}
}
this.notifyCharacteristicWrite(request.centralId, request.characteristicUuid, arrayBuffer);
return;
}
}
} catch (e) {
console.error('BlePeripheralManager: Error processing V2 frame', e);
}
}
try {
const jsonString = buffer.toString('utf-8');
const command = JSON.parse(jsonString);
console.log('BlePeripheralManager: Received command type: 0x' + (command.type?.toString(16) || 'unknown'));
// Process different command types
switch (command.type) {
case 0x01: // General command
console.log('BlePeripheralManager: Processing general command');
break;
case 0x02: // Device info request
console.log('BlePeripheralManager: Device info request received');
break;
default:
console.log('BlePeripheralManager: Unknown command type:', command.type);
}
this.notifyCharacteristicWrite(request.centralId, request.characteristicUuid, arrayBuffer);
} catch (error) {
console.error('BlePeripheralManager: Failed to process write request', error);
}
}
}
private async executeOrQueue(operation: () => Promise<void>): Promise<void> {
if (this.isReady && this.peripheral) {
await operation();
} else {
this.operationQueue.push(operation);
}
}
public async startAdvertising(deviceName?: string): Promise<void> {
try {
if (Platform.OS === 'web') {
throw new Error('BLE peripheral not supported on web platform');
}
if (this.isAdvertising) {
console.log('BlePeripheralManager: Already advertising');
return;
}
if (!this.peripheral) {
throw new Error('Peripheral not initialized');
}
const currentDeviceInfo = this.getDeviceInfo();
console.log('BlePeripheralManager: Starting BLE advertising...');
console.log('Device Name: ' + currentDeviceInfo.devname);
console.log('Advertising with service UUID: ' + PROTOCOL_UUIDS.SERVICE);
// iOS platform limitation warning
if (Platform.OS === 'ios') {
console.log('⚠️ iOS: Advertising will stop when app goes to background');
}
await this.executeOrQueue(async () => {
// Set up service
await this.peripheral!.addService(PROTOCOL_UUIDS.SERVICE, true);
console.log('BlePeripheralManager: Service added successfully');
await this.peripheral!.addCharacteristic(
PROTOCOL_UUIDS.SERVICE,
PROTOCOL_UUIDS.SERVICE,
Property.BROADCAST,
Permission.READABLE
)
// Set up read characteristic
await this.peripheral!.addCharacteristic(
PROTOCOL_UUIDS.SERVICE,
PROTOCOL_UUIDS.READ_CHARACTERISTIC,
Property.READ | Property.NOTIFY,
Permission.READABLE
);
console.log('BlePeripheralManager: Read characteristic added successfully');
// Set up write characteristic
await this.peripheral!.addCharacteristic(
PROTOCOL_UUIDS.SERVICE,
PROTOCOL_UUIDS.WRITE_CHARACTERISTIC,
Property.WRITE | Property.WRITE_NO_RESPONSE,
Permission.WRITEABLE
);
console.log('BlePeripheralManager: Write characteristic added successfully');
// Set initial read characteristic value
const deviceInfoResponse = {
type: 0x02,
data: currentDeviceInfo,
};
await this.peripheral!.updateValue(
PROTOCOL_UUIDS.SERVICE,
PROTOCOL_UUIDS.READ_CHARACTERISTIC,
Buffer.from(JSON.stringify(deviceInfoResponse), 'utf-8')
);
console.log('BlePeripheralManager: Initial characteristic value set');
// Start advertising
await this.peripheral!.startAdvertising();
console.log('BlePeripheralManager: Advertising started successfully');
});
this.isAdvertising = true;
this.notifyAdvertisingStateChange(true);
} catch (error) {
console.error('BlePeripheralManager: Failed to start advertising', error);
this.notifyAdvertisingStateChange(false, error instanceof Error ? error.message : 'Unknown error');
throw error;
}
}
public async stopAdvertising(): Promise<void> {
try {
if (Platform.OS === 'web') {
return;
}
if (!this.isAdvertising) {
console.log('BlePeripheralManager: Not advertising');
return;
}
if (!this.peripheral) {
throw new Error('Peripheral not initialized');
}
await this.executeOrQueue(async () => {
await this.peripheral!.stopAdvertising();
console.log('BlePeripheralManager: Advertising stopped');
});
this.isAdvertising = false;
this.connectedCentrals.clear();
this.notifyAdvertisingStateChange(false);
} catch (error) {
console.error('BlePeripheralManager: Failed to stop advertising', error);
throw error;
}
}
public getAdvertisingState(): boolean {
return this.isAdvertising;
}
public getConnectedCentrals(): string[] {
return Array.from(this.connectedCentrals);
}
public getConnectedCentralsCount(): number {
return this.connectedCentrals.size;
}
public updateDeviceInfo(updates: Partial<DeviceInfo>): void {
this.deviceInfo = {...this.deviceInfo, ...updates};
console.log('BlePeripheralManager: Device info updated', this.deviceInfo);
// Update the characteristic value if advertising
if (this.isAdvertising && this.peripheral && this.isReady) {
const response = {
type: 0x02,
data: this.deviceInfo,
};
this.peripheral.updateValue(
PROTOCOL_UUIDS.SERVICE,
PROTOCOL_UUIDS.READ_CHARACTERISTIC,
Buffer.from(JSON.stringify(response), 'utf-8')
).catch(error => {
console.error('BlePeripheralManager: Failed to update characteristic value', error);
});
}
}
public getDeviceInfo(): DeviceInfo {
return {...this.deviceInfo};
}
public resetDeviceInfo(): void {
this.deviceInfo = {...this.DEFAULT_DEVICE_INFO};
console.log('BlePeripheralManager: Device info reset to defaults');
// Update the characteristic value if advertising
if (this.isAdvertising && this.peripheral && this.isReady) {
const response = {
type: 0x02,
data: this.deviceInfo,
};
this.peripheral.updateValue(
PROTOCOL_UUIDS.SERVICE,
PROTOCOL_UUIDS.READ_CHARACTERISTIC,
Buffer.from(JSON.stringify(response), 'utf-8')
).catch(error => {
console.error('BlePeripheralManager: Failed to update characteristic value', error);
});
}
}
public addCallback(callback: PeripheralEventCallback): void {
this.callbacks.add(callback);
}
public removeCallback(callback: PeripheralEventCallback): void {
this.callbacks.delete(callback);
}
private notifyAdvertisingStateChange(isAdvertising: boolean, error?: string): void {
this.callbacks.forEach(callback => {
try {
callback.onAdvertisingStateChange?.(isAdvertising, error);
} catch (err) {
console.error('BlePeripheralManager: Callback error', err);
}
});
}
private notifyCentralConnected(centralId: string): void {
this.callbacks.forEach(callback => {
try {
callback.onCentralConnected?.(centralId);
} catch (err) {
console.error('BlePeripheralManager: Callback error', err);
}
});
}
private notifyCentralDisconnected(centralId: string): void {
this.callbacks.forEach(callback => {
try {
callback.onCentralDisconnected?.(centralId);
} catch (err) {
console.error('BlePeripheralManager: Callback error', err);
}
});
}
private notifyCharacteristicRead(centralId: string, characteristicUuid: string, data: ArrayBuffer): void {
this.callbacks.forEach(callback => {
try {
callback.onCharacteristicRead?.(centralId, characteristicUuid, data);
} catch (err) {
console.error('BlePeripheralManager: Callback error', err);
}
});
}
private notifyCharacteristicWrite(centralId: string, characteristicUuid: string, data: ArrayBuffer): void {
this.callbacks.forEach(callback => {
try {
callback.onCharacteristicWrite?.(centralId, characteristicUuid, data);
} catch (err) {
console.error('BlePeripheralManager: Callback error', err);
}
});
}
public cleanup(): void {
this.stopAdvertising().catch(console.error);
this.callbacks.clear();
this.operationQueue = [];
}
}

View File

@@ -5,8 +5,7 @@
"main": "index.ts",
"types": "index.ts",
"scripts": {
"build": "tsc",
"test": "jest"
"build": "tsc"
},
"keywords": [
"react-native",
@@ -23,14 +22,12 @@
},
"dependencies": {
"react-native-ble-plx": "^3.5.0",
"buffer": "^6.0.3"
"buffer": "^5.7.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-native": "^0.70.0",
"@types/crypto-js": "^4.1.1",
"typescript": "^4.8.0",
"jest": "^29.0.0"
"@types/react-native": "^0.73.0",
"typescript": "^5.9.0"
},
"files": [
"index.ts",

View File

@@ -9,14 +9,14 @@ export const BLE_UUIDS = {
} as const;
export const FRAME_CONSTANTS = {
HEAD_DEVICE_TO_APP: 0xb1,
HEAD_APP_TO_DEVICE: 0xc7,
HEAD_DEVICE_TO_APP: 0xb0,
// HEAD_APP_TO_DEVICE: 0xc7,
HEAD_APP_TO_DEVICE: 0xb1,
MAX_DATA_SIZE: 496,
HEADER_SIZE: 8,
FOOTER_SIZE: 1,
FRAME_INTERVAL: 35,
} as const;
// 562包 427包 384字节
// b1 05 02 32 01 ab 01 f0 e2cfbfc2199a5ea27f028e208c0257c40e18e4631c6cb63356db638528f2d82973cb70dc331968fec529061c3761c78ffda94dea8e2f42c8116e569eccc7b5f764c2463f9f922829090f0b6a03341a6956cdb1ab38e1117ee3ea76f75b8516b87cf83c1ba91b010a3eef22a8aa26d5483b68aa2aa680996b77d7ebdc87fd88d565cc2b7134cc90fc6135394a5478527ba31a7d455605344798061797cc1eb78e2c1060f26f86f7d89be5acb6c96de2cb9e91216db74a3c221b1c9ad6afabb587c81ba9720cb2e5851d94b18ca55247191a607993d91c3886368167ab19109809a18f200ecf75738a006832517a0ffa712c4c59c3fd36d0c80800176598aa506faeb26597ac1cf5d7278f3a0bfb40fd2768680efeec6e67dfbbd628b83b1fe91860001917820e3f2b1e14bde5ebd838f6c785f50aee6ed1438bc2e924068038d68340dc5b27dbe8ca38b3c4d00e257d9e2b76ce9546283e988cc505e52a3eeeed2496c4d552826e9473ac9a93af3df73415a2fbddf9bc2d0bbfafa5330ba4a19eda04e034c31809d56f37d8800e0491385477003c436d7a60f3df41a3257fd38bbed251b087ca18ae7071758e65d9cdb907a052bad5b4cbf648627a9d28cbcc19e208891c4f7008469d1ee76032902ba4fee15ab970523a5a0f639ceeefaf59ca16ebe71b96fbc16069ef3719d139e514b0
export const COMMAND_TYPES = {
ACTIVATION_QUERY: 0x01,

View File

@@ -1,4 +1,3 @@
import {Buffer} from 'buffer';
import {ProtocolFrame} from './types';
import {FRAME_CONSTANTS} from './Constants';
@@ -11,7 +10,8 @@ export class ProtocolManager {
for (let i = 0; i < frameData.length; i++) {
sum += frameData[i];
}
return sum & 0xff;
console.debug(`[ProtocolManager] Checksum calculated: 0 - ${sum} = ${(0 - sum) & 0xff}`);
return (0 - sum) & 0xff;
}
static verifyChecksum(frameData: Uint8Array, expectedChecksum: number): boolean {

View File

@@ -43,23 +43,6 @@ export interface ActivationTimeUpdate {
mes: number;
}
export interface PeripheralConnectionEvent {
id: string;
}
export interface PeripheralReadRequest {
centralId: string;
serviceUuid: string;
characteristicUuid: string;
}
export interface PeripheralWriteRequest {
centralId: string;
serviceUuid: string;
characteristicUuid: string;
value: string;
}
export interface FileTransferData {
type: number;
data: ArrayBuffer;

View File

@@ -1,6 +1,6 @@
import {BleClient} from '../core/BleClient';
import {ProtocolManager} from '../protocol/ProtocolManager';
import {BLE_UUIDS} from '../protocol/Constants';
import {BLE_UUIDS, FRAME_CONSTANTS} from '../protocol/Constants';
import {ProtocolFrame} from '../protocol/types';
import {Buffer} from 'buffer';
import {Subscription} from 'react-native-ble-plx';
@@ -156,6 +156,7 @@ export class BleProtocolService {
console.debug(`Writing frame ${i + 1}/${total}, length = ${frame.length}`);
const base64 = Buffer.from(frame).toString('base64');
const result = await this.client.write(deviceId, BLE_UUIDS.SERVICE, BLE_UUIDS.WRITE_CHARACTERISTIC, base64, false);
await new Promise(resolve => setTimeout(resolve, FRAME_CONSTANTS.FRAME_INTERVAL));
if (onProgress) {
onProgress((i + 1) / total);
}

View File

@@ -1,80 +0,0 @@
import * as Crypto from 'expo-crypto';
/**
* Cryptographic utilities for BLE communication
* Provides proper MD5 hashing to match Android implementation
*/
export class CryptoUtils {
/**
* Calculate MD5 hash of data
* @param data - ArrayBuffer or string to hash
* @returns Promise<string> - MD5 hash as hexadecimal string
*/
public static async calculateMD5(data: ArrayBuffer | string): Promise<string> {
let dataString: string;
if (data instanceof ArrayBuffer) {
// Convert ArrayBuffer to string
const uint8Array = new Uint8Array(data);
dataString = Array.from(uint8Array)
.map(byte => String.fromCharCode(byte))
.join('');
} else {
dataString = data;
}
const hash = await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.MD5,
dataString
);
return hash.toUpperCase();
}
/**
* Calculate MD5 hash of a file
* @param file - File object to hash
* @returns Promise<string> - MD5 hash as hexadecimal string
*/
public static async calculateFileMD5(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = async (event) => {
try {
const arrayBuffer = event.target?.result as ArrayBuffer;
const hash = await this.calculateMD5(arrayBuffer);
resolve(hash);
} catch (error) {
reject(error);
}
};
reader.onerror = () => {
reject(new Error('Failed to read file for MD5 calculation'));
};
reader.readAsArrayBuffer(file);
});
}
/**
* Get partial MD5 hash (substring) to match Android implementation
* Android uses substring(8, 24) for file transfers
* @param data - Data to hash
* @returns Promise<string> - Partial MD5 hash
*/
public static async calculatePartialMD5(data: ArrayBuffer | string): Promise<string> {
const fullHash = await this.calculateMD5(data);
return fullHash.substring(8, 24); // Match Android implementation
}
/**
* Get partial file MD5 hash
* @param file - File to hash
* @returns Promise<string> - Partial MD5 hash
*/
public static async calculatePartialFileMD5(file: File): Promise<string> {
const fullHash = await this.calculateFileMD5(file);
return fullHash.substring(8, 24); // Match Android implementation
}
}

View File

@@ -1,306 +0,0 @@
import { Buffer } from 'buffer';
import {
FRAME_CONSTANTS as PROTOCOL_V2_FRAME,
ERROR_CODES as BLE_V2_ERROR_CODES
} from '../protocol/Constants';
import {
ProtocolFrame,
JsonPayload
} from '../protocol/types';
/**
* Protocol utilities for BLE V2.1.1 communication
* Handles JSON data parsing, binary framing, and checksum calculation
*/
export class ProtocolUtilsV2 {
/**
* Calculate checksum for protocol frame
* New algorithm: 0 - (sum of all bytes except checksum)
*/
public static calculateChecksum(frameData: Uint8Array): number {
let sum = 0;
for (let i = 0; i < frameData.length; i++) {
sum += frameData[i];
}
return (256 - (sum % 256)) % 256; // Equivalent to 0 - sum modulo 256
}
/**
* Verify checksum of received frame
*/
public static verifyChecksum(frameData: Uint8Array, expectedChecksum: number): boolean {
const calculatedChecksum = this.calculateChecksum(frameData);
return calculatedChecksum === expectedChecksum;
}
/**
* Convert ArrayBuffer to Uint8Array
*/
public static arrayBufferToUint8Array(buffer: ArrayBuffer): Uint8Array {
return new Uint8Array(buffer);
}
/**
* Convert Uint8Array to ArrayBuffer
*/
public static uint8ArrayToArrayBuffer(array: Uint8Array): ArrayBuffer {
return new Uint8Array(array).buffer;
}
/**
* Create protocol frame for sending to device
*/
public static createFrame(
type: number,
jsonData: string,
requireFragmentation: boolean = false,
head: number = PROTOCOL_V2_FRAME.HEAD_APP_TO_DEVICE
): ArrayBuffer[] {
const dataBytes = Buffer.from(jsonData, 'utf8');
const frames: ArrayBuffer[] = [];
if (!requireFragmentation && dataBytes.length <= PROTOCOL_V2_FRAME.MAX_DATA_SIZE) {
// Single frame
const frame = this.createSingleFrame(type, dataBytes, head);
frames.push(frame);
} else {
// Fragmented frames
const fragmentedFrames = this.createFragmentedFrames(type, dataBytes, head);
frames.push(...fragmentedFrames);
}
return frames;
}
/**
* Create a single protocol frame
*/
private static createSingleFrame(
type: number,
dataBytes: Buffer,
head: number = PROTOCOL_V2_FRAME.HEAD_APP_TO_DEVICE
): ArrayBuffer {
const frameSize = PROTOCOL_V2_FRAME.HEADER_SIZE + dataBytes.length + PROTOCOL_V2_FRAME.FOOTER_SIZE;
const frame = new Uint8Array(frameSize);
let offset = 0;
// Header
frame[offset++] = head;
frame[offset++] = type;
frame[offset++] = 0x00; // Subpage total (high byte)
frame[offset++] = 0x00; // Subpage total (low byte)
frame[offset++] = 0x00; // Current page (high byte)
frame[offset++] = 0x00; // Current page (low byte)
frame[offset++] = (dataBytes.length >> 8) & 0xFF; // Data length (high byte)
frame[offset++] = dataBytes.length & 0xFF; // Data length (low byte)
// Data
for (let i = 0; i < dataBytes.length; i++) {
frame[offset++] = dataBytes[i];
}
// Calculate and add checksum
const checksumData = frame.slice(0, -1); // All bytes except checksum
const checksum = this.calculateChecksum(checksumData);
frame[offset] = checksum;
return this.uint8ArrayToArrayBuffer(frame);
}
/**
* Create fragmented protocol frames
*/
private static createFragmentedFrames(
type: number,
dataBytes: Buffer,
head: number = PROTOCOL_V2_FRAME.HEAD_APP_TO_DEVICE
): ArrayBuffer[] {
const frames: ArrayBuffer[] = [];
const totalFrames = Math.ceil(dataBytes.length / PROTOCOL_V2_FRAME.MAX_DATA_SIZE);
for (let i = 0; i < totalFrames; i++) {
const startOffset = i * PROTOCOL_V2_FRAME.MAX_DATA_SIZE;
const endOffset = Math.min(startOffset + PROTOCOL_V2_FRAME.MAX_DATA_SIZE, dataBytes.length);
const chunkSize = endOffset - startOffset;
const chunk = dataBytes.slice(startOffset, endOffset);
const frameSize = PROTOCOL_V2_FRAME.HEADER_SIZE + chunkSize + PROTOCOL_V2_FRAME.FOOTER_SIZE;
const frame = new Uint8Array(frameSize);
let offset = 0;
// Header
frame[offset++] = head;
frame[offset++] = type;
frame[offset++] = (totalFrames >> 8) & 0xFF; // Subpage total (high byte)
frame[offset++] = totalFrames & 0xFF; // Subpage total (low byte)
frame[offset++] = (i >> 8) & 0xFF; // Current page (high byte)
frame[offset++] = i & 0xFF; // Current page (low byte)
frame[offset++] = (chunkSize >> 8) & 0xFF; // Data length (high byte)
frame[offset++] = chunkSize & 0xFF; // Data length (low byte)
// Data
for (let j = 0; j < chunkSize; j++) {
frame[offset++] = chunk[j];
}
// Calculate and add checksum
const checksumData = frame.slice(0, -1); // All bytes except checksum
const checksum = this.calculateChecksum(checksumData);
frame[offset] = checksum;
frames.push(this.uint8ArrayToArrayBuffer(frame));
}
return frames;
}
/**
* Parse received protocol frame
*/
public static parseFrame(frameData: ArrayBuffer): ProtocolFrame | null {
try {
const bytes = this.arrayBufferToUint8Array(frameData);
if (bytes.length < PROTOCOL_V2_FRAME.HEADER_SIZE + PROTOCOL_V2_FRAME.FOOTER_SIZE) {
console.error('ProtocolUtilsV2: Frame too short');
return null;
}
let offset = 0;
// Parse header
const head = bytes[offset++];
const type = bytes[offset++];
const subpageTotal = (bytes[offset++] << 8) | bytes[offset++];
const curPage = (bytes[offset++] << 8) | bytes[offset++];
const dataLen = (bytes[offset++] << 8) | bytes[offset++];
// Validate data length - allow extra bytes for BLE MTU padding
const expectedFrameSize = PROTOCOL_V2_FRAME.HEADER_SIZE + dataLen + PROTOCOL_V2_FRAME.FOOTER_SIZE;
if (bytes.length < expectedFrameSize) {
console.error(`ProtocolUtilsV2: Frame too short. Expected: ${expectedFrameSize}, Actual: ${bytes.length}`);
return null;
}
// Trim to expected size to remove BLE padding bytes
if (bytes.length > expectedFrameSize) {
console.log(`ProtocolUtilsV2: Trimming frame from ${bytes.length} to ${expectedFrameSize} bytes (removing BLE padding)`);
const trimmedBytes = bytes.slice(0, expectedFrameSize);
return this.parseFrame(this.uint8ArrayToArrayBuffer(trimmedBytes));
}
// Extract data
const dataBytes = bytes.slice(offset, offset + dataLen);
const data = this.uint8ArrayToArrayBuffer(dataBytes);
offset += dataLen;
// Verify checksum
const checksum = bytes[offset];
const frameWithoutChecksum = bytes.slice(0, -1);
if (!this.verifyChecksum(frameWithoutChecksum, checksum)) {
console.error(`ProtocolUtilsV2: Checksum mismatch. Expected: ${checksum}, Calculated: ${this.calculateChecksum(frameWithoutChecksum)}`);
return null;
}
return {
head,
type,
subpageTotal,
curPage,
dataLen,
data,
checksum
};
} catch (error) {
console.error('ProtocolUtilsV2: Error parsing frame', error);
return null;
}
}
/**
* Parse JSON data from frame
*/
public static parseJsonData(data: ArrayBuffer): JsonPayload | null {
try {
const jsonString = Buffer.from(data).toString('utf8');
const jsonData = JSON.parse(jsonString);
return jsonData;
} catch (error) {
console.error('ProtocolUtilsV2: Error parsing JSON data', error);
return null;
}
}
/**
* Convert object to JSON string
*/
public static toJsonString(obj: object): string {
return JSON.stringify(obj);
}
/**
* Convert binary data to base64 for JSON embedding
*/
public static arrayBufferToBase64(buffer: ArrayBuffer): string {
return Buffer.from(buffer).toString('base64');
}
/**
* Convert base64 string to ArrayBuffer
*/
public static base64ToArrayBuffer(base64: string): ArrayBuffer {
const buffer = Buffer.from(base64, 'base64');
return this.uint8ArrayToArrayBuffer(buffer);
}
/**
* Validate frame header
*/
public static isValidFrameHead(head: number): boolean {
return head === PROTOCOL_V2_FRAME.HEAD_DEVICE_TO_APP || head === PROTOCOL_V2_FRAME.HEAD_APP_TO_DEVICE;
}
/**
* Check if frame is from device
*/
public static isDeviceFrame(frame: ProtocolFrame): boolean {
return frame.head === PROTOCOL_V2_FRAME.HEAD_DEVICE_TO_APP;
}
/**
* Check if frame is from app
*/
public static isAppFrame(frame: ProtocolFrame): boolean {
return frame.head === PROTOCOL_V2_FRAME.HEAD_APP_TO_DEVICE;
}
/**
* Check if frame is fragmented
*/
public static isFragmentedFrame(frame: ProtocolFrame): boolean {
return frame.subpageTotal > 0;
}
/**
* Get hex string for debugging
*/
public static bytesToHex(bytes: ArrayBuffer): string {
const buffer = Buffer.from(bytes);
return buffer.toString('hex').toUpperCase();
}
/**
* Create error response
*/
public static createErrorResponse(errorCode: number, message: string): JsonPayload {
return {
type: 0xFF, // Custom error type
error: errorCode,
message
} as any;
}
}

View File

@@ -11,6 +11,7 @@
"@react-navigation/native": "^7.1.8",
"expo": "~54.0.27",
"expo-constants": "~18.0.11",
"expo-crypto": "^15.0.8",
"expo-dev-client": "~6.0.20",
"expo-file-system": "~19.0.20",
"expo-font": "~14.0.10",
@@ -840,6 +841,8 @@
"expo-constants": ["expo-constants@18.0.11", "", { "dependencies": { "@expo/config": "~12.0.11", "@expo/env": "~2.0.8" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-xnfrfZ7lHjb+03skhmDSYeFF7OU2K3Xn/lAeP+7RhkV2xp2f5RCKtOUYajCnYeZesvMrsUxOsbGOP2JXSOH3NA=="],
"expo-crypto": ["expo-crypto@15.0.8", "", { "dependencies": { "base64-js": "^1.3.0" }, "peerDependencies": { "expo": "*" } }, "sha512-aF7A914TB66WIlTJvl5J6/itejfY78O7dq3ibvFltL9vnTALJ/7LYHvLT4fwmx9yUNS6ekLBtDGWivFWnj2Fcw=="],
"expo-dev-client": ["expo-dev-client@6.0.20", "", { "dependencies": { "expo-dev-launcher": "6.0.20", "expo-dev-menu": "7.0.18", "expo-dev-menu-interface": "2.0.0", "expo-manifests": "~1.0.10", "expo-updates-interface": "~2.0.0" }, "peerDependencies": { "expo": "*" } }, "sha512-5XjoVlj1OxakNxy55j/AUaGPrDOlQlB6XdHLLWAw61w5ffSpUDHDnuZzKzs9xY1eIaogOqTOQaAzZ2ddBkdXLA=="],
"expo-dev-launcher": ["expo-dev-launcher@6.0.20", "", { "dependencies": { "ajv": "^8.11.0", "expo-dev-menu": "7.0.18", "expo-manifests": "~1.0.10" }, "peerDependencies": { "expo": "*" } }, "sha512-a04zHEeT9sB0L5EB38fz7sNnUKJ2Ar1pXpcyl60Ki8bXPNCs9rjY7NuYrDkP/irM8+1DklMBqHpyHiLyJ/R+EA=="],

View File

@@ -18,7 +18,6 @@
"buildType": "apk"
},
"ios": {
"bundleIdentifier": "com.bowong.duooomi",
"simulator": false
},
"corepack": true

View File

@@ -1,323 +0,0 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { Platform, PermissionsAndroid } from 'react-native';
import { BLE_UUIDS as PROTOCOL_UUIDS } from '../ble/index';
import { BlePeripheralManager, PeripheralEventCallback, DeviceInfo } from '@/ble/manager/BlePeripheralManager';
import { Buffer } from 'buffer';
interface PeripheralState {
isAdvertising: boolean;
connectedCentralCount: number;
logs: string[];
deviceInfo: DeviceInfo;
loading: {
advertising: boolean;
};
error: string | null;
}
const MAX_LOGS = 100;
const DEFAULT_DEVICE_INFO: DeviceInfo = {
devname: 'BLE Test Device',
version: '2.1.1',
activated: true,
brand: 0,
size: 1,
allspace: 1024,
freespace: 512,
};
export const useBlePeripheral = () => {
const [state, setState] = useState<PeripheralState>({
isAdvertising: false,
connectedCentralCount: 0,
logs: [],
deviceInfo: DEFAULT_DEVICE_INFO,
loading: {
advertising: false,
},
error: null,
});
// Ref to track peripheral manager
const peripheralManagerRef = useRef<BlePeripheralManager>(BlePeripheralManager.getInstance());
// Ref to track advertising state for cleanup
const isAdvertisingRef = useRef(false);
// Ref to track component mount state
const isMountedRef = useRef(true);
// Add log message with limit
const addLog = useCallback((message: string) => {
const timestamp = new Date().toLocaleTimeString();
setState(prev => {
const newLogs = [...prev.logs, `[${timestamp}] ${message}`];
const trimmedLogs = newLogs.length > MAX_LOGS
? newLogs.slice(-MAX_LOGS)
: newLogs;
return { ...prev, logs: trimmedLogs };
});
}, []);
// Set error with user visibility
const setError = useCallback((error: string | null) => {
setState(prev => ({ ...prev, error }));
if (error) {
addLog(`ERROR: ${error}`);
}
}, [addLog]);
// Initialize peripheral mode
useEffect(() => {
if (Platform.OS === 'web') {
addLog('BLE peripheral mode not supported on web platform');
return;
}
addLog('BLE Peripheral mode initialized (Real BLE)');
addLog('Service UUID: ' + PROTOCOL_UUIDS.SERVICE);
addLog('Write Characteristic UUID: ' + PROTOCOL_UUIDS.WRITE_CHARACTERISTIC);
addLog('Read Characteristic UUID: ' + PROTOCOL_UUIDS.READ_CHARACTERISTIC);
// Set up event callbacks
const callbacks: PeripheralEventCallback = {
onAdvertisingStateChange: (isAdvertising, error) => {
if (isMountedRef.current) {
setState(prev => ({
...prev,
isAdvertising,
loading: { ...prev.loading, advertising: false },
error: error || null
}));
addLog(`Advertising ${isAdvertising ? 'started' : 'stopped'}${error ? ` with error: ${error}` : ''}`);
}
},
onCentralConnected: (centralId) => {
if (isMountedRef.current) {
setState(prev => ({
...prev,
connectedCentralCount: peripheralManagerRef.current.getConnectedCentralsCount()
}));
addLog(`Central device connected: ${centralId}`);
}
},
onCentralDisconnected: (centralId) => {
if (isMountedRef.current) {
setState(prev => ({
...prev,
connectedCentralCount: peripheralManagerRef.current.getConnectedCentralsCount()
}));
addLog(`Central device disconnected: ${centralId}`);
}
},
onCharacteristicRead: (centralId, characteristicUuid, data) => {
if (isMountedRef.current) {
addLog(`Characteristic read by ${centralId}: ${characteristicUuid}`);
}
},
onCharacteristicWrite: (centralId, characteristicUuid, data) => {
if (isMountedRef.current) {
const jsonString = Buffer.from(data).toString('utf-8');
addLog(`Characteristic write by ${centralId}: ${characteristicUuid}`);
addLog(`Data: ${jsonString}`);
}
}
};
peripheralManagerRef.current.addCallback(callbacks);
return () => {
isMountedRef.current = false;
if (isAdvertisingRef.current) {
stopAdvertising();
}
peripheralManagerRef.current.removeCallback(callbacks);
};
}, [addLog]);
// Check and request necessary permissions for Android 12+
const checkPermissions = useCallback(async (): Promise<boolean> => {
if (Platform.OS !== 'android') {
return true; // iOS handles permissions differently
}
try {
const apiLevel = Platform.Version;
if (typeof apiLevel === 'number' && apiLevel >= 31) {
// Android 12+ (API 31+) requires multiple Bluetooth permissions for peripheral mode
const requiredPermissions = [
PermissionsAndroid.PERMISSIONS.BLUETOOTH_ADVERTISE,
PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN
];
for (const permission of requiredPermissions) {
const granted = await PermissionsAndroid.check(permission);
if (!granted) {
const result = await PermissionsAndroid.request(permission, {
title: 'Bluetooth Permission Required',
message: 'This app needs Bluetooth permissions to act as a BLE peripheral device.',
buttonNeutral: 'Ask Me Later',
buttonNegative: 'Cancel',
buttonPositive: 'OK',
});
if (result !== PermissionsAndroid.RESULTS.GRANTED) {
setError(`${permission} is required for peripheral mode`);
return false;
}
}
}
}
return true;
} catch (error) {
console.error('Permission check failed:', error);
setError('Failed to check Bluetooth permissions');
return false;
}
}, [setError]);
// Start advertising as peripheral (real BLE)
const startAdvertising = useCallback(async () => {
if (Platform.OS === 'web') {
setError('BLE peripheral not supported on web');
return;
}
if (isAdvertisingRef.current) {
addLog('Already advertising as peripheral');
return;
}
try {
setError(null);
setState(prev => ({
...prev,
loading: { ...prev.loading, advertising: true }
}));
// Check permissions first
const hasPermissions = await checkPermissions();
if (!hasPermissions) {
if (isMountedRef.current) {
setState(prev => ({
...prev,
loading: { ...prev.loading, advertising: false }
}));
}
return;
}
const currentDeviceInfo = peripheralManagerRef.current.getDeviceInfo();
addLog('Starting BLE advertising...');
addLog('Device Name: ' + currentDeviceInfo.devname);
addLog('Advertising with service UUID: ' + PROTOCOL_UUIDS.SERVICE);
// iOS platform limitation warning
if (Platform.OS === 'ios') {
addLog('⚠️ iOS: Advertising will stop when app goes to background');
}
peripheralManagerRef
await peripheralManagerRef.current.startAdvertising(currentDeviceInfo.devname);
isAdvertisingRef.current = true;
} catch (error) {
if (isMountedRef.current) {
const errorMsg = `Failed to start advertising: ${error}`;
addLog(errorMsg);
setError(errorMsg);
setState(prev => ({
...prev,
loading: { ...prev.loading, advertising: false }
}));
}
}
}, [addLog, setError, checkPermissions]);
// Stop advertising
const stopAdvertising = useCallback(async () => {
if (!isAdvertisingRef.current) {
addLog('Not currently advertising');
return;
}
try {
addLog('Stopping BLE advertising...');
await peripheralManagerRef.current.stopAdvertising();
isAdvertisingRef.current = false;
setState(prev => ({
...prev,
isAdvertising: false,
connectedCentralCount: 0
}));
addLog('BLE advertising stopped');
} catch (error) {
addLog(`Failed to stop advertising: ${error}`);
}
}, [addLog]);
// Get characteristic read value (for testing/debugging: Shows what response will be sent when central reads this characteristic)
const getCharacteristicReadValue = useCallback((characteristicUuid: string) => {
if (!isAdvertisingRef.current) {
addLog('Cannot get read value - not advertising');
return null;
}
addLog(`Getting characteristic read value: ${characteristicUuid}`);
if (characteristicUuid === PROTOCOL_UUIDS.READ_CHARACTERISTIC) {
// Return device info as JSON - use manager to get latest data instead of stale state
const response = {
type: 0x02, // Device info response type
data: peripheralManagerRef.current.getDeviceInfo()
};
addLog('Returning device info response');
return JSON.stringify(response);
}
addLog('Unknown characteristic for read');
return null;
}, [addLog]);
// Update device info
const updateDeviceInfo = useCallback((updates: Partial<DeviceInfo>) => {
peripheralManagerRef.current.updateDeviceInfo(updates);
// Sync state with manager to avoid stale state issues
setState(prev => ({
...prev,
deviceInfo: peripheralManagerRef.current.getDeviceInfo()
}));
addLog('Device info updated');
}, [addLog]);
// Reset device info
const resetDeviceInfo = useCallback(() => {
peripheralManagerRef.current.resetDeviceInfo();
// Sync state with manager to avoid stale state issues
setState(prev => ({
...prev,
deviceInfo: peripheralManagerRef.current.getDeviceInfo()
}));
addLog('Device info reset to defaults');
}, [addLog]);
// Clear logs
const clearLogs = useCallback(() => {
setState(prev => ({ ...prev, logs: [] }));
}, []);
return {
...state,
startAdvertising,
stopAdvertising,
getCharacteristicReadValue,
updateDeviceInfo,
resetDeviceInfo,
clearLogs,
};
};

View File

@@ -20,6 +20,7 @@
"@react-navigation/native": "^7.1.8",
"expo": "~54.0.27",
"expo-constants": "~18.0.11",
"expo-crypto": "^15.0.8",
"expo-dev-client": "~6.0.20",
"expo-file-system": "~19.0.20",
"expo-font": "~14.0.10",