/** * @file control.c * @brief Control module implementation with BLE, NVS, and OTA */ #include "control.h" #include "led.h" #include "rcsignal.h" #include "animation.h" #include "esp_log.h" #include "esp_system.h" #include "nvs_flash.h" #include "nvs.h" #include "esp_timer.h" #include "esp_ota_ops.h" #include "esp_http_server.h" #include "esp_bt.h" #include "esp_gap_ble_api.h" #include "esp_gatts_api.h" #include "esp_bt_main.h" #include "esp_gatt_common_api.h" #include static const char *TAG = "CONTROL"; #define NVS_NAMESPACE "led_ctrl" #define CONFIG_MAGIC 0xDEADBEEF #define DEFAULT_NUM_LEDS_A 44 #define DEFAULT_NUM_LEDS_B 44 // BLE Configuration #define GATTS_SERVICE_UUID 0x00FF #define GATTS_CHAR_UUID_CONFIG 0xFF01 #define GATTS_CHAR_UUID_MODE 0xFF02 #define GATTS_CHAR_UUID_PWM 0xFF03 #define GATTS_CHAR_UUID_OTA 0xFF04 #define GATTS_NUM_HANDLE_TEST 8 #define DEVICE_NAME "LED-Controller" #define ADV_CONFIG_FLAG (1 << 0) #define SCAN_RSP_CONFIG_FLAG (1 << 1) // Global state static controller_config_t current_config = { .led_pin_strip_a = -1, .led_pin_strip_b = -1, .pwm_pin = -1, .ble_timeout = BLE_TIMEOUT_NEVER, .magic = CONFIG_MAGIC}; static bool ble_enabled = true; static uint8_t current_animation_mode = 0; static esp_timer_handle_t ble_timeout_timer = NULL; static bool ble_connected = false; // OTA state static const esp_partition_t *update_partition = NULL; static esp_ota_handle_t update_handle = 0; static size_t ota_bytes_written = 0; // BLE variables static uint8_t adv_config_done = 0; static uint16_t gatts_if_global = ESP_GATT_IF_NONE; static uint16_t conn_id_global = 0; static uint16_t service_handle = 0; // BLE advertising parameters static esp_ble_adv_params_t adv_params = { .adv_int_min = 0x20, .adv_int_max = 0x40, .adv_type = ADV_TYPE_IND, .own_addr_type = BLE_ADDR_TYPE_PUBLIC, .channel_map = ADV_CHNL_ALL, .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, }; // Characteristic handles static struct { uint16_t config_handle; uint16_t mode_handle; uint16_t pwm_handle; uint16_t ota_handle; } char_handles = {0}; // Forward declarations static void ble_timeout_callback(void *arg); static void on_mode_change(uint8_t new_mode); // NVS Functions static esp_err_t load_config_from_nvs(void) { nvs_handle_t nvs_handle; esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &nvs_handle); if (err != ESP_OK) { ESP_LOGW(TAG, "NVS not found, using defaults"); return ESP_ERR_NOT_FOUND; } size_t required_size = sizeof(controller_config_t); err = nvs_get_blob(nvs_handle, "config", ¤t_config, &required_size); nvs_close(nvs_handle); if (err != ESP_OK || current_config.magic != CONFIG_MAGIC) { ESP_LOGW(TAG, "Invalid config in NVS, using defaults"); return ESP_ERR_INVALID_STATE; } ESP_LOGI(TAG, "Loaded config from NVS"); ESP_LOGI(TAG, " Strip A: GPIO%d", current_config.led_pin_strip_a); ESP_LOGI(TAG, " Strip B: GPIO%d", current_config.led_pin_strip_b); ESP_LOGI(TAG, " PWM Pin: GPIO%d", current_config.pwm_pin); ESP_LOGI(TAG, " BLE Timeout: %d", current_config.ble_timeout); return ESP_OK; } static esp_err_t save_config_to_nvs(void) { nvs_handle_t nvs_handle; esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs_handle); if (err != ESP_OK) { return err; } current_config.magic = CONFIG_MAGIC; err = nvs_set_blob(nvs_handle, "config", ¤t_config, sizeof(controller_config_t)); if (err == ESP_OK) { err = nvs_commit(nvs_handle); } nvs_close(nvs_handle); if (err == ESP_OK) { ESP_LOGI(TAG, "Config saved to NVS"); } else { ESP_LOGE(TAG, "Failed to save config: %s", esp_err_to_name(err)); } return err; } esp_err_t control_reset_config(void) { current_config.led_pin_strip_a = -1; current_config.led_pin_strip_b = -1; current_config.pwm_pin = -1; current_config.ble_timeout = BLE_TIMEOUT_NEVER; current_config.magic = CONFIG_MAGIC; return save_config_to_nvs(); } const controller_config_t *control_get_config(void) { return ¤t_config; } esp_err_t control_update_config(const controller_config_t *config) { if (!config) { return ESP_ERR_INVALID_ARG; } // Reinitialize if pins changed bool pins_changed = (current_config.led_pin_strip_a != config->led_pin_strip_a) || (current_config.led_pin_strip_b != config->led_pin_strip_b) || (current_config.pwm_pin != config->pwm_pin); memcpy(¤t_config, config, sizeof(controller_config_t)); esp_err_t err = save_config_to_nvs(); if (err == ESP_OK && pins_changed) { ESP_LOGI(TAG, "Restarting to apply new pin configuration..."); vTaskDelay(pdMS_TO_TICKS(1000)); esp_restart(); } return err; } // BLE timeout handling static void ble_timeout_callback(void *arg) { if (!ble_connected) { ESP_LOGI(TAG, "BLE timeout reached, disabling BLE"); control_disable_ble(); } } static void start_ble_timeout(void) { if (current_config.ble_timeout == BLE_TIMEOUT_NEVER) { return; } if (ble_timeout_timer == NULL) { esp_timer_create_args_t timer_args = { .callback = ble_timeout_callback, .name = "ble_timeout"}; esp_timer_create(&timer_args, &ble_timeout_timer); } esp_timer_stop(ble_timeout_timer); esp_timer_start_once(ble_timeout_timer, (uint64_t)current_config.ble_timeout * 1000000ULL); ESP_LOGI(TAG, "BLE timeout started: %d seconds", current_config.ble_timeout); } void control_disable_ble(void) { if (!ble_enabled) return; ble_enabled = false; if (ble_timeout_timer) { esp_timer_stop(ble_timeout_timer); } // Stop BLE advertising esp_ble_gap_stop_advertising(); ESP_LOGI(TAG, "BLE disabled"); } bool control_is_ble_enabled(void) { return ble_enabled; } // Animation mode change callback static void on_mode_change(uint8_t new_mode) { current_animation_mode = new_mode; animation_set_mode((animation_mode_t)new_mode); } void control_set_animation_mode(uint8_t mode) { if (mode >= ANIM_MODE_COUNT) { mode = 0; } on_mode_change(mode); } uint8_t control_get_animation_mode(void) { return current_animation_mode; } void control_emulate_pwm_pulse(void) { rcsignal_trigger_mode_change(); } // Embedded web files (will be linked) extern const uint8_t index_html_start[] asm("_binary_index_html_start"); extern const uint8_t index_html_end[] asm("_binary_index_html_end"); extern const uint8_t app_js_start[] asm("_binary_app_js_start"); extern const uint8_t app_js_end[] asm("_binary_app_js_end"); extern const uint8_t style_css_start[] asm("_binary_style_css_start"); extern const uint8_t style_css_end[] asm("_binary_style_css_end"); extern const uint8_t favicon_ico_start[] asm("_binary_favicon_ico_start"); extern const uint8_t favicon_ico_end[] asm("_binary_favicon_ico_end"); // BLE GAP event handler static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { ESP_LOGI(TAG, "gap_event_handler() event: %i\n", event); switch (event) { case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: adv_config_done &= (~ADV_CONFIG_FLAG); if (adv_config_done == 0) { esp_ble_gap_start_advertising(&adv_params); } break; case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: adv_config_done &= (~SCAN_RSP_CONFIG_FLAG); if (adv_config_done == 0) { 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) { ESP_LOGI(TAG, "BLE advertising started"); } break; default: break; } } // BLE GATTS event handler static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { switch (event) { case ESP_GATTS_REG_EVT: ESP_LOGI(TAG, "GATTS register, status %d, app_id %d", param->reg.status, param->reg.app_id); gatts_if_global = gatts_if; // Set device name esp_ble_gap_set_device_name(DEVICE_NAME); // Config 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 = NULL, .service_data_len = 0, .p_service_data = NULL, .service_uuid_len = sizeof(uint16_t), .p_service_uuid = (uint8_t[]){GATTS_SERVICE_UUID & 0xFF, (GATTS_SERVICE_UUID >> 8) & 0xFF}, .flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT), }; esp_ble_gap_config_adv_data(&adv_data); adv_config_done |= ADV_CONFIG_FLAG; // Create service esp_gatt_srvc_id_t service_id = { .is_primary = true, .id.inst_id = 0, .id.uuid.len = ESP_UUID_LEN_16, .id.uuid.uuid.uuid16 = GATTS_SERVICE_UUID, }; esp_ble_gatts_create_service(gatts_if, &service_id, GATTS_NUM_HANDLE_TEST); break; case ESP_GATTS_CREATE_EVT: ESP_LOGI(TAG, "CREATE_SERVICE_EVT, status %d, service_handle %d", param->create.status, param->create.service_handle); service_handle = param->create.service_handle; esp_ble_gatts_start_service(service_handle); // Add characteristics esp_bt_uuid_t char_uuid; char_uuid.len = ESP_UUID_LEN_16; // Config characteristic char_uuid.uuid.uuid16 = GATTS_CHAR_UUID_CONFIG; esp_ble_gatts_add_char(service_handle, &char_uuid, ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE, ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_WRITE, NULL, NULL); // Mode characteristic char_uuid.uuid.uuid16 = GATTS_CHAR_UUID_MODE; esp_ble_gatts_add_char(service_handle, &char_uuid, ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE, ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_WRITE | ESP_GATT_CHAR_PROP_BIT_NOTIFY, NULL, NULL); // PWM emulation characteristic char_uuid.uuid.uuid16 = GATTS_CHAR_UUID_PWM; esp_ble_gatts_add_char(service_handle, &char_uuid, ESP_GATT_PERM_WRITE, ESP_GATT_CHAR_PROP_BIT_WRITE, NULL, NULL); // OTA characteristic char_uuid.uuid.uuid16 = GATTS_CHAR_UUID_OTA; esp_ble_gatts_add_char(service_handle, &char_uuid, ESP_GATT_PERM_WRITE, ESP_GATT_CHAR_PROP_BIT_WRITE, NULL, NULL); break; case ESP_GATTS_ADD_CHAR_EVT: ESP_LOGI(TAG, "ADD_CHAR_EVT, status %d, char_handle %d", param->add_char.status, param->add_char.attr_handle); // Store handles if (param->add_char.char_uuid.uuid.uuid16 == GATTS_CHAR_UUID_CONFIG) { char_handles.config_handle = param->add_char.attr_handle; } else if (param->add_char.char_uuid.uuid.uuid16 == GATTS_CHAR_UUID_MODE) { char_handles.mode_handle = param->add_char.attr_handle; } else if (param->add_char.char_uuid.uuid.uuid16 == GATTS_CHAR_UUID_PWM) { char_handles.pwm_handle = param->add_char.attr_handle; } else if (param->add_char.char_uuid.uuid.uuid16 == GATTS_CHAR_UUID_OTA) { char_handles.ota_handle = param->add_char.attr_handle; } break; case ESP_GATTS_CONNECT_EVT: ESP_LOGI(TAG, "BLE device connected"); conn_id_global = param->connect.conn_id; ble_connected = true; // Stop timeout timer when connected if (ble_timeout_timer) { esp_timer_stop(ble_timeout_timer); } break; case ESP_GATTS_DISCONNECT_EVT: ESP_LOGI(TAG, "BLE device disconnected"); ble_connected = false; // Restart advertising and timeout if (ble_enabled) { esp_ble_gap_start_advertising(&adv_params); start_ble_timeout(); } break; case ESP_GATTS_READ_EVT: ESP_LOGI(TAG, "GATTS_READ_EVT, handle %d", param->read.handle); esp_gatt_rsp_t rsp; memset(&rsp, 0, sizeof(esp_gatt_rsp_t)); rsp.attr_value.handle = param->read.handle; if (param->read.handle == char_handles.config_handle) { rsp.attr_value.len = sizeof(controller_config_t); memcpy(rsp.attr_value.value, ¤t_config, sizeof(controller_config_t)); } else if (param->read.handle == char_handles.mode_handle) { rsp.attr_value.len = 1; rsp.attr_value.value[0] = current_animation_mode; } esp_ble_gatts_send_response(gatts_if, param->read.conn_id, param->read.trans_id, ESP_GATT_OK, &rsp); break; case ESP_GATTS_WRITE_EVT: ESP_LOGI(TAG, "GATTS_WRITE_EVT, handle %d, len %d", param->write.handle, param->write.len); if (param->write.handle == char_handles.config_handle) { // Update configuration if (param->write.len == sizeof(controller_config_t)) { controller_config_t new_config; memcpy(&new_config, param->write.value, sizeof(controller_config_t)); control_update_config(&new_config); } } else if (param->write.handle == char_handles.mode_handle) { // Set animation mode if (param->write.len == 1) { control_set_animation_mode(param->write.value[0]); } } else if (param->write.handle == char_handles.pwm_handle) { // Emulate PWM pulse control_emulate_pwm_pulse(); } else if (param->write.handle == char_handles.ota_handle) { // Handle OTA data if (ota_bytes_written == 0) { // First packet - start OTA ESP_LOGI(TAG, "Starting OTA update..."); update_partition = esp_ota_get_next_update_partition(NULL); if (update_partition == NULL) { ESP_LOGE(TAG, "No OTA partition found"); break; } esp_err_t err = esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &update_handle); if (err != ESP_OK) { ESP_LOGE(TAG, "OTA begin failed: %s", esp_err_to_name(err)); break; } } // Write OTA data esp_err_t err = esp_ota_write(update_handle, param->write.value, param->write.len); if (err != ESP_OK) { ESP_LOGE(TAG, "OTA write failed: %s", esp_err_to_name(err)); esp_ota_abort(update_handle); ota_bytes_written = 0; break; } ota_bytes_written += param->write.len; ESP_LOGI(TAG, "OTA progress: %d bytes", ota_bytes_written); // Check if this is the last packet (indicated by packet size < MTU) if (param->write.len < 512) { ESP_LOGI(TAG, "OTA complete, total bytes: %d", ota_bytes_written); err = esp_ota_end(update_handle); if (err != ESP_OK) { ESP_LOGE(TAG, "OTA end failed: %s", esp_err_to_name(err)); break; } err = esp_ota_set_boot_partition(update_partition); if (err != ESP_OK) { ESP_LOGE(TAG, "OTA set boot partition failed: %s", esp_err_to_name(err)); break; } // Reset configuration control_reset_config(); ESP_LOGI(TAG, "OTA successful, restarting..."); vTaskDelay(pdMS_TO_TICKS(1000)); esp_restart(); } } if (!param->write.is_prep) { esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, NULL); } break; default: break; } } // Initialize BLE static esp_err_t init_ble(void) { if (!ble_enabled) { ESP_LOGI(TAG, "BLE disabled by configuration"); return ESP_OK; } esp_err_t 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) { ESP_LOGE(TAG, "BT controller init failed: %s", esp_err_to_name(ret)); return ret; } ret = esp_bt_controller_enable(ESP_BT_MODE_BLE); if (ret) { ESP_LOGE(TAG, "BT controller enable failed: %s", esp_err_to_name(ret)); return ret; } ret = esp_bluedroid_init(); if (ret) { ESP_LOGE(TAG, "Bluedroid init failed: %s", esp_err_to_name(ret)); return ret; } ret = esp_bluedroid_enable(); if (ret) { ESP_LOGE(TAG, "Bluedroid enable failed: %s", esp_err_to_name(ret)); return ret; } // Register callbacks esp_ble_gatts_register_callback(gatts_event_handler); esp_ble_gap_register_callback(gap_event_handler); esp_ble_gatts_app_register(0); // Set MTU esp_ble_gatt_set_local_mtu(517); // Start timeout timer start_ble_timeout(); esp_ble_gatts_app_register(0); vTaskDelay(pdMS_TO_TICKS(100)); esp_ble_gap_start_advertising(&adv_params); ESP_LOGI(TAG, "BLE initialized"); return ESP_OK; } // Main initialization esp_err_t control_init(void) { esp_err_t ret; ESP_LOGI(TAG, "Initializing LED Controller..."); // Initialize NVS ret = nvs_flash_init(); if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { ESP_ERROR_CHECK(nvs_flash_erase()); ret = nvs_flash_init(); } ESP_ERROR_CHECK(ret); // Load configuration load_config_from_nvs(); // Initialize LED strips ret = led_init(current_config.led_pin_strip_a, current_config.led_pin_strip_b, DEFAULT_NUM_LEDS_A, DEFAULT_NUM_LEDS_B); if (ret != ESP_OK) { ESP_LOGE(TAG, "LED init failed: %s", esp_err_to_name(ret)); return ret; } // Initialize animation system ret = animation_init(); if (ret != ESP_OK) { ESP_LOGE(TAG, "Animation init failed: %s", esp_err_to_name(ret)); return ret; } // Initialize RC signal ret = rcsignal_init(current_config.pwm_pin); if (ret != ESP_OK) { ESP_LOGE(TAG, "RC signal init failed: %s", esp_err_to_name(ret)); return ret; } // Register mode change callback rcsignal_register_callback(on_mode_change); // Initialize BLE ret = init_ble(); if (ret != ESP_OK) { ESP_LOGE(TAG, "BLE init failed: %s", esp_err_to_name(ret)); return ret; } ESP_LOGI(TAG, "Control system initialized successfully"); return ESP_OK; }