Port to ESP32

This commit is contained in:
2026-01-05 21:01:26 +01:00
parent 5f69141a86
commit 05be118dd1
21 changed files with 4025 additions and 29 deletions

14
main/CMakeLists.txt Normal file
View File

@ -0,0 +1,14 @@
idf_component_register(
SRCS
"main.c"
"control.c"
"led.c"
"rcsignal.c"
"animation.c"
INCLUDE_DIRS "."
EMBED_FILES
"../webapp/index.html"
"../webapp/app/app.js"
"../webapp/css/style.css"
"../webapp/data/favicon.ico"
)

471
main/animation.c Normal file
View File

@ -0,0 +1,471 @@
/**
* @file animation.c
* @brief LED animation patterns implementation
*/
#include "animation.h"
#include "led.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "esp_random.h"
#include <math.h>
#include <string.h>
static const char *TAG = "ANIMATION";
#define FRAMES_PER_SECOND 60
#define NUM_LEDS_DEFAULT 44 //TODO: Default from proof-of-concept
static animation_mode_t current_mode = ANIM_BLACK;
static uint8_t global_hue = 0;
static uint32_t frame_counter = 0;
// Beat calculation helper (similar to FastLED beatsin16)
static int16_t beatsin16(uint8_t bpm, int16_t min_val, int16_t max_val)
{
uint32_t ms = esp_timer_get_time() / 1000;
uint32_t beat = (ms * bpm * 256) / 60000;
uint8_t beat8 = (beat >> 8) & 0xFF;
// Sin approximation
float angle = (beat8 / 255.0f) * 2.0f * M_PI;
float sin_val = sinf(angle);
int16_t range = max_val - min_val;
int16_t result = min_val + (int16_t)((sin_val + 1.0f) * range / 2.0f);
return result;
}
// Beat calculation helper (beatsin8 variant)
static uint8_t beatsin8(uint8_t bpm, uint8_t min_val, uint8_t max_val)
{
return (uint8_t)beatsin16(bpm, min_val, max_val);
}
// Random helper
static uint8_t random8(void)
{
return esp_random() & 0xFF;
}
static uint16_t random16(uint16_t max)
{
if (max == 0)
return 0;
return esp_random() % max;
}
// Animation implementations
static void anim_black(void)
{
rgb_t black = {0, 0, 0};
led_fill_a(black);
led_fill_b(black);
}
static void anim_red(void)
{
rgb_t red = {255, 0, 0};
led_fill_a(red);
led_fill_b(red);
}
static void anim_blue(void)
{
rgb_t blue = {0, 0, 255};
led_fill_a(blue);
led_fill_b(blue);
}
static void anim_green(void)
{
rgb_t green = {0, 255, 0};
led_fill_a(green);
led_fill_b(green);
}
static void anim_white(void)
{
rgb_t white = {255, 255, 255};
led_fill_a(white);
led_fill_b(white);
}
static void anim_rainbow(void)
{
// FastLED's built-in rainbow generator
uint16_t num_leds_a = led_get_num_leds_a();
uint16_t num_leds_b = led_get_num_leds_b();
for (uint16_t i = 0; i < num_leds_a; i++)
{
hsv_t hsv = {(uint8_t)(global_hue + (i * 7)), 255, 255};
led_set_pixel_a(i, led_hsv_to_rgb(hsv));
}
for (uint16_t i = 0; i < num_leds_b; i++)
{
hsv_t hsv = {(uint8_t)(global_hue + (i * 7)), 255, 255};
led_set_pixel_b(i, led_hsv_to_rgb(hsv));
}
}
static void add_glitter(uint8_t chance_of_glitter)
{
if (random8() < chance_of_glitter)
{
uint16_t num_leds = led_get_num_leds_a() + led_get_num_leds_b();
uint16_t pos = random16(num_leds);
rgb_t white = {255, 255, 255};
if (pos < led_get_num_leds_a())
{
led_add_pixel_a(pos, white);
}
else
{
led_add_pixel_b(pos - led_get_num_leds_a(), white);
}
}
}
static void anim_rainbow_glitter(void)
{
anim_rainbow();
add_glitter(80);
}
static void anim_confetti(void)
{
// Random colored speckles that blink in and fade smoothly
led_fade_to_black(10);
uint16_t num_leds = led_get_num_leds_a() + led_get_num_leds_b();
uint16_t pos = random16(num_leds);
hsv_t hsv = {(uint8_t)(global_hue + random8()), 200, 255};
rgb_t color = led_hsv_to_rgb(hsv);
if (pos < led_get_num_leds_a())
{
led_add_pixel_a(pos, color);
}
else
{
led_add_pixel_b(pos - led_get_num_leds_a(), color);
}
}
static void anim_sinelon(void)
{
// A colored dot sweeping back and forth, with fading trails
led_fade_to_black(20);
uint16_t num_leds = led_get_num_leds_a() + led_get_num_leds_b();
int16_t pos = beatsin16(13, 0, num_leds - 1);
hsv_t hsv = {global_hue, 255, 192};
rgb_t color = led_hsv_to_rgb(hsv);
if (pos < led_get_num_leds_a())
{
led_add_pixel_a(pos, color);
}
else
{
led_add_pixel_b(pos - led_get_num_leds_a(), color);
}
}
static void anim_bpm(void)
{
// Colored stripes pulsing at 33 BPM
uint8_t bpm = 33;
uint8_t beat = beatsin8(bpm, 64, 255);
uint16_t num_leds_a = led_get_num_leds_a();
uint16_t num_leds_b = led_get_num_leds_b();
// PartyColors palette simulation
const uint8_t palette_colors[] = {
170, 240, 90, 150, 210, 30, 180, 0,
210, 270, 150, 240, 330, 60, 300, 120};
for (uint16_t i = 0; i < num_leds_a; i++)
{
uint8_t color_index = (global_hue + (i * 2)) & 0x0F;
uint8_t brightness = beat - global_hue + (i * 10);
hsv_t hsv = {palette_colors[color_index], 255, brightness};
led_set_pixel_a(i, led_hsv_to_rgb(hsv));
}
for (uint16_t i = 0; i < num_leds_b; i++)
{
uint8_t color_index = (global_hue + ((i + num_leds_a) * 2)) & 0x0F;
uint8_t brightness = beat - global_hue + ((i + num_leds_a) * 10);
hsv_t hsv = {palette_colors[color_index], 255, brightness};
led_set_pixel_b(i, led_hsv_to_rgb(hsv));
}
}
static void anim_navigation(void)
{
// Navigation lights: left red, right green, with blinking white
static uint8_t blink_state = 0;
led_clear_all();
uint16_t num_leds_a = led_get_num_leds_a();
uint16_t num_leds_b = led_get_num_leds_b();
rgb_t red = {255, 0, 0};
rgb_t green = {0, 255, 0};
rgb_t white = {255, 255, 255};
// Left side red (first 3 LEDs of strip A)
if (num_leds_a >= 3)
{
led_set_pixel_a(0, red);
led_set_pixel_a(1, red);
led_set_pixel_a(2, red);
}
// Right side green (last 3 LEDs)
if (num_leds_b >= 3)
{
led_set_pixel_b(num_leds_b - 1, green);
led_set_pixel_b(num_leds_b - 2, green);
led_set_pixel_b(num_leds_b - 3, green);
}
else if (num_leds_a >= 6)
{
led_set_pixel_a(num_leds_a - 1, green);
led_set_pixel_a(num_leds_a - 2, green);
led_set_pixel_a(num_leds_a - 3, green);
}
// Blinking white lights (positions 5-6 and 37-38 from original)
if (blink_state < FRAMES_PER_SECOND / 2)
{
if (num_leds_a > 6)
{
led_set_pixel_a(5, white);
led_set_pixel_a(6, white);
}
if (num_leds_b > 2)
{
led_set_pixel_b(1, white);
led_set_pixel_b(2, white);
}
else if (num_leds_a > 38)
{
led_set_pixel_a(37, white);
led_set_pixel_a(38, white);
}
}
blink_state = (blink_state + 1) % FRAMES_PER_SECOND;
}
static void anim_chase(void)
{
// Red dot sweeping with trailing dots
led_clear_all();
uint16_t num_leds = led_get_num_leds_a() + led_get_num_leds_b();
int16_t pos = beatsin16(40, 0, num_leds - 1);
rgb_t red = {255, 0, 0};
// Set main dot and trailing dots
for (int offset = -2; offset <= 2; offset++)
{
int16_t led_pos = pos + offset;
if (led_pos >= 0 && led_pos < num_leds)
{
if (led_pos < led_get_num_leds_a())
{
led_set_pixel_a(led_pos, red);
}
else
{
led_set_pixel_b(led_pos - led_get_num_leds_a(), red);
}
}
}
}
static void anim_chase_rgb(void)
{
// RGB cycling dot sweeping with trailing dots
led_clear_all();
uint16_t num_leds = led_get_num_leds_a() + led_get_num_leds_b();
int16_t pos = beatsin16(40, 0, num_leds - 1);
hsv_t hsv = {global_hue, 255, 192};
rgb_t color = led_hsv_to_rgb(hsv);
// Set main dot and trailing dots
for (int offset = -2; offset <= 2; offset++)
{
int16_t led_pos = pos + offset;
if (led_pos >= 0 && led_pos < num_leds)
{
if (led_pos < led_get_num_leds_a())
{
led_add_pixel_a(led_pos, color);
}
else
{
led_add_pixel_b(led_pos - led_get_num_leds_a(), color);
}
}
}
}
static void anim_random(void)
{
// Random LEDs get random colors
uint16_t num_leds = led_get_num_leds_a() + led_get_num_leds_b();
uint16_t random_pos = random16(num_leds);
// Randomly clear all (rare event)
if (random_pos == num_leds - 1 && random8() > 200)
{
led_clear_all();
return;
}
// Set random LED to random color
rgb_t random_color = {
random8(),
random8(),
random8()};
if (random_pos < led_get_num_leds_a())
{
led_set_pixel_a(random_pos, random_color);
}
else
{
led_set_pixel_b(random_pos - led_get_num_leds_a(), random_color);
}
}
esp_err_t animation_init(void)
{
current_mode = ANIM_BLACK;
global_hue = 0;
frame_counter = 0;
ESP_LOGI(TAG, "Animation system initialized");
return ESP_OK;
}
void animation_set_mode(animation_mode_t mode)
{
if (mode >= ANIM_MODE_COUNT)
{
mode = ANIM_BLACK;
}
current_mode = mode;
frame_counter = 0;
ESP_LOGI(TAG, "Animation mode set to: %s", animation_get_mode_name(mode));
}
animation_mode_t animation_get_mode(void)
{
return current_mode;
}
void animation_update(void)
{
// Update global hue every frame (slowly cycles colors)
frame_counter++;
if (frame_counter % 3 == 0)
{
global_hue++;
}
// Execute current animation
switch (current_mode)
{
case ANIM_BLACK:
anim_black();
break;
case ANIM_RED:
anim_red();
break;
case ANIM_BLUE:
anim_blue();
break;
case ANIM_GREEN:
anim_green();
break;
case ANIM_WHITE:
anim_white();
break;
case ANIM_RAINBOW:
anim_rainbow();
break;
case ANIM_RAINBOW_GLITTER:
anim_rainbow_glitter();
break;
case ANIM_CONFETTI:
anim_confetti();
break;
case ANIM_SINELON:
anim_sinelon();
break;
case ANIM_BPM:
anim_bpm();
break;
case ANIM_NAVIGATION:
anim_navigation();
break;
case ANIM_CHASE:
anim_chase();
break;
case ANIM_CHASE_RGB:
anim_chase_rgb();
break;
case ANIM_RANDOM:
anim_random();
break;
default:
anim_black();
break;
}
led_show();
}
const char *animation_get_mode_name(animation_mode_t mode)
{
static const char *mode_names[] = {
"Black",
"Red",
"Blue",
"Green",
"White",
"Rainbow",
"Rainbow with Glitter",
"Confetti",
"Sinelon",
"BPM",
"Navigation",
"Chase",
"Chase RGB",
"Random"};
if (mode >= ANIM_MODE_COUNT)
{
return "Unknown";
}
return mode_names[mode];
}

