23 Commits

Author SHA1 Message Date
3e20b79de7 sbus invertion in config 2026-02-14 22:06:28 +01:00
40d2880fdc define SBUS in config 2026-02-14 21:58:39 +01:00
4ccfc5128b support SBUS instead PWM 2026-02-14 21:42:46 +01:00
5cc2bd0b1d update hardware and nav animation 2026-02-14 21:18:41 +01:00
284553f3f8 C3 Demo 2026-01-17 23:15:51 +01:00
e25971af89 port to ESP32C3 2026-01-09 13:24:19 +01:00
5796b28e1a improve navigation animation 2026-01-06 23:02:54 +01:00
12a8710a2f cleanup 2026-01-06 22:44:36 +01:00
468d2cba74 fix chase rgb animation 2026-01-06 22:39:44 +01:00
d2d5d7dc4b fix chase animation 2026-01-06 22:33:28 +01:00
f1aac6611d fix random animation 2026-01-06 22:21:33 +01:00
733b05eaeb fix confetti animation 2026-01-06 22:06:02 +01:00
715d50c255 enable rainbow with glitter animation 2026-01-06 21:58:11 +01:00
883fff95dd fix rainbow animation 2026-01-06 21:55:00 +01:00
0f62418d93 fix sinelon 2026-01-06 21:35:28 +01:00
12b8acf81c fix navigation animation 2026-01-06 21:16:05 +01:00
a08dba780a cleanup 2026-01-06 20:16:11 +01:00
d576b4d42d handle BTN with interrupt 2026-01-06 18:15:53 +01:00
9ef50436a4 use sha256 instead of magic pattern 2026-01-06 12:38:40 +01:00
b1b179b5ff remove update function 2026-01-06 12:24:19 +01:00
3ada494d15 remove MAX_MODES 2026-01-06 12:20:27 +01:00
d33bda52d0 cleanup 2026-01-06 12:13:55 +01:00
0dd26fdcde localbtn deinit & cleanup 2026-01-06 11:57:24 +01:00
15 changed files with 1355 additions and 857 deletions

1
.gitignore vendored
View File

@ -291,3 +291,4 @@ dkms.conf
.vscode/settings.json
sdkconfig.defaults
.clangd

View File

@ -7,7 +7,7 @@ Professional LED controller firmware for ESP32. Designed for model aircraft with
### Hardware Support
- **ESP32 DevKitC** and **ESP32-C3 MINI** Development Board
- Dual WS2812B LED strip support (configurable GPIOs)
- PWM signal input for RC control
- PWM or SBUS signal input for RC control
- Real-time LED animation at 60 FPS
### Animation Modes
@ -32,9 +32,10 @@ Professional LED controller firmware for ESP32. Designed for model aircraft with
led-controller-firmware/
├── main/
│ ├── main.c # Application entry point
│ ├── control.c/h # NVS, initialization
│ ├── control.c/h # initialization
│ ├── config.c/h # NVS
│ ├── led.c/h # WS2812B control (RMT driver)
│ ├── rcsignal.c/h # PWM signal reading
│ ├── rcsignal.c/h # PWM/SBUS signal reading
│ ├── localbtn.c/h # Local btn reading
│ └── animation.c/h # LED animation patterns
├── CMakeLists.txt
@ -69,7 +70,7 @@ idf.py build
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
@ -79,7 +80,8 @@ ESP32 Pin -> Component
----------- ----------
GPIO XX -> WS2812B Strip A Data
GPIO XX -> WS2812B Strip B Data
GPIO XX -> RC PWM Signal
GPIO XX -> RC PWM/SBUS Signal
GPIO XX -> Local button Signal
GND -> Common Ground
5V -> LED Strip Power (if current < 500mA)
```
@ -91,11 +93,21 @@ GND -> Common Ground
- Add 100-500µF capacitor near strips
- Add 330Ω resistor on data line
### PWM Signal
### PWM Signal Mode
- Standard RC PWM: 1000-2000µs pulse width
- 1500µs threshold for mode switching
- Rising edge >1500µs after <1500µs triggers next mode
### SBUS Mode (FrSky X4R-SB)
```
X4R-SB SBUS/CH4 → ESP32 UART1 RX Pin (configured GPIO)
```
**Note**: The FrSky X4R-SB outputs inverted SBUS. The module automatically handles this inversion.
## Development
### Adding New Animations
@ -103,11 +115,6 @@ GND -> Common Ground
1. Add mode to `animation_mode_t` enum in `animation.h`
2. Implement animation function in `animation.c`
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
@ -121,6 +128,23 @@ idf.py monitor
# Exit monitor: Ctrl+]
```
#### Debug Output (SBUS Mode Only)
```c
// Print all 16 channels to console
rcsignal_debug_print_channels();
```
Example output:
```
I (12345) RCSIGNAL: SBUS Channels:
I (12345) RCSIGNAL: CH1: 992 CH2: 992 CH3: 172 CH4: 1811
I (12345) RCSIGNAL: CH5: 992 CH6: 992 CH7: 992 CH8: 992
I (12345) RCSIGNAL: CH9: 992 CH10: 992 CH11: 992 CH12: 992
I (12345) RCSIGNAL: CH13: 992 CH14: 992 CH15: 992 CH16: 992
I (12345) RCSIGNAL: Trigger channel (CH4): 1811
```
## License
See [LICENSE](LICENSE)

View File

