Product: nRF54LM20B custom hardware
SDK: nRF Connect SDK v3.4.0
RTOS: Zephyr
Component: Bluetooth LE / SMP pairing / Fixed passkey / Bonding
1. Issue Description
We are developing a BLE peripheral application on a custom nRF54LM20B board using nRF Connect SDK v3.4.0.
We are experiencing inconsistent BLE pairing behavior across different mobile phones.
On some phones, the pairing process works correctly: the peripheral displays a six-digit passkey, the phone presents a passkey-entry prompt, and entering the passkey successfully completes pairing and establishes the connection.
However, on certain other phones, the pairing process does not complete. The phone displays a generated passkey or pairing-related code, but the expected passkey-entry prompt does not appear. The connection subsequently disconnects without completing pairing.
The issue appears to depend on the mobile phone or its Bluetooth implementation, because the same firmware works with some phones but fails with others.
2. Expected Behavior
-
A new mobile phone initiates a BLE connection to the peripheral.
-
The peripheral initiates the appropriate pairing procedure.
-
The phone displays a passkey-entry prompt when passkey entry is the negotiated authentication method.
-
The user enters the passkey displayed by the peripheral.
-
Pairing, encryption, and bonding complete successfully.
-
Previously bonded phones reconnect using their stored keys without requiring passkey entry again.
-
New phones can pair and bond without affecting existing bonded devices, subject to the configured bond limit.
3. Actual Behavior
Working phones:
-
The passkey-entry prompt appears.
-
Entering the displayed passkey completes pairing.
-
The BLE connection proceeds normally.
Affected phones:
-
A passkey or pairing code appears on the phone, but the expected passkey-entry prompt is not displayed.
-
Pairing does not complete.
-
The BLE connection disconnects after the pairing attempt.
We would like to determine whether this behavior is related to the negotiated LE Secure Connections pairing method, IO capabilities, fixed-passkey configuration, SMP authentication callbacks, or another Bluetooth stack compatibility issue.
4. Relevant Application Configuration
The application uses the following Bluetooth configuration:
-
CONFIG_BT=y -
CONFIG_BT_PERIPHERAL=y -
CONFIG_BT_SMP=y -
CONFIG_BT_BONDABLE=y -
CONFIG_BT_FIXED_PASSKEY=y -
CONFIG_BT_SMP_SC_PAIR_ONLY=y -
CONFIG_BT_SETTINGS=y -
CONFIG_SETTINGS=y -
CONFIG_BT_MAX_CONN=2 -
CONFIG_BT_MAX_PAIRED=2 -
CONFIG_BT_PRIVACY=n
The fixed passkey is configured using:
bt_passkey_set(ble_passkey);
The default passkey is 123456, and the application also supports storing and updating the passkey through the settings subsystem.
The application registers the following authentication callbacks:
-
passkey_display -
cancel
It also registers pairing-information callbacks:
-
pairing_complete -
pairing_failed
When a connection is established, the application requests security using:
bt_conn_set_security(conn, BT_SECURITY_L2);
The application then waits for link encryption and GATT notification subscription before sending application data.
5. Assistance Requested from Nordic
Could you please review the attached BLE implementation and configuration and advise on the following?
-
Is
CONFIG_BT_FIXED_PASSKEY=ycompatible withCONFIG_BT_SMP_SC_PAIR_ONLY=yin nRF Connect SDK v3.4.0 for the nRF54LM20B? -
Can requesting
BT_SECURITY_L2after connection establishment cause an interaction with the fixed-passkey or Secure Connections pairing procedure? What security level and API sequence do you recommend for a peripheral that requires authenticated, passkey-based pairing? -
Why might some mobile phones display a pairing code without presenting a passkey-entry prompt, while other phones correctly prompt the user to enter the peripheral's passkey?
-
Are there known interoperability issues involving particular Android/iOS versions, IO capability negotiation, LE Secure Connections, or fixed passkeys in this SDK version?
-
What additional Bluetooth host/controller logs, SMP traces, or configuration changes would you recommend to identify the exact reason for the failed pairing?
We would appreciate a recommended configuration and authentication-callback implementation that supports reliable passkey-based pairing across different mobile phones while retaining bonded-device reconnection.
6. Attachments and Reproduction Information
We will provide the following for investigation:
-
BLE GATT implementation (
ble_gatt.c) -
BLE interface header (
ble_gatt.h)
5228.prj.confble_gatt.h
/*
* Copyright (c) 2026 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
*
* Behaviour:
* - First connection from any phone → passkey shown, bond stored
* - Subsequent connections from same phone → auto-reconnect, no passkey
* - Multiple phones each bond once and reconnect silently thereafter
* - Each advertising window: connect → send packet → disconnect
* - New/unknown phone while bonded phones exist → pairs with passkey,
* adds its bond alongside the existing ones (up to CONFIG_BT_MAX_PAIRED)
*/
#include "ble_gatt.h"
#include "app_threads.h"
#include "user_app.h"
#include "user_led.h"
#include "main.h"
#include "user_util.h"
#include "fota.h"
#include "npm1300.h"
#include <zephyr/sys/util.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/gatt.h>
#include <zephyr/bluetooth/hci.h>
#include <zephyr/bluetooth/uuid.h>
#include <zephyr/settings/settings.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/logging/log.h>
#include <zephyr/device.h>
#include <zephyr/kernel.h>
#include <zephyr/kvss/zms.h>
LOG_MODULE_REGISTER(ble_gatt, LOG_LEVEL_INF);
/*
* Maximum time (ms) to wait for encryption + CCCD after connection.
* A bonded phone re-encrypts in < 1 s; a fresh pairing takes up to
* ~10 s depending on the phone. 15 s covers both cases.
*/
#define BLE_SECURITY_TIMEOUT_MS 15000U
/* ------------------------------------------------------------------ */
/* Private state */
/* ------------------------------------------------------------------ */
static char ble_rx_buffer[BLE_RX_BUFFER_SIZE];
static size_t ble_rx_len;
static uint32_t ble_passkey = BLE_DEFAULT_PASSKEY;
static struct bt_conn *auth_conn;
static bool passkey_requested;
static bool ble_timeout_active;
bool ble_connected;
static bool power_on_ble_sequence_active;
static struct k_work_delayable power_on_ble_retry_work;
static void power_on_ble_retry_handler(struct k_work *work);
static struct k_work_delayable ble_disconnect_work;
static struct k_work_delayable ble_adv_timeout_work;
static struct k_work_delayable ble_connected_work;
static ble_ready_callback_t ble_ready_callback;
static bool ble_sequence_active;
static uint8_t ble_argument;
static bool ble_available;
static K_SEM_DEFINE(ble_availability_sem, 0, 1);
static ble_data_received_cb_t user_data_received_callback;
static bool ccc_enabled;
static volatile bool ble_secure;
static struct bt_conn *current_conn;
/* Elapsed ms inside the ready-state poller */
static uint32_t security_wait_ms;
/*
* FOTA mode (see fota.c). While set: connectable advertising with the SMP
* UUID is kept running, and the short advertising / ready-state / post-data
* disconnect timers below are suspended so a slow image upload survives.
*/
static volatile bool fota_mode;
static struct k_work_delayable fota_adv_work;
static struct k_work_delayable deferred_seq_work;
static ble_ready_callback_t deferred_seq_cb;
static uint8_t deferred_seq_arg;
static bool deferred_seq_valid;
static void fota_adv_work_handler(struct k_work *work);
static void deferred_seq_work_handler(struct k_work *work);
/* ------------------------------------------------------------------ */
/* Advertising data */
/* ------------------------------------------------------------------ */
const struct bt_data ad[] = {
BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
BT_DATA_BYTES(BT_DATA_UUID16_ALL, BT_UUID_16_ENCODE(BT_UUID_BAS_VAL)),
};
const struct bt_data sd[] = {
BT_DATA(BT_DATA_NAME_COMPLETE,
CONFIG_BT_DEVICE_NAME,
sizeof(CONFIG_BT_DEVICE_NAME) - 1),
};
/* ------------------------------------------------------------------ */
/* GATT service */
/* ------------------------------------------------------------------ */
#define NEUTON_SERVICE_UUID \
BT_UUID_128_ENCODE(0xa5d4f351, 0x9d11, 0x419f, 0x9f1b, 0x3dcdf0a15f4d)
#define NEUTON_OUT_CHARACTERISTIC_UUID \
BT_UUID_128_ENCODE(0x516a51c4, 0xb1e1, 0x47fa, 0x8327, 0x8acaeb3399eb)
#define NEUTON_IN_CHARACTERISTIC_UUID \
BT_UUID_128_ENCODE(0x516a51c4, 0xb1e1, 0x47fa, 0x8327, 0x8acaeb3399ec)
#define BT_UUID_NEUTON_SERVICE BT_UUID_DECLARE_128(NEUTON_SERVICE_UUID)
#define BT_UUID_NEUTON_CHAR_OUT BT_UUID_DECLARE_128(NEUTON_OUT_CHARACTERISTIC_UUID)
#define BT_UUID_NEUTON_CHAR_IN BT_UUID_DECLARE_128(NEUTON_IN_CHARACTERISTIC_UUID)
static ssize_t on_data_received(struct bt_conn *conn,
const struct bt_gatt_attr *attr,
const void *buf,
uint16_t len,
uint16_t offset,
uint8_t flags);
static void on_cccd_changed(const struct bt_gatt_attr *attr, uint16_t value)
{
ARG_UNUSED(attr);
LOG_INF("CCCD changed: value=0x%04x connected=%d conn=%p",
value, ble_connected, current_conn);
ccc_enabled = (value == BT_GATT_CCC_NOTIFY);
LOG_INF("Notifications %s", ccc_enabled ? "ENABLED" : "DISABLED");
}
BT_GATT_SERVICE_DEFINE(neuton_gatt,
BT_GATT_PRIMARY_SERVICE(BT_UUID_NEUTON_SERVICE),
BT_GATT_CHARACTERISTIC(BT_UUID_NEUTON_CHAR_OUT,
BT_GATT_CHRC_NOTIFY,
BT_GATT_PERM_NONE,
NULL, NULL, NULL),
BT_GATT_CCC(on_cccd_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
BT_GATT_CHARACTERISTIC(BT_UUID_NEUTON_CHAR_IN,
BT_GATT_CHRC_WRITE_WITHOUT_RESP,
BT_GATT_PERM_WRITE,
NULL, on_data_received, NULL)
);
/* ------------------------------------------------------------------ */
/* Connection callbacks */
/* ------------------------------------------------------------------ */
static void connected(struct bt_conn *conn, uint8_t err)
{
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
if (err) {
LOG_ERR("Failed to connect to %s (%u)", addr, err);
return;
}
LOG_INF("BLE CONNECTED: %s", addr);
if (current_conn != NULL) {
bt_conn_unref(current_conn);
current_conn = NULL;
}
current_conn = bt_conn_ref(conn);
ble_connected = true;
ble_secure = false;
ccc_enabled = false;
security_wait_ms = 0;
ble_common_set_connected(true);
/*
* Request encryption.
*
* Bonded phone → stack restores the LTK and re-encrypts
* automatically; security_changed() fires with
* the restored level (usually L2 or L4).
*
* Unknown phone → stack starts SMP pairing; auth_passkey_display()
* fires and we log the passkey; phone prompts user.
*
* We use BT_SECURITY_L2 (encrypt, no forced MITM) so that both
* "Just Works" and passkey-entry phones succeed without err=2.
*/
int sec_err = bt_conn_set_security(conn, BT_SECURITY_L2);
if (sec_err && sec_err != -EALREADY) {
LOG_ERR("bt_conn_set_security L2 failed: %d", sec_err);
}
/* Stop advertising window timer — connection has arrived */
k_work_cancel_delayable(&ble_adv_timeout_work);
/*
* Start the ready-state poller.
* It checks every 100 ms for encryption + CCCD, with a
* BLE_SECURITY_TIMEOUT_MS ceiling.
*/
k_work_cancel_delayable(&ble_connected_work);
if (!fota_mode) {
k_work_schedule_for_queue(&ble_workq, &ble_connected_work,
K_MSEC(BLE_CONNECTED_DELAY_MS));
}
ble_available = true;
k_sem_give(&ble_availability_sem);
LOG_INF("Waiting for encryption + CCCD (timeout %u ms)",
BLE_SECURITY_TIMEOUT_MS);
}
static void disconnected(struct bt_conn *conn, uint8_t reason)
{
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
LOG_INF("Disconnected from %s (reason 0x%02x)", addr, reason);
ccc_enabled = false;
ble_connected = false;
ble_secure = false;
k_work_cancel_delayable(&ble_connected_work);
k_work_cancel_delayable(&ble_disconnect_work);
if (current_conn) {
bt_conn_unref(current_conn);
current_conn = NULL;
}
ble_common_set_connected(false);
ble_available = false;
/*
* Only clear the bond when authentication actually failed
* (wrong passkey, key mismatch after deliberate unpairing on
* the phone side). Normal disconnects keep the bond so the
* phone can reconnect silently next time.
*/
if (reason == BT_HCI_ERR_AUTH_FAIL ||
reason == BT_HCI_ERR_PIN_OR_KEY_MISSING) {
LOG_INF("Auth failure — removing bond for this peer");
bt_unpair(BT_ID_DEFAULT, bt_conn_get_dst(conn));
}
if (fota_mode) {
/* phone dropped (or is resetting us): keep the FOTA window advertising */
k_work_reschedule_for_queue(&ble_workq, &fota_adv_work, K_MSEC(300));
} else if (deferred_seq_valid) {
/* a BLE sequence preempted FOTA while a link was up: run it now */
k_work_reschedule_for_queue(&ble_workq, &deferred_seq_work, K_MSEC(200));
}
}
static void security_changed(struct bt_conn *conn,
bt_security_t level,
enum bt_security_err err)
{
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
if (err) {
/*
* Common causes:
* err=2 (AUTH_REQUIREMENT) — phone tried to reconnect with
* a bond we no longer have (e.g. after a firmware
* flash that wiped settings).
* err=5 (UNSPECIFIED) — phone rejected the pairing.
*
* In both cases remove the stale local bond (if any) and
* let the ready-state poller time out, which will then
* disconnect and let the user try again. The next
* advertising window will have no bond on the nRF side,
* so the phone will be asked to pair fresh.
*/
LOG_WRN("Security failed for %s: level %u err %d",
addr, level, err);
bt_unpair(BT_ID_DEFAULT, bt_conn_get_dst(conn));
ble_secure = false;
/*
* Cancel the poller so it does not keep waiting — the
* timeout ceiling would handle it eventually, but
* cancelling here gives a faster disconnect.
* The security_wait_ms ceiling check in the poller fires
* on the next reschedule; by setting it to max we make it
* disconnect on the very next tick.
*/
security_wait_ms = BLE_SECURITY_TIMEOUT_MS;
return;
}
LOG_INF("Security changed for %s: level %u", addr, level);
if (level >= BT_SECURITY_L2) {
ble_secure = true;
LOG_INF("BLE link encrypted (level %u)", level);
}
}
BT_CONN_CB_DEFINE(conn_callbacks) = {
.connected = connected,
.disconnected = disconnected,
.security_changed = security_changed,
};
/* ------------------------------------------------------------------ */
/* Authentication callbacks */
/* ------------------------------------------------------------------ */
/*
* Called only for NEW (unbonded) phones during the passkey-entry phase.
* Bonded phones never trigger this — they re-encrypt with the stored LTK.
*/
static void auth_passkey_display(struct bt_conn *conn, unsigned int passkey)
{
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
LOG_INF("*** PASSKEY for %s: %06u ***", addr, passkey);
LOG_INF("Enter this code on the phone to complete pairing");
}
static void auth_cancel(struct bt_conn *conn)
{
ARG_UNUSED(conn);
LOG_INF("Bluetooth authentication cancelled by phone");
}
static void pairing_complete(struct bt_conn *conn, bool bonded)
{
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
LOG_INF("Pairing complete: %s bonded=%d "
"(phone will connect silently from now on)",
addr, bonded);
}
static void pairing_failed(struct bt_conn *conn,
enum bt_security_err reason)
{
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
LOG_ERR("Pairing failed: %s reason=%d", addr, reason);
/* Remove any partial bond so a clean retry is possible */
bt_unpair(BT_ID_DEFAULT, bt_conn_get_dst(conn));
}
static struct bt_conn_auth_cb auth_cb = {
.passkey_display = auth_passkey_display,
.cancel = auth_cancel,
};
static struct bt_conn_auth_info_cb auth_info_cb = {
.pairing_complete = pairing_complete,
.pairing_failed = pairing_failed,
};
/* ------------------------------------------------------------------ */
/* Passkey settings (stored in flash) */
/* ------------------------------------------------------------------ */
static int passkey_settings_set(const char *name,
size_t len,
settings_read_cb read_cb,
void *cb_arg)
{
if (strcmp(name, "value") != 0) {
return -ENOENT;
}
if (len != sizeof(ble_passkey)) {
LOG_ERR("Invalid stored passkey size: %u", (unsigned int)len);
return -EINVAL;
}
int ret = read_cb(cb_arg, &ble_passkey, sizeof(ble_passkey));
if (ret < 0) {
LOG_ERR("Failed to read stored passkey: %d", ret);
return ret;
}
if (ble_passkey > PASSKEY_MAX) {
LOG_ERR("Invalid stored passkey: %06u — resetting", ble_passkey);
ble_passkey = BLE_DEFAULT_PASSKEY;
return -EINVAL;
}
LOG_INF("Passkey loaded from flash: %06u", ble_passkey);
return 0;
}
static int passkey_settings_commit(void)
{
LOG_INF("Passkey settings committed: %06u", ble_passkey);
return 0;
}
static struct settings_handler passkey_settings_handler = {
.name = "ble_passkey",
.h_set = passkey_settings_set,
.h_commit = passkey_settings_commit,
};
/* ------------------------------------------------------------------ */
/* bt_enable callback */
/* ------------------------------------------------------------------ */
void bt_ready(int err)
{
if (err) {
LOG_ERR("Bluetooth init failed (err %d)", err);
return;
}
LOG_INF("Bluetooth initialized");
err = settings_register(&passkey_settings_handler);
if (err) {
LOG_ERR("Failed to register passkey settings: %d", err);
}
/*
* Load ALL settings — this restores both:
* 1. The passkey value (ble_passkey/value)
* 2. All stored bonds (bt/keys/...)
*
* Bonds MUST be loaded so bonded phones can reconnect without
* a passkey prompt. Do NOT call bt_unpair() here.
*/
if (IS_ENABLED(CONFIG_SETTINGS)) {
settings_load();
}
int ret = bt_passkey_set(ble_passkey);
if (ret) {
LOG_ERR("Failed to set BLE passkey: %d", ret);
} else {
LOG_INF("BLE fixed passkey set: %06u", ble_passkey);
}
}
/* ------------------------------------------------------------------ */
/* GATT data callbacks */
/* ------------------------------------------------------------------ */
static ssize_t on_data_received(struct bt_conn *conn,
const struct bt_gatt_attr *attr,
const void *buf,
uint16_t len,
uint16_t offset,
uint8_t flags)
{
ARG_UNUSED(conn);
ARG_UNUSED(attr);
ARG_UNUSED(offset);
ARG_UNUSED(flags);
if (user_data_received_callback) {
user_data_received_callback((const char *)buf, len);
} else {
LOG_ERR("Data received but no callback registered");
}
return len;
}
/* ------------------------------------------------------------------ */
/* RSSI helper */
/* ------------------------------------------------------------------ */
static int read_conn_rssi(struct bt_conn *conn, int8_t *out_rssi)
{
if (!conn || !out_rssi) {
return -EINVAL;
}
uint16_t handle;
int err = bt_hci_get_conn_handle(conn, &handle);
if (err) {
LOG_ERR("Failed to get HCI handle: %d", err);
return err;
}
struct bt_hci_cp_read_rssi cp = {
.handle = sys_cpu_to_le16(handle),
};
struct net_buf *buf = bt_hci_cmd_alloc(K_FOREVER);
if (!buf) {
return -ENOMEM;
}
net_buf_add_mem(buf, &cp, sizeof(cp));
struct net_buf *rsp_buf;
int ret = bt_hci_cmd_send_sync(BT_HCI_OP_READ_RSSI, buf, &rsp_buf);
if (ret) {
LOG_ERR("HCI RSSI request failed: %d", ret);
return ret;
}
struct bt_hci_rp_read_rssi *rp =
(struct bt_hci_rp_read_rssi *)rsp_buf->data;
if (rp->status == 0) {
*out_rssi = rp->rssi;
} else {
LOG_ERR("HCI RSSI status error: 0x%02x", rp->status);
}
net_buf_unref(rsp_buf);
return rp->status;
}
/* ------------------------------------------------------------------ */
/* Public API */
/* ------------------------------------------------------------------ */
int ble_gatt_init(ble_data_received_cb_t data_received_cb)
{
int err;
user_data_received_callback = data_received_cb;
err = bt_conn_auth_cb_register(&auth_cb);
if (err) {
LOG_ERR("Failed to register auth callbacks: %d", err);
return err;
}
err = bt_conn_auth_info_cb_register(&auth_info_cb);
if (err) {
LOG_ERR("Failed to register auth info callbacks: %d", err);
return err;
}
k_work_init_delayable(&power_on_ble_retry_work, power_on_ble_retry_handler);
k_work_init_delayable(&ble_adv_timeout_work, ble_adv_timeout_handler);
k_work_init_delayable(&ble_connected_work, ble_connected_work_handler);
k_work_init_delayable(&ble_disconnect_work, ble_disconnect_work_handler);
k_work_init_delayable(&fota_adv_work, fota_adv_work_handler);
k_work_init_delayable(&deferred_seq_work, deferred_seq_work_handler);
err = bt_enable(bt_ready);
if (err) {
LOG_ERR("Bluetooth init failed: %d", err);
return err;
}
return 0;
}
int ble_gatt_send_raw_data(const uint8_t *data, size_t len)
{
if (!data || len == 0) {
LOG_ERR("BLE TX: invalid data");
return -EINVAL;
}
if (!current_conn || !ble_connected) {
LOG_ERR("BLE TX: not connected");
return -ENOTCONN;
}
const struct bt_gatt_attr *attr = &neuton_gatt.attrs[1];
bool subscribed = bt_gatt_is_subscribed(current_conn,
attr,
BT_GATT_CCC_NOTIFY);
LOG_INF("BLE TX: conn=%p handle=%u len=%u subscribed=%d",
current_conn, attr->handle, (unsigned int)len, subscribed);
if (!subscribed) {
LOG_ERR("BLE TX: mobile has NOT subscribed");
return -EACCES;
}
LOG_HEXDUMP_INF(data, len, "BLE TX:");
int ret = bt_gatt_notify(current_conn, attr, data, len);
if (ret) {
LOG_ERR("bt_gatt_notify failed: %d", ret);
} else {
LOG_INF("bt_gatt_notify SUCCESS");
}
return ret;
}
int ble_gatt_start_advertising(void)
{
return ble_start_advertising_with_timeout();
}
int ble_gatt_get_rssi(int8_t *out_rssi)
{
if (!current_conn) {
LOG_ERR("No current connection");
return -EINVAL;
}
int8_t rssi = 0;
int res = read_conn_rssi(current_conn, &rssi);
if (res == 0 && out_rssi) {
*out_rssi = rssi;
}
return res;
}
void ble_data_received(const char *data, size_t len)
{
if (len >= BLE_RX_BUFFER_SIZE) {
len = BLE_RX_BUFFER_SIZE - 1;
}
memcpy(ble_rx_buffer, data, len);
ble_rx_buffer[len] = '\0';
ble_rx_len = len;
LOG_INF("BLE RX: %s", ble_rx_buffer);
frame_data_received(ble_rx_buffer, ble_rx_len);
}
int passkey_from_payload(const uint8_t *payload, uint32_t *passkey)
{
uint32_t value = 0;
for (int i = 0; i < 6; i++) {
if (payload[i] < '0' || payload[i] > '9') {
return -EINVAL;
}
value = (value * 10) + (payload[i] - '0');
}
*passkey = value;
return 0;
}
int ble_hid_set_passkey(uint32_t passkey)
{
if (passkey > PASSKEY_MAX) {
LOG_ERR("Invalid passkey: %u", passkey);
return -EINVAL;
}
ble_passkey = passkey;
int err = settings_save_one("ble_passkey/value",
&ble_passkey, sizeof(ble_passkey));
if (err) {
LOG_ERR("Failed to save passkey to flash: %d", err);
return err;
}
LOG_INF("Passkey saved to flash: %06u", ble_passkey);
err = bt_passkey_set(ble_passkey);
if (err) {
LOG_ERR("bt_passkey_set failed: %d", err);
}
return err;
}
int ble_user_entered_passkey(uint32_t passkey)
{
if (!current_conn) {
LOG_ERR("No active BLE connection");
return -ENOTCONN;
}
if (!auth_conn || !passkey_requested) {
LOG_ERR("No pending BLE passkey request");
return -EINVAL;
}
if (passkey > PASSKEY_MAX) {
LOG_ERR("Invalid passkey: %06u", passkey);
return -EINVAL;
}
LOG_INF("User entered passkey: %06u", passkey);
int err = bt_conn_auth_passkey_entry(auth_conn, passkey);
if (err) {
LOG_ERR("Failed to submit passkey: %d", err);
return err;
}
LOG_INF("Passkey submitted successfully");
passkey_requested = false;
bt_conn_unref(auth_conn);
auth_conn = NULL;
return 0;
}
int ble_availability_check(void)
{
if (ble_common_is_connected() && current_conn != NULL) {
LOG_INF("BLE already connected — AVAILABLE");
ble_available = true;
return 0;
}
while (k_sem_take(&ble_availability_sem, K_NO_WAIT) == 0) {
}
ble_available = false;
LOG_INF("Checking BLE availability...");
int ret = k_sem_take(&ble_availability_sem,
K_MSEC(BLE_AVAILABILITY_TIMEOUT_MS));
if (ret == 0 && ble_available) {
LOG_INF("BLE AVAILABLE");
return 0;
}
LOG_INF("BLE NOT AVAILABLE (timeout)");
return -ETIMEDOUT;
}
int ble_reset_passkey_to_default(void)
{
ble_passkey = BLE_DEFAULT_PASSKEY;
int err = settings_save_one("ble_passkey/value",
&ble_passkey, sizeof(ble_passkey));
if (err) {
LOG_ERR("Failed to reset passkey: %d", err);
return err;
}
err = bt_passkey_set(ble_passkey);
if (err) {
LOG_ERR("bt_passkey_set failed after reset: %d", err);
}
LOG_INF("BLE passkey reset to default: %06u", ble_passkey);
return err;
}
/*
* Remove bonds for ALL phones and force fresh pairing on next connect.
* Call this only when the user explicitly wants to "forget all phones"
* (e.g. long-press factory reset).
*/
int ble_forget_all_bonds(void)
{
int err = bt_unpair(BT_ID_DEFAULT, BT_ADDR_LE_ANY);
if (err) {
LOG_ERR("bt_unpair (all) failed: %d", err);
} else {
LOG_INF("All bonds removed — every phone must re-pair");
}
return err;
}
int ble_gatt_disconnect_and_stop_advertising(void)
{
if (fota_mode) {
LOG_INF("FOTA active - disconnect/stop request ignored");
return 0;
}
int err = bt_le_adv_stop();
if (err && err != -EALREADY) {
LOG_ERR("Failed to stop advertising: %d", err);
}
if (current_conn) {
err = bt_conn_disconnect(current_conn,
BT_HCI_ERR_REMOTE_USER_TERM_CONN);
if (err) {
LOG_ERR("Failed to disconnect: %d", err);
return err;
}
LOG_INF("BLE disconnect requested");
} else {
LOG_INF("No active BLE connection");
}
return 0;
}
bool ble_gatt_is_ready(void)
{
return ble_connected && ccc_enabled && (current_conn != NULL);
}
bool ble_notifications_enabled(void)
{
return ccc_enabled;
}
bool ble_is_ready_for_data(void)
{
return ble_connected &&
current_conn != NULL &&
ccc_enabled &&
ble_secure;
}
int ble_wait_for_notifications(uint32_t timeout_ms)
{
uint32_t elapsed = 0U;
LOG_INF("Waiting for notification subscription...");
while (elapsed < timeout_ms) {
if (!ble_connected || current_conn == NULL) {
LOG_ERR("BLE connection lost");
return -ENOTCONN;
}
if (ccc_enabled) {
LOG_INF("BLE notifications ENABLED");
return 0;
}
k_sleep(K_MSEC(100));
elapsed += 100U;
}
LOG_ERR("Notification subscription timeout");
return -ETIMEDOUT;
}
int ble_wait_until_ready_for_data(uint32_t timeout_ms)
{
uint32_t elapsed = 0U;
LOG_INF("Waiting for BLE data-ready state...");
while (elapsed < timeout_ms) {
if (!ble_connected || current_conn == NULL) {
LOG_ERR("BLE connection lost");
return -ENOTCONN;
}
if (ccc_enabled && ble_secure) {
LOG_INF("BLE READY: encryption + CCCD done");
return 0;
}
k_sleep(K_MSEC(50));
elapsed += 50U;
}
LOG_ERR("BLE data-ready timeout (connected=%d ccc=%d secure=%d)",
ble_connected, ccc_enabled, ble_secure);
return -ETIMEDOUT;
}
/* ------------------------------------------------------------------ */
/* Work handlers */
/* ------------------------------------------------------------------ */
/* No connection within advertising window */
void ble_adv_timeout_handler(struct k_work *work)
{
ARG_UNUSED(work);
if (fota_mode || (ble_connected && current_conn != NULL)) {
return;
}
LOG_INF("BLE: No connection within advertising window — stopping");
int err = bt_le_adv_stop();
if (err != 0 && err != -EALREADY) {
LOG_ERR("Failed to stop advertising: %d", err);
} else {
LOG_INF("BLE advertising stopped");
}
ble_timeout_active = false;
if (power_on_ble_sequence_active) {
LOG_INF("BLE: Power-on pairing attempt failed");
led_blink_led0(270, 730);
led_blink_led0(270, 730);
led_off();
LOG_INF("BLE: Next attempt in 30 minutes");
k_work_schedule_for_queue(&ble_workq, &power_on_ble_retry_work,
K_MSEC(BLE_POWER_ON_RETRY_MS));
}
}
/*
* Poll every 100 ms for encryption + CCCD subscription.
*
* Bonded phone: re-encrypts in < 500 ms, CCCD restored automatically
* (if CONFIG_BT_GATT_CACHING=y) or written by the app.
*
* New phone: SMP pairing + passkey entry takes several seconds.
*
* BLE_SECURITY_TIMEOUT_MS covers both cases.
* security_changed() sets security_wait_ms = BLE_SECURITY_TIMEOUT_MS
* to trigger an immediate timeout and disconnect on security errors.
*/
void ble_connected_work_handler(struct k_work *work)
{
ARG_UNUSED(work);
if (!ble_connected || current_conn == NULL) {
LOG_INF("BLE: Connection disappeared during wait");
return;
}
security_wait_ms += 100U;
/* ---- timeout ceiling ---- */
if (security_wait_ms >= BLE_SECURITY_TIMEOUT_MS) {
/*
* A phone that is bonded + authenticated (level >= 3) but never
* subscribes to our data characteristic is a DFU client (nRF Device
* Manager). On the charger, keep it connected as a FOTA session
* instead of cutting the link.
*/
if (IS_ENABLED(CONFIG_APP_FOTA) && npm1300_vbus_connected() &&
bt_conn_get_security(current_conn) >= BT_SECURITY_L3) {
LOG_INF("BLE: authenticated client without CCCD - treating as FOTA session");
ble_sequence_active = false;
ble_ready_callback = NULL;
fota_request_now();
return;
}
LOG_ERR("BLE: Ready-state timeout after %u ms — disconnecting",
security_wait_ms);
ble_sequence_active = false;
ble_ready_callback = NULL;
ble_gatt_disconnect_and_stop_advertising();
return;
}
/* ---- check encryption ---- */
bt_security_t sec = bt_conn_get_security(current_conn);
if (sec < BT_SECURITY_L2) {
/* Limit log spam: print only every 2 s */
if ((security_wait_ms % 2000U) < 100U) {
LOG_INF("BLE: Waiting for encryption "
"(level=%d, %u/%u ms)",
sec, security_wait_ms,
BLE_SECURITY_TIMEOUT_MS);
}
k_work_schedule_for_queue(&ble_workq, &ble_connected_work, K_MSEC(100));
return;
}
ble_secure = true;
/* ---- check CCCD subscription ---- */
bool subscribed = bt_gatt_is_subscribed(current_conn,
&neuton_gatt.attrs[1],
BT_GATT_CCC_NOTIFY);
if (!subscribed) {
if ((security_wait_ms % 2000U) < 100U) {
LOG_INF("BLE: Waiting for CCCD (%u/%u ms)",
security_wait_ms, BLE_SECURITY_TIMEOUT_MS);
}
k_work_schedule_for_queue(&ble_workq, &ble_connected_work, K_MSEC(100));
return;
}
ccc_enabled = true;
LOG_INF("BLE READY — encrypted (level=%d), CCCD subscribed "
"(took %u ms)", sec, security_wait_ms);
/* ---- invoke application callback ---- */
if (ble_ready_callback != NULL) {
int ret = ble_ready_callback(ble_argument);
if (ret != 0) {
LOG_ERR("BLE: Ready callback failed: %d", ret);
ble_sequence_active = false;
ble_ready_callback = NULL;
ble_gatt_disconnect_and_stop_advertising();
return;
}
}
ble_sequence_active = false;
ble_ready_callback = NULL;
/*
* Keep the connection alive for 5 s so the notification has time
* to reach the phone before we disconnect.
*/
k_work_schedule_for_queue(&ble_workq, &ble_disconnect_work, K_MSEC(5000));
}
/* Disconnect 5 s after application data delivered */
void ble_disconnect_work_handler(struct k_work *work)
{
ARG_UNUSED(work);
if (fota_mode) {
return;
}
LOG_INF("BLE: Post-data delay expired — disconnecting");
if (current_conn != NULL) {
int err = bt_conn_disconnect(current_conn,
BT_HCI_ERR_REMOTE_USER_TERM_CONN);
if (err) {
LOG_ERR("BLE disconnect failed: %d", err);
} else {
LOG_INF("BLE disconnect requested");
}
}
int err = bt_le_adv_stop();
if (err != 0 && err != -EALREADY) {
LOG_ERR("BLE advertising stop failed: %d", err);
} else {
LOG_INF("BLE advertising stopped");
}
}
/* 30-minute power-on retry */
static void power_on_ble_retry_handler(struct k_work *work)
{
ARG_UNUSED(work);
if (!power_on_ble_sequence_active) {
return;
}
LOG_INF("BLE: 30-minute power-on retry");
if (ble_common_is_connected()) {
LOG_INF("BLE already connected — cancelling retry");
power_on_ble_sequence_active = false;
return;
}
int ret = ble_gatt_start_advertising();
if (ret != 0) {
LOG_ERR("BLE retry advertising failed: %d", ret);
k_work_schedule_for_queue(&ble_workq, &power_on_ble_retry_work,
K_MSEC(BLE_POWER_ON_RETRY_MS));
} else {
LOG_INF("BLE: Retry advertising started");
}
}
/* Legacy simple timeout handler */
void ble_timeout_work_handler(struct k_work *work)
{
ARG_UNUSED(work);
if (fota_mode) {
return;
}
if (ble_connected && current_conn != NULL) {
bt_conn_disconnect(current_conn,
BT_HCI_ERR_REMOTE_USER_TERM_CONN);
return;
}
bt_le_adv_stop();
ble_timeout_active = false;
if (power_on_ble_sequence_active) {
led_blink_led0(270, 730);
led_blink_led0(270, 730);
led_off();
k_work_schedule_for_queue(&ble_workq, &power_on_ble_retry_work,
K_MSEC(BLE_POWER_ON_RETRY_MS));
}
}
/* ------------------------------------------------------------------ */
/* Advertising with timeout (NO bond-clear) */
/* ------------------------------------------------------------------ */
int ble_start_advertising_with_timeout(void)
{
if (fota_mode) {
return -EBUSY;
}
if (current_conn != NULL || ble_connected) {
LOG_INF("BLE already connected");
return -EBUSY;
}
k_work_cancel_delayable(&ble_adv_timeout_work);
k_work_cancel_delayable(&ble_connected_work);
k_work_cancel_delayable(&ble_disconnect_work);
ccc_enabled = false;
ble_secure = false;
security_wait_ms = 0;
/*
* Do NOT call bt_unpair() here.
*
* Bonds are kept so that already-paired phones reconnect
* automatically without seeing a passkey prompt again.
* Only new/unknown phones will go through the pairing ceremony.
*/
/* Stop any leftover advertising */
int err = bt_le_adv_stop();
if (err != 0 && err != -EALREADY) {
LOG_ERR("Failed to stop old advertising: %d", err);
}
err = bt_le_adv_start(BT_LE_ADV_CONN_FAST_1,
ad, ARRAY_SIZE(ad),
sd, ARRAY_SIZE(sd));
if (err) {
LOG_ERR("Advertising failed to start: %d", err);
return err;
}
ble_timeout_active = true;
k_work_schedule_for_queue(&ble_workq, &ble_adv_timeout_work, K_MSEC(BLE_ADV_TIMEOUT_MS));
LOG_INF("BLE advertising started — %u ms window "
"(bonded phones connect silently, new phones will pair)",
BLE_ADV_TIMEOUT_MS);
return 0;
}
/* ------------------------------------------------------------------ */
/* Power-on sequence helpers */
/* ------------------------------------------------------------------ */
void ble_power_on_retry_start(void)
{
power_on_ble_sequence_active = true;
LOG_INF("BLE: Power-on retry mechanism enabled");
}
void ble_power_on_retry_cancel(void)
{
power_on_ble_sequence_active = false;
k_work_cancel_delayable(&power_on_ble_retry_work);
LOG_INF("BLE: Power-on retry mechanism cancelled");
}
void power_on_seq(void)
{
LOG_INF("BLE POWER-ON SEQUENCE START");
power_on_led_blinks();
ble_power_on_retry_start();
int ret = ble_start_sequence(send_power_on_status, 0x00);
if (ret != 0) {
LOG_ERR("BLE: Failed to start power-on sequence: %d", ret);
led_blink_led0(270, 730);
led_blink_led0(270, 730);
led_off();
k_work_schedule_for_queue(&ble_workq, &power_on_ble_retry_work,
K_MSEC(BLE_POWER_ON_RETRY_MS));
return;
}
LOG_INF("BLE: Power-on advertising started");
}
/* ------------------------------------------------------------------ */
/* Sequence API */
/* ------------------------------------------------------------------ */
static int start_sequence(ble_ready_callback_t callback, uint8_t arg, bool urgent)
{
if (callback == NULL) {
return -EINVAL;
}
if (fota_mode && !urgent) {
/* power-on / battery / reset packets wait; they are retried after FOTA */
LOG_INF("BLE sequence postponed: FOTA in progress");
return -EBUSY;
}
if (fota_mode) {
/*
* An urgent sequence (fall alert) always wins over an update: close
* the FOTA window. If a phone is still connected the sequence starts
* as soon as that link is gone. MCUmgr uploads resume from the last
* written offset, so nothing is lost.
*/
LOG_WRN("Urgent BLE sequence requested during FOTA - preempting FOTA");
ble_gatt_fota_exit();
if (ble_connected || current_conn != NULL) {
deferred_seq_cb = callback;
deferred_seq_arg = arg;
deferred_seq_valid = true;
return 0;
}
}
if (ble_connected || current_conn != NULL) {
LOG_INF("BLE already connected");
return -EBUSY;
}
ble_ready_callback = callback;
ble_sequence_active = true;
ble_argument = arg;
return ble_gatt_start_advertising();
}
int ble_start_sequence(ble_ready_callback_t callback, uint8_t arg)
{
return start_sequence(callback, arg, false);
}
int ble_start_sequence_urgent(ble_ready_callback_t callback, uint8_t arg)
{
return start_sequence(callback, arg, true);
}
void ble_sequence_cancel(void)
{
ble_sequence_active = false;
ble_ready_callback = NULL;
k_work_cancel_delayable(&ble_connected_work);
k_work_cancel_delayable(&ble_disconnect_work);
k_work_cancel_delayable(&ble_adv_timeout_work);
ble_gatt_disconnect_and_stop_advertising();
}
/* ------------------------------------------------------------------ */
/* FOTA window */
/* ------------------------------------------------------------------ */
/* MCUmgr SMP service UUID 8D53DC1D-1DB7-4CD3-868B-8A527460AA84 */
#define FOTA_SMP_UUID_VAL \
BT_UUID_128_ENCODE(0x8d53dc1d, 0x1db7, 0x4cd3, 0x868b, 0x8a527460aa84)
static const struct bt_data fota_ad[] = {
BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
BT_DATA_BYTES(BT_DATA_UUID128_ALL, FOTA_SMP_UUID_VAL),
};
/* 100-150 ms: responsive enough for a phone, far cheaper than FAST_1 over minutes */
static const struct bt_le_adv_param fota_adv_param =
BT_LE_ADV_PARAM_INIT(BT_LE_ADV_OPT_CONN,
BT_GAP_ADV_FAST_INT_MIN_2,
BT_GAP_ADV_FAST_INT_MAX_2,
NULL);
bool ble_gatt_fota_mode(void)
{
return fota_mode;
}
bool ble_gatt_sequence_busy(void)
{
/* a normal sequence is advertising (window timer running) or has a link up */
return ble_connected || current_conn != NULL ||
k_work_delayable_is_pending(&ble_adv_timeout_work);
}
static void fota_adv_work_handler(struct k_work *work)
{
ARG_UNUSED(work);
if (!fota_mode || current_conn != NULL) {
return;
}
int err = bt_le_adv_start(&fota_adv_param, fota_ad, ARRAY_SIZE(fota_ad),
sd, ARRAY_SIZE(sd));
if (err == -EALREADY) {
return;
}
if (err) {
LOG_ERR("FOTA advertising failed: %d - retrying", err);
k_work_reschedule_for_queue(&ble_workq, &fota_adv_work, K_SECONDS(1));
return;
}
LOG_INF("FOTA advertising started (SMP service)");
}
static void deferred_seq_work_handler(struct k_work *work)
{
ARG_UNUSED(work);
if (!deferred_seq_valid) {
return;
}
deferred_seq_valid = false;
int ret = ble_start_sequence_urgent(deferred_seq_cb, deferred_seq_arg);
if (ret != 0) {
LOG_ERR("Deferred BLE sequence failed to start: %d", ret);
}
}
int ble_gatt_fota_enter(void)
{
/* Suspend the short-lived timers of the normal sequence. */
k_work_cancel_delayable(&ble_adv_timeout_work);
k_work_cancel_delayable(&ble_connected_work);
k_work_cancel_delayable(&ble_disconnect_work);
k_work_cancel_delayable(&power_on_ble_retry_work);
ble_sequence_active = false;
ble_ready_callback = NULL;
fota_mode = true;
if (current_conn == NULL) {
/* drop any normal-mode advertising so the FOTA ad data replaces it */
(void)bt_le_adv_stop();
k_work_reschedule_for_queue(&ble_workq, &fota_adv_work, K_NO_WAIT);
}
return 0;
}
void ble_gatt_fota_exit(void)
{
if (!fota_mode) {
return;
}
fota_mode = false;
k_work_cancel_delayable(&fota_adv_work);
int err = bt_le_adv_stop();
if (err != 0 && err != -EALREADY) {
LOG_ERR("FOTA: failed to stop advertising: %d", err);
}
if (current_conn != NULL) {
(void)bt_conn_disconnect(current_conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN);
}
if (power_on_ble_sequence_active) {
/* the power-on status was held back by FOTA: send it now, not in 30 min */
k_work_schedule_for_queue(&ble_workq, &power_on_ble_retry_work, K_SECONDS(5));
}
LOG_INF("FOTA window closed");
}
prj.conf
-
Mobile phone models, operating-system versions, and Bluetooth details(working mobiles are realme and poco android version16 not working modules are samsung and moto with android version16)
not working mobile phone log is(nrf connect mobile app)
nRF Connect, 2026-10-27
DTDS_AI_FALL (CE:42:EF:A8:BC:21)
V 17:09:18.556 Connecting to CE:42:EF:A8:BC:21...
D 17:09:18.556 gatt = device.connectGatt(autoConnect = false, TRANSPORT_LE, preferred PHY = LE 1M)
D 17:09:18.562 [Callback] Connection state changed with status: 0 and new state: CONNECTED (2)
I 17:09:18.562 Connected to CE:42:EF:A8:BC:21
D 17:09:20.890 [Broadcast] Action received: android.bluetooth.device.extra.PAIRING_VARIANT, pairing variant: DISPLAY PIN, key: 890075
I 17:09:23.403 Connection parameters updated (interval: 40.0ms, latency: 0, timeout: 420ms)
D 17:09:23.570 [Callback] Connection state changed with status: 19 and new state: DISCONNECTED (0)
W 17:09:23.570 Connection terminated by peer (status 19)
I 17:09:23.570 Disconnected
D 17:09:23.582 [Broadcast] Action received: android.bluetooth.device.action.ACL_DISCONNECTED
D 17:09:23.599 [Broadcast] Action received: android.bluetooth.device.action.BOND_STATE_CHANGED, bond state changed to: NONE (10), reason: AUTH FAILED (1)
I 17:09:23.599 Bonding failed, reason: AUTH FAILED (1)
Please advise whether this is an expected result of Bluetooth pairing-method negotiation or whether an application/configuration change is required.
Thank you for your support.