From 2fec06b9f80e1ab8c47b8be3d7215c2c6db0b78e Mon Sep 17 00:00:00 2001 From: jrhoffa Date: Sat, 17 Sep 2022 10:37:53 -0700 Subject: [PATCH] Refactor presets to use JSON definitions - New definitions can be delivered via MQTT - Includes extensible pattern class for new preset types --- main/colors.json | 32 +++++++ main/device.cpp | 34 +++++-- main/device.hpp | 1 + main/leds.cpp | 26 ++++-- main/leds.hpp | 55 +++++++++-- main/main.cpp | 50 +--------- main/mqtt.cpp | 1 + main/presets.cpp | 226 +++++++++++++++++++++++++--------------------- main/presets.hpp | 43 +-------- main/presets.json | 17 ++++ main/utils.cpp | 44 +++++++++ main/utils.hpp | 6 ++ 12 files changed, 327 insertions(+), 208 deletions(-) create mode 100644 main/colors.json create mode 100644 main/presets.json diff --git a/main/colors.json b/main/colors.json new file mode 100644 index 0000000..448ad28 --- /dev/null +++ b/main/colors.json @@ -0,0 +1,32 @@ +{ +"black": [ 0, 0, 0], +"white": [255, 255, 255], + +"red": [255, 0, 0], +"orange": [ 78, 25, 0], +"yellow": [255, 160, 0], +"green": [ 0, 255, 0], +"blue": [ 0, 0, 255], +"purple": [ 38, 10, 42], + +"dark_red": [ 32, 0, 0], +"dark_orange": [ 20, 6, 0], +"dark_yellow": [ 64, 40, 0], +"dark_green": [ 0, 32, 0], +"dark_blue": [ 0, 0, 32], +"dark_purple": [ 8, 3, 10], + +"pale_red": [255, 64, 64], +"pale_orange": [ 78, 25, 10], +"pale_yellow": [255, 160, 10], +"pale_green": [ 64, 255, 25], +"pale_blue": [ 70, 70, 255], +"pale_purple": [ 42, 20, 42], + +"pink": "pale_red", +"peach": "pale_orange", +"brown": [ 6, 2, 0], +"chartreuse": [ 35, 45, 5], +"teal": [ 0, 85, 35], +"magenta": [255, 0, 255] +} diff --git a/main/device.cpp b/main/device.cpp index 2c0e6d6..f58ee42 100644 --- a/main/device.cpp +++ b/main/device.cpp @@ -3,12 +3,14 @@ static const char *TAG = "device"; #include "device.hpp" #include "presets.hpp" +#include "utils.hpp" Device::Device(std::string _id) : id(_id), state_topic("light/" + id + "/state"), cmd_topic("light/" + id + "/cmd"), + data_topic("light/" + id + "/data/+"), ready(false), should_publish(false), mutex(xSemaphoreCreateMutex()), sem(xSemaphoreCreateBinary()), @@ -45,7 +47,11 @@ void Device::on_mqtt_connect(esp_mqtt_client_handle_t client) { if (esp_mqtt_client_subscribe(client, cmd_topic.c_str(), 0) < 0) { ESP_LOGE(TAG, "Failed to subscribe to %s", cmd_topic.c_str()); } + if (esp_mqtt_client_subscribe(client, data_topic.c_str(), 0) < 0) { + ESP_LOGE(TAG, "Failed to subscribe to %s", data_topic.c_str()); + } + // TODO: Re-announce when presets change cJSON *json = cJSON_CreateObject(); cJSON_AddStringToObject(json, "unique_id", id.c_str()); cJSON *device = cJSON_AddObjectToObject(json, "device"); @@ -57,8 +63,8 @@ void Device::on_mqtt_connect(esp_mqtt_client_handle_t client) { cJSON_AddStringToObject(json, "schema", "json"); cJSON_AddBoolToObject(json, "effect", true); cJSON *effects = cJSON_AddArrayToObject(json, "effect_list"); - for (const auto &[k, _]: Presets::map) - cJSON_AddItemToArray(effects, cJSON_CreateString(k.c_str())); + for (auto iter = Presets::map_begin(); iter != Presets::map_end(); ++iter) + cJSON_AddItemToArray(effects, cJSON_CreateString(iter->first.c_str())); char *config = cJSON_PrintUnformatted(json); @@ -81,13 +87,25 @@ void Device::on_mqtt_connect(esp_mqtt_client_handle_t client) { } void Device::on_mqtt_message(esp_mqtt_client_handle_t, esp_mqtt_event_handle_t event) { + std::string topic(event->topic, event->topic_len); ESP_LOGI(TAG, "Received command via MQTT"); - cJSON *json = cJSON_ParseWithLength(event->data, event->data_len); - if (json) { - set_json_config(json); - cJSON_Delete(json); - } else { - ESP_LOGE(TAG, "Invalid JSON data"); + if (topic == cmd_topic) { + cJSON *json = cJSON_ParseWithLength(event->data, event->data_len); + if (json) { + set_json_config(json); + cJSON_Delete(json); + } else { + ESP_LOGE(TAG, "Invalid JSON data"); + } + + } else /* data topic */ { + size_t slash = topic.rfind('/'); + if (slash != std::string::npos) { + write_file( + ("/spiffs/" + topic.substr(slash + 1) + ".json").c_str(), + event->data, event->data_len + ); + } } xSemaphoreGive(sem); } diff --git a/main/device.hpp b/main/device.hpp index 6171b05..a40ba95 100644 --- a/main/device.hpp +++ b/main/device.hpp @@ -35,6 +35,7 @@ private: std::string state_topic; std::string cmd_topic; + std::string data_topic; bool ready, should_publish; diff --git a/main/leds.cpp b/main/leds.cpp index 121ee29..ffd9dc9 100644 --- a/main/leds.cpp +++ b/main/leds.cpp @@ -30,11 +30,12 @@ Color Gradient::at(Fixed x) const { return color; } +void GradientPattern::step(Color pixels[], int len, Pattern::State *_state) const { + GradientPattern::State *state = (GradientPattern::State*)_state; -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; + int64_t duration_us = state->last_us == 0 ? 0 : now_us - state->last_us; + state->last_us = now_us; //ESP_LOGI(TAG, "duration %d", duration_us); /* @@ -49,13 +50,13 @@ void Pattern::step(Color pixels[], int len, int64_t &last_us, Fixed &offset) con int offset_delta = (duration_us << shift) / (cycle_time_ms * 1000); - if (reverse) offset += offset_delta; else offset -= offset_delta; + if (reverse) state->offset += offset_delta; else state->offset -= offset_delta; Fixed off = march ? - (((int)offset * cycle_length) & ~((1 << shift) - 1)) / cycle_length : - offset; + (((int)state->offset * cycle_length) & ~((1 << shift) - 1)) / cycle_length : + state->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)); + pixels[i] = gradient->at(off + ((i << shift) / cycle_length)); } } @@ -63,18 +64,25 @@ void Pattern::step(Color pixels[], int len, int64_t &last_us, Fixed &offset) con LEDStrip::LEDStrip(int _length) : length(_length), pattern(NULL), pixels(new Color[_length]), - last_us(0), offset(0) + state(NULL) {} +void LEDStrip::update_state() { + if (state) delete state; + if (pattern) state = pattern->start(length); + else state = NULL; +} + void LEDStrip::setLength(int _length) { length_changing(_length); delete[] pixels; length = _length; pixels = new Color[_length]; + update_state(); } void LEDStrip::step() { - if (pattern) pattern->step(pixels, length, last_us, offset); + if (pattern) pattern->step(pixels, length, state); } static std::string termcolor(Color c) { diff --git a/main/leds.hpp b/main/leds.hpp index 82589f0..e4a220a 100644 --- a/main/leds.hpp +++ b/main/leds.hpp @@ -16,18 +16,60 @@ constexpr int shift = 16; struct Gradient { Color at(Fixed x) const; + static Gradient* make(int n) { + Gradient *gradient = (Gradient*)malloc( + sizeof(Gradient) + n * sizeof(*colors) + ); + gradient->n_colors = n; + return gradient; + } + unsigned int n_colors; Color colors[]; }; -struct Pattern { - void step(Color[], int, int64_t &last_us, Fixed &offset) const; +class Pattern { +public: + virtual ~Pattern() {}; + + struct State {}; + + virtual State* start(int) const = 0; + + virtual void step(Color[], int, State*) const = 0; +}; + +class GradientPattern : public Pattern { +public: + GradientPattern( + Gradient *_gradient, + int _cycle_length=20, int _cycle_time_ms=-1, + bool _reverse=false, bool _march=false + ) : cycle_length(_cycle_length), + cycle_time_ms( _cycle_time_ms < 0 ? + _cycle_length * 500 : _cycle_time_ms + ), + reverse(_reverse), march(_march), gradient(_gradient) + {} + + ~GradientPattern() { free(gradient); } + + State* start(int) const { return new State(); } + + void step(Color[], int, State*) const; + +private: + struct State : Pattern::State { + State() : last_us(0), offset(0) {} + int64_t last_us; + Fixed offset; + }; int cycle_length; int cycle_time_ms; bool reverse; bool march; - Gradient gradient; + Gradient *gradient; }; class LEDStrip { @@ -39,7 +81,7 @@ public: virtual void show() = 0; virtual void length_changing(int) {}; - void setPattern(const Pattern *_pattern) { pattern = _pattern; } + void setPattern(const Pattern *_pattern) { pattern = _pattern; update_state(); } const Pattern* getPattern() const { return pattern; } void setLength(int length); @@ -51,8 +93,9 @@ protected: Color *pixels; private: - int64_t last_us; - Fixed offset; + void update_state(); + + Pattern::State *state; }; class TerminalLEDs : public LEDStrip { diff --git a/main/main.cpp b/main/main.cpp index 924987b..da7ac5a 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -56,7 +56,7 @@ static void start_filesystem() { ESP_ERROR_CHECK(esp_vfs_spiffs_register(&conf)); } -/* + #include #include #include @@ -84,49 +84,6 @@ static void log_dir(const std::string &path) { closedir(dir); } -*/ - -static std::string read_file(const char *path) { - std::string data; - FILE *f = fopen(path, "r"); - if (!f) { - ESP_LOGE(TAG, "Failed to read %s", path); - return data; - } - constexpr size_t szbuf = 4096; - char *buf = (char*)malloc(szbuf); - size_t nread; - while ((nread = fread(buf, 1, szbuf - 1, f)) > 0) { - buf[nread] = '\0'; - data += buf; - } - fclose(f); - free(buf); - return data; -} - -static void write_file(const char *path, const std::string &data) { - FILE *f = fopen(path, "w"); - if (!f) { - ESP_LOGE(TAG, "Failed to open %s", path); - return; - } - const char *ptr = data.c_str(); - size_t remain = data.length(); - size_t nwritten = fwrite(ptr, 1, remain, f); - if (nwritten != remain) { - ESP_LOGE(TAG, "Failed to write to %s: %d/%d", path, nwritten, remain); - } - fclose(f); -} - - -std::string gen_config(bool is_on, const std::string &effect) { - return std::string() + "{ " - "\"state\": \"" + (is_on ? "ON" : "OFF") + "\", " - "\"effect\": \"" + effect + "\"" - " }"; -} constexpr char config_path[] = "/spiffs/blinky.json"; @@ -159,7 +116,7 @@ extern "C" void app_main(void) { // Dummy Filesystem start_filesystem(); // TODO: Scrape this out - //log_dir("/spiffs"); + log_dir("/spiffs"); Device device("blinky-jr"); @@ -173,6 +130,8 @@ extern "C" void app_main(void) { LEDStrip *LEDs = new SPI_LEDs(39); //new TerminalLEDs(); while (true) { + Presets::reload(); + ESP_LOGI(TAG, "Configuring LEDs"); device.lock(); @@ -188,6 +147,7 @@ extern "C" void app_main(void) { const Pattern *pattern = Presets::find(effect); if (pattern) { + ESP_LOGI(TAG, "Setting pattern '%s'", effect.c_str()); LEDs->setPattern(pattern); } else { ESP_LOGW(TAG, "Could not find pattern '%s'", effect.c_str()); diff --git a/main/mqtt.cpp b/main/mqtt.cpp index 369d63a..49788f5 100644 --- a/main/mqtt.cpp +++ b/main/mqtt.cpp @@ -88,6 +88,7 @@ esp_mqtt_client_handle_t start_mqtt_client( .cert_pem = (const char *)broker_pem_start, .username = CONFIG_BROKER_USER, .password = CONFIG_BROKER_PASSWORD, + .buffer_size = 4096, }; esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqtt_cfg); diff --git a/main/presets.cpp b/main/presets.cpp index 3ca744a..6931c83 100644 --- a/main/presets.cpp +++ b/main/presets.cpp @@ -1,118 +1,140 @@ +static const char *TAG = "presets"; +#include + +#include + #include "presets.hpp" +#include "utils.hpp" -namespace Gradients { +namespace Presets { -#define GRADIENT(name, len, ...) \ - constexpr Gradient name = { \ - .n_colors = len, \ - .colors = __VA_ARGS__, \ - } +/* Preset Map */ -GRADIENT(rainbow, 6, { - Colors::red, - Colors::orange, - Colors::yellow, - Colors::green, - Colors::blue, - Colors::purple, -}); - -GRADIENT(dark_rainbow, 6, { - Colors::dark_red, - Colors::dark_orange, - Colors::dark_yellow, - Colors::dark_green, - Colors::dark_blue, - Colors::dark_purple, -}); - -GRADIENT(pale_rainbow, 6, { - Colors::pale_red, - Colors::pale_orange, - Colors::pale_yellow, - Colors::pale_green, - Colors::pale_blue, - Colors::pale_purple, -}); - -GRADIENT(peacock, 4, { - Colors::teal, - Colors::black, - Colors::purple, - Colors::blue, -}); - -// Guaranteed to be random. https://xkcd.com/221/ -GRADIENT(random, 20, { - {223, 241, 107}, - Colors::black, - {34, 170, 82}, - Colors::black, - {98, 222, 224}, - Colors::black, - {230, 114, 8}, - Colors::black, - {226, 215, 213}, - Colors::black, - {94, 187, 179}, - Colors::black, - {76, 185, 214}, - Colors::black, - {40, 115, 111}, - Colors::black, - {230, 234, 120}, - Colors::black, - {157, 128, 68}, - Colors::black, -}); - - -} // Gradients - - -namespace Patterns { - -#define PATTERN(x) \ - static const Pattern x = { \ - .cycle_length = 20, \ - .cycle_time_ms = 10000, \ - .reverse = false, \ - .march = false, \ - .gradient = Gradients::x, \ - } +std::map map; + +const Pattern* find(const std::string &name) { + auto e = map.find(name); + return e == map.end() ? NULL : e->second; +} + +std::map::const_iterator map_begin() { + return map.begin(); +} -PATTERN(rainbow); -PATTERN(dark_rainbow); -PATTERN(pale_rainbow); -PATTERN(peacock); +std::map::const_iterator map_end() { + return map.end(); +} -static const Pattern random = { - .cycle_length = 20, - .cycle_time_ms = 10000, - .reverse = false, - .march = true, - .gradient = Gradients::random -}; +static inline cJSON* load_json(const char *path) { + return cJSON_Parse(read_file(path).c_str()); +} -} // Patterns +/* Preset Loader */ -namespace Presets { +static Color json_raw_color(cJSON *json) { + if (json->type != cJSON_Array) { + ESP_LOGE(TAG, "Not an array: %s", json->string); + return {0, 0, 0}; + } + int count = cJSON_GetArraySize(json); + if (count != 3) { + ESP_LOGE(TAG, "Wrong size for %s: %d", json->string, count); + return {0, 0, 0}; + } + // TODO: Number checks for array elements? + return Color{ + (uint8_t)cJSON_GetArrayItem(json, 0)->valueint, + (uint8_t)cJSON_GetArrayItem(json, 1)->valueint, + (uint8_t)cJSON_GetArrayItem(json, 2)->valueint + }; +} -#define PRESET(x) {#x, &Patterns::x} +static Color json_color(cJSON *json, const std::map &colors) { + if (json->type == cJSON_Array) return json_raw_color(json); + if (json->type != cJSON_String) { + ESP_LOGE(TAG, "Not a string: %s", json->string); + return {0, 0, 0}; + } + auto iter = colors.find(json->valuestring); + if (iter == colors.end()) { + ESP_LOGE(TAG, "Not a color: %s", json->valuestring); + return {0, 0, 0}; + } + return iter->second; +} -const std::map map = { - PRESET(rainbow), - PRESET(dark_rainbow), - PRESET(pale_rainbow), - PRESET(peacock), - PRESET(random), -}; +static Pattern* json_pattern(cJSON *json, const std::map &colors) { + if (json->type != cJSON_Object) { + ESP_LOGE(TAG, "Not an object: %s", json->string); + return NULL; + } -const Pattern* find(const std::string &name) { - auto e = map.find(name); - return e == map.end() ? NULL : e->second; + cJSON *pattern = json->child; + std::string type(pattern->string); + + if (type == "gradient") { + if (pattern->type != cJSON_Object) { + ESP_LOGE(TAG, "Not an object: %s", pattern->type); + return NULL; + } + cJSON *jcolors = cJSON_GetObjectItem(pattern, "colors"); + if (!jcolors) { + ESP_LOGE(TAG, "No colors for %s", json->string); + return NULL; + } + if (jcolors->type != cJSON_Array) { + ESP_LOGE(TAG, "Not an array: %s", jcolors->string); + return NULL; + } + Gradient *gradient = Gradient::make(cJSON_GetArraySize(jcolors)); + int i = 0; + cJSON *jcolor; + cJSON_ArrayForEach(jcolor, jcolors) { + Color color = json_color(jcolor, colors); + gradient->colors[i++] = color; + } + return new GradientPattern(gradient); + + } else { + ESP_LOGE(TAG, "Unknown pattern type: %s", type.c_str()); + } + + return NULL; +} + +void reload() { + std::map colors; + cJSON *jcolors = load_json("/spiffs/colors.json"); + if (!jcolors) { + ESP_LOGW(TAG, "No preset colors!"); + } else { + cJSON *jcolor; + cJSON_ArrayForEach(jcolor, jcolors) { + Color color = json_color(jcolor, colors); + colors[jcolor->string] = color; + } + cJSON_Delete(jcolors); + } + + for (const auto &k_v : map) delete k_v.second; + map.clear(); + + cJSON *jpresets = load_json("/spiffs/presets.json"); + if (!jpresets) { + ESP_LOGE(TAG, "No preset patterns!"); + } else { + cJSON *jpreset; + cJSON_ArrayForEach(jpreset, jpresets) { + Pattern *pattern = json_pattern(jpreset, colors); + if (pattern) { + ESP_LOGI(TAG, "Creating preset: %s", jpreset->string); + map[jpreset->string] = pattern; + } + } + cJSON_Delete(jpresets); + } } } // Presets diff --git a/main/presets.hpp b/main/presets.hpp index 0633dc8..fca5767 100644 --- a/main/presets.hpp +++ b/main/presets.hpp @@ -7,44 +7,11 @@ namespace Presets { -const struct Pattern* find(const std::string&); +const Pattern* find(const std::string&); -extern const std::map map; +std::map::const_iterator map_begin(); +std::map::const_iterator map_end(); -} // 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}; +void reload(); -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 +} // Presets diff --git a/main/presets.json b/main/presets.json new file mode 100644 index 0000000..d9dd401 --- /dev/null +++ b/main/presets.json @@ -0,0 +1,17 @@ +{ +"rainbow": {"gradient": {"colors": ["red", "orange", "yellow", "green", "blue", "purple"]}}, +"dark_rainbow": {"gradient": {"colors": ["dark_red", "dark_orange", "dark_yellow", "dark_green", "dark_blue", "dark_purple"]}}, +"pale_rainbow": {"gradient": {"colors": ["pale_red", "pale_orange", "pale_yellow", "pale_green", "pale_blue", "pale_purple"]}}, + +"peacock": {"gradient": {"colors": ["purple", "blue", "teal", "black"]}}, +"pusheen": {"gradient": {"colors": ["teal", "pink"]}}, + +"love": {"gradient": {"colors": ["pink", "black", "red", "black"]}}, +"irish": {"gradient": {"colors": ["dark_green", "black", "green", "black"]}}, +"bunny": {"gradient": {"colors": ["pink", "pale_purple", "black", "pale_yellow", "pale_green", "black"]}}, +"patriot": {"gradient": {"colors": ["red", "black", "black", "white", "black", "black", "blue", "black", "black"]}}, +"pumpkin": {"gradient": {"colors": ["orange", "black", "purple", "black"]}}, +"turkey": {"gradient": {"colors": ["orange", "black", "dark_green", "black", "brown", "black", "dark_yellow", "black"]}}, +"mazel": {"gradient": {"colors": ["blue", "black", "white", "black"]}}, +"santa": {"gradient": {"colors": ["red", "white", "green", "black"]}} +} diff --git a/main/utils.cpp b/main/utils.cpp index 2f6f399..9127991 100644 --- a/main/utils.cpp +++ b/main/utils.cpp @@ -1,4 +1,8 @@ +static const char *TAG = "utils"; +#include + #include +#include #include "utils.hpp" @@ -11,3 +15,43 @@ int64_t time_us() { clock_gettime(CLOCK_MONOTONIC, &ts); return (int64_t)ts.tv_sec * 1000000L + (int64_t)ts.tv_nsec / 1000L; } + + + +std::string read_file(const char *path) { + std::string data; + FILE *f = fopen(path, "r"); + if (!f) { + ESP_LOGE(TAG, "Failed to read %s", path); + return data; + } + constexpr size_t szbuf = 4096; + char *buf = (char*)malloc(szbuf); + size_t nread; + while ((nread = fread(buf, 1, szbuf - 1, f)) > 0) { + buf[nread] = '\0'; + data += buf; + } + fclose(f); + free(buf); + return data; +} + +void write_file(const char *path, const std::string &data) { + return write_file(path, data.c_str(), data.length()); +} + +void write_file(const char *path, const char *data, int len) { + FILE *f = fopen(path, "w"); + if (!f) { + ESP_LOGE(TAG, "Failed to open %s", path); + return; + } + const char *ptr = data; + size_t remain = (len < 0 ? strlen(data) : len); + size_t nwritten = fwrite(ptr, 1, remain, f); + if (nwritten != remain) { + ESP_LOGE(TAG, "Failed to write to %s: %d/%d", path, nwritten, remain); + } + fclose(f); +} diff --git a/main/utils.hpp b/main/utils.hpp index 8ca0720..226fa11 100644 --- a/main/utils.hpp +++ b/main/utils.hpp @@ -1,6 +1,12 @@ #pragma once #include +#include +#include int64_t time_us(); + +std::string read_file(const char *path); +void write_file(const char *path, const std::string &data); +void write_file(const char *path, const char *data, int len = -1);