Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scanning for previously bonded device #93

Closed
tobobo opened this issue Jul 27, 2022 · 13 comments
Closed

Scanning for previously bonded device #93

tobobo opened this issue Jul 27, 2022 · 13 comments

Comments

@tobobo
Copy link

tobobo commented Jul 27, 2022

I'm trying to recreate a feature that I had previously implemented using the Bluedroid stack, but abandoned because of high memory usage. The process goes like this:

  1. Create BLE server on ESP32
  2. Connect and bond to ESP32 BLE device from smartphone (in this case, an iPhone)
  3. At a later time, use a BLE scan to determine if the smartphone is nearby.

This was working with Bluedroid, but I can't make it work with this NimBLE stack. I suspect it has something to do with resolving RPA addresses. Does anyone know if this is possible with the current state of ESP's NimBLE stack?

I wonder if it might be related to issues making a bonded connection when RPA is enabled. It seems like I can enable either RPA or bonding, but not both.

  • If I enable RPA using NimBLEDevice::setOwnAddrType(BLE_OWN_ADDR_RANDOM, true);, bonding doesn't seem to occur as expected, as my iPhone doesn't "remember" the ESP32 device, nor can the ESP32 see the iPhone at the specified address when scanning.
  • If I enable bonding with NimBLEDevice::setSecurityAuth(true, true, true);, the iPhone "remembers" the device, but its address doesn't show up as expected when scanning.
  • If I do both of the above, the iPhone fails to read any characteristics from the ESP32.

I've seen a number of threads about RPA issues, some specifically with iPhones, but I can't tell if my problems are related or if this has been resolved since 2020/2021 when most of the threads were posted.

@h2zero
Copy link
Owner

h2zero commented Jul 27, 2022

The issue may be that you are using a non-resolvable private address on the esp32. If the phone cannot resolve the address then bonding should fail since it would not be able to recognize the device when it's address changes.

Try using NimBLEDevice::setOwnAddrType(BLE_OWN_ADDR_RANDOM, false); and let me know.

@tobobo
Copy link
Author

tobobo commented Jul 27, 2022

Thank you! That does seem to have resulted in some progress. If I don't enable bonding with setSecurityAuth, I can scan for the address reported as the address for the phone and the address shows up in the scan as expected.

However, the behavior is still unexpected when enabling bonding. If I have both of these lines:

  NimBLEDevice::setOwnAddrType(BLE_OWN_ADDR_RANDOM, false);
  NimBLEDevice::setSecurityAuth(true, true, true);

...then the phone doesn't need to pair with the ESP32 every time, which seems good. However, the address reported is no longer an address that shows up when scanning.

So, it seems like I have to choose between either a prompt to re-pair every time on the phone, or having an address mismatch that makes the scan impossible. It's not a big deal if there is a prompt on the phone, but I'm worried that the scan might fail, as it doesn't seem like the phone is truly bonding with the ESP32. So, I wonder if it's possible to use NimBLEDevice::setOwnAddrType(BLE_OWN_ADDR_RANDOM, false); while also enabling bonding.

@h2zero
Copy link
Owner

h2zero commented Jul 28, 2022

When using a random address it means it will change occasionally. On the esp32, when scanning, it will report the random address seen in the advertisement packet and not the real address of the device due to the host based privacy used instead of controller based. This is the main cause of the issue threads you mentioned earlier, since the controller has no idea about the random address of the device or the bond status it will report what it sees to the host and the app. It's an unfortunate limitation of the esp32 BLE controller, which is a closed source binary blob.

When you say scanning, are you also scanning with the esp32? Could you describe your setup in more detail? In general, when random address's are used you need to use another way to detect specific devices, it sounds like you need the address for this in your application.

@tobobo
Copy link
Author

tobobo commented Jul 28, 2022

...since the controller has no idea about the random address of the device or the bond status it will report what it sees to the host and the app. It's an unfortunate limitation of the esp32 BLE controller, which is a closed source binary blob.

I'm not entirely convinced this is the case, as I was able to do this using the Bluedroid-based stack. Over a number of days/weeks, the address shown when scanning would match the address originally reported when connecting/pairing with the phone. This would suggest that RPA was working, and bonding seemed to be working as well, since I would not be prompted to re-pair every time.

That said, if you think there's another way to identify the phone consistently, and in particular differentiate it from other iPhones (I'm not sure if different iPhones would have the same manufacturer data), then that would solve my problem!

Here's what I'm testing with right now (with NimBLE):

