innoveit / react-native-ble-manager

React Native BLE communication module
http://innoveit.github.io/react-native-ble-manager/
Apache License 2.0
2.13k stars 768 forks source link

Long response after request #1287

Open SaNFeeDep opened 1 week ago

SaNFeeDep commented 1 week ago

The problem is that when sending commands to the device, the response time from the device is 10 seconds. I use Expo for assembly. I thought that when assembling the “product” version this time would become shorter, but it remained the same. Below is an example of a request and response. Please pay attention to the time.

LOG BLEManager: Send command = "pass 0|who|fuel|bat|temp|time" on device C5:8B:00:78:C8:FD. Time: 13:37:38 LOG BLEManager: Writed... LOG BLEManager: Received => PASS=OK|WHO=FW:0.0.1,SN:12340000|FUEL=0|BAT=3.1|TEMP=25|TIME=0,6345. Time: 13:37:48.

It takes 10 seconds to respond to this command -_- When using a third party application for BLE, response time to commands is less than 200 ms from the same device

Below is the class I use to work with BleManager.

import { Buffer } from 'buffer'
import { Permission, PermissionsAndroid, Platform } from 'react-native'
import BleManager, {
  BleConnectPeripheralEvent,
  BleDisconnectPeripheralEvent,
  BleDiscoverPeripheralEvent,
  BleManagerDidUpdateStateEvent,
  BleManagerDidUpdateValueForCharacteristicEvent,
  BleState,
  BleStopScanEvent,
  Peripheral,
  PeripheralInfo,
} from 'react-native-ble-manager'

import { useDeviceStore } from '../stores/devices'
import { logger } from './logger'

type QueueType = {
  command: string
  response: string | null
  callback: (response: string | null, error?: any) => void
}

type ListenerType = {
  BleManagerStopScan: BleStopScanEvent
  BleManagerConnectPeripheral: BleConnectPeripheralEvent
  BleManagerDidUpdateState: BleManagerDidUpdateStateEvent
  BleManagerDiscoverPeripheral: BleDiscoverPeripheralEvent
  BleManagerDisconnectPeripheral: BleDisconnectPeripheralEvent
  BleManagerDidUpdateValueForCharacteristic: BleManagerDidUpdateValueForCharacteristicEvent
}

const addListener = <T extends keyof ListenerType>(
  eventType: T,
  listener: (event: ListenerType[T]) => void,
  context?: Object
) => BleManager.addListener(eventType, listener, context)

const removeListener = <T extends keyof ListenerType>(eventType: T) => {
  BleManager.removeAllListeners(eventType)
}

export type InitErrorType = 'Permissions' | 'Bluetooth' | 'Unknown'

const devicesState = useDeviceStore.getState()

class BleHelper {

  public readonly SERVICE_CHANNEL = '6e400001-b5a3-f393-e0a9-e50e24dcca9e'

  public readonly READ_CHANNEL = '6e400002-b5a3-f393-e0a9-e50e24dcca9e'

  public readonly WRITE_CHANNEL = '6e400003-b5a3-f393-e0a9-e50e24dcca9e'

  public readonly manager = BleManager

  public timerTimeout = 50

  private inited: boolean = false

  private scaning: boolean = false

  private busy = false

  private queue: QueueType[] = []

  public device: PeripheralInfo | null = null

  private timer: NodeJS.Timeout | null = null

  private currentCommand: QueueType | null = null

  private resetQueueParams() {
    this.queue = []
    this.busy = false
    this.currentCommand = null
  }

  private initQueueManager(device: Peripheral) {
    this.device = device
    this.resetQueueParams()

    if (!this.timer)
      this.timer = setInterval(this.onTimer.bind(this), this.timerTimeout)
  }

  private destroyQueueManager() {
    this.device = null
    this.resetQueueParams()

    if (this.timer) {
      clearInterval(this.timer)
      this.timer = null
    }
  }

  private breakQueueItem(response: string | null, error?: any) {
    if (this.currentCommand) {
      this.currentCommand.callback(response, error ?? null)
      this.currentCommand.response = null
    }

    this.currentCommand = null
    this.busy = false
  }