63
main/animation.h Normal file
View File

@ -0,0 +1,63 @@
/**
* @file animation.h
* @brief LED animation patterns
*/
#ifndef ANIMATION_H
#define ANIMATION_H
#include <stdint.h>
#include "esp_err.h"
/**
* @brief Animation modes
*/
typedef enum {
ANIM_BLACK = 0, // All off
ANIM_RED = 1, // All red
ANIM_BLUE = 2, // All blue
ANIM_GREEN = 3, // All green
ANIM_WHITE = 4, // All white
ANIM_RAINBOW = 5, // FastLED rainbow
ANIM_RAINBOW_GLITTER = 6, // Rainbow with glitter
ANIM_CONFETTI = 7, // Random colored speckles
ANIM_SINELON = 8, // Colored dot sweeping (RGB cycling)
ANIM_BPM = 9, // Colored stripes @ 33 BPM
ANIM_NAVIGATION = 10, // Navigation lights (red left, green right)
ANIM_CHASE = 11, // Red dot sweeping
ANIM_CHASE_RGB = 12, // RGB cycling dot sweeping
ANIM_RANDOM = 13, // Random mode
ANIM_MODE_COUNT
} animation_mode_t;
/**
* @brief Initialize animation system
* @return ESP_OK on success
*/
esp_err_t animation_init(void);
/**
* @brief Set current animation mode
* @param mode Animation mode
*/
void animation_set_mode(animation_mode_t mode);
/**
* @brief Get current animation mode
* @return Current mode
*/
animation_mode_t animation_get_mode(void);
/**
* @brief Update animation (call periodically, e.g., 30-60 FPS)
*/
void animation_update(void);
/**
* @brief Get animation mode name
* @param mode Animation mode
* @return Mode name string
*/
const char* animation_get_mode_name(animation_mode_t mode);
#endif // ANIMATION_H

