Project ESP-IDF hoàn chỉnh dùng NimBLE, HLP GATT service và notification để nhận dữ liệu Waze.
Ví dụ ESP-IDF 5.5.x này dành cho ESP32-C3/C6/H2/S3. main.c dựng HLP GATT service, nhận raw state từ Android qua TX write và in state hợp lệ trong serial monitor. Chiều RX notification chỉ dùng cho handshake dev/pong; project không kèm renderer màn hình.
cd esp32-hlp-ble
idf.py set-target esp32s3
idf.py build
idf.py -p COM5 flash monitor
Thay COM5 bằng cổng serial của board. Sau khi flash, chọn đúng transport và thiết bị trong Cài đặt Mod → Thiết bị → HUD Link.
esp32-hlp-ble/CMakeLists.txt
esp32-hlp-ble/sdkconfig.defaults
esp32-hlp-ble/main/CMakeLists.txt
esp32-hlp-ble/main/main.c
CMakeLists.txtcmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(esp32_hlp_ble)
sdkconfig.defaults# ESP-IDF 5.5.x: enable the NimBLE host and BLE-only controller mode.
CONFIG_BT_ENABLED=y
CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y
CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=n
CONFIG_BTDM_CTRL_MODE_BTDM=n
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
main/CMakeLists.txtidf_component_register(
SRCS "main.c" "../../esp32-hlp-spp/shared/hlp_core.c"
"../../esp32-hlp-spp/shared/hlp_messages.c"
"../../esp32-hlp-spp/shared/hlp_device_config.c"
INCLUDE_DIRS "." "../../esp32-hlp-spp/shared"
REQUIRES bt nvs_flash json
)
main/main.c#include "host/ble_att.h"
#include "host/ble_hs.h"
#include "host/ble_uuid.h"
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "host/ble_hs_id.h"
#include "services/gap/ble_svc_gap.h"
#include "services/gatt/ble_svc_gatt.h"
#include "esp_log.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include "hlp_core.h"
#include "hlp_messages.h"
#include "hlp_device_config.h"
#include <string.h>
static const char *TAG = "hlp_ble";
static const ble_uuid128_t SERVICE =
BLE_UUID128_INIT(0x01,0x00,0x4c,0x50,0x4c,0x48,0x9d,0x9a,
0x48,0x4c,0x6e,0x4d,0x01,0x00,0x7e,0x8a);
static const ble_uuid128_t TX =
BLE_UUID128_INIT(0x01,0x00,0x4c,0x50,0x4c,0x48,0x9d,0x9a,
0x48,0x4c,0x6e,0x4d,0x02,0x00,0x7e,0x8a);
static const ble_uuid128_t RX =
BLE_UUID128_INIT(0x01,0x00,0x4c,0x50,0x4c,0x48,0x9d,0x9a,
0x48,0x4c,0x6e,0x4d,0x03,0x00,0x7e,0x8a);
static const ble_uuid128_t CAPABILITIES =
BLE_UUID128_INIT(0x01,0x00,0x4c,0x50,0x4c,0x48,0x9d,0x9a,
0x48,0x4c,0x6e,0x4d,0x04,0x00,0x7e,0x8a);
typedef struct { uint16_t length; uint8_t bytes[HLP_MAX_FRAME]; } ble_chunk_t;
static QueueHandle_t chunks;
static hlp_rx_t receiver;
static uint16_t connection = BLE_HS_CONN_HANDLE_NONE;
static uint16_t rx_value_handle;
static bool notify_enabled;
static volatile bool send_dev_pending;
static uint8_t own_addr_type;
static struct ble_gap_adv_params adv_params;
static void send_line(const char *line, void *user) {
(void)user;
if (!notify_enabled || connection == BLE_HS_CONN_HANDLE_NONE) return;
size_t length = strlen(line);
if (length + 1 > HLP_MAX_FRAME) return;
char frame[HLP_MAX_FRAME];
memcpy(frame, line, length);
frame[length++] = '\n';
uint16_t mtu = ble_att_mtu(connection);
size_t payload = mtu > 3 ? mtu - 3 : BLE_ATT_MTU_DFLT - 3;
for (size_t offset = 0; offset < length; offset += payload) {
size_t chunk_length = length - offset;
if (chunk_length > payload) chunk_length = payload;
int rc = BLE_HS_ENOMEM;
for (int attempt = 0; attempt < 3 && rc == BLE_HS_ENOMEM; ++attempt) {
struct os_mbuf *om = ble_hs_mbuf_from_flat(frame + offset, chunk_length);
if (!om) {
rc = BLE_HS_ENOMEM;
} else {
rc = ble_gatts_notify_custom(connection, rx_value_handle, om);
}
if (rc == BLE_HS_ENOMEM) vTaskDelay(pdMS_TO_TICKS(5));
}
if (rc != 0) {
ESP_LOGW(TAG, "RX notification failed: rc=%d", rc);
return;
}
}
}
static void state_line(const char *line, size_t length, void *user) {
(void)user;
ESP_LOGI(TAG, "HLP state: %.*s", (int)length, line);
}
static void on_line(const char *line, size_t length, void *user) {
(void)user;
hlp_handle_line(line, length, send_line, state_line, NULL);
}
static int access_cb(uint16_t conn_handle, uint16_t attr_handle,
struct ble_gatt_access_ctxt *ctxt, void *arg) {
(void)attr_handle; (void)arg;
if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
size_t length = OS_MBUF_PKTLEN(ctxt->om);
if (length > HLP_MAX_FRAME) return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
ble_chunk_t chunk = { .length = length };
if (os_mbuf_copydata(ctxt->om, 0, length, chunk.bytes) != 0)
return BLE_ATT_ERR_UNLIKELY;
if (xQueueSend(chunks, &chunk, 0) != pdTRUE)
return BLE_ATT_ERR_INSUFFICIENT_RES;
return 0;
}
if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) {
const char caps[] = "{\"v\":1,\"caps\":{\"transport\":\"ble\",\"maxFrame\":512}}\n";
return os_mbuf_append(ctxt->om, caps, sizeof(caps) - 1) == 0
? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
}
(void)conn_handle;
return BLE_ATT_ERR_UNLIKELY;
}
static struct ble_gatt_chr_def characteristics[] = {
{ .uuid = &TX.u, .access_cb = access_cb,
.flags = BLE_GATT_CHR_F_WRITE },
{ .uuid = &RX.u, .val_handle = &rx_value_handle,
.flags = BLE_GATT_CHR_F_NOTIFY },
{ .uuid = &CAPABILITIES.u, .access_cb = access_cb,
.flags = BLE_GATT_CHR_F_READ },
{ 0 }
};
static const struct ble_gatt_svc_def services[] = {
{ .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &SERVICE.u,
.characteristics = characteristics },
{ 0 }
};
static int gap_event(struct ble_gap_event *event, void *arg) {
(void)arg;
switch (event->type) {
case BLE_GAP_EVENT_CONNECT:
if (event->connect.status != 0) {
connection = BLE_HS_CONN_HANDLE_NONE;
ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER,
&adv_params, gap_event, NULL);
} else connection = event->connect.conn_handle;
break;
case BLE_GAP_EVENT_DISCONNECT:
connection = BLE_HS_CONN_HANDLE_NONE;
notify_enabled = false;
ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER,
&adv_params, gap_event, NULL);
break;
case BLE_GAP_EVENT_SUBSCRIBE:
if (event->subscribe.attr_handle == rx_value_handle) {
notify_enabled = event->subscribe.cur_notify;
if (notify_enabled) send_dev_pending = true;
}
break;
default: break;
}
return 0;
}
static void advertise(void) {
struct ble_hs_adv_fields fields = {0};
fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
fields.name = (uint8_t *)"WazeHUD";
fields.name_len = 7;
fields.name_is_complete = 1;
fields.uuids128 = (ble_uuid128_t *)&SERVICE;
fields.num_uuids128 = 1;
fields.uuids128_is_complete = 1;
int rc = ble_gap_adv_set_fields(&fields);
if (rc != 0) {
ESP_LOGE(TAG, "Cannot set advertising fields: rc=%d", rc);
return;
}
adv_params = (struct ble_gap_adv_params){0};
adv_params.conn_mode = BLE_GAP_CONN_MODE_UND;
adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN;
rc = ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER,
&adv_params, gap_event, NULL);
if (rc != 0) ESP_LOGE(TAG, "Cannot start advertising: rc=%d", rc);
}
static void on_sync(void) {
int rc = ble_hs_id_infer_auto(0, &own_addr_type);
if (rc != 0) {
ESP_LOGE(TAG, "Cannot infer address type: rc=%d", rc);
return;
}
advertise();
}
static void host_task(void *arg) {
(void)arg;
nimble_port_run();
nimble_port_freertos_deinit();
}
static void protocol_task(void *arg) {
(void)arg;
ble_chunk_t chunk;
for (;;) {
if (send_dev_pending) {
send_dev_pending = false;
hlp_send_dev(send_line, NULL, "ble", "ESP32-BLE", "1.0.0", 4);
}
if (xQueueReceive(chunks, &chunk, pdMS_TO_TICKS(100)) == pdTRUE
&& chunk.length > 0)
hlp_rx_feed(&receiver, chunk.bytes, chunk.length);
}
}
void app_main(void) {
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init();
}
ESP_ERROR_CHECK(err);
hlp_device_config_init();
chunks = xQueueCreate(16, sizeof(ble_chunk_t));
if (!chunks) {
ESP_LOGE(TAG, "Cannot create receive queue");
return;
}
hlp_rx_init(&receiver, on_line, NULL);
ESP_ERROR_CHECK(nimble_port_init());
ble_svc_gap_init();
ble_svc_gatt_init();
int rc = ble_gatts_count_cfg(services);
if (rc == 0) rc = ble_gatts_add_svcs(services);
if (rc == 0) rc = ble_svc_gap_device_name_set("WazeHUD");
if (rc != 0) {
ESP_LOGE(TAG, "Cannot configure GATT server: rc=%d", rc);
return;
}
ble_hs_cfg.sync_cb = on_sync;
if (xTaskCreate(protocol_task, "hlp_protocol", 4096, NULL, 5, NULL) != pdPASS) {
ESP_LOGE(TAG, "Cannot create protocol task");
return;
}
nimble_port_freertos_init(host_task);
ESP_LOGI(TAG, "HLP BLE service started");
}