@ -15,35 +15,33 @@
static const char *TAG = "ANIMATION";
#define FRAMES_PER_SECOND 60
#define NUM_LEDS_DEFAULT 44 // TODO: Default from proof-of-concept
static animation_mode_t current_mode = ANIM_BLACK;
static uint8_t global_hue = 0;
static uint32_t frame_counter = 0;
// Beat calculation helper (similar to FastLED beatsin16)
// Beat calculation helper
static int16_t beatsin16(uint8_t bpm, int16_t min_val, int16_t max_val)
{
uint32_t ms = esp_timer_get_time() / 1000;
uint32_t beat = (ms * bpm * 256) / 60000;
uint8_t beat8 = (beat >> 8) & 0xFF;
// Use uint64_t to prevent overflow
uint64_t us = esp_timer_get_time(); // Microseconds
// Sin approximation
float angle = (beat8 / 255.0f) * 2.0f * M_PI;
// Calculate beat phase (0-65535 repeating at BPM rate)
// 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);
// Map sin (-1 to +1) to output range (min_val to max_val)
int16_t range = max_val - min_val;
int16_t result = min_val + (int16_t)((sin_val + 1.0f) * range / 2.0f);
return result;
}
// Beat calculation helper (beatsin8 variant)
static uint8_t beatsin8(uint8_t bpm, uint8_t min_val, uint8_t max_val)
{
return (uint8_t)beatsin16(bpm, min_val, max_val);
}
// Random helper
static uint8_t random8(void)
{
@ -95,20 +93,24 @@ static void anim_white(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_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};
led_set_pixel_a(i, led_hsv_to_rgb(hsv));
}
rgb_t color = led_hsv_to_rgb(hsv);
for (uint16_t i = 0; i < num_leds_b; i++)
{
hsv_t hsv = {(uint8_t)(global_hue + (i * 7)), 255, 255};
led_set_pixel_b(i, led_hsv_to_rgb(hsv));
if (i < num_leds_a)
{
led_set_pixel_a(num_leds_a - i - 1, color);
}
else
{
led_set_pixel_b(i - num_leds_a, color);
}
}
}
@ -134,7 +136,7 @@ static void add_glitter(uint8_t chance_of_glitter)
static void anim_rainbow_glitter(void)
{
anim_rainbow();
add_glitter(80);
add_glitter(255);
}
static void anim_confetti(void)
@ -145,16 +147,16 @@ static void anim_confetti(void)
uint16_t num_leds = led_get_num_leds_a() + led_get_num_leds_b();
uint16_t pos = random16(num_leds);
hsv_t hsv = {(uint8_t)(global_hue + random8()), 200, 255};
hsv_t hsv = {(uint8_t)(global_hue + random8()), 255, 255};
rgb_t color = led_hsv_to_rgb(hsv);
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
{
led_add_pixel_b(pos - led_get_num_leds_a(), color);
led_set_pixel_b(pos - led_get_num_leds_a(), color);
}
}
@ -164,14 +166,14 @@ static void anim_sinelon(void)
led_fade_to_black(20);
uint16_t num_leds = led_get_num_leds_a() + led_get_num_leds_b();
int16_t pos = beatsin16(13, 0, num_leds - 1);
int16_t pos = beatsin16(13, 0, num_leds);
hsv_t hsv = {global_hue, 255, 192};
rgb_t color = led_hsv_to_rgb(hsv);
if (pos < led_get_num_leds_a())
{
led_add_pixel_a(pos, color);
led_add_pixel_a(led_get_num_leds_a() - pos - 1, color);
}
else
{
@ -179,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, 255, 150, 240, 255, 60, 255, 120};
for (uint16_t i = 0; i < num_leds_a; i++)
{
uint8_t color_index = (global_hue + (i * 2)) & 0x0F;
uint8_t brightness = beat - global_hue + (i * 10);
hsv_t hsv = {palette_colors[color_index], 255, brightness};
led_set_pixel_a(i, led_hsv_to_rgb(hsv));
}
for (uint16_t i = 0; i < num_leds_b; i++)
{
uint8_t color_index = (global_hue + ((i + num_leds_a) * 2)) & 0x0F;
uint8_t brightness = beat - global_hue + ((i + num_leds_a) * 10);
hsv_t hsv = {palette_colors[color_index], 255, brightness};
led_set_pixel_b(i, led_hsv_to_rgb(hsv));
}
}
static void anim_navigation(void)
{
// Navigation lights: left red, right green, with blinking white
static uint8_t blink_state = 0;
// Aviation navigation lights with strobe overlay:
// - 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();
uint16_t num_leds_a = led_get_num_leds_a();
@ -223,50 +197,34 @@ static void anim_navigation(void)
rgb_t red = {255, 0, 0};
rgb_t green = {0, 255, 0};
rgb_t white = {255, 255, 255};
rgb_t black = {0, 0, 0};
// 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)
{
led_set_pixel_a(0, red);
led_set_pixel_a(1, red);
led_set_pixel_a(2, red);
rgb_t color_a = strobe_active ? white : black;
led_set_pixel_a(num_leds_a - 1, color_a);
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)
{
led_set_pixel_b(num_leds_b - 1, green);
rgb_t color_b = strobe_active ? white : black;
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 - 3, green);
}
else if (num_leds_a >= 6)
{
led_set_pixel_a(num_leds_a - 1, green);
led_set_pixel_a(num_leds_a - 2, green);
led_set_pixel_a(num_leds_a - 3, green);
}
// Blinking white lights (positions 5-6 and 37-38 from original)
if (blink_state < FRAMES_PER_SECOND / 2)
{
if (num_leds_a > 6)
{
led_set_pixel_a(5, white);
led_set_pixel_a(6, white);
}
if (num_leds_b > 2)
{
led_set_pixel_b(1, white);
led_set_pixel_b(2, white);
}
else if (num_leds_a > 38)
{
led_set_pixel_a(37, white);
led_set_pixel_a(38, white);
}
}
blink_state = (blink_state + 1) % FRAMES_PER_SECOND;
// Strobe cycle: 90 frames = 1.5 second at 60 FPS
strobe_counter = (strobe_counter + 1) % 90;
}
static void anim_chase(void)
@ -274,25 +232,45 @@ static void anim_chase(void)
// Red dot sweeping with trailing dots
led_clear_all();
uint16_t num_leds = led_get_num_leds_a() + led_get_num_leds_b();
int16_t pos = beatsin16(40, 0, num_leds - 1);
uint16_t num_leds_a = led_get_num_leds_a();
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};
// Set main dot and trailing dots
for (int offset = -2; offset <= 2; offset++)
// Draw center dot with dimmed trailing dots (3 dots total: center ±1)
for (int8_t offset = -1; offset <= 1; offset++)
{
int16_t led_pos = pos + offset;
if (led_pos >= 0 && led_pos < num_leds)
int16_t led_pos = center_pos + offset;
// 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())
{
led_set_pixel_a(led_pos, red);
}
else
{
led_set_pixel_b(led_pos - led_get_num_leds_a(), red);
}
// Strip A (mirrored: position 0 maps to last LED)
uint16_t strip_a_index = num_leds_a - led_pos - 1;
led_set_pixel_a(strip_a_index, dimmed_red);
}
else
{
// Strip B (direct mapping)
uint16_t strip_b_index = led_pos - num_leds_a;
led_set_pixel_b(strip_b_index, dimmed_red);
}
}
}
@ -302,26 +280,46 @@ static void anim_chase_rgb(void)
// RGB cycling dot sweeping with trailing dots
led_clear_all();
uint16_t num_leds = led_get_num_leds_a() + led_get_num_leds_b();
int16_t pos = beatsin16(40, 0, num_leds - 1);
uint16_t num_leds_a = led_get_num_leds_a();
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};
rgb_t color = led_hsv_to_rgb(hsv);
// Set main dot and trailing dots
for (int offset = -2; offset <= 2; offset++)
// Draw center dot with dimmed trailing dots (3 dots total: center ±1)
for (int8_t offset = -1; offset <= 1; offset++)
{
int16_t led_pos = pos + offset;
if (led_pos >= 0 && led_pos < num_leds)
int16_t led_pos = center_pos + offset;
// 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())
{
led_add_pixel_a(led_pos, color);
}
else
{
led_add_pixel_b(led_pos - led_get_num_leds_a(), color);
}
// Strip A (mirrored: position 0 maps to last LED)
uint16_t strip_a_index = num_leds_a - led_pos - 1;
led_set_pixel_a(strip_a_index, dimmed_color);
}
else
{
// Strip B (direct mapping)
uint16_t strip_b_index = led_pos - num_leds_a;
led_set_pixel_b(strip_b_index, dimmed_color);
}
}
}
@ -332,18 +330,26 @@ static void anim_random(void)
uint16_t num_leds = led_get_num_leds_a() + led_get_num_leds_b();
uint16_t random_pos = random16(num_leds);
// Randomly clear all (rare event)
if (random_pos == num_leds - 1 && random8() > 200)
{
led_clear_all();
return;
}
// Set random LED to random color
rgb_t random_color = {
random8(),
random8(),
random8()};
0,
0,
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())
{
@ -358,31 +364,26 @@ static void anim_random(void)
esp_err_t animation_init(void)
{
current_mode = ANIM_BLACK;
global_hue = 0;
frame_counter = 0;
global_hue = 0U;
frame_counter = 0U;
ESP_LOGI(TAG, "Animation system initialized");
ESP_LOGI(TAG, "Animation initialized");
return ESP_OK;
}
void animation_set_mode(animation_mode_t mode)
{
if (mode >= ANIM_MODE_COUNT)
if ((mode >= ANIM_MODE_COUNT) || (mode < 0U))
{
mode = ANIM_BLACK;
}
current_mode = mode;
frame_counter = 0;
frame_counter = 0U;
ESP_LOGI(TAG, "Animation mode set to: %s", animation_get_mode_name(mode));
}
animation_mode_t animation_get_mode(void)
{
return current_mode;
}
void animation_update(void)
{
// Update global hue every frame (slowly cycles colors)
@ -422,9 +423,6 @@ void animation_update(void)
case ANIM_SINELON:
anim_sinelon();
break;
case ANIM_BPM:
anim_bpm();
break;
case ANIM_NAVIGATION:
anim_navigation();
break;
@ -457,7 +455,6 @@ const char *animation_get_mode_name(animation_mode_t mode)
"Rainbow with Glitter",
"Confetti",
"Sinelon",
"BPM",
"Navigation",
"Chase",
"Chase RGB",

View File

@ -23,11 +23,10 @@ typedef enum {
ANIM_RAINBOW_GLITTER = 6, // Rainbow with glitter
ANIM_CONFETTI = 7, // Random colored speckles
ANIM_SINELON = 8, // Colored dot sweeping (RGB cycling)
ANIM_BPM = 9, // Colored stripes @ 33 BPM
ANIM_NAVIGATION = 10, // Navigation lights (red left, green right)
ANIM_CHASE = 11, // Red dot sweeping
ANIM_CHASE_RGB = 12, // RGB cycling dot sweeping
ANIM_RANDOM = 13, // Random mode
ANIM_NAVIGATION = 9, // Navigation lights (red left, green right)
ANIM_CHASE = 10, // Red dot sweeping
ANIM_CHASE_RGB = 11, // RGB cycling dot sweeping
ANIM_RANDOM = 12, // Random mode
ANIM_MODE_COUNT
} animation_mode_t;
@ -43,12 +42,6 @@ esp_err_t animation_init(void);
*/
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)
*/
@ -59,6 +52,6 @@ void animation_update(void);
* @param mode Animation mode
* @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

View File

@ -11,21 +11,36 @@
#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 CONFIG_MAGIC 0xDEADBEEF
#define HARDCODED_CONFIG
#ifdef HARDCODED_CONFIG
#define HARDCODED_CONFIG_LED_STRIP_A_PIN 12U
#define HARDCODED_CONFIG_LED_STRIP_B_PIN 14U
#define HARDCODED_CONFIG_LED_STRIP_A_COUNT 7U
#define HARDCODED_CONFIG_LED_STRIP_B_COUNT 7U
#define HARDCODED_CONFIG_PWM_PIN 13U
#define HARDCODED_CONFIG_LED_STRIP_A_PIN 2U
#define HARDCODED_CONFIG_LED_STRIP_B_PIN 3U
#define HARDCODED_CONFIG_LED_STRIP_A_COUNT 9U
#define HARDCODED_CONFIG_LED_STRIP_B_COUNT 9U
#define HARDCODED_CONFIG_RC_SIGNAL_PIN 1U
#define HARDCODED_CONFIG_USE_SBUS_MODE true
#define HARDCODED_CONFIG_SBUS_TRIGGER_CHANNEL 3U // Channel 4
#define HARDCODED_CONFIG_SBUS_THRESHOLD_LOW 800U
#define HARDCODED_CONFIG_SBUS_THRESHOLD_HIGH 1100U
#define HARDCODED_CONFIG_SBUS_INVERTED true // Set inverted RX for FrSky receivers (they output inverted SBUS)
#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
@ -34,40 +49,72 @@ static config_t current_config = {
.led_pin_strip_b = -1,
.led_count_strip_a = -1,
.led_count_strip_b = -1,
.pwm_pin = -1,
.magic = CONFIG_MAGIC};
.rc_signal_pin = -1,
.localBtn_pin = -1,
.use_sbus_mode = false,
.sbus_trigger_channel = 3,
.sbus_threshold_low = 800,
.sbus_threshold_high = 1100,
.sbus_inverted = false,
};
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;
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 size = sizeof(config_t);
config_t tmp;
size_t required_size = sizeof(config_t);
err = nvs_get_blob(nvs_handle, "config", &current_config, &required_size);
nvs_close(nvs_handle);
if (err != ESP_OK || current_config.magic != CONFIG_MAGIC)
for (uint8_t i = 0; i < 2U; i++)
{
ESP_LOGW(TAG, "Invalid config in NVS, using defaults");
return ESP_ERR_INVALID_STATE;
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, " PWM Pin: GPIO%d", current_config.pwm_pin);
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, " RC Signal Pin: GPIO%d", current_config.rc_signal_pin);
ESP_LOGI(TAG, " RC Signal Mode: %s", current_config.use_sbus_mode ? "SBUS" : "PWM");
if (current_config.use_sbus_mode)
{
ESP_LOGI(TAG, " SBUS Trigger Channel: %d", current_config.sbus_trigger_channel + 1);
ESP_LOGI(TAG, " SBUS Thresholds: %d / %d", current_config.sbus_threshold_low, current_config.sbus_threshold_high);
}
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(&current_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)
@ -75,7 +122,6 @@ static esp_err_t save_config_to_nvs(void)
return err;
}
current_config.magic = CONFIG_MAGIC;
err = nvs_set_blob(nvs_handle, "config", &current_config, sizeof(config_t));
if (err == ESP_OK)
{
@ -102,8 +148,12 @@ esp_err_t config_reset_config(void)
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.magic = CONFIG_MAGIC;
current_config.rc_signal_pin = -1;
current_config.localBtn_pin = -1;
current_config.use_sbus_mode = false;
current_config.sbus_trigger_channel = 3;
current_config.sbus_threshold_low = 800;
current_config.sbus_threshold_high = 1100;
return save_config_to_nvs();
}
@ -114,40 +164,19 @@ void config_get_config(config_t *const cnf)
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->rc_signal_pin = current_config.rc_signal_pin;
cnf->localBtn_pin = current_config.localBtn_pin;
cnf->use_sbus_mode = current_config.use_sbus_mode;
cnf->sbus_trigger_channel = current_config.sbus_trigger_channel;
cnf->sbus_threshold_low = current_config.sbus_threshold_low;
cnf->sbus_threshold_high = current_config.sbus_threshold_high;
}
esp_err_t config_update_config(const config_t *config)
{
if (!config)
{
return ESP_ERR_INVALID_ARG;
}
// Reinitialize if pins changed
bool pins_changed = (current_config.led_pin_strip_a != config->led_pin_strip_a) ||
(current_config.led_pin_strip_b != config->led_pin_strip_b) ||
(current_config.pwm_pin != config->pwm_pin);
memcpy(&current_config, config, sizeof(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;
}
// Main initialization
esp_err_t config_init(void)
{
esp_err_t ret;
ESP_LOGI(TAG, "Initializing Config Controller...");
ESP_LOGI(TAG, "Initializing Config...");
// Initialize NVS
ret = nvs_flash_init();
@ -155,6 +184,7 @@ esp_err_t config_init(void)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
ESP_ERROR_CHECK(config_reset_config());
}
ESP_ERROR_CHECK(ret);
@ -163,8 +193,13 @@ esp_err_t config_init(void)
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.magic = CONFIG_MAGIC;
current_config.rc_signal_pin = HARDCODED_CONFIG_RC_SIGNAL_PIN;
current_config.localBtn_pin = HARDCODED_CONFIG_LOCALBTN_PIN;
current_config.use_sbus_mode = HARDCODED_CONFIG_USE_SBUS_MODE;
current_config.sbus_trigger_channel = HARDCODED_CONFIG_SBUS_TRIGGER_CHANNEL;
current_config.sbus_threshold_low = HARDCODED_CONFIG_SBUS_THRESHOLD_LOW;
current_config.sbus_threshold_high = HARDCODED_CONFIG_SBUS_THRESHOLD_HIGH;
current_config.sbus_inverted = HARDCODED_CONFIG_SBUS_INVERTED;
save_config_to_nvs();
#endif
@ -172,7 +207,23 @@ esp_err_t config_init(void)
// Load configuration
load_config_from_nvs();
ESP_LOGI(TAG, "Config system initialized successfully");
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);
}