686
main/control.c Normal file
View File

@ -0,0 +1,686 @@
/**
* @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;
}

87
main/control.h Normal file
View File

@ -0,0 +1,87 @@
/**
* @file control.h
* @brief Control module for LED controller - handles initialization of LEDs, PWM, and Bluetooth
*/
#ifndef CONTROL_H
#define CONTROL_H
#include "esp_err.h"
#include <stdint.h>
#include <stdbool.h>
/**
* @brief BLE auto-off timeout options
*/
typedef enum {
BLE_TIMEOUT_NEVER = 0,
BLE_TIMEOUT_1MIN = 60,
BLE_TIMEOUT_5MIN = 300
} ble_timeout_t;
/**
* @brief Configuration structure stored in NVS
*/
typedef struct {
int8_t led_pin_strip_a; // GPIO pin for LED strip A (-1 = not configured)
int8_t led_pin_strip_b; // GPIO pin for LED strip B (-1 = not configured)
int8_t pwm_pin; // GPIO pin for PWM input (-1 = not configured)
ble_timeout_t ble_timeout; // BLE auto-off timeout
uint32_t magic; // Magic number to validate config (0xDEADBEEF)
} controller_config_t;
/**
* @brief Initialize the control system
* Loads configuration from NVS and initializes subsystems
* @return ESP_OK on success
*/
esp_err_t control_init(void);
/**
* @brief Get current configuration
* @return Pointer to current configuration (read-only)
*/
const controller_config_t* control_get_config(void);
/**
* @brief Update configuration and save to NVS
* @param config New configuration
* @return ESP_OK on success
*/
esp_err_t control_update_config(const controller_config_t* config);
/**
* @brief Reset configuration to defaults
* @return ESP_OK on success
*/
esp_err_t control_reset_config(void);
/**
* @brief Get BLE enabled status
* @return true if BLE is enabled
*/
bool control_is_ble_enabled(void);
/**
* @brief Manually disable BLE
*/
void control_disable_ble(void);
/**
* @brief Emulate PWM pulse (for web button)
*/
void control_emulate_pwm_pulse(void);
/**
* @brief Set animation mode manually
* @param mode Animation mode (0-13)
*/
void control_set_animation_mode(uint8_t mode);
/**
* @brief Get current animation mode
* @return Current mode (0-13)
*/
uint8_t control_get_animation_mode(void);
#endif // CONTROL_H