  private onTimer() {
    if (!this.device) return

    if (this.busy || !this.device || !this.queue.length) return

    this.busy = true
    this.currentCommand = this.queue.shift()

    this.execSendCommand()
  }

  private execSendCommand() {
    const { command } = this.currentCommand

    try {
      const time = `Time: ${new Date().toLocaleTimeString()}`
      const commandText =
        command.length > 50 ? command.substring(0, 50) + '...' : command

      logger.log(
        `BLEManager: Send command = "${commandText}" on device ${this.device.id}. ${time}`
      )

      const data = Buffer.from(command).toJSON().data

      BleManager.writeWithoutResponse(
        this.device.id,
        this.SERVICE_CHANNEL,
        this.WRITE_CHANNEL,
        data
      ).then(() => {
        logger.log('BLEManager: Writed...')
      })
    } catch (e) {
      this.breakQueueItem(null, e)
      logger.warn(
        `BLEManager: Error send command = "${command}" on device ${this.device.id}. Reason =>`,
        e
      )
    }
  }

  private onRecive(event: BleManagerDidUpdateValueForCharacteristicEvent) {
    if (this.currentCommand === null) return

    const { response } = this.currentCommand

    if (!event.value) {
      logger.warn('BLEManager: Empty response')
      return this.breakQueueItem(null)
    }

    const data = Buffer.from(event.value).toString('utf-8')

    const concatedResponse = response ? response + data : data
    this.currentCommand.response = concatedResponse

    if (!data.match(/\r\n/)) return

    const resolveData = concatedResponse.replace(/\r\n/, '')

    const time = new Date().toLocaleTimeString()
    logger.log(
      `BLEManager: Received => ${resolveData}. Time: ${time}. Current queue =>`,
      this.queue.length
    )

    this.breakQueueItem(resolveData)
  }

  private async enableBluetooth() {
    try {
      await BleManager.enableBluetooth()
      return true
    } catch (e) {
      return false
    }
  }

  private async requestBluetoothPermission() {
    if (Platform.OS !== 'android') return true

    const requestPermissions = [
      PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
      PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
      PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
    ] as Permission[]

    const permissionsResult = await PermissionsAndroid.requestMultiple(
      requestPermissions
    )

    const isAllGranted = requestPermissions.every((perm) => {
      return permissionsResult[perm] === PermissionsAndroid.RESULTS.GRANTED
    })

    return isAllGranted
  }

  public initManager() {
    return new Promise<InitErrorType | null>(async (resolve) => {
      const isGranted = await this.requestBluetoothPermission()

      if (!isGranted) return resolve('Permissions')

      try {
        const state = await BleManager.checkState()

        if (state === BleState.Off) await this.enableBluetooth()

        await BleManager.start({ showAlert: false })

        addListener(
          'BleManagerDidUpdateValueForCharacteristic',
          this.onRecive.bind(this)
        )

        addListener('BleManagerDiscoverPeripheral', (device) => {
          if (device.name && device.name.match('FL'))
            devicesState.upsertOne(device)
        })

        addListener('BleManagerStopScan', () => {
          this.scaning = false
        })

        this.inited = true
        resolve(null)
      } catch (e) {
        logger.log('BLEManager: Error init => ', e)
        resolve('Bluetooth')
      }
    })
  }

  public scanDevice() {
    if (this.scaning) {
      logger.warn(`BLEManager: Already scaning`)
      return
    }

    if (!this.inited) {
      logger.error(`BLEManager: Manager doesn't inited`)
      return
    }

    BleManager.scan([], 5, false)
      .then(() => {
        this.scaning = true
        logger.log('BLEManager: Start scan device...')
      })
      .catch((e) => {
        logger.warn('BLEManager: Scan failed =>', e)
      })
  }

