BLE Pairing not possible with Nuki 2.0

I’m using a microcontroller to connect to the Nuki lock via BLE. This worked quite fine with Nuki 1.0 .
But now with 2.0 I wasn’t able to pair with the lock yet. Already wasted a whole afternoon thinking it was some problem with my firmware, when it finally occured to me that I’m using the new Nuki version which might be causing the problem. Used a Nuki 1.0 : works fine.

I’m iniating the pairing by sending 0100030027A7 as per the documentation but there is no response, not even an error.

The firmware on the lock is version 2.3.11

I suspect there are some undocumented changes in the API

This has been somewhat mentioned here as well: Nuki 2.0 Bluetooth API documentation

Edit for future readers:
Solved
Problem had nothing do with the smart lock itself. I’m using an ESP32 with the Espressif Arduino library: https://github.com/espressif/arduino-esp32
Updating the library, specifically the BLE library, to 1.0.1 solved the problem

Hi Felix,

In general the pairing process of Smart Lock 2.0 works the same way the Smart Lock 1.0 did.
This is necessary to be compatible with previous products (Fob, Bridge).

But due to the HomeKit integration there are more BLE services and characteristics available on the Smart Lock and some of the handles to access the characteristics did change.

How do you establish the connection to the Smart Lock?
Do you first perform a service discovery, match the service and then discover the characteristics of the service or do you use fixed handles to connect to the characteristics?

regards,
Marc

To be honest I’m only maintaining that firmware, so I’m no expert. But here’s what I gathered:

I do a scan for all BLE devices in the vicinity and filter the Nuki devices by name.
Then I connect to the pairing service with:

  • the lock’s MAC address
  • the service’s UUID: a92ee100-5501-11e4-916c-0800200c9a66
  • the service’s characteristic’s UUID: a92ee101-5501-11e4-916c-0800200c9a66

From the looks of it the connection succeeds (at least that’s what the BLE library tells me). Then I send 0100030027A7 to initiate the pairing but receive no answer.
I did notice that it behaves slightly differently depending on if the Nuki’s in pairing mode or not.
When it’s not in pairing mode I’m disconnected immediately and get an error from the BLE level if I try to send something again. With Nuki 1.0 I would instead get an error from the lock that it is not in pairing mode. I could just keep sending the command until I put the lock into pairing mode.
When it is in pairing mode I do not get these BLE errors but there still is no response. But that looks to me like I must have a more or less valid connection to the device.

I did not notice any change in the documentation regarding these changed handles.
Is there way to detect whether I’m communicating with a Nuki 1.0 or Nuki 2.0 before the pairing? Maybe based on the Nuki ID?

Do you register for indications before you write your command on the characteristic?

You may differ between Smart Lock 1.0 and 2.0 by looking at the services available.
Smart Lock 2.0 has many more BLE services that are necessary for HomeKit.

regards,
Marc

Thank you for you help so far, Marc.

You may differ between Smart Lock 1.0 and 2.0 by looking at the services available.
Smart Lock 2.0 has many more BLE services that are necessary for HomeKit.

OK that looks promising.

Do you register for indications before you write your command on the characteristic?

Yep. I had a closer look now at the code. Here’s the general workflow:

  1. Find a “Nuki…” BLE device in the vicinity
  2. Get all services and their characteristics from the device (via its MAC address). Verify that the keyturner pairing service is available (a92ee100-5501-11e4-916c-0800200c9a66) along with the proper characteristic (a92ee101-5501-11e4-916c-0800200c9a66)
  3. Register for notifications on this characteristic
  4. Write the value 0100030027A7 into the characteristic to initiate the pairing

That should be it. Works fine for Nuki 1.0, but I get no response with the Nuki 2.0 .
With the Android app “nRF Connect” I scanned the Smart Lock and was able to read-back the exact value the microcontroller wrote into the characteristic. The writing part works at the least.

I’m a bit stuck here. Guess I’ll have to setup a Bluetooth sniffer and check what’s going on, though I’d rather avoid that step.

  1. Register for notifications on this characteristic