476
main/led.c Normal file
View File

@ -0,0 +1,476 @@
/**
* @file led.c
* @brief WS2812B LED strip control implementation using RMT
*/
#include "led.h"
#include "driver/rmt_tx.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include <string.h>
#include <stdlib.h>
static const char *TAG = "LED";
// WS2812B timing (in nanoseconds)
#define WS2812_T0H_NS 350
#define WS2812_T0L_NS 900
#define WS2812_T1H_NS 900
#define WS2812_T1L_NS 350
#define WS2812_RESET_US 280
// LED strip data structures
typedef struct
{
rmt_channel_handle_t rmt_channel;
rmt_encoder_handle_t encoder;
rgb_t *buffer;
uint16_t num_leds;
int8_t gpio_pin;
bool initialized;
} led_strip_t;
static led_strip_t strip_a = {0};
static led_strip_t strip_b = {0};
static SemaphoreHandle_t led_mutex = NULL;
// RMT encoder for WS2812B
typedef struct
{
rmt_encoder_t base;
rmt_encoder_t *bytes_encoder;
rmt_encoder_t *copy_encoder;
int state;
rmt_symbol_word_t reset_code;
} rmt_led_strip_encoder_t;
static size_t rmt_encode_led_strip(rmt_encoder_t *encoder, rmt_channel_handle_t channel,
const void *primary_data, size_t data_size,
rmt_encode_state_t *ret_state)
{
rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base);
rmt_encode_state_t session_state = RMT_ENCODING_RESET;
rmt_encode_state_t state = RMT_ENCODING_RESET;
size_t encoded_symbols = 0;
switch (led_encoder->state)
{
case 0: // send RGB data
encoded_symbols += led_encoder->bytes_encoder->encode(led_encoder->bytes_encoder, channel,
primary_data, data_size, &session_state);
if (session_state & RMT_ENCODING_COMPLETE)
{
led_encoder->state = 1; // switch to next state when current encoding session finished
}
if (session_state & RMT_ENCODING_MEM_FULL)
{
state |= RMT_ENCODING_MEM_FULL;
goto out;
}
// fall-through
case 1: // send reset code
encoded_symbols += led_encoder->copy_encoder->encode(led_encoder->copy_encoder, channel,
&led_encoder->reset_code,
sizeof(led_encoder->reset_code), &session_state);
if (session_state & RMT_ENCODING_COMPLETE)
{
led_encoder->state = RMT_ENCODING_RESET;
state |= RMT_ENCODING_COMPLETE;
}
if (session_state & RMT_ENCODING_MEM_FULL)
{
state |= RMT_ENCODING_MEM_FULL;
goto out;
}
}
out:
*ret_state = state;
return encoded_symbols;
}
static esp_err_t rmt_del_led_strip_encoder(rmt_encoder_t *encoder)
{
rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base);
rmt_del_encoder(led_encoder->bytes_encoder);
rmt_del_encoder(led_encoder->copy_encoder);
free(led_encoder);
return ESP_OK;
}
static esp_err_t rmt_led_strip_encoder_reset(rmt_encoder_t *encoder)
{
rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base);
rmt_encoder_reset(led_encoder->bytes_encoder);
rmt_encoder_reset(led_encoder->copy_encoder);
led_encoder->state = RMT_ENCODING_RESET;
return ESP_OK;
}
static esp_err_t rmt_new_led_strip_encoder(rmt_encoder_handle_t *ret_encoder)
{
esp_err_t ret = ESP_OK;
rmt_led_strip_encoder_t *led_encoder = calloc(1, sizeof(rmt_led_strip_encoder_t));
if (!led_encoder)
{
return ESP_ERR_NO_MEM;
}
led_encoder->base.encode = rmt_encode_led_strip;
led_encoder->base.del = rmt_del_led_strip_encoder;
led_encoder->base.reset = rmt_led_strip_encoder_reset;
// WS2812 timing
rmt_bytes_encoder_config_t bytes_encoder_config = {
.bit0 = {
.level0 = 1,
.duration0 = WS2812_T0H_NS * 80 / 1000, // 80MHz clock
.level1 = 0,
.duration1 = WS2812_T0L_NS * 80 / 1000,
},
.bit1 = {
.level0 = 1,
.duration0 = WS2812_T1H_NS * 80 / 1000,
.level1 = 0,
.duration1 = WS2812_T1L_NS * 80 / 1000,
},
.flags.msb_first = 1,
};
ret = rmt_new_bytes_encoder(&bytes_encoder_config, &led_encoder->bytes_encoder);
if (ret != ESP_OK)
{
goto err;
}
rmt_copy_encoder_config_t copy_encoder_config = {};
ret = rmt_new_copy_encoder(&copy_encoder_config, &led_encoder->copy_encoder);
if (ret != ESP_OK)
{
goto err;
}
uint32_t reset_ticks = WS2812_RESET_US * 80; // 80MHz
led_encoder->reset_code = (rmt_symbol_word_t){
.level0 = 0,
.duration0 = reset_ticks & 0x7FFF,
.level1 = 0,
.duration1 = reset_ticks & 0x7FFF,
};
*ret_encoder = &led_encoder->base;
return ESP_OK;
err:
if (led_encoder->bytes_encoder)
{
rmt_del_encoder(led_encoder->bytes_encoder);
}
if (led_encoder->copy_encoder)
{
rmt_del_encoder(led_encoder->copy_encoder);
}
free(led_encoder);
return ret;
}
static esp_err_t init_strip(led_strip_t *strip, int8_t pin, uint16_t num_leds)
{
if (pin < 0 || num_leds == 0)
{
return ESP_OK; // Skip if not configured
}
strip->buffer = calloc(num_leds, sizeof(rgb_t));
if (!strip->buffer)
{
return ESP_ERR_NO_MEM;
}
strip->num_leds = num_leds;
strip->gpio_pin = pin;
rmt_tx_channel_config_t tx_chan_config = {
.clk_src = RMT_CLK_SRC_DEFAULT,
.gpio_num = pin,
.mem_block_symbols = 64,
.resolution_hz = 80000000, // 80MHz
.trans_queue_depth = 4,
};
ESP_ERROR_CHECK(rmt_new_tx_channel(&tx_chan_config, &strip->rmt_channel));
ESP_ERROR_CHECK(rmt_new_led_strip_encoder(&strip->encoder));
ESP_ERROR_CHECK(rmt_enable(strip->rmt_channel));
strip->initialized = true;
ESP_LOGI(TAG, "Initialized strip on GPIO%d with %d LEDs", pin, num_leds);
return ESP_OK;
}
esp_err_t led_init(int8_t pin_a, int8_t pin_b, uint16_t num_leds_a, uint16_t num_leds_b)
{
if (led_mutex == NULL)
{
led_mutex = xSemaphoreCreateMutex();
if (!led_mutex)
{
return ESP_ERR_NO_MEM;
}
}
esp_err_t ret = ESP_OK;
if (pin_a >= 0)
{
ret = init_strip(&strip_a, pin_a, num_leds_a);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "Failed to init strip A: %s", esp_err_to_name(ret));
return ret;
}
}
if (pin_b >= 0)
{
ret = init_strip(&strip_b, pin_b, num_leds_b);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "Failed to init strip B: %s", esp_err_to_name(ret));
return ret;
}
}
return ESP_OK;
}
void led_deinit(void)
{
if (strip_a.initialized)
{
rmt_disable(strip_a.rmt_channel);
rmt_del_channel(strip_a.rmt_channel);
free(strip_a.buffer);
strip_a.initialized = false;
}
if (strip_b.initialized)
{
rmt_disable(strip_b.rmt_channel);
rmt_del_channel(strip_b.rmt_channel);
free(strip_b.buffer);
strip_b.initialized = false;
}
}
void led_set_pixel_a(uint16_t index, rgb_t color)
{
if (!strip_a.initialized || index >= strip_a.num_leds)
return;
xSemaphoreTake(led_mutex, portMAX_DELAY);
strip_a.buffer[index] = color;
xSemaphoreGive(led_mutex);
}
void led_set_pixel_b(uint16_t index, rgb_t color)
{
if (!strip_b.initialized || index >= strip_b.num_leds)
return;
xSemaphoreTake(led_mutex, portMAX_DELAY);
strip_b.buffer[index] = color;
xSemaphoreGive(led_mutex);
}
void led_fill_a(rgb_t color)
{
if (!strip_a.initialized)
return;
xSemaphoreTake(led_mutex, portMAX_DELAY);
for (uint16_t i = 0; i < strip_a.num_leds; i++)
{
strip_a.buffer[i] = color;
}
xSemaphoreGive(led_mutex);
}
void led_fill_b(rgb_t color)
{
if (!strip_b.initialized)
return;
xSemaphoreTake(led_mutex, portMAX_DELAY);
for (uint16_t i = 0; i < strip_b.num_leds; i++)
{
strip_b.buffer[i] = color;
}
xSemaphoreGive(led_mutex);
}
void led_clear_all(void)
{
rgb_t black = {0, 0, 0};
led_fill_a(black);
led_fill_b(black);
}
static void show_strip(led_strip_t *strip)
{
if (!strip->initialized)
return;
// Convert RGB to GRB for WS2812B
uint8_t *grb_data = malloc(strip->num_leds * 3);
if (!grb_data)
return;
for (uint16_t i = 0; i < strip->num_leds; i++)
{
grb_data[i * 3 + 0] = strip->buffer[i].g;
grb_data[i * 3 + 1] = strip->buffer[i].r;
grb_data[i * 3 + 2] = strip->buffer[i].b;
}
rmt_transmit_config_t tx_config = {
.loop_count = 0,
};
rmt_transmit(strip->rmt_channel, strip->encoder, grb_data, strip->num_leds * 3, &tx_config);
free(grb_data);
}
void led_show(void)
{
xSemaphoreTake(led_mutex, portMAX_DELAY);
show_strip(&strip_a);
show_strip(&strip_b);
xSemaphoreGive(led_mutex);
}
void led_fade_to_black(uint8_t amount)
{
xSemaphoreTake(led_mutex, portMAX_DELAY);
if (strip_a.initialized)
{
for (uint16_t i = 0; i < strip_a.num_leds; i++)
{
strip_a.buffer[i].r = (strip_a.buffer[i].r * (255 - amount)) / 255;
strip_a.buffer[i].g = (strip_a.buffer[i].g * (255 - amount)) / 255;
strip_a.buffer[i].b = (strip_a.buffer[i].b * (255 - amount)) / 255;
}
}
if (strip_b.initialized)
{
for (uint16_t i = 0; i < strip_b.num_leds; i++)
{
strip_b.buffer[i].r = (strip_b.buffer[i].r * (255 - amount)) / 255;
strip_b.buffer[i].g = (strip_b.buffer[i].g * (255 - amount)) / 255;
strip_b.buffer[i].b = (strip_b.buffer[i].b * (255 - amount)) / 255;
}
}
xSemaphoreGive(led_mutex);
}
rgb_t led_hsv_to_rgb(hsv_t hsv)
{
rgb_t rgb = {0};
uint8_t region, remainder, p, q, t;
if (hsv.s == 0)
{
rgb.r = hsv.v;
rgb.g = hsv.v;
rgb.b = hsv.v;
return rgb;
}
region = hsv.h / 43;
remainder = (hsv.h - (region * 43)) * 6;
p = (hsv.v * (255 - hsv.s)) >> 8;
q = (hsv.v * (255 - ((hsv.s * remainder) >> 8))) >> 8;
t = (hsv.v * (255 - ((hsv.s * (255 - remainder)) >> 8))) >> 8;
switch (region)
{
case 0:
rgb.r = hsv.v;
rgb.g = t;
rgb.b = p;
break;
case 1:
rgb.r = q;
rgb.g = hsv.v;
rgb.b = p;
break;
case 2:
rgb.r = p;
rgb.g = hsv.v;
rgb.b = t;
break;
case 3:
rgb.r = p;
rgb.g = q;
rgb.b = hsv.v;
break;
case 4:
rgb.r = t;
rgb.g = p;
rgb.b = hsv.v;
break;
default:
rgb.r = hsv.v;
rgb.g = p;
rgb.b = q;
break;
}
return rgb;
}
uint16_t led_get_num_leds_a(void) { return strip_a.num_leds; }
uint16_t led_get_num_leds_b(void) { return strip_b.num_leds; }
rgb_t led_get_pixel_a(uint16_t index)
{
rgb_t color = {0};
if (!strip_a.initialized || index >= strip_a.num_leds)
return color;
xSemaphoreTake(led_mutex, portMAX_DELAY);
color = strip_a.buffer[index];
xSemaphoreGive(led_mutex);
return color;
}
rgb_t led_get_pixel_b(uint16_t index)
{
rgb_t color = {0};
if (!strip_b.initialized || index >= strip_b.num_leds)
return color;
xSemaphoreTake(led_mutex, portMAX_DELAY);
color = strip_b.buffer[index];
xSemaphoreGive(led_mutex);
return color;
}
void led_add_pixel_a(uint16_t index, rgb_t color)
{
if (!strip_a.initialized || index >= strip_a.num_leds)
return;
xSemaphoreTake(led_mutex, portMAX_DELAY);
strip_a.buffer[index].r = (strip_a.buffer[index].r + color.r > 255) ? 255 : strip_a.buffer[index].r + color.r;
strip_a.buffer[index].g = (strip_a.buffer[index].g + color.g > 255) ? 255 : strip_a.buffer[index].g + color.g;
strip_a.buffer[index].b = (strip_a.buffer[index].b + color.b > 255) ? 255 : strip_a.buffer[index].b + color.b;
xSemaphoreGive(led_mutex);
}
void led_add_pixel_b(uint16_t index, rgb_t color)
{
if (!strip_b.initialized || index >= strip_b.num_leds)
return;
xSemaphoreTake(led_mutex, portMAX_DELAY);
strip_b.buffer[index].r = (strip_b.buffer[index].r + color.r > 255) ? 255 : strip_b.buffer[index].r + color.r;
strip_b.buffer[index].g = (strip_b.buffer[index].g + color.g > 255) ? 255 : strip_b.buffer[index].g + color.g;
strip_b.buffer[index].b = (strip_b.buffer[index].b + color.b > 255) ? 255 : strip_b.buffer[index].b + color.b;
xSemaphoreGive(led_mutex);
}