  public stopScanDevice() {
    if (!this.scaning) return

    BleManager.stopScan()
      .then(() => {
        logger.log('BLEManager: Stop scan device')
        BleManager.getDiscoveredPeripherals().then((devices) => {
          devices.forEach((device) => {
            if (device.name && device.name.match('FL'))
              devicesState.upsertOne(device)
          })
        })
      })
      .catch((e) => {
        logger.warn('BLEManager: Stop scan failed =>', e)
      })
      .finally(() => {
        this.scaning = false
      })
  }

  public async isDeviceConnected(deviceId: string) {
    try {
      return await BleManager.isPeripheralConnected(deviceId)
    } catch (e) {
      return false
    }
  }

  public async connectToDevice(
    deviceId: string
  ): Promise<PeripheralInfo | null> {
    try {
      logger.log(`BLEManager: Connected to ${deviceId}`)

      await BleManager.connect(deviceId)

      const device = await BleManager.retrieveServices(deviceId, [
        this.SERVICE_CHANNEL,
      ])

      await BleManager.startNotification(
        device.id,
        this.SERVICE_CHANNEL,
        this.READ_CHANNEL
      )

      this.initQueueManager(device)

      return device
    } catch (e) {
      logger.error(
        `BLEManager: Error when connected to device ${deviceId} =>`,
        e
      )
      return null
    }
  }

  public async disconnectFromDevice(deviceId: string) {
    try {
      const isConnected = await BleManager.isPeripheralConnected(deviceId)

      if (!isConnected) {
        logger.log(`BLEManager: Device ${deviceId} already disconnected`)
        return
      }

      await BleManager.stopNotification(
        deviceId,
        this.SERVICE_CHANNEL,
        this.READ_CHANNEL
      )

      this.destroyQueueManager()

      await BleManager.disconnect(deviceId)
      logger.log(`BLEManager: Device ${deviceId} disconnected successful`)
    } catch (e) {
      logger.error(`BLEManager: Failed disconect from device ${deviceId}`, e)
    }
  }

  public sendCommand(
    command: QueueType['command'],
    callback: QueueType['callback']
  ) {
    const commandText =
      command.length > 50 ? command.substring(0, 50) + '...' : command

    logger.log(
      `BLEManager: Command = "${commandText}" add in queue. Queue =>`,
      this.queue.length
    )

    this.queue.push({ command, callback, response: null })
  }

  public eraseQueue() {
    this.queue = []
    this.breakQueueItem(null)
  }

  public destroy() {
    removeListener('BleManagerConnectPeripheral')
    removeListener('BleManagerDisconnectPeripheral')
    removeListener('BleManagerDiscoverPeripheral')
    removeListener('BleManagerStopScan')
    removeListener('BleManagerDidUpdateValueForCharacteristic')

    this.inited = false
  }
}

const bleHelper = new BleHelper()

export default bleHelper

Import from logger.ts it just console.log:

export const logger = {
  warn: (...rest: any) => __DEV__ && console.warn(...rest),
  log: (...rest: any) => __DEV__ && console.log(...rest),
  error: (...rest: any) => __DEV__ && console.error(...rest),
}

Other info: 1) Android: 12 SKQ1.211202.001 2) Expo: 51.0.28 3) BleManager: 11.5.6 4) React-Native: 0.74.5 5) Buffer: 6.0.3

SaNFeeDep commented 1 week ago

I found a link to GitHub in the application I use - https://github.com/kai-morich/SimpleBluetoothTerminal Link on GooglePlay - https://play.google.com/store/apps/details?id=de.kai_morich.serial_bluetooth_terminal

marcosinigaglia commented 3 days ago

Try to increase the MTU and use the maxByteSize parameter to send all the data in a single write.

SaNFeeDep commented 2 days ago

@marcosinigaglia I tried increasing the MTU size and sending the response in maxByteSize, but it didn't work :( My device does not support MTU greater than 23. I tried lowering the MTU size but it did not help.

I checked the packets using WireShark. The result is very strange. Data is sent from your phone to your device almost instantly. The device also responds to these packets almost instantly. They come back to the phone and that's it...

They'll get stuck somewhere for 10 seconds.

387090832-69574f0f-b15d-4ade-a871-bfe57ede773b