forked from yudi_xiao/expo-ble-app-demo
expo-ble模块测试demo 联调BLE模块, 文件发送通过校验,去除多余的ANI文件缓存步骤
This commit is contained in:
parent
eab4d172e6
commit
e3ee0f607c
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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: () => {
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue