Compare commits
21 Commits
feature/es
...
feature/es
| Author | SHA1 | Date | |
|---|---|---|---|
| e25971af89 | |||
| 5796b28e1a | |||
| 12a8710a2f | |||
| 468d2cba74 | |||
| d2d5d7dc4b | |||
| f1aac6611d | |||
| 733b05eaeb | |||
| 715d50c255 | |||
| 883fff95dd | |||
| 0f62418d93 | |||
| 12b8acf81c | |||
| a08dba780a | |||
| d576b4d42d | |||
| 9ef50436a4 | |||
| b1b179b5ff | |||
| 3ada494d15 | |||
| d33bda52d0 | |||
| 0dd26fdcde | |||
| 1012d3bb2f | |||
| 25b2c028b6 | |||
| fa6a19d1aa |
3
.gitignore
vendored
3
.gitignore
vendored
@ -3,7 +3,6 @@
|
|||||||
# https://github.com/espressif/esp-idf
|
# https://github.com/espressif/esp-idf
|
||||||
|
|
||||||
build/
|
build/
|
||||||
sdkconfig
|
|
||||||
sdkconfig.old
|
sdkconfig.old
|
||||||
|
|
||||||
# ---> VisualStudioCode
|
# ---> VisualStudioCode
|
||||||
@ -187,7 +186,6 @@ cython_debug/
|
|||||||
# https://github.com/espressif/esp-idf
|
# https://github.com/espressif/esp-idf
|
||||||
|
|
||||||
build/
|
build/
|
||||||
sdkconfig
|
|
||||||
sdkconfig.old
|
sdkconfig.old
|
||||||
|
|
||||||
# ---> CMake
|
# ---> CMake
|
||||||
@ -293,3 +291,4 @@ dkms.conf
|
|||||||
|
|
||||||
.vscode/settings.json
|
.vscode/settings.json
|
||||||
sdkconfig.defaults
|
sdkconfig.defaults
|
||||||
|
.clangd
|
||||||
|
|||||||
390
ARCHITECTURE.md
390
ARCHITECTURE.md
@ -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
|
|
||||||
|
|
||||||
62
README.md
62
README.md
@ -1,6 +1,6 @@
|
|||||||
# ESP32 LED Controller for Model Aircraft
|
# 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
|
## Features
|
||||||
|
|
||||||
@ -26,28 +26,18 @@ Professional LED controller firmware for ESP32 with Web-BLE configuration interf
|
|||||||
13. **Chase (RGB)** - RGB cycling chase effect
|
13. **Chase (RGB)** - RGB cycling chase effect
|
||||||
14. **Random** - Random LED colors
|
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
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
led-controller-firmware/
|
led-controller-firmware/
|
||||||
├── main/
|
├── main/
|
||||||
│ ├── main.c # Application entry point
|
│ ├── main.c # Application entry point
|
||||||
│ ├── control.c/h # BLE, NVS, initialization
|
│ ├── control.c/h # initialization
|
||||||
|
│ ├── config.c/h # NVS
|
||||||
│ ├── led.c/h # WS2812B control (RMT driver)
|
│ ├── led.c/h # WS2812B control (RMT driver)
|
||||||
│ ├── rcsignal.c/h # PWM signal reading
|
│ ├── rcsignal.c/h # PWM signal reading
|
||||||
|
│ ├── localbtn.c/h # Local btn reading
|
||||||
│ └── animation.c/h # LED animation patterns
|
│ └── 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
|
├── CMakeLists.txt
|
||||||
├── sdkconfig.defaults
|
├── sdkconfig.defaults
|
||||||
└── partitions.csv # OTA-enabled partition table
|
└── partitions.csv # OTA-enabled partition table
|
||||||
@ -80,7 +70,7 @@ idf.py build
|
|||||||
idf.py -p /dev/ttyUSB0 flash monitor
|
idf.py -p /dev/ttyUSB0 flash monitor
|
||||||
```
|
```
|
||||||
|
|
||||||
Replace `/dev/ttyUSB0` with your serial port (COM3 on Windows).
|
Replace `/dev/ttyUSB0` with your serial port.
|
||||||
|
|
||||||
## Hardware Setup
|
## Hardware Setup
|
||||||
|
|
||||||
@ -91,6 +81,7 @@ ESP32 Pin -> Component
|
|||||||
GPIO XX -> WS2812B Strip A Data
|
GPIO XX -> WS2812B Strip A Data
|
||||||
GPIO XX -> WS2812B Strip B Data
|
GPIO XX -> WS2812B Strip B Data
|
||||||
GPIO XX -> RC PWM Signal
|
GPIO XX -> RC PWM Signal
|
||||||
|
GPIO XX -> Local button Signal
|
||||||
GND -> Common Ground
|
GND -> Common Ground
|
||||||
5V -> LED Strip Power (if current < 500mA)
|
5V -> LED Strip Power (if current < 500mA)
|
||||||
```
|
```
|
||||||
@ -107,42 +98,6 @@ GND -> Common Ground
|
|||||||
- 1500µs threshold for mode switching
|
- 1500µs threshold for mode switching
|
||||||
- Rising edge >1500µs after <1500µs triggers next mode
|
- 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
|
## Development
|
||||||
|
|
||||||
### Adding New Animations
|
### Adding New Animations
|
||||||
@ -150,11 +105,6 @@ Configure via Web-BLE before use.
|
|||||||
1. Add mode to `animation_mode_t` enum in `animation.h`
|
1. Add mode to `animation_mode_t` enum in `animation.h`
|
||||||
2. Implement animation function in `animation.c`
|
2. Implement animation function in `animation.c`
|
||||||
3. Add case to `animation_update()` switch statement
|
3. Add case to `animation_update()` switch statement
|
||||||
4. Update `MODE_NAMES` array in `webapp/app/app.js`
|
|
||||||
|
|
||||||
### Modifying LED Count
|
|
||||||
|
|
||||||
Edit `DEFAULT_NUM_LEDS_A` and `DEFAULT_NUM_LEDS_B` in `control.c`. TODO
|
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
|
|||||||
212
functions.ino
212
functions.ino
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -5,10 +5,7 @@ idf_component_register(
|
|||||||
"led.c"
|
"led.c"
|
||||||
"rcsignal.c"
|
"rcsignal.c"
|
||||||
"animation.c"
|
"animation.c"
|
||||||
|
"localbtn.c"
|
||||||
|
"config.c"
|
||||||
INCLUDE_DIRS "."
|
INCLUDE_DIRS "."
|
||||||
EMBED_FILES
|
|
||||||
"../webapp/index.html"
|
|
||||||
"../webapp/app/app.js"
|
|
||||||
"../webapp/css/style.css"
|
|
||||||
"../webapp/data/favicon.ico"
|
|
||||||
)
|
)
|
||||||
|
|||||||
277
main/animation.c
277
main/animation.c
@ -5,44 +5,43 @@
|
|||||||
|
|
||||||
#include "animation.h"
|
#include "animation.h"
|
||||||
#include "led.h"
|
#include "led.h"
|
||||||
|
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_timer.h"
|
#include "esp_timer.h"
|
||||||
#include "esp_random.h"
|
#include "esp_random.h"
|
||||||
|
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
static const char *TAG = "ANIMATION";
|
static const char *TAG = "ANIMATION";
|
||||||
|
|
||||||
#define FRAMES_PER_SECOND 60
|
#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 animation_mode_t current_mode = ANIM_BLACK;
|
||||||
static uint8_t global_hue = 0;
|
static uint8_t global_hue = 0;
|
||||||
static uint32_t frame_counter = 0;
|
static uint32_t frame_counter = 0;
|
||||||
|
|
||||||
// Beat calculation helper (similar to FastLED beatsin16)
|
// Beat calculation helper
|
||||||
static int16_t beatsin16(uint8_t bpm, int16_t min_val, int16_t max_val)
|
static int16_t beatsin16(uint8_t bpm, int16_t min_val, int16_t max_val)
|
||||||
{
|
{
|
||||||
uint32_t ms = esp_timer_get_time() / 1000;
|
// Use uint64_t to prevent overflow
|
||||||
uint32_t beat = (ms * bpm * 256) / 60000;
|
uint64_t us = esp_timer_get_time(); // Microseconds
|
||||||
uint8_t beat8 = (beat >> 8) & 0xFF;
|
|
||||||
|
|
||||||
// Sin approximation
|
// Calculate beat phase (0-65535 repeating at BPM rate)
|
||||||
float angle = (beat8 / 255.0f) * 2.0f * M_PI;
|
// beats_per_minute → beats_per_microsecond = bpm / 60,000,000
|
||||||
|
uint64_t beat_phase = (us * (uint64_t)bpm * 65536ULL) / 60000000ULL;
|
||||||
|
uint16_t beat16 = (uint16_t)(beat_phase & 0xFFFF);
|
||||||
|
|
||||||
|
// Convert to angle (0 to 2π)
|
||||||
|
float angle = (beat16 / 65535.0f) * 2.0f * M_PI;
|
||||||
float sin_val = sinf(angle);
|
float sin_val = sinf(angle);
|
||||||
|
|
||||||
|
// Map sin (-1 to +1) to output range (min_val to max_val)
|
||||||
int16_t range = max_val - min_val;
|
int16_t range = max_val - min_val;
|
||||||
int16_t result = min_val + (int16_t)((sin_val + 1.0f) * range / 2.0f);
|
int16_t result = min_val + (int16_t)((sin_val + 1.0f) * range / 2.0f);
|
||||||
|
|
||||||
return result;
|
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
|
// Random helper
|
||||||
static uint8_t random8(void)
|
static uint8_t random8(void)
|
||||||
{
|
{
|
||||||
@ -94,20 +93,24 @@ static void anim_white(void)
|
|||||||
|
|
||||||
static void anim_rainbow(void)
|
static void anim_rainbow(void)
|
||||||
{
|
{
|
||||||
// FastLED's built-in rainbow generator
|
// Rainbow generator
|
||||||
uint16_t num_leds_a = led_get_num_leds_a();
|
uint16_t num_leds_a = led_get_num_leds_a();
|
||||||
uint16_t num_leds_b = led_get_num_leds_b();
|
uint16_t num_leds_b = led_get_num_leds_b();
|
||||||
|
uint16_t num_leds = num_leds_a + num_leds_b;
|
||||||
|
|
||||||
for (uint16_t i = 0; i < num_leds_a; i++)
|
for (uint16_t i = 0; i < num_leds; i++)
|
||||||
{
|
{
|
||||||
hsv_t hsv = {(uint8_t)(global_hue + (i * 7)), 255, 255};
|
hsv_t hsv = {(uint8_t)(global_hue + (i * 7)), 255, 255};
|
||||||
led_set_pixel_a(i, led_hsv_to_rgb(hsv));
|
rgb_t color = led_hsv_to_rgb(hsv);
|
||||||
|
|
||||||
|
if (i < num_leds_a)
|
||||||
|
{
|
||||||
|
led_set_pixel_a(num_leds_a - i - 1, color);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
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 - num_leds_a, color);
|
||||||
led_set_pixel_b(i, led_hsv_to_rgb(hsv));
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,7 +136,7 @@ static void add_glitter(uint8_t chance_of_glitter)
|
|||||||
static void anim_rainbow_glitter(void)
|
static void anim_rainbow_glitter(void)
|
||||||
{
|
{
|
||||||
anim_rainbow();
|
anim_rainbow();
|
||||||
add_glitter(80);
|
add_glitter(255);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void anim_confetti(void)
|
static void anim_confetti(void)
|
||||||
@ -144,16 +147,16 @@ static void anim_confetti(void)
|
|||||||
uint16_t num_leds = led_get_num_leds_a() + led_get_num_leds_b();
|
uint16_t num_leds = led_get_num_leds_a() + led_get_num_leds_b();
|
||||||
uint16_t pos = random16(num_leds);
|
uint16_t pos = random16(num_leds);
|
||||||
|
|
||||||
hsv_t hsv = {(uint8_t)(global_hue + random8()), 200, 255};
|
hsv_t hsv = {(uint8_t)(global_hue + random8()), 255, 255};
|
||||||
rgb_t color = led_hsv_to_rgb(hsv);
|
rgb_t color = led_hsv_to_rgb(hsv);
|
||||||
|
|
||||||
if (pos < led_get_num_leds_a())
|
if (pos < led_get_num_leds_a())
|
||||||
{
|
{
|
||||||
led_add_pixel_a(pos, color);
|
led_set_pixel_a(led_get_num_leds_a() - pos - 1, color);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
led_add_pixel_b(pos - led_get_num_leds_a(), color);
|
led_set_pixel_b(pos - led_get_num_leds_a(), color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,14 +166,14 @@ static void anim_sinelon(void)
|
|||||||
led_fade_to_black(20);
|
led_fade_to_black(20);
|
||||||
|
|
||||||
uint16_t num_leds = led_get_num_leds_a() + led_get_num_leds_b();
|
uint16_t num_leds = led_get_num_leds_a() + led_get_num_leds_b();
|
||||||
int16_t pos = beatsin16(13, 0, num_leds - 1);
|
int16_t pos = beatsin16(13, 0, num_leds);
|
||||||
|
|
||||||
hsv_t hsv = {global_hue, 255, 192};
|
hsv_t hsv = {global_hue, 255, 192};
|
||||||
rgb_t color = led_hsv_to_rgb(hsv);
|
rgb_t color = led_hsv_to_rgb(hsv);
|
||||||
|
|
||||||
if (pos < led_get_num_leds_a())
|
if (pos < led_get_num_leds_a())
|
||||||
{
|
{
|
||||||
led_add_pixel_a(pos, color);
|
led_add_pixel_a(led_get_num_leds_a() - pos - 1, color);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -178,42 +181,14 @@ static void anim_sinelon(void)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
static void anim_navigation(void)
|
||||||
{
|
{
|
||||||
// Navigation lights: left red, right green, with blinking white
|
// Aviation navigation lights with strobe overlay:
|
||||||
static uint8_t blink_state = 0;
|
// - Red: Port (left) wingtip - steady
|
||||||
|
// - Green: Starboard (right) wingtip - steady
|
||||||
|
// - White strobe: Overlays outer nav lights with bright flashes
|
||||||
|
|
||||||
|
static uint8_t strobe_counter = 0;
|
||||||
led_clear_all();
|
led_clear_all();
|
||||||
|
|
||||||
uint16_t num_leds_a = led_get_num_leds_a();
|
uint16_t num_leds_a = led_get_num_leds_a();
|
||||||
@ -223,49 +198,32 @@ static void anim_navigation(void)
|
|||||||
rgb_t green = {0, 255, 0};
|
rgb_t green = {0, 255, 0};
|
||||||
rgb_t white = {255, 255, 255};
|
rgb_t white = {255, 255, 255};
|
||||||
|
|
||||||
// Left side red (first 3 LEDs of strip A)
|
// Anti-collision strobe pattern: Double flash at ~1 Hz
|
||||||
|
// Flash duration: 3 frames (~50ms) for high-intensity effect
|
||||||
|
bool first_flash = (strobe_counter < 3);
|
||||||
|
bool second_flash = (strobe_counter >= 7 && strobe_counter < 10);
|
||||||
|
bool strobe_active = (first_flash || second_flash);
|
||||||
|
|
||||||
|
// Port (left) - Red navigation light OR white strobe (outer 3 LEDs of strip A)
|
||||||
if (num_leds_a >= 3)
|
if (num_leds_a >= 3)
|
||||||
{
|
{
|
||||||
led_set_pixel_a(0, red);
|
rgb_t color_a = strobe_active ? white : red;
|
||||||
led_set_pixel_a(1, red);
|
led_set_pixel_a(num_leds_a - 1, color_a);
|
||||||
led_set_pixel_a(2, red);
|
led_set_pixel_a(num_leds_a - 2, red);
|
||||||
|
led_set_pixel_a(num_leds_a - 3, red);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Right side green (last 3 LEDs)
|
// Starboard (right) - Green navigation light OR white strobe (outer 3 LEDs of strip B)
|
||||||
if (num_leds_b >= 3)
|
if (num_leds_b >= 3)
|
||||||
{
|
{
|
||||||
led_set_pixel_b(num_leds_b - 1, green);
|
rgb_t color_b = strobe_active ? white : green;
|
||||||
|
led_set_pixel_b(num_leds_b - 1, color_b);
|
||||||
led_set_pixel_b(num_leds_b - 2, green);
|
led_set_pixel_b(num_leds_b - 2, green);
|
||||||
led_set_pixel_b(num_leds_b - 3, 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)
|
// Strobe cycle: 90 frames = 1.5 second at 60 FPS
|
||||||
if (blink_state < FRAMES_PER_SECOND / 2)
|
strobe_counter = (strobe_counter + 1) % 90;
|
||||||
{
|
|
||||||
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)
|
static void anim_chase(void)
|
||||||
@ -273,25 +231,45 @@ static void anim_chase(void)
|
|||||||
// Red dot sweeping with trailing dots
|
// Red dot sweeping with trailing dots
|
||||||
led_clear_all();
|
led_clear_all();
|
||||||
|
|
||||||
uint16_t num_leds = led_get_num_leds_a() + led_get_num_leds_b();
|
uint16_t num_leds_a = led_get_num_leds_a();
|
||||||
int16_t pos = beatsin16(40, 0, num_leds - 1);
|
uint16_t num_leds_b = led_get_num_leds_b();
|
||||||
|
uint16_t total_leds = num_leds_a + num_leds_b;
|
||||||
|
|
||||||
|
// Get oscillating position across both strips
|
||||||
|
int16_t center_pos = beatsin16(40, 0, total_leds - 1);
|
||||||
|
|
||||||
rgb_t red = {255, 0, 0};
|
rgb_t red = {255, 0, 0};
|
||||||
|
|
||||||
// Set main dot and trailing dots
|
// Draw center dot with dimmed trailing dots (3 dots total: center ±1)
|
||||||
for (int offset = -2; offset <= 2; offset++)
|
for (int8_t offset = -1; offset <= 1; offset++)
|
||||||
{
|
{
|
||||||
int16_t led_pos = pos + offset;
|
int16_t led_pos = center_pos + offset;
|
||||||
if (led_pos >= 0 && led_pos < num_leds)
|
|
||||||
|
// Skip if position is out of bounds
|
||||||
|
if (led_pos < 0 || led_pos >= total_leds)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Calculate brightness based on distance from center
|
||||||
|
uint8_t brightness = (offset == 0) ? 255 : 32; // Center: full, trailing: 12%
|
||||||
|
|
||||||
|
// Create dimmed color
|
||||||
|
rgb_t dimmed_red = {
|
||||||
|
(red.r * brightness) / 255,
|
||||||
|
(red.g * brightness) / 255,
|
||||||
|
(red.b * brightness) / 255};
|
||||||
|
|
||||||
|
// Map virtual position to physical LED
|
||||||
|
if (led_pos < num_leds_a)
|
||||||
{
|
{
|
||||||
if (led_pos < led_get_num_leds_a())
|
// Strip A (mirrored: position 0 maps to last LED)
|
||||||
{
|
uint16_t strip_a_index = num_leds_a - led_pos - 1;
|
||||||
led_set_pixel_a(led_pos, red);
|
led_set_pixel_a(strip_a_index, dimmed_red);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
led_set_pixel_b(led_pos - led_get_num_leds_a(), red);
|
// Strip B (direct mapping)
|
||||||
}
|
uint16_t strip_b_index = led_pos - num_leds_a;
|
||||||
|
led_set_pixel_b(strip_b_index, dimmed_red);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -301,26 +279,46 @@ static void anim_chase_rgb(void)
|
|||||||
// RGB cycling dot sweeping with trailing dots
|
// RGB cycling dot sweeping with trailing dots
|
||||||
led_clear_all();
|
led_clear_all();
|
||||||
|
|
||||||
uint16_t num_leds = led_get_num_leds_a() + led_get_num_leds_b();
|
uint16_t num_leds_a = led_get_num_leds_a();
|
||||||
int16_t pos = beatsin16(40, 0, num_leds - 1);
|
uint16_t num_leds_b = led_get_num_leds_b();
|
||||||
|
uint16_t total_leds = num_leds_a + num_leds_b;
|
||||||
|
|
||||||
|
// Get oscillating position across both strips
|
||||||
|
int16_t center_pos = beatsin16(40, 0, total_leds - 1);
|
||||||
|
|
||||||
hsv_t hsv = {global_hue, 255, 192};
|
hsv_t hsv = {global_hue, 255, 192};
|
||||||
rgb_t color = led_hsv_to_rgb(hsv);
|
rgb_t color = led_hsv_to_rgb(hsv);
|
||||||
|
|
||||||
// Set main dot and trailing dots
|
// Draw center dot with dimmed trailing dots (3 dots total: center ±1)
|
||||||
for (int offset = -2; offset <= 2; offset++)
|
for (int8_t offset = -1; offset <= 1; offset++)
|
||||||
{
|
{
|
||||||
int16_t led_pos = pos + offset;
|
int16_t led_pos = center_pos + offset;
|
||||||
if (led_pos >= 0 && led_pos < num_leds)
|
|
||||||
|
// Skip if position is out of bounds
|
||||||
|
if (led_pos < 0 || led_pos >= total_leds)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Calculate brightness based on distance from center
|
||||||
|
uint8_t brightness = (offset == 0) ? 255 : 32; // Center: full, trailing: 12%
|
||||||
|
|
||||||
|
// Create dimmed color
|
||||||
|
rgb_t dimmed_color = {
|
||||||
|
(color.r * brightness) / 255,
|
||||||
|
(color.g * brightness) / 255,
|
||||||
|
(color.b * brightness) / 255};
|
||||||
|
|
||||||
|
// Map virtual position to physical LED
|
||||||
|
if (led_pos < num_leds_a)
|
||||||
{
|
{
|
||||||
if (led_pos < led_get_num_leds_a())
|
// Strip A (mirrored: position 0 maps to last LED)
|
||||||
{
|
uint16_t strip_a_index = num_leds_a - led_pos - 1;
|
||||||
led_add_pixel_a(led_pos, color);
|
led_set_pixel_a(strip_a_index, dimmed_color);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
led_add_pixel_b(led_pos - led_get_num_leds_a(), color);
|
// Strip B (direct mapping)
|
||||||
}
|
uint16_t strip_b_index = led_pos - num_leds_a;
|
||||||
|
led_set_pixel_b(strip_b_index, dimmed_color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -331,18 +329,26 @@ static void anim_random(void)
|
|||||||
uint16_t num_leds = led_get_num_leds_a() + led_get_num_leds_b();
|
uint16_t num_leds = led_get_num_leds_a() + led_get_num_leds_b();
|
||||||
uint16_t random_pos = random16(num_leds);
|
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 = {
|
rgb_t random_color = {
|
||||||
random8(),
|
0,
|
||||||
random8(),
|
0,
|
||||||
random8()};
|
0};
|
||||||
|
|
||||||
|
// Set random LED to random basis color
|
||||||
|
switch (random16(3))
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
random_color.r = 255;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
random_color.g = 255;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
random_color.b = 255;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (random_pos < led_get_num_leds_a())
|
if (random_pos < led_get_num_leds_a())
|
||||||
{
|
{
|
||||||
@ -357,31 +363,26 @@ static void anim_random(void)
|
|||||||
esp_err_t animation_init(void)
|
esp_err_t animation_init(void)
|
||||||
{
|
{
|
||||||
current_mode = ANIM_BLACK;
|
current_mode = ANIM_BLACK;
|
||||||
global_hue = 0;
|
global_hue = 0U;
|
||||||
frame_counter = 0;
|
frame_counter = 0U;
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Animation system initialized");
|
ESP_LOGI(TAG, "Animation initialized");
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
void animation_set_mode(animation_mode_t mode)
|
void animation_set_mode(animation_mode_t mode)
|
||||||
{
|
{
|
||||||
if (mode >= ANIM_MODE_COUNT)
|
if ((mode >= ANIM_MODE_COUNT) || (mode < 0U))
|
||||||
{
|
{
|
||||||
mode = ANIM_BLACK;
|
mode = ANIM_BLACK;
|
||||||
}
|
}
|
||||||
|
|
||||||
current_mode = mode;
|
current_mode = mode;
|
||||||
frame_counter = 0;
|
frame_counter = 0U;
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Animation mode set to: %s", animation_get_mode_name(mode));
|
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)
|
void animation_update(void)
|
||||||
{
|
{
|
||||||
// Update global hue every frame (slowly cycles colors)
|
// Update global hue every frame (slowly cycles colors)
|
||||||
@ -421,9 +422,6 @@ void animation_update(void)
|
|||||||
case ANIM_SINELON:
|
case ANIM_SINELON:
|
||||||
anim_sinelon();
|
anim_sinelon();
|
||||||
break;
|
break;
|
||||||
case ANIM_BPM:
|
|
||||||
anim_bpm();
|
|
||||||
break;
|
|
||||||
case ANIM_NAVIGATION:
|
case ANIM_NAVIGATION:
|
||||||
anim_navigation();
|
anim_navigation();
|
||||||
break;
|
break;
|
||||||
@ -456,7 +454,6 @@ const char *animation_get_mode_name(animation_mode_t mode)
|
|||||||
"Rainbow with Glitter",
|
"Rainbow with Glitter",
|
||||||
"Confetti",
|
"Confetti",
|
||||||
"Sinelon",
|
"Sinelon",
|
||||||
"BPM",
|
|
||||||
"Navigation",
|
"Navigation",
|
||||||
"Chase",
|
"Chase",
|
||||||
"Chase RGB",
|
"Chase RGB",
|
||||||
|
|||||||
@ -6,9 +6,10 @@
|
|||||||
#ifndef ANIMATION_H
|
#ifndef ANIMATION_H
|
||||||
#define ANIMATION_H
|
#define ANIMATION_H
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Animation modes
|
* @brief Animation modes
|
||||||
*/
|
*/
|
||||||
@ -22,11 +23,10 @@ typedef enum {
|
|||||||
ANIM_RAINBOW_GLITTER = 6, // Rainbow with glitter
|
ANIM_RAINBOW_GLITTER = 6, // Rainbow with glitter
|
||||||
ANIM_CONFETTI = 7, // Random colored speckles
|
ANIM_CONFETTI = 7, // Random colored speckles
|
||||||
ANIM_SINELON = 8, // Colored dot sweeping (RGB cycling)
|
ANIM_SINELON = 8, // Colored dot sweeping (RGB cycling)
|
||||||
ANIM_BPM = 9, // Colored stripes @ 33 BPM
|
ANIM_NAVIGATION = 9, // Navigation lights (red left, green right)
|
||||||
ANIM_NAVIGATION = 10, // Navigation lights (red left, green right)
|
ANIM_CHASE = 10, // Red dot sweeping
|
||||||
ANIM_CHASE = 11, // Red dot sweeping
|
ANIM_CHASE_RGB = 11, // RGB cycling dot sweeping
|
||||||
ANIM_CHASE_RGB = 12, // RGB cycling dot sweeping
|
ANIM_RANDOM = 12, // Random mode
|
||||||
ANIM_RANDOM = 13, // Random mode
|
|
||||||
ANIM_MODE_COUNT
|
ANIM_MODE_COUNT
|
||||||
} animation_mode_t;
|
} animation_mode_t;
|
||||||
|
|
||||||
@ -42,12 +42,6 @@ esp_err_t animation_init(void);
|
|||||||
*/
|
*/
|
||||||
void animation_set_mode(animation_mode_t 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)
|
* @brief Update animation (call periodically, e.g., 30-60 FPS)
|
||||||
*/
|
*/
|
||||||
@ -58,6 +52,6 @@ void animation_update(void);
|
|||||||
* @param mode Animation mode
|
* @param mode Animation mode
|
||||||
* @return Mode name string
|
* @return Mode name string
|
||||||
*/
|
*/
|
||||||
const char* animation_get_mode_name(animation_mode_t mode);
|
const char *animation_get_mode_name(animation_mode_t mode);
|
||||||
|
|
||||||
#endif // ANIMATION_H
|
#endif // ANIMATION_H
|
||||||
|
|||||||
199
main/config.c
Normal file
199
main/config.c
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
/**
|
||||||
|
* @file config.c
|
||||||
|
* @brief Config module implementation
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "config.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 "soc/gpio_num.h"
|
||||||
|
#include "mbedtls/sha256.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static const char *TAG = "CONFIG";
|
||||||
|
|
||||||
|
#define NVS_NAMESPACE "led_ctrl"
|
||||||
|
|
||||||
|
#define HARDCODED_CONFIG
|
||||||
|
#ifdef HARDCODED_CONFIG
|
||||||
|
#define HARDCODED_CONFIG_LED_STRIP_A_PIN 3U
|
||||||
|
#define HARDCODED_CONFIG_LED_STRIP_B_PIN 2U
|
||||||
|
#define HARDCODED_CONFIG_LED_STRIP_A_COUNT 10U
|
||||||
|
#define HARDCODED_CONFIG_LED_STRIP_B_COUNT 10U
|
||||||
|
#define HARDCODED_CONFIG_PWM_PIN 1U
|
||||||
|
|
||||||
|
#if defined(CONFIG_IDF_TARGET_ESP32C3)
|
||||||
|
#define HARDCODED_CONFIG_LOCALBTN_PIN 9
|
||||||
|
#elif defined(CONFIG_IDF_TARGET_ESP32)
|
||||||
|
#define HARDCODED_CONFIG_LOCALBTN_PIN 0
|
||||||
|
#else
|
||||||
|
#error "Unsupported target: BOOT button GPIO not defined"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Global state
|
||||||
|
static config_t current_config = {
|
||||||
|
.led_pin_strip_a = -1,
|
||||||
|
.led_pin_strip_b = -1,
|
||||||
|
.led_count_strip_a = -1,
|
||||||
|
.led_count_strip_b = -1,
|
||||||
|
.pwm_pin = -1,
|
||||||
|
.localBtn_pin = -1};
|
||||||
|
|
||||||
|
static void calculate_config_hash(const config_t *cfg, uint8_t *out_hash);
|
||||||
|
|
||||||
|
// NVS Functions
|
||||||
|
static esp_err_t load_config_from_nvs(void)
|
||||||
|
{
|
||||||
|
nvs_handle_t nvs_handle;
|
||||||
|
size_t size = sizeof(config_t);
|
||||||
|
config_t tmp;
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < 2U; i++)
|
||||||
|
{
|
||||||
|
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &nvs_handle);
|
||||||
|
if (err != ESP_OK)
|
||||||
|
{
|
||||||
|
ESP_LOGW(TAG, "NVS not found, using defaults");
|
||||||
|
config_reset_config();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = nvs_get_blob(nvs_handle, "config", &tmp, &size);
|
||||||
|
nvs_close(nvs_handle);
|
||||||
|
|
||||||
|
uint8_t calc_hash[CONFIG_HASH_LEN];
|
||||||
|
calculate_config_hash(&tmp, calc_hash);
|
||||||
|
|
||||||
|
if (memcmp(calc_hash, tmp.hash, CONFIG_HASH_LEN) != 0)
|
||||||
|
{
|
||||||
|
ESP_LOGW(TAG, "Invalid config in NVS, using defaults");
|
||||||
|
config_reset_config();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We found a valid config
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
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, " Strip A LED count: %d", current_config.led_count_strip_a);
|
||||||
|
ESP_LOGI(TAG, " Strip B LED count: %d", current_config.led_count_strip_b);
|
||||||
|
ESP_LOGI(TAG, " PWM Pin: GPIO%d", current_config.pwm_pin);
|
||||||
|
ESP_LOGI(TAG, " Local btn Pin: GPIO%d", current_config.localBtn_pin);
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t save_config_to_nvs(void)
|
||||||
|
{
|
||||||
|
calculate_config_hash(¤t_config, current_config.hash);
|
||||||
|
|
||||||
|
nvs_handle_t nvs_handle;
|
||||||
|
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs_handle);
|
||||||
|
if (err != ESP_OK)
|
||||||
|
{
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = nvs_set_blob(nvs_handle, "config", ¤t_config, sizeof(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 config_reset_config(void)
|
||||||
|
{
|
||||||
|
current_config.led_pin_strip_a = -1;
|
||||||
|
current_config.led_pin_strip_b = -1;
|
||||||
|
current_config.led_count_strip_a = -1;
|
||||||
|
current_config.led_count_strip_b = -1;
|
||||||
|
current_config.pwm_pin = -1;
|
||||||
|
current_config.localBtn_pin = -1;
|
||||||
|
|
||||||
|
return save_config_to_nvs();
|
||||||
|
}
|
||||||
|
|
||||||
|
void config_get_config(config_t *const cnf)
|
||||||
|
{
|
||||||
|
cnf->led_pin_strip_a = current_config.led_pin_strip_a;
|
||||||
|
cnf->led_pin_strip_b = current_config.led_pin_strip_b;
|
||||||
|
cnf->led_count_strip_a = current_config.led_count_strip_a;
|
||||||
|
cnf->led_count_strip_b = current_config.led_count_strip_b;
|
||||||
|
cnf->pwm_pin = current_config.pwm_pin;
|
||||||
|
cnf->localBtn_pin = current_config.localBtn_pin;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t config_init(void)
|
||||||
|
{
|
||||||
|
esp_err_t ret;
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Initializing Config...");
|
||||||
|
|
||||||
|
// 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(config_reset_config());
|
||||||
|
}
|
||||||
|
ESP_ERROR_CHECK(ret);
|
||||||
|
|
||||||
|
#ifdef HARDCODED_CONFIG
|
||||||
|
current_config.led_pin_strip_a = HARDCODED_CONFIG_LED_STRIP_A_PIN;
|
||||||
|
current_config.led_pin_strip_b = HARDCODED_CONFIG_LED_STRIP_B_PIN;
|
||||||
|
current_config.led_count_strip_a = HARDCODED_CONFIG_LED_STRIP_A_COUNT;
|
||||||
|
current_config.led_count_strip_b = HARDCODED_CONFIG_LED_STRIP_B_COUNT;
|
||||||
|
current_config.pwm_pin = HARDCODED_CONFIG_PWM_PIN;
|
||||||
|
current_config.localBtn_pin = HARDCODED_CONFIG_LOCALBTN_PIN;
|
||||||
|
|
||||||
|
save_config_to_nvs();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Load configuration
|
||||||
|
load_config_from_nvs();
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Config initialized successfully");
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void calculate_config_hash(const config_t *cfg, uint8_t *out_hash)
|
||||||
|
{
|
||||||
|
mbedtls_sha256_context ctx;
|
||||||
|
|
||||||
|
mbedtls_sha256_init(&ctx);
|
||||||
|
mbedtls_sha256_starts(&ctx, 0); // 0 = SHA-256, 1 = SHA-224
|
||||||
|
|
||||||
|
mbedtls_sha256_update(
|
||||||
|
&ctx,
|
||||||
|
(const unsigned char *)cfg,
|
||||||
|
offsetof(config_t, hash));
|
||||||
|
|
||||||
|
mbedtls_sha256_finish(&ctx, out_hash);
|
||||||
|
mbedtls_sha256_free(&ctx);
|
||||||
|
}
|
||||||
48
main/config.h
Normal file
48
main/config.h
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* @file config.h
|
||||||
|
* @brief Config module for LED controller - handles read and store of persistent data
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef CONFIG_H
|
||||||
|
#define CONFIG_H
|
||||||
|
|
||||||
|
#include "esp_err.h"
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#define CONFIG_HASH_LEN 32 // SHA256
|
||||||
|
/**
|
||||||
|
* @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 led_count_strip_a; // LED count for LED strip A (-1 = not configured)
|
||||||
|
int8_t led_count_strip_b; // LED count for LED strip B (-1 = not configured)
|
||||||
|
int8_t pwm_pin; // GPIO pin for PWM input (-1 = not configured)
|
||||||
|
int8_t localBtn_pin; // GPIO pin for local btn input (-1 = not configured)
|
||||||
|
uint8_t hash[CONFIG_HASH_LEN]; // SHA256 Hash of config
|
||||||
|
} config_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initialize the config system
|
||||||
|
* Loads configuration from NVS
|
||||||
|
* @return ESP_OK on success
|
||||||
|
*/
|
||||||
|
esp_err_t config_init(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get current configuration
|
||||||
|
* @param Pointer to current configuration (read-only)
|
||||||
|
*/
|
||||||
|
void config_get_config(config_t *const cnf);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Reset configuration to defaults
|
||||||
|
* @return ESP_OK on success
|
||||||
|
*/
|
||||||
|
esp_err_t config_reset_config(void);
|
||||||
|
|
||||||
|
#endif // CONFIG_H
|
||||||
642
main/control.c
642
main/control.c
@ -1,263 +1,27 @@
|
|||||||
/**
|
/**
|
||||||
* @file control.c
|
* @file control.c
|
||||||
* @brief Control module implementation with BLE, NVS, and OTA
|
* @brief Control module implementation
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "control.h"
|
#include "control.h"
|
||||||
|
#include "config.h"
|
||||||
#include "led.h"
|
#include "led.h"
|
||||||
#include "rcsignal.h"
|
#include "rcsignal.h"
|
||||||
|
#include "localbtn.h"
|
||||||
#include "animation.h"
|
#include "animation.h"
|
||||||
|
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
#include "esp_log.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";
|
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 uint8_t current_animation_mode = 0;
|
||||||
static esp_timer_handle_t ble_timeout_timer = NULL;
|
|
||||||
static bool ble_connected = false;
|
|
||||||
|
|
||||||
// OTA state
|
|
||||||
static const esp_partition_t *update_partition = NULL;
|
|
||||||
static esp_ota_handle_t update_handle = 0;
|
|
||||||
static size_t ota_bytes_written = 0;
|
|
||||||
|
|
||||||
// BLE variables
|
|
||||||
static uint8_t adv_config_done = 0;
|
|
||||||
static uint16_t gatts_if_global = ESP_GATT_IF_NONE;
|
|
||||||
static uint16_t conn_id_global = 0;
|
|
||||||
static uint16_t service_handle = 0;
|
|
||||||
|
|
||||||
// BLE advertising parameters
|
|
||||||
static esp_ble_adv_params_t adv_params = {
|
|
||||||
.adv_int_min = 0x20,
|
|
||||||
.adv_int_max = 0x40,
|
|
||||||
.adv_type = ADV_TYPE_IND,
|
|
||||||
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
|
|
||||||
.channel_map = ADV_CHNL_ALL,
|
|
||||||
.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Characteristic handles
|
|
||||||
static struct
|
|
||||||
{
|
|
||||||
uint16_t config_handle;
|
|
||||||
uint16_t mode_handle;
|
|
||||||
uint16_t pwm_handle;
|
|
||||||
uint16_t ota_handle;
|
|
||||||
} char_handles = {0};
|
|
||||||
|
|
||||||
// Forward declarations
|
|
||||||
static void ble_timeout_callback(void *arg);
|
|
||||||
static void on_mode_change(uint8_t new_mode);
|
|
||||||
|
|
||||||
// NVS Functions
|
|
||||||
static esp_err_t load_config_from_nvs(void)
|
|
||||||
{
|
|
||||||
nvs_handle_t nvs_handle;
|
|
||||||
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &nvs_handle);
|
|
||||||
if (err != ESP_OK)
|
|
||||||
{
|
|
||||||
ESP_LOGW(TAG, "NVS not found, using defaults");
|
|
||||||
return ESP_ERR_NOT_FOUND;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t required_size = sizeof(controller_config_t);
|
|
||||||
err = nvs_get_blob(nvs_handle, "config", ¤t_config, &required_size);
|
|
||||||
nvs_close(nvs_handle);
|
|
||||||
|
|
||||||
if (err != ESP_OK || current_config.magic != CONFIG_MAGIC)
|
|
||||||
{
|
|
||||||
ESP_LOGW(TAG, "Invalid config in NVS, using defaults");
|
|
||||||
return ESP_ERR_INVALID_STATE;
|
|
||||||
}
|
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Loaded config from NVS");
|
|
||||||
ESP_LOGI(TAG, " Strip A: GPIO%d", current_config.led_pin_strip_a);
|
|
||||||
ESP_LOGI(TAG, " Strip B: GPIO%d", current_config.led_pin_strip_b);
|
|
||||||
ESP_LOGI(TAG, " PWM Pin: GPIO%d", current_config.pwm_pin);
|
|
||||||
ESP_LOGI(TAG, " BLE Timeout: %d", current_config.ble_timeout);
|
|
||||||
|
|
||||||
return ESP_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
static esp_err_t save_config_to_nvs(void)
|
|
||||||
{
|
|
||||||
nvs_handle_t nvs_handle;
|
|
||||||
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs_handle);
|
|
||||||
if (err != ESP_OK)
|
|
||||||
{
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
current_config.magic = CONFIG_MAGIC;
|
|
||||||
err = nvs_set_blob(nvs_handle, "config", ¤t_config, sizeof(controller_config_t));
|
|
||||||
if (err == ESP_OK)
|
|
||||||
{
|
|
||||||
err = nvs_commit(nvs_handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
nvs_close(nvs_handle);
|
|
||||||
|
|
||||||
if (err == ESP_OK)
|
|
||||||
{
|
|
||||||
ESP_LOGI(TAG, "Config saved to NVS");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ESP_LOGE(TAG, "Failed to save config: %s", esp_err_to_name(err));
|
|
||||||
}
|
|
||||||
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
esp_err_t control_reset_config(void)
|
|
||||||
{
|
|
||||||
current_config.led_pin_strip_a = -1;
|
|
||||||
current_config.led_pin_strip_b = -1;
|
|
||||||
current_config.pwm_pin = -1;
|
|
||||||
current_config.ble_timeout = BLE_TIMEOUT_NEVER;
|
|
||||||
current_config.magic = CONFIG_MAGIC;
|
|
||||||
|
|
||||||
return save_config_to_nvs();
|
|
||||||
}
|
|
||||||
|
|
||||||
const controller_config_t *control_get_config(void)
|
|
||||||
{
|
|
||||||
return ¤t_config;
|
|
||||||
}
|
|
||||||
|
|
||||||
esp_err_t control_update_config(const controller_config_t *config)
|
|
||||||
{
|
|
||||||
if (!config)
|
|
||||||
{
|
|
||||||
return ESP_ERR_INVALID_ARG;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reinitialize if pins changed
|
|
||||||
bool pins_changed = (current_config.led_pin_strip_a != config->led_pin_strip_a) ||
|
|
||||||
(current_config.led_pin_strip_b != config->led_pin_strip_b) ||
|
|
||||||
(current_config.pwm_pin != config->pwm_pin);
|
|
||||||
|
|
||||||
memcpy(¤t_config, config, sizeof(controller_config_t));
|
|
||||||
esp_err_t err = save_config_to_nvs();
|
|
||||||
|
|
||||||
if (err == ESP_OK && pins_changed)
|
|
||||||
{
|
|
||||||
ESP_LOGI(TAG, "Restarting to apply new pin configuration...");
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
|
||||||
esp_restart();
|
|
||||||
}
|
|
||||||
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
// BLE timeout handling
|
|
||||||
static void ble_timeout_callback(void *arg)
|
|
||||||
{
|
|
||||||
if (!ble_connected)
|
|
||||||
{
|
|
||||||
ESP_LOGI(TAG, "BLE timeout reached, disabling BLE");
|
|
||||||
control_disable_ble();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void start_ble_timeout(void)
|
|
||||||
{
|
|
||||||
if (current_config.ble_timeout == BLE_TIMEOUT_NEVER)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ble_timeout_timer == NULL)
|
|
||||||
{
|
|
||||||
esp_timer_create_args_t timer_args = {
|
|
||||||
.callback = ble_timeout_callback,
|
|
||||||
.name = "ble_timeout"};
|
|
||||||
esp_timer_create(&timer_args, &ble_timeout_timer);
|
|
||||||
}
|
|
||||||
|
|
||||||
esp_timer_stop(ble_timeout_timer);
|
|
||||||
esp_timer_start_once(ble_timeout_timer, (uint64_t)current_config.ble_timeout * 1000000ULL);
|
|
||||||
ESP_LOGI(TAG, "BLE timeout started: %d seconds", current_config.ble_timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
void control_disable_ble(void)
|
|
||||||
{
|
|
||||||
if (!ble_enabled)
|
|
||||||
return;
|
|
||||||
|
|
||||||
ble_enabled = false;
|
|
||||||
|
|
||||||
if (ble_timeout_timer)
|
|
||||||
{
|
|
||||||
esp_timer_stop(ble_timeout_timer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop BLE advertising
|
|
||||||
esp_ble_gap_stop_advertising();
|
|
||||||
|
|
||||||
ESP_LOGI(TAG, "BLE disabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool control_is_ble_enabled(void)
|
|
||||||
{
|
|
||||||
return ble_enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Animation mode change callback
|
// Animation mode change callback
|
||||||
static void on_mode_change(uint8_t new_mode)
|
static void on_mode_change()
|
||||||
{
|
{
|
||||||
current_animation_mode = new_mode;
|
current_animation_mode = (current_animation_mode + 1) % ANIM_MODE_COUNT;
|
||||||
animation_set_mode((animation_mode_t)new_mode);
|
animation_set_mode((animation_mode_t)current_animation_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)
|
uint8_t control_get_animation_mode(void)
|
||||||
@ -265,366 +29,6 @@ uint8_t control_get_animation_mode(void)
|
|||||||
return current_animation_mode;
|
return current_animation_mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
void control_emulate_pwm_pulse(void)
|
|
||||||
{
|
|
||||||
rcsignal_trigger_mode_change();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Embedded web files (will be linked)
|
|
||||||
extern const uint8_t index_html_start[] asm("_binary_index_html_start");
|
|
||||||
extern const uint8_t index_html_end[] asm("_binary_index_html_end");
|
|
||||||
extern const uint8_t app_js_start[] asm("_binary_app_js_start");
|
|
||||||
extern const uint8_t app_js_end[] asm("_binary_app_js_end");
|
|
||||||
extern const uint8_t style_css_start[] asm("_binary_style_css_start");
|
|
||||||
extern const uint8_t style_css_end[] asm("_binary_style_css_end");
|
|
||||||
extern const uint8_t favicon_ico_start[] asm("_binary_favicon_ico_start");
|
|
||||||
extern const uint8_t favicon_ico_end[] asm("_binary_favicon_ico_end");
|
|
||||||
|
|
||||||
// BLE GAP event handler
|
|
||||||
static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param)
|
|
||||||
{
|
|
||||||
ESP_LOGI(TAG, "gap_event_handler() event: %i\n", event);
|
|
||||||
switch (event)
|
|
||||||
{
|
|
||||||
case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
|
|
||||||
adv_config_done &= (~ADV_CONFIG_FLAG);
|
|
||||||
if (adv_config_done == 0)
|
|
||||||
{
|
|
||||||
esp_ble_gap_start_advertising(&adv_params);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
|
|
||||||
adv_config_done &= (~SCAN_RSP_CONFIG_FLAG);
|
|
||||||
if (adv_config_done == 0)
|
|
||||||
{
|
|
||||||
esp_ble_gap_start_advertising(&adv_params);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
|
|
||||||
if (param->adv_start_cmpl.status == ESP_BT_STATUS_SUCCESS)
|
|
||||||
{
|
|
||||||
ESP_LOGI(TAG, "BLE advertising started");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BLE GATTS event handler
|
|
||||||
static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param)
|
|
||||||
{
|
|
||||||
switch (event)
|
|
||||||
{
|
|
||||||
case ESP_GATTS_REG_EVT:
|
|
||||||
ESP_LOGI(TAG, "GATTS register, status %d, app_id %d", param->reg.status, param->reg.app_id);
|
|
||||||
gatts_if_global = gatts_if;
|
|
||||||
|
|
||||||
// Set device name
|
|
||||||
esp_ble_gap_set_device_name(DEVICE_NAME);
|
|
||||||
|
|
||||||
// Config advertising data
|
|
||||||
esp_ble_adv_data_t adv_data = {
|
|
||||||
.set_scan_rsp = false,
|
|
||||||
.include_name = true,
|
|
||||||
.include_txpower = true,
|
|
||||||
.min_interval = 0x0006,
|
|
||||||
.max_interval = 0x0010,
|
|
||||||
.appearance = 0x00,
|
|
||||||
.manufacturer_len = 0,
|
|
||||||
.p_manufacturer_data = NULL,
|
|
||||||
.service_data_len = 0,
|
|
||||||
.p_service_data = NULL,
|
|
||||||
.service_uuid_len = sizeof(uint16_t),
|
|
||||||
.p_service_uuid = (uint8_t[]){GATTS_SERVICE_UUID & 0xFF, (GATTS_SERVICE_UUID >> 8) & 0xFF},
|
|
||||||
.flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT),
|
|
||||||
};
|
|
||||||
esp_ble_gap_config_adv_data(&adv_data);
|
|
||||||
adv_config_done |= ADV_CONFIG_FLAG;
|
|
||||||
|
|
||||||
// Create service
|
|
||||||
esp_gatt_srvc_id_t service_id = {
|
|
||||||
.is_primary = true,
|
|
||||||
.id.inst_id = 0,
|
|
||||||
.id.uuid.len = ESP_UUID_LEN_16,
|
|
||||||
.id.uuid.uuid.uuid16 = GATTS_SERVICE_UUID,
|
|
||||||
};
|
|
||||||
esp_ble_gatts_create_service(gatts_if, &service_id, GATTS_NUM_HANDLE_TEST);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ESP_GATTS_CREATE_EVT:
|
|
||||||
ESP_LOGI(TAG, "CREATE_SERVICE_EVT, status %d, service_handle %d", param->create.status, param->create.service_handle);
|
|
||||||
service_handle = param->create.service_handle;
|
|
||||||
|
|
||||||
esp_ble_gatts_start_service(service_handle);
|
|
||||||
|
|
||||||
// Add characteristics
|
|
||||||
esp_bt_uuid_t char_uuid;
|
|
||||||
char_uuid.len = ESP_UUID_LEN_16;
|
|
||||||
|
|
||||||
// Config characteristic
|
|
||||||
char_uuid.uuid.uuid16 = GATTS_CHAR_UUID_CONFIG;
|
|
||||||
esp_ble_gatts_add_char(service_handle, &char_uuid,
|
|
||||||
ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
|
|
||||||
ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_WRITE,
|
|
||||||
NULL, NULL);
|
|
||||||
|
|
||||||
// Mode characteristic
|
|
||||||
char_uuid.uuid.uuid16 = GATTS_CHAR_UUID_MODE;
|
|
||||||
esp_ble_gatts_add_char(service_handle, &char_uuid,
|
|
||||||
ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
|
|
||||||
ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_WRITE | ESP_GATT_CHAR_PROP_BIT_NOTIFY,
|
|
||||||
NULL, NULL);
|
|
||||||
|
|
||||||
// PWM emulation characteristic
|
|
||||||
char_uuid.uuid.uuid16 = GATTS_CHAR_UUID_PWM;
|
|
||||||
esp_ble_gatts_add_char(service_handle, &char_uuid,
|
|
||||||
ESP_GATT_PERM_WRITE,
|
|
||||||
ESP_GATT_CHAR_PROP_BIT_WRITE,
|
|
||||||
NULL, NULL);
|
|
||||||
|
|
||||||
// OTA characteristic
|
|
||||||
char_uuid.uuid.uuid16 = GATTS_CHAR_UUID_OTA;
|
|
||||||
esp_ble_gatts_add_char(service_handle, &char_uuid,
|
|
||||||
ESP_GATT_PERM_WRITE,
|
|
||||||
ESP_GATT_CHAR_PROP_BIT_WRITE,
|
|
||||||
NULL, NULL);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ESP_GATTS_ADD_CHAR_EVT:
|
|
||||||
ESP_LOGI(TAG, "ADD_CHAR_EVT, status %d, char_handle %d", param->add_char.status, param->add_char.attr_handle);
|
|
||||||
|
|
||||||
// Store handles
|
|
||||||
if (param->add_char.char_uuid.uuid.uuid16 == GATTS_CHAR_UUID_CONFIG)
|
|
||||||
{
|
|
||||||
char_handles.config_handle = param->add_char.attr_handle;
|
|
||||||
}
|
|
||||||
else if (param->add_char.char_uuid.uuid.uuid16 == GATTS_CHAR_UUID_MODE)
|
|
||||||
{
|
|
||||||
char_handles.mode_handle = param->add_char.attr_handle;
|
|
||||||
}
|
|
||||||
else if (param->add_char.char_uuid.uuid.uuid16 == GATTS_CHAR_UUID_PWM)
|
|
||||||
{
|
|
||||||
char_handles.pwm_handle = param->add_char.attr_handle;
|
|
||||||
}
|
|
||||||
else if (param->add_char.char_uuid.uuid.uuid16 == GATTS_CHAR_UUID_OTA)
|
|
||||||
{
|
|
||||||
char_handles.ota_handle = param->add_char.attr_handle;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ESP_GATTS_CONNECT_EVT:
|
|
||||||
ESP_LOGI(TAG, "BLE device connected");
|
|
||||||
conn_id_global = param->connect.conn_id;
|
|
||||||
ble_connected = true;
|
|
||||||
|
|
||||||
// Stop timeout timer when connected
|
|
||||||
if (ble_timeout_timer)
|
|
||||||
{
|
|
||||||
esp_timer_stop(ble_timeout_timer);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ESP_GATTS_DISCONNECT_EVT:
|
|
||||||
ESP_LOGI(TAG, "BLE device disconnected");
|
|
||||||
ble_connected = false;
|
|
||||||
|
|
||||||
// Restart advertising and timeout
|
|
||||||
if (ble_enabled)
|
|
||||||
{
|
|
||||||
esp_ble_gap_start_advertising(&adv_params);
|
|
||||||
start_ble_timeout();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ESP_GATTS_READ_EVT:
|
|
||||||
ESP_LOGI(TAG, "GATTS_READ_EVT, handle %d", param->read.handle);
|
|
||||||
|
|
||||||
esp_gatt_rsp_t rsp;
|
|
||||||
memset(&rsp, 0, sizeof(esp_gatt_rsp_t));
|
|
||||||
rsp.attr_value.handle = param->read.handle;
|
|
||||||
|
|
||||||
if (param->read.handle == char_handles.config_handle)
|
|
||||||
{
|
|
||||||
rsp.attr_value.len = sizeof(controller_config_t);
|
|
||||||
memcpy(rsp.attr_value.value, ¤t_config, sizeof(controller_config_t));
|
|
||||||
}
|
|
||||||
else if (param->read.handle == char_handles.mode_handle)
|
|
||||||
{
|
|
||||||
rsp.attr_value.len = 1;
|
|
||||||
rsp.attr_value.value[0] = current_animation_mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
esp_ble_gatts_send_response(gatts_if, param->read.conn_id, param->read.trans_id,
|
|
||||||
ESP_GATT_OK, &rsp);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ESP_GATTS_WRITE_EVT:
|
|
||||||
ESP_LOGI(TAG, "GATTS_WRITE_EVT, handle %d, len %d", param->write.handle, param->write.len);
|
|
||||||
|
|
||||||
if (param->write.handle == char_handles.config_handle)
|
|
||||||
{
|
|
||||||
// Update configuration
|
|
||||||
if (param->write.len == sizeof(controller_config_t))
|
|
||||||
{
|
|
||||||
controller_config_t new_config;
|
|
||||||
memcpy(&new_config, param->write.value, sizeof(controller_config_t));
|
|
||||||
control_update_config(&new_config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (param->write.handle == char_handles.mode_handle)
|
|
||||||
{
|
|
||||||
// Set animation mode
|
|
||||||
if (param->write.len == 1)
|
|
||||||
{
|
|
||||||
control_set_animation_mode(param->write.value[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (param->write.handle == char_handles.pwm_handle)
|
|
||||||
{
|
|
||||||
// Emulate PWM pulse
|
|
||||||
control_emulate_pwm_pulse();
|
|
||||||
}
|
|
||||||
else if (param->write.handle == char_handles.ota_handle)
|
|
||||||
{
|
|
||||||
// Handle OTA data
|
|
||||||
if (ota_bytes_written == 0)
|
|
||||||
{
|
|
||||||
// First packet - start OTA
|
|
||||||
ESP_LOGI(TAG, "Starting OTA update...");
|
|
||||||
update_partition = esp_ota_get_next_update_partition(NULL);
|
|
||||||
if (update_partition == NULL)
|
|
||||||
{
|
|
||||||
ESP_LOGE(TAG, "No OTA partition found");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
esp_err_t err = esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &update_handle);
|
|
||||||
if (err != ESP_OK)
|
|
||||||
{
|
|
||||||
ESP_LOGE(TAG, "OTA begin failed: %s", esp_err_to_name(err));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write OTA data
|
|
||||||
esp_err_t err = esp_ota_write(update_handle, param->write.value, param->write.len);
|
|
||||||
if (err != ESP_OK)
|
|
||||||
{
|
|
||||||
ESP_LOGE(TAG, "OTA write failed: %s", esp_err_to_name(err));
|
|
||||||
esp_ota_abort(update_handle);
|
|
||||||
ota_bytes_written = 0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
ota_bytes_written += param->write.len;
|
|
||||||
ESP_LOGI(TAG, "OTA progress: %d bytes", ota_bytes_written);
|
|
||||||
|
|
||||||
// Check if this is the last packet (indicated by packet size < MTU)
|
|
||||||
if (param->write.len < 512)
|
|
||||||
{
|
|
||||||
ESP_LOGI(TAG, "OTA complete, total bytes: %d", ota_bytes_written);
|
|
||||||
|
|
||||||
err = esp_ota_end(update_handle);
|
|
||||||
if (err != ESP_OK)
|
|
||||||
{
|
|
||||||
ESP_LOGE(TAG, "OTA end failed: %s", esp_err_to_name(err));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
err = esp_ota_set_boot_partition(update_partition);
|
|
||||||
if (err != ESP_OK)
|
|
||||||
{
|
|
||||||
ESP_LOGE(TAG, "OTA set boot partition failed: %s", esp_err_to_name(err));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset configuration
|
|
||||||
control_reset_config();
|
|
||||||
|
|
||||||
ESP_LOGI(TAG, "OTA successful, restarting...");
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
|
||||||
esp_restart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!param->write.is_prep)
|
|
||||||
{
|
|
||||||
esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id,
|
|
||||||
ESP_GATT_OK, NULL);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize BLE
|
|
||||||
static esp_err_t init_ble(void)
|
|
||||||
{
|
|
||||||
if (!ble_enabled)
|
|
||||||
{
|
|
||||||
ESP_LOGI(TAG, "BLE disabled by configuration");
|
|
||||||
return ESP_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
esp_err_t ret;
|
|
||||||
|
|
||||||
// Initialize BT controller
|
|
||||||
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
|
|
||||||
ret = esp_bt_controller_init(&bt_cfg);
|
|
||||||
if (ret)
|
|
||||||
{
|
|
||||||
ESP_LOGE(TAG, "BT controller init failed: %s", esp_err_to_name(ret));
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
|
|
||||||
if (ret)
|
|
||||||
{
|
|
||||||
ESP_LOGE(TAG, "BT controller enable failed: %s", esp_err_to_name(ret));
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
ret = esp_bluedroid_init();
|
|
||||||
if (ret)
|
|
||||||
{
|
|
||||||
ESP_LOGE(TAG, "Bluedroid init failed: %s", esp_err_to_name(ret));
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
ret = esp_bluedroid_enable();
|
|
||||||
if (ret)
|
|
||||||
{
|
|
||||||
ESP_LOGE(TAG, "Bluedroid enable failed: %s", esp_err_to_name(ret));
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register callbacks
|
|
||||||
esp_ble_gatts_register_callback(gatts_event_handler);
|
|
||||||
esp_ble_gap_register_callback(gap_event_handler);
|
|
||||||
esp_ble_gatts_app_register(0);
|
|
||||||
|
|
||||||
// Set MTU
|
|
||||||
esp_ble_gatt_set_local_mtu(517);
|
|
||||||
|
|
||||||
// Start timeout timer
|
|
||||||
start_ble_timeout();
|
|
||||||
|
|
||||||
esp_ble_gatts_app_register(0);
|
|
||||||
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(100));
|
|
||||||
esp_ble_gap_start_advertising(&adv_params);
|
|
||||||
|
|
||||||
ESP_LOGI(TAG, "BLE initialized");
|
|
||||||
|
|
||||||
return ESP_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main initialization
|
// Main initialization
|
||||||
esp_err_t control_init(void)
|
esp_err_t control_init(void)
|
||||||
{
|
{
|
||||||
@ -632,21 +36,20 @@ esp_err_t control_init(void)
|
|||||||
|
|
||||||
ESP_LOGI(TAG, "Initializing LED Controller...");
|
ESP_LOGI(TAG, "Initializing LED Controller...");
|
||||||
|
|
||||||
// Initialize NVS
|
// Initialize config
|
||||||
ret = nvs_flash_init();
|
ret = config_init();
|
||||||
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
|
if (ret != ESP_OK)
|
||||||
{
|
{
|
||||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
ESP_LOGE(TAG, "Config init failed: %s", esp_err_to_name(ret));
|
||||||
ret = nvs_flash_init();
|
return ret;
|
||||||
}
|
}
|
||||||
ESP_ERROR_CHECK(ret);
|
|
||||||
|
|
||||||
// Load configuration
|
config_t current_config;
|
||||||
load_config_from_nvs();
|
config_get_config(¤t_config);
|
||||||
|
|
||||||
// Initialize LED strips
|
// Initialize LED strips
|
||||||
ret = led_init(current_config.led_pin_strip_a, current_config.led_pin_strip_b,
|
ret = led_init(current_config.led_pin_strip_a, current_config.led_pin_strip_b,
|
||||||
DEFAULT_NUM_LEDS_A, DEFAULT_NUM_LEDS_B);
|
current_config.led_count_strip_a, current_config.led_count_strip_b);
|
||||||
if (ret != ESP_OK)
|
if (ret != ESP_OK)
|
||||||
{
|
{
|
||||||
ESP_LOGE(TAG, "LED init failed: %s", esp_err_to_name(ret));
|
ESP_LOGE(TAG, "LED init failed: %s", esp_err_to_name(ret));
|
||||||
@ -669,17 +72,18 @@ esp_err_t control_init(void)
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register mode change callback
|
// Initialize local BTN
|
||||||
rcsignal_register_callback(on_mode_change);
|
ret = localbtn_init(current_config.localBtn_pin);
|
||||||
|
|
||||||
// Initialize BLE
|
|
||||||
ret = init_ble();
|
|
||||||
if (ret != ESP_OK)
|
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;
|
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");
|
ESP_LOGI(TAG, "Control system initialized successfully");
|
||||||
|
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
|
|||||||
@ -1,35 +1,16 @@
|
|||||||
/**
|
/**
|
||||||
* @file control.h
|
* @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
|
#ifndef CONTROL_H
|
||||||
#define CONTROL_H
|
#define CONTROL_H
|
||||||
|
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdbool.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
|
* @brief Initialize the control system
|
||||||
* Loads configuration from NVS and initializes subsystems
|
* Loads configuration from NVS and initializes subsystems
|
||||||
@ -37,47 +18,6 @@ typedef struct {
|
|||||||
*/
|
*/
|
||||||
esp_err_t control_init(void);
|
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
|
* @brief Get current animation mode
|
||||||
* @return Current mode (0-13)
|
* @return Current mode (0-13)
|
||||||
|
|||||||
23
main/led.c
23
main/led.c
@ -4,10 +4,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "led.h"
|
#include "led.h"
|
||||||
|
|
||||||
#include "driver/rmt_tx.h"
|
#include "driver/rmt_tx.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/semphr.h"
|
#include "freertos/semphr.h"
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
|
||||||
@ -192,7 +194,7 @@ static esp_err_t init_strip(led_strip_t *strip, int8_t pin, uint16_t num_leds)
|
|||||||
rmt_tx_channel_config_t tx_chan_config = {
|
rmt_tx_channel_config_t tx_chan_config = {
|
||||||
.clk_src = RMT_CLK_SRC_DEFAULT,
|
.clk_src = RMT_CLK_SRC_DEFAULT,
|
||||||
.gpio_num = pin,
|
.gpio_num = pin,
|
||||||
.mem_block_symbols = 64,
|
.mem_block_symbols = 48,
|
||||||
.resolution_hz = 80000000, // 80MHz
|
.resolution_hz = 80000000, // 80MHz
|
||||||
.trans_queue_depth = 4,
|
.trans_queue_depth = 4,
|
||||||
};
|
};
|
||||||
@ -319,7 +321,10 @@ static void show_strip(led_strip_t *strip)
|
|||||||
// Convert RGB to GRB for WS2812B
|
// Convert RGB to GRB for WS2812B
|
||||||
uint8_t *grb_data = malloc(strip->num_leds * 3);
|
uint8_t *grb_data = malloc(strip->num_leds * 3);
|
||||||
if (!grb_data)
|
if (!grb_data)
|
||||||
|
{
|
||||||
|
ESP_LOGE(TAG, "Failed to allocate GRB buffer");
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (uint16_t i = 0; i < strip->num_leds; i++)
|
for (uint16_t i = 0; i < strip->num_leds; i++)
|
||||||
{
|
{
|
||||||
@ -332,7 +337,21 @@ static void show_strip(led_strip_t *strip)
|
|||||||
.loop_count = 0,
|
.loop_count = 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
rmt_transmit(strip->rmt_channel, strip->encoder, grb_data, strip->num_leds * 3, &tx_config);
|
esp_err_t ret = rmt_transmit(strip->rmt_channel, strip->encoder, grb_data, strip->num_leds * 3, &tx_config);
|
||||||
|
if (ret != ESP_OK)
|
||||||
|
{
|
||||||
|
ESP_LOGE(TAG, "RMT transmit failed: %s", esp_err_to_name(ret));
|
||||||
|
free(grb_data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for transmission to complete before freeing buffer
|
||||||
|
ret = rmt_tx_wait_all_done(strip->rmt_channel, pdMS_TO_TICKS(100));
|
||||||
|
if (ret != ESP_OK)
|
||||||
|
{
|
||||||
|
ESP_LOGW(TAG, "RMT wait timeout");
|
||||||
|
}
|
||||||
|
|
||||||
free(grb_data);
|
free(grb_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,9 +6,10 @@
|
|||||||
#ifndef LED_H
|
#ifndef LED_H
|
||||||
#define LED_H
|
#define LED_H
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
#define LED_STRIP_MAX_LEDS 100 // Maximum LEDs per strip
|
#define LED_STRIP_MAX_LEDS 100 // Maximum LEDs per strip
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
208
main/localbtn.c
Normal file
208
main/localbtn.c
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
/**
|
||||||
|
* @file localbtn.c
|
||||||
|
* @brief Local GPIO button reading using interrupt-based edge detection
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "localbtn.h"
|
||||||
|
|
||||||
|
#include "driver/gpio.h"
|
||||||
|
#include "esp_timer.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "freertos/queue.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static const char *TAG = "LOCALBTN";
|
||||||
|
|
||||||
|
#define DEBOUNCE_TIME_MS 50 // Debounce time in milliseconds
|
||||||
|
|
||||||
|
// Button state
|
||||||
|
static struct
|
||||||
|
{
|
||||||
|
int8_t gpio_pin;
|
||||||
|
bool initialized;
|
||||||
|
TaskHandle_t task_handle;
|
||||||
|
QueueHandle_t event_queue;
|
||||||
|
localbtn_mode_change_callback_t callback;
|
||||||
|
int64_t last_press_time; // For debouncing
|
||||||
|
} button_state = {
|
||||||
|
.gpio_pin = -1,
|
||||||
|
.initialized = false,
|
||||||
|
.task_handle = NULL,
|
||||||
|
.event_queue = NULL,
|
||||||
|
.callback = NULL,
|
||||||
|
.last_press_time = 0};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief GPIO interrupt handler (ISR)
|
||||||
|
* Minimal work in ISR - just send event to task
|
||||||
|
*/
|
||||||
|
static void IRAM_ATTR gpio_isr_handler(void *arg)
|
||||||
|
{
|
||||||
|
int64_t now = esp_timer_get_time();
|
||||||
|
|
||||||
|
// Send timestamp to queue for debouncing in task
|
||||||
|
BaseType_t high_priority_task_woken = pdFALSE;
|
||||||
|
xQueueSendFromISR(button_state.event_queue, &now, &high_priority_task_woken);
|
||||||
|
|
||||||
|
if (high_priority_task_woken)
|
||||||
|
{
|
||||||
|
portYIELD_FROM_ISR();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Button handling task
|
||||||
|
* Handles debouncing and callback execution
|
||||||
|
*/
|
||||||
|
static void localbtn_task(void *arg)
|
||||||
|
{
|
||||||
|
int64_t event_time;
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Button task started, monitoring GPIO%d", button_state.gpio_pin);
|
||||||
|
|
||||||
|
while (1)
|
||||||
|
{
|
||||||
|
// Wait for button press event from ISR
|
||||||
|
if (xQueueReceive(button_state.event_queue, &event_time, portMAX_DELAY))
|
||||||
|
{
|
||||||
|
// Debouncing: Check if enough time has passed since last press
|
||||||
|
int64_t time_since_last_press = (event_time - button_state.last_press_time) / 1000; // Convert to ms
|
||||||
|
|
||||||
|
if (time_since_last_press >= DEBOUNCE_TIME_MS)
|
||||||
|
{
|
||||||
|
// Valid button press - verify button is still pressed
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(10)); // Small delay to ensure stable state
|
||||||
|
|
||||||
|
if (gpio_get_level(button_state.gpio_pin) == 0)
|
||||||
|
{
|
||||||
|
ESP_LOGI(TAG, "Button press detected on GPIO%d", button_state.gpio_pin);
|
||||||
|
|
||||||
|
button_state.last_press_time = event_time;
|
||||||
|
|
||||||
|
// Execute callback
|
||||||
|
if (button_state.callback)
|
||||||
|
{
|
||||||
|
button_state.callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t localbtn_init(int8_t pin_localbtn)
|
||||||
|
{
|
||||||
|
if (pin_localbtn < 0)
|
||||||
|
{
|
||||||
|
ESP_LOGW(TAG, "Button disabled (invalid pin: %d)", pin_localbtn);
|
||||||
|
return ESP_ERR_NOT_SUPPORTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (button_state.initialized)
|
||||||
|
{
|
||||||
|
ESP_LOGW(TAG, "Button already initialized");
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
button_state.gpio_pin = pin_localbtn;
|
||||||
|
button_state.last_press_time = 0U;
|
||||||
|
|
||||||
|
// Create event queue for ISR->Task communication
|
||||||
|
button_state.event_queue = xQueueCreate(10, sizeof(int64_t));
|
||||||
|
if (button_state.event_queue == NULL)
|
||||||
|
{
|
||||||
|
ESP_LOGE(TAG, "Failed to create event queue");
|
||||||
|
return ESP_ERR_NO_MEM;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure GPIO
|
||||||
|
gpio_config_t io_conf = {
|
||||||
|
.pin_bit_mask = (1ULL << pin_localbtn),
|
||||||
|
.mode = GPIO_MODE_INPUT,
|
||||||
|
.pull_up_en = GPIO_PULLUP_ENABLE, // Enable internal pull-up (safe even with external)
|
||||||
|
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||||
|
.intr_type = GPIO_INTR_NEGEDGE // Interrupt on falling edge (button press)
|
||||||
|
};
|
||||||
|
|
||||||
|
esp_err_t ret = gpio_config(&io_conf);
|
||||||
|
if (ret != ESP_OK)
|
||||||
|
{
|
||||||
|
ESP_LOGE(TAG, "GPIO config failed: %s", esp_err_to_name(ret));
|
||||||
|
vQueueDelete(button_state.event_queue);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add ISR handler for this GPIO
|
||||||
|
ret = gpio_isr_handler_add(pin_localbtn, gpio_isr_handler, NULL);
|
||||||
|
if (ret != ESP_OK)
|
||||||
|
{
|
||||||
|
ESP_LOGE(TAG, "ISR handler add failed: %s", esp_err_to_name(ret));
|
||||||
|
vQueueDelete(button_state.event_queue);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create button handling task
|
||||||
|
BaseType_t task_ret = xTaskCreate(
|
||||||
|
localbtn_task,
|
||||||
|
"localbtn_task",
|
||||||
|
2048,
|
||||||
|
NULL,
|
||||||
|
5, // Priority 5 (same as other tasks)
|
||||||
|
&button_state.task_handle);
|
||||||
|
|
||||||
|
if (task_ret != pdPASS)
|
||||||
|
{
|
||||||
|
ESP_LOGE(TAG, "Failed to create button task");
|
||||||
|
gpio_isr_handler_remove(pin_localbtn);
|
||||||
|
vQueueDelete(button_state.event_queue);
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
button_state.initialized = true;
|
||||||
|
ESP_LOGI(TAG, "Button initialized on GPIO%d with interrupt-based detection", pin_localbtn);
|
||||||
|
ESP_LOGI(TAG, "Debounce time: %d ms", DEBOUNCE_TIME_MS);
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void localbtn_deinit(void)
|
||||||
|
{
|
||||||
|
if (!button_state.initialized)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove ISR handler
|
||||||
|
if (button_state.gpio_pin >= 0)
|
||||||
|
{
|
||||||
|
gpio_isr_handler_remove(button_state.gpio_pin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete task
|
||||||
|
if (button_state.task_handle)
|
||||||
|
{
|
||||||
|
vTaskDelete(button_state.task_handle);
|
||||||
|
button_state.task_handle = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete queue
|
||||||
|
if (button_state.event_queue)
|
||||||
|
{
|
||||||
|
vQueueDelete(button_state.event_queue);
|
||||||
|
button_state.event_queue = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
button_state.initialized = false;
|
||||||
|
button_state.callback = NULL;
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Button deinitialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
void localbtn_register_callback(localbtn_mode_change_callback_t cb)
|
||||||
|
{
|
||||||
|
button_state.callback = cb;
|
||||||
|
ESP_LOGI(TAG, "Callback registered");
|
||||||
|
}
|
||||||
37
main/localbtn.h
Normal file
37
main/localbtn.h
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* @file localbtn.h
|
||||||
|
* @brief Local GPIO button reading using interrupt-based edge detection
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef LOCALBTN_H
|
||||||
|
#define LOCALBTN_H
|
||||||
|
|
||||||
|
#include "esp_err.h"
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Callback function type for mode changes
|
||||||
|
*/
|
||||||
|
typedef void (*localbtn_mode_change_callback_t)();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initialize local button with interrupt-based detection
|
||||||
|
* @param pin_localbtn GPIO pin number for button (active low)
|
||||||
|
* @return ESP_OK on success
|
||||||
|
*/
|
||||||
|
esp_err_t localbtn_init(int8_t pin_localbtn);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Deinitialize local button reading
|
||||||
|
*/
|
||||||
|
void localbtn_deinit(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Register callback for mode changes
|
||||||
|
* @param cb Callback function
|
||||||
|
*/
|
||||||
|
void localbtn_register_callback(localbtn_mode_change_callback_t cb);
|
||||||
|
|
||||||
|
#endif // LOCALBTN_H
|
||||||
21
main/main.c
21
main/main.c
@ -3,15 +3,16 @@
|
|||||||
* @brief Main application entry point for LED Controller
|
* @brief Main application entry point for LED Controller
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <stdio.h>
|
#include "control.h"
|
||||||
|
#include "animation.h"
|
||||||
|
#include "led.h"
|
||||||
|
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_system.h"
|
#include "esp_system.h"
|
||||||
|
|
||||||
#include "control.h"
|
#include <stdio.h>
|
||||||
#include "animation.h"
|
|
||||||
#include "led.h"
|
|
||||||
|
|
||||||
static const char *TAG = "MAIN";
|
static const char *TAG = "MAIN";
|
||||||
|
|
||||||
@ -44,7 +45,7 @@ void app_main(void)
|
|||||||
ESP_LOGI(TAG, " ESP32 LED Controller for Model Aircraft");
|
ESP_LOGI(TAG, " ESP32 LED Controller for Model Aircraft");
|
||||||
ESP_LOGI(TAG, "==============================================");
|
ESP_LOGI(TAG, "==============================================");
|
||||||
|
|
||||||
// Initialize control system (LEDs, PWM, BLE)
|
// Initialize control system (LEDs, PWM)
|
||||||
esp_err_t ret = control_init();
|
esp_err_t ret = control_init();
|
||||||
if (ret != ESP_OK)
|
if (ret != ESP_OK)
|
||||||
{
|
{
|
||||||
@ -75,9 +76,9 @@ void app_main(void)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
animation_set_mode((animation_mode_t)control_get_animation_mode());
|
||||||
|
|
||||||
ESP_LOGI(TAG, "System initialized successfully");
|
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
|
// Main loop - just monitor system status
|
||||||
while (1)
|
while (1)
|
||||||
@ -85,10 +86,6 @@ void app_main(void)
|
|||||||
vTaskDelay(pdMS_TO_TICKS(5000));
|
vTaskDelay(pdMS_TO_TICKS(5000));
|
||||||
|
|
||||||
// Periodic status logging
|
// Periodic status logging
|
||||||
//const controller_config_t *config = control_get_config();
|
ESP_LOGI(TAG, "Animation Mode set to: %s", animation_get_mode_name(control_get_animation_mode()));
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,19 +4,19 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "rcsignal.h"
|
#include "rcsignal.h"
|
||||||
|
|
||||||
#include "driver/gpio.h"
|
#include "driver/gpio.h"
|
||||||
#include "esp_timer.h"
|
#include "esp_timer.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
static const char *TAG = "RCSIGNAL";
|
static const char *TAG = "RCSIGNAL";
|
||||||
|
|
||||||
#define MAX_MODES 14
|
|
||||||
#define PULSE_THRESHOLD_US 1500
|
#define PULSE_THRESHOLD_US 1500
|
||||||
#define SIGNAL_TIMEOUT_MS 100
|
#define SIGNAL_TIMEOUT_MS 100
|
||||||
|
|
||||||
static struct
|
static struct
|
||||||
{
|
{
|
||||||
int8_t gpio_pin;
|
int8_t gpio_pin;
|
||||||
@ -98,14 +98,10 @@ static void monitor_task(void *arg)
|
|||||||
{
|
{
|
||||||
// Mode change detected
|
// Mode change detected
|
||||||
rcsignal.pull_detected = false;
|
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)
|
if (rcsignal.callback)
|
||||||
{
|
{
|
||||||
rcsignal.callback(rcsignal.current_mode);
|
rcsignal.callback();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -185,17 +181,6 @@ bool rcsignal_is_active(void)
|
|||||||
return rcsignal.signal_active;
|
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)
|
uint8_t rcsignal_get_current_mode(void)
|
||||||
{
|
{
|
||||||
return rcsignal.current_mode;
|
return rcsignal.current_mode;
|
||||||
|
|||||||
@ -6,15 +6,15 @@
|
|||||||
#ifndef RCSIGNAL_H
|
#ifndef RCSIGNAL_H
|
||||||
#define RCSIGNAL_H
|
#define RCSIGNAL_H
|
||||||
|
|
||||||
|
#include "esp_err.h"
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include "esp_err.h"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Callback function type for mode changes
|
* @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);
|
typedef void (*rcsignal_mode_change_callback_t)();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Initialize RC signal reading
|
* @brief Initialize RC signal reading
|
||||||
@ -46,11 +46,6 @@ uint32_t rcsignal_get_pulse_width(void);
|
|||||||
*/
|
*/
|
||||||
bool rcsignal_is_active(void);
|
bool rcsignal_is_active(void);
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Manually trigger mode change (for emulation)
|
|
||||||
*/
|
|
||||||
void rcsignal_trigger_mode_change(void);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get current mode
|
* @brief Get current mode
|
||||||
* @return Current animation mode (0-13)
|
* @return Current animation mode (0-13)
|
||||||
|
|||||||
85
nfc01.ino
85
nfc01.ino
@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -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)
|
|
||||||
@ -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
|
|
||||||
@ -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();
|
|
||||||
});
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
Reference in New Issue
Block a user