h2zero / NimBLE-Arduino

A fork of the NimBLE library structured for compilation with Arduino, for use with ESP32, nRF5x.
https://h2zero.github.io/NimBLE-Arduino/
Apache License 2.0
672 stars 138 forks source link

Server and Client on one device, core dump on using uninitalized NimBLEAdvertisedDevice #561

Closed frankcohen closed 1 month ago

frankcohen commented 1 year ago

I'm getting a core panic when running code that connects to a BLE server. I am using NimBLE 1.4.1, ESP32-S3, Arduino Version: 2.1.1-nightly-20230627, and derived by code from https://github.com/h2zero/NimBLE-Arduino/blob/release/1.4/examples/NimBLE_Server/NimBLE_Server.ino and https://github.com/h2zero/NimBLE-Arduino/blob/release/1.4/examples/NimBLE_Client/NimBLE_Client.ino.

My code crashes when executing:

    if (!pClient->connect(advDevice)) 
    {
      /** Created a client but failed to connect, don't need to keep it as it has no data */
      NimBLEDevice::deleteClient(pClient);
      Serial.println("Failed to connect, deleted client");
      return false;
    }

I'm going to keep debugging to solve the problem, and I will post the solution here. Still, if anyone wants to offer me ideas, solutions, and criticism I would be glad to receive it. Thanks, in advance.

Here is what I see in the Serial Monitor:

CALLIOPE-F4: Server advertising started
CALLIOPE-F4: BLE Advertised Device found: Name: CALLIOPE-B4, Address: 60:55:f9:f5:c3:b5, serviceUUID: 7d9029fe-48d5-49e0-b9ad-8fd7dac70354
New client created
a
b
c
advDevice == nothing
CALLIOPE-F4: Failed to connect to server
CALLIOPE-F4: BLE Advertised Device found: Name: CALLIOPE-B4, Address: 60:55:f9:f5:c3:b5, serviceUUID: 7d9029fe-48d5-49e0-b9ad-8fd7dac70354
Guru Meditation Error: Core  1 panic'ed (LoadProhibited). Exception was unhandled.

Core  1 register dump:
PC      : 0x40056f17  PS      : 0x00060530  A0      : 0x82007fc8  A1      : 0x3fcebe30  
A2      : 0x3fcebe49  A3      : 0x00000000  A4      : 0x00000007  A5      : 0x3fcebe49  
A6      : 0x02c982d8  A7      : 0x00ffffff  A8      : 0x60023000  A9      : 0x60023040  
A10     : 0x1d87af98  A11     : 0x000fffff  A12     : 0x00000003  A13     : 0x3fca4c40  
A14     : 0x00060023  A15     : 0x00000003  SAR     : 0x0000000a  EXCCAUSE: 0x0000001c  
EXCVADDR: 0x00000000  LBEG    : 0x40056f5c  LEND    : 0x40056f72  LCOUNT  : 0xffffffff  

Backtrace: 0x40056f14:0x3fcebe30 |<-CORRUPTED

ELF file SHA256: 9c739da8436c0751

Rebooting...
ESP-ROM:esp32s3-20210327
Build:Mar 27 2021
rst:0xc (RTC_SW_CPU_RST),boot:0x28 (SPI_FAST_FLASH_BOOT)
Saved PC:0x420c07e2
SPIWP:0xee
mode:DIO, clock div:1
load:0x3fce3808,len:0x44c
load:0x403c9700,len:0xbe4
load:0x403cc700,len:0x2a38
entry 0x403c98d4

BLE.h contents:

#ifndef _BLE_
#define _BLE_

#include "Arduino.h"
#include <WiFi.h>

#include "config.h"
#include "secrets.h"

#include <NimBLEDevice.h>

class BLE
{
  public:
    BLE();
    void begin();
    void loop();
    String getHeading();
    void setHeading( String heading );
    String getHeadingValue();
    bool connectToServer();

  private:
    std::string devname;
    int mycounter;
    unsigned long serverWaitTime;
    unsigned long clientWaitTime;
    String heading;
};

#endif // _BLE_

BLE.cpp contents:

 Reflections, distributed entertainment device

 Repository is at https://github.com/frankcohen/ReflectionsOS
 Includes board wiring directions, server side components, examples, support

 Licensed under GPL v3 Open Source Software
 (c) Frank Cohen, All rights reserved. fcohen@starlingwatch.com
 Read the license in the license.txt file that comes with this code.

 I originally tried using the ESP32 blue BLE library. I ran out of memory. Switched to
 NimBLE-Arduino, an Arduino fork of Apache NimBLE, a replacement open-source BLE library
 https://github.com/h2zero/NimBLE-Arduino
 https://www.arduino.cc/reference/en/libraries/nimble-arduino/
 https://h2zero.github.io/esp-nimble-cpp/md__migration_guide.html#autotoc_md46