Please make sure you register for indications not only for notifications on the characteristics.

regards,
Marc

I’m interested about this post. But I don’t know how to manage the step : 3. Register for notifications on this characteristic
What does it mean ?
I’m using an Arduino Nano with an BLE Module (HM-10). I can connect to the Nuki via AT+CONXXXX but I obtain an LOST command few seconds later.

You find some examples in the BLE API documentation:
https://developer.nuki.io/page/nuki-smart-lock-api-200/2/#heading--authorize-app

Can you go into more detail at which step you got problems?

Hello. The examples in the BLE API documentation contain only 1 line to describe the step “3. Register for notifications on this characteristic”. After connecting my BLE Module to Nuki SL with AT command I don’t know how to manage this step. I guess that I need to use a BLE Librairie with my Arduino to “register for notification” but I don’t know how to. Do you have any example code for this step in c or c++ langage ? As I wrote, I lose my “AT” Connexion few seconds after I obtained the AT+CONA answer (using the adress of my nuki obtained by an AT+DISC? command).

Finally solved it by updating the BLE library. I still don’t really know what the actual issue was, but oh well.
This was a good tip however; I did indeed register for indications, but the updated library changed that bevahior and turned it around. Only found about that via Wireshark.

So thanks for your help!

Hello Felix,
Good to know. But Can you share an example of code you used to ?
I’m using HM-10 module. Can’t find complete library for BLE. What BLE module do you use ?
Thanks.

I’m using an ESP32: https://github.com/espressif/arduino-esp32
There is a bluetooth module integrated in the chip, the library is high-level and relatively simple. So sorry, can’t really help you out.

I have one guess for you:

That’s how BLE works, you don’t have a permanent connection to the smart lock.

Thank you Felix. I will try with an ESP32 component. I’m interested by your Arduino code using this library. I need to manage lock & unlock with my Nuki via BLE.

I don’t get any notification from Nuki trying to write in Pairing Characteristic.
Here is my config : HELTEC Lora ESP32 / Nuki Firmware : 1.7.3

Code :

#include "BLEDevice.h"
//#include "BLEScan.h"

// The remote service we wish to connect to.
static BLEUUID serviceUUID("a92ee200-5501-11e4-916c-0800200c9a66"); //KeyTurner Service

// The characteristic of the remote service we are interested in.
static BLEUUID    charUUID("a92ee201-5501-11e4-916c-0800200c9a66");//GDIO Characteristic ! 

static boolean connected = false;
static BLERemoteCharacteristic* pRemoteCharacteristic;

static void notifyCallback(
  BLERemoteCharacteristic* pBLERemoteCharacteristic,
  uint8_t* pData,
  size_t length,
  bool isNotify) {
    Serial.print("Notify callback for characteristic ");
    Serial.print(pBLERemoteCharacteristic->getUUID().toString().c_str());
    Serial.print(" of data length ");
    Serial.println(length);
    Serial.print("data: ");
    Serial.print(pBLERemoteCharacteristic->readValue().c_str());
}