Server creation:

  NimBLEDevice::init("Tuneshine");
  NimBLEDevice::deleteAllBonds();
  NimBLEDevice::setOwnAddrType(BLE_OWN_ADDR_RANDOM, false);
  // NimBLEDevice::setSecurityAuth(true, true, true);
  // NimBLEDevice::setSecurityInitKey(BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID);
  // NimBLEDevice::setSecurityRespKey(BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID);
  bleServer = NimBLEDevice::createServer();
  bleServer->setCallbacks(new ServerCallbacks(this));
  NimBLEAdvertising *bleAdvertising = NimBLEDevice::getAdvertising();

  tuneshineService = bleServer->createService(tuneshineServiceUuid);
  bleAdvertising->addServiceUUID(tuneshineService->getUUID());
  bleAdvertising->setScanResponse(true);

  // ...creation of characteristics for tuneshineService...

  tuneshineService->start();
  NimBLEDevice::startAdvertising();

Then later, when scanning:

  NimBLEScan *scan = NimBLEDevice::getScan();
  scan->setAdvertisedDeviceCallbacks(new AdvertisedDeviceCallbacks([this](const uint8_t deviceAddress[DEVICE_ADDRESS_BYTE_LENGTH], int rssi)
                                                                      { this->bleDevices->setDeviceRssi(deviceAddress, rssi); }));
  scan->start(5, false);

With the following as the implementation of AdvertisedDeviceCallbacks

AdvertisedDeviceCallbacks::AdvertisedDeviceCallbacks(ScanResultHandler paramScanResultHandler)
{
  scanResultHandler = paramScanResultHandler;
}

void AdvertisedDeviceCallbacks::onResult(NimBLEAdvertisedDevice *device)
{
  ESP_LOGI(TAG, "Found device: %s", device->getAddress().toString().c_str());
  ESP_LOGI(TAG, "manufacturer data: %s", device->getManufacturerData().c_str());
  // ESP_LOGI(TAG, "device data: %s", device->toString().c_str());
  ESP_LOG_BUFFER_HEXDUMP(TAG, device->getAddress().getNative(), 6, ESP_LOG_INFO);
  scanResultHandler(device->getAddress().getNative(), device->getRSSI());
}

I'm using react-native-ble-plx to connect to ESP32 from the iPhone by scanning for a device with a name matching "Tuneshine" as set during BleDevice::init()

The Bluedroid code is much less concise due to the complexity of the ESP32 Bluedroid APIs, but the gist is that on connection, I would call esp_ble_set_encryption(param->connect.remote_bda, ESP_BLE_SEC_ENCRYPT_MITM);, and I could scan using the value of param->connect.remote_bda, with the same address from remote_bda appearing consistently in param->scan_rst.bda when performing a scan.

@h2zero
Copy link
Owner

h2zero commented Jul 28, 2022