Todo:
Make this work among multiple Reflections devices
Balance the active scanning for battery life

*/

#include "BLE.h"

BLE::BLE(){}

// Server
static NimBLEServer* pServer;
static NimBLECharacteristic* pHeadingCharacteristic;
static boolean serverConnected;
static boolean clientConnected;
static String devicename;
static NimBLERemoteCharacteristic* pRemoteCharacteristic;
static NimBLEAdvertisedDevice* myDevice;
static boolean doConnect = false;
static boolean connected = false;
static boolean doScan = false;
static String remotename;
static NimBLEAdvertisedDevice* advDevice;
static uint32_t scanTime = 0; /** 0 = scan forever */

static BLEUUID serviceUUID( SERVICE_UUID_CALLIOPE );
static BLEUUID charUUID( CHARACTERISTIC_UUID_HEADING );

// Client

String BLE::getHeading()
{
  return heading;
}

void BLE::setHeading( String _heading )
{
  heading = _heading;
}

String BLE::getHeadingValue()
{
  String myc = String( mycounter++ ) + heading;
  std::string mys = myc.c_str();
  return myc;
}

/** Callback to process the results of the last scan or restart it */
void scanEndedCB(NimBLEScanResults results){
    Serial.println("Scan Ended");
}

/**
 * Scan for BLE servers and find the first one that advertises the service we are looking for.
 */
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
 /**
   * Called for each advertising BLE server.
   */

  void onResult(BLEAdvertisedDevice* advertisedDevice) 
  {
    //Serial.print( devicename );
    //Serial.print(": BLE advertised device found: ");
    //Serial.println(advertisedDevice->toString().c_str());

    // We have found a device, let us now see if it contains the service we are looking for.
    if ( advertisedDevice -> haveServiceUUID() && advertisedDevice -> isAdvertisingService( serviceUUID ) )
    {
      remotename = advertisedDevice->toString().c_str();

      Serial.print( devicename );
      Serial.print(": BLE Advertised Device found: ");
      Serial.println( remotename );

      BLEDevice::getScan()->stop();
      myDevice = advertisedDevice;
      doConnect = true;
      doScan = true;

    }
  }
};

class ServerCallbacks: public NimBLEServerCallbacks {

    void onConnect(NimBLEServer* pServer) 
    {
      Serial.print( devicename );
      Serial.println(": Server connected");
      NimBLEDevice::startAdvertising();
    };

    void onDisconnect(NimBLEServer* pServer) 
    {
      Serial.print( devicename );
      Serial.println(": Client disconnected, starting advertising");
      NimBLEDevice::startAdvertising();
    };

    void onMTUChange(uint16_t MTU, ble_gap_conn_desc* desc) {
      Serial.print( devicename );
      Serial.printf(": BLE MTU updated: %u for connection ID: %u\n", MTU, desc->conn_handle);
    };
};

class MyClientCallback: public BLEClientCallbacks {

  void onConnect(BLEClient* pclient) 
  {
    Serial.print( devicename );
    Serial.println(": onConnect");
  }

  void onDisconnect(BLEClient* pclient) {
    connected = false;
    Serial.print( devicename );
    Serial.println(": onDisconnect");
  }

};

/**  None of these are required as they will be handled by the library with defaults. **
 **                       Remove as you see fit for your needs                        */
class ClientCallbacks: public NimBLEClientCallbacks {
    void onConnect(NimBLEClient* pClient) {
        Serial.println("Connected");
        /** After connection we should change the parameters if we don't need fast response times.
         *  These settings are 150ms interval, 0 latency, 450ms timout.
         *  Timeout should be a multiple of the interval, minimum is 100ms.
         *  I find a multiple of 3-5 * the interval works best for quick response/reconnect.
         *  Min interval: 120 * 1.25ms = 150, Max interval: 120 * 1.25ms = 150, 0 latency, 60 * 10ms = 600ms timeout
         */
        pClient->updateConnParams(120,120,0,60);
    };

    void onDisconnect(NimBLEClient* pClient) {
        Serial.print(pClient->getPeerAddress().toString().c_str());
        Serial.println(" Disconnected - Starting scan");
        NimBLEDevice::getScan()->start(scanTime, scanEndedCB);
    };

    /** Called when the peripheral requests a change to the connection parameters.
     *  Return true to accept and apply them or false to reject and keep
     *  the currently used parameters. Default will return true.
     */
    bool onConnParamsUpdateRequest(NimBLEClient* pClient, const ble_gap_upd_params* params) {
        if(params->itvl_min < 24) { /** 1.25ms units */
            return false;
        } else if(params->itvl_max > 40) { /** 1.25ms units */
            return false;
        } else if(params->latency > 2) { /** Number of intervals allowed to skip */
            return false;
        } else if(params->supervision_timeout > 100) { /** 10ms units */
            return false;
        }

        return true;
    };

    /********************* Security handled here **********************
    ****** Note: these are the same return values as defaults ********/
    uint32_t onPassKeyRequest(){
        Serial.println("Client Passkey Request");
        /** return the passkey to send to the server */
        return 123456;
    };

    bool onConfirmPIN(uint32_t pass_key){
        Serial.print("The passkey YES/NO number: ");
        Serial.println(pass_key);
    /** Return false if passkeys don't match. */
        return true;
    };

    /** Pairing process complete, we can check the results in ble_gap_conn_desc */
    void onAuthenticationComplete(ble_gap_conn_desc* desc){
        if(!desc->sec_state.encrypted) {
            Serial.println("Encrypt connection failed - disconnecting");
            /** Find the client with the connection handle provided in desc */
            NimBLEDevice::getClientByID(desc->conn_handle)->disconnect();
            return;
        }
    };
};

/** Handler class for server characteristic actions */

class CharacteristicCallbacks: public NimBLECharacteristicCallbacks {
    void onRead(NimBLECharacteristic* pCharacteristic){
      Serial.print( devicename );
      Serial.print(": BLE server ");
      Serial.print(pCharacteristic->getUUID().toString().c_str());
      Serial.print(": onRead(), value: ");
      Serial.println(pCharacteristic->getValue().c_str());
    };

    void onWrite(NimBLECharacteristic* pCharacteristic) {
      Serial.print( devicename );
      Serial.print( ": ");
      Serial.print(pCharacteristic->getUUID().toString().c_str());
      Serial.print(": onWrite(), value: ");
      Serial.println(pCharacteristic->getValue().c_str());
    };
    /** Called before notification or indication is sent,
     *  the value can be changed here before sending if desired.
     */
    void onNotify(NimBLECharacteristic* pCharacteristic) {
      Serial.print( devicename );
      Serial.println(": Sending notification to clients");
    };

    /** The status returned in status is defined in NimBLECharacteristic.h.
     *  The value returned in code is the NimBLE host return code.
     */
    void onStatus(NimBLECharacteristic* pCharacteristic, Status status, int code) {
      String str = ("Notification/Indication status code: ");
      str += status;
      str += ", return code: ";
      str += code;
      str += ", ";
      str += NimBLEUtils::returnCodeToString(code);

      Serial.print( devicename );
      Serial.print( ": ");
      Serial.println(str);
    };

    void onSubscribe(NimBLECharacteristic* pCharacteristic, ble_gap_conn_desc* desc, uint16_t subValue) {
        String str = "Client ID: ";
        str += desc->conn_handle;
        str += " Address: ";
        str += std::string(NimBLEAddress(desc->peer_ota_addr)).c_str();
        if(subValue == 0) {
            str += " Unsubscribed to ";
        }else if(subValue == 1) {
            str += " Subscribed to notfications for ";
        } else if(subValue == 2) {
            str += " Subscribed to indications for ";
        } else if(subValue == 3) {
            str += " Subscribed to notifications and indications for ";
        }
        str += std::string(pCharacteristic->getUUID()).c_str();

        Serial.println(str);
    };
};

/** Notification / Indication receiving handler callback */

void notifyCB(NimBLERemoteCharacteristic* pRemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify){
    std::string str = (isNotify == true) ? "Notification" : "Indication";
    str += " from ";
    /** NimBLEAddress and NimBLEUUID have std::string operators */
    str += std::string(pRemoteCharacteristic->getRemoteService()->getClient()->getPeerAddress());
    str += ": Service = " + std::string(pRemoteCharacteristic->getRemoteService()->getUUID());
    str += ", Characteristic = " + std::string(pRemoteCharacteristic->getUUID());
    str += ", Value = " + std::string((char*)pData, length);
    Serial.println(str.c_str());
}

/** Create a single global instance of the callback class to be used by all clients */
static ClientCallbacks clientCB;

