904 lines
22 KiB
C
904 lines
22 KiB
C
/*******************************************************************************
|
|
* @file ble_service.c
|
|
* @brief BLE NUS service module
|
|
*
|
|
* Original VesiScan-Basic BLE implementation ported to Zephyr/NCS:
|
|
* - NUS (Nordic UART Service) for data exchange
|
|
* - Advertising: 40ms interval, 10-min timeout
|
|
* - Connection: 15ms interval, 0 latency, 10s supervision timeout
|
|
* - TX power +8 dBm, 2M PHY preferred
|
|
* - Dev mode: no security / Production mode: bonding + passkey
|
|
******************************************************************************/
|
|
#include <zephyr/kernel.h>
|
|
#include <zephyr/bluetooth/bluetooth.h>
|
|
#include <zephyr/bluetooth/hci.h>
|
|
#include <zephyr/bluetooth/hci_vs.h>
|
|
#include <zephyr/sys/byteorder.h>
|
|
#include <zephyr/bluetooth/conn.h>
|
|
#include <zephyr/bluetooth/uuid.h>
|
|
#include <zephyr/bluetooth/gatt.h>
|
|
#if IS_ENABLED(CONFIG_SETTINGS)
|
|
#include <zephyr/settings/settings.h>
|
|
#endif
|
|
#include <zephyr/mgmt/mcumgr/transport/smp_bt.h>
|
|
#include <bluetooth/services/nus.h>
|
|
#include <errno.h>
|
|
|
|
#include "ble_service.h"
|
|
#include "main.h"
|
|
#include "debug_print.h"
|
|
#include "led_control.h"
|
|
|
|
LOG_MODULE_REGISTER(ble_svc, LOG_LEVEL_INF);
|
|
|
|
/* Module variables */
|
|
static struct bt_conn *current_conn;
|
|
static ble_data_rx_cb_t rx_callback;
|
|
static bool dfu_advertising_mode;
|
|
static bool advertising_unlimited;
|
|
static uint8_t conn_param_update_attempts;
|
|
static int64_t last_rx_conn_param_update_ms;
|
|
|
|
#define BLE_TX_POWER_NORMAL_DBM 4
|
|
#define BLE_TX_POWER_STRESS_DBM 8
|
|
#define BLE_RSSI_MONITOR_ENABLED 0
|
|
#define BLE_RSSI_STRESS_DBM (-80)
|
|
#define BLE_RSSI_STRESS_COUNT 3
|
|
#define BLE_RSSI_MONITOR_INTERVAL_MS 500
|
|
#define BLE_TX_SLOW_MS 50
|
|
#define BLE_TX_CRITICAL_MS 100
|
|
#define BLE_TX_STUCK_MS 200
|
|
#define BLE_TX_SLOW_COUNT 3
|
|
#define BLE_TX_BUSY_STRESS_RETRY 4
|
|
|
|
extern int bt_hci_get_conn_handle(const struct bt_conn *conn, uint16_t *conn_handle);
|
|
|
|
static uint16_t current_conn_handle;
|
|
static bool current_conn_handle_valid;
|
|
static int8_t current_tx_power_dbm = BLE_TX_POWER_NORMAL_DBM;
|
|
static uint8_t low_rssi_count;
|
|
static uint8_t slow_tx_count;
|
|
static struct k_work_delayable tx_power_monitor_work;
|
|
|
|
#if IS_ENABLED(CONFIG_BT_SMP)
|
|
#define BLE_REQUIRED_SECURITY_LEVEL BT_SECURITY_L4
|
|
static uint32_t pairing_passkey = BT_PASSKEY_RAND;
|
|
#endif
|
|
|
|
/*
|
|
* - mutex: 여러 곳에서 동시에 NUS 전송을 시작하지 못하게 막음
|
|
* - sem : 직전 전송이 정말 끝났는지 기다리는 신호
|
|
*/
|
|
static struct k_mutex nus_tx_lock;
|
|
static struct k_sem nus_tx_done_sem;
|
|
|
|
/* BLE API는 ISR/콜백에서 직접 호출하면 안전하지 않을 수 있으므로 k_work 사용 */
|
|
static struct k_work adv_restart_work;
|
|
static struct k_work_delayable adv_timeout_work;
|
|
static struct k_work_delayable conn_param_update_work;
|
|
|
|
static int ble_tx_power_write(uint8_t handle_type, uint16_t handle, int8_t dbm,
|
|
const char *reason)
|
|
{
|
|
struct net_buf *buf;
|
|
struct net_buf *rsp;
|
|
struct bt_hci_cp_vs_write_tx_power_level *cp;
|
|
struct bt_hci_rp_vs_write_tx_power_level *rp;
|
|
int err;
|
|
|
|
buf = bt_hci_cmd_alloc(K_NO_WAIT);
|
|
if (buf == NULL)
|
|
{
|
|
return -ENOBUFS;
|
|
}
|
|
|
|
cp = net_buf_add(buf, sizeof(*cp));
|
|
cp->handle_type = handle_type;
|
|
cp->handle = sys_cpu_to_le16(handle);
|
|
cp->tx_power_level = dbm;
|
|
|
|
err = bt_hci_cmd_send_sync(BT_HCI_OP_VS_WRITE_TX_POWER_LEVEL, buf, &rsp);
|
|
if (err)
|
|
{
|
|
DBG_ERR("[BLE] TX power set fail reason=%s err=%d\r\n", reason, err);
|
|
return err;
|
|
}
|
|
|
|
rp = (void *)rsp->data;
|
|
if (rp->status != 0U)
|
|
{
|
|
err = -EIO;
|
|
DBG_ERR("[BLE] TX power set rejected reason=%s status=0x%02x\r\n",
|
|
reason, rp->status);
|
|
}
|
|
else
|
|
{
|
|
current_tx_power_dbm = rp->selected_tx_power;
|
|
DBG_CORE("[BLE] TX power %d dBm reason=%s\r\n",
|
|
current_tx_power_dbm, reason);
|
|
}
|
|
|
|
net_buf_unref(rsp);
|
|
return err;
|
|
}
|
|
|
|
static int ble_set_conn_tx_power(int8_t dbm, const char *reason)
|
|
{
|
|
if (!current_conn_handle_valid || current_tx_power_dbm == dbm)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
return ble_tx_power_write(BT_HCI_VS_LL_HANDLE_TYPE_CONN,
|
|
current_conn_handle,
|
|
dbm,
|
|
reason ? reason : "unknown");
|
|
}
|
|
|
|
static void ble_tx_power_stress(const char *reason)
|
|
{
|
|
(void)ble_set_conn_tx_power(BLE_TX_POWER_STRESS_DBM, reason);
|
|
}
|
|
|
|
static int ble_read_conn_rssi(int8_t *rssi)
|
|
{
|
|
struct net_buf *buf;
|
|
struct net_buf *rsp;
|
|
struct bt_hci_cp_read_rssi *cp;
|
|
struct bt_hci_rp_read_rssi *rp;
|
|
int err;
|
|
|
|
if (!current_conn_handle_valid || rssi == NULL)
|
|
{
|
|
return -ENOTCONN;
|
|
}
|
|
|
|
buf = bt_hci_cmd_alloc(K_NO_WAIT);
|
|
if (buf == NULL)
|
|
{
|
|
return -ENOBUFS;
|
|
}
|
|
|
|
cp = net_buf_add(buf, sizeof(*cp));
|
|
cp->handle = sys_cpu_to_le16(current_conn_handle);
|
|
|
|
err = bt_hci_cmd_send_sync(BT_HCI_OP_READ_RSSI, buf, &rsp);
|
|
if (err)
|
|
{
|
|
return err;
|
|
}
|
|
|
|
rp = (void *)rsp->data;
|
|
if (rp->status != 0U)
|
|
{
|
|
err = -EIO;
|
|
}
|
|
else
|
|
{
|
|
*rssi = rp->rssi;
|
|
}
|
|
|
|
net_buf_unref(rsp);
|
|
return err;
|
|
}
|
|
|
|
static void tx_power_monitor_handler(struct k_work *work)
|
|
{
|
|
int8_t rssi = 0;
|
|
int err;
|
|
|
|
ARG_UNUSED(work);
|
|
|
|
if (!current_conn_handle_valid)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!BLE_RSSI_MONITOR_ENABLED)
|
|
{
|
|
return;
|
|
}
|
|
|
|
err = ble_read_conn_rssi(&rssi);
|
|
if (err)
|
|
{
|
|
ble_tx_power_stress("rssi-silence");
|
|
}
|
|
else if (rssi <= BLE_RSSI_STRESS_DBM)
|
|
{
|
|
if (low_rssi_count < UINT8_MAX)
|
|
{
|
|
low_rssi_count++;
|
|
}
|
|
|
|
if (low_rssi_count >= BLE_RSSI_STRESS_COUNT)
|
|
{
|
|
ble_tx_power_stress("rssi-low");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
low_rssi_count = 0U;
|
|
}
|
|
|
|
k_work_schedule(&tx_power_monitor_work,
|
|
K_MSEC(BLE_RSSI_MONITOR_INTERVAL_MS));
|
|
}
|
|
|
|
static void ble_log_local_identity(void)
|
|
{
|
|
bt_addr_le_t addr = {0};
|
|
size_t count = 1;
|
|
char addr_str[BT_ADDR_LE_STR_LEN];
|
|
|
|
bt_id_get(&addr, &count);
|
|
if (count == 0U)
|
|
{
|
|
DBG_ERR("[BLE] No local identity (MAC address) configured\r\n");
|
|
return;
|
|
}
|
|
|
|
bt_addr_le_to_str(&addr, addr_str, sizeof(addr_str));
|
|
DBG_CORE("[BLE] Local identity (MAC address): %s\r\n", addr_str);
|
|
}
|
|
|
|
#if IS_ENABLED(CONFIG_BT_SMP)
|
|
static bool ble_conn_is_secure(const struct bt_conn *conn)
|
|
{
|
|
return (conn != NULL) && (bt_conn_get_security(conn) >= BLE_REQUIRED_SECURITY_LEVEL);
|
|
}
|
|
|
|
static int ble_passkey_to_uint(const char *passkey, unsigned int *value)
|
|
{
|
|
unsigned int parsed = 0U;
|
|
|
|
if ((passkey == NULL) || (value == NULL))
|
|
{
|
|
return -EINVAL;
|
|
}
|
|
|
|
for (size_t i = 0; i < PASSKEY_LENGTH; i++)
|
|
{
|
|
if ((passkey[i] < '0') || (passkey[i] > '9'))
|
|
{
|
|
return -EINVAL;
|
|
}
|
|
|
|
parsed = (parsed * 10U) + (unsigned int)(passkey[i] - '0');
|
|
}
|
|
|
|
*value = parsed;
|
|
return 0;
|
|
}
|
|
|
|
static int ble_security_configure(void)
|
|
{
|
|
unsigned int passkey;
|
|
int err = ble_passkey_to_uint(m_static_passkey, &passkey);
|
|
|
|
if (err)
|
|
{
|
|
DBG_ERR("[BLE] Invalid stored passkey, using default\r\n");
|
|
memset(m_static_passkey, 0, sizeof(m_static_passkey));
|
|
memcpy(m_static_passkey, DEFAULT_PASSKEY, PASSKEY_LENGTH);
|
|
|
|
err = ble_passkey_to_uint(m_static_passkey, &passkey);
|
|
if (err)
|
|
{
|
|
return err;
|
|
}
|
|
}
|
|
|
|
pairing_passkey = passkey;
|
|
DBG_CORE("[BLE] Fixed passkey configured\r\n");
|
|
return 0;
|
|
}
|
|
|
|
static void auth_pairing_confirm(struct bt_conn *conn)
|
|
{
|
|
int err = bt_conn_auth_pairing_confirm(conn);
|
|
|
|
if (err)
|
|
{
|
|
DBG_ERR("[BLE] Pairing confirm failed err=%d\r\n", err);
|
|
return;
|
|
}
|
|
|
|
DBG_CORE("[BLE] Pairing confirmed\r\n");
|
|
}
|
|
|
|
static void auth_cancel(struct bt_conn *conn)
|
|
{
|
|
ARG_UNUSED(conn);
|
|
DBG_ERR("[BLE] Pairing cancelled\r\n");
|
|
}
|
|
|
|
#if IS_ENABLED(CONFIG_BT_APP_PASSKEY)
|
|
static uint32_t auth_app_passkey(struct bt_conn *conn)
|
|
{
|
|
unsigned int passkey;
|
|
|
|
ARG_UNUSED(conn);
|
|
|
|
if (ble_passkey_to_uint(m_static_passkey, &passkey) == 0)
|
|
{
|
|
return passkey;
|
|
}
|
|
|
|
DBG_ERR("[BLE] Invalid runtime passkey, using configured fallback\r\n");
|
|
return pairing_passkey;
|
|
}
|
|
#endif
|
|
|
|
static struct bt_conn_auth_cb auth_cb =
|
|
{
|
|
.pairing_confirm = auth_pairing_confirm,
|
|
.cancel = auth_cancel,
|
|
#if IS_ENABLED(CONFIG_BT_APP_PASSKEY)
|
|
.app_passkey = auth_app_passkey,
|
|
#endif
|
|
};
|
|
|
|
static void auth_pairing_complete(struct bt_conn *conn, bool bonded)
|
|
{
|
|
ARG_UNUSED(conn);
|
|
DBG_CORE("[BLE] Pairing complete bonded=%u\r\n", bonded ? 1U : 0U);
|
|
}
|
|
|
|
static void auth_pairing_failed(struct bt_conn *conn, enum bt_security_err reason)
|
|
{
|
|
DBG_ERR("[BLE] Pairing failed reason=%u\r\n", reason);
|
|
(void)bt_conn_disconnect(conn, BT_HCI_ERR_AUTH_FAIL);
|
|
}
|
|
|
|
static struct bt_conn_auth_info_cb auth_info_cb =
|
|
{
|
|
.pairing_complete = auth_pairing_complete,
|
|
.pairing_failed = auth_pairing_failed,
|
|
};
|
|
|
|
static int ble_security_init(void)
|
|
{
|
|
int err = bt_conn_auth_cb_register(&auth_cb);
|
|
|
|
if (err)
|
|
{
|
|
DBG_ERR("[BLE] auth cb register failed err=%d\r\n", err);
|
|
return err;
|
|
}
|
|
|
|
err = bt_conn_auth_info_cb_register(&auth_info_cb);
|
|
if (err)
|
|
{
|
|
DBG_ERR("[BLE] auth info cb register failed err=%d\r\n", err);
|
|
return err;
|
|
}
|
|
|
|
return ble_security_configure();
|
|
}
|
|
#endif
|
|
|
|
static void adv_restart_handler(struct k_work *work)
|
|
{
|
|
ARG_UNUSED(work);
|
|
ble_advertising_start();
|
|
led_set_state(LED_STATE_ADVERTISING);
|
|
}
|
|
|
|
static bool ble_disconnect_reason_uses_unlimited_adv(uint8_t reason)
|
|
{
|
|
switch (reason)
|
|
{
|
|
case BT_HCI_ERR_CONN_TIMEOUT:
|
|
case BT_HCI_ERR_LL_RESP_TIMEOUT:
|
|
case BT_HCI_ERR_TERM_DUE_TO_MIC_FAIL:
|
|
case BT_HCI_ERR_CONN_FAIL_TO_ESTAB:
|
|
return true;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static void adv_timeout_handler(struct k_work *work)
|
|
{
|
|
ARG_UNUSED(work);
|
|
|
|
if (!ble_connection_st && !advertising_unlimited && !dfu_advertising_mode)
|
|
{
|
|
DBG_PRINTF("[BLE] Advertising timeout\r\n");
|
|
(void)ble_advertising_stop();
|
|
device_power_off_reason("ble-advertising-timeout");
|
|
}
|
|
}
|
|
|
|
static bool ble_conn_param_is_preferred(uint16_t interval, uint16_t latency,
|
|
uint16_t timeout)
|
|
{
|
|
return (interval == BLE_MIN_CONN_INTERVAL) &&
|
|
(latency == BLE_SLAVE_LATENCY) &&
|
|
(timeout == BLE_CONN_SUP_TIMEOUT);
|
|
}
|
|
|
|
static int ble_request_preferred_conn_params(bool count_attempt)
|
|
{
|
|
struct bt_le_conn_param conn_param =
|
|
{
|
|
.interval_min = BLE_MIN_CONN_INTERVAL,
|
|
.interval_max = BLE_MAX_CONN_INTERVAL,
|
|
.latency = BLE_SLAVE_LATENCY,
|
|
.timeout = BLE_CONN_SUP_TIMEOUT,
|
|
};
|
|
int err;
|
|
|
|
if (current_conn == NULL)
|
|
{
|
|
return -ENOTCONN;
|
|
}
|
|
|
|
if (count_attempt)
|
|
{
|
|
conn_param_update_attempts++;
|
|
}
|
|
|
|
DBG_PRINTF("[BLE] Request params: interval=%u latency=%u timeout=%u attempt=%u\r\n",
|
|
BLE_MIN_CONN_INTERVAL,
|
|
BLE_SLAVE_LATENCY,
|
|
BLE_CONN_SUP_TIMEOUT,
|
|
conn_param_update_attempts);
|
|
|
|
err = bt_conn_le_param_update(current_conn, &conn_param);
|
|
if (err)
|
|
{
|
|
DBG_ERR("[BLE] Param update failed err=%d\r\n", err);
|
|
return err;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void conn_param_update_handler(struct k_work *work)
|
|
{
|
|
ARG_UNUSED(work);
|
|
|
|
if (current_conn == NULL)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (conn_param_update_attempts >= BLE_CONN_PARAM_MAX_ATTEMPTS)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (ble_request_preferred_conn_params(true) == 0 &&
|
|
conn_param_update_attempts < BLE_CONN_PARAM_MAX_ATTEMPTS)
|
|
{
|
|
k_work_schedule(&conn_param_update_work,
|
|
K_MSEC(BLE_CONN_PARAM_RETRY_INTERVAL_MS));
|
|
}
|
|
}
|
|
|
|
/* Advertising data */
|
|
|
|
static const struct bt_data sd[] =
|
|
{
|
|
// 평상시 Web Bluetooth 앱이 찾을 수 있도록 NUS UUID 광고
|
|
BT_DATA_BYTES(BT_DATA_UUID128_ALL, BT_UUID_NUS_VAL),
|
|
};
|
|
|
|
static const struct bt_data sd_dfu[] =
|
|
{
|
|
// DFU 진입 후 mcumgr 앱이 찾을 수 있도록 SMP UUID 광고
|
|
BT_DATA_BYTES(BT_DATA_UUID128_SOME, SMP_BT_SVC_UUID_VAL),
|
|
};
|
|
|
|
/* Connection callbacks */
|
|
static void connected(struct bt_conn *conn, uint8_t err)
|
|
{
|
|
if (err)
|
|
{
|
|
DBG_ERR("[BLE] Connect failed (err %d) -> re-adv\r\n", err);
|
|
k_work_submit(&adv_restart_work);
|
|
return;
|
|
}
|
|
|
|
current_conn = bt_conn_ref(conn);
|
|
ble_connection_st = true;
|
|
current_conn_handle_valid = (bt_hci_get_conn_handle(conn, ¤t_conn_handle) == 0);
|
|
current_tx_power_dbm = BLE_TX_POWER_NORMAL_DBM;
|
|
low_rssi_count = 0U;
|
|
slow_tx_count = 0U;
|
|
(void)ble_set_conn_tx_power(BLE_TX_POWER_NORMAL_DBM, "connected");
|
|
if (BLE_RSSI_MONITOR_ENABLED)
|
|
{
|
|
k_work_schedule(&tx_power_monitor_work, K_MSEC(BLE_RSSI_MONITOR_INTERVAL_MS));
|
|
}
|
|
|
|
/* 연결 정보 출력 */
|
|
char addr_str[BT_ADDR_LE_STR_LEN];
|
|
bt_addr_le_to_str(bt_conn_get_dst(conn), addr_str, sizeof(addr_str));
|
|
DBG_CORE("[BLE] Peer: %s\r\n", addr_str);
|
|
|
|
conn_param_update_attempts = 0U;
|
|
last_rx_conn_param_update_ms = 0;
|
|
|
|
#if IS_ENABLED(CONFIG_BT_SMP)
|
|
err = bt_conn_set_security(conn, BLE_REQUIRED_SECURITY_LEVEL);
|
|
if (err)
|
|
{
|
|
DBG_ERR("[BLE] Security request failed err=%d\r\n", err);
|
|
(void)bt_conn_disconnect(conn, BT_HCI_ERR_AUTH_FAIL);
|
|
return;
|
|
}
|
|
DBG_CORE("[BLE] Security requested level=%u\r\n", BLE_REQUIRED_SECURITY_LEVEL);
|
|
#endif
|
|
|
|
led_set_state(LED_STATE_OFF);
|
|
DBG_CORE("[BLE] Connected\r\n");
|
|
}
|
|
|
|
static void disconnected(struct bt_conn *conn, uint8_t reason)
|
|
{
|
|
k_work_cancel_delayable(&conn_param_update_work);
|
|
k_work_cancel_delayable(&tx_power_monitor_work);
|
|
conn_param_update_attempts = 0U;
|
|
last_rx_conn_param_update_ms = 0;
|
|
current_conn_handle_valid = false;
|
|
current_tx_power_dbm = BLE_TX_POWER_NORMAL_DBM;
|
|
low_rssi_count = 0U;
|
|
slow_tx_count = 0U;
|
|
|
|
if (current_conn)
|
|
{
|
|
bt_conn_unref(current_conn);
|
|
current_conn = NULL;
|
|
}
|
|
|
|
ble_connection_st = false;
|
|
advertising_unlimited = ble_disconnect_reason_uses_unlimited_adv(reason);
|
|
DBG_CORE("[BLE] Disconnected reason=0x%02x adv=%s\r\n",
|
|
reason,
|
|
advertising_unlimited ? "unlimited" : "10min");
|
|
|
|
// 워크큐에서 advertising 재시작
|
|
k_work_submit(&adv_restart_work);
|
|
}
|
|
|
|
static bool le_param_req(struct bt_conn *conn, struct bt_le_conn_param *param)
|
|
{
|
|
ARG_UNUSED(conn);
|
|
ARG_UNUSED(param);
|
|
|
|
return true;
|
|
}
|
|
|
|
static void le_param_updated(struct bt_conn *conn, uint16_t interval,
|
|
uint16_t latency, uint16_t timeout)
|
|
{
|
|
ARG_UNUSED(conn);
|
|
|
|
DBG_PRINTF("[BLE] Params: interval=%d latency=%d timeout=%d\r\n",
|
|
interval, latency, timeout);
|
|
|
|
if (ble_conn_param_is_preferred(interval, latency, timeout))
|
|
{
|
|
k_work_cancel_delayable(&conn_param_update_work);
|
|
}
|
|
}
|
|
|
|
#if IS_ENABLED(CONFIG_BT_SMP)
|
|
static void security_changed(struct bt_conn *conn, bt_security_t level,
|
|
enum bt_security_err err)
|
|
{
|
|
ARG_UNUSED(conn);
|
|
|
|
if (err)
|
|
{
|
|
DBG_ERR("[BLE] Security failed level=%u err=%u\r\n", level, err);
|
|
return;
|
|
}
|
|
|
|
DBG_CORE("[BLE] Security ready level=%u\r\n", level);
|
|
}
|
|
#endif
|
|
|
|
BT_CONN_CB_DEFINE(conn_callbacks) =
|
|
{
|
|
.connected = connected,
|
|
.disconnected = disconnected,
|
|
.le_param_req = le_param_req,
|
|
.le_param_updated = le_param_updated,
|
|
#if IS_ENABLED(CONFIG_BT_SMP)
|
|
.security_changed = security_changed,
|
|
#endif
|
|
};
|
|
|
|
/* NUS callbacks */
|
|
static void nus_received(struct bt_conn *conn, const uint8_t *data, uint16_t len)
|
|
{
|
|
#if IS_ENABLED(CONFIG_BT_SMP)
|
|
if (!ble_conn_is_secure(conn))
|
|
{
|
|
DBG_ERR("[NUS RX] blocked until BLE security is ready\r\n");
|
|
return;
|
|
}
|
|
#else
|
|
ARG_UNUSED(conn);
|
|
#endif
|
|
|
|
//DBG_CORE("[NUS RX] len=%u\r\n", len);
|
|
|
|
if (current_conn == conn)
|
|
{
|
|
int64_t now_ms = k_uptime_get();
|
|
|
|
ARG_UNUSED(now_ms);
|
|
}
|
|
|
|
if (rx_callback)
|
|
{
|
|
rx_callback(data, len);
|
|
}
|
|
}
|
|
|
|
static void nus_sent(struct bt_conn *conn)
|
|
{
|
|
ARG_UNUSED(conn);
|
|
// 직전 NUS 패킷 전송 완료
|
|
k_sem_give(&nus_tx_done_sem);
|
|
//DBG_CORE("[NUS TX] complete\r\n");
|
|
}
|
|
|
|
static struct bt_nus_cb nus_cb =
|
|
{
|
|
.received = nus_received,
|
|
.sent = nus_sent,
|
|
};
|
|
|
|
/* Public functions */
|
|
int ble_service_init(ble_data_rx_cb_t rx_cb)
|
|
{
|
|
int err;
|
|
|
|
rx_callback = rx_cb;
|
|
k_mutex_init(&nus_tx_lock);
|
|
k_sem_init(&nus_tx_done_sem, 0, 1);
|
|
|
|
k_work_init(&adv_restart_work, adv_restart_handler);
|
|
k_work_init_delayable(&adv_timeout_work, adv_timeout_handler);
|
|
k_work_init_delayable(&conn_param_update_work, conn_param_update_handler);
|
|
|
|
k_work_init_delayable(&tx_power_monitor_work, tx_power_monitor_handler);
|
|
// Enable BLE stack
|
|
err = bt_enable(NULL);
|
|
if (err)
|
|
{
|
|
DBG_ERR("[BLE] bt_enable failed (err %d)\r\n", err);
|
|
return err;
|
|
}
|
|
DBG_CORE("[BLE] Stack enabled\r\n");
|
|
|
|
#if IS_ENABLED(CONFIG_BT_SETTINGS)
|
|
err = settings_load_subtree("bt");
|
|
if (err)
|
|
{
|
|
DBG_ERR("[BLE] BT settings load failed err=%d\r\n", err);
|
|
return err;
|
|
}
|
|
DBG_CORE("[BLE] BT settings loaded\r\n");
|
|
#endif
|
|
|
|
ble_log_local_identity();
|
|
|
|
#if IS_ENABLED(CONFIG_BT_SMP)
|
|
err = ble_security_init();
|
|
if (err)
|
|
{
|
|
return err;
|
|
}
|
|
#endif
|
|
|
|
// Initialize NUS
|
|
err = bt_nus_init(&nus_cb);
|
|
if (err)
|
|
{
|
|
DBG_ERR("[BLE] NUS init failed (err %d)\r\n", err);
|
|
return err;
|
|
}
|
|
DBG_CORE("[BLE] NUS initialized\r\n");
|
|
|
|
return 0;
|
|
}
|
|
|
|
int ble_advertising_start(void)
|
|
{
|
|
int err;
|
|
const struct bt_data *scan_data = dfu_advertising_mode ? sd_dfu : sd;
|
|
size_t scan_data_count = dfu_advertising_mode ? ARRAY_SIZE(sd_dfu) : ARRAY_SIZE(sd);
|
|
size_t serial_name_len = strnlen(SERIAL_NO, SERIAL_NO_LENGTH);
|
|
const struct bt_data ad[] =
|
|
{
|
|
BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
|
|
BT_DATA(BT_DATA_NAME_COMPLETE, SERIAL_NO, serial_name_len),
|
|
};
|
|
|
|
struct bt_le_adv_param adv_param = BT_LE_ADV_PARAM_INIT(
|
|
BT_LE_ADV_OPT_CONN,
|
|
APP_ADV_INTERVAL, // min interval
|
|
APP_ADV_INTERVAL, // max interval
|
|
NULL // undirected
|
|
);
|
|
|
|
err = bt_set_name(SERIAL_NO);
|
|
if (err)
|
|
{
|
|
DBG_ERR("[BLE] Name set failed (err %d)\r\n", err);
|
|
return err;
|
|
}
|
|
|
|
err = bt_le_adv_start(&adv_param, ad, ARRAY_SIZE(ad), scan_data, scan_data_count);
|
|
if (err)
|
|
{
|
|
DBG_ERR("[BLE] Adv start failed (err %d)\r\n", err);
|
|
return err;
|
|
}
|
|
|
|
k_work_cancel_delayable(&adv_timeout_work);
|
|
if (!advertising_unlimited && !dfu_advertising_mode)
|
|
{
|
|
k_work_schedule(&adv_timeout_work, K_MSEC(APP_ADV_DURATION * 10));
|
|
}
|
|
|
|
DBG_PRINTF("[BLE] Advertising started (%s)\r\n", dfu_advertising_mode ? "SMP" : "NUS");
|
|
return 0;
|
|
}
|
|
|
|
int ble_advertising_stop(void)
|
|
{
|
|
int err = bt_le_adv_stop();
|
|
if (err)
|
|
{
|
|
DBG_ERR("[BLE] Adv stop failed (err %d)\r\n", err);
|
|
return err;
|
|
}
|
|
|
|
k_work_cancel_delayable(&adv_timeout_work);
|
|
DBG_PRINTF("[BLE] Advertising stopped\r\n");
|
|
return 0;
|
|
}
|
|
|
|
int ble_dfu_advertising_enable(void)
|
|
{
|
|
if (dfu_advertising_mode)
|
|
{
|
|
DBG_ERR("[BLE] DFU advertising already enabled\r\n");
|
|
return -EALREADY;
|
|
}
|
|
|
|
// mdf? 응답 전송 후 현재 연결을 끊고 SMP UUID 광고로 재시작
|
|
dfu_advertising_mode = true;
|
|
DBG_PRINTF("[BLE] DFU advertising mode enabled\r\n");
|
|
|
|
if (current_conn)
|
|
{
|
|
int err = bt_conn_disconnect(current_conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN);
|
|
if (err)
|
|
{
|
|
DBG_ERR("[BLE] DFU disconnect failed err=%d\r\n", err);
|
|
return err;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
(void)ble_advertising_stop();
|
|
return ble_advertising_start();
|
|
}
|
|
|
|
bool ble_dfu_advertising_is_enabled(void)
|
|
{
|
|
return dfu_advertising_mode;
|
|
}
|
|
|
|
int ble_data_send(const uint8_t *data, uint16_t len)
|
|
{
|
|
int err;
|
|
int64_t tx_start_ms;
|
|
|
|
if (!current_conn)
|
|
{
|
|
DBG_ERR("[NUS TX] not connected\r\n");
|
|
return -ENOTCONN;
|
|
}
|
|
|
|
#if IS_ENABLED(CONFIG_BT_SMP)
|
|
if (!ble_conn_is_secure(current_conn))
|
|
{
|
|
DBG_ERR("[NUS TX] blocked until BLE security is ready\r\n");
|
|
return -EACCES;
|
|
}
|
|
#endif
|
|
|
|
// 먼저 들어온 전송이 끝나기 전 다음 전송이 겹치지 않게 잠금 (NUS는 한 번에 한 패킷씩 보내는 것이 안전)
|
|
k_mutex_lock(&nus_tx_lock, K_FOREVER);
|
|
|
|
// 혹시 남아 있는 완료 신호가 있으면 비우고 이번 전송 전용 상태로 만듦
|
|
while (k_sem_take(&nus_tx_done_sem, K_NO_WAIT) == 0) {
|
|
}
|
|
|
|
//DBG_CORE("[NUS TX] send len=%u\r\n", len);
|
|
tx_start_ms = k_uptime_get();
|
|
for (int retry = 0; ; retry++)
|
|
{
|
|
err = bt_nus_send(current_conn, data, len);
|
|
if (err != -ENOMEM || retry >= 20) {
|
|
break;
|
|
}
|
|
|
|
/*
|
|
* -ENOMEM : BLE TX 버퍼가 가득참
|
|
* 바로 포기하지 않고 짧게 쉬었다가 다시 시도
|
|
*/
|
|
DBG_ERR("[NUS TX] busy, retry=%d\r\n", retry + 1);
|
|
if ((retry + 1) >= BLE_TX_BUSY_STRESS_RETRY)
|
|
{
|
|
ble_tx_power_stress("tx-queue-busy");
|
|
}
|
|
k_msleep(5);
|
|
}
|
|
|
|
if (err)
|
|
{
|
|
DBG_ERR("[NUS TX] failed (err %d)\r\n", err);
|
|
k_mutex_unlock(&nus_tx_lock);
|
|
return err;
|
|
}
|
|
|
|
// bt_nus_send() 성공 후 sent 콜백이 올 때까지 기다림 (다음 패킷을 보내도 되는 시점)
|
|
err = k_sem_take(&nus_tx_done_sem, K_MSEC(BLE_TX_STUCK_MS));
|
|
if (err)
|
|
{
|
|
ble_tx_power_stress("tx-complete-stuck");
|
|
err = k_sem_take(&nus_tx_done_sem, K_MSEC(1000 - BLE_TX_STUCK_MS));
|
|
if (err)
|
|
{
|
|
DBG_ERR("[NUS TX] completion timeout (err %d)\r\n", err);
|
|
}
|
|
}
|
|
|
|
if (!err)
|
|
{
|
|
int64_t tx_elapsed_ms = k_uptime_get() - tx_start_ms;
|
|
|
|
if (tx_elapsed_ms >= BLE_TX_CRITICAL_MS)
|
|
{
|
|
ble_tx_power_stress("tx-complete-slow100");
|
|
slow_tx_count = 0U;
|
|
}
|
|
else if (tx_elapsed_ms >= BLE_TX_SLOW_MS)
|
|
{
|
|
if (slow_tx_count < UINT8_MAX)
|
|
{
|
|
slow_tx_count++;
|
|
}
|
|
|
|
if (slow_tx_count >= BLE_TX_SLOW_COUNT)
|
|
{
|
|
ble_tx_power_stress("tx-complete-slow50");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
slow_tx_count = 0U;
|
|
}
|
|
}
|
|
|
|
k_mutex_unlock(&nus_tx_lock);
|
|
return err;
|
|
}
|
|
|
|
bool ble_is_connected(void)
|
|
{
|
|
return (current_conn != NULL);
|
|
}
|