136
main/led.h Normal file
View File

@ -0,0 +1,136 @@
/**
* @file led.h
* @brief LED strip control module for WS2812B
*/
#ifndef LED_H
#define LED_H
#include <stdint.h>
#include "esp_err.h"
#define LED_STRIP_MAX_LEDS 100 // Maximum LEDs per strip
/**
* @brief RGB color structure
*/
typedef struct {
uint8_t r;
uint8_t g;
uint8_t b;
} rgb_t;
/**
* @brief HSV color structure
*/
typedef struct {
uint8_t h; // Hue: 0-255
uint8_t s; // Saturation: 0-255
uint8_t v; // Value/Brightness: 0-255
} hsv_t;
/**
* @brief Initialize LED strips
* @param pin_a GPIO pin for strip A (-1 to disable)
* @param pin_b GPIO pin for strip B (-1 to disable)
* @param num_leds_a Number of LEDs in strip A
* @param num_leds_b Number of LEDs in strip B
* @return ESP_OK on success
*/
esp_err_t led_init(int8_t pin_a, int8_t pin_b, uint16_t num_leds_a, uint16_t num_leds_b);
/**
* @brief Deinitialize LED strips
*/
void led_deinit(void);
/**
* @brief Set pixel color on strip A
* @param index LED index
* @param color RGB color
*/
void led_set_pixel_a(uint16_t index, rgb_t color);
/**
* @brief Set pixel color on strip B
* @param index LED index
* @param color RGB color
*/
void led_set_pixel_b(uint16_t index, rgb_t color);
/**
* @brief Set all pixels on strip A to same color
* @param color RGB color
*/
void led_fill_a(rgb_t color);
/**
* @brief Set all pixels on strip B to same color
* @param color RGB color
*/
void led_fill_b(rgb_t color);
/**
* @brief Clear all pixels on both strips (set to black)
*/
void led_clear_all(void);
/**
* @brief Refresh/update LED strips to show changes
*/
void led_show(void);
/**
* @brief Fade all pixels towards black
* @param amount Fade amount (0-255)
*/
void led_fade_to_black(uint8_t amount);
/**
* @brief Convert HSV to RGB
* @param hsv HSV color
* @return RGB color
*/
rgb_t led_hsv_to_rgb(hsv_t hsv);
/**
* @brief Get number of LEDs in strip A
* @return Number of LEDs
*/
uint16_t led_get_num_leds_a(void);
/**
* @brief Get number of LEDs in strip B
* @return Number of LEDs
*/
uint16_t led_get_num_leds_b(void);
/**
* @brief Get current color of pixel on strip A
* @param index LED index
* @return RGB color
*/
rgb_t led_get_pixel_a(uint16_t index);
/**
* @brief Get current color of pixel on strip B
* @param index LED index
* @return RGB color
*/
rgb_t led_get_pixel_b(uint16_t index);
/**
* @brief Add color to existing pixel (blending)
* @param index LED index on strip A
* @param color RGB color to add
*/
void led_add_pixel_a(uint16_t index, rgb_t color);
/**
* @brief Add color to existing pixel (blending)
* @param index LED index on strip B
* @param color RGB color to add
*/
void led_add_pixel_b(uint16_t index, rgb_t color);
#endif // LED_H

