- 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 "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); | |||
| } | |||
| @@ -35,6 +35,7 @@ private: | |||
| std::string state_topic; | |||
| std::string cmd_topic; | |||
| std::string data_topic; | |||
| bool ready, should_publish; | |||
| @@ -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) { | |||
| @@ -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 { | |||
| @@ -56,7 +56,7 @@ static void start_filesystem() { | |||
| ESP_ERROR_CHECK(esp_vfs_spiffs_register(&conf)); | |||
| } | |||
| /* | |||
| #include <string> | |||
| #include <dirent.h> | |||
| #include <sys/stat.h> | |||
| @@ -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()); | |||
| @@ -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); | |||
| @@ -1,118 +1,140 @@ | |||
| static const char *TAG = "presets"; | |||
| #include <esp_log.h> | |||
| #include <cJSON.h> | |||
| #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 | |||
| @@ -7,44 +7,11 @@ | |||
| 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 <cstring> | |||
| #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); | |||
| } | |||
| @@ -1,6 +1,12 @@ | |||
| #pragma once | |||
| #include <cstdint> | |||
| #include <string> | |||
| #include <cstdio> | |||
| 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); | |||