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));
}