94
main/main.c Normal file
View File

@ -0,0 +1,94 @@
/**
* @file main.c
* @brief Main application entry point for LED Controller
*/
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_system.h"
#include "control.h"
#include "animation.h"
#include "led.h"
static const char *TAG = "MAIN";
#define ANIMATION_UPDATE_RATE_MS 16 // ~60 FPS
/**
* @brief Animation update task
* Runs continuously to update LED animations
*/
static void animation_task(void *pvParameters)
{
ESP_LOGI(TAG, "Animation task started");
TickType_t last_wake_time = xTaskGetTickCount();
const TickType_t update_interval = pdMS_TO_TICKS(ANIMATION_UPDATE_RATE_MS);
while (1)
{
animation_update();
vTaskDelayUntil(&last_wake_time, update_interval);
}
}
/**
* @brief Main application entry point
*/
void app_main(void)
{
ESP_LOGI(TAG, "==============================================");
ESP_LOGI(TAG, " ESP32 LED Controller for Model Aircraft");
ESP_LOGI(TAG, "==============================================");
// Initialize control system (LEDs, PWM, BLE)
esp_err_t ret = control_init();
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "Failed to initialize control system: %s", esp_err_to_name(ret));
ESP_LOGE(TAG, "System halted. Please reset the device.");
while (1)
{
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
// Create animation update task
BaseType_t task_ret = xTaskCreate(
animation_task,
"animation",
4096,
NULL,
5,
NULL);
if (task_ret != pdPASS)
{
ESP_LOGE(TAG, "Failed to create animation task");
ESP_LOGE(TAG, "System halted. Please reset the device.");
while (1)
{
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
ESP_LOGI(TAG, "System initialized successfully");
ESP_LOGI(TAG, "BLE Device Name: LED-Controller");
ESP_LOGI(TAG, "Connect via Web-BLE to configure");
// Main loop - just monitor system status
while (1)
{
vTaskDelay(pdMS_TO_TICKS(5000));
// Periodic status logging
//const controller_config_t *config = control_get_config();
ESP_LOGI(TAG, "Status - Mode: %d, BLE: %s, PWM Active: %s",
control_get_animation_mode(),
control_is_ble_enabled() ? "ON" : "OFF",
"N/A"); // Could add rcsignal_is_active() here
}
}

202
main/rcsignal.c Normal file
View File

@ -0,0 +1,202 @@
/**
* @file rcsignal.c
* @brief RC PWM signal reading implementation using edge capture
*/
#include "rcsignal.h"
#include "driver/gpio.h"
#include "esp_timer.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <string.h>
static const char *TAG = "RCSIGNAL";
#define MAX_MODES 14
#define PULSE_THRESHOLD_US 1500
#define SIGNAL_TIMEOUT_MS 100
static struct
{
int8_t gpio_pin;
volatile uint32_t pulse_width_us;
volatile int64_t last_edge_time;
volatile int64_t pulse_start_time;
volatile bool last_level;
volatile bool signal_active;
volatile bool pull_detected;
uint8_t current_mode;
rcsignal_mode_change_callback_t callback;
bool initialized;
TaskHandle_t monitor_task;
} rcsignal = {
.gpio_pin = -1,
.pulse_width_us = 0,
.last_edge_time = 0,
.pulse_start_time = 0,
.last_level = false,
.signal_active = false,
.pull_detected = false,
.current_mode = 0,
.callback = NULL,
.initialized = false,
.monitor_task = NULL,
};
static void IRAM_ATTR gpio_isr_handler(void *arg)
{
int64_t now = esp_timer_get_time();
bool level = gpio_get_level(rcsignal.gpio_pin);
if (level && !rcsignal.last_level)
{
// Rising edge - start of pulse
rcsignal.pulse_start_time = now;
}
else if (!level && rcsignal.last_level)
{
// Falling edge - end of pulse
if (rcsignal.pulse_start_time > 0)
{
rcsignal.pulse_width_us = (uint32_t)(now - rcsignal.pulse_start_time);
rcsignal.last_edge_time = now;
rcsignal.signal_active = true;
}
}
rcsignal.last_level = level;
}
static void monitor_task(void *arg)
{
uint32_t last_pulse_width = 0;
while (1)
{
vTaskDelay(pdMS_TO_TICKS(10));
// Check for signal timeout
int64_t now = esp_timer_get_time();
if (rcsignal.signal_active && (now - rcsignal.last_edge_time) > (SIGNAL_TIMEOUT_MS * 1000))
{
rcsignal.signal_active = false;
rcsignal.pulse_width_us = 0;
}
// Detect mode change (rising edge on PWM signal > 1500us)
if (rcsignal.pulse_width_us != last_pulse_width)
{
last_pulse_width = rcsignal.pulse_width_us;
if (rcsignal.pulse_width_us < PULSE_THRESHOLD_US)
{
rcsignal.pull_detected = true;
}
if (rcsignal.pulse_width_us > PULSE_THRESHOLD_US && rcsignal.pull_detected)
{
// Mode change detected
rcsignal.pull_detected = false;
rcsignal.current_mode = (rcsignal.current_mode + 1) % MAX_MODES;
ESP_LOGI(TAG, "Mode changed to: %d (pulse: %lu us)",
rcsignal.current_mode, rcsignal.pulse_width_us);
if (rcsignal.callback)
{
rcsignal.callback(rcsignal.current_mode);
}
}
}
}
}
esp_err_t rcsignal_init(int8_t pin)
{
if (pin < 0)
{
ESP_LOGI(TAG, "RC signal disabled (no pin configured)");
return ESP_OK;
}
rcsignal.gpio_pin = pin;
// Configure GPIO
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << pin),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_ANYEDGE,
};
ESP_ERROR_CHECK(gpio_config(&io_conf));
// Install ISR service
ESP_ERROR_CHECK(gpio_install_isr_service(0));
ESP_ERROR_CHECK(gpio_isr_handler_add(pin, gpio_isr_handler, NULL));
// Create monitor task
BaseType_t ret = xTaskCreate(monitor_task, "rcsignal_monitor", 2048, NULL, 5, &rcsignal.monitor_task);
if (ret != pdPASS)
{
gpio_isr_handler_remove(pin);
gpio_uninstall_isr_service();
return ESP_FAIL;
}
rcsignal.initialized = true;
ESP_LOGI(TAG, "RC signal initialized on GPIO%d", pin);
return ESP_OK;
}
void rcsignal_deinit(void)
{
if (!rcsignal.initialized)
return;
if (rcsignal.monitor_task)
{
vTaskDelete(rcsignal.monitor_task);
rcsignal.monitor_task = NULL;
}
if (rcsignal.gpio_pin >= 0)
{
gpio_isr_handler_remove(rcsignal.gpio_pin);
}
rcsignal.initialized = false;
}
void rcsignal_register_callback(rcsignal_mode_change_callback_t callback)
{
rcsignal.callback = callback;
}
uint32_t rcsignal_get_pulse_width(void)
{
return rcsignal.pulse_width_us;
}
bool rcsignal_is_active(void)
{
return rcsignal.signal_active;
}
void rcsignal_trigger_mode_change(void)
{
rcsignal.current_mode = (rcsignal.current_mode + 1) % MAX_MODES;
ESP_LOGI(TAG, "Manual mode change to: %d", rcsignal.current_mode);
if (rcsignal.callback)
{
rcsignal.callback(rcsignal.current_mode);
}
}
uint8_t rcsignal_get_current_mode(void)
{
return rcsignal.current_mode;
}