class MyClientCallback : public BLEClientCallbacks {
  void onConnect(BLEClient* pclient) {
  }

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

bool connectToServer() {
    Serial.print("Forming a connection to ");
    std::string myNukiAddr="54:d2:72:39:19:71";
    
    BLEClient*  pClient  = BLEDevice::createClient();
    Serial.println(" - Created client");

    pClient->setClientCallbacks(new MyClientCallback());

    // Connect to the remove BLE Server.
    Serial.print(" Essai de connexion à Nuki");
  
    pClient->connect(myNukiAddr);  // if you pass BLEAdvertisedDevice instead of address, it will be recognized type of peer device address (public or private)
    Serial.println(" - Connected to server");

    // Obtain a reference to the service we are after in the remote BLE server.
    BLERemoteService* pRemoteService = pClient->getService(serviceUUID);
    if (pRemoteService == nullptr) {
      Serial.print("Failed to find our service UUID: ");
      Serial.println(serviceUUID.toString().c_str());
      pClient->disconnect();
      return false;
    }
    Serial.println(" - Found our service");


    // Obtain a reference to the characteristic in the service of the remote BLE server.
    pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
    if (pRemoteCharacteristic == nullptr) {
      Serial.print("Failed to find our characteristic UUID: ");
      Serial.println(charUUID.toString().c_str());
      pClient->disconnect();
      return false;
    }
    Serial.println(" - Found our characteristic");
    String newValue = "0x0100030027A7";
    Serial.print("CanRead : ");
    Serial.println(pRemoteCharacteristic->canRead()); 
    Serial.print("CanWrite : ");
    Serial.println(pRemoteCharacteristic->canWrite()); 
    Serial.print("CanNotify : ");
    Serial.println(pRemoteCharacteristic->canNotify()); 
 
    if(pRemoteCharacteristic->canWrite()) {
      pRemoteCharacteristic->writeValue(newValue.c_str(), newValue.length());
      }
    // Read the value of the characteristic.
    if(pRemoteCharacteristic->canRead()) {
      uint32_t value = pRemoteCharacteristic->readUInt32();
Serial.printf("The characteristic value was: %#08x\n", value);
    //  std::string value = pRemoteCharacteristic->readValue();
    //  Serial.print("The characteristic value was: ");
    //  Serial.println(value.c_str());
    }    
    if(pRemoteCharacteristic->canNotify()) {
      pRemoteCharacteristic->registerForNotify(notifyCallback);
    } 
    connected = true;
     Serial.print("connected : ");
    Serial.println(connected); 
 
}

void setup() {
  Serial.begin(115200);
  Serial.println("Starting Arduino BLE Client application...");
  BLEDevice::init("");

    if (connectToServer()) {
      Serial.println("We are now connected to the BLE Server.");
    } else {
      Serial.println("We have failed to connect to the server; there is nothin more we will do.");
    } 


} // End of setup.


// This is the Arduino main loop function.
void loop() {
  // If we are connected to a peer BLE Server, update the characteristic each time 
  if (connected) {
//    String newValue = "Time since boot: " + String(millis()/1000);
      String newValue = "0100030027A7";
      if(pRemoteCharacteristic->canWrite()) {
      pRemoteCharacteristic->writeValue(newValue.c_str(), newValue.length());
      Serial.println("Setting new characteristic value to \"" + newValue + "\" ok");
        }
      else 
        {Serial.print("The characteristic value can't be written: ");}
    } else {
    Serial.println("not connected");
    }
  delay(5000); // Delay between loops.
} // End of loop

Serial Print Answer :
Starting Arduino BLE Client application…
Forming a connection to - Created client
Essai de connexion à Nuki - Connected to server

  • Found our service
  • Found our characteristic
    CanRead : 1
    CanWrite : 1
    CanNotify : 1
    The characteristic value was: 00000000
    connected : 1
    We are now connected to the BLE Server.
    Notify callback for characteristic a92ee201-5501-11e4-916c-0800200c9a66 of data length 7
    data:

(no change even if I enter Nuki in pairing mode or not)

Any Idea ?

@Felix : I’m still really interested in your code example if you mind, Thanks !

You are already on a good track. I think you are just missing a few things here:

  • For the pairing you need to use the Service UUID a92ee100-5501-11e4-916c-0800200c9a66 and the characteristics UUID a92ee101-5501-11e4-916c-0800200c9a66. The UUIDs you used are for after the pairing, to open the lock for example (the keyturner service)
  • You need to registerForNotify() before writing into the characteristic
  • You need to call registerForNotify(callback, FALSE) to get indications (with the most recent ESP32/BLE libraries)
  • Writing “0x0100030027A7” is not correct. You don’t want to send that string, but the actual byte values instead. This will write something entirely different (the string will translate to 0x30, 0x78, 0x30, 0x31…). Try writing a uint8_t array instead

Can’t easily share my code I’m afraid, as that is company owned. I can see if can extract some examples if you hit a will

Thanks a lot Felix for your advices. I checked every suggestions you’ve made. No success.
Point 1 : service & characteristic : done,
Point 2 : done, with
const uint8_t indicationOn[] = {0x2,0x0}; pRemoteCharacteristic->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)indicationOn,2,true);
pRemoteCharacteristic->registerForNotify(notifyCallback, false);
Point 3 : done with the latest ESP32/BLE library
Point 4 : tried with something like : uint8_t arrayFV[] = {0x01,0x00,0x03,0x00,0x27,0xA7}
-> and pRemoteCharacteristic->writeValue(arrayFV, 6);
Did I miss something ?