View File

@ -11,6 +11,7 @@
#include <stdint.h>
#include <stdbool.h>
#define CONFIG_HASH_LEN 32 // SHA256
/**
* @brief Configuration structure stored in NVS
*/
@ -20,13 +21,22 @@ typedef struct
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)
uint32_t magic; // Magic number to validate config (0xDEADBEEF) //TODO: use sha256
int8_t rc_signal_pin; // GPIO pin for RC signal input (-1 = not configured)
int8_t localBtn_pin; // GPIO pin for local btn input (-1 = not configured)
// RC Signal mode settings
bool use_sbus_mode; // true = SBUS mode, false = PWM mode
uint8_t sbus_trigger_channel; // SBUS channel for mode trigger (0-15, typically 3 for CH4)
uint16_t sbus_threshold_low; // SBUS low threshold (default 800)
uint16_t sbus_threshold_high; // SBUS high threshold (default 1100)
bool sbus_inverted; // true = SBUS in inverted mode (like FrSky), false = normal mode
uint8_t hash[CONFIG_HASH_LEN]; // SHA256 Hash of config
} config_t;
/**
* @brief Initialize the config system
* Loads configuration from NVS and initializes subsystems
* Loads configuration from NVS
* @return ESP_OK on success
*/
esp_err_t config_init(void);
@ -37,13 +47,6 @@ esp_err_t config_init(void);
*/
void config_get_config(config_t *const cnf);
/**
* @brief Update configuration and save to NVS
* @param config New configuration
* @return ESP_OK on success
*/
esp_err_t config_update_config(const config_t *config);
/**
* @brief Reset configuration to defaults
* @return ESP_OK on success

View File

@ -13,28 +13,16 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "driver/gpio.h"
static const char *TAG = "CONTROL";
static uint8_t current_animation_mode = 0;
// Forward declarations
static void on_mode_change(uint8_t new_mode);
// Animation mode change callback
static void on_mode_change(uint8_t new_mode)
static void on_mode_change()
{
current_animation_mode = new_mode;
animation_set_mode((animation_mode_t)new_mode);
}
void control_set_animation_mode(uint8_t mode)
{
if (mode >= ANIM_MODE_COUNT)
{
mode = 0;
}
on_mode_change(mode);
current_animation_mode = (current_animation_mode + 1) % ANIM_MODE_COUNT;
animation_set_mode((animation_mode_t)current_animation_mode);
}
uint8_t control_get_animation_mode(void)
@ -42,11 +30,6 @@ uint8_t control_get_animation_mode(void)
return current_animation_mode;
}
void control_emulate_pwm_pulse(void)
{
rcsignal_trigger_mode_change();
}
// Main initialization
esp_err_t control_init(void)
{
@ -67,7 +50,7 @@ esp_err_t control_init(void)
// Initialize LED strips
ret = led_init(current_config.led_pin_strip_a, current_config.led_pin_strip_b,
current_config.led_count_strip_a, current_config.led_count_strip_a);
current_config.led_count_strip_a, current_config.led_count_strip_b);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "LED init failed: %s", esp_err_to_name(ret));
@ -82,8 +65,11 @@ esp_err_t control_init(void)
return ret;
}
// Start ISR service
ESP_ERROR_CHECK(gpio_install_isr_service(0));
// Initialize RC signal
ret = rcsignal_init(current_config.pwm_pin);
ret = rcsignal_init(&current_config);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "RC signal init failed: %s", esp_err_to_name(ret));
@ -91,7 +77,7 @@ esp_err_t control_init(void)
}
// Initialize local BTN
ret = localbtn_init();
ret = localbtn_init(current_config.localBtn_pin);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "Local BTN init failed: %s", esp_err_to_name(ret));

View File

@ -18,12 +18,6 @@
*/
esp_err_t control_init(void);
/**
* @brief Set animation mode manually
* @param mode Animation mode (0-13)
*/
void control_set_animation_mode(uint8_t mode);
/**
* @brief Get current animation mode
* @return Current mode (0-13)

View File

@ -194,7 +194,13 @@ 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 = {
.clk_src = RMT_CLK_SRC_DEFAULT,
.gpio_num = pin,
#if defined(CONFIG_IDF_TARGET_ESP32C3)
.mem_block_symbols = 48,
#elif defined(CONFIG_IDF_TARGET_ESP32)
.mem_block_symbols = 64,
#else
#error "Unsupported target: rmt block symbols undefined"
#endif
.resolution_hz = 80000000, // 80MHz
.trans_queue_depth = 4,
};
@ -321,7 +327,10 @@ static void show_strip(led_strip_t *strip)
// Convert RGB to GRB for WS2812B
uint8_t *grb_data = malloc(strip->num_leds * 3);
if (!grb_data)
{
ESP_LOGE(TAG, "Failed to allocate GRB buffer");
return;
}
for (uint16_t i = 0; i < strip->num_leds; i++)
{
@ -334,7 +343,21 @@ static void show_strip(led_strip_t *strip)
.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);
}

View File

@ -1,6 +1,6 @@
/**
* @file localbtn.c
* @brief Local GPIO0 BTN reading implementation using edge capture
* @brief Local GPIO button reading using interrupt-based edge detection
*/
#include "localbtn.h"
@ -10,78 +10,199 @@
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "soc/gpio_num.h"
#include "freertos/queue.h"
#include <string.h>
static const char *TAG = "LOCALBTN";
uint8_t current_mode;
localbtn_mode_change_callback_t callback;
#define BOOT_BTN GPIO_NUM_0
#define MAX_MODES 14 // TODO: get from control
#define DEBOUNCE_TIME_MS 50 // Debounce time in milliseconds
bool boot_button_pressed(void)
// Button state
static struct
{
return gpio_get_level(BOOT_BTN) == 0; // active LOW
}
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};
static void monitor_task(void *arg)
/**
* @brief GPIO interrupt handler (ISR)
* Minimal work in ISR - just send event to task
*/
static void IRAM_ATTR gpio_isr_handler(void *arg)
{
bool lastState = false;
while (1)
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)
{
vTaskDelay(pdMS_TO_TICKS(10));
bool currentState = boot_button_pressed();
if ((currentState) && (lastState != currentState))
{
printf("BOOT button pressed\n");
current_mode = (current_mode + 1) % MAX_MODES;
ESP_LOGI(TAG, "Mode changed to: %d ", current_mode);
if (callback)
{
callback(current_mode);
}
}
lastState = currentState;
portYIELD_FROM_ISR();
}
}
esp_err_t localbtn_init()
/**
* @brief Button handling task
* Handles debouncing and callback execution
*/
static void localbtn_task(void *arg)
{
int64_t event_time;
gpio_config_t io_conf = {
.pin_bit_mask = 1ULL << BOOT_BTN,
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE, // safe even if external pull-up exists
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE};
ESP_ERROR_CHECK(gpio_config(&io_conf));
ESP_LOGI(TAG, "Button task started, monitoring GPIO%d", button_state.gpio_pin);
// Create monitor task
BaseType_t ret = xTaskCreate(monitor_task, "localbtn_monitor", 2048, NULL, 5, NULL);
if (ret != pdPASS)
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;
}
// TODO: rcsignal.initialized = true;
ESP_LOGI(TAG, "local btn initialized on GPIO%d", BOOT_BTN);
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)
{
// TODO:
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)
{
callback = cb;
}
button_state.callback = cb;
ESP_LOGI(TAG, "Callback registered");
}

