Automate nRF52840 Dongle running pc-nrfconnect-ble

Hello,

 I'm very unexperienced with low-level programming, so I apologize is some of my questions make little sense.

  We have this board that sends telemetry data using a nRF52840 to a Window 10 PC.  Looking for a quick and dirty way to scan for the board, connect to it, and record the RSSI value I found out the nRF52840 dongle.  Running Bluetooth Low Energy app that comes with nRF Connect for Desktop works like a charm, but now I'm looking for a way to replicate this thru LabView. I took a gander at the serial port communications between the app and the dongle and it's mostly HEX commands, so I tried looking around github for a piece of code or example that I could use to duplicate these commands, but I can't figure out where or what to look for..

 I'm hoping someone can point me to a piece of code/example or document that explains how to send and read the commands to a nRF52840 dongle programmed with pc-nrfconnect-ble.

Thanks.

  • Hello,

    Attached below is a python script for the pc-ble-driver-py I quickly put together which will connect to any BLE device named 'TARGET_DEV_NAME' and start logging the RSSI. You can use it with the same dongle and FW you use with the Bluetooth low energy app.

    #
    # Copyright (c) 2016 Nordic Semiconductor ASA
    # All rights reserved.
    #
    # Redistribution and use in source and binary forms, with or without modification,
    # are permitted provided that the following conditions are met:
    #
    #   1. Redistributions of source code must retain the above copyright notice, this
    #   list of conditions and the following disclaimer.
    #
    #   2. Redistributions in binary form must reproduce the above copyright notice, this
    #   list of conditions and the following disclaimer in the documentation and/or
    #   other materials provided with the distribution.
    #
    #   3. Neither the name of Nordic Semiconductor ASA nor the names of other
    #   contributors to this software may be used to endorse or promote products
    #   derived from this software without specific prior written permission.
    #
    #   4. This software must only be used in or with a processor manufactured by Nordic
    #   Semiconductor ASA, or in or with a processor manufactured by a third party that
    #   is used in combination with a processor manufactured by Nordic Semiconductor.
    #
    #   5. Any software provided in binary or object form under this license must not be
    #   reverse engineered, decompiled, modified and/or disassembled.
    #
    # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
    # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
    # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
    # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
    # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
    # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
    # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
    # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    #
    
    import sys
    import time
    import logging
    from queue import Queue, Empty
    from pc_ble_driver_py.observers import *
    
    TARGET_DEV_NAME = "Nordic_UART"
    CONNECTIONS = 1
    CFG_TAG = 1
    
    global config, BLEDriver, BLEAdvData, BLEEvtID, BLEAdapter, BLEEnableParams, BLEGapTimeoutSrc, BLEUUID, BLEUUIDBase, BLEConfigCommon, BLEConfig, BLEConfigConnGatt, BLEGapScanParams, BLEConfigConnGap, BLEGapConnParams
    from pc_ble_driver_py import config
    config.__conn_ic_id__ = "NRF52"
    # noinspection PyUnresolvedReferences
    from pc_ble_driver_py.ble_driver import (
            BLEDriver,
        BLEAdvData,
        BLEEvtID,
        BLEEnableParams,
        BLEGapTimeoutSrc,
        BLEUUID,
        BLEUUIDBase,
        BLEGapScanParams,
        BLEGapConnParams,
        BLEConfigCommon,
        BLEConfig,
        BLEConfigConnGatt,
        BLEConfigConnGap,
    )
    
    
    def init(conn_ic_id):
        # noinspection PyGlobalUndefined
        global config, BLEDriver, BLEAdvData, BLEEvtID, BLEAdapter, BLEEnableParams, BLEGapTimeoutSrc, BLEUUID, BLEUUIDBase, BLEConfigCommon, BLEConfig, BLEConfigConnGatt, BLEGapScanParams, BLEConfigConnGap, BLEGapConnParams
        from pc_ble_driver_py import config
    
        config.__conn_ic_id__ = conn_ic_id
        # noinspection PyUnresolvedReferences
        from pc_ble_driver_py.ble_driver import (
            BLEDriver,
            BLEAdvData,
            BLEEvtID,
            BLEEnableParams,
            BLEGapTimeoutSrc,
            BLEUUID,
            BLEUUIDBase,
            BLEGapScanParams,
            BLEGapConnParams,
            BLEConfigCommon,
            BLEConfig,
            BLEConfigConnGatt,
            BLEConfigConnGap,
        )
    
        # noinspection PyUnresolvedReferences
        from pc_ble_driver_py.ble_adapter import BLEAdapter
    
        global nrf_sd_ble_api_ver
        nrf_sd_ble_api_ver = config.sd_api_ver_get()
    
    
    class NusCollector(BLEDriverObserver, BLEAdapterObserver):
        
        CUS_BASE_UUID = BLEUUIDBase([
                0x6e, 0x40, 0x00, 0x00, 0xb5, 0xa3, 0xf3, 0x93, 0xe0, 0xa9, 0xe5, 0x0e, 0x24, 0xdc, 0xca, 0x9e
            ])
        CUS_WR_UUID = BLEUUID(0x0002, CUS_BASE_UUID)
        CUS_RD_UUID = BLEUUID(0x0003, CUS_BASE_UUID)
        def __init__(self, adapter):
            super(NusCollector, self).__init__()
            self.first = 1
            self.adapter = adapter
            self.conn_q = Queue()
            self.adapter.observer_register(self)
            self.adapter.driver.observer_register(self)
            self.adapter.default_mtu = 247
            self.nus_base = BLEUUIDBase([
                0x6e, 0x40, 0x00, 0x00, 0xb5, 0xa3, 0xf3, 0x93, 0xe0, 0xa9, 0xe5, 0x0e, 0x24, 0xdc, 0xca, 0x9e
            ], 0x2)
            self.nus_rx = BLEUUID(0x0002, self.nus_base)
            self.nus_tx = BLEUUID(0x0003, self.nus_base)
    
        def open(self):
            self.adapter.driver.open()
            if config.__conn_ic_id__ == "NRF51":
                self.adapter.driver.ble_enable(
                    BLEEnableParams(
                        vs_uuid_count=1,
                        service_changed=0,
                        periph_conn_count=0,
                        central_conn_count=1,
                        central_sec_count=0,
                    )
                )
            elif config.__conn_ic_id__ == "NRF52":
                gatt_cfg = BLEConfigConnGatt()
                gatt_cfg.att_mtu = self.adapter.default_mtu
                gatt_cfg.tag = CFG_TAG
                self.adapter.driver.ble_cfg_set(BLEConfig.conn_gatt, gatt_cfg)
    
                conn_cfg = BLEConfigConnGap()
                conn_cfg.conn_count = 1
                conn_cfg.event_length = 320
                self.adapter.driver.ble_cfg_set(BLEConfig.conn_gap, conn_cfg)
    
                self.adapter.driver.ble_enable()
                self.adapter.driver.ble_vs_uuid_add(NusCollector.CUS_BASE_UUID)
    
    
    
        def close(self):
            self.adapter.driver.close()
    
        def connect_and_discover(self):
            scan_duration = 5
            scan_params = BLEGapScanParams(interval_ms=200, window_ms=150, timeout_s=scan_duration)
    
            self.adapter.driver.ble_gap_scan_start(scan_params=scan_params)
    
            try:
                new_conn = self.conn_q.get(timeout=scan_duration)
                self.adapter.service_discovery(new_conn)
                self.adapter.driver.ble_gap_rssi_start(new_conn, 0, 0)
                return new_conn
            except Empty:
                print("No device advertising with name {TARGET_DEV_NAME} found.")
                return None
    
        def on_gattc_evt_exchange_mtu_rsp(self, ble_driver, conn_handle, status, att_mtu):
            print("ATT MTU updated to {}".format(att_mtu))
        
        def on_gap_evt_data_length_update(
            self, ble_driver, conn_handle, data_length_params
        ):
            print("Max rx octets: {}".format(data_length_params.max_rx_octets))
            print("Max tx octets: {}".format(data_length_params.max_tx_octets))
            print("Max rx time: {}".format(data_length_params.max_rx_time_us))
            print("Max tx time: {}".format(data_length_params.max_tx_time_us))
    
        def on_gatts_evt_exchange_mtu_request(self, ble_driver, conn_handle, client_mtu):
            print("Client requesting to update ATT MTU to {} bytes".format(client_mtu))
        def on_gap_evt_connected(
            self, ble_driver, conn_handle, peer_addr, role, conn_params
        ):
            print("New connection: {}".format(conn_handle))
            self.conn_q.put(conn_handle)
    
        def on_gap_evt_disconnected(self, ble_driver, conn_handle, reason):
            print("Disconnected: {} {}".format(conn_handle, reason))
    
        def on_gap_evt_adv_report(
            self, ble_driver, conn_handle, peer_addr, rssi, adv_type, adv_data
        ):
            conn_params = BLEGapConnParams(min_conn_interval_ms=7.5, max_conn_interval_ms=7.5, conn_sup_timeout_ms=4000, slave_latency=0)
            
            if BLEAdvData.Types.complete_local_name in adv_data.records:
                dev_name_list = adv_data.records[BLEAdvData.Types.complete_local_name]
    
            elif BLEAdvData.Types.short_local_name in adv_data.records:
                dev_name_list = adv_data.records[BLEAdvData.Types.short_local_name]
    
            else:
                return
    
            dev_name = "".join(chr(e) for e in dev_name_list)
            address_string = "".join("{0:02X}".format(b) for b in peer_addr.addr)
            print(
                "Received advertisment report, address: 0x{}, device_name: {}".format(
                    address_string, dev_name
                )
            )
    
            if dev_name == TARGET_DEV_NAME:
                self.adapter.connect(peer_addr, conn_params = conn_params, tag=CFG_TAG)
    
        def on_notification(self, ble_adapter, conn_handle, uuid, data):
            if len(data) > 32:
                data = "({}...)".format(data[0:10])
            print("Connection: {}, {} = {}".format(conn_handle, uuid, data))
    
        def on_gap_evt_rssi_changed(self, ble_driver, conn_handle, rssi):
            print("RSSI: {}".format(rssi))
    
    
    def main(selected_serial_port):
        print("Serial port used: {}".format(selected_serial_port))
        driver = BLEDriver(
            serial_port=selected_serial_port, auto_flash=False, baud_rate=1000000, log_severity_level="info"
        )
    
        adapter = BLEAdapter(driver)
        collector = NusCollector(adapter)
        collector.open()
        conn = collector.connect_and_discover()
    
        while conn is not None:
            time.sleep(10)
        collector.adapter.disconnect(conn)
        collector.close()
    
    
    def item_choose(item_list):
        for i, it in enumerate(item_list):
            print("\t{} : {}".format(i, it))
        print(" ")
    
        while True:
            try:
                choice = int(input("Enter your choice: "))
                if (choice >= 0) and (choice < len(item_list)):
                    break
            except Exception:
                pass
            print("\tTry again...")
        return choice
    
    
    if __name__ == "__main__":
        logging.basicConfig(
            level="INFO",
           format="%(asctime)s [%(thread)d/%(threadName)s] %(message)s",
        )
        serial_port = None
        if len(sys.argv) < 2:
            print("Please specify connectivity IC identifier (NRF51, NRF52)")
            exit(1)
        init(sys.argv[1])
        if len(sys.argv) == 3:
            serial_port = sys.argv[2]
        else:
            descs = BLEDriver.enum_serial_ports()
            choices = ["{}: {}".format(d.port, d.serial_number) for d in descs]
            choice = item_choose(choices)
            serial_port = descs[choice].port
        main(serial_port)
        quit()
    

    Steps for testing the script:

    1. Install pc-ble-driver-py through PyPi: $ pip install --user pc-ble-driver-py

    2. Change TARGET_DEV_NAME in the script to match your device.

    3. Run script with python: $ python3 rssi_logger.py NRF52 <COM port for nrf52840 dongle>

    After the connection has been established you should start seeing the measured RSSIs from the connection:

    $ python3 rssi_logger.py NRF52 /dev/ttyACM0
    Serial port used: /dev/ttyACM0
    2022-04-11 15:54:27,049 [139762313049856/LogThread] Successfully opened /dev/ttyACM0. Baud rate: 1000000. Flow control: none. Parity: none.
    Received advertisment report, address: 0xC4A1E304973B, device_name: Nordic_UART
    New connection: 0
    Client requesting to update ATT MTU to 247 bytes
    Max rx octets: 251
    Max tx octets: 251
    Max rx time: 2120
    Max tx time: 2120
    RSSI: -39
    RSSI: -37
    RSSI: -37
    RSSI: -37
    RSSI: -40
    RSSI: -42
    RSSI: -38
    RSSI: -39
    RSSI: -39
    RSSI: -39
    

    I do not have much experience with LabView, but I hope you will be able to parse the log output from it.

    Best regards,

    Vidar

  • Thank you very much Vidar, unfortunately I wasn't able to take the time to learn Python (my programming experience is currently limited to Labview and VB 6.0 ;-). 

    At the end I took a different approach.  First I was able to open "pc-ble-driver" in VS2022 and build the "heart_rate_collector".  Then I modified the code to allow extended advertisements and to ouput the rssi.  Once I got the EXE where I wanted, I simply run it using System Exec.vi in Labview.

    Not the most elegant solution, but it's the quick and dirty way for someone looking for a very basic interface between the nRF52840 dongle and Labview.

    Quick side note, I also tried to write a .NET C# application using Windows UWP BluetoothLEAdvertisementWatcher and some cheapo USB Bluetooth adapters from Amazon, but even after enabling the AllowExtendedAdvertisements property, I wasn't able to scan extended advertisements, only the BLE legacy ones.

  • Good to hear that you found a solution, thanks for the update.

    Quick side note, I also tried to write a .NET C# application using Windows UWP BluetoothLEAdvertisementWatcher and some cheapo USB Bluetooth adapters from Amazon, but even after enabling the AllowExtendedAdvertisements property, I wasn't able to scan extended advertisements, only the BLE legacy ones.

    Extended advertisements must also be supported by the Bluetooth adapter. Maybe that is why it did not work.

Related