Thanks, I understand better now.
The NimBLE scan callback does not resolve the random address, unlike the bluedroid API. As I stated above, this is due to the host based privacy limitation, which does not seem to be an issue for bluedroid. This was implemented but later removed ( as you can here espressif/esp-nimble#10).

That said I believe what you're attempting may be possible with some lower level calls to the NimBLE privacy functions to resolve the address. When I get a bit of time I can look them up and post a possible solution with them for you.

@tobobo
Copy link
Author

tobobo commented Jul 28, 2022

Thank you so much, I really appreciate the detailed help! If you have a link to the documentation for these lower level functions I may do some digging as well.

@h2zero
Copy link
Owner

h2zero commented Jul 28, 2022

You're welcome.

The Espressif modifications are not actually documented anywhere (I had to find them manually). That said, from the issue link I posted above there are some clues from the code that was removed.

#if MYNEWT_VAL(BLE_HOST_BASED_PRIVACY)
        if (ble_host_rpa_enabled()) {
            /* Now RPA to be resolved here, since controller is unaware of the
             * address is RPA  */
            ble_rpa_replace_peer_params_with_rl(desc.addr.val,
                                                &desc.addr.type, NULL);
        }
#endif

That's the best place to begin searching.

@tobobo
Copy link
Author

tobobo commented Jul 28, 2022

Thanks. I'm a bit stuck as I can't seem to figure out how to include any of the functions in ble_hs_resolv.c in my code. Maybe I need to recreate the contents of ble_hs_resolv_rpa, but has its own calls to other private functions...

I'm using PlatformIO to build this project. Not sure if that is creating additional headaches for me when trying to access private functions in the espressif libraries.

@tobobo
Copy link
Author

tobobo commented Jul 28, 2022

Okay, good news! I was able to experiment with this a bit more today by directly modifying the files in my ~/.platformio/packages directory.

I was just able to get a successful scan for a bonded device by adding the above code block, but without the ble_host_rpa_enabled() check. This seems to match the way it's used in ble_hs_hci_evt_le_enh_conn_complete and elsewhere.

I was prompted to remove that check because I'm still not able to form a connection when I both have my address type set to BLE_OWN_ADDR_RANDOM and bonding enabled. When I don't set my address type to BLE_OWN_ADDR_RANDOM, I'm able to enable bonding with my phone, so that a peer record is saved. But, that also makes it so that ble_host_rpa_enabled() returns false. Maybe that's okay though—it seems fine to use ble_rpa_replace_peer_params_with_rl even when host rpa is disabled.

I'm going to keep running this scan to make sure it doesn't stop working over the next day or so. If this seems like a reasonable solution, I'll need to make sure I can keep using it, and making a manual modification to my .platformio directory seems like it's not the best way to solve it. Do you know what the easiest way would be for me to keep using my esp-nimble modifications?

Here's the complete body of the function I modified:

static int
ble_hs_hci_evt_le_adv_rpt(uint8_t subevent, const void *data, unsigned int len)
{
    const struct ble_hci_ev_le_subev_adv_rpt *ev = data;
    struct ble_gap_disc_desc desc = {0};
    const struct adv_report *rpt;
    int rc;
    int i;

    /* Validate the event is formatted correctly */
    rc = ble_hs_hci_evt_le_adv_rpt_first_pass(data, len);
    if (rc != 0)
    {
        return rc;
    }

    data += sizeof(*ev);

    desc.direct_addr = *BLE_ADDR_ANY;

    for (i = 0; i < ev->num_reports; i++)
    {
        rpt = data;

        data += sizeof(rpt) + rpt->data_len + 1;

        desc.event_type = rpt->type;
        desc.addr.type = rpt->addr_type;
        memcpy(desc.addr.val, rpt->addr, BLE_DEV_ADDR_LEN);

#if MYNEWT_VAL(BLE_HOST_BASED_PRIVACY)
        ble_rpa_replace_peer_params_with_rl(desc.addr.val,
                                            &desc.addr.type, NULL);
#endif

        desc.length_data = rpt->data_len;
        desc.data = rpt->data;
        desc.rssi = rpt->data[rpt->data_len];

        ble_gap_rx_adv_report(&desc);
    }

    return 0;
}

Thank you again for the ongoing help!

@h2zero
Copy link
Owner

h2zero commented Jul 29, 2022

Glad that works for you! It would be best if this could be used by the callback in the app, then the NimBLE code would not need to be altered. This function will not work in that manner though, but there may be another one that does.

@tobobo
Copy link
Author

tobobo commented Jul 29, 2022

That makes sense. I can't figure out how to do it without modifying NimBLE, though. ble_rpa_replace_peer_params_with_rl and all the functions it calls seem to be missing from NimBLE's public API, including the functions to get the required data from NVS and the data structure that contains the various kinds of addresses. Unless I recreated a whole lot of code from esp-nimble, including the data structures required such as ble_hs_resolv_entry, I'm not sure how it's possible to do this with the public API functions available, unless I'm missing something obvious...

@h2zero
Copy link
Owner

h2zero commented Jul 29, 2022

Unfortunately I have come to the same conclusion. Perhaps this is something that should be brought up upstream in the esp-nimble repo. For now I don't have much else to suggest and I would continue with what you have until a fix or other solution is in place.

@tobobo
Copy link
Author

tobobo commented Jul 29, 2022

Will do!

I was at least able to make the process a bit nicer by using extra_scripts to automatically apply the patch to esp-nimble when needed (based on these instructions: https://docs.platformio.org/en/latest/scripting/examples/override_package_files.html )

Hopefully this makes this a bit easier if someone else comes across this issue and there's still no fix available upstream!

In platformio.ini:

extra_scripts = pre:build/apply_patches.py

build/apply_patches.py

from os.path import join, isfile

Import("env")

FRAMEWORK_DIR = env.PioPlatform().get_package_dir("framework-espidf")
patchflag_path = join(FRAMEWORK_DIR, ".patching-done-v1")

# patch file only if we didn't do it before
if not isfile(patchflag_path):
    original_file = join(FRAMEWORK_DIR, "components", "bt", "host", "nimble", "nimble", "nimble", "host", "src", "ble_hs_hci_evt.c")
    patch_file = join("build", "patches", "esp-idf-nimble-scan-rpa-resolution.patch")

    assert isfile(original_file) and isfile(patch_file)

    env.Execute("patch %s %s" % (original_file, patch_file))

    def _touch(path):
        with open(path, "w") as fp:
            fp.write("")

    env.Execute(lambda *args, **kwargs: _touch(patchflag_path))

build/patches/esp-idf-nimble-scan-rpa-resolution.patch

494a495,497
> #if MYNEWT_VAL(BLE_HOST_BASED_PRIVACY)
>         ble_rpa_replace_peer_params_with_rl(desc.addr.val, &desc.addr.type, NULL);
> #endif

Thank you again so much for the help.

@tobobo tobobo closed this as completed Jul 29, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants