react-native-ble-plx icon indicating copy to clipboard operation
react-native-ble-plx copied to clipboard

🐛 Device not showing up in scan after disconnecting

Open olalonde opened this issue 1 year ago • 11 comments

Prerequisites

  • [x] I checked the documentation and FAQ without finding a solution
  • [x] I checked to make sure that this issue has not already been filed

Expected Behavior

I hit a weird bug on Android where after disconnecting from a BLE device, it no longer shows up in the device scan. I have confirmed through "nrf connect for mobile" that the device is indeed advertising and scannable. Sometimes the device will suddenly show up during the scan again. Very strange.

Current Behavior

It stops showing the device in the scan after disconnecting from it.

Library version

3.5.0

Device

Android 15 (pixel 9 pro)

Environment info

info Fetching system and libraries information...
System:
  OS: macOS 15.3.1
  CPU: (8) arm64 Apple M1
  Memory: 131.61 MB / 16.00 GB
  Shell:
    version: "5.9"
    path: /bin/zsh
Binaries:
  Node:
    version: 23.9.0
    path: ~/.local/state/fnm_multishells/42435_1741810219145/bin/node
  Yarn: Not Found
  npm:
    version: 10.9.2
    path: ~/.local/state/fnm_multishells/42435_1741810219145/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.2
      - iOS 18.2
      - macOS 15.2
      - tvOS 18.2
      - visionOS 2.2
      - watchOS 11.2
  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.2/16C5032a
    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: true
  newArchEnabled: true

info React Native v0.78.0 is now available (your project is running on v0.76.7).
info Changelog: https://github.com/facebook/react-native/releases/tag/v0.78.0
info Diff: https://react-native-community.github.io/upgrade-helper/?from=0.76.7&to=0.78.0
info For more info, check out "https://reactnative.dev/docs/upgrading?os=macos".

Steps to reproduce

  1. Connect to BLE device
  2. Disconnect from BLE device
  3. Scan

Formatted code sample or link to a repository

import { BleManager, Device, State } from 'react-native-ble-plx';
import { Platform, PermissionsAndroid } from 'react-native';
import { Buffer } from "buffer";

// Nordic UART Service UUID
const NUS_SERVICE_UUID = '6e400001-b5a3-f393-e0a9-e50e24dcca9e';
// Nordic UART TX characteristic (for receiving data from the device)
const NUS_TX_CHAR_UUID = '6e400003-b5a3-f393-e0a9-e50e24dcca9e';
// Nordic UART RX characteristic (for sending data to the device)
const NUS_RX_CHAR_UUID = '6e400002-b5a3-f393-e0a9-e50e24dcca9e';

class BleService {
  private manager: BleManager;
  private device: Device | null = null;
  private isConnecting: boolean = false;
  private isScanning: boolean = false;

  constructor() {
    this.manager = new BleManager();
  }

  async requestPermissions(): Promise<boolean> {
    if (Platform.OS === 'android') {
      try {
        const permissionsToRequest = [
          PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
          PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
          PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT
        ].filter(Boolean); // Filter out undefined permissions

        const results = await PermissionsAndroid.requestMultiple(permissionsToRequest);
        
        return Object.values(results).every(
          result => result === PermissionsAndroid.RESULTS.GRANTED
        );
      } catch (error) {
        console.error('Error requesting permissions:', error);
        return false;
      }
    }
    return true; // iOS handles permissions differently via Info.plist
  }

  async startScan(onDeviceFound: (device: Device) => void): Promise<void> {
    if (this.isScanning) return;
    
    try {
      this.isScanning = true;
      console.log('Starting BLE scan...');
      
      // Check if Bluetooth is enabled
      const state = await this.manager.state();
      if (state !== State.PoweredOn) {
        console.log('Bluetooth is not powered on');
        this.isScanning = false;
        return;
      }
      
      // Request permissions
      const hasPermission = await this.requestPermissions();
      if (!hasPermission) {
        console.log('Bluetooth permissions not granted');
        this.isScanning = false;
        return;
      }

      // Start scanning for BLE devices
      this.manager.startDeviceScan(
        null, // null means scan for all services
        { allowDuplicates: false },
        (error, device) => {
          if (error) {
            console.error('Scan error:', error);
            this.isScanning = false;
            return;
          }

          if (device && device.name) {
            console.log(`Found device: ${device.name} (${device.id})`);
            onDeviceFound(device);
          }
        }
      );
    } catch (error) {
      console.error('Error starting scan:', error);
      this.isScanning = false;
    }
  }

  stopScan(): void {
    if (this.isScanning) {
      this.manager.stopDeviceScan();
      this.isScanning = false;
      console.log('BLE scan stopped');
    }
  }

  async connectToDevice(deviceId: string): Promise<boolean> {
    if (this.isConnecting) return false;
    
    try {
      this.isConnecting = true;
      console.log(`Connecting to device: ${deviceId}`);
      
      // Stop scanning before connecting
      this.stopScan();
      
      // Connect to the device
      const device = await this.manager.connectToDevice(deviceId);
      console.log('Connected, discovering services and characteristics...');
      
      // Discover services and characteristics
      await device.discoverAllServicesAndCharacteristics();
      
      // Check if device has the Nordic UART Service
      const services = await device.services();
      const hasNusService = services.some(service => 
        service.uuid.toLowerCase() === NUS_SERVICE_UUID
      );
      
      if (!hasNusService) {
        console.log('Device does not have the Nordic UART Service');
        await device.cancelConnection();
        this.isConnecting = false;
        return false;
      }
      
      // Setup notification for receiving data
      await this.setupNotifications(device);
      
      this.device = device;
      this.isConnecting = false;
      console.log('Connected successfully');
      return true;
    } catch (error) {
      console.error('Connection error:', error);
      this.isConnecting = false;
      return false;
    }
  }

  async setupNotifications(device: Device): Promise<void> {
    try {
      // Monitor for incoming data on the TX characteristic
      device.monitorCharacteristicForService(
        NUS_SERVICE_UUID,
        NUS_TX_CHAR_UUID,
        (error, characteristic) => {
          if (error) {
            console.error('Notification error:', error);
            return;
          }
          
          if (characteristic?.value) {
            const decodedValue = this.decodeBase64(characteristic.value);
            console.log('Received:', decodedValue);
          }
        }
      );
    } catch (error) {
      console.error('Error setting up notifications:', error);
    }
  }

  decodeBase64(data: string): string {
    try {
      return Buffer.from(data, 'base64').toString('utf8');
    } catch (error) {
      console.error('Error decoding base64:', error);
      return '';
    }
  }

  async sendCommand(command: string): Promise<boolean> {
    if (!this.device) {
      console.log('No device connected');
      return false;
    }
    
    try {
      // Convert string to base64 encoded data
      const data = Buffer.from(command).toString('base64');
      
      // Write to the RX characteristic
      await this.device.writeCharacteristicWithResponseForService(
        NUS_SERVICE_UUID,
        NUS_RX_CHAR_UUID,
        data
      );
      console.log(`Command sent: ${command}`);
      return true;
    } catch (error) {
      console.error('Error sending command:', error);
      return false;
    }
  }

  async startRelay(): Promise<boolean> {
    return this.sendCommand('start');
  }

  async stopRelay(): Promise<boolean> {
    return this.sendCommand('stop');
  }

  async disconnect(): Promise<void> {
    if (this.device) {
      try {
        await this.device.cancelConnection();
        console.log('Disconnected from device');
      } catch (error) {
        console.error('Error disconnecting:', error);
      } finally {
        this.device = null;
      }
    }
  }

  cleanup(): void {
    this.stopScan();
    this.disconnect();
    this.manager.destroy();
  }
}

// Create singleton instance
const bleService = new BleService();
export default bleService;

Relevant log output

(NOBRIDGE) LOG  Starting BLE scan...
 (NOBRIDGE) LOG  Found device: BLE_Relay (E7:15:B1:4E:A4:59)
 (NOBRIDGE) LOG  Found device: BLE_Relay (E7:15:B1:4E:A4:59)
 (NOBRIDGE) LOG  Found device: BLE_Relay (E7:15:B1:4E:A4:59)
 (NOBRIDGE) LOG  Found device: BLE_Relay (E7:15:B1:4E:A4:59)
 (NOBRIDGE) LOG  Connecting to device: E7:15:B1:4E:A4:59
 (NOBRIDGE) LOG  BLE scan stopped
 (NOBRIDGE) LOG  Connected, discovering services and characteristics...
 (NOBRIDGE) LOG  Connected successfully
 (NOBRIDGE) LOG  Disconnected from device
 (NOBRIDGE) ERROR  Notification error: [BleError: Device E7:15:B1:4E:A4:59 was disconnected]
 (NOBRIDGE) LOG  Starting BLE scan...
 (NOBRIDGE) LOG  Found device: M10K3QE (78:9C:85:1B:FF:AF)
 (NOBRIDGE) LOG  Found device: M10K3QE (78:9C:85:1B:FF:AF)
 (NOBRIDGE) LOG  Found device: M10K3QE (78:9C:85:1B:FF:AF)
etc.

Additional information

No response

olalonde avatar Mar 12 '25 20:03 olalonde

Strangely, when I open the "Nrf Connect for Mobile" app, scan BLE devices and go back to my app, the device suddenly shows up again in my own app. Very strange. Rebooting the app also fixes the issue.

olalonde avatar Mar 12 '25 21:03 olalonde

same problem

claudioPrisco avatar Apr 01 '25 08:04 claudioPrisco

@olalonde do you trigger startDeviceScan again after disconnecting? It should return a full list of reachable devices

aliberski avatar May 26 '25 21:05 aliberski

@aliberski me and my team also look to be seeing this issue right now

benletchford avatar May 29 '25 00:05 benletchford

Were you able to get this working, @olalonde?

animaonline avatar Jun 11 '25 10:06 animaonline

experiencing the same thing on android only as well

kent-williams avatar Jun 18 '25 02:06 kent-williams

I face this issue when disconnecting from a device shortly after the connection had been established. My workaround for now is to make sure to keep the connection for at least 5-10 seconds before allowing to disconnect from the device. Then I wait for around 20 seconds before scanning for the device again. Adding timeouts does not feel right, and the problem still occurs from time to time, but it helps a bit.

irenmax avatar Jun 18 '25 06:06 irenmax

@olalonde @claudioPrisco @animaonline @kent-williams @irenmax I may have a potential patch but need assistance validating it properly: #1296

The way discoveredDevices is handled is a little suspect.

benletchford avatar Jun 18 '25 10:06 benletchford

@benletchford, thanks for the quick fix attempt!

Unfortunately, I'm not seeing improved behavior yet with the branch. I'm testing with two devices, and I've noticed that the devices eventually show up in the scans if you wait for 30 sec or more.

I'm interested to see what others are seeing.

kent-williams avatar Jun 18 '25 17:06 kent-williams

This issue only seems to affect Android. I added scanMode: ScanMode.Balanced to the Android scan options, and it fixed the problem. Hope this helps!

const scanOptions: ScanOptions = {
  callbackType: ScanCallbackType.AllMatches,
  ...(Platform.OS === 'android' && { scanMode: ScanMode.Balanced }),
};

manager.startDeviceScan(null, scanOptions, (error, device) => {
  // …
});

yoshifumi4423 avatar Jun 19 '25 17:06 yoshifumi4423

Decreasing the scan latency with ScanMode pretty much fixed things for me. Thanks @yoshifumi4423 !

kent-williams avatar Jun 19 '25 18:06 kent-williams