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?