This discussion has been locked.
You can no longer post new replies to this discussion. If you have a question you can start a new discussion

Reliability of the BLE UART example

In my application I need a reliable BLE link between peripheral (in this case based on a nRF52840) and a PC / Laptop. I need to stream data at a rate of 200kbps with a very high reliability. i.e loss of more than 0.0001% of the data is unacceptable.

My data is divided into 236 byte packets. 4 bytes sequence number, counter, 228 bytes data, and 4 bytes CRC.

The CRC is used to check consistency of the data, and the sequence number to check for missing data.

I've read other forum posts and it seems that the BLE UART example is the most applicable to my case.

I downloaded nRF5_SDK_17.1.0_ddde560 and Segger v5.68 (Nordic edition).

I have a nRF52840-DK (PCA10056)

The setup looks roughly like this:

PC->UART->nRF52840-DK->BLE->PC

The RF conditions are ideal. The DK and the PC are about 30cm apart, and there are not many other BT devices in the vicinity

I made the following changes to the UART example (nRF5_SDK_17.1.0_ddde560\examples\ble_peripheral\ble_app_uart\main.c):

diff --git a/ble_peripheral/ble_app_uart/main.c b/ble_peripheral/ble_app_uart/main.c
index 36d1a82..54ddf49 100644
--- a/ble_peripheral/ble_app_uart/main.c
+++ b/ble_peripheral/ble_app_uart/main.c
@@ -91,8 +91,8 @@

 #define APP_ADV_DURATION                18000                                       /**< The advertising duration (180 seconds) in units of 10 milliseconds. */

-#define MIN_CONN_INTERVAL               MSEC_TO_UNITS(20, UNIT_1_25_MS)             /**< Minimum acceptable connection interval (20 ms), Connection interval uses 1.25 ms units. */
-#define MAX_CONN_INTERVAL               MSEC_TO_UNITS(75, UNIT_1_25_MS)             /**< Maximum acceptable connection interval (75 ms), Connection interval uses 1.25 ms units. */
+#define MIN_CONN_INTERVAL               MSEC_TO_UNITS(8, UNIT_1_25_MS)             /**< Minimum acceptable connection interval (20 ms), Connection interval uses 1.25 ms units. */
+#define MAX_CONN_INTERVAL               MSEC_TO_UNITS(8, UNIT_1_25_MS)             /**< Maximum acceptable connection interval (75 ms), Connection interval uses 1.25 ms units. */
 #define SLAVE_LATENCY                   0                                           /**< Slave latency. */
 #define CONN_SUP_TIMEOUT                MSEC_TO_UNITS(4000, UNIT_10_MS)             /**< Connection supervisory timeout (4 seconds), Supervision Timeout uses 10 ms units. */
 #define FIRST_CONN_PARAMS_UPDATE_DELAY  APP_TIMER_TICKS(5000)                       /**< Time from initiating event (connect or start of notification) to first time sd_ble_gap_conn_param_update is called (5 seconds). */
@@ -583,10 +583,10 @@ static void uart_init(void)
         .tx_pin_no    = TX_PIN_NUMBER,
         .rts_pin_no   = RTS_PIN_NUMBER,
         .cts_pin_no   = CTS_PIN_NUMBER,
-        .flow_control = APP_UART_FLOW_CONTROL_DISABLED,
+        .flow_control = APP_UART_FLOW_CONTROL_ENABLED,
         .use_parity   = false,
 #if defined (UART_PRESENT)
-        .baud_rate    = NRF_UART_BAUDRATE_115200
+        .baud_rate    = NRF_UART_BAUDRATE_230400
 #else
         .baud_rate    = NRF_UARTE_BAUDRATE_115200
 #endif

I wrote a Python script to generate test traffic in the above format and send it on a COM port:

import serial
import threading
import sys
import time
import binascii
import os


class UartSender:
    def __init__(self, com_port, baud=115200, rtscts=True, test_size=228, test_string='data', inter_packet_gap_ms=1000):
        self.uart = serial.Serial(com_port, baud, rtscts=rtscts) #115200 or 460800, 921600
        self.uart_sending_thread = None
        self.running = False
        self.packet_counter = 0 #Receiver can keep track of packet counter
        self.data_array = bytearray()
        self.ipg = inter_packet_gap_ms / 1000.0 #Python is likely inaccurate below 1000ms

        #contruct test data
        data = bytearray(test_string, 'utf-8')
        for i in range(0, int(test_size/len(data))):
            self.data_array.extend(data)
        print(len(self.data_array))

    @staticmethod
    def still_running_checker(caller):
        return caller.running

    @staticmethod
    def thread_send(uart, still_running_checker, caller):
        while still_running_checker(caller):
            send_array = bytearray(caller.packet_counter.to_bytes(4, byteorder = 'little'))
            send_array.extend(caller.data_array)
            send_array.extend(bytearray(binascii.crc32(send_array).to_bytes(4, byteorder='little')))
            send_array.extend(bytearray('\n', 'utf-8'))
            uart.write(send_array) #Should block until all bytes are sent
            caller.packet_counter += 1
            time.sleep(caller.ipg)

    def start_sending(self):
        self.uart_sending_thread = threading.Thread(target=self.thread_send, args=(self.uart, self.still_running_checker,
                                                                                   self))
        self.running = True
        self.uart_sending_thread.start()

    def stop_sending(self):
        if self.running:
            self.running = False
            self.uart_sending_thread.join(2)

    def disconnect(self):
        self.stop_sending()
        self.uart.close()

