diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 54fa613..c7079c2 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -6,6 +6,9 @@ idf_component_register( "ota.cpp" "mqtt.cpp" "device.cpp" + "utils.cpp" + "leds.cpp" + "presets.cpp" INCLUDE_DIRS "." ) diff --git a/main/device.cpp b/main/device.cpp index 03d4893..cdc7c8a 100644 --- a/main/device.cpp +++ b/main/device.cpp @@ -14,7 +14,7 @@ Device::Device(std::string _id) : power_on(true), client(start_mqtt_client(on_mqtt_connect, on_mqtt_message, this)) {} - + void Device::set_mqtt_config(cJSON*) { xSemaphoreTake(mutex, portMAX_DELAY); diff --git a/main/device.hpp b/main/device.hpp index 38b0641..117b342 100644 --- a/main/device.hpp +++ b/main/device.hpp @@ -10,7 +10,7 @@ class Device { public: Device(std::string id); - + void set_mqtt_config(cJSON*); void lock() const { xSemaphoreTake(mutex, portMAX_DELAY); } diff --git a/main/leds.cpp b/main/leds.cpp new file mode 100644 index 0000000..4c429c2 --- /dev/null +++ b/main/leds.cpp @@ -0,0 +1,76 @@ +static const char *TAG = "leds"; +#include + +#include "leds.hpp" +#include "utils.hpp" + + +static inline uint8_t mix(int l, int r, Fixed x) { + return ((l << shift) + (x * (r - l))) >> shift; +} + +Color Gradient::at(Fixed x) const { + int i_left = (x * n_colors) >> shift; + int i_right = i_left < n_colors - 1 ? i_left + 1 : 0; + Fixed ratio = (x * n_colors) - (i_left << shift); + //ESP_LOGI(TAG, "at(%d) = %d between %d and %d", x, ratio, i_left, i_right); + Color cl = colors[i_left]; + Color cr = colors[i_right]; + Color color = { + mix(cl.r, cr.r, ratio), + mix(cl.g, cr.g, ratio), + mix(cl.b, cr.b, ratio) + }; + /*ESP_LOGI(TAG, "(%d, %d, %d) %f (%d, %d, %d) -> (%d, %d, %d)", + cl.r, cl.g, cl.b, + ratio / (float)factor, + cr.r, cr.g, cr.b, + color.r, color.g, color.b + );*/ + return color; +} + + +void Pattern::step(Color pixels[], int len, int64_t &last_us, Fixed &offset) const { + int64_t now_us = time_us(); + int64_t duration_us = last_us == 0 ? 0 : now_us - last_us; + last_us = now_us; + //ESP_LOGI(TAG, "duration %d", duration_us); + + int offset_delta = (duration_us << shift) / (cycle_time_ms * 1000); + if (reverse) offset -= offset_delta; else offset += offset_delta; + Fixed off = march ? + (((int)offset * cycle_length) & ~((1 << shift) - 1)) / cycle_length : + offset; + //ESP_LOGI(TAG, "cycle time %d, delta %d, offset %d, off %d", cycle_time_ms, offset_delta, offset, off); + for (int i = 0; i < len; ++i) { + pixels[i] = gradient.at(off + ((i << shift) / cycle_length)); + } +} + + +LEDStrip::LEDStrip(int _length) : + length(_length), pattern(NULL), + pixels(new Color[_length]), + last_us(0), offset(0) + {} + +void LEDStrip::step() { + if (pattern) pattern->step(pixels, length, last_us, offset); +} + +static std::string termcolor(Color c) { + return "\x1b[48;2;" + + std::to_string((int)c.r) + ";" + + std::to_string((int)c.g) + ";" + + std::to_string((int)c.b) + "m"; +} + +void TerminalLEDs::show() const { + std::string line; + for (int i = 0; i < length; i++) { + line += termcolor(pixels[i]) + " "; + } + line += "\x1b[0m"; + ESP_LOGI(TAG, "%s", line.c_str()); +} diff --git a/main/leds.hpp b/main/leds.hpp new file mode 100644 index 0000000..acaa16a --- /dev/null +++ b/main/leds.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include +#include + + +struct Color { + uint8_t r, g, b; +}; + +typedef uint16_t Fixed; +constexpr int factor = 65536; +constexpr int shift = 16; + + +struct Gradient { + Color at(Fixed x) const; + + unsigned int n_colors; + Color colors[]; +}; + +struct Pattern { + void step(Color[], int, int64_t &last_us, Fixed &offset) const; + + int cycle_length; + int cycle_time_ms; + bool reverse; + bool march; + Gradient gradient; +}; + +class LEDStrip { +public: + LEDStrip(int length); + + void step(); + + virtual void show() const = 0; + + void setPattern(const Pattern* _pattern) { pattern = _pattern; } + const Pattern* getPattern() const { return pattern; } + + int getLength() const { return length; } + +protected: + const int length; + const Pattern *pattern; + Color *pixels; + +private: + int64_t last_us; + Fixed offset; +}; + +class TerminalLEDs : public LEDStrip { +public: + TerminalLEDs(int length) : LEDStrip(length) {} + void show() const; +}; diff --git a/main/main.cpp b/main/main.cpp index 0b8d291..554886f 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -11,6 +11,10 @@ static const char *TAG = "blinky"; #include "http_serv.hpp" #include "ota.hpp" #include "device.hpp" +#include "utils.hpp" +#include "leds.hpp" +#include "presets.hpp" +#include "utils.hpp" // mDNS / NetBIOS @@ -80,13 +84,6 @@ static void log_dir(const std::string &path) { } -int64_t now_us() { - struct timeval tv_now; - gettimeofday(&tv_now, NULL); - return (int64_t)tv_now.tv_sec * 1000000L + (int64_t)tv_now.tv_usec; -} - - // Entry Point extern "C" void app_main(void) { @@ -112,26 +109,63 @@ extern "C" void app_main(void) { // Dummy Filesystem start_filesystem(); - log_dir("/spiffs"); + //log_dir("/spiffs"); - // TODO: Something useful Device device("blinky-jr"); -/* - int period_us = 1000000; - int64_t target_us = now_us(); + // TODO: Load config from SPIFFS + + auto LEDs = TerminalLEDs(20); + /*** *** ***/ + LEDs.setPattern(Presets::find("rainbow")); + /*** *** ***/ + while (true) { - int64_t delay_us = target_us - now_us(); - if (delay_us < 0) delay_us = 0; - if (device.wait(delay_us / 1000)) - ESP_LOGI(TAG, "Reload!"); - else { - ESP_LOGI(TAG, "Timeout!"); - target_us += period_us; + ESP_LOGI(TAG, "Configuring LEDs"); + + device.lock(); + + const Pattern *pattern = Presets::find(device.get_effect()); + if (pattern) { + LEDs.setPattern(pattern); + } else { + ESP_LOGW(TAG, "Could not find pattern '%s'", device.get_effect()); + pattern = LEDs.getPattern(); + } + + // TODO: Save Config to SPIFFS + + bool is_on = device.is_on(); + + device.unlock(); + + if (!pattern || !is_on) { + if (!is_on) ESP_LOGI(TAG, "Device off"); + if (!pattern) ESP_LOGW(TAG, "No LED pattern set"); + ESP_LOGI(TAG, "Waiting for new config"); + + device.wait(); + + } else { + ESP_LOGI(TAG, "Starting animation"); + + int period_us = 1000000; + int64_t target_us = time_us(); + while (true) { + int64_t delay_us = target_us - time_us(); + if (delay_us < 0) delay_us = 0; + if (device.wait(delay_us / 1000)) { + // If the semaphore is set, go back to top of outer loop + break; + } + + LEDs.step(); + LEDs.show(); + + target_us += period_us; + } } } -*/ - while (device.wait()) ESP_LOGI(TAG, "Reload!"); // Spin } diff --git a/main/presets.cpp b/main/presets.cpp new file mode 100644 index 0000000..533edf7 --- /dev/null +++ b/main/presets.cpp @@ -0,0 +1,61 @@ +#include + +#include "presets.hpp" + + +namespace Gradients { + +#define GRADIENT(name, len, ...) \ + constexpr Gradient name = { \ + .n_colors = len, \ + .colors = __VA_ARGS__, \ + } + +GRADIENT(rainbow, 6, { + Colors::red, + Colors::orange, + Colors::yellow, + Colors::green, + Colors::blue, + Colors::purple, +}); + +GRADIENT(peacock, 4, { + Colors::teal, + Colors::black, + Colors::purple, + Colors::blue, +}); + +} // Gradients + + +namespace Patterns { + +#define PATTERN(x) \ + static const Pattern x = { \ + .cycle_length = 20, \ + .cycle_time_ms = 10000, \ + .reverse = false, \ + .march = false, \ + .gradient = Gradients::x, \ + } + +PATTERN(rainbow); +PATTERN(peacock); + +} // Patterns + + +#define PRESET(x) {#x, &Patterns::x} + +static const std::map presets = { + PRESET(rainbow), + PRESET(peacock), +}; + +const Pattern* Presets::find(const std::string &name) { + auto e = presets.find(name); + return e == presets.end() ? NULL : e->second; +} + diff --git a/main/presets.hpp b/main/presets.hpp new file mode 100644 index 0000000..450902a --- /dev/null +++ b/main/presets.hpp @@ -0,0 +1,47 @@ +#pragma once +#include + +#include "leds.hpp" + + +namespace Presets { + +const struct Pattern* find(const std::string&); + +} // Presets + + +namespace Colors { + +constexpr Color red = {255, 0, 0}; +constexpr Color orange = {78, 25, 0}; +constexpr Color yellow = {255, 160, 0}; +constexpr Color green = {0, 255, 0}; +constexpr Color blue = {0, 0, 255}; +constexpr Color purple = {38, 10, 42}; + +constexpr Color dark_red = {32, 0, 0}; +constexpr Color dark_orange = {20, 6, 0}; +constexpr Color dark_yellow = {64, 40, 0}; +constexpr Color dark_green = {0, 32, 0}; +constexpr Color dark_blue = {0, 0, 32}; +constexpr Color dark_purple = {8, 3, 10}; + +constexpr Color pale_red = {255, 64, 64}; +constexpr Color pale_orange = {78, 25, 10}; +constexpr Color pale_yellow = {255, 160, 10}; +constexpr Color pale_green = {64, 255, 25}; +constexpr Color pale_blue = {70, 70, 255}; +constexpr Color pale_purple = {42, 20, 42}; + +constexpr Color black = {0, 0, 0}; +constexpr Color white = {255, 255, 255}; + +constexpr Color pink = pale_red; +constexpr Color peach = pale_orange; +// TODO: chartreuse +constexpr Color brown = {6, 2, 0}; +constexpr Color teal = {0, 85, 35}; +constexpr Color magenta = {255, 0, 255}; + +} // Colors diff --git a/main/utils.cpp b/main/utils.cpp new file mode 100644 index 0000000..d164c0c --- /dev/null +++ b/main/utils.cpp @@ -0,0 +1,10 @@ +#include + +#include "utils.hpp" + + +int64_t time_us() { + struct timeval tv_now; + gettimeofday(&tv_now, NULL); + return (int64_t)tv_now.tv_sec * 1000000L + (int64_t)tv_now.tv_usec; +} diff --git a/main/utils.hpp b/main/utils.hpp new file mode 100644 index 0000000..8ca0720 --- /dev/null +++ b/main/utils.hpp @@ -0,0 +1,6 @@ +#pragma once + +#include + + +int64_t time_us();