noble / noble

A Node.js BLE (Bluetooth Low Energy) central module
MIT License
3.38k stars 864 forks source link

Error in peripheral.once('connect') keeps device from disconnecting #580

Open Bonitis opened 7 years ago

Bonitis commented 7 years ago

Setup noble: v1.8.0 node: v6.10.0 Raspbian Jessie on Raspberry Pi 3 USB BLE antenna with onboard BT disabled

After I reboot my rPi or restart my node app, I am no longer able to connect to devices, and am given this error:

0|tracker  | [Thu Mar 23 2017 02:26:54 GMT+0000 (UTC)] INFO Found device with local name: RHYTHM+452723
0|tracker  | [Thu Mar 23 2017 02:26:54 GMT+0000 (UTC)] ERROR Peripheral [RHYTHM+452723] connection error: Error: Command Disallowed

That is being triggered from my noble.on('connect') function, as well as a disconnect function, seen here:

SetUpBLE() {
    noble.on('discover', (peripheral) => {
      if (peripheral.state !== 'disconnected') {
        return;
      }

      log.info(`Found device with local name: ${peripheral.advertisement.localName.green}`);

      peripheral.once('connect', (error) => {
         if (error) {
           const err = `Peripheral [${peripheral.advertisement.localName}] connection error: ${error}`;
           this.DisconnectAndUntrackPeripheral(peripheral, err);
           return;
         }
         // discover services and characteristics...
      });

      peripheral.once('disconnect', (error) => {
        const hrm = HRMDataFromPeripheral(peripheral);

        if (error) {
          const err = `Error disconnecting from peripheral ${peripheral.advertisement.localName}: ${error}`;
          log.error(err);
          return;
        }

        log.info(`${peripheral.advertisement.localName.red} disconnected.`);

        // starting scan to see if we can reconnect to this HRM
        noble.startScanning(Constants.SERVICE_UUIDS, false, (err) => {
          if (err) {
            log.error(`StartScanning Error in peripheral disconnect method: ${err}`);
          }
        });
      });
   });
}

DisconnectAndUntrackPeripheral(peripheral, error) {
    if (error) {
      log.error(error);
    }
    const hrm = HRMDataFromPeripheral(peripheral);
    peripheral.disconnect();
 }

If I run sudo hcitool lescan then restart the app, it will sometimes fix the problem, but not always. Then, if it is fixed and I can connect to a device, if I turn that device off and back on, I sometimes get the same error. It seems like even though I am calling peripheral.disconnect() the 'disconnect' event is not firing.

How do I best avoid these connection errors, and when they do happen, what is the best way to try connecting to that device again?

sandeepmistry commented 7 years ago

This is not really a full code snippet that can be used to reproduce the issue. HRMDataFromPeripheral is missing for sure.

An HCI dump should give us some more info as to what's going on sudo hcidump -t -x

Also, what USB adapter are you using?

Bonitis commented 7 years ago

Here is the adapter info from hciconfig:

hci0:   Type: BR/EDR  Bus: USB
    BD Address: 00:25:BF:70:02:C8  ACL MTU: 310:12  SCO MTU: 64:8
    UP RUNNING 
    RX bytes:374409 acl:1055 sco:0 events:13782 errors:0
    TX bytes:3605 acl:55 sco:0 commands:205 errors:0
    Features: 0xff 0xff 0x8f 0xfe 0xdb 0xff 0x5b 0x87
    Packet type: DM1 DM3 DM5 DH1 DH3 DH5 HV1 HV2 HV3 
    Link policy: RSWITCH HOLD SNIFF PARK 
    Link mode: SLAVE ACCEPT 
    Name: 'raspberrypi-2-0'
    Class: 0x000000
    Service Classes: Unspecified
    Device Class: Miscellaneous, 
    HCI Version: 4.1 (0x7)  Revision: 0x2933
    LMP Version: 4.1 (0x7)  Subversion: 0x2933
    Manufacturer: Cambridge Silicon Radio (10)

I have since reworked my code, so here is a more relevant sample

import bleno from 'bleno';
import events from 'events';
import fs from 'fs';
import noble from 'noble';
import util from 'util';
import Log from 'log';
import colors from 'colors'; // eslint-disable-line no-unused-vars
import { size } from 'lodash';

import Constants from '../Constants';

function HRMDataFromPeripheral(hrm) {
  if (hrm) {
    return {
      id: hrm.id,
      address: hrm.address,
      name: hrm.advertisement.localName,
    };
  }
}

// starts the noble scanning process to discovery peripherals
const StartScanningForPeripherals = () => {
  log.debug('Starting to scan for peripherals');
  noble.startScanning(Constants.SERVICE_UUIDS, false, (err) => {
    if (err) {
      log.error(`startScanning error: ${err}`);
    }
  });
};

class Tracker {
  constructor() {
    this.Initialize();
  }

  Initialize() {
    this.SetUpBLE();
  }

