Prerequisites
Expected Behavior
The library should be able to disconnect a connected device.
Current Behavior
I hit a weird bug where the device is shown as "Connected" in Android Bluetooth Settings but the API returns "not connected" and there seems to be no way to connect/disconnect the device from the library. Restarting the app doesn't fix the issue. The only solution seems to be to disable / re-enabling Bluetooth in Android settings. Any idea what might cause the OS to enter that state or what I can do to prevent/recover from it?
Library version
3.5.0
Device
Android Pixel 8 Pro , Anrdoid 15
Environment info
System:
OS: macOS 15.3.1
CPU: (8) arm64 Apple M1
Memory: 116.64 MB / 16.00 GB
Shell:
version: "5.9"
path: /bin/zsh
Binaries:
Node:
version: 23.9.0
path: ~/.local/state/fnm_multishells/17502_1744143714559/bin/node
Yarn: Not Found
npm:
version: 10.9.2
path: ~/.local/state/fnm_multishells/17502_1744143714559/bin/npm
Watchman:
version: 2025.03.03.00
path: /opt/homebrew/bin/watchman
Managers:
CocoaPods:
version: 1.16.2
path: /opt/homebrew/bin/pod
SDKs:
iOS SDK:
Platforms:
- DriverKit 24.4
- iOS 18.4
- macOS 15.4
- tvOS 18.4
- visionOS 2.4
- watchOS 11.4
Android SDK:
API Levels:
- "21"
- "26"
- "27"
- "31"
- "32"
- "34"
- "35"
Build Tools:
- 30.0.2
- 30.0.3
- 31.0.0
- 34.0.0
- 35.0.0
System Images:
- android-28 | Google ARM64-V8a Play ARM 64 v8a
- android-31 | Google APIs ARM 64 v8a
- android-34 | Google Play ARM 64 v8a
Android NDK: 26.1.10909125
IDEs:
Android Studio: 2024.1 AI-241.18034.62.2411.12071903
Xcode:
version: 16.3/16E140
path: /usr/bin/xcodebuild
Languages:
Java:
version: 17.0.11
path: /usr/bin/javac
Ruby:
version: 2.6.10
path: /usr/bin/ruby
npmPackages:
"@react-native-community/cli":
installed: 18.0.0
wanted: ^18.0.0
react:
installed: 18.3.1
wanted: 18.3.1
react-native:
installed: 0.76.7
wanted: 0.76.7
react-native-macos: Not Found
npmGlobalPackages:
"*react-native*": Not Found
Android:
hermesEnabled: true
newArchEnabled: true
iOS:
hermesEnabled: false
newArchEnabled: true
info React Native v0.79.0 is now available (your project is running on v0.76.7).
info Changelog: https://github.com/facebook/react-native/releases/tag/v0.79.0
info Diff: https://react-native-community.github.io/upgrade-helper/?from=0.76.7&to=0.79.0
info For more info, check out "https://reactnative.dev/docs/upgrading?os=macos".
Steps to reproduce
Difficult to reproduce, I'm not exactly sure how to voluntarily enter that state. It occasionally happens but not sure which conditions to trigger it. It seems to usually happen when I'm in a re-connection loop.
Formatted code sample or link to a repository
import ble from './bleManager';
import { wait } from './utils';
import { EventEmitter } from 'events';
import TypedEmitter from 'typed-emitter'
import { BleError, Device } from 'react-native-ble-plx';
import bleManager from './bleManager';
type ConnectionInfo = {
rssi: number
}
export type ConnectionState = 'connecting' | 'connected' | 'disconnecting' | 'disconnected';
export type DisconnectReason = 'user' | 'timeout' | 'error' | 'unknown';
function deviceToConnectionInfo(device: Device): ConnectionInfo {
// TODO: when is rssi null?
if (!device.rssi === null) {
console.error('device.rssi is null, this should not happen');
}
return {
rssi: device.rssi ?? 0,
};
}
export type ConnectionEvents = {
error: (error: Error) => void,
connecting: () => void,
connected: (connectionInfo: ConnectionInfo) => void,
disconnecting: (reason: DisconnectReason) => void,
disconnected: (reason: DisconnectReason) => void,
stateChanged: (newState: ConnectionState, oldState: ConnectionState) => void,
}
export class Connection extends (EventEmitter as new () => TypedEmitter<ConnectionEvents>) {
deviceId: string;
state: ConnectionState = 'disconnected';
autoReconnect = true;
isConnecting(): boolean {
return this.state === 'connecting';
}
isConnected(): boolean {
return this.state === 'connected';
}
isDisconnecting(): boolean {
return this.state === 'disconnecting';
}
isDisconnected(): boolean {
return this.state === 'disconnected';
}
isDisconnectable(): boolean {
return this.isConnecting() || this.isConnected();
}
isConnectable(): boolean {
return this.isDisconnected();
}
constructor(deviceId: string, autoReconnect = true) {
super();
this.deviceId = deviceId;
this.autoReconnect = autoReconnect;
bleManager.onDeviceDisconnected(deviceId, (error: BleError | null) => {
// Expected disconnection
if (this.isDisconnecting()) {
return;
}
// Ignore disconnection if we are connecting, retries are already handled in the connect function
if (this.isConnecting()) {
return;
}
this.handleUnexpectedDisconnection(error);
});
}
private handleUnexpectedDisconnection(error: BleError | null) {
console.log(`Device ${this.deviceId} unexexpectedly disconnected: ${error}`);
const reason = error ? 'error' : 'unknown';
this.changeStateDisconnecting(reason);
this.changeStateDisconnected(reason);
// TODO: distinguish between retriable and non-retriable errors?
// Unexpected disconnection
if (this.autoReconnect) {
console.log(`Unexpectedly disconnected, trying to reconnect...`, error);
this.connect();
return
}
}
private changeState(newState: ConnectionState) {
const oldState = this.state;
this.state = newState;
this.emit('stateChanged', newState, oldState);
// this.callbacks.onStateChange(newState, oldState);
}
private changeStateConnecting() {
this.changeState('connecting');
this.emit('connecting');
// this.callbacks.onConnecting();
}
private changeStateConnected(connectionInfo: ConnectionInfo) {
this.changeState('connected');
this.emit('connected', connectionInfo);
// this.callbacks.onConnected();
}
private changeStateDisconnecting(reason: DisconnectReason) {
this.changeState('disconnecting');
this.emit('disconnecting', reason);
// this.callbacks.onDisconnecting();
}
private changeStateDisconnected(reason: DisconnectReason) {
this.changeState('disconnected');
this.emit('disconnected', reason);
// this.callbacks.onDisconnected(reason);
}
async connect(maxRetries: number = Infinity) {
try {
this.changeStateConnecting();
let device;
let retries = 0;
while (true) {
// TODO: implement maxRetries and exponential backoff
if (!this.isConnecting()) {
// State changed while we are retrying (e.g. user called .disconnect())
// Abort...
return;
}
try {
if (retries > 0) {
console.log(`Retrying #${retries}...`);
}
// TODO: implement a timeout?
// Ensure scanner is stopped to prevent android bug
await ble.stopDeviceScan();
// first check if we are already connected (android bug...)
// console.log(this.deviceId)
// const [maybeDevice] = await ble.connectedDevices([]);
// console.log({ maybeDevice })
// const isConnected = await ble.isDeviceConnected(this.deviceId);
// console.log({ isConnected });
// make sure we are not connected
// await ble.cancelDeviceConnection(this.deviceId).catch(console.error);
device = await ble.connectToDevice(this.deviceId, {
autoConnect: false,
});
// break out of retry loop since we connected
break;
} catch (error) {
console.log("ble.connectToDevice error: ", error);
// TODO: use proper error code / reason instead of matching message string
if (error instanceof BleError && error.message.match(/is already connected/)) {
// TODO: figure out why this happens
console.log("Already connected device", error);
console.log({ ble });
try {
await ble.cancelDeviceConnection(this.deviceId);
} catch (err) {
console.error('Unexpected error while cancelling connection: ', err);
}
// Device is already connected, we are done
// TODO: fix me
}
retries += 1;
if (retries > maxRetries) {
throw error;
}
// TODO: distinguish between retriable and non-retriable errors
console.log('Error connecting: ', error);
await wait(500);
}
}
this.changeStateConnected(deviceToConnectionInfo(device));
} catch (error) {
this.changeStateDisconnecting('error');
this.changeStateDisconnected('error');
this.emit('error', error as Error)
throw error;
}
}
async disconnect(reason: DisconnectReason = "user") {
if (!this.isDisconnectable()) return;
this.changeStateDisconnecting(reason);
try {
await ble.cancelDeviceConnection(this.deviceId);
} catch (error) {
console.error('Unexpected error while disconnecting: ', error);
this.emit('error', error as Error);
// this.callbacks.onError(error as Error);
throw error;
} finally {
this.changeStateDisconnected(reason);
}
}
}
Relevant log output
Additional information
No response
Prerequisites
Expected Behavior
The library should be able to disconnect a connected device.
Current Behavior
I hit a weird bug where the device is shown as "Connected" in Android Bluetooth Settings but the API returns "not connected" and there seems to be no way to connect/disconnect the device from the library. Restarting the app doesn't fix the issue. The only solution seems to be to disable / re-enabling Bluetooth in Android settings. Any idea what might cause the OS to enter that state or what I can do to prevent/recover from it?
Library version
3.5.0
Device
Android Pixel 8 Pro , Anrdoid 15
Environment info
System: OS: macOS 15.3.1 CPU: (8) arm64 Apple M1 Memory: 116.64 MB / 16.00 GB Shell: version: "5.9" path: /bin/zsh Binaries: Node: version: 23.9.0 path: ~/.local/state/fnm_multishells/17502_1744143714559/bin/node Yarn: Not Found npm: version: 10.9.2 path: ~/.local/state/fnm_multishells/17502_1744143714559/bin/npm Watchman: version: 2025.03.03.00 path: /opt/homebrew/bin/watchman Managers: CocoaPods: version: 1.16.2 path: /opt/homebrew/bin/pod SDKs: iOS SDK: Platforms: - DriverKit 24.4 - iOS 18.4 - macOS 15.4 - tvOS 18.4 - visionOS 2.4 - watchOS 11.4 Android SDK: API Levels: - "21" - "26" - "27" - "31" - "32" - "34" - "35" Build Tools: - 30.0.2 - 30.0.3 - 31.0.0 - 34.0.0 - 35.0.0 System Images: - android-28 | Google ARM64-V8a Play ARM 64 v8a - android-31 | Google APIs ARM 64 v8a - android-34 | Google Play ARM 64 v8a Android NDK: 26.1.10909125 IDEs: Android Studio: 2024.1 AI-241.18034.62.2411.12071903 Xcode: version: 16.3/16E140 path: /usr/bin/xcodebuild Languages: Java: version: 17.0.11 path: /usr/bin/javac Ruby: version: 2.6.10 path: /usr/bin/ruby npmPackages: "@react-native-community/cli": installed: 18.0.0 wanted: ^18.0.0 react: installed: 18.3.1 wanted: 18.3.1 react-native: installed: 0.76.7 wanted: 0.76.7 react-native-macos: Not Found npmGlobalPackages: "*react-native*": Not Found Android: hermesEnabled: true newArchEnabled: true iOS: hermesEnabled: false newArchEnabled: true info React Native v0.79.0 is now available (your project is running on v0.76.7). info Changelog: https://github.com/facebook/react-native/releases/tag/v0.79.0 info Diff: https://react-native-community.github.io/upgrade-helper/?from=0.76.7&to=0.79.0 info For more info, check out "https://reactnative.dev/docs/upgrading?os=macos".Steps to reproduce
Difficult to reproduce, I'm not exactly sure how to voluntarily enter that state. It occasionally happens but not sure which conditions to trigger it. It seems to usually happen when I'm in a re-connection loop.
Formatted code sample or link to a repository
Relevant log output
Additional information
No response