def main(args):
    uart = UartSender("COM45", baud=230400, rtscts=False, inter_packet_gap_ms=8)
    uart.start_sending()
    if __name__ == '__main__':
        try:
            time.sleep(3*60*60)
        except KeyboardInterrupt:
            print("exiting on KeyboardInterrupt")
            uart.disconnect()
            try:
                sys.exit(0)
            except SystemExit:
                os._exit(0)


    uart.stop_sending()

if __name__=="__main__":
    main(sys.argv)

This then goes to the nRF and the nRF sends it over BLE back to the PC

On the PC there is another Python script that uses the Bleak library to listen on the TX Characteristic (UUID: 6E400003-B5A3-F393-E0A9-E50E24DCCA9E) for notifications. Once a notification is received, the data is parsed to extract the sequence number and CRC. The CRC is also calculated again over the data. See function notification_received in the following code:

import asyncio
import binascii
from bleak import BleakClient
from bleak import BleakScanner
import time
import threading

detected_peripherals = {}

RX_CHAR = '6E400002-B5A3-F393-E0A9-E50E24DCCA9E'
TX_CHAR = '6E400003-B5A3-F393-E0A9-E50E24DCCA9E'

class BLEReceiver:
    def __init__(self, logger):
        self.client = None
        self.loop = asyncio.get_event_loop()
        self.logger = logger
        self.running = False
        self.ble_notif_thread = None
        self.prev_count = 0
        self.lost_msgs = 0


    def connect(self, index):
        if self.client is None:
            try:
                key_list = list(detected_peripherals)
                device = detected_peripherals[key_list[index]]
                self.logger.log(20, "creating bleak client\n")
                self.client = BleakClient(device)
                self.logger.log(20, "connecting to device {}\n".format(str(device)))
                self.loop.run_until_complete(connect(self.client))

                while not self.client.is_connected:
                    asyncio.sleep(0.1)
                self.client.set_disconnected_callback(self.on_disconnect)
                self.logger.log(20, "connected...!\n")

                self.ble_notif_thread = threading.Thread(target=self.thread_proc, args=(self.notification_received,
                                                                                        self.still_running_checker,
                                                                                        self))
                self.running = True
                self.prev_count = 0
                self.ble_notif_thread.start()
                return True
            except Exception as e:
                raise e
                #self.client = None
                #return False

    def parse_rx_data(self, bytes):
        offset = 0
        ret = {}
        crc = binascii.crc32(bytes[0:len(bytes)-5]) #Calculate the CRC, skip last 5 bytes as they are the CRC and '\n'
        offset, ret['CNT'] = extract_word_32(bytes, offset) #The count (sequence number)
        offset, ret['DATA'] = extract_data(bytes, 2, offset, offset+228)#extract data
        offset, ret['CRC'] = extract_word_32(bytes, offset)#Extract CRC

        return ret, crc == ret['CRC'] #Return data and CRC result

    def notification_received(self, sender, data):
        ret, crc_check = self.parse_rx_data(data)
        
        if ret['CNT'] > self.prev_count + 1: #Check for a missing sequence number (aka 'count')
            self.logger.log(40, 'Missing '+ str(self.prev_count) + '\n')
            if (self.prev_count + 1) % 100 == 0: #If a multiple of 100 is missing, reset the lost message count
                self.lost_msgs = 0
            self.lost_msgs += 1
        self.prev_count = ret['CNT']
        
        if not crc_check: #Log a CRC failure
            self.logger.log(40, 'CRC FAIL ' + str(ret['CNT']) + '\n')
            self.lost_msgs += 1
            
        if ret['CNT'] % 100 == 0: #On every 100th packet, print the loss percentage
            self.logger.log(40, 'Received '+ str(ret['CNT']) + ' messages. Loss percentage: ' + str(self.lost_msgs) + '%\n')
            self.lost_msgs = 0

    @staticmethod
    def still_running_checker(caller):
        return caller.running

    @staticmethod
    def thread_proc(notification_callback, still_running_checker, caller):
        try:
            while still_running_checker(caller):
                caller.loop.run_until_complete(enableNotification(caller.client, TX_CHAR, notification_callback))

                if still_running_checker(caller):
                    caller.loop.run_until_complete(asyncio.sleep(1))
                else:
                    caller.logger.log(40, "stopping notifications thread!\n")
                    caller.loop.run_until_complete(disableNotification(caller.client, TX_CHAR))
        except Exception as e:
            caller.logger.log(40, "notifications thread terminated!\n {}\n".format(e))

    def on_disconnect(self, arg):
        self.logger.log(40, "ble stack disconnected!\n")
        self.running = False
        self.client = None

    def disconnect(self):
        if self.running:
            self.logger.log(40, 'disable notification....!\n')
            self.running = False
            # if self.ble_notif_thread != None:
            self.ble_notif_thread.join(2)
            self.logger.log(40, '...notification disabled!\n')
            self.logger.log(40, 'disconnect initiated...!\n')
            self.loop.run_until_complete(disconnect(self.client))
            self.logger.log(40, '...disconnect completed!\n')
            self.client = None

The problem is that about 5-10% of the traffic is lost. Either it is a missing sequence number (i.e. data did not arrive in the first place) or a CRC failure which seems to be due to a couple of bytes missing.

How can I get a reliable data transfer from peripheral to PC?

Parents Reply Children
No Data
Related