  SetUpBLE() {
    noble.on('discover', (peripheral) => {
      if (peripheral.state !== 'disconnected') {
        return;
      }

      // eslint-disable-next-line no-underscore-dangle
      log.debug(`Noble currently showing ${size(noble._peripherals)} peripherals`);
      log.info(`Found device with local name: ${peripheral.advertisement.localName.green}`);

      peripheral.once('disconnect', (error) => {
        const hrm = HRMDataFromPeripheral(peripheral);

        if (error) {
          const err = `Error disconnecting from peripheral ${peripheral.advertisement.localName}: ${error}`;
          log.error(err);
          return;
        }

        log.info(`${peripheral.advertisement.localName.red} disconnected.`);
        StartScanningForPeripherals();
      });

      peripheral.once('connect', (error) => {
        if (error) {
          const err = `Peripheral [${peripheral.advertisement.localName}] connection error: ${error}`;
          this.DisconnectAndUntrackPeripheral(peripheral, err);
          return;
        }
        log.debug(`Connected to peripheral: ${peripheral.advertisement.localName.green}`);

        peripheral.discoverServices([Constants.HEART_RATE_SERVICE_UUID],
          (servicesError, services) => {
            if (servicesError) {
              const err = `Discover services error for ${peripheral.advertisement.localName}: ${error}`;
              this.DisconnectAndUntrackPeripheral(peripheral, err);
              return;
            }

            const deviceInformationService = services[0];
            if (deviceInformationService === undefined) {
              const err = `Error: deviceInformationService for ${peripheral.advertisement.localName.red} not found.`;
              this.DisconnectAndUntrackPeripheral(peripheral, err);
              return;
            }

            deviceInformationService.discoverCharacteristics([Constants.HEART_RATE_MEASUREMENT_CHARACTERTISTIC_UUID],
              (characteristicsError, characteristics) => {
                if (error || characteristicsError) {
                  const err = `Error: discoverCharacteristics failed for ${peripheral.advertisement.localName}: ${characteristicsError.red}`;
                  this.DisconnectAndUntrackPeripheral(peripheral, err);
                  return;
                }

                const hrCharacteristic = characteristics[0];

                if (hrCharacteristic) {
                  log.debug(`discovered hr measurement characteristic for ${peripheral.advertisement.localName}`);

                  // subscribe to the HR characteristic and check for errors
                  hrCharacteristic.subscribe((characteristicError) => {
                    if (characteristicError) {
                      const err = `Error with HR characteristic for ${peripheral.advertisement.localName}: ${characteristicError}`;
                      this.DisconnectAndUntrackPeripheral(peripheral, err);
                    }
                    log.info(`hr notification on for ${peripheral.advertisement.localName.green}`);
                  });
                } else {
                  // there is no HR characteristic, so disconnect to the peripheral, hopefully reconnecting
                  const err = `Error: no hrCharacteristic found for ${peripheral.advertisement.localName}, disconnecting`;
                  this.DisconnectAndUntrackPeripheral(peripheral, err);
                }
              });
          });
      });

      peripheral.connect();
    });

    noble.on('stateChange', (state) => {
      if (state === 'poweredOn') {
        log.debug('Bluetooth Powered On, Scanning for devices...'.green);
        StartScanningForPeripherals();
      } else {
        log.info('BLE Antenna state currently', state, '.  Waiting for device to Power on.');
         // noble.stopScanning();
      }
    });

    noble.on('scanStop', () => {
      log.debug('Detected bluetooth scan stop - scanning again');
      StartScanningForPeripherals();
    });

    noble.on('warning', (message) => {
      log.warning(`noble warning: ${message}`);
    });
  }

  DisconnectAndUntrackPeripheral(peripheral, error) {
    if (error) {
      log.error(error);
    }

    // disconnect from this peripheral
    peripheral.disconnect();

    // restart scanning to try to pick the device back up
    StartScanningForPeripherals();
  }
}

util.inherits(Tracker, events.EventEmitter);

module.exports = Tracker;

Constants.js

module.exports.HEART_RATE_SERVICE_UUID = '180d';
module.exports.HEART_RATE_MEASUREMENT_CHARACTERTISTIC_UUID = '2a37';
module.exports.SERVICE_UUIDS = [module.exports.HEART_RATE_SERVICE_UUID];

I don't have a relevant hcidump, but I will try to get you one. With my tweaked code I have not seen the same errors, but I am trying every possible scenario that would trigger an error. I don't know if what I have here is a good way to do it, but it seems to have less problems.

Bonitis commented 7 years ago

@sandeepmistry here is an hcidump. https://gist.github.com/Bonitis/ca14d7a82f797d2089614d9058bf5b0e

Bonitis commented 7 years ago

I finally got an hcidump while this command disallowed issue was happening: https://gist.github.com/Bonitis/b62f0977254ab1b4d3af6a9ecc5cbd86

I'm not really sure what to make of it, but it seems that if it happens to one device, I can't connect any other devices and need to reboot/restart.

benjaminhon commented 7 years ago

Yes, i have this issue as well, one onConnect gives an error, the peripheral is broken, noble no longer detects it, is there a way to recover from onConnect errors?

gotleg commented 6 years ago

@Bonitis I got command disallowed with my dongle when I try to connect to device without stopping the scan. You should try stop scanning before connect (carefull of your noble.on('scanStop', () ) method)

@benjaminhon Have a look at my Issue here : https://github.com/sandeepmistry/noble/issues/664 It looks similar to your problem, it may help.