expo-ble模块测试demo 联调BLE模块
This commit is contained in:
@@ -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;
|
||||
477
ble/hooks/useBleExplorer.ts
Normal file
477
ble/hooks/useBleExplorer.ts
Normal file
@@ -0,0 +1,477 @@
|
||||
import {useCallback, useEffect, useState} from 'react';
|
||||
import {Alert, PermissionsAndroid, Platform} from 'react-native';
|
||||
import {
|
||||
ActivationStatus,
|
||||
BLE_UUIDS,
|
||||
BleClient,
|
||||
BleDevice,
|
||||
BleError,
|
||||
BleProtocolService, COMMAND_TYPES,
|
||||
ConnectionState,
|
||||
DeviceInfo,
|
||||
} 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';
|
||||
import * as ImageManipulator from 'expo-image-manipulator';
|
||||
|
||||
interface BleState {
|
||||
isScanning: boolean;
|
||||
isConnected: boolean;
|
||||
connectedDevice: BleDevice | null;
|
||||
deviceInfo: DeviceInfo | null;
|
||||
version: string;
|
||||
isActivated: boolean;
|
||||
transferProgress: number;
|
||||
isTransferring: boolean;
|
||||
discoveredDevices: BleDevice[];
|
||||
loading: {
|
||||
connecting: boolean;
|
||||
querying: boolean;
|
||||
transferring: boolean;
|
||||
};
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
|
||||
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,
|
||||
connectedDevice: null,
|
||||
deviceInfo: null,
|
||||
version: '',
|
||||
isActivated: false,
|
||||
transferProgress: 0,
|
||||
isTransferring: false,
|
||||
discoveredDevices: [],
|
||||
loading: {
|
||||
connecting: false,
|
||||
querying: false,
|
||||
transferring: false,
|
||||
},
|
||||
error: null,
|
||||
});
|
||||
|
||||
const setError = useCallback(
|
||||
(error: string | null) => {
|
||||
setState(prev => ({...prev, error}));
|
||||
if (error) {
|
||||
console.error(`ERROR: ${error}`);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const requestBluetoothPermissions = useCallback(async (): Promise<boolean> => {
|
||||
if (Platform.OS !== 'android') return true;
|
||||
try {
|
||||
const permissions = [
|
||||
PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
|
||||
PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
|
||||
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
|
||||
];
|
||||
const results = await PermissionsAndroid.requestMultiple(permissions);
|
||||
const allGranted = Object.values(results).every(result => result === PermissionsAndroid.RESULTS.GRANTED);
|
||||
if (!allGranted) {
|
||||
const denied = Object.entries(results)
|
||||
.filter(([_, result]) => result !== PermissionsAndroid.RESULTS.GRANTED)
|
||||
.map(([permission]) => permission.split('.').pop());
|
||||
setError(`Bluetooth permissions required: ${denied.join(', ')}`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
setError(`Permission request failed: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}, [setError]);
|
||||
|
||||
useEffect(() => {
|
||||
if (Platform.OS === 'web') {
|
||||
console.log('BLE not supported on web platform');
|
||||
return;
|
||||
}
|
||||
|
||||
const onConnectionStateChange = ({deviceId, state: connState}: {
|
||||
deviceId: string,
|
||||
state: ConnectionState
|
||||
}) => {
|
||||
const isConnected = connState === ConnectionState.CONNECTED;
|
||||
setState(prev => {
|
||||
const updatedDevices = prev.discoveredDevices.map(d => {
|
||||
if (d.id === deviceId) {
|
||||
const newDevice = Object.assign(Object.create(Object.getPrototypeOf(d)), d) as BleDevice;
|
||||
newDevice.connected = isConnected;
|
||||
return newDevice;
|
||||
}
|
||||
return d;
|
||||
});
|
||||
|
||||
return {
|
||||
...prev,
|
||||
isConnected,
|
||||
connectedDevice: connState === ConnectionState.DISCONNECTED ? null : prev.connectedDevice,
|
||||
discoveredDevices: updatedDevices,
|
||||
};
|
||||
});
|
||||
console.log(`Connection state ${deviceId}: ${connState}`);
|
||||
};
|
||||
|
||||
const onScanError = (error: BleError) => {
|
||||
setError(`Scan error: ${error.message}`);
|
||||
setState(prev => ({...prev, isScanning: false}));
|
||||
};
|
||||
|
||||
bleClient.addListener('connectionStateChange', onConnectionStateChange);
|
||||
bleClient.addListener('scanError', onScanError);
|
||||
|
||||
const onDeviceInfo = (info: DeviceInfo) => {
|
||||
setState(prev => ({...prev, deviceInfo: info}));
|
||||
console.log(`Device info received: ${info.devname}`);
|
||||
};
|
||||
|
||||
const onActivationStatus = (status: ActivationStatus) => {
|
||||
const activated = status.state === 1;
|
||||
setState(prev => ({...prev, isActivated: activated}));
|
||||
console.log(`Activation status: ${activated ? 'Activated' : 'Not activated'}`);
|
||||
};
|
||||
|
||||
deviceInfoService.addListener('deviceInfo', onDeviceInfo);
|
||||
deviceInfoService.addListener('activationStatus', onActivationStatus);
|
||||
|
||||
return () => {
|
||||
bleClient.removeListener('connectionStateChange', onConnectionStateChange);
|
||||
bleClient.removeListener('scanError', onScanError);
|
||||
deviceInfoService.removeListener('deviceInfo', onDeviceInfo);
|
||||
deviceInfoService.removeListener('activationStatus', onActivationStatus);
|
||||
};
|
||||
}, [bleClient, deviceInfoService, setError]);
|
||||
|
||||
const startScan = useCallback(async () => {
|
||||
if (Platform.OS === 'web') return;
|
||||
try {
|
||||
setError(null);
|
||||
console.log('Starting scan...');
|
||||
const hasPerms = await requestBluetoothPermissions();
|
||||
if (!hasPerms) return;
|
||||
|
||||
setState(prev => ({...prev, isScanning: true, discoveredDevices: []}));
|
||||
|
||||
// Check for already connected devices
|
||||
try {
|
||||
const relevantDevices = await bleClient.getConnectedDevices([BLE_UUIDS.SERVICE]);
|
||||
|
||||
if (relevantDevices.length > 0) {
|
||||
console.log(`Found ${relevantDevices.length} system-connected devices`);
|
||||
setState(prev => {
|
||||
// Avoid duplicates if any
|
||||
const newDevices = relevantDevices.filter(
|
||||
nd => !prev.discoveredDevices.some(ed => ed.id === nd.id)
|
||||
);
|
||||
newDevices.forEach(d => {
|
||||
d.connected = false;
|
||||
}); // Treat as disconnected until explicit connect
|
||||
return {
|
||||
...prev,
|
||||
discoveredDevices: [...prev.discoveredDevices, ...newDevices]
|
||||
};
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Failed to check connected devices", e);
|
||||
}
|
||||
await bleClient.startScan(
|
||||
// [BLE_UUIDS.SERVICE],
|
||||
null,
|
||||
{scanMode: ScanMode.LowLatency, allowDuplicates: false},
|
||||
(result) => {
|
||||
setState(prev => {
|
||||
const device = result.device as BleDevice;
|
||||
|
||||
// Filter only for service UUID
|
||||
if (device.name?.startsWith("707")) {
|
||||
// continue
|
||||
} else if (!device.serviceUUIDs) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
if (prev.discoveredDevices.find(d => d.id === device.id)) return prev;
|
||||
|
||||
device.connected = false;
|
||||
|
||||
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, setError]);
|
||||
|
||||
const stopScan = useCallback(() => {
|
||||
bleClient.stopScan();
|
||||
setState(prev => ({...prev, isScanning: false}));
|
||||
console.log('Scan stopped');
|
||||
}, [bleClient]);
|
||||
|
||||
const connectToDevice = useCallback(async (device: BleDevice) => {
|
||||
try {
|
||||
stopScan();
|
||||
setState(prev => ({...prev, loading: {...prev.loading, connecting: true}}));
|
||||
console.log(`Connecting to ${device.name}...`);
|
||||
|
||||
const connectedDevice = await bleClient.connect(device.id) as BleDevice;
|
||||
connectedDevice.connected = true;
|
||||
|
||||
// Scan and log all services and characteristics
|
||||
try {
|
||||
const services = await connectedDevice.services();
|
||||
for (const service of services) {
|
||||
console.log(`[Service] ${service.uuid}`);
|
||||
const characteristics = await service.characteristics();
|
||||
for (const char of characteristics) {
|
||||
const props = [
|
||||
char.isReadable ? 'Read' : '',
|
||||
char.isWritableWithResponse ? 'Write' : '',
|
||||
char.isWritableWithoutResponse ? 'WriteNoResp' : '',
|
||||
char.isNotifiable ? 'Notify' : '',
|
||||
char.isIndicatable ? 'Indicate' : ''
|
||||
].filter(Boolean).join(',');
|
||||
console.log(` -> [Char] ${char.uuid} (${props})`);
|
||||
}
|
||||
}
|
||||
} catch (scanError) {
|
||||
console.error(`Error scanning services: ${scanError}`);
|
||||
}
|
||||
|
||||
await protocolService.initialize(device.id);
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
connectedDevice,
|
||||
isConnected: true,
|
||||
loading: {...prev.loading, connecting: false}
|
||||
}));
|
||||
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, setError]);
|
||||
|
||||
const disconnectDevice = useCallback(async () => {
|
||||
try {
|
||||
console.log('Disconnecting...');
|
||||
await bleClient.disconnect();
|
||||
protocolService.disconnect();
|
||||
} catch (e: any) {
|
||||
console.error(`Disconnect failed: ${e.message}`);
|
||||
}
|
||||
}, [bleClient, protocolService]);
|
||||
|
||||
const requestDeviceInfo = useCallback(async () => {
|
||||
if (!state.connectedDevice) return;
|
||||
try {
|
||||
console.log(`[${state.connectedDevice.id}] Requesting Device Info...`);
|
||||
await deviceInfoService.queryDeviceInfo(state.connectedDevice.id);
|
||||
console.log(`[${state.connectedDevice.id}] Device Info query request sent.`);
|
||||
} catch (e: any) {
|
||||
setError(`Request failed: ${e.message}`);
|
||||
}
|
||||
}, [deviceInfoService, state.connectedDevice, setError]);
|
||||
|
||||
const queryActivationStatus = useCallback(async () => {
|
||||
if (!state.connectedDevice) return;
|
||||
try {
|
||||
console.log('Querying Activation Status...');
|
||||
await deviceInfoService.queryActivationStatus(state.connectedDevice.id);
|
||||
} catch (e: any) {
|
||||
setError(`Query failed: ${e.message}`);
|
||||
}
|
||||
}, [deviceInfoService, state.connectedDevice, setError]);
|
||||
|
||||
const queryDeviceVersion = useCallback(async () => {
|
||||
if (!state.connectedDevice) return;
|
||||
try {
|
||||
console.log(`[${state.connectedDevice.id}] Requesting Device Version...`);
|
||||
await deviceInfoService.queryDeviceVersion(state.connectedDevice.id);
|
||||
console.log(`[${state.connectedDevice.id}] Device Version query request sent`);
|
||||
} catch (e: any) {
|
||||
setError(`Request failed: ${e.message}`);
|
||||
}
|
||||
}, [state.connectedDevice, deviceInfoService, setError])
|
||||
|
||||
const pickImage = async () => {
|
||||
// No permissions request is necessary for launching the image library.
|
||||
// Manually request permissions for videos on iOS when `allowsEditing` is set to `false`
|
||||
// and `videoExportPreset` is `'Passthrough'` (the default), ideally before launching the picker
|
||||
// so the app users aren't surprised by a system dialog after picking a video.
|
||||
// See "Invoke permissions for videos" sub section for more details.
|
||||
const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||
|
||||
if (!permissionResult.granted) {
|
||||
Alert.alert('Permission required', 'Permission to access the media library is required.');
|
||||
return [];
|
||||
}
|
||||
|
||||
let result = await ImagePicker.launchImageLibraryAsync({
|
||||
mediaTypes: ['images', 'videos'],
|
||||
allowsEditing: false,
|
||||
aspect: [1, 1],
|
||||
quality: 1.0,
|
||||
});
|
||||
if (!result.canceled) {
|
||||
return result.assets;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const convertToANI = useCallback(async (tempDir: Directory, media: ImagePicker.ImagePickerAsset): Promise<File> => {
|
||||
const tempFile = new File(tempDir, `${media.fileName}.ani`)
|
||||
const formData = new FormData();
|
||||
formData.append('file', {
|
||||
uri: media.uri,
|
||||
name: media.fileName || 'video.mp4',
|
||||
type: media.mimeType || 'video/mp4',
|
||||
} as any);
|
||||
const response = await fetch("https://bowongai-test--ani-video-converter-fastapi-app.modal.run/api/convert/ani", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
headers: {'Accept': 'multipart/form-data',}
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Conversion failed with status ${response.status}`);
|
||||
}
|
||||
const content = await response.arrayBuffer()
|
||||
tempFile.write(new Uint8Array(content));
|
||||
const tempFileInfo = tempFile.info()
|
||||
console.debug(`Converted video saved to ${tempFile.uri}, size : ${tempFileInfo.size} bytes`);
|
||||
return tempFile;
|
||||
}, []);
|
||||
|
||||
const transferSampleFile = useCallback(async () => {
|
||||
if (!state.connectedDevice) {
|
||||
setError('No device connected');
|
||||
return;
|
||||
}
|
||||
const tempDir = new Directory(Paths.cache, 'anis')
|
||||
if (!tempDir.exists) tempDir.create();
|
||||
const medias = await pickImage();
|
||||
if (medias.length === 0) return;
|
||||
console.debug(`[${state.connectedDevice.id}] processing ${medias.length} files...`);
|
||||
for (const media of medias) {
|
||||
if (media.type === 'video') {
|
||||
let tempFile: File;
|
||||
console.debug(`Converting video: ${media.fileName || 'video'}...`);
|
||||
tempFile = await convertToANI(tempDir, media)
|
||||
console.log(`Transferring converted file to device...`);
|
||||
await fileTransferService.transferFile(
|
||||
state.connectedDevice.id,
|
||||
tempFile.uri,
|
||||
COMMAND_TYPES.TRANSFER_ANI_VIDEO,
|
||||
(progress) => {
|
||||
setState(prev => ({...prev, transferProgress: progress * 100}));
|
||||
// Optional: throttle logs to avoid spam
|
||||
if (Math.round(progress * 100) % 10 === 0) {
|
||||
// addLog(`Transfer progress: ${Math.round(progress * 100)}%`);
|
||||
}
|
||||
}
|
||||
);
|
||||
tempFile.delete();
|
||||
console.log(`Transfer successful`);
|
||||
} else if (media.type === 'image') {
|
||||
try {
|
||||
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) {
|
||||
console.debug(`Converting GIF to ANI: ${media.fileName || 'gif'}...`);
|
||||
const tempFile = await convertToANI(tempDir, media)
|
||||
console.log(`Transferring converted file to device...`);
|
||||
await fileTransferService.transferFile(
|
||||
state.connectedDevice.id,
|
||||
tempFile.uri,
|
||||
COMMAND_TYPES.TRANSFER_ANI_VIDEO,
|
||||
(progress) => {
|
||||
setState(prev => ({...prev, transferProgress: progress * 100}));
|
||||
}
|
||||
);
|
||||
console.log(`Transfer successful`);
|
||||
tempFile.delete();
|
||||
console.log(`Cleaned up temp file`);
|
||||
return;
|
||||
}
|
||||
|
||||
let imageUri = media.uri;
|
||||
|
||||
// Check if conversion is needed (if not jpeg)
|
||||
// Note: mimeType might not always be reliable, so we can also check filename extension if needed,
|
||||
// or just always run manipulator to ensure consistency.
|
||||
// Here we check if it is NOT jpeg/jpg
|
||||
const isJpeg = media.mimeType === 'image/jpeg' || media.mimeType === 'image/jpg' || media.uri.toLowerCase().endsWith('.jpg') || media.uri.toLowerCase().endsWith('.jpeg');
|
||||
|
||||
if (!isJpeg) {
|
||||
console.debug(`Converting image to JPEG...`);
|
||||
const context = ImageManipulator.ImageManipulator.manipulate(media.uri);
|
||||
const imageRef = await context.renderAsync();
|
||||
const result = await imageRef.saveAsync({
|
||||
compress: 1,
|
||||
format: ImageManipulator.SaveFormat.JPEG,
|
||||
});
|
||||
imageUri = result.uri;
|
||||
console.debug(`Conversion successful: ${imageUri}`);
|
||||
}
|
||||
|
||||
console.log(`Transferring image to device...`);
|
||||
await fileTransferService.transferFile(
|
||||
state.connectedDevice.id,
|
||||
imageUri,
|
||||
COMMAND_TYPES.TRANSFER_JPEG_IMAGE, // Assuming PHOTO_ALBUM is for images, adjust if needed
|
||||
(progress) => {
|
||||
setState(prev => ({...prev, transferProgress: progress * 100}));
|
||||
}
|
||||
);
|
||||
|
||||
// If we created a temporary file via manipulation, we might want to clean it up?
|
||||
// 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.
|
||||
|
||||
console.log(`Transfer successful`);
|
||||
} catch (e: any) {
|
||||
console.error(`Error: ${e.message}`);
|
||||
setError(e.message);
|
||||
}
|
||||
} else {
|
||||
console.log(`Unsupported media type: ${media.type}`);
|
||||
}
|
||||
}
|
||||
}, [state.connectedDevice, fileTransferService, convertToANI, setError]);
|
||||
|
||||
const clearLogs = useCallback(() => setState(prev => ({...prev, logs: []})), []);
|
||||
|
||||
return {
|
||||
...state,
|
||||
startScan,
|
||||
stopScan,
|
||||
connectToDevice,
|
||||
disconnectDevice,
|
||||
requestDeviceInfo,
|
||||
queryDeviceVersion,
|
||||
queryActivationStatus,
|
||||
transferSampleFile,
|
||||
clearLogs,
|
||||
updateActivationTime: () => {
|
||||
},
|
||||
sendIdentityCheck: () => {
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -6,3 +6,4 @@ export * from './protocol/ProtocolManager';
|
||||
export * from './services/BleProtocolService';
|
||||
export * from './services/DeviceInfoService';
|
||||
export * from './services/FileTransferService';
|
||||
export * from './hooks/useBleExplorer';
|
||||
|
||||
@@ -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 = [];
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user