basic led control with local btn

This commit is contained in:
2026-01-05 22:18:58 +01:00
parent 05be118dd1
commit fa6a19d1aa
17 changed files with 156 additions and 2138 deletions

View File

@ -1,390 +0,0 @@
# Architecture Documentation
## System Overview
The ESP32 LED Controller is a real-time embedded system designed for model aircraft LED control with the following key components:
```
┌─────────────────────────────────────────────────────────┐
│ Application Layer │
│ ┌──────────┐ ┌───────────┐ ┌──────────────────────┐ │
│ │ main.c │ │ Web-BLE │ │ Animation Patterns │ │
│ └────┬─────┘ └─────┬─────┘ └──────────┬───────────┘ │
└───────┼──────────────┼────────────────────┼─────────────┘
│ │ │
┌───────┴──────────────┴────────────────────┴─────────────┐
│ Control Layer │
│ ┌──────────────────────────────────────────────────┐ │
│ │ control.c - System Orchestration │ │
│ │ - BLE GATT Server │ │
│ │ - NVS Configuration Storage │ │
│ │ - OTA Firmware Update │ │
│ │ - Subsystem Initialization │ │
│ └──────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
│ │ │
┌───────┼──────────────┼────────────────────┼─────────────┐
│ │ │ │ │
│ ┌────▼────┐ ┌────▼─────┐ ┌────▼─────────┐ │
│ │ LED │ │ RC Signal│ │ Animation │ │
│ │ Driver │ │ Reader │ │ Engine │ │
│ └─────────┘ └──────────┘ └──────────────┘ │
│ │
│ Hardware Layer │
└──────────────────────────────────────────────────────────┘
│ │ │
┌───────┼──────────────┼────────────────────┼─────────────┐
│ ┌────▼────┐ ┌────▼─────┐ │
│ │ RMT │ │ GPIO │ │
│ │ (WS2812)│ │ (PWM) │ │
│ └─────────┘ └──────────┘ │
│ │
│ ESP-IDF HAL & Drivers │
└──────────────────────────────────────────────────────────┘
```
## Module Details
### 1. main.c - Application Entry Point
**Responsibilities:**
- System initialization orchestration
- Animation task creation and management
- System health monitoring
- Error handling and recovery
**Key Functions:**
- `app_main()`: Entry point, initializes control system
- `animation_task()`: FreeRTOS task for 60 FPS animation updates
**Task Priority:** Animation task runs at priority 5 (above default)
---
### 2. control.c/h - System Control & Orchestration
**Responsibilities:**
- BLE GATT server implementation
- Configuration management (NVS)
- OTA firmware update handling
- Subsystem initialization and coordination
**BLE Service Structure:**
```
Service UUID: 0x00FF
├── Config Characteristic (0xFF01)
│ ├── Read: Get current configuration
│ └── Write: Update and persist configuration
├── Mode Characteristic (0xFF02)
│ ├── Read: Get current animation mode
│ └── Write: Set animation mode
├── PWM Emulation (0xFF03)
│ └── Write: Trigger mode change
└── OTA Characteristic (0xFF04)
└── Write: Stream firmware binary
```
**Configuration Storage:**
```c
typedef struct {
int8_t led_pin_strip_a; // -1 = disabled
int8_t led_pin_strip_b; // -1 = disabled
int8_t pwm_pin; // -1 = disabled
ble_timeout_t ble_timeout; // 0, 60, 300 seconds
uint32_t magic; // 0xDEADBEEF validation
} controller_config_t;
```
**NVS Namespace:** `led_ctrl`
**NVS Key:** `config`
**BLE Timeout Logic:**
- Timer starts at boot
- Pauses when device connects
- Resumes when device disconnects
- Disables BLE advertising on timeout
**OTA Update Flow:**
1. Client writes firmware data in 512-byte chunks
2. ESP32 writes to inactive OTA partition
3. Last chunk (< 512 bytes) triggers completion
4. Validates partition, sets boot partition
5. Resets configuration and restarts
---
### 3. led.c/h - WS2812B LED Driver
**Technology:** ESP32 RMT (Remote Control) peripheral
**Why RMT?**
- Hardware timing generation (no CPU overhead)
- Precise WS2812B timing requirements:
- T0H: 350ns ±150ns
- T0L: 900ns ±150ns
- T1H: 900ns ±150ns
- T1L: 350ns ±150ns
- Reset: >280µs
**Implementation:**
```
RMT Channel → Custom Encoder → WS2812B Strip
↓ ↓ ↓
80 MHz GRB Encoding Serial Data
```
**Color Management:**
- Internal RGB buffer for each strip
- Thread-safe access via mutex
- Hardware converts RGB → GRB for WS2812B
- Support for HSV → RGB conversion
**Key Functions:**
- `led_init()`: Configure RMT channels
- `led_set_pixel_a/b()`: Set individual LED
- `led_fill_a/b()`: Set all LEDs same color
- `led_show()`: Update physical LEDs
- `led_fade_to_black()`: Fade effect for trails
**Memory Usage:**
- Strip A buffer: num_leds * 3 bytes (RGB)
- Strip B buffer: num_leds * 3 bytes (RGB)
- Default: 44 LEDs * 2 strips * 3 = 264 bytes
---
### 4. rcsignal.c/h - PWM Signal Reader
**Technology:** GPIO interrupt + software edge detection
**PWM Signal Specification:**
- Standard RC PWM: 1000-2000µs pulse width
- Detection threshold: 1500µs
- Timeout: 100ms (signal loss detection)
**Mode Change Logic:**
```
PWM < 1500µs → Set "pull_detected" flag
PWM > 1500µs AND pull_detected → Mode++
pull_detected = false
```
**Implementation:**
- ISR captures rising/falling edges
- Calculates pulse width in microseconds
- Monitor task (10ms interval) detects mode changes
- Callback notifies animation system
**Thread Safety:**
- Volatile variables for ISR communication
- Monitor task runs at priority 5
---
### 5. animation.c/h - Animation Engine
**Update Rate:** 60 FPS (16.67ms per frame)
**Global State:**
- `global_hue`: Slow color cycling (updates every 3 frames)
- `frame_counter`: Frame synchronization
- `current_mode`: Active animation pattern
**Animation Techniques:**
#### Fade Effects
```c
// Smooth trails for chase animations
led_fade_to_black(amount);
// Each LED: color = (color * (255 - amount)) / 255
```
#### Beat Synchronization
```c
// Sine wave based on BPM and time
beatsin16(bpm, min_val, max_val);
// Returns position oscillating between min and max
```
#### HSV Color Space
- Hue: 0-255 (color wheel)
- Saturation: 0-255 (color intensity)
- Value: 0-255 (brightness)
- Automatic RGB conversion
**Animation Modes Breakdown:**
1. **Static Colors** (Black, Red, Blue, Green, White)
- Single `led_fill()` call
- No per-frame updates needed
2. **Rainbow**
- HSV hue gradient across strip
- Hue offset per LED: `global_hue + (i * 7)`
- Global hue increments for animation
3. **Rainbow Glitter**
- Base rainbow + random white sparkles
- 80/255 chance per frame
- Random LED position
4. **Confetti**
- Fade to black (10/255 per frame)
- Random position + random hue
- Creates "fireworks" effect
5. **Sinelon**
- Sweeping dot using sine wave
- Position: `beatsin16(13 BPM)`
- 20/255 fade creates trails
6. **BPM**
- Color palette based on party colors
- Beat: `beatsin8(33 BPM)`
- Brightness modulation per LED
7. **Navigation**
- Fixed positions for aviation lights
- Red: LEDs 0-2 (left)
- Green: Last 3 LEDs (right)
- White blink: 30 Hz (half frame rate)
8. **Chase (Red)**
- Red dot with ±2 LED trail
- Position: `beatsin16(40 BPM)`
- No fade (instant clear)
9. **Chase RGB**
- Same as Chase but HSV color cycling
- Hue: `global_hue`
10. **Random**
- Random LED, random color each frame
- Rare full clear event
---
## Data Flow
### Configuration Update Flow
```
Web Browser → BLE Write → control.c → NVS Save → Restart (if pins changed)
```
### Animation Update Flow
```
animation_task (60Hz) → animation_update() → LED buffer → led_show() → RMT → LEDs
```
### PWM Mode Change Flow
```
RC Signal → GPIO ISR → rcsignal.c → Callback → control.c → animation.c
```
### OTA Update Flow
```
Web Browser → BLE Write (chunks) → control.c → esp_ota → Flash → Restart
```
## Thread Safety
### Mutexes Used
1. **led_mutex**: Protects LED buffer access
- Used by: animation_update(), led_show()
- Type: FreeRTOS mutex
### ISR Safety
- **rcsignal.c**: Volatile variables for ISR communication
- **Minimal ISR work**: Only timestamp and edge detection
- **Deferred processing**: Monitor task handles logic
### Task Priorities
```
Priority 5: animation_task, rcsignal_monitor_task
Priority 1: BLE stack tasks (default)
Priority 0: Idle task
```
## Memory Management
### Static Allocation
- Configuration structure: 11 bytes (NVS)
- Animation state: ~100 bytes (global variables)
### Dynamic Allocation
- LED buffers: `num_leds * 3 * 2` bytes (both strips)
- RMT encoder: ~200 bytes per strip
- BLE stack: ~30KB (ESP-IDF managed)
### Flash Usage
- Code: ~500KB (with BLE stack)
- OTA partitions: 2x 1MB (dual-boot)
- NVS: 24KB
- Factory: 1MB
### Heap Usage Estimate
- Total: ~50KB during operation
- Available: ~250KB on ESP32
## Power Optimization
### Active Mode
- CPU: 240 MHz (animation processing)
- BLE: Active (advertising/connected)
- Power: ~180mA (ESP32 only)
### BLE Disabled Mode
- CPU: 240 MHz (animation only)
- BLE: Disabled after timeout
- Power: ~100mA (ESP32 only)
### LED Power
- Per LED: ~60mA at full white
- 44 LEDs full white: ~2.6A
- Typical animation: ~500mA average
## ESP32 vs ESP32-C3 Differences
### ESP32 (Xtensa)
- Dual-core: FreeRTOS symmetric multiprocessing
- BLE + Classic Bluetooth controller
- More GPIO pins available
- Recommended for complex projects
### ESP32-C3 (RISC-V)
- Single-core: Simpler task management
- BLE only (no Classic Bluetooth)
- Fewer GPIO pins
- Lower cost option
### Compatibility
- Same codebase works on both
- Pin numbers differ (check datasheet)
- RMT peripheral identical
- BLE functionality identical
## Performance Characteristics
### Latency
- **PWM detection**: <10ms
- **BLE command**: <100ms
- **Mode change**: <20ms (next frame)
- **LED update**: 16.67ms (60 FPS locked)
### Throughput
- **LED data**: ~13.44 Mbps theoretical (RMT)
- **BLE**: ~1 Mbps (limited by MTU)
- **OTA**: ~40 KB/s (BLE transfer)
### Timing Precision
- **Animation frame**: ±0.5ms jitter
- **WS2812B timing**: ±50ns (RMT hardware)
- **PWM measurement**: ±1µs (ISR timing)
---
## Future Enhancement Ideas TODO
1. **Dynamic LED Count**: Auto-detect number of LEDs
2. **Multi-Strip Sync**: Synchronized patterns
3. **Pattern Editor**: Visual animation designer
4. **Scheduler**: Time-based mode changes