This not necessary, as registerForNotify() already does that.

Other than that I don’t think you are missing anything obvious as far as I can tell. Can you share your updated code again? I can take a look again this evening and on the weekend.

1 Like

Thank you very much Felix.
Here is my entire code.

/**
 * A BLE client example that is rich in capabilities.
 * There is a lot new capabilities implemented.
 * author unknown
 * updated by chegewara
 */

#include "BLEDevice.h"
#include "esp_log.h"
//#include "BLEScan.h"

// The remote service we wish to connect to.
static BLEUUID serviceUUID("a92ee100-5501-11e4-916c-0800200c9a66");//Pairing service

// The characteristic of the remote service we are interested in.
static BLEUUID    charUUID("a92ee101-5501-11e4-916c-0800200c9a66");//Characteristic of pairing service
//std::vector<uint8_t> response(200);
static boolean connected = false;
static BLERemoteCharacteristic* pRemoteCharacteristic;
uint8_t arrayFV[] = {0x01,0x00,0x03,0x00,0x27,0xA7}; //"0x0100030027A7";//first value to send as an array of byte to initiate Nuki Pairing

static void notifyCallback(
  BLERemoteCharacteristic* pBLERemoteCharacteristic,
  uint8_t* pData,
  size_t length,
  bool isNotify) {
    Serial.print("******************** Notify callback for characteristic ");
    Serial.println(pBLERemoteCharacteristic->getUUID().toString().c_str());
    Serial.print(" of data length ");
    Serial.println(length);
    Serial.print("data: ");
    Serial.println((char*)pData);
 //   for(int i= 0;i<length;i++) response.push_back(*pData++);
} // end notifyCallback

class MyClientCallback : public BLEClientCallbacks {
  void onConnect(BLEClient* pclient) {
  }

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

bool connectToServer() {
    Serial.print("Forming a connection to ");
    std::string myNukiAddr="54:d2:72:39:19:71";
    
    BLEClient*  pClient  = BLEDevice::createClient();
    Serial.println(" - Created client");

    pClient->setClientCallbacks(new MyClientCallback());

    // Connect to the remove BLE Server.
    Serial.print(" Essai de connexion à Nuki");
  
    pClient->connect(myNukiAddr);  // if you pass BLEAdvertisedDevice instead of address, it will be recognized type of peer device address (public or private)
    Serial.println(" - Connected to server");
    delay(100);
    // Obtain a reference to the service we are after in the remote BLE server.
    BLERemoteService* pRemoteService = pClient->getService(serviceUUID);
    if (pRemoteService == nullptr) {
      Serial.print("Failed to find our service UUID: ");
      Serial.println(serviceUUID.toString().c_str());
      pClient->disconnect();
      return false;
    }
    Serial.println(" - Found our service");

    // Obtain a reference to the characteristic in the service of the remote BLE server.
    pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
    if (pRemoteCharacteristic == nullptr) {
      Serial.print("Failed to find our characteristic UUID: ");
      Serial.println(charUUID.toString().c_str());
      pClient->disconnect();
      return false;
    }
    Serial.println(" - Found our characteristic");
    Serial.print("CanRead : ");
    Serial.println(pRemoteCharacteristic->canRead()); 
    Serial.print("CanWrite : ");
    Serial.println(pRemoteCharacteristic->canWrite()); 
    Serial.print("CanNotify : ");
    Serial.println(pRemoteCharacteristic->canNotify()); 
    Serial.print("CanIndicate : ");
    Serial.println(pRemoteCharacteristic->canIndicate()); 
   
    connected = true;
    Serial.print("connected : ");
    Serial.println(connected); 
}

void setup() {
//esp_log_level_set("*", ESP_LOG_DEBUG);
  Serial.begin(115200);
  Serial.println("Starting Arduino BLE Client application...");
  BLEDevice::init("");

    if (connectToServer()) {
      Serial.println("We are now connected to the BLE Server.");
    } else {
      Serial.println("We have failed to connect to the server; there is nothin more we will do.");
    } 
   //register for indication   
    const uint8_t indicationOn[] = {0x2,0x0};
    pRemoteCharacteristic->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)indicationOn,2,true);
    pRemoteCharacteristic->registerForNotify(notifyCallback, false); //false = indication, true = notification
    delay(100);
   //write Value
    if(pRemoteCharacteristic->canWrite()) {
      pRemoteCharacteristic->writeValue(arrayFV, 1, true);
            delay(50);           
      }
     std::string value = pRemoteCharacteristic->readValue();
      Serial.print("The characteristic value was: ");
      Serial.println(value.c_str());
} // End of setup


// This is the Arduino main loop function.
void loop() {
  while (connected) {
  Serial.println("debut loop...");
    Serial.println("Still connected, loop");
       // Read the value of the characteristic.
     std::string value = pRemoteCharacteristic->readValue();
      Serial.print("The characteristic value was: ");
      Serial.println(value.c_str());

      delay(1000); // Delay between loops.
    } 
    Serial.println("not connected");
    
} // End of loop

Hi! Hope it is o.k. I corrected the markup for your post so the code is in one piece. :slight_smile:

Btw., very nice to see you helping out each other where we can not provide much help ourselfs! :+1:

1 Like

You made one mistake, other than that it worked right away. I made some notes marked with //fw:

/**
 * A BLE client example that is rich in capabilities.
 * There is a lot new capabilities implemented.
 * author unknown
 * updated by chegewara
 */

#include "BLEDevice.h"
#include "esp_log.h"
//#include "BLEScan.h"

// The remote service we wish to connect to.
static BLEUUID serviceUUID("a92ee100-5501-11e4-916c-0800200c9a66");//Pairing service

// The characteristic of the remote service we are interested in.
static BLEUUID    charUUID("a92ee101-5501-11e4-916c-0800200c9a66");//Characteristic of pairing service
//std::vector<uint8_t> response(200);
static boolean connected = false;
static BLERemoteCharacteristic* pRemoteCharacteristic;
uint8_t arrayFV[] = {0x01,0x00,0x03,0x00,0x27,0xA7}; //"0x0100030027A7";//first value to send as an array of byte to initiate Nuki Pairing

static void notifyCallback(
  BLERemoteCharacteristic* pBLERemoteCharacteristic,
  uint8_t* pData,
  size_t length,
  bool isNotify) {
    Serial.print("******************** Notify callback for characteristic ");
    Serial.println(pBLERemoteCharacteristic->getUUID().toString().c_str());
    Serial.print(" of data length ");
    Serial.println(length);
    Serial.print("data: ");
    //fw: don't print this directly. the value is binary, not a 0-terminated string
    //Serial.println((char*)pData);
    debug_dump_binary(pData, length);
 //   for(int i= 0;i<length;i++) response.push_back(*pData++);
} // end notifyCallback

