Files
WS2812B-LED-RC-Controller/main/control.c
2026-01-05 21:01:26 +01:00

687 lines
20 KiB
C

/**
* @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 <string.h>
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", &current_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", &current_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 &current_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(&current_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, &current_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;
}