View File

@ -1,6 +1,6 @@
# ESP32 LED Controller for Model Aircraft
Professional LED controller firmware for ESP32 with Web-BLE configuration interface. Designed for model aircraft with WS2812B LED strips.
Professional LED controller firmware for ESP32. Designed for model aircraft with WS2812B LED strips.
## Features
@ -26,28 +26,17 @@ Professional LED controller firmware for ESP32 with Web-BLE configuration interf
13. **Chase (RGB)** - RGB cycling chase effect
14. **Random** - Random LED colors
### Web-BLE Configuration
- **Pin Configuration**: Set GPIO pins for LED strips and PWM input
- **BLE Auto-Off**: Configure timeout (Never/1min/5min)
- **Manual Control**: Select animation modes from web interface
- **PWM Emulation**: Test mode switching without RC signal
- **OTA Firmware Update**: Upload new firmware via Bluetooth
## Project Structure
```
led-controller-firmware/
├── main/
│ ├── main.c # Application entry point
│ ├── control.c/h # BLE, NVS, initialization
│ ├── control.c/h # NVS, initialization
│ ├── led.c/h # WS2812B control (RMT driver)
│ ├── rcsignal.c/h # PWM signal reading
│ ├── localbtn.c/h # Local btn reading
│ └── animation.c/h # LED animation patterns
├── webapp/
│ ├── index.html # Web-BLE interface
│ ├── app/app.js # BLE communication logic
│ ├── css/style.css # UI styling
│ └── data/favicon.ico
├── CMakeLists.txt
├── sdkconfig.defaults
└── partitions.csv # OTA-enabled partition table
@ -107,42 +96,6 @@ GND -> Common Ground
- 1500µs threshold for mode switching
- Rising edge >1500µs after <1500µs triggers next mode
## Web-BLE Configuration
### Access the Interface
1. Open `webapp/index.html` in Chrome, Edge, or Opera (Web Bluetooth required)
2. Click "Connect via BLE"
3. Select "LED-Controller" from device list
4. Configure settings and control LEDs
### Configuration Options
#### Pin Setup
- **LED Strip A GPIO**: -1 to disable, 0-48 for GPIO pin
- **LED Strip B GPIO**: -1 to disable, 0-48 for GPIO pin
- **PWM Input GPIO**: -1 to disable, 0-48 for GPIO pin
#### BLE Timeout
- **Never**: BLE stays on until manually disabled
- **1 Minute**: Auto-disable after 1 min of boot (unless connected)
- **5 Minutes**: Auto-disable after 5 min of boot (unless connected)
#### Firmware Update
1. Build firmware: `idf.py build`
2. Find binary: `build/led_controller.bin`
3. Upload via Web-BLE interface
4. Device restarts with new firmware (settings reset)
## Default Configuration
On first boot or after reset:
- All pins: **Not configured** (-1)
- BLE timeout: **Never**
- Animation mode: **Black** (off)
Configure via Web-BLE before use.
## Development
### Adding New Animations
@ -154,7 +107,7 @@ Configure via Web-BLE before use.
### Modifying LED Count
Edit `DEFAULT_NUM_LEDS_A` and `DEFAULT_NUM_LEDS_B` in `control.c`. TODO
Edit `DEFAULT_NUM_LEDS_A` and `DEFAULT_NUM_LEDS_B` in `control.c`. TODO:
### Testing

View File

@ -1,212 +0,0 @@
boolean getRC01() {
rc01Val = pulseIn(rc01, HIGH);
//Serial.println(rc01Val);
if (rc01Val > 1500) {
//Serial.println("RC1 ON");
return true;
} else {
//Serial.println("RC1 OFF");
return false;
}
}
boolean getRC02() {
rc02Val = pulseIn(rc02, HIGH);
// Serial.println(rc02Val);
if (rc02Val < 1500) {
pullRC = true;
}
if (rc02Val > 1500 && pullRC) {
//Serial.println("RC2 ON");
pullRC = false;
return true;
} else {
//Serial.println("RC2 OFF");
return false;
}
}
void serialPrintModus(int modus) {
switch (modus) {
case 0:
Serial.println("Black");
break;
case 1:
Serial.println("Red");
break;
case 2:
Serial.println("Blue");
break;
case 3:
Serial.println("Green");
break;
case 4:
Serial.println("White");
break;
case 5:
Serial.println("Rainbow");
break;
case 6:
Serial.println("RainbowWithGlitter");
break;
case 7:
Serial.println("Confetti");
break;
case 8:
Serial.println("Sinelon");
break;
case 9:
Serial.println("BPM");
break;
case 10:
Serial.println("Navigation");
break;
case 11:
Serial.println("Chase");
break;
case 12:
Serial.println("ChaseRGB");
break;
case 13:
Serial.println("Random");
break;
}
}
void rainbow()
{
Serial.println("Rainbow");
// FastLED's built-in rainbow generator
fill_rainbow( leds, NUM_LEDS, gHue, 7);
}
void rainbowWithGlitter()
{
// built-in FastLED rainbow, plus some random sparkly glitter
rainbow();
addGlitter(255);
}
void addGlitter( fract8 chanceOfGlitter)
{
if ( random8() < chanceOfGlitter) {
leds[ random16(NUM_LEDS) ] += CRGB::White;
}
}
void confetti()
{
// random colored speckles that blink in and fade smoothly
fadeToBlackBy( leds, NUM_LEDS, 10);
int pos = random16(NUM_LEDS);
leds[pos] += CHSV( gHue + random8(64), 200, 255);
}
void sinelon()
{
// a colored dot sweeping back and forth, with fading trails
fadeToBlackBy( leds, NUM_LEDS, 20);
int pos = beatsin16(13, 0, NUM_LEDS);
leds[pos] += CHSV( gHue, 255, 192);
}
void bpm()
{
// colored stripes pulsing at a defined Beats-Per-Minute (BPM)
uint8_t BeatsPerMinute = 33;
CRGBPalette16 palette = PartyColors_p;
uint8_t beat = beatsin8( BeatsPerMinute, 64, 255);
for ( int i = 0; i < NUM_LEDS; i++) { //9948
leds[i] = ColorFromPalette(palette, gHue + (i * 2), beat - gHue + (i * 10));
}
}
void blackMode() {
fill_solid(leds, NUM_LEDS, CRGB::Black); // Just to be sure, let's really make it BLACK.
}
void redMode() {
fill_solid(leds, NUM_LEDS, CRGB::Red);
}
void blueMode() {
fill_solid(leds, NUM_LEDS, CRGB::Blue);
}
void greenMode() {
fill_solid(leds, NUM_LEDS, CRGB::Green);
}
void whiteMode() {
fill_solid(leds, NUM_LEDS, CRGB::White);
}
void navigation() {
FastLED.clear();
leds[0] = CRGB::Red;
leds[1] = CRGB::Red;
leds[2] = CRGB::Red;
leds[41] = CRGB::Green;
leds[42] = CRGB::Green;
leds[43] = CRGB::Green;
leds[5] = CRGB::White;
leds[6] = CRGB::White;
leds[37] = CRGB::White;
leds[38] = CRGB::White;
FastLED.delay(100);
leds[5] = CRGB::Black;
leds[6] = CRGB::Black;
leds[37] = CRGB::Black;
leds[38] = CRGB::Black;
}
void chase() {
FastLED.clear();
// a colored dot sweeping back and forth, with fading trails
//fadeToBlackBy( leds, NUM_LEDS, 20);
int pos = beatsin16(40, 0, NUM_LEDS);
leds[pos] = CRGB::Red;
if (pos < 41) {
leds[pos + 1] = CRGB::Red;
leds[pos + 2] = CRGB::Red;
}
if (pos > 1) {
leds[pos - 1] = CRGB::Red;
leds[pos - 2] = CRGB::Red;
}
}
void chaseRGB() {
FastLED.clear();
// a colored dot sweeping back and forth, with fading trails
//fadeToBlackBy( leds, NUM_LEDS, 20);
int pos = beatsin16(40, 0, NUM_LEDS);
leds[pos] += CHSV( gHue, 255, 192);
if (pos < 41) {
leds[pos + 1] += CHSV( gHue, 255, 192);
leds[pos + 2] += CHSV( gHue, 255, 192);
}
if (pos > 1) {
leds[pos - 1] += CHSV( gHue, 255, 192);
leds[pos - 2] += CHSV( gHue, 255, 192);
}
}
void randomMode(){
randomVal = random(0,45);
if(randomVal == 44){
if(random(5,11) == 9){
FastLED.clear();
}
}else{
leds[randomVal] = random(0, 16777216);
}
}

View File

@ -5,10 +5,6 @@ idf_component_register(
"led.c"
"rcsignal.c"
"animation.c"
"localbtn.c"
INCLUDE_DIRS "."
EMBED_FILES
"../webapp/index.html"
"../webapp/app/app.js"
"../webapp/css/style.css"
"../webapp/data/favicon.ico"
)

View File

@ -190,7 +190,7 @@ static void anim_bpm(void)
// PartyColors palette simulation
const uint8_t palette_colors[] = {
170, 240, 90, 150, 210, 30, 180, 0,
210, 270, 150, 240, 330, 60, 300, 120};
210, 255, 150, 240, 255, 60, 255, 120};
for (uint16_t i = 0; i < num_leds_a; i++)
{

View File

@ -1,26 +1,21 @@
/**
* @file control.c
* @brief Control module implementation with BLE, NVS, and OTA
* @brief Control module implementation
*/
#include "control.h"
#include "led.h"
#include "rcsignal.h"
#include "localbtn.h"
#include "animation.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.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>
@ -28,66 +23,19 @@ 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)
#define DEFAULT_NUM_LEDS_A 7
#define DEFAULT_NUM_LEDS_B 7
// 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
@ -115,7 +63,6 @@ static esp_err_t load_config_from_nvs(void)
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;
}
@ -152,10 +99,9 @@ static esp_err_t save_config_to_nvs(void)
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.led_pin_strip_a = 12;
current_config.led_pin_strip_b = 14;
current_config.pwm_pin = 13;
current_config.magic = CONFIG_MAGIC;
return save_config_to_nvs();
@ -191,59 +137,6 @@ esp_err_t control_update_config(const controller_config_t *config)
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)
{
@ -270,361 +163,6 @@ 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)
{
@ -669,17 +207,18 @@ esp_err_t control_init(void)
return ret;
}
// Register mode change callback
rcsignal_register_callback(on_mode_change);
// Initialize BLE
ret = init_ble();
// Initialize local BTN
ret = localbtn_init();
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "BLE init failed: %s", esp_err_to_name(ret));
ESP_LOGE(TAG, "Local BTN init failed: %s", esp_err_to_name(ret));
return ret;
}
// Register mode change callback
rcsignal_register_callback(on_mode_change);
localbtn_register_callback(on_mode_change);
ESP_LOGI(TAG, "Control system initialized successfully");
return ESP_OK;

View File

@ -1,6 +1,6 @@
/**
* @file control.h
* @brief Control module for LED controller - handles initialization of LEDs, PWM, and Bluetooth
* @brief Control module for LED controller - handles initialization of LEDs, PWM
*/
#ifndef CONTROL_H
@ -10,24 +10,15 @@
#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)
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)
uint32_t magic; // Magic number to validate config (0xDEADBEEF)
} controller_config_t;
/**
@ -41,14 +32,14 @@ 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);
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);
esp_err_t control_update_config(const controller_config_t *config);
/**
* @brief Reset configuration to defaults
@ -56,22 +47,6 @@ esp_err_t control_update_config(const controller_config_t* config);
*/
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)

85
main/localbtn.c Normal file
View File

@ -0,0 +1,85 @@
/**
* @file localbtn.c
* @brief Local GPIO0 BTN reading implementation using edge capture
*/
#include "localbtn.h"
#include "driver/gpio.h"
#include "esp_timer.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "soc/gpio_num.h"
#include <string.h>
static const char *TAG = "LOCALBTN";
uint8_t current_mode;
localbtn_mode_change_callback_t callback;
#define BOOT_BTN GPIO_NUM_0
#define MAX_MODES 14 // TODO: get from control
bool boot_button_pressed(void)
{
return gpio_get_level(BOOT_BTN) == 0; // active LOW
}
static void monitor_task(void *arg)
{
bool lastState = false;
while (1)
{
vTaskDelay(pdMS_TO_TICKS(10));
bool currentState = boot_button_pressed();
if ((currentState) && (lastState != currentState))
{
printf("BOOT button pressed\n");
current_mode = (current_mode + 1) % MAX_MODES;
ESP_LOGI(TAG, "Mode changed to: %d ", current_mode);
if (callback)
{
callback(current_mode);
}
}
lastState = currentState;
}
}
esp_err_t localbtn_init()
{
gpio_config_t io_conf = {
.pin_bit_mask = 1ULL << BOOT_BTN,
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE, // safe even if external pull-up exists
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE};
ESP_ERROR_CHECK(gpio_config(&io_conf));
// Create monitor task
BaseType_t ret = xTaskCreate(monitor_task, "localbtn_monitor", 2048, NULL, 5, NULL);
if (ret != pdPASS)
{
return ESP_FAIL;
}
// TODO: rcsignal.initialized = true;
ESP_LOGI(TAG, "local btn initialized on GPIO%d", BOOT_BTN);
return ESP_OK;
}
void localbtn_deinit(void)
{
// TODO:
}
void localbtn_register_callback(localbtn_mode_change_callback_t cb)
{
callback = cb;
}

36
main/localbtn.h Normal file
View File

@ -0,0 +1,36 @@
/**
* @file localbtn.h
* @brief Local GPIO0 BTN reading implementation using edge capture
*/
#ifndef LOCALBTN_H
#define LOCALBTN_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 (*localbtn_mode_change_callback_t)(uint8_t new_mode);
/**
* @brief Initialize local btn reading
* @return ESP_OK on success
*/
esp_err_t localbtn_init();
/**
* @brief Deinitialize local btn reading
*/
void localbtn_deinit(void);
/**
* @brief Register callback for mode changes
* @param callback Callback function
*/
void localbtn_register_callback(localbtn_mode_change_callback_t cb);
#endif // LOCALBTN_H

View File

@ -44,7 +44,7 @@ void app_main(void)
ESP_LOGI(TAG, " ESP32 LED Controller for Model Aircraft");
ESP_LOGI(TAG, "==============================================");
// Initialize control system (LEDs, PWM, BLE)
// Initialize control system (LEDs, PWM)
esp_err_t ret = control_init();
if (ret != ESP_OK)
{
@ -56,6 +56,8 @@ void app_main(void)
}
}
control_reset_config();
// Create animation update task
BaseType_t task_ret = xTaskCreate(
animation_task,
@ -76,8 +78,6 @@ void app_main(void)
}
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)
@ -85,10 +85,6 @@ void app_main(void)
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
ESP_LOGI(TAG, "Status - Mode: %d", control_get_animation_mode());
}
}

View File

@ -1,85 +0,0 @@
#include "FastLED.h"
int rc01 = 9;
int rc02 = 10;
int led_spotlight = 2;
int rc01Val = 0;
int rc02Val = 0;
int modus = 0;
int modusMax = 13;
int red = 0;
int green = 0;
int blue = 0;
int randomVal = 0;
#define DATA_PIN 3
#define LED_TYPE WS2812B
#define COLOR_ORDER GRB
#define NUM_LEDS 44
CRGB leds[NUM_LEDS];
#define BRIGHTNESS 255
#define FRAMES_PER_SECOND 60
#define ARRAY_SIZE(A) (sizeof(A) / sizeof((A)[0]))
boolean pullRC = true;
void setup() {
//Serial.begin(9600);
Serial.println("_-_-_- Night Fly Controller V01 _-_-_-");
pinMode(rc01, INPUT);
pinMode(rc02, INPUT);
pinMode(led_spotlight, OUTPUT);
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, HIGH);
delay(3000); // 3 second delay for recovery
// tell FastLED about the LED strip configuration
FastLED.addLeds<LED_TYPE, DATA_PIN, COLOR_ORDER>(leds, NUM_LEDS).setCorrection(TypicalLEDStrip);
// set master brightness control
FastLED.setBrightness(BRIGHTNESS);
}
// List of patterns to cycle through. Each is defined as a separate function below.
typedef void (*SimplePatternList[])();
SimplePatternList gPatterns = { blackMode, redMode, blueMode, greenMode, whiteMode, rainbow, rainbowWithGlitter, confetti, sinelon, bpm, navigation, chase, chaseRGB, randomMode };
uint8_t gHue = 0; // rotating "base color" used by many of the patterns
void loop() {
if (getRC01()) {
digitalWrite(led_spotlight, HIGH);
} else {
digitalWrite(led_spotlight, LOW);
}
setModus();
}
void setModus(){
if (getRC02()) {
modus = modus + 1;
if (modus > modusMax) {
modus = 1;
}
}
Serial.println(modus);
serialPrintModus(modus);
gPatterns[modus]();
// send the 'leds' array out to the actual LED strip
FastLED.show();
// insert a delay to keep the framerate modest
FastLED.delay(1000 / FRAMES_PER_SECOND);
// do some periodic updates
EVERY_N_MILLISECONDS( 20 ) {
gHue++; // slowly cycle the "base color" through the rainbow
}
}

View File

@ -1,120 +0,0 @@
#!/usr/bin/env python3
"""
Ad-hoc Flask HTTPS Server for LED Controller Webapp
Serves the webapp over HTTPS (required for Web Bluetooth API)
"""
import os
import sys
from flask import Flask, send_from_directory, send_file
from pathlib import Path
# Configuration
HOST = '0.0.0.0' # Listen on all interfaces
PORT = 5000 # HTTPS port
DEBUG = True
# Get webapp directory (one level up from tools/)
SCRIPT_DIR = Path(__file__).parent
WEBAPP_DIR = SCRIPT_DIR.parent / 'webapp'
# Verify webapp directory exists
if not WEBAPP_DIR.exists():
print(f"❌ ERROR: Webapp directory not found at {WEBAPP_DIR}")
print(f" Please run this script from the base repository directory")
sys.exit(1)
# Create Flask app
app = Flask(__name__)
@app.route('/')
def index():
"""Serve index.html"""
return send_file(WEBAPP_DIR / 'index.html')
@app.route('/app/<path:filename>')
def serve_app(filename):
"""Serve files from app/ directory"""
return send_from_directory(WEBAPP_DIR / 'app', filename)
@app.route('/css/<path:filename>')
def serve_css(filename):
"""Serve files from css/ directory"""
return send_from_directory(WEBAPP_DIR / 'css', filename)
@app.route('/data/<path:filename>')
def serve_data(filename):
"""Serve files from data/ directory"""
return send_from_directory(WEBAPP_DIR / 'data', filename)
@app.route('/favicon.ico')
def favicon():
"""Serve favicon"""
return send_file(WEBAPP_DIR / 'data' / 'favicon.ico')
def print_banner():
"""Print startup banner with instructions"""
print("=" * 70)
print(" 🚀 LED Controller HTTPS Development Server")
print("=" * 70)
print()
print("📱 Web Bluetooth requires HTTPS!")
print(" This server provides a self-signed certificate for development.")
print()
print("🌐 Access the webapp at:")
print(f" https://localhost:{PORT}")
print(f" https://127.0.0.1:{PORT}")
print(f" https://<your-ip>:{PORT}")
print()
print("⚠️ Browser Security Warning:")
print(" You'll see a 'Not Secure' warning - this is normal!")
print(" Click 'Advanced''Proceed to localhost' (or similar)")
print()
print("🔧 To stop the server: Press Ctrl+C")
print("=" * 70)
print()
def get_local_ip():
"""Get local IP address for convenience"""
import socket
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
s.close()
return ip
except:
return "unknown"
if __name__ == '__main__':
print_banner()
# Print local IP for convenience
local_ip = get_local_ip()
if local_ip != "unknown":
print(f"💡 Your local IP: {local_ip}")
print(f" Access from phone: https://{local_ip}:{PORT}")
print()
print("🔄 Starting HTTPS server...")
print()
try:
# Run with ad-hoc SSL context (self-signed certificate)
# Flask will automatically generate a certificate
app.run(
host=HOST,
port=PORT,
debug=DEBUG,
ssl_context='adhoc' # Auto-generate self-signed cert
)
except OSError as e:
if "Address already in use" in str(e):
print(f"❌ ERROR: Port {PORT} is already in use!")
print(f" Try a different port or stop the other service.")
sys.exit(1)
else:
raise
except KeyboardInterrupt:
print("\n\n👋 Server stopped by user")
sys.exit(0)

View File

@ -1,5 +0,0 @@
# Flask HTTPS Development Server Requirements
# Install with: pip install -r requirements.txt
Flask>=2.3.0
pyOpenSSL>=23.0.0 # Required for adhoc SSL certificate generation

View File

@ -1,322 +0,0 @@
/**
* LED Controller Web-BLE Application
* Communicates with ESP32 LED Controller via Bluetooth Low Energy
*/
// BLE Service and Characteristic UUIDs
const SERVICE_UUID = 0x00FF;
const CHAR_CONFIG_UUID = 0xFF01;
const CHAR_MODE_UUID = 0xFF02;
const CHAR_PWM_UUID = 0xFF03;
const CHAR_OTA_UUID = 0xFF04;
// Animation mode names
const MODE_NAMES = [
'Black', 'Red', 'Blue', 'Green', 'White',
'Rainbow', 'Rainbow Glitter', 'Confetti', 'Sinelon',
'BPM Pulse', 'Navigation', 'Chase (Red)', 'Chase (RGB)', 'Random'
];
class LEDController {
constructor() {
this.device = null;
this.server = null;
this.service = null;
this.characteristics = {
config: null,
mode: null,
pwm: null,
ota: null
};
this.connected = false;
this.initUI();
}
initUI() {
// Connect button
document.getElementById('connect-btn').addEventListener('click', () => this.connect());
// Save configuration button
document.getElementById('save-config-btn').addEventListener('click', () => this.saveConfig());
// PWM emulation button
document.getElementById('pwm-emulate-btn').addEventListener('click', () => this.emulatePWM());
// Mode selection buttons
document.querySelectorAll('.btn-mode').forEach(btn => {
btn.addEventListener('click', (e) => {
const mode = parseInt(e.target.dataset.mode);
this.setMode(mode);
});
});
// Firmware file selection
document.getElementById('firmware-file').addEventListener('change', (e) => {
const uploadBtn = document.getElementById('ota-upload-btn');
uploadBtn.disabled = !e.target.files.length;
});
// OTA upload button
document.getElementById('ota-upload-btn').addEventListener('click', () => this.uploadFirmware());
}
async connect() {
try {
console.log('Requesting Bluetooth Device...');
this.device = await navigator.bluetooth.requestDevice({
filters: [{ name: 'LED-Controller' }],
optionalServices: [SERVICE_UUID]
});
console.log('Connecting to GATT Server...');
this.server = await this.device.gatt.connect();
console.log('Getting Service...');
this.service = await this.server.getPrimaryService(SERVICE_UUID);
console.log('Getting Characteristics...');
this.characteristics.config = await this.service.getCharacteristic(CHAR_CONFIG_UUID);
this.characteristics.mode = await this.service.getCharacteristic(CHAR_MODE_UUID);
this.characteristics.pwm = await this.service.getCharacteristic(CHAR_PWM_UUID);
this.characteristics.ota = await this.service.getCharacteristic(CHAR_OTA_UUID);
this.connected = true;
this.updateConnectionStatus(true);
// Load current configuration
await this.loadConfig();
await this.loadCurrentMode();
// Show control sections
document.getElementById('config-section').classList.remove('hidden');
document.getElementById('control-section').classList.remove('hidden');
document.getElementById('ota-section').classList.remove('hidden');
console.log('Connected successfully!');
// Handle disconnection
this.device.addEventListener('gattserverdisconnected', () => {
this.onDisconnected();
});
} catch (error) {
console.error('Connection failed:', error);
alert('Failed to connect: ' + error.message);
}
}
async loadConfig() {
try {
const value = await this.characteristics.config.readValue();
const config = this.parseConfig(value);
document.getElementById('led-pin-a').value = config.ledPinA;
document.getElementById('led-pin-b').value = config.ledPinB;
document.getElementById('pwm-pin').value = config.pwmPin;
document.getElementById('ble-timeout').value = config.bleTimeout;
console.log('Configuration loaded:', config);
} catch (error) {
console.error('Failed to load config:', error);
}
}
async loadCurrentMode() {
try {
const value = await this.characteristics.mode.readValue();
const mode = value.getUint8(0);
this.updateCurrentMode(mode);
} catch (error) {
console.error('Failed to load current mode:', error);
}
}
parseConfig(dataView) {
return {
ledPinA: dataView.getInt8(0),
ledPinB: dataView.getInt8(1),
pwmPin: dataView.getInt8(2),
bleTimeout: dataView.getUint32(3, true),
magic: dataView.getUint32(7, true)
};
}
createConfigBuffer(config) {
const buffer = new ArrayBuffer(11);
const view = new DataView(buffer);
view.setInt8(0, config.ledPinA);
view.setInt8(1, config.ledPinB);
view.setInt8(2, config.pwmPin);
view.setUint32(3, config.bleTimeout, true);
view.setUint32(7, 0xDEADBEEF, true); // Magic number
return buffer;
}
async saveConfig() {
try {
const config = {
ledPinA: parseInt(document.getElementById('led-pin-a').value),
ledPinB: parseInt(document.getElementById('led-pin-b').value),
pwmPin: parseInt(document.getElementById('pwm-pin').value),
bleTimeout: parseInt(document.getElementById('ble-timeout').value)
};
const buffer = this.createConfigBuffer(config);
await this.characteristics.config.writeValue(buffer);
alert('Configuration saved! Device will restart if pins changed.');
console.log('Configuration saved:', config);
} catch (error) {
console.error('Failed to save config:', error);
alert('Failed to save configuration: ' + error.message);
}
}
async setMode(mode) {
try {
const buffer = new Uint8Array([mode]);
await this.characteristics.mode.writeValue(buffer);
this.updateCurrentMode(mode);
console.log('Mode set to:', MODE_NAMES[mode]);
} catch (error) {
console.error('Failed to set mode:', error);
}
}
async emulatePWM() {
try {
const buffer = new Uint8Array([1]);
await this.characteristics.pwm.writeValue(buffer);
console.log('PWM pulse emulated');
// Read back the new mode
setTimeout(() => this.loadCurrentMode(), 100);
} catch (error) {
console.error('Failed to emulate PWM:', error);
}
}
async uploadFirmware() {
const fileInput = document.getElementById('firmware-file');
const file = fileInput.files[0];
if (!file) {
alert('Please select a firmware file');
return;
}
if (!confirm('This will update the firmware and reset all settings. Continue?')) {
return;
}
try {
const arrayBuffer = await file.arrayBuffer();
const data = new Uint8Array(arrayBuffer);
const progressBar = document.getElementById('ota-progress-bar');
const progressText = document.getElementById('ota-progress-text');
const progressContainer = document.getElementById('ota-progress');
progressContainer.classList.remove('hidden');
const chunkSize = 512;
const totalChunks = Math.ceil(data.length / chunkSize);
console.log(`Starting OTA upload: ${data.length} bytes, ${totalChunks} chunks`);
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, data.length);
const chunk = data.slice(start, end);
await this.characteristics.ota.writeValue(chunk);
const progress = Math.round((i + 1) / totalChunks * 100);
progressBar.style.width = progress + '%';
progressText.textContent = progress + '%';
console.log(`Upload progress: ${progress}%`);
// Small delay to prevent overwhelming the device
await new Promise(resolve => setTimeout(resolve, 20));
}
alert('Firmware uploaded successfully! Device will restart.');
console.log('OTA upload complete');
} catch (error) {
console.error('OTA upload failed:', error);
alert('Firmware upload failed: ' + error.message);
}
}
updateCurrentMode(mode) {
const modeName = MODE_NAMES[mode] || 'Unknown';
document.getElementById('current-mode').textContent = modeName;
}
updateConnectionStatus(connected) {
const indicator = document.getElementById('status-indicator');
const statusText = document.getElementById('connection-status');
const connectBtn = document.getElementById('connect-btn');
if (connected) {
indicator.classList.remove('disconnected');
indicator.classList.add('connected');
statusText.textContent = 'Connected';
connectBtn.textContent = 'Disconnect';
connectBtn.onclick = () => this.disconnect();
} else {
indicator.classList.remove('connected');
indicator.classList.add('disconnected');
statusText.textContent = 'Disconnected';
connectBtn.textContent = 'Connect via BLE';
connectBtn.onclick = () => this.connect();
}
}
disconnect() {
if (this.device && this.device.gatt.connected) {
this.device.gatt.disconnect();
}
this.onDisconnected();
}
onDisconnected() {
console.log('Disconnected from device');
this.connected = false;
this.device = null;
this.server = null;
this.service = null;
this.characteristics = {
config: null,
mode: null,
pwm: null,
ota: null
};
this.updateConnectionStatus(false);
// Hide control sections
document.getElementById('config-section').classList.add('hidden');
document.getElementById('control-section').classList.add('hidden');
document.getElementById('ota-section').classList.add('hidden');
}
}
// Initialize app when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
// Check if Web Bluetooth is supported
if (!navigator.bluetooth) {
alert('Web Bluetooth is not supported in this browser. Please use Chrome, Edge, or Opera.');
document.getElementById('connect-btn').disabled = true;
return;
}
console.log('LED Controller Web-BLE Interface loaded');
new LEDController();
});

View File

@ -1,313 +0,0 @@
/* LED Controller Web-BLE Interface Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary-color: #2196F3;
--success-color: #4CAF50;
--warning-color: #FF9800;
--danger-color: #F44336;
--dark-bg: #1a1a2e;
--card-bg: #16213e;
--text-primary: #ffffff;
--text-secondary: #b0b0b0;
--border-color: #0f4c75;
--hover-bg: #1e3a5f;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, var(--dark-bg) 0%, #0f3460 100%);
color: var(--text-primary);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
}
header {
text-align: center;
margin-bottom: 30px;
}
header h1 {
font-size: 2.5em;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
}
.subtitle {
color: var(--text-secondary);
font-size: 1.1em;
}
.card {
background: var(--card-bg);
border-radius: 12px;
padding: 25px;
margin-bottom: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
border: 1px solid var(--border-color);
}
.card h2 {
margin-bottom: 20px;
color: var(--primary-color);
font-size: 1.5em;
}
.hidden {
display: none !important;
}
/* Connection Status */
.connection-status {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 15px;
font-size: 1.1em;
}
.status-indicator {
width: 16px;
height: 16px;
border-radius: 50%;
animation: pulse 2s infinite;
}
.status-indicator.disconnected {
background: var(--danger-color);
}
.status-indicator.connected {
background: var(--success-color);
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Form Elements */
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: var(--text-primary);
}
.form-group input[type="number"],
.form-group select {
width: 100%;
padding: 12px;
background: var(--dark-bg);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-primary);
font-size: 1em;
}
.form-group input[type="number"]:focus,
.form-group select:focus {
outline: none;
border-color: var(--primary-color);
}
.form-group small {
display: block;
margin-top: 5px;
color: var(--text-secondary);
font-size: 0.9em;
}
.form-group input[type="file"] {
width: 100%;
padding: 10px;
background: var(--dark-bg);
border: 2px dashed var(--border-color);
border-radius: 6px;
color: var(--text-primary);
cursor: pointer;
}
/* Buttons */
.btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
width: 100%;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
.btn:active {
transform: translateY(0);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-primary {
background: var(--primary-color);
color: white;
}
.btn-success {
background: var(--success-color);
color: white;
}
.btn-warning {
background: var(--warning-color);
color: white;
}
.btn-secondary {
background: var(--border-color);
color: white;
}
.btn-mode {
background: var(--hover-bg);
color: var(--text-primary);
border: 1px solid var(--border-color);
}
.btn-mode:hover {
background: var(--primary-color);
}
/* Button Grid */
.button-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 10px;
margin-bottom: 20px;
}
/* Current Mode Display */
.current-mode {
background: var(--dark-bg);
padding: 15px;
border-radius: 6px;
margin-bottom: 20px;
text-align: center;
font-size: 1.1em;
border: 1px solid var(--border-color);
}
.current-mode span {
color: var(--primary-color);
font-weight: bold;
}
/* Progress Bar */
.progress-container {
width: 100%;
height: 40px;
background: var(--dark-bg);
border-radius: 20px;
overflow: hidden;
margin: 20px 0;
position: relative;
border: 1px solid var(--border-color);
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, var(--primary-color), var(--success-color));
transition: width 0.3s ease;
width: 0%;
}
.progress-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-weight: bold;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
}
/* Warning Box */
.warning {
background: rgba(255, 152, 0, 0.1);
border: 1px solid var(--warning-color);
border-radius: 6px;
padding: 15px;
margin-bottom: 20px;
color: var(--warning-color);
font-weight: 500;
}
/* Divider */
.divider {
height: 1px;
background: var(--border-color);
margin: 20px 0;
}
/* Footer */
footer {
text-align: center;
margin-top: 40px;
padding: 20px;
color: var(--text-secondary);
}
footer small {
font-size: 0.9em;
}
/* Responsive Design */
@media (max-width: 600px) {
header h1 {
font-size: 2em;
}
.button-grid {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
}
.btn {
font-size: 0.9em;
padding: 10px 16px;
}
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
animation: fadeIn 0.5s ease;
}

View File

@ -1,115 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LED Controller Config</title>
<link rel="stylesheet" href="css/style.css">
<link rel="icon" href="data/favicon.ico">
</head>
<body>
<div class="container">
<header>
<h1>✈️ LED Controller</h1>
<p class="subtitle">Model Aircraft LED Configuration via Web-BLE</p>
</header>
<div class="card">
<h2>🔗 Connection</h2>
<div class="connection-status">
<span id="status-indicator" class="status-indicator disconnected"></span>
<span id="connection-status">Disconnected</span>
</div>
<button id="connect-btn" class="btn btn-primary">Connect via BLE</button>
</div>
<div id="config-section" class="card hidden">
<h2>⚙️ Configuration</h2>
<div class="form-group">
<label for="led-pin-a">LED Strip A GPIO Pin</label>
<input type="number" id="led-pin-a" min="-1" max="48" value="-1">
<small>-1 = Disabled</small>
</div>
<div class="form-group">
<label for="led-pin-b">LED Strip B GPIO Pin</label>
<input type="number" id="led-pin-b" min="-1" max="48" value="-1">
<small>-1 = Disabled</small>
</div>
<div class="form-group">
<label for="pwm-pin">PWM Input GPIO Pin</label>
<input type="number" id="pwm-pin" min="-1" max="48" value="-1">
<small>-1 = Disabled</small>
</div>
<div class="form-group">
<label for="ble-timeout">BLE Auto-Off Timeout</label>
<select id="ble-timeout">
<option value="0">Never</option>
<option value="60">1 Minute</option>
<option value="300">5 Minutes</option>
</select>
</div>
<button id="save-config-btn" class="btn btn-success">💾 Save Configuration</button>
</div>
<div id="control-section" class="card hidden">
<h2>🎮 Control</h2>
<div class="current-mode">
<strong>Current Mode:</strong> <span id="current-mode">Black</span>
</div>
<div class="button-grid">
<button class="btn btn-mode" data-mode="0">⚫ Black</button>
<button class="btn btn-mode" data-mode="1">🔴 Red</button>
<button class="btn btn-mode" data-mode="2">🔵 Blue</button>
<button class="btn btn-mode" data-mode="3">🟢 Green</button>
<button class="btn btn-mode" data-mode="4">⚪ White</button>
<button class="btn btn-mode" data-mode="5">🌈 Rainbow</button>
<button class="btn btn-mode" data-mode="6">✨ Rainbow Glitter</button>
<button class="btn btn-mode" data-mode="7">🎊 Confetti</button>
<button class="btn btn-mode" data-mode="8">🌀 Sinelon</button>
<button class="btn btn-mode" data-mode="9">💓 BPM Pulse</button>
<button class="btn btn-mode" data-mode="10">🛩️ Navigation</button>
<button class="btn btn-mode" data-mode="11">🏃 Chase (Red)</button>
<button class="btn btn-mode" data-mode="12">🏃 Chase (RGB)</button>
<button class="btn btn-mode" data-mode="13">🎲 Random</button>
</div>
<div class="divider"></div>
<button id="pwm-emulate-btn" class="btn btn-secondary">⏭️ Next Mode (PWM Emulation)</button>
</div>
<div id="ota-section" class="card hidden">
<h2>🔄 Firmware Update</h2>
<div class="warning">
⚠️ Warning: Firmware update will reset all settings!
</div>
<div class="form-group">
<label for="firmware-file">Select Firmware File (.bin)</label>
<input type="file" id="firmware-file" accept=".bin">
</div>
<div id="ota-progress" class="progress-container hidden">
<div class="progress-bar" id="ota-progress-bar"></div>
<div class="progress-text" id="ota-progress-text">0%</div>
</div>
<button id="ota-upload-btn" class="btn btn-warning" disabled>📤 Upload Firmware</button>
</div>
<footer>
<p>ESP32 LED Controller © 2026</p>
<p><small>Supports ESP32 DevKitC & ESP32-C3 MINI</small></p>
</footer>
</div>
<script src="app/app.js"></script>
</body>
</html>