- New definitions can be delivered via MQTT - Includes extensible pattern class for new preset typesS3
| @@ -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] | |||||
| } | |||||
| @@ -3,12 +3,14 @@ static const char *TAG = "device"; | |||||
| #include "device.hpp" | #include "device.hpp" | ||||
| #include "presets.hpp" | #include "presets.hpp" | ||||
| #include "utils.hpp" | |||||
| Device::Device(std::string _id) : | Device::Device(std::string _id) : | ||||
| id(_id), | id(_id), | ||||
| state_topic("light/" + id + "/state"), | state_topic("light/" + id + "/state"), | ||||
| cmd_topic("light/" + id + "/cmd"), | cmd_topic("light/" + id + "/cmd"), | ||||
| data_topic("light/" + id + "/data/+"), | |||||
| ready(false), should_publish(false), | ready(false), should_publish(false), | ||||
| mutex(xSemaphoreCreateMutex()), | mutex(xSemaphoreCreateMutex()), | ||||
| sem(xSemaphoreCreateBinary()), | 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) { | if (esp_mqtt_client_subscribe(client, cmd_topic.c_str(), 0) < 0) { | ||||
| ESP_LOGE(TAG, "Failed to subscribe to %s", cmd_topic.c_str()); | 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 *json = cJSON_CreateObject(); | ||||
| cJSON_AddStringToObject(json, "unique_id", id.c_str()); | cJSON_AddStringToObject(json, "unique_id", id.c_str()); | ||||
| cJSON *device = cJSON_AddObjectToObject(json, "device"); | 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_AddStringToObject(json, "schema", "json"); | ||||
| cJSON_AddBoolToObject(json, "effect", true); | cJSON_AddBoolToObject(json, "effect", true); | ||||
| cJSON *effects = cJSON_AddArrayToObject(json, "effect_list"); | 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); | 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) { | 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"); | 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); | xSemaphoreGive(sem); | ||||
| } | } | ||||
| @@ -35,6 +35,7 @@ private: | |||||
| std::string state_topic; | std::string state_topic; | ||||
| std::string cmd_topic; | std::string cmd_topic; | ||||
| std::string data_topic; | |||||
| bool ready, should_publish; | bool ready, should_publish; | ||||
| @@ -30,11 +30,12 @@ Color Gradient::at(Fixed x) const { | |||||
| return color; | 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 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); | //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); | 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 ? | 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); | //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) { | 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) : | LEDStrip::LEDStrip(int _length) : | ||||
| length(_length), pattern(NULL), | length(_length), pattern(NULL), | ||||
| pixels(new Color[_length]), | 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) { | void LEDStrip::setLength(int _length) { | ||||
| length_changing(_length); | length_changing(_length); | ||||
| delete[] pixels; | delete[] pixels; | ||||
| length = _length; | length = _length; | ||||
| pixels = new Color[_length]; | pixels = new Color[_length]; | ||||
| update_state(); | |||||
| } | } | ||||
| void LEDStrip::step() { | 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) { | static std::string termcolor(Color c) { | ||||
| @@ -16,18 +16,60 @@ constexpr int shift = 16; | |||||
| struct Gradient { | struct Gradient { | ||||
| Color at(Fixed x) const; | 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; | unsigned int n_colors; | ||||
| Color 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_length; | ||||
| int cycle_time_ms; | int cycle_time_ms; | ||||
| bool reverse; | bool reverse; | ||||
| bool march; | bool march; | ||||
| Gradient gradient; | |||||
| Gradient *gradient; | |||||
| }; | }; | ||||
| class LEDStrip { | class LEDStrip { | ||||
| @@ -39,7 +81,7 @@ public: | |||||
| virtual void show() = 0; | virtual void show() = 0; | ||||
| virtual void length_changing(int) {}; | 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; } | const Pattern* getPattern() const { return pattern; } | ||||
| void setLength(int length); | void setLength(int length); | ||||
| @@ -51,8 +93,9 @@ protected: | |||||
| Color *pixels; | Color *pixels; | ||||
| private: | private: | ||||
| int64_t last_us; | |||||
| Fixed offset; | |||||
| void update_state(); | |||||
| Pattern::State *state; | |||||
| }; | }; | ||||
| class TerminalLEDs : public LEDStrip { | class TerminalLEDs : public LEDStrip { | ||||
| @@ -56,7 +56,7 @@ static void start_filesystem() { | |||||
| ESP_ERROR_CHECK(esp_vfs_spiffs_register(&conf)); | ESP_ERROR_CHECK(esp_vfs_spiffs_register(&conf)); | ||||
| } | } | ||||
| /* | |||||
| #include <string> | #include <string> | ||||
| #include <dirent.h> | #include <dirent.h> | ||||
| #include <sys/stat.h> | #include <sys/stat.h> | ||||
| @@ -84,49 +84,6 @@ static void log_dir(const std::string &path) { | |||||
| closedir(dir); | 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"; | constexpr char config_path[] = "/spiffs/blinky.json"; | ||||
| @@ -159,7 +116,7 @@ extern "C" void app_main(void) { | |||||
| // Dummy Filesystem | // Dummy Filesystem | ||||
| start_filesystem(); | start_filesystem(); | ||||
| // TODO: Scrape this out | // TODO: Scrape this out | ||||
| //log_dir("/spiffs"); | |||||
| log_dir("/spiffs"); | |||||
| Device device("blinky-jr"); | Device device("blinky-jr"); | ||||
| @@ -173,6 +130,8 @@ extern "C" void app_main(void) { | |||||
| LEDStrip *LEDs = new SPI_LEDs(39); //new TerminalLEDs(); | LEDStrip *LEDs = new SPI_LEDs(39); //new TerminalLEDs(); | ||||
| while (true) { | while (true) { | ||||
| Presets::reload(); | |||||
| ESP_LOGI(TAG, "Configuring LEDs"); | ESP_LOGI(TAG, "Configuring LEDs"); | ||||
| device.lock(); | device.lock(); | ||||
| @@ -188,6 +147,7 @@ extern "C" void app_main(void) { | |||||
| const Pattern *pattern = Presets::find(effect); | const Pattern *pattern = Presets::find(effect); | ||||
| if (pattern) { | if (pattern) { | ||||
| ESP_LOGI(TAG, "Setting pattern '%s'", effect.c_str()); | |||||
| LEDs->setPattern(pattern); | LEDs->setPattern(pattern); | ||||
| } else { | } else { | ||||
| ESP_LOGW(TAG, "Could not find pattern '%s'", effect.c_str()); | ESP_LOGW(TAG, "Could not find pattern '%s'", effect.c_str()); | ||||
| @@ -88,6 +88,7 @@ esp_mqtt_client_handle_t start_mqtt_client( | |||||
| .cert_pem = (const char *)broker_pem_start, | .cert_pem = (const char *)broker_pem_start, | ||||
| .username = CONFIG_BROKER_USER, | .username = CONFIG_BROKER_USER, | ||||
| .password = CONFIG_BROKER_PASSWORD, | .password = CONFIG_BROKER_PASSWORD, | ||||
| .buffer_size = 4096, | |||||
| }; | }; | ||||
| esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqtt_cfg); | esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqtt_cfg); | ||||
| @@ -1,118 +1,140 @@ | |||||
| static const char *TAG = "presets"; | |||||
| #include <esp_log.h> | |||||
| #include <cJSON.h> | |||||
| #include "presets.hpp" | #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 <std::string, const Pattern*> map; | |||||
| const Pattern* find(const std::string &name) { | |||||
| auto e = map.find(name); | |||||
| return e == map.end() ? NULL : e->second; | |||||
| } | |||||
| std::map<std::string, const Pattern*>::const_iterator map_begin() { | |||||
| return map.begin(); | |||||
| } | |||||
| PATTERN(rainbow); | |||||
| PATTERN(dark_rainbow); | |||||
| PATTERN(pale_rainbow); | |||||
| PATTERN(peacock); | |||||
| std::map<std::string, const Pattern*>::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 <std::string, Color> &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 <std::string, const Pattern*> map = { | |||||
| PRESET(rainbow), | |||||
| PRESET(dark_rainbow), | |||||
| PRESET(pale_rainbow), | |||||
| PRESET(peacock), | |||||
| PRESET(random), | |||||
| }; | |||||
| static Pattern* json_pattern(cJSON *json, const std::map <std::string, Color> &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 <std::string, Color> 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 | } // Presets | ||||
| @@ -7,44 +7,11 @@ | |||||
| namespace Presets { | namespace Presets { | ||||
| const struct Pattern* find(const std::string&); | |||||
| const Pattern* find(const std::string&); | |||||
| extern const std::map <std::string, const Pattern*> map; | |||||
| std::map<std::string, const Pattern*>::const_iterator map_begin(); | |||||
| std::map<std::string, const Pattern*>::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 | |||||
| @@ -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"]}} | |||||
| } | |||||
| @@ -1,4 +1,8 @@ | |||||
| static const char *TAG = "utils"; | |||||
| #include <esp_log.h> | |||||
| #include <sys/time.h> | #include <sys/time.h> | ||||
| #include <cstring> | |||||
| #include "utils.hpp" | #include "utils.hpp" | ||||
| @@ -11,3 +15,43 @@ int64_t time_us() { | |||||
| clock_gettime(CLOCK_MONOTONIC, &ts); | clock_gettime(CLOCK_MONOTONIC, &ts); | ||||
| return (int64_t)ts.tv_sec * 1000000L + (int64_t)ts.tv_nsec / 1000L; | 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); | |||||
| } | |||||
| @@ -1,6 +1,12 @@ | |||||
| #pragma once | #pragma once | ||||
| #include <cstdint> | #include <cstdint> | ||||
| #include <string> | |||||
| #include <cstdio> | |||||
| int64_t time_us(); | 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); | |||||