#include #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_nimble_hci.h" #include "nimble/nimble_port.h" #include "nimble/nimble_port_freertos.h" #include "host/ble_hs.h" #include "host/ble_hs_id.h" #include "host/ble_uuid.h" #include "host/ble_gap.h" #include "host/util/util.h" #include "host/ble_sm.h" #include "host/ble_att.h" void ble_store_config_init(void); #include "services/gap/ble_svc_gap.h" #include "state_machine.h" #include "services/gatt/ble_svc_gatt.h" #include "ble_service.h" #include "device_name.h" #include "arduino_reset.h" #include "serial_bridge.h" #include "state_machine.h" #include "binary_parser.h" #include "usb_host.h" static const char *TAG = "BLE_SVC"; static ble_data_cb_t flashing_callback = NULL; static ble_serial_cb_t serial_callback = NULL; static uint16_t flashing_attr_handle; static uint16_t serial_attr_handle; static bool ble_connected = false; static uint16_t conn_handle = 0; static uint8_t own_addr_type = 0; static const ble_uuid128_t service_uuid = BLE_UUID128_INIT( 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4F, 0x49, 0x58, 0x4C, 0x45, 0x56 ); static const ble_uuid128_t flashing_char_uuid = BLE_UUID128_INIT( 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4F, 0x49, 0x58, 0x4C, 0x45, 0x56 ); static const ble_uuid128_t serial_char_uuid = BLE_UUID128_INIT( 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4F, 0x49, 0x58, 0x4C, 0x45, 0x56 ); static int ble_gap_event_cb(struct ble_gap_event *event, void *arg); static void send_ack(uint16_t conn_handle, uint8_t cmd, uint16_t index); static int flashing_char_access_cb(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg) { if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) { /* Return current state machine state as 1 byte. * Webapp polls this after END to detect flash completion: * 0=IDLE, 1=RECEIVING, 2=VERIFYING, 3=FLASHING, * 4=SERIAL_BRIDGE (success), 5=ERROR_TARGET, 6=ERROR_CHECKSUM */ uint8_t state = (uint8_t)state_machine_get_current(); os_mbuf_append(ctxt->om, &state, 1); ESP_LOGD(TAG, "Flashing read: state=%d (%s)", state, state_machine_get_state_name()); return 0; } if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) { size_t len = OS_MBUF_PKTLEN(ctxt->om); uint8_t *data = malloc(len); if (data) { os_mbuf_copydata(ctxt->om, 0, len, data); ESP_LOGI(TAG, "Flashing write: len=%d conn=%d handle=0x%04X cmd=0x%02X", len, conn_handle, attr_handle, len > 0 ? data[0] : 0); if (flashing_callback) { flashing_callback(data, len); } free(data); } } return 0; } static int serial_char_access_cb(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg) { if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) { size_t len = OS_MBUF_PKTLEN(ctxt->om); uint8_t *data = malloc(len); if (data) { os_mbuf_copydata(ctxt->om, 0, len, data); ESP_LOGI(TAG, "Serial write: len=%d conn=%d", len, conn_handle); if (len == 1 && data[0] == CMD_RESET) { ESP_LOGI(TAG, "CMD_RESET: triggering Arduino reset"); arduino_trigger_reset(); send_ack(conn_handle, CMD_RESET, 0); } else if (len == 5 && data[0] == CMD_SET_BAUD) { uint32_t baud = (uint32_t)data[1] | ((uint32_t)data[2] << 8) | ((uint32_t)data[3] << 16) | ((uint32_t)data[4] << 24); ESP_LOGI(TAG, "CMD_SET_BAUD: %lu", (unsigned long)baud); usb_host_set_baud_rate(baud); } else { serial_bridge_on_ble_write(data, len); } free(data); } } return 0; } static const struct ble_gatt_svc_def gatt_svcs[] = { { .type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &service_uuid.u, .characteristics = (struct ble_gatt_chr_def[]) { { .uuid = &flashing_char_uuid.u, .access_cb = flashing_char_access_cb, .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_NOTIFY, .val_handle = &flashing_attr_handle }, { .uuid = &serial_char_uuid.u, .access_cb = serial_char_access_cb, .flags = BLE_GATT_CHR_F_WRITE_NO_RSP | BLE_GATT_CHR_F_NOTIFY, .val_handle = &serial_attr_handle }, { 0 } } }, { 0 } }; static void ble_restart_adv(void) { int rc; struct ble_gap_adv_params adv_params; uint8_t adv_data[31]; uint8_t adv_data_len = 0; struct ble_hs_adv_fields fields; memset(&fields, 0, sizeof(fields)); fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP; const char *name = device_name_get(); size_t name_len = strlen(name); // Check if full name fits in 31-byte adv packet // Adv fields overhead: flags(3) + name header(2) = ~5 bytes overhead for name // So name content needs ≤ 26 bytes max if (name_len <= 24) { // safe margin under 31-byte limit fields.name = (uint8_t *)name; fields.name_len = name_len; fields.name_is_complete = 1; } else { // Truncate to short prefix for advertising fields.name = (uint8_t *)"Velxio"; fields.name_len = 6; fields.name_is_complete = 0; } fields.uuids16 = NULL; fields.num_uuids16 = 0; rc = ble_hs_adv_set_fields(&fields, adv_data, &adv_data_len, sizeof(adv_data)); if (rc != 0) { ESP_LOGE(TAG, "ble_hs_adv_set_fields rc=%d", rc); return; } rc = ble_gap_adv_set_data(adv_data, adv_data_len); if (rc != 0) { ESP_LOGE(TAG, "ble_gap_adv_set_data rc=%d", rc); return; } memset(&adv_params, 0, sizeof(adv_params)); 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, ble_gap_event_cb, NULL); if (rc == 0) { ESP_LOGI(TAG, "BLE advertising started"); } else { ESP_LOGE(TAG, "ble_gap_adv_start rc=%d", rc); } } static int ble_gap_event_cb(struct ble_gap_event *event, void *arg) { switch (event->type) { case BLE_GAP_EVENT_CONNECT: conn_handle = event->connect.conn_handle; ble_connected = true; ESP_LOGI(TAG, "BLE connected, handle=%d", conn_handle); break; case BLE_GAP_EVENT_DISCONNECT: conn_handle = 0; ble_connected = false; ESP_LOGI(TAG, "BLE disconnected"); state_machine_process_event(EVENT_BLE_DISCONNECT, NULL); ble_restart_adv(); break; case BLE_GAP_EVENT_ADV_COMPLETE: ESP_LOGI(TAG, "ADV_COMPLETE, reason=%d", event->adv_complete.reason); ble_restart_adv(); break; case BLE_GAP_EVENT_SUBSCRIBE: ESP_LOGI(TAG, "Subscribe attr_handle=%d notify=%d indicate=%d prev_notify=%d prev_indicate=%d", event->subscribe.attr_handle, event->subscribe.cur_notify, event->subscribe.cur_indicate, event->subscribe.prev_notify, event->subscribe.prev_indicate); break; default: break; } return 0; } static void ble_on_reset(int reason) { ESP_LOGE(TAG, "NimBLE host reset, reason=%d", reason); } static void ble_on_sync(void) { int rc; rc = ble_hs_util_ensure_addr(1); if (rc != 0) { ESP_LOGE(TAG, "ble_hs_util_ensure_addr failed: %d", rc); return; } rc = ble_hs_id_infer_auto(0, &own_addr_type); if (rc != 0) { ESP_LOGE(TAG, "ble_hs_id_infer_auto failed: %d", rc); return; } ESP_LOGI(TAG, "Address type: %d", own_addr_type); rc = ble_att_set_preferred_mtu(255); if (rc == 0) { ESP_LOGI(TAG, "Preferred MTU set: 255"); } else { ESP_LOGW(TAG, "ble_att_set_preferred_mtu failed: %d", rc); } ble_restart_adv(); } static void host_task(void *param) { nimble_port_run(); } void ble_service_init(void) { esp_nimble_hci_init(); nimble_port_init(); ble_hs_cfg.reset_cb = ble_on_reset; ble_hs_cfg.sync_cb = ble_on_sync; ble_hs_cfg.sm_io_cap = BLE_HS_IO_NO_INPUT_OUTPUT; ble_hs_cfg.sm_sc = 0; ble_svc_gap_init(); ble_svc_gatt_init(); ble_gatts_count_cfg(gatt_svcs); ble_gatts_add_svcs(gatt_svcs); ble_svc_gap_device_name_set(device_name_get()); ble_svc_gap_device_appearance_set(0x0080); ble_store_config_init(); nimble_port_freertos_init(host_task); ESP_LOGI(TAG, "BLE service initialized"); } void ble_service_set_flashing_callback(ble_data_cb_t cb) { flashing_callback = cb; } void ble_service_set_serial_callback(ble_serial_cb_t cb) { serial_callback = cb; } void ble_service_send_notify_flashing(uint8_t *data, size_t len) { if (!ble_connected) return; struct os_mbuf *om = NULL; for (int retry = 0; retry < 3; retry++) { om = ble_hs_mbuf_from_flat(data, len); if (om) break; ESP_LOGW(TAG, "flashing mbuf alloc failed (retry %d/3)", retry + 1); vTaskDelay(1); } if (!om) { ESP_LOGE(TAG, "flashing ACK DROPPED: mbuf pool exhausted after 3 retries"); return; } int rc = ble_gatts_notify_custom(conn_handle, flashing_attr_handle, om); if (rc != 0) { ESP_LOGW(TAG, "flashing notify failed: rc=%d handle=0x%04X conn=%d", rc, flashing_attr_handle, conn_handle); } } void ble_service_send_notify_serial(uint8_t *data, size_t len) { if (!ble_connected) return; struct os_mbuf *om = ble_hs_mbuf_from_flat(data, len); if (om) { int rc = ble_gatts_notify_custom(conn_handle, serial_attr_handle, om); if (rc != 0) { ESP_LOGW(TAG, "serial notify failed: rc=%d handle=0x%04X conn=%d", rc, serial_attr_handle, conn_handle); } } } static void send_ack(uint16_t conn_handle, uint8_t cmd, uint16_t index) { uint8_t ack[4] = { CMD_ACK, cmd, (uint8_t)(index & 0xFF), (uint8_t)((index >> 8) & 0xFF) }; ble_service_send_notify_flashing(ack, sizeof(ack)); ESP_LOGI(TAG, "ACK sent: cmd=0x%02X index=%u", cmd, index); } bool ble_service_is_connected(void) { return ble_connected; }