Files
VesiScan-Basic_Zephyr/src/ble/ble_service.c
T
jh.chun 046919b7d9 연결 직후 FW에서 먼저 MTU Exchange 요청
- 기존: central이 MTU Exchange Request를 보내면 FW에서 응답값 전달
 → central에서 요청하지 않는 경우 기본값 23 유지됨
2026-07-20 17:45:47 +09:00

868 lines
23 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/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 "ble_tx_power.h"
#include "main.h"
#include "debug_print.h"
#include "led_control.h"
LOG_MODULE_REGISTER(ble_svc, LOG_LEVEL_INF);
/* BLE service 상태 */
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;
static uint16_t current_notify_mtu = 23U;
#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;
/*
* Store the UATT MTU used by normal GATT/NUS notifications.
* NUS notifications can carry application payload up to UATT MTU - 3 bytes.
*/
static void att_mtu_updated(struct bt_conn *conn, uint16_t tx, uint16_t rx)
{
uint16_t uatt = bt_gatt_get_uatt_mtu(conn);
if (uatt != 0U)
{
current_notify_mtu = uatt;
}
else
{
current_notify_mtu = (tx < rx) ? tx : rx;
}
DBG_PRINTF("[BLE] ATT MTU updated tx=%u rx=%u uatt=%u used=%u notify_payload=%u\r\n",
tx, rx, uatt, current_notify_mtu,
(current_notify_mtu > 3U) ? (current_notify_mtu - 3U) : 0U);
}
/* MTU Exchange 연결 직후 요청 */
static void mtu_exchange_cb(struct bt_conn *conn, uint8_t err, struct bt_gatt_exchange_params *params)
{
uint16_t uatt;
ARG_UNUSED(params);
if (err)
{
DBG_ERR("[BLE] MTU exchange failed err=%u\r\n", err);
return;
}
uatt = bt_gatt_get_uatt_mtu(conn);
if (uatt != 0U)
{
current_notify_mtu = uatt;
}
DBG_PRINTF("[BLE] MTU exchange complete uatt=%u notify_payload=%u\r\n",
uatt,
(uatt > 3U) ? (uatt - 3U) : 0U);
}
static struct bt_gatt_exchange_params mtu_exchange_params =
{
.func = mtu_exchange_cb,
};
static struct bt_gatt_cb gatt_callbacks =
{
.att_mtu_updated = att_mtu_updated,
};
/* 로컬 BLE MAC 주소 로그 */
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)
/* NUS 접근 전 보안 level 확인 */
static bool ble_conn_is_secure(const struct bt_conn *conn)
{
return (conn != NULL) && (bt_conn_get_security(conn) >= BLE_REQUIRED_SECURITY_LEVEL);
}
/* 6자리 ASCII passkey를 숫자로 변환 */
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++)
{
// 숫자 6자리만 허용
if ((passkey[i] < '0') || (passkey[i] > '9'))
{
return -EINVAL;
}
parsed = (parsed * 10U) + (unsigned int)(passkey[i] - '0');
}
*value = parsed;
return 0;
}
/* 저장된 passkey 검증, 실패 시 기본값 사용 */
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;
}
/* Just Works/passkey pairing 요청 자동 승인 */
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");
}
/* pairing 취소 로그 */
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)
/* Zephyr pairing callback에 현재 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
/* pairing 처리 callback 묶음 */
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
};
/* pairing 성공 로그 */
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);
}
/* pairing 실패 시 연결 종료 */
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);
}
/* pairing 결과 callback 묶음 */
static struct bt_conn_auth_info_cb auth_info_cb =
{
.pairing_complete = auth_pairing_complete,
.pairing_failed = auth_pairing_failed,
};
/* BLE 보안 callback 등록, passkey 설정 */
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
/* callback context 밖에서 advertising 재시작 */
static void adv_restart_handler(struct k_work *work)
{
ARG_UNUSED(work);
ble_advertising_start();
led_set_state(LED_STATE_ADVERTISING);
}
/*
* 연결 해제 후 재광고 정책
* - 앱이 연결을 정상 종료(0x13)한 경우만 10분 타임아웃 후 자동 전원 OFF
* - 그 외(거리 이탈, 링크 타임아웃, 폰 전원 OFF, 페어링/보안 실패 등)는 재연결 대기를 위해 무제한 광고
*/
static bool ble_disconnect_reason_uses_unlimited_adv(uint8_t reason)
{
switch (reason)
{
case BT_HCI_ERR_REMOTE_USER_TERM_CONN: // 0x13 앱이 연결을 정상 종료
return false; // 10분 타임아웃 후 전원 OFF
default:
return true; // 그 외 전부 → 무제한 광고
}
}
/* 광고 timeout 시 자동 전원 OFF 판단 */
static void adv_timeout_handler(struct k_work *work)
{
ARG_UNUSED(work);
// 미연결 상태에서만 발동하므로 DFU 광고도 동일하게 10분 후 종료(연결 중이면 발동 안 함)
if (!ble_connection_st && !advertising_unlimited)
{
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);
}
/* 15ms interval 목표로 connection parameter update 요청 */
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,
};
struct bt_conn_info info;
int err;
// 연결 없으면 요청 불가
if (current_conn == NULL)
{
return -ENOTCONN;
}
// 현재 체결된 파라미터를 조회해서, 이미 목표값이면 재요청/재시도 불필요
err = bt_conn_get_info(current_conn, &info);
if ((err == 0) && (info.type == BT_CONN_TYPE_LE))
{
uint16_t cur_interval = info.le.interval;
uint16_t cur_latency = info.le.latency;
uint16_t cur_timeout = info.le.timeout;
DBG_PRINTF("[BLE] Current params: interval=%u(%u.%01ums) latency=%u timeout=%u(%ums)\r\n",
cur_interval,
(cur_interval * 125U) / 100U,
((cur_interval * 125U) % 100U) / 10U,
cur_latency,
cur_timeout,
cur_timeout * 10U);
if (ble_conn_param_is_preferred(cur_interval, cur_latency, cur_timeout))
{
DBG_PRINTF("[BLE] Already at preferred params, skip request\r\n");
return -EALREADY;
}
}
// 예약 재시도에서만 attempt 증가
if (count_attempt)
{
conn_param_update_attempts++;
}
DBG_PRINTF("[BLE] Request params: interval=%u(%u.%01ums) latency=%u timeout=%u(%ums) attempt=%u\r\n",
BLE_MIN_CONN_INTERVAL,
(BLE_MIN_CONN_INTERVAL * 125U) / 100U,
((BLE_MIN_CONN_INTERVAL * 125U) % 100U) / 10U,
BLE_SLAVE_LATENCY,
BLE_CONN_SUP_TIMEOUT,
BLE_CONN_SUP_TIMEOUT * 10U,
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;
}
/* connection parameter 재시도 work */
static void conn_param_update_handler(struct k_work *work)
{
ARG_UNUSED(work);
if (current_conn == NULL)
{
return;
}
// central이 계속 거부하면 최대 횟수에서 중지
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 scan response 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),
};
/* BLE 연결 완료 처리 */
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;
ble_tx_power_on_connected(conn); // TX power/RSSI 상태는 별도 모듈에서 관리
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); // 연결 정보 출력
err = bt_gatt_exchange_mtu(conn, &mtu_exchange_params);
if (err)
{
DBG_ERR("[BLE] MTU exchange request failed err=%d\r\n", err);
}
else
{
DBG_PRINTF("[BLE] MTU exchange requested\r\n");
}
conn_param_update_attempts = 0U;
last_rx_conn_param_update_ms = 0;
// 연결 직후 약간 기다린 뒤 preferred parameter 요청
k_work_schedule(&conn_param_update_work, K_MSEC(BLE_CONN_PARAM_FIRST_DELAY_MS));
#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");
}
/* BLE 연결 해제 처리 */
static void disconnected(struct bt_conn *conn, uint8_t reason)
{
k_work_cancel_delayable(&conn_param_update_work);
ble_tx_power_on_disconnected();
conn_param_update_attempts = 0U;
last_rx_conn_param_update_ms = 0;
if (current_conn)
{
bt_conn_unref(current_conn);
current_conn = NULL;
}
ble_connection_st = false;
current_notify_mtu = 23U;
if (dfu_advertising_mode)
{
// DFU 광고는 미연결 10분 타임아웃 적용
advertising_unlimited = false;
}
else
{
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);
}
/* central의 connection parameter 요청 허용 */
static bool le_param_req(struct bt_conn *conn, struct bt_le_conn_param *param)
{
ARG_UNUSED(conn);
ARG_UNUSED(param);
return true;
}
/* connection parameter 변경 결과 확인 */
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=%u(%u.%01ums) latency=%u timeout=%u(%ums)\r\n",
interval,
(interval * 125U) / 100U,
((interval * 125U) % 100U) / 10U,
latency,
timeout,
timeout * 10U);
// 목표값 도달 시 재시도 work 중지
if (ble_conn_param_is_preferred(interval, latency, timeout))
{
k_work_cancel_delayable(&conn_param_update_work);
}
else if (conn_param_update_attempts < BLE_CONN_PARAM_MAX_ATTEMPTS)
{
k_work_schedule(&conn_param_update_work, K_MSEC(BLE_CONN_PARAM_RETRY_INTERVAL_MS));
}
}
#if IS_ENABLED(CONFIG_BT_SMP)
/* BLE security level 변경 결과 확인 */
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 RX: 보안 확인 후 app parser로 전달 */
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);
}
}
/* NUS TX 완료 callback */
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,
};
/* BLE stack, security, NUS service 초기화 */
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);
ble_tx_power_init();
// 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();
bt_gatt_cb_register(&gatt_callbacks);
#if IS_ENABLED(CONFIG_BT_SMP)
err = ble_security_init();
if (err)
{
return err;
}
#endif
// NUS service 초기화
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;
}
/* 현재 모드에 맞는 advertising 시작 */
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;
}
ble_tx_power_apply_advertising();
k_work_cancel_delayable(&adv_timeout_work);
if (!advertising_unlimited) // DFU 광고 포함, 미연결 10분 시 전원 OFF
{
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;
}
/* advertising 중지, timeout work 취소 */
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;
}
/* DFU 모드 진입, SMP advertising 준비 */
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();
}
/* DFU 진입용 disconnect */
bool ble_dfu_advertising_is_enabled(void)
{
return dfu_advertising_mode;
}
/* DFU watchdog: 현재 연결 강제 disconnect */
int ble_disconnect_active(void)
{
// 연결 없으면 요청 불가
if (current_conn == NULL)
{
return -ENOTCONN;
}
return bt_conn_disconnect(current_conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN);
}
/* NUS TX: 직렬화, retry, 완료 대기 */
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)
{
// stale completion 비우기
}
//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);
// -ENOMEM만 짧게 재시도, 그 외 error는 즉시 종료
if (err != -ENOMEM || retry >= 20)
{
break;
}
// -ENOMEM : BLE TX 버퍼가 가득찬 경우: 바로 포기하지 않고 짧게 쉬었다가 다시 시도
DBG_ERR("[NUS TX] busy, retry=%d\r\n", retry + 1);
// busy 반복 시 TX power boost 판단
ble_tx_power_on_tx_busy_retry(retry + 1);
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_power_tx_stuck_ms()));
if (err)
{
// sent callback 지연도 링크 stress로 처리
ble_tx_power_on_tx_complete_timeout();
err = k_sem_take(&nus_tx_done_sem, K_MSEC(1000 - ble_tx_power_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;
// 완료 시간 기반 TX power 복구/boost 판단
ble_tx_power_on_tx_complete(tx_elapsed_ms);
}
k_mutex_unlock(&nus_tx_lock);
return err;
}
/* BLE 연결 상태 */
bool ble_is_connected(void)
{
return (current_conn != NULL);
}
/* ATT MTU used to calculate notify payload size */
uint16_t ble_current_mtu(void)
{
return current_notify_mtu;
}