expo-ble模块测试demo 联调BLE模块 take 10 新增log 打印ani文件前后512字节

This commit is contained in:
Yudi Xiao 2025-12-11 16:42:41 +08:00
parent f816165779
commit 83cdf722ac
8 changed files with 32 additions and 29 deletions

View File

@ -16,7 +16,6 @@ export default function TabTwoScreen() {
version, version,
isActivated, isActivated,
transferProgress, transferProgress,
isTransferring,
discoveredDevices, discoveredDevices,
loading, loading,
error, error,
@ -118,7 +117,8 @@ export default function TabTwoScreen() {
{/* Transfer Status */} {/* Transfer Status */}
<ThemedView style={styles.section}> <ThemedView style={styles.section}>
<ThemedText type="subtitle">File Transfer</ThemedText> <ThemedText type="subtitle">File Transfer</ThemedText>
<ThemedText>Transferring: {isTransferring ? 'Yes' : 'No'}</ThemedText> <ThemedText>Converting: {loading.converting ? 'Yes' : 'No'}</ThemedText>
<ThemedText>Transferring: {loading.transferring ? 'Yes' : 'No'}</ThemedText>
<ThemedText>Progress: {transferProgress}%</ThemedText> <ThemedText>Progress: {transferProgress}%</ThemedText>
</ThemedView> </ThemedView>

View File

@ -96,7 +96,7 @@ export class BleClient {
requestMTU: BLE_UUIDS.REQUEST_MTU requestMTU: BLE_UUIDS.REQUEST_MTU
}); });
if (device.mtu < BLE_UUIDS.REQUEST_MTU) { if (device.mtu < BLE_UUIDS.REQUEST_MTU) {
console.log("MTU not supported, requesting default to ", BLE_UUIDS.REQUEST_MTU); console.log(`MTU ${device.mtu} not supported, requesting default to ${BLE_UUIDS.REQUEST_MTU}`);
device = await device.requestMTU(BLE_UUIDS.REQUEST_MTU); device = await device.requestMTU(BLE_UUIDS.REQUEST_MTU);
// Give some time for the stack to stabilize after MTU change // Give some time for the stack to stabilize after MTU change
await new Promise(resolve => setTimeout(resolve, 500)); await new Promise(resolve => setTimeout(resolve, 500));

View File

@ -6,9 +6,9 @@ import {
BleClient, BleClient,
BleDevice, BleDevice,
BleError, BleError,
BleProtocolService, COMMAND_TYPES, BleProtocolService, APP_COMMAND_TYPES,
ConnectionState, ConnectionState,
DeviceInfo, DeviceInfo, COMMAND_TYPES,
} from '../index'; } from '../index';
import {DeviceInfoService} from '../services/DeviceInfoService' import {DeviceInfoService} from '../services/DeviceInfoService'
import {FileTransferService} from "../services/FileTransferService"; import {FileTransferService} from "../services/FileTransferService";
@ -25,11 +25,11 @@ interface BleState {
version: string; version: string;
isActivated: boolean; isActivated: boolean;
transferProgress: number; transferProgress: number;
isTransferring: boolean;
discoveredDevices: BleDevice[]; discoveredDevices: BleDevice[];
loading: { loading: {
connecting: boolean; connecting: boolean;
querying: boolean; querying: boolean;
converting: boolean;
transferring: boolean; transferring: boolean;
}; };
error: string | null; error: string | null;
@ -50,11 +50,11 @@ export const useBleExplorer = () => {
version: '', version: '',
isActivated: false, isActivated: false,
transferProgress: 0, transferProgress: 0,
isTransferring: false,
discoveredDevices: [], discoveredDevices: [],
loading: { loading: {
connecting: false, connecting: false,
querying: false, querying: false,
converting: false,
transferring: false, transferring: false,
}, },
error: null, error: null,
@ -334,7 +334,7 @@ export const useBleExplorer = () => {
}; };
const convertToANI = useCallback(async (tempDir: Directory, media: ImagePicker.ImagePickerAsset): Promise<File> => { const convertToANI = useCallback(async (tempDir: Directory, media: ImagePicker.ImagePickerAsset): Promise<File> => {
const tempFile = new File(tempDir, `${media.fileName}.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', {
uri: media.uri, uri: media.uri,
@ -370,7 +370,9 @@ export const useBleExplorer = () => {
if (media.type === 'video') { if (media.type === 'video') {
let tempFile: File; let tempFile: File;
console.debug(`Converting video: ${media.fileName || 'video'}...`); console.debug(`Converting video: ${media.fileName || 'video'}...`);
setState(prev => ({...prev, loading: {...prev.loading, converting: true}}));
tempFile = await convertToANI(tempDir, media) tempFile = await convertToANI(tempDir, media)
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, state.connectedDevice.id,

View File

@ -2,7 +2,6 @@ export const PROTOCOL_VERSION = "1.0.0";
export const BLE_UUIDS = { export const BLE_UUIDS = {
SERVICE: '000002c4-0000-1000-8000-00805f9b34fb', SERVICE: '000002c4-0000-1000-8000-00805f9b34fb',
SERVICE_SHORT: 'ae00',
BROADCAST_CHARACTERISTIC: "000002c1-0000-1000-8000-00805f9b34fb", BROADCAST_CHARACTERISTIC: "000002c1-0000-1000-8000-00805f9b34fb",
WRITE_CHARACTERISTIC: '000002c5-0000-1000-8000-00805f9b34fb', WRITE_CHARACTERISTIC: '000002c5-0000-1000-8000-00805f9b34fb',
READ_CHARACTERISTIC: '000002c6-0000-1000-8000-00805f9b34fb', READ_CHARACTERISTIC: '000002c6-0000-1000-8000-00805f9b34fb',
@ -11,13 +10,13 @@ export const BLE_UUIDS = {
export const FRAME_CONSTANTS = { export const FRAME_CONSTANTS = {
HEAD_DEVICE_TO_APP: 0xb0, HEAD_DEVICE_TO_APP: 0xb0,
// HEAD_APP_TO_DEVICE: 0xc7,
HEAD_APP_TO_DEVICE: 0xb1, HEAD_APP_TO_DEVICE: 0xb1,
MAX_DATA_SIZE: 496, MAX_DATA_SIZE: 496,
HEADER_SIZE: 8, HEADER_SIZE: 8,
FOOTER_SIZE: 1, FOOTER_SIZE: 1,
FRAME_INTERVAL: 35, FRAME_INTERVAL: 35, // package transfer idle interval in ms, set 35 ms for ble device have enough time to process data
} as const; } as const;
export type FRAME_HEAD = typeof FRAME_CONSTANTS.HEAD_DEVICE_TO_APP | typeof FRAME_CONSTANTS.HEAD_APP_TO_DEVICE; export type FRAME_HEAD = typeof FRAME_CONSTANTS.HEAD_DEVICE_TO_APP | typeof FRAME_CONSTANTS.HEAD_APP_TO_DEVICE;
export const COMMAND_TYPES = { export const COMMAND_TYPES = {
@ -32,7 +31,8 @@ export const COMMAND_TYPES = {
DEVICE_INFO_SETTINGS: 0x0d, DEVICE_INFO_SETTINGS: 0x0d,
DEVICE_IDENTITY_CHECK: 0x0e, DEVICE_IDENTITY_CHECK: 0x0e,
} as const; } as const;
export type COMMAND_TYPES = typeof COMMAND_TYPES[keyof typeof COMMAND_TYPES];
export type APP_COMMAND_TYPES = typeof COMMAND_TYPES[keyof typeof COMMAND_TYPES];
export const RESPONSE_TYPES = { export const RESPONSE_TYPES = {
ACTIVATION_STATUS: 0x01, ACTIVATION_STATUS: 0x01,

View File

@ -1,5 +1,5 @@
import {ProtocolFrame} from './types'; import {ProtocolFrame} from './types';
import {FRAME_CONSTANTS, COMMAND_TYPES, FRAME_HEAD} from './Constants'; import {FRAME_CONSTANTS, APP_COMMAND_TYPES, FRAME_HEAD} from './Constants';
export class ProtocolManager { export class ProtocolManager {
@ -13,9 +13,7 @@ export class ProtocolManager {
for (let i = 0; i < frameData.length; i++) { for (let i = 0; i < frameData.length; i++) {
sum += frameData[i]; sum += frameData[i];
} }
const checksumV1 = (0 - sum) & 0xff
const checksum = (~sum + 1) & 0xff const checksum = (~sum + 1) & 0xff
console.debug(`[ProtocolManager] Checksum V1 calculated: 0 - ${sum} = ${checksumV1.toString(16).padStart(2, '0')}`);
console.debug(`[ProtocolManager] Checksum calculated: 0 - ${sum} = ${checksum.toString(16).padStart(2, '0')}`); console.debug(`[ProtocolManager] Checksum calculated: 0 - ${sum} = ${checksum.toString(16).padStart(2, '0')}`);
return checksum; return checksum;
} }
@ -25,10 +23,9 @@ export class ProtocolManager {
} }
static createFrame( static createFrame(
type: COMMAND_TYPES, type: APP_COMMAND_TYPES,
data: Uint8Array, data: Uint8Array,
head: FRAME_HEAD = FRAME_CONSTANTS.HEAD_APP_TO_DEVICE, head: FRAME_HEAD = FRAME_CONSTANTS.HEAD_APP_TO_DEVICE,
requireFragmentation: boolean = true,
maxDataSize: number = FRAME_CONSTANTS.MAX_DATA_SIZE maxDataSize: number = FRAME_CONSTANTS.MAX_DATA_SIZE
): Uint8Array[] { ): Uint8Array[] {
// Max pages index is 4 bytes, so we can fit up to 65535 pages of data // Max pages index is 4 bytes, so we can fit up to 65535 pages of data
@ -36,7 +33,7 @@ export class ProtocolManager {
if (data.length > maxTotalSize) { if (data.length > maxTotalSize) {
throw new Error(`Data size ${data.length} exceeds max size ${maxTotalSize}`); throw new Error(`Data size ${data.length} exceeds max size ${maxTotalSize}`);
} }
if (data.length <= maxDataSize || !requireFragmentation) { if (data.length <= maxDataSize) {
console.debug(`[ProtocolManager] Frame size ${data.length} is less than max size ${maxDataSize}, not fragmented`); console.debug(`[ProtocolManager] Frame size ${data.length} is less than max size ${maxDataSize}, not fragmented`);
return [this.createSingleFrame(type, data, head)]; return [this.createSingleFrame(type, data, head)];
} else { } else {
@ -45,7 +42,7 @@ export class ProtocolManager {
} }
} }
private static createSingleFrame(type: COMMAND_TYPES, data: Uint8Array, head: FRAME_HEAD): Uint8Array { private static createSingleFrame(type: APP_COMMAND_TYPES, data: Uint8Array, head: FRAME_HEAD): Uint8Array {
const buffer = new Uint8Array(FRAME_CONSTANTS.HEADER_SIZE + data.length + FRAME_CONSTANTS.FOOTER_SIZE); const buffer = new Uint8Array(FRAME_CONSTANTS.HEADER_SIZE + data.length + FRAME_CONSTANTS.FOOTER_SIZE);
let offset = 0; let offset = 0;
@ -78,7 +75,7 @@ export class ProtocolManager {
return buffer; return buffer;
} }
private static createFragmentedFrames(type: COMMAND_TYPES, data: Uint8Array, head: FRAME_HEAD, maxDataSize: number): Uint8Array[] { private static createFragmentedFrames(type: APP_COMMAND_TYPES, data: Uint8Array, head: FRAME_HEAD, maxDataSize: number): Uint8Array[] {
const frames: Uint8Array[] = []; const frames: Uint8Array[] = [];
const totalSize = data.length; const totalSize = data.length;
const totalPages = Math.ceil(totalSize / maxDataSize); const totalPages = Math.ceil(totalSize / maxDataSize);

View File

@ -1,6 +1,6 @@
import {BleClient} from '../core/BleClient'; import {BleClient} from '../core/BleClient';
import {ProtocolManager} from '../protocol/ProtocolManager'; import {ProtocolManager} from '../protocol/ProtocolManager';
import {BLE_UUIDS, COMMAND_TYPES, FRAME_CONSTANTS} from '../protocol/Constants'; import {BLE_UUIDS, APP_COMMAND_TYPES, FRAME_CONSTANTS} from '../protocol/Constants';
import {ProtocolFrame} from '../protocol/types'; import {ProtocolFrame} from '../protocol/types';
import {Buffer} from 'buffer'; import {Buffer} from 'buffer';
import {Subscription} from 'react-native-ble-plx'; import {Subscription} from 'react-native-ble-plx';
@ -139,13 +139,16 @@ export class BleProtocolService {
} }
} }
public async send(deviceId: string, type: COMMAND_TYPES, data: object | ArrayBuffer | Uint8Array, onProgress?: (progress: number) => void): Promise<void> { public async send(deviceId: string, type: APP_COMMAND_TYPES, data: object | ArrayBuffer | Uint8Array, onProgress?: (progress: number) => void): Promise<void> {
let payload: Uint8Array; let payload: Uint8Array;
if (data instanceof ArrayBuffer) { if (data instanceof ArrayBuffer) {
console.debug("[BleProtocolService] Sending ArrayBuffer");
payload = new Uint8Array(data); payload = new Uint8Array(data);
} else if (data instanceof Uint8Array) { } else if (data instanceof Uint8Array) {
console.debug("[BleProtocolService] Sending Uint8Array");
payload = data; payload = data;
} else { } else {
console.debug("[BleProtocolService] Sending JSON payload");
const jsonStr = JSON.stringify(data); const jsonStr = JSON.stringify(data);
payload = new Uint8Array(Buffer.from(jsonStr)); payload = new Uint8Array(Buffer.from(jsonStr));
} }
@ -158,8 +161,10 @@ export class BleProtocolService {
const safeMaxDataSize = Math.max(1, Math.min(maxPayloadSize, FRAME_CONSTANTS.MAX_DATA_SIZE)); const safeMaxDataSize = Math.max(1, Math.min(maxPayloadSize, FRAME_CONSTANTS.MAX_DATA_SIZE));
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 frames = ProtocolManager.createFrame(type, payload, FRAME_CONSTANTS.HEAD_APP_TO_DEVICE, true, safeMaxDataSize); const formattedRawPayloadHex = rawPayloadHex.substring(0, 512) + "\n......\n" + rawPayloadHex.substring(rawPayloadHex.length - 512);
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 total = frames.length; const total = frames.length;
console.debug(`Sending ${total} frames`); console.debug(`Sending ${total} frames`);
for (let i = 0; i < total; i++) { for (let i = 0; i < total; i++) {
@ -171,8 +176,7 @@ export class BleProtocolService {
if (onProgress) { if (onProgress) {
onProgress((i + 1) / total); onProgress((i + 1) / total);
} }
// console.debug("Wrote frame", result);
console.debug("Wrote frame", result);
} }
} }
} }

View File

@ -1,5 +1,5 @@
import {BleProtocolService} from './BleProtocolService'; import {BleProtocolService} from './BleProtocolService';
import {COMMAND_TYPES, RESPONSE_TYPES} from '../protocol/Constants'; import {APP_COMMAND_TYPES, 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

@ -1,5 +1,5 @@
import {BleProtocolService} from './BleProtocolService'; import {BleProtocolService} from './BleProtocolService';
import {COMMAND_TYPES} from '../protocol/Constants'; import {APP_COMMAND_TYPES, COMMAND_TYPES} from '../protocol/Constants';
export class FileTransferService { export class FileTransferService {
@ -16,7 +16,7 @@ export class FileTransferService {
return FileTransferService.instance; return FileTransferService.instance;
} }
public async transferFile(deviceId: string, filePath: string, type: COMMAND_TYPES, onProgress?: (progress: number) => void): Promise<void> { public async transferFile(deviceId: string, filePath: string, type: APP_COMMAND_TYPES, onProgress?: (progress: number) => void): Promise<void> {
try { try {
const response = await fetch(filePath); const response = await fetch(filePath);
if (!response.ok) { if (!response.ok) {