Skip to content

Commit

Permalink
fix: resolves #1103 - crash on disconnect (#1111)
Browse files Browse the repository at this point in the history
* chore: recreate native error
* test: android device disconnecting example
* fix: resolves #1103 - crash on disconnect
* refactor: sorted imports
* refactor: types
* fix: fast reuse of connect to device leads to 'device not connected error'
* fix: fast reuse of connect to device leads to 'device not connected error' (only for Android)
* refactor: change numbers into variables
  • Loading branch information
gmiszewski-intent authored Oct 16, 2023
1 parent 1d8c26e commit c1cdb03
Show file tree
Hide file tree
Showing 13 changed files with 321 additions and 26 deletions.
3 changes: 2 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/**/node_modules/*
node_modules/
docs/**
docs/**
lib/**
16 changes: 15 additions & 1 deletion android/src/main/java/com/bleplx/BlePlxModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,36 @@
import com.bleplx.converter.ServiceToJsObjectConverter;
import com.bleplx.utils.ReadableArrayConverter;
import com.bleplx.utils.SafePromise;
import com.polidea.rxandroidble2.internal.RxBleLog;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import android.app.Activity;

import io.reactivex.exceptions.UndeliverableException;
import io.reactivex.plugins.RxJavaPlugins;

@ReactModule(name = BlePlxModule.NAME)
public class BlePlxModule extends ReactContextBaseJavaModule {
public static final String NAME = "BlePlx";

public BlePlxModule(ReactApplicationContext reactContext) {
super(reactContext);
RxJavaPlugins.setErrorHandler(throwable -> {
if (throwable instanceof UndeliverableException) {
RxBleLog.e("Handle all unhandled exceptions from RxJava: " + throwable.getMessage());
} else {
Thread currentThread = Thread.currentThread();
Thread.UncaughtExceptionHandler errorHandler = currentThread.getUncaughtExceptionHandler();
if (errorHandler != null) {
errorHandler.uncaughtException(currentThread, throwable);
}
}
});
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React from 'react'
import type { TestStateType } from 'example/types'
import type { TestStateType } from '../../../types'
import { AppText } from '../AppText/AppText'
import { Container, Header, Label } from './TestStateDisplay.styled'

export type TestStateDisplayProps = {
label?: string
state: TestStateType
state?: TestStateType
value?: string
}

Expand All @@ -21,9 +21,9 @@ export function TestStateDisplay({ label, state, value }: TestStateDisplayProps)
<Container>
<Header>
<Label>{label}</Label>
<AppText>{marks[state]}</AppText>
{!!state && <AppText>{marks[state]}</AppText>}
</Header>
{value && <AppText>{value}</AppText>}
{!!value && <AppText>{value}</AppText>}
</Container>
)
}
13 changes: 13 additions & 0 deletions example/src/consts/nRFDeviceConsts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { fullUUID } from 'react-native-ble-plx'
import base64 from 'react-native-base64'
import { getDateAsBase64 } from '../utils/getDateAsBase64'

export const deviceTimeService = fullUUID('1847')
export const currentTimeCharacteristic = fullUUID('2A2B')
export const deviceTimeCharacteristic = fullUUID('2B90')
export const currentTimeCharacteristicTimeTriggerDescriptor = fullUUID('290E')

export const writeWithResponseBase64Time = getDateAsBase64(new Date('2022-08-11T08:17:19Z'))
export const writeWithoutResponseBase64Time = getDateAsBase64(new Date('2023-09-12T10:12:16Z'))
export const monitorExpectedMessage = 'Hi, it works!'
export const currentTimeCharacteristicTimeTriggerDescriptorValue = base64.encode('BLE-PLX')
8 changes: 8 additions & 0 deletions example/src/navigation/navigators/MainStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type MainStackParamList = {
DASHBOARD_SCREEN: undefined
DEVICE_DETAILS_SCREEN: undefined
DEVICE_NRF_TEST_SCREEN: undefined
DEVICE_CONNECT_DISCONNECT_TEST_SCREEN: undefined
}

const MainStack = createNativeStackNavigator<MainStackParamList>()
Expand Down Expand Up @@ -37,6 +38,13 @@ export function MainStackComponent() {
headerTitle: 'nRF device test'
}}
/>
<MainStack.Screen
name="DEVICE_CONNECT_DISCONNECT_TEST_SCREEN"
component={screenComponents.DeviceConnectDisconnectTestScreen}
options={{
headerTitle: 'Connect/disconnect'
}}
/>
</MainStack.Navigator>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ export function DashboardScreen({ navigation }: DashboardScreenProps) {
/>
<AppButton label="Ask for permissions" onPress={BLEService.requestBluetoothPermission} />
<AppButton label="Go to nRF test" onPress={() => navigation.navigate('DEVICE_NRF_TEST_SCREEN')} />
<AppButton label="Call disconnect with wrong id" onPress={() => BLEService.isDeviceWithIdConnected('asd')} />
<AppButton
label="Connect/disconnect test"
onPress={() => navigation.navigate('DEVICE_CONNECT_DISCONNECT_TEST_SCREEN')}
/>
<FlatList
style={{ flex: 1 }}
data={foundDevices}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
import React, { useRef, useState } from 'react'
import type { NativeStackScreenProps } from '@react-navigation/native-stack'
import { BleError, Characteristic, Device, type Subscription, type DeviceId, BleErrorCode } from 'react-native-ble-plx'
import { ScrollView } from 'react-native'
import base64 from 'react-native-base64'
import Toast from 'react-native-toast-message'
import type { TestStateType } from '../../../types'
import { BLEService } from '../../../services'
import type { MainStackParamList } from '../../../navigation/navigators'
import { AppButton, AppTextInput, ScreenDefaultContainer, TestStateDisplay } from '../../../components/atoms'
import { currentTimeCharacteristic, deviceTimeService } from '../../../consts/nRFDeviceConsts'
import { wait } from '../../../utils/wait'

type DeviceConnectDisconnectTestScreenProps = NativeStackScreenProps<
MainStackParamList,
'DEVICE_CONNECT_DISCONNECT_TEST_SCREEN'
>
const NUMBER_OF_CALLS_IN_THE_TEST_SCENARIO = 10

export function DeviceConnectDisconnectTestScreen(_props: DeviceConnectDisconnectTestScreenProps) {
const [expectedDeviceName, setExpectedDeviceName] = useState('')
const [testScanDevicesState, setTestScanDevicesState] = useState<TestStateType>('WAITING')
const [deviceId, setDeviceId] = useState('')
const [connectCounter, setConnectCounter] = useState(0)
const [characteristicDiscoverCounter, setCharacteristicDiscoverCounter] = useState(0)
const [connectInDisconnectTestCounter, setConnectInDisconnectTestCounter] = useState(0)
const [disconnectCounter, setDisconnectCounter] = useState(0)
const [monitorMessages, setMonitorMessages] = useState<string[]>([])
const monitorSubscriptionRef = useRef<Subscription | null>(null)

const addMonitorMessage = (message: string) => setMonitorMessages(prevMessages => [...prevMessages, message])

const checkDeviceName = (device: Device) =>
device.name?.toLocaleLowerCase() === expectedDeviceName.toLocaleLowerCase()

const startConnectAndDiscover = async () => {
setTestScanDevicesState('IN_PROGRESS')
await BLEService.initializeBLE()
await BLEService.scanDevices(connectAndDiscoverOnDeviceFound, [deviceTimeService])
}

const startConnectAndDisconnect = async () => {
setTestScanDevicesState('IN_PROGRESS')
await BLEService.initializeBLE()
await BLEService.scanDevices(connectAndDisconnectOnDeviceFound, [deviceTimeService])
}

const startConnectOnly = async () => {
setTestScanDevicesState('IN_PROGRESS')
await BLEService.initializeBLE()
await BLEService.scanDevices(
async (device: Device) => {
if (checkDeviceName(device)) {
console.info(`connecting to ${device.id}`)
await startConnectToDevice(device)
setConnectCounter(prevCount => prevCount + 1)
setTestScanDevicesState('DONE')
setDeviceId(device.id)
}
},
[deviceTimeService]
)
}

const connectAndDiscoverOnDeviceFound = async (device: Device) => {
if (checkDeviceName(device)) {
setTestScanDevicesState('DONE')
setDeviceId(device.id)
try {
for (let i = 0; i < NUMBER_OF_CALLS_IN_THE_TEST_SCENARIO; i += 1) {
console.info(`connecting to ${device.id}`)
await startConnectToDevice(device)
setConnectCounter(prevCount => prevCount + 1)
console.info(`discovering in ${device.id}`)
await startDiscoverServices()
setCharacteristicDiscoverCounter(prevCount => prevCount + 1)
}
console.info('Multiple connect success')
} catch (error) {
console.error('Multiple connect error')
}
}
}
const connectAndDisconnectOnDeviceFound = async (device: Device) => {
if (checkDeviceName(device)) {
setTestScanDevicesState('DONE')
setDeviceId(device.id)
try {
for (let i = 0; i < NUMBER_OF_CALLS_IN_THE_TEST_SCENARIO; i += 1) {
await startConnectToDevice(device)
console.info(`connecting to ${device.id}`)
setConnectInDisconnectTestCounter(prevCount => prevCount + 1)
await startDisconnect(device)
console.info(`disconnecting from ${device.id}`)
setDisconnectCounter(prevCount => prevCount + 1)
}
console.info('connect/disconnect success')
} catch (error) {
console.error('Connect/disconnect error')
}
}
}

const discoverCharacteristicsOnly = async () => {
if (!deviceId) {
console.error('Device not ready')
return
}
try {
for (let i = 0; i < NUMBER_OF_CALLS_IN_THE_TEST_SCENARIO; i += 1) {
console.info(`discovering in ${deviceId}`)
await startDiscoverServices()
setCharacteristicDiscoverCounter(prevCount => prevCount + 1)
}
console.info('Multiple discovering success')
} catch (error) {
console.error('Multiple discovering error')
}
}

const startConnectToDevice = (device: Device) => BLEService.connectToDevice(device.id)

const startDiscoverServices = () => BLEService.discoverAllServicesAndCharacteristicsForDevice()

const startDisconnect = (device: Device) => BLEService.disconnectDeviceById(device.id)

const startCharacteristicMonitor = (directDeviceId?: DeviceId) => {
if (!deviceId && !directDeviceId) {
console.error('Device not ready')
return
}
monitorSubscriptionRef.current = BLEService.setupCustomMonitor(
directDeviceId || deviceId,
deviceTimeService,
currentTimeCharacteristic,
characteristicListener
)
}

const characteristicListener = (error: BleError | null, characteristic: Characteristic | null) => {
if (error) {
if (error.errorCode === BleErrorCode.ServiceNotFound || error.errorCode === BleErrorCode.ServicesNotDiscovered) {
startDiscoverServices().then(() => startCharacteristicMonitor())
return
}
console.error(JSON.stringify(error))
}
if (characteristic) {
if (characteristic.value) {
const message = base64.decode(characteristic.value)
console.info(message)
addMonitorMessage(message)
}
}
}

const setupOnDeviceDisconnected = (directDeviceId?: DeviceId) => {
if (!deviceId && !directDeviceId) {
console.error('Device not ready')
return
}
BLEService.onDeviceDisconnectedCustom(directDeviceId || deviceId, disconnectedListener)
}

const disconnectedListener = (error: BleError | null, device: Device | null) => {
if (error) {
console.error('onDeviceDisconnected')
console.error(JSON.stringify(error, null, 4))
}
if (device) {
console.info(JSON.stringify(device, null, 4))
}
}

// https://github.com/dotintent/react-native-ble-plx/issues/1103
const showIssue1103Crash = async () => {
setTestScanDevicesState('IN_PROGRESS')
await BLEService.initializeBLE()
await BLEService.scanDevices(
async (device: Device) => {
if (checkDeviceName(device)) {
console.info(`connecting to ${device.id}`)
await startConnectToDevice(device)
setConnectCounter(prevCount => prevCount + 1)
setTestScanDevicesState('DONE')
setDeviceId(device.id)
await startDiscoverServices()
await wait(1000)
setupOnDeviceDisconnected(device.id)
await wait(1000)
startCharacteristicMonitor(device.id)
await wait(1000)
const info = 'Now disconnect device'
console.info(info)
Toast.show({
type: 'info',
text1: info
})
}
},
[deviceTimeService]
)
}

return (
<ScreenDefaultContainer>
<ScrollView showsVerticalScrollIndicator={false}>
<AppTextInput
placeholder="Device name to connect"
value={expectedDeviceName}
onChangeText={setExpectedDeviceName}
/>
<AppButton label="#1103" onPress={showIssue1103Crash} />
<AppButton label="Just connect" onPress={startConnectOnly} />
<AppButton label="Setup on device disconnected" onPress={() => setupOnDeviceDisconnected()} />
<TestStateDisplay label="Looking for device" state={testScanDevicesState} />
<AppButton label="Start connect and discover" onPress={startConnectAndDiscover} />
<AppButton label="Discover characteristics only" onPress={discoverCharacteristicsOnly} />
<TestStateDisplay label="Connect counter" value={connectCounter.toString()} />
<TestStateDisplay label="Characteristic discover counter" value={characteristicDiscoverCounter.toString()} />
<AppButton label="Start connect and disconnect" onPress={startConnectAndDisconnect} />
<TestStateDisplay
label="Connect in disconnect test counter"
value={connectInDisconnectTestCounter.toString()}
/>
<TestStateDisplay label="Disconnect counter" value={disconnectCounter.toString()} />
<AppButton label="Setup monitor" onPress={() => startCharacteristicMonitor()} />
<AppButton label="Remove monitor" onPress={monitorSubscriptionRef.current?.remove} />
<TestStateDisplay label="Connect in disconnect test counter" value={JSON.stringify(monitorMessages, null, 4)} />
</ScrollView>
</ScreenDefaultContainer>
)
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
import React, { useState, type Dispatch } from 'react'
import type { TestStateType } from 'example/types'
import type { NativeStackScreenProps } from '@react-navigation/native-stack'
import { Device, fullUUID, type Base64 } from 'react-native-ble-plx'
import { Device, type Base64 } from 'react-native-ble-plx'
import { Platform, ScrollView } from 'react-native'
import base64 from 'react-native-base64'
import { getDateAsBase64 } from '../../../utils/getDateAsBase64'
import type { TestStateType } from '../../../types'
import { BLEService } from '../../../services'
import type { MainStackParamList } from '../../../navigation/navigators'
import { AppButton, AppTextInput, ScreenDefaultContainer, TestStateDisplay } from '../../../components/atoms'
import { wait } from '../../../utils/wait'
import {
currentTimeCharacteristic,
currentTimeCharacteristicTimeTriggerDescriptor,
currentTimeCharacteristicTimeTriggerDescriptorValue,
deviceTimeCharacteristic,
deviceTimeService,
monitorExpectedMessage,
writeWithResponseBase64Time,
writeWithoutResponseBase64Time
} from '../../../consts/nRFDeviceConsts'

type DevicenRFTestScreenProps = NativeStackScreenProps<MainStackParamList, 'DEVICE_NRF_TEST_SCREEN'>

const deviceTimeService = fullUUID('1847')
const currentTimeCharacteristic = fullUUID('2A2B')
const deviceTimeCharacteristic = fullUUID('2B90')
const currentTimeCharacteristicTimeTriggerDescriptor = fullUUID('290E')

const writeWithResponseBase64Time = getDateAsBase64(new Date('2022-08-11T08:17:19Z'))
const writeWithoutResponseBase64Time = getDateAsBase64(new Date('2023-09-12T10:12:16Z'))
const monitorExpectedMessage = 'Hi, it works!'
const currentTimeCharacteristicTimeTriggerDescriptorValue = base64.encode('BLE-PLX')

export function DevicenRFTestScreen(_props: DevicenRFTestScreenProps) {
const [expectedDeviceName, setExpectedDeviceName] = useState('')
const [testScanDevicesState, setTestScanDevicesState] = useState<TestStateType>('WAITING')
Expand Down
1 change: 1 addition & 0 deletions example/src/screens/MainStack/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './DashboardScreen/DashboardScreen'
export * from './DeviceDetailsScreen/DeviceDetailsScreen'
export * from './DevicenRFTestScreen/DevicenRFTestScreen'
export * from './DeviceConnectDisconnectTestScreen/DeviceConnectDisconnectTestScreen'
Loading

0 comments on commit c1cdb03

Please sign in to comment.