bool BLE::connectToServer() 
{
  NimBLEClient* pClient = nullptr;

  /** Check if we have a client we should reuse first **/
  if(NimBLEDevice::getClientListSize()) 
  {
    /** Special case when we already know this device, we send false as the
      *  second argument in connect() to prevent refreshing the service database.
      *  This saves considerable time and power.
      */
    pClient = NimBLEDevice::getClientByPeerAddress(advDevice->getAddress());
    if(pClient)
    {
      if(!pClient->connect(advDevice, false)) 
      {
        Serial.print( devicename );
        Serial.println(": Reconnect failed");
        return false;
      }
      Serial.print( devicename );
      Serial.println(": Reconnected client");
    }
    else 
    {
      pClient = NimBLEDevice::getDisconnectedClient();
    }
  }

  /** No client to reuse? Create a new one. */
  if( !pClient ) 
  {
    if(NimBLEDevice::getClientListSize() >= NIMBLE_MAX_CONNECTIONS) 
    {
      Serial.println("Max clients reached - no more connections available");
      return false;
    }

    pClient = NimBLEDevice::createClient();

    Serial.println("New client created");

    Serial.println("a");

    pClient->setClientCallbacks(&clientCB, false);
    /** Set initial connection parameters: These settings are 15ms interval, 0 latency, 120ms timout.
      *  These settings are safe for 3 clients to connect reliably, can go faster if you have less
      *  connections. Timeout should be a multiple of the interval, minimum is 100ms.
      *  Min interval: 12 * 1.25ms = 15, Max interval: 12 * 1.25ms = 15, 0 latency, 51 * 10ms = 510ms timeout
      */
    Serial.println("b");

    pClient->setConnectionParams(12,12,0,51);
    /** Set how long we are willing to wait for the connection to complete (seconds), default is 30. */
    pClient->setConnectTimeout(5);

    Serial.println("c");

    if ( advDevice )
    {
      Serial.println( "advDevice == something" );
    }
    else
    {
      Serial.println( "advDevice == nothing" );
      return false;
    }

    if (!pClient->connect(advDevice)) 
    {
      /** Created a client but failed to connect, don't need to keep it as it has no data */
      NimBLEDevice::deleteClient(pClient);
      Serial.println("Failed to connect, deleted client");
      return false;
    }

    Serial.println("d");
    if(!pClient->isConnected()) {
        if (!pClient->connect(advDevice)) {
            Serial.println("Failed to connect");
            return false;
        }
    }

    Serial.print("Connected to: ");
    Serial.println(pClient->getPeerAddress().toString().c_str());
    Serial.print("RSSI: ");
    Serial.println(pClient->getRssi());

    /** Now we can read/write/subscribe the charateristics of the services we are interested in */
    NimBLERemoteService* pSvc = nullptr;
    NimBLERemoteCharacteristic* pChr = nullptr;
    NimBLERemoteDescriptor* pDsc = nullptr;

    Serial.println("e");

    pSvc = pClient->getService( serviceUUID );
    if( pSvc )
    {
      pChr = pSvc->getCharacteristic( charUUID );

      if(pChr) 
      {
        if(pChr->canRead()) {
            Serial.print(pChr->getUUID().toString().c_str());
            Serial.print(" Value: ");
            Serial.println(pChr->readValue().c_str());
        }

        /** registerForNotify() has been deprecated and replaced with subscribe() / unsubscribe().
          *  Subscribe parameter defaults are: notifications=true, notifyCallback=nullptr, response=false.
          *  Unsubscribe parameter defaults are: response=false.
          */
        if(pChr->canNotify()) {
            //if(!pChr->registerForNotify(notifyCB)) {
            if(!pChr->subscribe(true, notifyCB)) {
                /** Disconnect if subscribe failed */
                pClient->disconnect();
                return false;
            }
          }        
        else if(pChr->canIndicate())
        {
          /** Send false as first argument to subscribe to indications instead of notifications */
          //if(!pChr->registerForNotify(notifyCB, false)) {
          if( !pChr -> subscribe(false, notifyCB) ) 
          {
            /** Disconnect if subscribe failed */
            pClient->disconnect();
            return false;
          }
        }
      }
    } 
    else
    {
      Serial.print( devicename );
      Serial.println(": Service not found.");
    }

    Serial.println("Done with this device!");
    return true;
  }
}

static CharacteristicCallbacks chrCallbacks;