60
main/rcsignal.h Normal file
View File

@ -0,0 +1,60 @@
/**
* @file rcsignal.h
* @brief RC PWM signal reading and parsing module
*/
#ifndef RCSIGNAL_H
#define RCSIGNAL_H
#include <stdint.h>
#include <stdbool.h>
#include "esp_err.h"
/**
* @brief Callback function type for mode changes
* @param new_mode New animation mode (0-13)
*/
typedef void (*rcsignal_mode_change_callback_t)(uint8_t new_mode);
/**
* @brief Initialize RC signal reading
* @param pin GPIO pin for PWM input (-1 to disable)
* @return ESP_OK on success
*/
esp_err_t rcsignal_init(int8_t pin);
/**
* @brief Deinitialize RC signal reading
*/
void rcsignal_deinit(void);
/**
* @brief Register callback for mode changes
* @param callback Callback function
*/
void rcsignal_register_callback(rcsignal_mode_change_callback_t callback);
/**
* @brief Get current PWM pulse width in microseconds
* @return Pulse width in µs (0 if no signal)
*/
uint32_t rcsignal_get_pulse_width(void);
/**
* @brief Check if PWM signal is active
* @return true if signal detected in last 100ms
*/
bool rcsignal_is_active(void);
/**
* @brief Manually trigger mode change (for emulation)
*/
void rcsignal_trigger_mode_change(void);
/**
* @brief Get current mode
* @return Current animation mode (0-13)
*/
uint8_t rcsignal_get_current_mode(void);
#endif // RCSIGNAL_H