/******************************************************************************* * @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 #include #include #include #include #include #include #include #if IS_ENABLED(CONFIG_SETTINGS) #include #endif #include #include #include #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); }