View File

@ -1,6 +1,6 @@
/**
* @file localbtn.h
* @brief Local GPIO0 BTN reading implementation using edge capture
* @brief Local GPIO button reading using interrupt-based edge detection
*/
#ifndef LOCALBTN_H
@ -13,24 +13,24 @@
/**
* @brief Callback function type for mode changes
* @param new_mode New animation mode (0-13)
*/
typedef void (*localbtn_mode_change_callback_t)(uint8_t new_mode);
typedef void (*localbtn_mode_change_callback_t)();
/**
* @brief Initialize local btn reading
* @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();
esp_err_t localbtn_init(int8_t pin_localbtn);
/**
* @brief Deinitialize local btn reading
* @brief Deinitialize local button reading
*/
void localbtn_deinit(void);
/**
* @brief Register callback for mode changes
* @param callback Callback function
* @param cb Callback function
*/
void localbtn_register_callback(localbtn_mode_change_callback_t cb);

View File

@ -76,6 +76,8 @@ void app_main(void)
}
}
animation_set_mode((animation_mode_t)control_get_animation_mode());
ESP_LOGI(TAG, "System initialized successfully");
// Main loop - just monitor system status
@ -84,6 +86,6 @@ void app_main(void)
vTaskDelay(pdMS_TO_TICKS(5000));
// Periodic status logging
ESP_LOGI(TAG, "Status - Mode: %d", control_get_animation_mode());
ESP_LOGI(TAG, "Animation Mode set to: %s", animation_get_mode_name(control_get_animation_mode()));
}
}

