Bluetooth Pairing not completing

I am using an Argon with OS 6.4.1 to connect via Bluetooth to a 3rd party device (Shelly relay). It all works great as long as the Shelly device runs pre-2.0 firmware.

Shelly added the requirement for Bluetooth pairing in their firmware 2.0 release, and I am having problems getting the Argon to fully pair with it.

I have verified that my phone (using an app called BLE Scanner) can easily pair and communicate with the Shelly, so it definitely works.

Using the Particle example code for LESC simple pairing (Just Works) from the Particle Docs, I see the following on the serial log:

0000013686 [app] INFO: about to connect
0000014441 [hal.ble] ERROR: Not a proven connection!!!
0000014441 [app] INFO: failed to connect, retrying
0000019442 [app] INFO: about to connect
0000020945 [app] INFO: onPairingEvent REQUEST_RECEIVED
0000022607 [app] INFO: onPairingEvent STATUS_UPDATED status=0 lesc=1 bonded=0
0000025037 [app] INFO: Connected, starting pairing...
0000027827 [hal.ble] ERROR: BLE read characteristic failed: 264, handle: 11.
0000027828 [app] INFO: RX LengthA 60
0000027828 [app] INFO: sent counter=0
0000029357 [hal.ble] ERROR: BLE read characteristic failed: 264, handle: 11.
0000029358 [app] INFO: RX LengthA 60
0000029359 [app] INFO: sent counter=1
0000031697 [hal.ble] ERROR: BLE read characteristic failed: 264, handle: 11.
0000031697 [app] INFO: RX LengthA 60
0000031698 [app] INFO: sent counter=2
0000033632 [hal.ble] ERROR: BLE read characteristic failed: 264, handle: 11.
0000033632 [app] INFO: RX LengthA 60
0000033633 [app] INFO: sent counter=3
0000035522 [hal.ble] ERROR: BLE read characteristic failed: 264, handle: 11.
0000035522 [app] INFO: RX LengthA 60
0000035523 [app] INFO: sent counter=4

On the Shelly phone app I see that a device (i.e. the Argon) connects BT, but the Shelly app never shows it being paired. Not pairing is also confirmed above by not being able to write a message to to the Shelly. (BTW if I run the same code on a pre-2.0 Shelly it works.)

Am i missing something in the pairing process in my code?

// Include Particle Device OS APIs
#include "Particle.h"

SYSTEM_THREAD(ENABLED);

SerialLogHandler logHandler;

void onPairingEvent(const BlePairingEvent& event, void* context);
void onDataReceived(const uint8_t* data, size_t len, const BlePeerDevice& peer, void* context);
void scanResultCallback(const BleScanResult *scanResult, void *context);
void stateConnect();
void stateRun();

BleAddress ShellyAddress = "ecda3bc249de"; //FAN - requires pairing LESC
//BleAddress ShellyAddress = "c049ef8ca546"; //SPARE - does not require pairing


//**************************************************
// Initialize the UUIDS for the Shelly Device switch
//**************************************************
BleUuid uuidSERVICE     ("5f6D4F53-5F52-5043-5F53-56435F49445F");
BleUuid uuidRW          ("5F6D4F53-5F52-5043-5F64-6174615F5F5F");
BleUuid uuidREAD_NOTIFY ("5F6D4F53-5F52-5043-5F72-785F63746C5F");
BleUuid uuidW           ("5F6D4F53-5F52-5043-5F74-785F63746C5F");

//*****************************************************
//Below used for when we are a peripheral for the Boron
//*****************************************************
BleCharacteristic txCharacteristic  (NULL, BleCharacteristicProperty::READ  | BleCharacteristicProperty::NOTIFY | BleCharacteristicProperty::INDICATE, uuidREAD_NOTIFY, uuidSERVICE);
BleCharacteristic txrxCharacteristic(NULL, BleCharacteristicProperty::WRITE | BleCharacteristicProperty::READ, uuidRW, uuidSERVICE, onDataReceived, NULL);
BleCharacteristic rxCharacteristic  (NULL, BleCharacteristicProperty::WRITE,                                   uuidW,  uuidSERVICE);

