Hello,
I am a student currently studying Bluetooth Low Energy (BLE).
I am working with two Adafruit nRF52832 boards (Adafruit Feather nRF52 Bluefruit LE - nRF52832, link to product: www.adafruit.com/.../3406), setting up a 1-to-1 connection as a server and client. Currently, I'm using the "write without response" method with a 10ms connection interval and a 5ms event length. I have also set the TxPower to 4 and the data rate to 2Mbps. (Arduino Environment)
In this setup, the client writes a packet to the server every 10ms and the packet size is 8 bytes (two float values), with first element containing the time of transmission and the second element containing packet count sent by the client.
I monitored the values written by the client every 10ms and observed what the server receives. I noticed a delay in the packets on the client side, likely due to retransmission. Given the 10ms connection interval, new packets should ideally be sent every 10ms. However, the packet numbers at t+10ms and t+20ms are sometimes identical, indicating a delay in transmission. (Each sample row in my picture represents a 10ms interval)
SERVER
On the server side, I observed that packets were occasionally duplicated or lost. The duplication happens when the server reads the same packet value at both t+10ms and t+20ms, likely due to retransmission. (Each sample row in my picture represents a 10ms interval)
CLIENT
Additionally, packet loss occurs when the expected consecutive packet numbers do not arrive at the server within the expected intervals. (Each sample row in my picture represents a 10ms interval)
CLIENT
Is packet retransmission, delay, or loss a common issue in BLE communication, or could there be something incorrect in my setup? I would greatly appreciate any advice on this matter.
My code is included below.
Thank you in advance for your guidance.
CLINET CODE
#include <bluefruit.h> #include <Adafruit_LittleFS.h> #include <InternalFileSystem.h> #include <Wire.h> #include <utility/imumaths.h> #define dataNum 2 // timestampe 포함 #define dataSize dataNum*4 // default: 20, maximum: 224 #define packetTime 8 // unit: 1.25ms #define SLAVE_ADDRESS 0x61 // I2C 슬레이브 주소 BLEClientService hrms(UUID16_SVC_HEART_RATE); BLEClientCharacteristic hrmc(UUID16_CHR_HEART_RATE_MEASUREMENT); float packetCount = 0; uint8_t buffer[dataNum * sizeof(float)]; void setup() { Serial.begin(115200); while ( !Serial ) delay(10); // for nrf52840 with native usb Serial.println("Bluefruit52 Peripheral Server"); Serial.println("--------------------------------------\n"); Serial.println("Initialize the Bluefruit nRF52 module"); // Change MTU size uint16_t requestedMtu = dataSize; Bluefruit.configCentralConn(requestedMtu+3, 4, BLE_GATTS_HVN_TX_QUEUE_SIZE_DEFAULT, BLE_GATTC_WRITE_CMD_TX_QUEUE_SIZE_DEFAULT); // BLE_GAP_EVENT_LENGTH_DEFAULT = 3 Bluefruit.begin(0, 1); // maximum connections as Peripheral = 0, Central = 1 Bluefruit.setTxPower(4); Bluefruit.setName("Bluefruit52 Central"); // Initialize HRM client hrms.begin(); hrmc.begin(); // Increase Blink rate to different from PrPh advertising mode Bluefruit.setConnLedInterval(250); // Callbacks for Central Bluefruit.Central.setConnInterval(packetTime, packetTime); Bluefruit.Central.setDisconnectCallback(disconnect_callback); Bluefruit.Central.setConnectCallback(connect_callback); /* Start Central Scanning * - Enable auto scan if disconnected * - Interval = 160 ms, window = 80 ms * - Don't use active scan * - Filter only accept HRM service * - Start(timeout) with timeout = 0 will scan forever (until connected) */ Bluefruit.Scanner.setRxCallback(scan_callback); Bluefruit.Scanner.restartOnDisconnect(true); Bluefruit.Scanner.setInterval(160, 80); // in unit of 0.625 ms Bluefruit.Scanner.filterUuid(hrms.uuid); Bluefruit.Scanner.useActiveScan(false); Bluefruit.Scanner.start(0); // // 0 = Don't stop scanning after n seconds /*************************I2C***************************/ Wire.setPins(25, 26); // SCL = 25, SDA = 26 핀 설정 Wire.begin(SLAVE_ADDRESS); // 슬레이브 모드로 I2C 초기화 Wire.onRequest(requestEvent); // 마스터의 읽기 요청이 들어올 때 호출될 콜백 설정 Wire.setClock(400000); // Set I2C speed 나중에 필요하면 사용 } /**************************************************************************/ void readTimedata(float (&timeData)[dataNum]) { sensors_event_t accelerometerData; // 선언된 변수 이름 수정 timeData[0] = millis(); // timestamp를 imuData의 첫 번째 요소에 저장 packetCount++; memcpy(buffer, timeData, sizeof(buffer)); timeData[1] = packetCount; hrmc.write((uint8_t*)timeData, sizeof(timeData)); } /**************************************************************************/ void loop() { float timeData[dataNum]; if (Bluefruit.connected()) { readTimedata(timeData); // Serial.println(imuData[1]); } } /**********************************callback*********************************/ /** * Callback invoked when scanner pick up an advertising data * @param report Structural advertising data */ void scan_callback(ble_gap_evt_adv_report_t* report) { // Since we configure the scanner with filterUuid() // Scan callback only invoked for device with hrm service advertised // Connect to device with HRM service in advertising Bluefruit.Central.connect(report); } /** * Callback invoked when an connection is established * @param conn_handle */ void connect_callback(uint16_t conn_handle) { Serial.println("Connected"); Serial.print("Discovering Service ... "); // If HRM is not found, disconnect and return if ( !hrms.discover(conn_handle) ) { Serial.println("Found NONE"); // disconnect since we couldn't find HRM service Bluefruit.disconnect(conn_handle); return; } // Once HRM service is found, we continue to discover its characteristic Serial.println("Found it"); Serial.print("Discovering characteristic ... "); if ( !hrmc.discover() ) { // chr is mandatory, if it is not found (valid), then disconnect Serial.println("not found !!!"); Serial.println("characteristic is mandatory but not found"); Bluefruit.disconnect(conn_handle); return; } Serial.println("Found it"); // Get the reference to current connection BLEConnection* connection = Bluefruit.Connection(conn_handle); // Request MTU exchange for rx_client if (connection) { connection->requestMtuExchange(dataSize + 3); // Request MTU of 247 bytes } } /** * Callback invoked when a connection is dropped * @param conn_handle * @param reason is a BLE_HCI_STATUS_CODE which can be found in ble_hci.h */ void disconnect_callback(uint16_t conn_handle, uint8_t reason) { (void) conn_handle; (void) reason; Serial.print("Disconnected, reason = 0x"); Serial.println(reason, HEX); } void requestEvent() { // 요청이 있을 때 I2C 마스터에게 데이터를 전송 Wire.write(buffer, sizeof(buffer)); }
SERVER CODE
#include <bluefruit.h> #include <Adafruit_LittleFS.h> #include <InternalFileSystem.h> #include <Wire.h> #include <Adafruit_Sensor.h> #include <Adafruit_BNO055.h> #include <utility/imumaths.h> #define dataNum 2 #define dataSize dataNum*4 // default: 20, maximum: 224 #define packetTime 8 // * 1.25ms #define SLAVE_ADDRESS 0x60 // I2C 슬레이브 주소 BLEService hrms = BLEService(UUID16_SVC_HEART_RATE); BLECharacteristic hrmc = BLECharacteristic(UUID16_CHR_HEART_RATE_MEASUREMENT); BLEDis bledis; // DIS (Device Information Service) helper class instance volatile bool updateVALUEFlag = false; // A flag to signal when to read IMU data float receivedData[dataNum]; float tempData[dataNum]; uint8_t buffer[dataNum * sizeof(float)]; void setup() { Serial.begin(115200); while ( !Serial ) delay(10); // for nrf52840 with native usb Serial.println("Bluefruit52 Central client"); Serial.println("--------------------------------------\n"); // Initialize the Bluefruit module Serial.println("Initialize the Bluefruit nRF52 module"); uint16_t requestedMtu = dataSize; Bluefruit.configPrphConn(requestedMtu+3, 4, BLE_GATTS_HVN_TX_QUEUE_SIZE_DEFAULT, BLE_GATTC_WRITE_CMD_TX_QUEUE_SIZE_DEFAULT); // MTU set to 247 bytes, BLE_GAP_EVENT_LENGTH_DEFAULT = 3 Bluefruit.begin(); // maximum connections as Peripheral = 1, Central = 0 (default) Bluefruit.setTxPower(4); // Set transmission power // Set the connect/disconnect callback handlers Bluefruit.Periph.setConnInterval(packetTime, packetTime); Bluefruit.Periph.setConnectCallback(connect_callback); Bluefruit.Periph.setDisconnectCallback(disconnect_callback); // Configure and Start the Device Information Service Serial.println("Configuring the Device Information Service"); bledis.setManufacturer("Adafruit Industries"); bledis.setModel("Bluefruit Feather52"); bledis.begin(); // Setup the Heart Rate Monitor service using // BLEService and BLECharacteristic classes Serial.println("Configuring the Heart Rate Monitor Service"); setupHRM(); // Setup the advertising packet(s) Serial.println("Setting up the advertising payload(s)"); startAdv(); Serial.println("\nAdvertising"); /*************************I2C***************************/ Wire.setPins(25, 26); // SCL = 25, SDA = 26 핀 설정 Wire.begin(SLAVE_ADDRESS); // 슬레이브 모드로 I2C 초기화 Wire.onRequest(requestEvent); // 마스터의 읽기 요청이 들어올 때 호출될 콜백 설정 Wire.setClock(400000); // Set I2C speed 나중에 필요하면 사용 // Board-specific Serial initialization delay (없애도 될 것 같음) #if defined(__AVR_ATmega32U4__) || defined(__SAM3X8E__) while (!Serial) delay(10); #endif } void startAdv(void) { // Advertising packet Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); Bluefruit.Advertising.addTxPower(); // Include HRM Service UUID Bluefruit.Advertising.addService(hrms); // Include Name Bluefruit.Advertising.addName(); Bluefruit.Advertising.restartOnDisconnect(true); Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds } void setupHRM(void) { hrms.begin(); // set up callback for receiving measurement hrmc.setProperties(CHR_PROPS_WRITE_WO_RESP | CHR_PROPS_READ); // CHR_PROPS_WRITE hrmc.setPermission(SECMODE_OPEN, SECMODE_OPEN); // SECMODE_OPEN or SECMODE_NO_ACCESS hrmc.setFixedLen(dataSize); hrmc.begin(); hrmc.setWriteCallback(hrm_write_callback); } void connect_callback(uint16_t conn_handle) { // Get the reference to current connection BLEConnection* connection = Bluefruit.Connection(conn_handle); char central_name[32] = { 0 }; connection->getPeerName(central_name, sizeof(central_name)); // Request MTU exchange for rx_client if (connection) { connection->requestMtuExchange(dataSize + 3); // Request MTU of 247 bytes } uint16_t mtu; mtu = connection->getMtu(); Serial.print("Connected to "); Serial.print(central_name); Serial.print(" with mtu size: "); Serial.print(mtu); connection->requestPHY(BLE_GAP_PHY_2MBPS); } /** * Callback invoked when a connection is dropped * @param conn_handle connection where this event happens * @param reason is a BLE_HCI_STATUS_CODE which can be found in ble_hci.h */ void disconnect_callback(uint16_t conn_handle, uint8_t reason) { (void) conn_handle; (void) reason; Serial.print("Disconnected, reason = 0x"); Serial.println(reason, HEX); Serial.println("Advertising!"); } /**************************************************************************/ void loop() { if ( Bluefruit.connected() ) { updateIMUData(); } } void hrm_write_callback(uint16_t conn_handle, BLECharacteristic* chr, uint8_t* data, uint16_t len) { // 수신된 데이터가 float 배열의 크기인지 확인 if (len == sizeof(float) * dataNum) { // 수신된 데이터를 float 배열 receivedData로 복사 memcpy(receivedData, data, sizeof(receivedData)); // 배열의 크기만큼 복사 } else { Serial.println("[RX]: 데이터 크기가 다름."); } } void updateIMUData() { memcpy(buffer, receivedData, sizeof(buffer)); // 여기에 IMU 데이터를 읽고 receivedData에 저장하는 로직 추가 } void requestEvent() { // 요청이 있을 때 I2C 마스터에게 데이터를 전송 Wire.write(buffer, sizeof(buffer)); }