expo-ble模块测试demo 联调BLE模块, 文件发送通过校验,去除多余的ANI文件缓存步骤

This commit is contained in:
Yudi Xiao 2025-12-11 18:33:28 +08:00
parent eab4d172e6
commit e3ee0f607c
7 changed files with 82 additions and 82 deletions

View File

@ -28,8 +28,7 @@ export default function TabTwoScreen() {
requestDeviceInfo, requestDeviceInfo,
updateActivationTime, updateActivationTime,
sendIdentityCheck, sendIdentityCheck,
transferSampleFile, transferMedia,
transferEmptyTestPackage,
} = useBleExplorer(); } = useBleExplorer();
@ -164,12 +163,7 @@ export default function TabTwoScreen() {
<ThemedView style={styles.buttonRow}> <ThemedView style={styles.buttonRow}>
<Button <Button
title={loading.transferring ? 'Transferring...' : 'Transfer File'} title={loading.transferring ? 'Transferring...' : 'Transfer File'}
onPress={transferSampleFile} onPress={transferMedia}
disabled={!isConnected || loading.transferring}
/>
<Button
title={loading.transferring ? 'Transferring...' : 'Transfer Test'}
onPress={transferEmptyTestPackage}
disabled={!isConnected || loading.transferring} disabled={!isConnected || loading.transferring}
/> />
</ThemedView> </ThemedView>

View File

@ -1,7 +1,7 @@
import {BleManager, Device, Characteristic, BleError as PlxError, ScanOptions} from 'react-native-ble-plx'; import {BleManager, Device, Characteristic, ScanOptions} from 'react-native-ble-plx';
import {Platform, PermissionsAndroid} from 'react-native'; import {Platform} from 'react-native';
import {BleDevice, ConnectionState, BleError, ScanResult} from './types'; import {BleDevice, ConnectionState, BleError, ScanResult} from './types';
import {BLE_UUIDS} from "@/ble"; import {BLE_UUIDS} from "../protocol/Constants";
export class BleClient { export class BleClient {
private static instance: BleClient; private static instance: BleClient;

View File

@ -1,4 +1,4 @@
import { Device, BleError as PlxBleError } from 'react-native-ble-plx'; import {Device} from 'react-native-ble-plx';
export interface BleScanInfo { export interface BleScanInfo {
rawData?: ArrayBuffer; rawData?: ArrayBuffer;

View File

@ -6,7 +6,7 @@ import {
BleClient, BleClient,
BleDevice, BleDevice,
BleError, BleError,
BleProtocolService, APP_COMMAND_TYPES, BleProtocolService,
ConnectionState, ConnectionState,
DeviceInfo, COMMAND_TYPES, DeviceInfo, COMMAND_TYPES,
} from '../index'; } from '../index';
@ -333,7 +333,7 @@ export const useBleExplorer = () => {
} }
}; };
const convertToANI = useCallback(async (tempDir: Directory, media: ImagePicker.ImagePickerAsset): Promise<File> => { const convertToANIAsFile = useCallback(async (tempDir: Directory, media: ImagePicker.ImagePickerAsset): Promise<File> => {
const tempFile = new File(tempDir, `${media.fileName?.split('.')[0]}.ani`) const tempFile = new File(tempDir, `${media.fileName?.split('.')[0]}.ani`)
const formData = new FormData(); const formData = new FormData();
formData.append('file', { formData.append('file', {
@ -356,7 +356,27 @@ export const useBleExplorer = () => {
return tempFile; return tempFile;
}, []); }, []);
const transferSampleFile = useCallback(async () => { const convertToANIAsBuffer = useCallback(async (tempDir: Directory, media: ImagePicker.ImagePickerAsset): Promise<ArrayBuffer> => {
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()
console.debug(`Converted video size : ${content.byteLength} bytes`);
return content;
}, []);
const transferMedia = useCallback(async () => {
if (!state.connectedDevice) { if (!state.connectedDevice) {
setError('No device connected'); setError('No device connected');
return; return;
@ -368,25 +388,22 @@ export const useBleExplorer = () => {
console.debug(`[${state.connectedDevice.id}] processing ${medias.length} files...`); console.debug(`[${state.connectedDevice.id}] processing ${medias.length} files...`);
for (const media of medias) { for (const media of medias) {
if (media.type === 'video') { if (media.type === 'video') {
let tempFile: File; // let tempFile: File;
let tempBuffer: ArrayBuffer;
console.debug(`Converting video: ${media.fileName || 'video'}...`); console.debug(`Converting video: ${media.fileName || 'video'}...`);
setState(prev => ({...prev, loading: {...prev.loading, converting: true}})); setState(prev => ({...prev, loading: {...prev.loading, converting: true}}));
tempFile = await convertToANI(tempDir, media) // tempFile = await convertToANIAsFile(tempDir, media)
tempBuffer = await convertToANIAsBuffer(tempDir, media)
setState(prev => ({...prev, loading: {...prev.loading, converting: false}})); setState(prev => ({...prev, loading: {...prev.loading, converting: false}}));
console.log(`Transferring converted file to device...`); console.log(`Transferring converted file to device...`);
await fileTransferService.transferFile( await fileTransferService.transferFile(state.connectedDevice.id, tempBuffer, COMMAND_TYPES.TRANSFER_ANI_VIDEO, (progress) => {
state.connectedDevice.id,
tempFile.uri,
COMMAND_TYPES.TRANSFER_ANI_VIDEO,
(progress) => {
setState(prev => ({...prev, transferProgress: progress * 100})); setState(prev => ({...prev, transferProgress: progress * 100}));
// Optional: throttle logs to avoid spam // Optional: throttle logs to avoid spam
if (Math.round(progress * 100) % 10 === 0) { if (Math.round(progress * 100) % 10 === 0) {
// addLog(`Transfer progress: ${Math.round(progress * 100)}%`); // addLog(`Transfer progress: ${Math.round(progress * 100)}%`);
} }
} });
); // tempFile.delete();
tempFile.delete();
console.log(`Transfer successful`); console.log(`Transfer successful`);
} else if (media.type === 'image') { } else if (media.type === 'image') {
try { try {
@ -397,18 +414,15 @@ export const useBleExplorer = () => {
if (isGif) { if (isGif) {
console.debug(`Converting GIF to ANI: ${media.fileName || 'gif'}...`); console.debug(`Converting GIF to ANI: ${media.fileName || 'gif'}...`);
const tempFile = await convertToANI(tempDir, media) // const tempFile = await convertToANIAsFile(tempDir, media)
const tempBuffer = await convertToANIAsBuffer(tempDir, media)
console.log(`Transferring converted file to device...`); console.log(`Transferring converted file to device...`);
await fileTransferService.transferFile( await fileTransferService.transferFile(state.connectedDevice.id, tempBuffer, COMMAND_TYPES.TRANSFER_ANI_VIDEO, (progress) => {
state.connectedDevice.id,
tempFile.uri,
COMMAND_TYPES.TRANSFER_ANI_VIDEO,
(progress) => {
setState(prev => ({...prev, transferProgress: progress * 100})); setState(prev => ({...prev, transferProgress: progress * 100}));
} }
); );
console.log(`Transfer successful`); console.log(`Transfer successful`);
tempFile.delete(); // tempFile.delete();
console.log(`Cleaned up temp file`); console.log(`Cleaned up temp file`);
return; return;
} }
@ -456,20 +470,7 @@ export const useBleExplorer = () => {
console.log(`Unsupported media type: ${media.type}`); console.log(`Unsupported media type: ${media.type}`);
} }
} }
}, [state.connectedDevice, fileTransferService, convertToANI, setError]); }, [state.connectedDevice, fileTransferService, convertToANIAsBuffer, setError]);
const transferEmptyTestPackage = useCallback(async () => {
if (!state.connectedDevice) {
setError('No device connected');
return;
}
await fileTransferService.transferTestPackage(
state.connectedDevice.id,
(progress) => {
setState(prev => ({...prev, transferProgress: progress * 100}));
}
)
}, [fileTransferService, setError, state.connectedDevice])
const clearLogs = useCallback(() => setState(prev => ({...prev, logs: []})), []); const clearLogs = useCallback(() => setState(prev => ({...prev, logs: []})), []);
@ -482,8 +483,7 @@ export const useBleExplorer = () => {
requestDeviceInfo, requestDeviceInfo,
queryDeviceVersion, queryDeviceVersion,
queryActivationStatus, queryActivationStatus,
transferSampleFile, transferMedia,
transferEmptyTestPackage,
clearLogs, clearLogs,
updateActivationTime: () => { updateActivationTime: () => {
}, },

View File

@ -162,7 +162,7 @@ export class BleProtocolService {
console.debug(`[BleProtocolService] Sending with MTU=${mtu}, maxDataSize=${safeMaxDataSize}`); console.debug(`[BleProtocolService] Sending with MTU=${mtu}, maxDataSize=${safeMaxDataSize}`);
const rawPayloadHex = payload.reduce((acc, val) => acc + val.toString(16).padStart(2, '0') + ' ', ''); const rawPayloadHex = payload.reduce((acc, val) => acc + val.toString(16).padStart(2, '0') + ' ', '');
const formattedRawPayloadHex = rawPayloadHex.substring(0, 512) + "\n......\n" + rawPayloadHex.substring(rawPayloadHex.length - 512); const formattedRawPayloadHex = rawPayloadHex.substring(0, 512 * 2) + "\n......\n" + rawPayloadHex.substring(rawPayloadHex.length - (512 * 2));
console.debug(`[BleProtocolService] Sending payload size=${payload.byteLength}, raw payload hex=\n${formattedRawPayloadHex}`); console.debug(`[BleProtocolService] Sending payload size=${payload.byteLength}, raw payload hex=\n${formattedRawPayloadHex}`);
const frames = ProtocolManager.createFrame(type, payload, FRAME_CONSTANTS.HEAD_APP_TO_DEVICE, safeMaxDataSize); const frames = ProtocolManager.createFrame(type, payload, FRAME_CONSTANTS.HEAD_APP_TO_DEVICE, safeMaxDataSize);
const total = frames.length; const total = frames.length;

View File

@ -1,5 +1,5 @@
import {BleProtocolService} from './BleProtocolService'; import {BleProtocolService} from './BleProtocolService';
import {APP_COMMAND_TYPES, COMMAND_TYPES, RESPONSE_TYPES} from '../protocol/Constants'; import {COMMAND_TYPES, RESPONSE_TYPES} from '../protocol/Constants';
import {DeviceInfo, ActivationStatus} from '../protocol/types'; import {DeviceInfo, ActivationStatus} from '../protocol/types';
import {Buffer} from 'buffer'; import {Buffer} from 'buffer';

View File

@ -16,22 +16,28 @@ export class FileTransferService {
return FileTransferService.instance; return FileTransferService.instance;
} }
public async transferFile(deviceId: string, filePath: string, type: APP_COMMAND_TYPES, onProgress?: (progress: number) => void): Promise<void> { public async transferFile(deviceId: string, file: string | ArrayBuffer, type: APP_COMMAND_TYPES, onProgress?: (progress: number) => void): Promise<void> {
try { try {
const response = await fetch(filePath); const startAt = Date.now();
let arrayBuffer: ArrayBuffer;
if (file instanceof ArrayBuffer) {
arrayBuffer = file
} else {
const response = await fetch(file);
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to load file: ${response.statusText}`); throw new Error(`Failed to load file: ${response.statusText}`);
} }
const blob = await response.blob(); const blob = await response.blob();
const reader = new FileReader(); const reader = new FileReader();
const arrayBuffer = await new Promise<ArrayBuffer>((resolve, reject) => { arrayBuffer = await new Promise<ArrayBuffer>((resolve, reject) => {
reader.onload = () => resolve(reader.result as ArrayBuffer); reader.onload = () => resolve(reader.result as ArrayBuffer);
reader.onerror = reject; reader.onerror = reject;
reader.readAsArrayBuffer(blob); reader.readAsArrayBuffer(blob);
}); });
}
await this.protocol.send(deviceId, type, arrayBuffer, onProgress); await this.protocol.send(deviceId, type, arrayBuffer, onProgress);
const transferredAt = Date.now();
console.debug(`File transferred in ${(transferredAt - startAt) / 1000} s`);
} catch (e) { } catch (e) {
console.error("File transfer failed", e); console.error("File transfer failed", e);
throw e; throw e;