BleCharacteristic   peerTxCharacteristic;
BleCharacteristic   peerRxCharacteristic;
BleCharacteristic   peerRxTxCharacteristic;
const char setswStatus[] = "{\"id\":123,\"method\":\"Switch.Set\",\"params\":{\"id\":0,\"on\":true}}";


BlePeerDevice peer;
BleCharacteristic counterCharacteristic;

enum class State {
	SCAN,
	CONNECT,
	RUN,
	WAIT
};
State state = State::CONNECT; //Don't scn - we know address for Shelly
unsigned long stateTime;
BleAddress serverAddr;
int counter = 0;

void setup() {
	waitFor(Serial.isConnected, 10000);

	BLE.on();

	BLE.setPairingIoCaps(BlePairingIoCaps::DISPLAY_YESNO);
	BLE.setPairingAlgorithm(BlePairingAlgorithm::LESC_ONLY);
	BLE.onPairingEvent(onPairingEvent);

}

void loop() {
	switch(state) {
		case State::SCAN:
			state = State::WAIT;
			//BLE.scan(scanResultCallback, NULL);
			break;
			
		case State::CONNECT:
			stateConnect();
			break;

		case State::RUN:
			stateRun();
			break;

		case State::WAIT:
			if (millis() - stateTime >= 5000) {
				state = State::CONNECT;
			}
			break;
	}

}

void onPairingEvent(const BlePairingEvent& event, void* context) {
	if (event.type == BlePairingEventType::REQUEST_RECEIVED) {
		Log.info("onPairingEvent REQUEST_RECEIVED");
	}
	else
	if (event.type == BlePairingEventType::PASSKEY_DISPLAY) {
		char passKeyStr[BLE_PAIRING_PASSKEY_LEN + 1];
		memcpy(passKeyStr, event.payload.passkey, BLE_PAIRING_PASSKEY_LEN);
		passKeyStr[BLE_PAIRING_PASSKEY_LEN] = 0;

		Log.info("onPairingEvent PASSKEY_DISPLAY %s", passKeyStr);
	}
	else
	if (event.type == BlePairingEventType::STATUS_UPDATED) {
		Log.info("onPairingEvent STATUS_UPDATED status=%d lesc=%d bonded=%d", 
			event.payload.status.status,
			(int)event.payload.status.lesc,
			(int)event.payload.status.bonded);
	}
	else
	if (event.type == BlePairingEventType::NUMERIC_COMPARISON) {
		Log.info("onPairingEvent NUMERIC_COMPARISON");
	}
}


void onDataReceived(const uint8_t* data, size_t len, const BlePeerDevice& peer, void* context) {
	Log.info("dataReceived");
}

void stateConnect() {
	Log.info("about to connect");

	peer = BLE.connect(ShellyAddress);
	if (!peer.connected()) {
		Log.info("failed to connect, retrying");
		state = State::WAIT;
		stateTime = millis();
		return;
	}
	Log.info("Connected, starting pairing...");

	peer.getCharacteristicByUUID(peerTxCharacteristic,   uuidW);
	peer.getCharacteristicByUUID(peerRxCharacteristic,   uuidRW);
	peer.getCharacteristicByUUID(peerRxTxCharacteristic, uuidREAD_NOTIFY);
	
    
    //I've tried with and without this - no difference
    //The Shelly is in pairing mode when I start the Argon
	//BLE.startPairing(peer);

	state = State::RUN;
	stateTime = millis();
}

void stateRun() {
	if (!peer.connected()) {
		// Server disconnected
		state = State::WAIT;
		stateTime = millis();
		return;
	}
	if (millis() - stateTime < 2000) {
		return;
	}
	if (BLE.isPairing(peer)) {
		// Still in process of doing LESC secure pairing
    	Log.info("Still pairing...");
		return;
	}
	stateTime = millis();

    WriteReadBLE(setswStatus); //Turn shelly relay on

	Log.info("sent counter=%d", counter);
	counter++;
}


