diff --git a/.github/workflows/esp-idf.yaml b/.github/workflows/esp-idf.yaml index f13f299..1f4f314 100644 --- a/.github/workflows/esp-idf.yaml +++ b/.github/workflows/esp-idf.yaml @@ -2,9 +2,9 @@ name: ESP-IDF on: push: - branches: ["master"] + branches: ["*"] pull_request: - branches: ["master"] + branches: ["*"] jobs: build: @@ -26,4 +26,4 @@ jobs: with: esp_idf_version: v5.2.2 target: ${{matrix.target}} - path: '.' + path: '.' \ No newline at end of file diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 036aff2..5f984b7 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,12 +1,34 @@ set(JAC_ESP32_VERSION "0.0.17") +# Base source files +set(COMPONENT_SRCS + "main.cpp" + "platform/espWifi.cpp" + "platform/espNvsKeyValue.cpp" + "espFeatures/gridui/gridUiFeature.cpp" + "espFeatures/gridui/widgets/_common.cpp" +) + +# Base requirements +set(COMPONENT_REQUIRES + jac-dcore jac-machine jac-link + driver pthread spiffs vfs fatfs + SmartLeds esp_timer Esp32-RBGridUI +) + +# Conditionally add BLE support +if(CONFIG_JAC_ESP32_ENABLE_BLE) + list(APPEND COMPONENT_SRCS "util/bleStream.cpp") + list(APPEND COMPONENT_REQUIRES "bt") + message(STATUS "BLE Stream support enabled") +else() + message(STATUS "BLE Stream support disabled") +endif() + idf_component_register( - SRCS "main.cpp" "platform/espWifi.cpp" "platform/espNvsKeyValue.cpp" - "espFeatures/gridui/gridUiFeature.cpp" "espFeatures/gridui/widgets/_common.cpp" + SRCS ${COMPONENT_SRCS} INCLUDE_DIRS "" - REQUIRES jac-dcore jac-machine jac-link - driver pthread spiffs vfs fatfs - SmartLeds esp_timer Esp32-RBGridUI + REQUIRES ${COMPONENT_REQUIRES} ) target_compile_definitions(${COMPONENT_LIB} PRIVATE JAC_ESP32_VERSION="${JAC_ESP32_VERSION}") diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild new file mode 100644 index 0000000..9c8197c --- /dev/null +++ b/main/Kconfig.projbuild @@ -0,0 +1,69 @@ +menu "Jaculus ESP32 Configuration" + + config JAC_ESP32_ENABLE_BLE + bool "Enable BLE Stream Support" + default y + help + Enable BLE (Bluetooth Low Energy) stream support for wireless communication. + This allows the ESP32 to act as a BLE GATT server that can be used + as a communication transport alongside UART, TCP, and JTAG streams. + + When enabled: + - BLE stack will be initialized + - GATT server with custom service will be created + - Device will be discoverable as "ESP32_JAC_BLE_XXXX" (XXXX = MAC suffix) + - BLE stream will be available on mux channel 4 + + When disabled: + - BLE functionality is completely removed from build + - Reduces binary size and memory usage + - BT component dependency is optional + + config JAC_ESP32_BLE_DEVICE_NAME + string "BLE Device Name Prefix" + depends on JAC_ESP32_ENABLE_BLE + default "ESP32_JAC_BLE" + help + The prefix for the BLE device name. The actual device name will be + this prefix followed by the last 2 bytes of the MAC address. + For example: "ESP32_JAC_BLE_A1B2" + + config JAC_ESP32_BLE_SERVICE_UUID + hex "BLE GATT Service UUID (16-bit)" + depends on JAC_ESP32_ENABLE_BLE + default 0x00FF + range 0x0001 0xFFFE + help + The 16-bit UUID for the custom BLE GATT service. + Default is 0x00FF (255 in decimal). + + config JAC_ESP32_BLE_CHARACTERISTIC_UUID + hex "BLE GATT Characteristic UUID (16-bit)" + depends on JAC_ESP32_ENABLE_BLE + default 0xFF01 + range 0x0001 0xFFFE + help + The 16-bit UUID for the BLE GATT characteristic used for data transfer. + Default is 0xFF01 (65281 in decimal). + + config JAC_ESP32_BLE_MTU_SIZE + int "BLE MTU Size" + depends on JAC_ESP32_ENABLE_BLE + default 500 + range 23 512 + help + Maximum Transmission Unit (MTU) size for BLE communication. + Larger values allow more data per packet but may not be supported + by all devices. Default is 500 bytes. + + config JAC_ESP32_BLE_DEBUG_LOGS + bool "Enable BLE Debug Logging" + depends on JAC_ESP32_ENABLE_BLE + default n + help + Enable detailed debug logging for BLE operations. + This will show step-by-step initialization, connection events, + and data transfer details. Useful for debugging but increases + log output significantly. + +endmenu diff --git a/main/main.cpp b/main/main.cpp index a73b6d6..b302bc1 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -34,6 +34,9 @@ #include "util/uartStream.h" #include "util/tcpStream.h" +#ifdef CONFIG_JAC_ESP32_ENABLE_BLE +#include "util/bleStream.h" +#endif #include "resources/resources.h" @@ -124,6 +127,9 @@ jac::Device device( using Mux_t = jac::Mux; std::unique_ptr muxUart; std::unique_ptr muxTcp; +#ifdef CONFIG_JAC_ESP32_ENABLE_BLE +std::unique_ptr muxBle; +#endif #if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) std::unique_ptr muxJtag; @@ -214,6 +220,17 @@ int main() { muxTcp->bindRx(std::make_unique(std::move(handleTcp))); } +#ifdef CONFIG_JAC_ESP32_ENABLE_BLE + // initialize BLE stream + auto bleStream = std::make_unique("ESP32_JAC_BLE"); + bleStream->start(); + + muxBle = std::make_unique(std::move(bleStream)); + muxBle->setErrorHandler(reportMuxError); + auto handleBle = device.router().subscribeTx(4, *muxBle); + muxBle->bindRx(std::make_unique(std::move(handleBle))); +#endif + device.onConfigureMachine([&](Machine &machine) { device.machineIO().in->clear(); diff --git a/main/resources/CMakeLists.txt b/main/resources/CMakeLists.txt index 437a85f..99ef5a0 100644 --- a/main/resources/CMakeLists.txt +++ b/main/resources/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.0) +cmake_minimum_required(VERSION 3.10) set(ts_examples_dir ${CMAKE_CURRENT_SOURCE_DIR}/../../ts-examples) set(ts_examples_tgz ts_examples.tar.gz) diff --git a/main/util/bleStream.cpp b/main/util/bleStream.cpp new file mode 100644 index 0000000..ebec972 --- /dev/null +++ b/main/util/bleStream.cpp @@ -0,0 +1,410 @@ +#include "bleStream.h" +#include + +// Static member initialization +esp_ble_adv_data_t BleStream::adv_data = {}; +esp_ble_adv_params_t BleStream::adv_params = { + .adv_int_min = 0x20, + .adv_int_max = 0x40, + .adv_type = ADV_TYPE_IND, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .peer_addr = {}, + .peer_addr_type = BLE_ADDR_TYPE_PUBLIC, + .channel_map = ADV_CHNL_ALL, + .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, +}; + +void BleStream::start() { + if (_isInitialized) { + return; + } + + BLE_LOG_INFO("Starting BLE Stream initialization..."); + esp_err_t ret; + + // Release Classic BT memory (only if not already released) + static bool bt_mem_released = false; + if (!bt_mem_released) { + ret = esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); + if (ret == ESP_OK) { + bt_mem_released = true; + } else { + BLE_LOG_ERROR("Failed to release Classic BT memory: " + std::string(esp_err_to_name(ret))); + } + } + + // Initialize BT controller + esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); + ret = esp_bt_controller_init(&bt_cfg); + if (ret) { + BLE_LOG_ERROR("BLE controller init failed: " + std::string(esp_err_to_name(ret))); + return; + } + + ret = esp_bt_controller_enable(ESP_BT_MODE_BLE); + if (ret) { + BLE_LOG_ERROR("BLE controller enable failed: " + std::string(esp_err_to_name(ret))); + return; + } + + // Initialize Bluedroid + ret = esp_bluedroid_init(); + if (ret) { + BLE_LOG_ERROR("Bluedroid init failed: " + std::string(esp_err_to_name(ret))); + return; + } + + ret = esp_bluedroid_enable(); + if (ret) { + BLE_LOG_ERROR("Bluedroid enable failed: " + std::string(esp_err_to_name(ret))); + return; + } + + // Register callbacks + ret = esp_ble_gatts_register_callback(gatts_event_handler); + if (ret) { + BLE_LOG_ERROR("GATTS register callback failed: " + std::string(esp_err_to_name(ret))); + return; + } + + ret = esp_ble_gap_register_callback(gap_event_handler); + if (ret) { + BLE_LOG_ERROR("GAP register callback failed: " + std::string(esp_err_to_name(ret))); + return; + } + + // Register GATT application + ret = esp_ble_gatts_app_register(0); + if (ret) { + BLE_LOG_ERROR("GATTS app register failed: " + std::string(esp_err_to_name(ret))); + return; + } + + // Set MTU + esp_err_t local_mtu_ret = esp_ble_gatt_set_local_mtu(MAX_CHUNK_SIZE); + if (local_mtu_ret) { + BLE_LOG_ERROR("Set local MTU failed: " + std::string(esp_err_to_name(local_mtu_ret))); + } + + _isInitialized = true; + BLE_LOG_INFO("BLE Stream initialized successfully"); +} + +bool BleStream::put(uint8_t c) { + std::array arr{c}; + return write(std::span(arr)) == 1; +} + +size_t BleStream::write(std::span data) { + if (!_isConnected || !_notifyEnabled) { + return data.size(); // Pretend write succeeded when not connected + } + + size_t written = 0; + while (written < data.size()) { + size_t chunkSize = std::min(MAX_CHUNK_SIZE, data.size() - written); + + esp_err_t ret = esp_ble_gatts_send_indicate( + gatts_if, + conn_id, + char_handle, + chunkSize, + const_cast(data.data() + written), + false // notification, not indication + ); + + if (ret != ESP_OK) { + BLE_LOG_ERROR("BLE notification failed: " + std::string(esp_err_to_name(ret))); + break; + } + + written += chunkSize; + + // Small delay to avoid overwhelming the BLE stack + if (written < data.size()) { + vTaskDelay(pdMS_TO_TICKS(1)); + } + } + + return written; +} + +int BleStream::get() { + std::lock_guard lock(_bufferMutex); + if (_buffer.empty()) { + return -1; + } + uint8_t c = _buffer.front(); + _buffer.pop_front(); + return c; +} + +size_t BleStream::read(std::span data) { + std::lock_guard lock(_bufferMutex); + size_t len = std::min(data.size(), _buffer.size()); + std::copy_n(_buffer.begin(), len, data.begin()); + _buffer.erase(_buffer.begin(), _buffer.begin() + len); + return len; +} + +bool BleStream::flush() { + return true; // BLE notifications are sent immediately +} + +void BleStream::onData(std::function callback) { + _onData = callback; +} + +void BleStream::cleanup() { + if (_isInitialized) { + esp_bluedroid_disable(); + esp_bluedroid_deinit(); + esp_bt_controller_disable(); + esp_bt_controller_deinit(); + _isInitialized = false; + } +} + +void BleStream::addReceivedData(const uint8_t* data, size_t len) { + { + std::lock_guard lock(_bufferMutex); + _buffer.insert(_buffer.end(), data, data + len); + } + + if (_onData) { + _onData(); + } +} + +std::string BleStream::generateDeviceName() { + uint8_t mac[6]; + esp_err_t ret = esp_read_mac(mac, ESP_MAC_WIFI_STA); + if (ret != ESP_OK) { + BLE_LOG_ERROR("Failed to read MAC address: " + std::string(esp_err_to_name(ret))); + return std::string(DEVICE_NAME_PREFIX); + } + + // Use last 2 bytes of MAC address for uniqueness + char name_buffer[32]; + snprintf(name_buffer, sizeof(name_buffer), "%s_%02X%02X", + DEVICE_NAME_PREFIX, mac[4], mac[5]); + return std::string(name_buffer); +} + +void BleStream::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + switch (event) { + case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: + esp_ble_gap_start_advertising(&adv_params); + break; + case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: + if (param->adv_start_cmpl.status != ESP_BT_STATUS_SUCCESS) { + BLE_LOG_ERROR("Advertising start failed with status: " + std::to_string(param->adv_start_cmpl.status)); + } else { + BLE_LOG_INFO("BLE advertising started successfully! Device should be discoverable now."); + } + break; + case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: + if (param->adv_stop_cmpl.status != ESP_BT_STATUS_SUCCESS) { + BLE_LOG_ERROR("Advertising stop failed"); + } + break; + default: + break; + } +} + +void BleStream::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if_param, esp_ble_gatts_cb_param_t *param) { + if (!instance) return; + + switch (event) { + case ESP_GATTS_REG_EVT: + { + gatts_if = gatts_if_param; + + // Generate device name with MAC address + std::string device_name = generateDeviceName(); + esp_err_t set_dev_name_ret = esp_ble_gap_set_device_name(device_name.c_str()); + if (set_dev_name_ret) { + BLE_LOG_ERROR("Set device name failed: " + std::string(esp_err_to_name(set_dev_name_ret))); + } + + // Configure advertising data + esp_ble_adv_data_t adv_data = { + .set_scan_rsp = false, + .include_name = true, + .include_txpower = true, + .min_interval = 0x0006, + .max_interval = 0x0010, + .appearance = 0x00, + .manufacturer_len = 0, + .p_manufacturer_data = nullptr, + .service_data_len = 0, + .p_service_data = nullptr, + .service_uuid_len = 0, + .p_service_uuid = nullptr, + .flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT), + }; + + esp_err_t ret = esp_ble_gap_config_adv_data(&adv_data); + if (ret) { + BLE_LOG_ERROR("Config adv data failed: " + std::string(esp_err_to_name(ret))); + } + + // Create service + esp_gatt_srvc_id_t service_id; + service_id.is_primary = true; + service_id.id.inst_id = 0x00; + service_id.id.uuid.len = ESP_UUID_LEN_16; + service_id.id.uuid.uuid.uuid16 = GATTS_SERVICE_UUID; + esp_err_t create_ret = esp_ble_gatts_create_service(gatts_if, &service_id, GATTS_NUM_HANDLE); + if (create_ret) { + BLE_LOG_ERROR("Create service failed: " + std::string(esp_err_to_name(create_ret))); + } + break; + } + + case ESP_GATTS_CREATE_EVT: + { + service_handle = param->create.service_handle; + + esp_err_t start_ret = esp_ble_gatts_start_service(service_handle); + if (start_ret) { + BLE_LOG_ERROR("Start service failed: " + std::string(esp_err_to_name(start_ret))); + } + + // Add characteristic + esp_bt_uuid_t char_uuid = { + .len = ESP_UUID_LEN_16, + .uuid = {.uuid16 = GATTS_CHAR_UUID} + }; + + esp_gatt_char_prop_t char_property = ESP_GATT_CHAR_PROP_BIT_READ | + ESP_GATT_CHAR_PROP_BIT_WRITE | + ESP_GATT_CHAR_PROP_BIT_NOTIFY; + + esp_err_t add_char_ret = esp_ble_gatts_add_char(service_handle, &char_uuid, + ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE, + char_property, nullptr, nullptr); + if (add_char_ret) { + BLE_LOG_ERROR("Add characteristic failed: " + std::string(esp_err_to_name(add_char_ret))); + } + break; + } + + case ESP_GATTS_ADD_CHAR_EVT: + { + char_handle = param->add_char.attr_handle; + + // Add descriptor for notifications + esp_bt_uuid_t descr_uuid = { + .len = ESP_UUID_LEN_16, + .uuid = {.uuid16 = GATTS_DESCR_UUID} + }; + + esp_err_t add_descr_ret = esp_ble_gatts_add_char_descr(service_handle, &descr_uuid, + ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE, + nullptr, nullptr); + if (add_descr_ret) { + BLE_LOG_ERROR("Add descriptor failed: " + std::string(esp_err_to_name(add_descr_ret))); + } + break; + } + + case ESP_GATTS_ADD_CHAR_DESCR_EVT: + { + descr_handle = param->add_char_descr.attr_handle; + // Now that everything is set up, start advertising + esp_err_t adv_start_ret = esp_ble_gap_start_advertising(&adv_params); + if (adv_start_ret) { + BLE_LOG_ERROR("Failed to start advertising: " + std::string(esp_err_to_name(adv_start_ret))); + } + break; + } + + case ESP_GATTS_START_EVT: + break; + + case ESP_GATTS_CONNECT_EVT: + { + BLE_LOG_INFO("BLE client connected"); + conn_id = param->connect.conn_id; + instance->_isConnected = true; + + // Update connection parameters + esp_ble_conn_update_params_t conn_params = {}; + memcpy(conn_params.bda, param->connect.remote_bda, sizeof(esp_bd_addr_t)); + conn_params.latency = 0; + conn_params.max_int = 0x20; + conn_params.min_int = 0x10; + conn_params.timeout = 400; + esp_ble_gap_update_conn_params(&conn_params); + break; + } + + case ESP_GATTS_DISCONNECT_EVT: + BLE_LOG_INFO("BLE client disconnected"); + instance->_isConnected = false; + instance->_notifyEnabled = false; + esp_ble_gap_start_advertising(&adv_params); + break; + + case ESP_GATTS_WRITE_EVT: + { + if (param->write.handle == descr_handle && param->write.len == 2) { + // Client Characteristic Configuration descriptor written + uint16_t descr_value = param->write.value[1] << 8 | param->write.value[0]; + if (descr_value == 0x0001) { + BLE_LOG_INFO("BLE notifications enabled"); + instance->_notifyEnabled = true; + } else { + instance->_notifyEnabled = false; + } + } else if (param->write.handle == char_handle) { + // Data written to characteristic + instance->addReceivedData(param->write.value, param->write.len); + } + + if (param->write.need_rsp) { + esp_ble_gatts_send_response(gatts_if, param->write.conn_id, + param->write.trans_id, ESP_GATT_OK, nullptr); + } + break; + } + + case ESP_GATTS_READ_EVT: + { + // Respond to read requests with empty data + esp_gatt_rsp_t rsp = {}; + rsp.attr_value.handle = param->read.handle; + rsp.attr_value.len = 0; + rsp.attr_value.value[0] = 0x00; // Set at least one byte + + esp_err_t rsp_err = esp_ble_gatts_send_response(gatts_if, param->read.conn_id, + param->read.trans_id, ESP_GATT_OK, &rsp); + if (rsp_err != ESP_OK) { + BLE_LOG_ERROR("Failed to send read response: " + std::string(esp_err_to_name(rsp_err))); + } + break; + } + + // Handle other events without error + case ESP_GATTS_EXEC_WRITE_EVT: + case ESP_GATTS_MTU_EVT: + case ESP_GATTS_CONF_EVT: + case ESP_GATTS_UNREG_EVT: + case ESP_GATTS_ADD_INCL_SRVC_EVT: + case ESP_GATTS_DELETE_EVT: + case ESP_GATTS_STOP_EVT: + case ESP_GATTS_OPEN_EVT: + case ESP_GATTS_CANCEL_OPEN_EVT: + case ESP_GATTS_CLOSE_EVT: + case ESP_GATTS_LISTEN_EVT: + case ESP_GATTS_CONGEST_EVT: + case ESP_GATTS_RESPONSE_EVT: + case ESP_GATTS_CREAT_ATTR_TAB_EVT: + case ESP_GATTS_SET_ATTR_VAL_EVT: + case ESP_GATTS_SEND_SERVICE_CHANGE_EVT: + default: + break; + } +} diff --git a/main/util/bleStream.h b/main/util/bleStream.h new file mode 100644 index 0000000..3087a17 --- /dev/null +++ b/main/util/bleStream.h @@ -0,0 +1,145 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_bt.h" +#include "esp_bt_main.h" +#include "esp_gap_ble_api.h" +#include "esp_gatts_api.h" +#include "esp_bt_defs.h" +#include "esp_gatt_common_api.h" +#include "esp_system.h" +#include "esp_mac.h" + +// BLE Debug Logging Control +// Can be controlled via Kconfig (menuconfig) or by defining BLE_STREAM_DEBUG_LOGS +#if defined(CONFIG_JAC_ESP32_BLE_DEBUG_LOGS) || defined(BLE_STREAM_DEBUG_LOGS) + #define BLE_LOG_DEBUG(msg) jac::Logger::debug(msg) + #define BLE_LOG_ERROR(msg) jac::Logger::error(msg) + #define BLE_LOG_INFO(msg) jac::Logger::debug(msg) // Use debug for info when enabled +#else + #define BLE_LOG_DEBUG(msg) ((void)0) + #define BLE_LOG_ERROR(msg) jac::Logger::error(msg) // Keep errors always visible + #define BLE_LOG_INFO(msg) ((void)0) // Disable info messages when debugging is off +#endif + +/** + * @brief BLE Stream implementation for ESP32 + * + * Provides a BLE GATT server that can be used as a duplex stream for communication. + * Integrates with the Jaculus-esp32 project as an additional transport method. + */ +class BleStream : public jac::Duplex { +private: + // Member variables + std::function _onData; + std::deque _buffer; + std::mutex _bufferMutex; + std::atomic _isConnected{false}; + std::atomic _notifyEnabled{false}; + std::atomic _isInitialized{false}; + + // BLE configuration constants + static constexpr uint16_t GATTS_SERVICE_UUID = CONFIG_JAC_ESP32_BLE_SERVICE_UUID; + static constexpr uint16_t GATTS_CHAR_UUID = CONFIG_JAC_ESP32_BLE_CHARACTERISTIC_UUID; + static constexpr uint16_t GATTS_DESCR_UUID = 0x2902; // Client Characteristic Configuration + static constexpr uint16_t GATTS_NUM_HANDLE = 4; + static constexpr const char* DEVICE_NAME_PREFIX = CONFIG_JAC_ESP32_BLE_DEVICE_NAME; + static constexpr size_t MAX_CHUNK_SIZE = CONFIG_JAC_ESP32_BLE_MTU_SIZE; // Configurable MTU size + + // BLE handles - shared across all instances + static inline esp_gatt_if_t gatts_if = ESP_GATT_IF_NONE; + static inline uint16_t service_handle = 0; + static inline uint16_t char_handle = 0; + static inline uint16_t descr_handle = 0; + static inline uint16_t conn_id = 0; + + // Global instance pointer for callbacks (singleton pattern) + static inline BleStream* instance = nullptr; + + // BLE advertising configuration + static esp_ble_adv_data_t adv_data; + static esp_ble_adv_params_t adv_params; + +public: + /** + * @brief Constructor + * @param deviceName Device name for BLE advertising (currently unused, uses DEVICE_NAME constant) + * @throws std::runtime_error if another instance already exists (singleton pattern) + */ + explicit BleStream(const std::string& deviceName = "BLE_STREAM") { + if (instance != nullptr) { + throw std::runtime_error("Only one BleStream instance is allowed"); + } + instance = this; + } + + // Disable copy and move operations + BleStream(const BleStream&) = delete; + BleStream(BleStream&&) = delete; + BleStream& operator=(const BleStream&) = delete; + BleStream& operator=(BleStream&&) = delete; + + /** + * @brief Destructor - cleans up BLE resources + */ + ~BleStream() override { + cleanup(); + if (instance == this) { + instance = nullptr; + } + } + + /** + * @brief Initialize and start the BLE stack + * @note This method is idempotent - calling it multiple times is safe + */ + void start(); + + // Duplex interface implementation + bool put(uint8_t c) override; + size_t write(std::span data) override; + int get() override; + size_t read(std::span data) override; + bool flush() override; + void onData(std::function callback) override; + +private: + /** + * @brief Clean up BLE resources + */ + void cleanup(); + + /** + * @brief Add received data to buffer and trigger callback + */ + void addReceivedData(const uint8_t* data, size_t len); + + /** + * @brief Generate device name with MAC address suffix + */ + static std::string generateDeviceName(); + + /** + * @brief GAP event handler + */ + static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); + + /** + * @brief GATTS event handler + */ + static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if_param, esp_ble_gatts_cb_param_t *param); +};