void BLE::begin()
{
  devname = "CALLIOPE-";
  std::string mac = WiFi.macAddress().c_str();
  devname.append( mac.substr( 15, 2 ) );
  NimBLEDevice::init( devname );

  devicename = devname.c_str();

  advDevice = nullptr;

  mycounter = 0;
  clientWaitTime = millis();
  clientConnected = false;
  serverWaitTime = millis();
  serverConnected = false;

  // Server set-up

  pServer = NimBLEDevice::createServer();
  pServer->setCallbacks( new ServerCallbacks() );

  NimBLEService* pService = pServer -> createService( SERVICE_UUID_CALLIOPE );

  pHeadingCharacteristic = pService -> createCharacteristic( CHARACTERISTIC_UUID_HEADING, NIMBLE_PROPERTY::READ );
  pHeadingCharacteristic -> setCallbacks( &chrCallbacks );

  pHeadingCharacteristic -> setValue( getHeadingValue() );

  pService->start();

  NimBLEAdvertising* pAdvertising = NimBLEDevice::getAdvertising();
  pAdvertising -> addServiceUUID( pService -> getUUID() );
  pAdvertising->setScanResponse(true);
  pAdvertising->setMinPreferred(0x06);  // functions that help with iPhone connections issue
  pAdvertising->setMaxPreferred(0x12);

  NimBLEDevice::startAdvertising();

  Serial.print( devicename );
  Serial.println(": Server advertising started");

  // Client set-up

  // Retrieve a Scanner and set the callback we want to use to be informed when we
  // have detected a new device.  Specify that we want active scanning and start the
  // scan to run for 5 seconds.
  BLEScan* pBLEScan = BLEDevice::getScan();
  pBLEScan -> setAdvertisedDeviceCallbacks( new MyAdvertisedDeviceCallbacks() );
  pBLEScan -> setInterval(1349);
  pBLEScan -> setWindow(449);
  pBLEScan -> setActiveScan(true);
  pBLEScan -> start(5, false);
}

void BLE::loop()
{
  // Server
  if ((millis() - serverWaitTime) > 5000)
  {
    serverWaitTime = millis();

    String myc = String( mycounter++ ) + heading;
    std::string mys = myc.c_str();
    pHeadingCharacteristic->setValue( mys );
  }

  // Client

  if ((millis() - clientWaitTime) > 5000)
  {
    clientWaitTime = millis();

    // If the flag "doConnect" is true then we have scanned for and found the desired
    // BLE Server with which we wish to connect.  Now we connect to it.  Once we are 
    // connected we set the connected flag to be true.

    if ( doConnect == true )
    {
      if ( connectToServer() )
      {
        Serial.print( devicename );
        Serial.println(": Connected to BLE server.");
        doScan = false;
        doConnect = true;
    } 
      else
      {
        Serial.print( devicename );
        Serial.println(": Failed to connect to server" );
        doConnect = true;
        doScan = true;
      }
    }

    if(doScan)
    {
      NimBLEDevice::getScan()->start(0);  // this is just eample to start scan after disconnect, most likely there is better way to do it in arduino
      doScan = false;
    }
  }
}
frankcohen commented 1 year ago

This is a part of my Reflection's open source entertainment platform making project https://github.com/frankcohen/ReflectionsOS. The code needs to have server and client on the same device as the device may be in a room full of other devices, and there's no way to declare a single device to be a beacon or server. -Frank

frankcohen commented 1 year ago

Found it. Line 34: static NimBLEAdvertisedDevice* myDevice; needs to be named advDevice, and line 39 deleted, and line 93 needs to be advDevice = advertisedDevice; Of course, now I get a different error:

E NimBLEClient: A connection to 60:55:f9:f5:c3:b5 already exists

Digging into this next. I love C/C++, not.

chrisdimoff commented 6 months ago

@frankcohen Did you figure out your new error? I am having some problem

frankcohen commented 6 months ago

@frankcohen Did you figure out your new error? I am having some problem

I found that I was making some basic mistakes on how to initialize the libraries objects. The working code is in BLE.cpp and BLE.h in https://github.com/frankcohen/ReflectionsOS/tree/main/Devices/BlueFish/ReflectionsOfFrank. Glad to get your feedback and changes.

chrisdimoff commented 6 months ago

Thanks!

On Wed, Dec 27, 2023 at 2:27 PM Frank Cohen @.***> wrote:

@frankcohen https://github.com/frankcohen Did you figure out your new error? I am having some problem

I found that I was making some basic mistakes on how to initialize the libraries objects. The working code is in BLE.cpp and BLE.h in https://github.com/frankcohen/ReflectionsOS/tree/main/Devices/BlueFish/ReflectionsOfFrank. Glad to get your feedback and changes.

— Reply to this email directly, view it on GitHub https://github.com/h2zero/NimBLE-Arduino/issues/561#issuecomment-1870570829, or unsubscribe https://github.com/notifications/unsubscribe-auth/AFKFNLVXPKEP6GTRD2Y5P53YLRZBDAVCNFSM6AAAAAAZY2QPSOVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTQNZQGU3TAOBSHE . You are receiving this because you commented.Message ID: @.***>