int WriteReadBLE(const char * msg)
{
    int ret = 1; //Assume success

    //*****************************
    //If we've disconnected get out
    //*****************************
    if (!peer.connected())
    {
        return 0;
    }

    int len = TxBLEpacket(peerTxCharacteristic, peerRxCharacteristic, peerRxTxCharacteristic, msg);

    Log.info("RX LengthA %d",len);

    if (len < 1)
    {
        ret = 0; //Problem
        Log.info("TX Problem");
    }

    return ret;
}
//******************************
//Write a message out a BLE link
//******************************
int TxBLEpacket(BleCharacteristic &pTx, BleCharacteristic &pRx, BleCharacteristic &pRxTx, const char * txbuf)
{
    int len = strlen(txbuf);
    
    //*******************************
    //Set correct endianess of length
    //*******************************
    len = change_endian( (uint8_t *) &len);
    //*******************************************
    //Send the length of the packet we're sending
    //*******************************************
    pTx.setValue((uint8_t *)&len,4);
    //***********************
    //Send the actual message
    //***********************
    pRx.setValue((uint8_t *)txbuf,strlen(txbuf));
    //**********************
    //Get length of response
    //**********************
    pRxTx.getValue((uint8_t *)&len,4);
    //****************************************
    //Set correct endianess of returned length
    //****************************************
    len = change_endian( (uint8_t *) &len);
    //**********************************************************************
    //Return number of bytes that device returned to us and needs to be read
    //**********************************************************************
    return len;
}
//********************************
//Change endianess of passed value
//********************************
int change_endian(const uint8_t * v)
{
    uint8_t res[4];
    //Swap bytes
    res[0] = v[3];
    res[1] = v[2];
    res[2] = v[1];
    res[3] = v[0];
    return *(int *)res;
}

Just to update: I added

    if (BLE.isPaired(peer))
    {
    	Log.info("PAIRED!");
    }

in the run code section, and the Argon does think it's paired, however the Shelly pairing table is still empty and the reads are rejected by the Shelly.

Also, I tried all the various BLE.setPairingIoCaps and they all worked the same.

Here's from the Shelly documentation in case:

Shelly BLE Info

My question is this can you share some tips how i connect

Bluetooth Low Energy | Shelly Technical Documentation Is this link open in your side

Yes the Argon connects properly to the Shelly if that’s what you’re asking. Refer to the serial trace above.

Look at the code I posted above. If you’re trying to connect to a Shelly you need to get the MAC address for the Bluetooth side (not the MAC address for the WiFi).

I haven't tested LESC just works pairing since it was implemented. I'll try to run the test program again and see if there's anything missing from your code. Hopefully that will find something since the Shelly smart locks don't seem to be listed on the Shelly United States web site yet.

Thanks Rick. It’s a Shelly mini Gen 3 relay I’m using. Based on what I see during the pairing, it looks like there’s a final step missing in the negotiations from the Argon. When my iPhone pairs with the Shelly, a screen pops up stating “Shelly_xxxxx mini wants to pair with you” with a pair/cancel box. Maybe it’s this final packet that’s missing.

Thanks for looking into this.

I think I know what the problem is. The Shelly BLE devices do use LESC "just works" pairing, but then use BLE bonding for the persistent connection according to this API doc. The problem is that Particle Gen 3 and Gen 4 devices don't support persistent bonding, so that's why the negotiation process stops before they can actually communicate. I don't believe there will be a way to make the connection from an Argon (or any Particle device).

Thanks Rick. That makes sense. I’ve submitted a request to Shelly for a method to manually add a bonded MAC address to bypass the pairing process all together. Don’t have high hopes they’ll do it though.

I’m guessing Particle doesn’t have plans to implement bonded pairing?

For me this simply means I stop upgrading my Shelly devices since they work perfectly fine without pairing (albeit with reduced BT security).

Again, thanks for looking into it.

Btw I did notice that support for Bluetooth packet fragmentation has been added and it works. Thank you to the Manager that approved adding it and the person/people that implemented it.

To that Manager : please add support for bonded LESC Bt pairing soon :folded_hands: