Dear Nordic Semiconductor Support Team,
My name is Iwo, and I’m developing a BLE central application using the nRF52840 DK with nRF Connect SDK v3.2.1.
The application works reliably when connecting to a single custom BLE peripheral (a smart lamp with service 0xFFE5 and characteristic 0xFFE9), but I am unable to achieve stable connections to two identical peripherals simultaneously or sequentially in a reliable cycle. I ultimately want to achieve a setup with 10 lamps, so failure at just 2 is hard to swallow.
Here is a summary of my current situation:
Working Single-Lamp Configuration
The configuration and code I'll attach below successfully connects, discovers the characteristic (FFE9 at handle 32), performs MTU exchange to 65, and sends 22-byte write-without-response commands to toggle the lamp.
The Problem with Two Lamps
I have two identical lamps with known public MAC addresses:
- Lamp 1: 57:60:07:A8:EA:EC
- Lamp 2: 40:B2:C4:2B:A1:A0
I have tried various approaches to connect to both, including:
- Concurrent connections using CONFIG_BT_MAX_CONN=2 + increased buffers (ACL/L2CAP/ATT)
- Sequential cycling (connect → write → disconnect → next lamp)
- Continuous scanning with auto-connect on filter match
- Skipping MTU exchange and discovery (hardcoded handle 32)
- Using the multi-link/multi-role example from https://github.com/NordicPlayground/nrf52-ble-multi-link-multi-role as reference
Typical behavior in logs:
- Scan starts
- Finds one lamp (randomly 1 or 2)
- Connects successfully ("Connected")
- MTU exchange often fails (err 14: BT_ATT_ERR_UNLIKELY) or succeeds to 65
- Discovery finds FFE9 at handle 32
- Writes fail with -12 (ENOMEM) or -128 (no ATT channel)
- Disconnects with reason 62 (connection timeout)
The lamp does not show the BT connection icon on its display (even when logs say "Connected"), but it does respond to writes when single-lamp code works (lamp flashes).
Questions / Help Needed
- Is there a known limitation in nRF Connect SDK v3.2.1 with the SoftDevice Controller when acting as BLE central to multiple identical custom peripherals?
- What is the correct/recommended way to increase buffers and enable reliable 2 concurrent central connections on nRF52840DK? (I tried the buffer configs above but still get ENOMEM/-12 on writes)
- Is MTU exchange mandatory for 22-byte writes? Some lamps seem to reject it (err 14). Is there a workaround to use default MTU reliably?
- Why does the connection appear established in logs but the peripheral (lamp) does not show the BT icon? Does the lamp require a specific GATT action (e.g., write to FFE9) to consider the connection "active"?
Working Single-Lamp Configuration:
# Enable BLE stack as central with GATT client
CONFIG_BT=y
CONFIG_BT_CENTRAL=y
CONFIG_BT_SMP=y
CONFIG_BT_GATT_CLIENT=y
# Enable scanning
CONFIG_BT_SCAN=y
CONFIG_BT_SCAN_FILTER_ENABLE=y
CONFIG_BT_SCAN_ADDRESS_CNT=1
# Enable GATT discovery manager
CONFIG_BT_GATT_DM=y
# Increase MTU and buffer sizes for 21-byte writes (default MTU 23 allows only ~20-byte payload)
CONFIG_BT_L2CAP_TX_MTU=65
CONFIG_BT_BUF_ACL_TX_SIZE=69
CONFIG_BT_BUF_ACL_RX_SIZE=69
# Logging and heap
CONFIG_LOG=y
CONFIG_HEAP_MEM_POOL_SIZE=2048
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/uuid.h>
#include <zephyr/bluetooth/gatt.h>
#include <bluetooth/gatt_dm.h>
#include <bluetooth/scan.h>
#include <zephyr/bluetooth/att.h>
#include <zephyr/device.h>
#include <zephyr/drivers/gpio.h>
LOG_MODULE_REGISTER(central_lamp, CONFIG_LOG_DEFAULT_LEVEL);
static struct bt_conn *lamp_conn;
static uint16_t cmd_handle;
static bool discovered = false;
static struct bt_uuid_16 service_uuid = BT_UUID_INIT_16(0xFFE5); // Lamp's service UUID from app screenshots
static struct bt_uuid_16 cmd_uuid = BT_UUID_INIT_16(0xFFE9);
static const uint8_t on_cmd[] = {0x3a, 0x26, 0xa3, 0x0d, 0x40, 0xb2, 0x01, 0x00, 0xff, 0x2d, 0x64, 0x19, 0xff, 0xff, 0xff, 0xff, 0x0a, 0x78, 0x07, 0x0d, 0x0a};
static const uint8_t off_cmd[] = {0x3a, 0x26, 0xa3, 0x0d, 0x40, 0xb2, 0x00, 0x00, 0xff, 0x2d, 0x64, 0x19, 0xff, 0xff, 0xff, 0xff, 0x0a, 0x77, 0x07, 0x0d, 0x0a};
static bt_addr_le_t target_addr = {
.type = BT_ADDR_LE_PUBLIC,
.a.val = {0xEC, 0xEA, 0xA8, 0x07, 0x60, 0x57} // Reversed byte order for 57:60:07:A8:EA:EC
};
/* LED setup using devicetree alias led0 (LED1 on nRF52840 DK) */
#define LED0_NODE DT_ALIAS(led0)
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);
/* Debug: Log all found devices */
static void device_found(const bt_addr_le_t *addr, int8_t rssi, uint8_t type,
struct net_buf_simple *ad)
{
char addr_str[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(addr, addr_str, sizeof(addr_str));
LOG_INF("Found device: %s (RSSI %d, type %u)", addr_str, rssi, type);
}
static void scan_filter_match(struct bt_scan_device_info *device_info,
struct bt_scan_filter_match *filter_match,
bool connectable)
{
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(device_info->recv_info->addr, addr, sizeof(addr));
LOG_INF("Filters matched. Address: %s connectable: %d", addr, connectable);
}
static void scan_connecting_error(struct bt_scan_device_info *device_info)
{
LOG_WRN("Connecting failed");
}
static void scan_connecting(struct bt_scan_device_info *device_info, struct bt_conn *conn)
{
lamp_conn = bt_conn_ref(conn);
}
BT_SCAN_CB_INIT(scan_cb, scan_filter_match, device_found, scan_connecting_error, scan_connecting);
static void start_scan(void)
{
int err;
struct bt_le_scan_param scan_param = {
.type = BT_LE_SCAN_TYPE_ACTIVE,
.options = BT_LE_SCAN_OPT_FILTER_DUPLICATE,
.interval = BT_GAP_SCAN_FAST_INTERVAL,
.window = BT_GAP_SCAN_FAST_WINDOW,
};
struct bt_scan_init_param scan_init = {
.connect_if_match = 1,
.scan_param = &scan_param,
.conn_param = BT_LE_CONN_PARAM_DEFAULT,
};
bt_scan_init(&scan_init);
bt_scan_cb_register(&scan_cb);
err = bt_scan_filter_add(BT_SCAN_FILTER_TYPE_ADDR, &target_addr);
if (err) {
LOG_ERR("Scanning filters cannot be set (err %d)", err);
return;
}
err = bt_scan_filter_enable(BT_SCAN_ADDR_FILTER, false);
if (err) {
LOG_ERR("Filters cannot be enabled (err %d)", err);
return;
}
err = bt_scan_start(BT_SCAN_TYPE_SCAN_ACTIVE);
if (err) {
LOG_ERR("Scanning failed to start (err %d)", err);
} else {
LOG_INF("Scan started");
}
}
static void discovery_complete(struct bt_gatt_dm *dm, void *ctx)
{
LOG_INF("Service discovery completed");
const struct bt_gatt_dm_attr *attr = bt_gatt_dm_char_by_uuid(dm, &cmd_uuid.uuid);
if (attr) {
struct bt_gatt_chrc *chrc = bt_gatt_dm_attr_chrc_val(attr);
if (chrc) {
cmd_handle = chrc->value_handle;
LOG_INF("Found FFE9 characteristic, handle: %u", cmd_handle);
discovered = true;
} else {
LOG_WRN("FFE9 value not found");
}
} else {
LOG_WRN("FFE9 characteristic not found");
}
bt_gatt_dm_data_release(dm);
}
static void discovery_service_not_found(struct bt_conn *conn, void *ctx)
{
LOG_ERR("Service not found");
}
static void discovery_error(struct bt_conn *conn, int err, void *ctx)
{
LOG_ERR("Error discovering (err %d)", err);
}
static const struct bt_gatt_dm_cb discovery_cb = {
.completed = discovery_complete,
.service_not_found = discovery_service_not_found,
.error_found = discovery_error,
};
static void exchange_func(struct bt_conn *conn, uint8_t att_err, struct bt_gatt_exchange_params *params)
{
if (att_err) {
LOG_ERR("MTU exchange failed (err %u)", att_err);
return;
}
uint16_t mtu = bt_gatt_get_mtu(conn);
LOG_INF("MTU exchange done, new MTU: %u", mtu);
int err = bt_gatt_dm_start(conn, &service_uuid.uuid, &discovery_cb, NULL);
if (err) {
LOG_ERR("bt_gatt_dm_start failed (err %d)", err);
}
}
static void connected(struct bt_conn *conn, uint8_t conn_err)
{
if (conn_err) {
LOG_ERR("Connection failed (err %u)", conn_err);
if (lamp_conn) {
bt_conn_unref(lamp_conn);
lamp_conn = NULL;
}
start_scan();
return;
}
LOG_INF("Connected");
static struct bt_gatt_exchange_params exchange_params;
exchange_params.func = exchange_func;
int err = bt_gatt_exchange_mtu(conn, &exchange_params);
if (err) {
LOG_ERR("MTU exchange failed (err %d)", err);
exchange_func(conn, 0, NULL); // Fallback
}
}
static void disconnected(struct bt_conn *conn, uint8_t reason)
{
LOG_INF("Disconnected (reason %u)", reason);
discovered = false;
if (lamp_conn) {
bt_conn_unref(lamp_conn);
lamp_conn = NULL;
}
start_scan();
}
BT_CONN_CB_DEFINE(conn_callbacks) = {
.connected = connected,
.disconnected = disconnected,
};
int main(void)
{
int err;
/* Initialize LED */
if (!gpio_is_ready_dt(&led)) {
LOG_ERR("LED device not ready");
return -1;
}
err = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
if (err < 0) {
LOG_ERR("LED configure failed (err %d)", err);
return -1;
}
LOG_INF("LED initialized (should start blinking soon)");
/* Initialize Bluetooth */
LOG_INF("Initializing Bluetooth...");
err = bt_enable(NULL);
if (err) {
LOG_ERR("Bluetooth init failed (err %d)", err);
return -1;
}
LOG_INF("Bluetooth initialized");
start_scan();
uint32_t blink_counter = 0;
while (1) {
/* Blink LED every 500 ms to confirm code is running */
gpio_pin_toggle_dt(&led);
blink_counter++;
if (blink_counter % 10 == 0) { // Log every 5 seconds
LOG_INF("Alive - LED toggled %u times", blink_counter);
}
/* BLE logic */
if (discovered && lamp_conn) {
err = bt_gatt_write_without_response(lamp_conn, cmd_handle, on_cmd, sizeof(on_cmd), false);
if (err) {
LOG_ERR("Write ON failed (err %d)", err);
} else {
LOG_INF("Sent ON command successfully");
}
k_sleep(K_SECONDS(1));
err = bt_gatt_write_without_response(lamp_conn, cmd_handle, off_cmd, sizeof(off_cmd), false);
if (err) {
LOG_ERR("Write OFF failed (err %d)", err);
} else {
LOG_INF("Sent OFF command successfully");
}
k_sleep(K_SECONDS(1));
} else {
k_sleep(K_MSEC(100));
}
}
return 0;
}
I would greatly appreciate any guidance, sample project, or Kconfig recommendations for stable multi-central operation with custom GATT services on nRF52840DK.Thank you very much for your time and support.
I’m happy to provide full project files, complete logs, or any additional information. Best regards,
Iwo