View File

@ -1,11 +1,13 @@
/**
* @file rcsignal.c
* @brief RC PWM signal reading implementation using edge capture
* @brief RC PWM/SBUS signal reading implementation with runtime mode selection
*/
#include "rcsignal.h"
#include "config.h"
#include "driver/gpio.h"
#include "driver/uart.h"
#include "esp_timer.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
@ -15,16 +17,41 @@
static const char *TAG = "RCSIGNAL";
#define MAX_MODES 14 //TODO: Get from config
// SBUS protocol constants
#define SBUS_FRAME_SIZE 25
#define SBUS_HEADER 0x0F
#define SBUS_FOOTER 0x00
#define SBUS_FOOTER2 0x04 // Alternative footer
#define SBUS_BAUDRATE 100000
#define SBUS_CH_MIN 172
#define SBUS_CH_CENTER 992
#define SBUS_CH_MAX 1811
#define UART_NUM UART_NUM_1
#define UART_BUF_SIZE 256
// PWM mode constants
#define PULSE_THRESHOLD_US 1500
#define SIGNAL_TIMEOUT_MS 100
static struct
{
int8_t gpio_pin;
bool use_sbus_mode; // Runtime mode selection
uint8_t sbus_trigger_channel; // SBUS trigger channel
uint16_t sbus_threshold_low; // SBUS low threshold
uint16_t sbus_threshold_high; // SBUS high threshold
// SBUS state
volatile uint16_t channels[SBUS_NUM_CHANNELS];
volatile int64_t last_frame_time;
uint8_t rx_buffer[SBUS_FRAME_SIZE];
// PWM state
volatile uint32_t pulse_width_us;
volatile int64_t last_edge_time;
volatile int64_t pulse_start_time;
volatile bool last_level;
// Common state
volatile bool signal_active;
volatile bool pull_detected;
uint8_t current_mode;
@ -33,6 +60,13 @@ static struct
TaskHandle_t monitor_task;
} rcsignal = {
.gpio_pin = -1,
.use_sbus_mode = false,
.sbus_trigger_channel = 3,
.sbus_threshold_low = 800,
.sbus_threshold_high = 1100,
.channels = {0},
.last_frame_time = 0,
.rx_buffer = {0},
.pulse_width_us = 0,
.last_edge_time = 0,
.pulse_start_time = 0,
@ -45,6 +79,7 @@ static struct
.monitor_task = NULL,
};
// PWM Mode: GPIO ISR handler
static void IRAM_ATTR gpio_isr_handler(void *arg)
{
int64_t now = esp_timer_get_time();
@ -69,85 +104,214 @@ static void IRAM_ATTR gpio_isr_handler(void *arg)
rcsignal.last_level = level;
}
// SBUS Mode: Parse SBUS frame
static bool parse_sbus_frame(const uint8_t *frame, uint16_t *channels)
{
// Check header and footer
if (frame[0] != SBUS_HEADER || (frame[24] != SBUS_FOOTER && frame[24] != SBUS_FOOTER2))
{
return false;
}
// SBUS uses 11 bits per channel, packed into bytes
channels[0] = ((frame[1] | frame[2] << 8) & 0x07FF);
channels[1] = ((frame[2] >> 3 | frame[3] << 5) & 0x07FF);
channels[2] = ((frame[3] >> 6 | frame[4] << 2 | frame[5] << 10) & 0x07FF);
channels[3] = ((frame[5] >> 1 | frame[6] << 7) & 0x07FF);
channels[4] = ((frame[6] >> 4 | frame[7] << 4) & 0x07FF);
channels[5] = ((frame[7] >> 7 | frame[8] << 1 | frame[9] << 9) & 0x07FF);
channels[6] = ((frame[9] >> 2 | frame[10] << 6) & 0x07FF);
channels[7] = ((frame[10] >> 5 | frame[11] << 3) & 0x07FF);
channels[8] = ((frame[12] | frame[13] << 8) & 0x07FF);
channels[9] = ((frame[13] >> 3 | frame[14] << 5) & 0x07FF);
channels[10] = ((frame[14] >> 6 | frame[15] << 2 | frame[16] << 10) & 0x07FF);
channels[11] = ((frame[16] >> 1 | frame[17] << 7) & 0x07FF);
channels[12] = ((frame[17] >> 4 | frame[18] << 4) & 0x07FF);
channels[13] = ((frame[18] >> 7 | frame[19] << 1 | frame[20] << 9) & 0x07FF);
channels[14] = ((frame[20] >> 2 | frame[21] << 6) & 0x07FF);
channels[15] = ((frame[21] >> 5 | frame[22] << 3) & 0x07FF);
return true;
}
static void monitor_task(void *arg)
{
uint32_t last_pulse_width = 0;
while (1)
if (rcsignal.use_sbus_mode)
{
vTaskDelay(pdMS_TO_TICKS(10));
// Check for signal timeout
int64_t now = esp_timer_get_time();
if (rcsignal.signal_active && (now - rcsignal.last_edge_time) > (SIGNAL_TIMEOUT_MS * 1000))
// SBUS mode
while (1)
{
rcsignal.signal_active = false;
rcsignal.pulse_width_us = 0;
}
// Read SBUS data from UART
int len = uart_read_bytes(UART_NUM, rcsignal.rx_buffer, SBUS_FRAME_SIZE, pdMS_TO_TICKS(20));
// Detect mode change (rising edge on PWM signal > 1500us)
if (rcsignal.pulse_width_us != last_pulse_width)
{
last_pulse_width = rcsignal.pulse_width_us;
if (rcsignal.pulse_width_us < PULSE_THRESHOLD_US)
if (len == SBUS_FRAME_SIZE)
{
rcsignal.pull_detected = true;
uint16_t temp_channels[SBUS_NUM_CHANNELS];
if (parse_sbus_frame(rcsignal.rx_buffer, temp_channels))
{
// Copy parsed channels
for (int i = 0; i < SBUS_NUM_CHANNELS; i++)
{
rcsignal.channels[i] = temp_channels[i];
}
rcsignal.last_frame_time = esp_timer_get_time();
rcsignal.signal_active = true;
// Check trigger channel for mode change
uint16_t ch_value = rcsignal.channels[rcsignal.sbus_trigger_channel];
// Detect pull low
if (ch_value < rcsignal.sbus_threshold_low)
{
rcsignal.pull_detected = true;
}
// Detect rising edge (pull high after low)
if (ch_value > rcsignal.sbus_threshold_high && rcsignal.pull_detected)
{
rcsignal.pull_detected = false;
if (rcsignal.callback)
{
rcsignal.callback();
}
}
}
}
if (rcsignal.pulse_width_us > PULSE_THRESHOLD_US && rcsignal.pull_detected)
// Check for signal timeout
int64_t now = esp_timer_get_time();
if (rcsignal.signal_active && (now - rcsignal.last_frame_time) > (SIGNAL_TIMEOUT_MS * 1000))
{
// Mode change detected
rcsignal.pull_detected = false;
rcsignal.current_mode = (rcsignal.current_mode + 1) % MAX_MODES;
rcsignal.signal_active = false;
memset((void *)rcsignal.channels, 0, sizeof(rcsignal.channels));
}
ESP_LOGI(TAG, "Mode changed to: %d (pulse: %lu us)",
rcsignal.current_mode, rcsignal.pulse_width_us);
vTaskDelay(pdMS_TO_TICKS(5));
}
}
else
{
// PWM mode
uint32_t last_pulse_width = 0;
if (rcsignal.callback)
while (1)
{
vTaskDelay(pdMS_TO_TICKS(10));
// Check for signal timeout
int64_t now = esp_timer_get_time();
if (rcsignal.signal_active && (now - rcsignal.last_edge_time) > (SIGNAL_TIMEOUT_MS * 1000))
{
rcsignal.signal_active = false;
rcsignal.pulse_width_us = 0;
}
// Detect mode change (rising edge on PWM signal > 1500us)
if (rcsignal.pulse_width_us != last_pulse_width)
{
last_pulse_width = rcsignal.pulse_width_us;
if (rcsignal.pulse_width_us < PULSE_THRESHOLD_US)
{
rcsignal.callback(rcsignal.current_mode);
rcsignal.pull_detected = true;
}
if (rcsignal.pulse_width_us > PULSE_THRESHOLD_US && rcsignal.pull_detected)
{
// Mode change detected
rcsignal.pull_detected = false;
if (rcsignal.callback)
{
rcsignal.callback();
}
}
}
}
}
}
esp_err_t rcsignal_init(int8_t pin)
esp_err_t rcsignal_init(const config_t *config)
{
if (pin < 0)
if (!config || config->rc_signal_pin < 0)
{
ESP_LOGI(TAG, "RC signal disabled (no pin configured)");
return ESP_OK;
}
rcsignal.gpio_pin = pin;
// Store configuration
rcsignal.gpio_pin = config->rc_signal_pin;
rcsignal.use_sbus_mode = config->use_sbus_mode;
rcsignal.sbus_trigger_channel = config->sbus_trigger_channel;
rcsignal.sbus_threshold_low = config->sbus_threshold_low;
rcsignal.sbus_threshold_high = config->sbus_threshold_high;
// Configure GPIO
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << pin),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_ANYEDGE,
};
ESP_ERROR_CHECK(gpio_config(&io_conf));
if (rcsignal.use_sbus_mode)
{
// SBUS Mode: Configure UART with inverted RX
ESP_LOGI(TAG, "Initializing SBUS mode on GPIO%d", rcsignal.gpio_pin);
ESP_LOGI(TAG, " Trigger channel: CH%d", rcsignal.sbus_trigger_channel + 1);
ESP_LOGI(TAG, " Thresholds: %d / %d", rcsignal.sbus_threshold_low, rcsignal.sbus_threshold_high);
// Install ISR service
ESP_ERROR_CHECK(gpio_install_isr_service(0));
ESP_ERROR_CHECK(gpio_isr_handler_add(pin, gpio_isr_handler, NULL));
uart_config_t uart_config = {
.baud_rate = SBUS_BAUDRATE,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_EVEN,
.stop_bits = UART_STOP_BITS_2,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_APB,
};
ESP_ERROR_CHECK(uart_param_config(UART_NUM, &uart_config));
ESP_ERROR_CHECK(uart_set_pin(UART_NUM, UART_PIN_NO_CHANGE, rcsignal.gpio_pin, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
ESP_ERROR_CHECK(uart_driver_install(UART_NUM, UART_BUF_SIZE * 2, 0, 0, NULL, 0));
if (config->sbus_inverted)
{
// Set inverted RX for FrSky receivers (they output inverted SBUS)
ESP_ERROR_CHECK(uart_set_line_inverse(UART_NUM, UART_SIGNAL_RXD_INV));
}
ESP_LOGI(TAG, "SBUS UART configured with inverted RX");
}
else
{
// PWM Mode: Configure GPIO with interrupts
ESP_LOGI(TAG, "Initializing PWM mode on GPIO%d", rcsignal.gpio_pin);
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << rcsignal.gpio_pin),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_ANYEDGE,
};
ESP_ERROR_CHECK(gpio_config(&io_conf));
// Add ISR handler (ISR service must be installed by caller)
ESP_ERROR_CHECK(gpio_isr_handler_add(rcsignal.gpio_pin, gpio_isr_handler, NULL));
}
// Create monitor task
BaseType_t ret = xTaskCreate(monitor_task, "rcsignal_monitor", 2048, NULL, 5, &rcsignal.monitor_task);
if (ret != pdPASS)
{
gpio_isr_handler_remove(pin);
gpio_uninstall_isr_service();
if (rcsignal.use_sbus_mode)
{
uart_driver_delete(UART_NUM);
}
else
{
gpio_isr_handler_remove(rcsignal.gpio_pin);
}
return ESP_FAIL;
}
rcsignal.initialized = true;
ESP_LOGI(TAG, "RC signal initialized on GPIO%d", pin);
ESP_LOGI(TAG, "RC signal initialized successfully");
return ESP_OK;
}
@ -163,9 +327,16 @@ void rcsignal_deinit(void)
rcsignal.monitor_task = NULL;
}
if (rcsignal.gpio_pin >= 0)
if (rcsignal.use_sbus_mode)
{
gpio_isr_handler_remove(rcsignal.gpio_pin);
uart_driver_delete(UART_NUM);
}
else
{
if (rcsignal.gpio_pin >= 0)
{
gpio_isr_handler_remove(rcsignal.gpio_pin);
}
}
rcsignal.initialized = false;
@ -178,7 +349,21 @@ void rcsignal_register_callback(rcsignal_mode_change_callback_t callback)
uint32_t rcsignal_get_pulse_width(void)
{
return rcsignal.pulse_width_us;
if (rcsignal.use_sbus_mode)
{
// In SBUS mode, return trigger channel value mapped to microseconds
// SBUS: 172-1811 -> PWM: ~1000-2000us
if (rcsignal.signal_active)
{
uint16_t ch_val = rcsignal.channels[rcsignal.sbus_trigger_channel];
return 1000 + ((ch_val - SBUS_CH_MIN) * 1000) / (SBUS_CH_MAX - SBUS_CH_MIN);
}
return 0;
}
else
{
return rcsignal.pulse_width_us;
}
}
bool rcsignal_is_active(void)
@ -186,18 +371,49 @@ bool rcsignal_is_active(void)
return rcsignal.signal_active;
}
void rcsignal_trigger_mode_change(void)
{
rcsignal.current_mode = (rcsignal.current_mode + 1) % MAX_MODES;
ESP_LOGI(TAG, "Manual mode change to: %d", rcsignal.current_mode);
if (rcsignal.callback)
{
rcsignal.callback(rcsignal.current_mode);
}
}
uint8_t rcsignal_get_current_mode(void)
{
return rcsignal.current_mode;
}
uint16_t rcsignal_get_sbus_channel(uint8_t channel)
{
if (!rcsignal.use_sbus_mode || channel >= SBUS_NUM_CHANNELS)
{
return 0;
}
return rcsignal.channels[channel];
}
void rcsignal_debug_print_channels(void)
{
if (!rcsignal.use_sbus_mode)
{
ESP_LOGW(TAG, "Not in SBUS mode");
return;
}
if (!rcsignal.signal_active)
{
ESP_LOGW(TAG, "No SBUS signal active");
return;
}
ESP_LOGI(TAG, "SBUS Channels:");
ESP_LOGI(TAG, " CH1: %4d CH2: %4d CH3: %4d CH4: %4d",
rcsignal.channels[0], rcsignal.channels[1],
rcsignal.channels[2], rcsignal.channels[3]);
ESP_LOGI(TAG, " CH5: %4d CH6: %4d CH7: %4d CH8: %4d",
rcsignal.channels[4], rcsignal.channels[5],
rcsignal.channels[6], rcsignal.channels[7]);
ESP_LOGI(TAG, " CH9: %4d CH10: %4d CH11: %4d CH12: %4d",
rcsignal.channels[8], rcsignal.channels[9],
rcsignal.channels[10], rcsignal.channels[11]);
ESP_LOGI(TAG, " CH13: %4d CH14: %4d CH15: %4d CH16: %4d",
rcsignal.channels[12], rcsignal.channels[13],
rcsignal.channels[14], rcsignal.channels[15]);
// Highlight the trigger channel
ESP_LOGI(TAG, "Trigger channel (CH%d): %d", rcsignal.sbus_trigger_channel + 1,
rcsignal.channels[rcsignal.sbus_trigger_channel]);
}