class MyClientCallback : public BLEClientCallbacks {
  void onConnect(BLEClient* pclient) {
  }

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

bool connectToServer() {
    Serial.print("Forming a connection to ");
    std::string myNukiAddr="54:d2:72:39:19:71";
    
    BLEClient*  pClient  = BLEDevice::createClient();
    Serial.println(" - Created client");

    pClient->setClientCallbacks(new MyClientCallback());

    // Connect to the remove BLE Server.
    Serial.print(" Essai de connexion à Nuki");
  
    pClient->connect(myNukiAddr);  // if you pass BLEAdvertisedDevice instead of address, it will be recognized type of peer device address (public or private)
    Serial.println(" - Connected to server");
    delay(100);
    // Obtain a reference to the service we are after in the remote BLE server.
    BLERemoteService* pRemoteService = pClient->getService(serviceUUID);
    if (pRemoteService == nullptr) {
      Serial.print("Failed to find our service UUID: ");
      Serial.println(serviceUUID.toString().c_str());
      pClient->disconnect();
      return false;
    }
    Serial.println(" - Found our service");

    // Obtain a reference to the characteristic in the service of the remote BLE server.
    pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
    if (pRemoteCharacteristic == nullptr) {
      Serial.print("Failed to find our characteristic UUID: ");
      Serial.println(charUUID.toString().c_str());
      pClient->disconnect();
      return false;
    }
    Serial.println(" - Found our characteristic");
    Serial.print("CanRead : ");
    Serial.println(pRemoteCharacteristic->canRead()); 
    Serial.print("CanWrite : ");
    Serial.println(pRemoteCharacteristic->canWrite()); 
    Serial.print("CanNotify : ");
    Serial.println(pRemoteCharacteristic->canNotify()); 
    Serial.print("CanIndicate : ");
    Serial.println(pRemoteCharacteristic->canIndicate()); 
   
    connected = true;
    Serial.print("connected : ");
    Serial.println(connected); 
}

void setup() {
//esp_log_level_set("*", ESP_LOG_DEBUG);
  Serial.begin(115200);
  Serial.println("Starting Arduino BLE Client application...");
  BLEDevice::init("");

    if (connectToServer()) {
      Serial.println("We are now connected to the BLE Server.");
    } else {
      Serial.println("We have failed to connect to the server; there is nothin more we will do.");
    } 
   //register for indication   
   //fw: don't need this. this is done by registerForNotify()
    //const uint8_t indicationOn[] = {0x2,0x0};
    //pRemoteCharacteristic->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)indicationOn,2,true);
    pRemoteCharacteristic->registerForNotify(notifyCallback, false); //false = indication, true = notification
    delay(100);
   //write Value
    if(pRemoteCharacteristic->canWrite()) {
      //fw: and here's your problem. you were sending the first byte only (writeValue(arrayFV, 1)
      pRemoteCharacteristic->writeValue(arrayFV, sizeof(arrayFV), true);
            delay(50);           
      }
     std::string value = pRemoteCharacteristic->readValue();
      Serial.print("The characteristic value was: ");
      Serial.println(value.c_str());
} // End of setup


// This is the Arduino main loop function.
void loop() {
  //fw: I don't think you should loop here. loop() is looping anyways
  Serial.println("debut loop...");
  if (connected) {
    Serial.println("Still connected, loop");
       // Read the value of the characteristic.
     std::string value = pRemoteCharacteristic->readValue();
      Serial.print("The characteristic value was: ");
      Serial.println(value.c_str());

      delay(1000); // Delay between loops.
    }
    else { 
     Serial.println("not connected");
    }
    
} // End of loop

void debug_dump_binary(const void* buffer, size_t size)
{

  for(size_t i = 0; i < size; ++i)
  {
    if(i > 0 && (i % 8) == 0)
    {
      Serial.println("");
    }
    Serial.printf("%0x ", ((const uint8_t*) buffer)[i]);
  }
}

Now you will need to do some crypto handshaking and encryption. I suggest you fight through that part on your own, provides a nice practice.

One more tip for dealing with the BLE library: it throws exceptions when the lock disconnects. That is why your code crashes after a few seconds. You just need to have some proper exception handling with try-catch blocks. The tricky thing here however: it sometimes throws Exceptions and sometimes it throws Exceptions* . Best practice here is probably to always have two catch blocks; one for Exception objects and one for Exception pointers. Like this:

catch(const std::exception* e)
{
  //handle it
  delete e;
}
catch(const std::exception& e)
{
  //handle it
}