diff --git a/.eslintignore b/.eslintignore index 3ef8f41d..fddf3ca8 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ /**/node_modules/* node_modules/ -docs/** \ No newline at end of file +docs/** +lib/** \ No newline at end of file diff --git a/android/src/main/java/com/bleplx/BlePlxModule.java b/android/src/main/java/com/bleplx/BlePlxModule.java index 1464bd27..b165a1b3 100644 --- a/android/src/main/java/com/bleplx/BlePlxModule.java +++ b/android/src/main/java/com/bleplx/BlePlxModule.java @@ -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 diff --git a/example/src/components/atoms/TestStateDisplay/TestStateDisplay.tsx b/example/src/components/atoms/TestStateDisplay/TestStateDisplay.tsx index 44f88be0..29366514 100644 --- a/example/src/components/atoms/TestStateDisplay/TestStateDisplay.tsx +++ b/example/src/components/atoms/TestStateDisplay/TestStateDisplay.tsx @@ -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 } @@ -21,9 +21,9 @@ export function TestStateDisplay({ label, state, value }: TestStateDisplayProps)
- {marks[state]} + {!!state && {marks[state]}}
- {value && {value}} + {!!value && {value}}
) } diff --git a/example/src/consts/nRFDeviceConsts.ts b/example/src/consts/nRFDeviceConsts.ts new file mode 100644 index 00000000..66395ca6 --- /dev/null +++ b/example/src/consts/nRFDeviceConsts.ts @@ -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') diff --git a/example/src/navigation/navigators/MainStack.tsx b/example/src/navigation/navigators/MainStack.tsx index adbabf63..8efa01a2 100644 --- a/example/src/navigation/navigators/MainStack.tsx +++ b/example/src/navigation/navigators/MainStack.tsx @@ -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() @@ -37,6 +38,13 @@ export function MainStackComponent() { headerTitle: 'nRF device test' }} /> + ) } diff --git a/example/src/screens/MainStack/DashboardScreen/DashboardScreen.tsx b/example/src/screens/MainStack/DashboardScreen/DashboardScreen.tsx index d2c5e85b..0fc6c65f 100644 --- a/example/src/screens/MainStack/DashboardScreen/DashboardScreen.tsx +++ b/example/src/screens/MainStack/DashboardScreen/DashboardScreen.tsx @@ -79,6 +79,11 @@ export function DashboardScreen({ navigation }: DashboardScreenProps) { /> navigation.navigate('DEVICE_NRF_TEST_SCREEN')} /> + BLEService.isDeviceWithIdConnected('asd')} /> + navigation.navigate('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('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([]) + const monitorSubscriptionRef = useRef(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 ( + + + + + + setupOnDeviceDisconnected()} /> + + + + + + + + + startCharacteristicMonitor()} /> + + + + + ) +} diff --git a/example/src/screens/MainStack/DevicenRFTestScreen/DevicenRFTestScreen.tsx b/example/src/screens/MainStack/DevicenRFTestScreen/DevicenRFTestScreen.tsx index 1784f1a9..1b3715b3 100644 --- a/example/src/screens/MainStack/DevicenRFTestScreen/DevicenRFTestScreen.tsx +++ b/example/src/screens/MainStack/DevicenRFTestScreen/DevicenRFTestScreen.tsx @@ -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 -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('WAITING') diff --git a/example/src/screens/MainStack/index.ts b/example/src/screens/MainStack/index.ts index afb59930..20784153 100644 --- a/example/src/screens/MainStack/index.ts +++ b/example/src/screens/MainStack/index.ts @@ -1,3 +1,4 @@ export * from './DashboardScreen/DashboardScreen' export * from './DeviceDetailsScreen/DeviceDetailsScreen' export * from './DevicenRFTestScreen/DevicenRFTestScreen' +export * from './DeviceConnectDisconnectTestScreen/DeviceConnectDisconnectTestScreen' diff --git a/example/src/services/BLEService/BLEService.ts b/example/src/services/BLEService/BLEService.ts index 6775c705..2f175525 100644 --- a/example/src/services/BLEService/BLEService.ts +++ b/example/src/services/BLEService/BLEService.ts @@ -5,6 +5,7 @@ import { Device, State as BluetoothState, LogLevel, + type DeviceId, type TransactionId, type UUID, type Characteristic, @@ -77,6 +78,16 @@ class BLEServiceInstance { }) } + disconnectDeviceById = (id: DeviceId) => + this.manager + .cancelDeviceConnection(id) + .then(() => this.showSuccessToast('Device disconnected')) + .catch(error => { + if (error?.code !== BleErrorCode.DeviceDisconnected) { + this.onError(error) + } + }) + onBluetoothPowerOff = () => { this.showErrorToast('Bluetooth is turned off') } @@ -95,7 +106,7 @@ class BLEServiceInstance { }) } - connectToDevice = (deviceId: string) => + connectToDevice = (deviceId: DeviceId) => new Promise((resolve, reject) => { this.manager.stopDeviceScan() this.manager @@ -211,6 +222,9 @@ class BLEServiceInstance { ) } + setupCustomMonitor: BleManager['monitorCharacteristicForDevice'] = (...args) => + this.manager.monitorCharacteristicForDevice(...args) + finishMonitor = () => { this.isCharacteristicMonitorDisconnectExpected = true this.characteristicMonitor?.remove() @@ -255,7 +269,7 @@ class BLEServiceInstance { }) } - getCharacteristicsForDevice = (serviceUUID: string) => { + getCharacteristicsForDevice = (serviceUUID: UUID) => { if (!this.device) { this.showErrorToast(deviceNotConnectedErrorText) throw new Error(deviceNotConnectedErrorText) @@ -265,7 +279,7 @@ class BLEServiceInstance { }) } - getDescriptorsForDevice = (serviceUUID: string, characteristicUUID: string) => { + getDescriptorsForDevice = (serviceUUID: UUID, characteristicUUID: UUID) => { if (!this.device) { this.showErrorToast(deviceNotConnectedErrorText) throw new Error(deviceNotConnectedErrorText) @@ -280,11 +294,11 @@ class BLEServiceInstance { this.showErrorToast(deviceNotConnectedErrorText) throw new Error(deviceNotConnectedErrorText) } - return this.manager.isDeviceConnected(this.device.id).catch(error => { - this.onError(error) - }) + return this.manager.isDeviceConnected(this.device.id) } + isDeviceWithIdConnected = (id: DeviceId) => this.manager.isDeviceConnected(id).catch(console.error) + getConnectedDevices = (expectedServices: UUID[]) => { if (!this.device) { this.showErrorToast(deviceNotConnectedErrorText) @@ -313,6 +327,9 @@ class BLEServiceInstance { return this.manager.onDeviceDisconnected(this.device.id, listener) } + onDeviceDisconnectedCustom: BleManager['onDeviceDisconnected'] = (...args) => + this.manager.onDeviceDisconnected(...args) + readRSSIForDevice = () => { if (!this.device) { this.showErrorToast(deviceNotConnectedErrorText) @@ -333,7 +350,7 @@ class BLEServiceInstance { }) } - cancelTransaction = (transactionId: string) => this.manager.cancelTransaction(transactionId) + cancelTransaction = (transactionId: TransactionId) => this.manager.cancelTransaction(transactionId) enable = () => this.manager.enable().catch(error => { diff --git a/example/types/TestStateType.ts b/example/src/types/TestStateType.ts similarity index 100% rename from example/types/TestStateType.ts rename to example/src/types/TestStateType.ts diff --git a/example/types/index.ts b/example/src/types/index.ts similarity index 100% rename from example/types/index.ts rename to example/src/types/index.ts diff --git a/src/BleManager.js b/src/BleManager.js index cc401555..9f16a5b1 100644 --- a/src/BleManager.js +++ b/src/BleManager.js @@ -28,6 +28,7 @@ import type { ConnectionOptions, BleManagerOptions } from './TypeDefinition' +import { Platform } from 'react-native' const enableDisableDeprecatedMessage = 'react-native-ble-plx: The enable and disable feature is no longer supported. In Android SDK 31+ there were major changes in permissions, which may cause problems with these functions, and in SDK 33+ they were completely removed.' @@ -457,6 +458,9 @@ export class BleManager { * @returns {Promise} Connected {@link Device} object if successful. */ async connectToDevice(deviceIdentifier: DeviceId, options: ?ConnectionOptions): Promise { + if (Platform.OS === 'android' && (await this.isDeviceConnected(deviceIdentifier))) { + await this.cancelDeviceConnection(deviceIdentifier) + } const nativeDevice = await this._callPromise(BleModule.connectToDevice(deviceIdentifier, options)) return new Device(nativeDevice, this) }