View File

@ -1,28 +1,31 @@
/**
* @file rcsignal.h
* @brief RC PWM signal reading and parsing module
* @brief RC PWM/SBUS signal reading and parsing module
*/
#ifndef RCSIGNAL_H
#define RCSIGNAL_H
#include "esp_err.h"
#include "config.h"
#include <stdint.h>
#include <stdbool.h>
// SBUS protocol constants (always defined for use in both modes)
#define SBUS_NUM_CHANNELS 16 // SBUS supports 16 proportional channels
/**
* @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
* @param pin GPIO pin for PWM input (-1 to disable)
* @param config Pointer to configuration structure
* @return ESP_OK on success
*/
esp_err_t rcsignal_init(int8_t pin);
esp_err_t rcsignal_init(const config_t *config);
/**
* @brief Deinitialize RC signal reading
@ -47,15 +50,22 @@ uint32_t rcsignal_get_pulse_width(void);
*/
bool rcsignal_is_active(void);
/**
* @brief Manually trigger mode change (for emulation)
*/
void rcsignal_trigger_mode_change(void);
/**
* @brief Get current mode
* @return Current animation mode (0-13)
*/
uint8_t rcsignal_get_current_mode(void);
/**
* @brief Get SBUS channel value (only valid in SBUS mode)
* @param channel Channel index (0-15)
* @return Channel value (172-1811) or 0 if invalid/not in SBUS mode
*/
uint16_t rcsignal_get_sbus_channel(uint8_t channel);
/**
* @brief Debug function to print all SBUS channels (only valid in SBUS mode)
*/
void rcsignal_debug_print_channels(void);
#endif // RCSIGNAL_H

1011
sdkconfig

File diff suppressed because it is too large Load Diff