From f4f66dd13cb148c29bb2544d7c1d51c4121d87b4 Mon Sep 17 00:00:00 2001 From: jrhoffa Date: Wed, 14 Sep 2022 19:25:11 -0700 Subject: [PATCH 01/16] Remove extra debugging --- main/main.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/main/main.cpp b/main/main.cpp index 68eef8e..dfe8d83 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -56,6 +56,7 @@ static void start_filesystem() { ESP_ERROR_CHECK(esp_vfs_spiffs_register(&conf)); } +/* #include #include #include @@ -83,7 +84,7 @@ static void log_dir(const std::string &path) { closedir(dir); } - +*/ static std::string read_file(const char *path) { std::string data; @@ -157,7 +158,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"); @@ -213,14 +214,16 @@ extern "C" void app_main(void) { int period_us = 1000000 / 60; int64_t start = time_us(); int64_t target_us = start; +/* int64_t total_delay_us = 0; int n_delays = 0; int64_t after_sleep_us = 0; int n_processes = 0; int64_t total_process_us = 0; +*/ while (true) { int64_t now = time_us(); - +/* if (after_sleep_us > 0) { total_process_us += now - after_sleep_us; ++n_processes; @@ -237,19 +240,19 @@ extern "C" void app_main(void) { start = now; } - +*/ int64_t delay_us = target_us - now; 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; } - +/* after_sleep_us = time_us(); total_delay_us += (after_sleep_us - now); ++n_delays; - +*/ LEDs->step(); LEDs->show(); From 7cce98377408642c62b3ab624b7d30a8a9988998 Mon Sep 17 00:00:00 2001 From: jrhoffa Date: Wed, 14 Sep 2022 20:56:06 -0700 Subject: [PATCH 02/16] Add asynchronous DMA support and fix timing issues --- main/leds.cpp | 15 ++++++++++-- main/leds.hpp | 4 ++-- main/main.cpp | 17 +++++++------- main/spi_leds.cpp | 59 +++++++++++++++++++++++++++++------------------ main/spi_leds.hpp | 6 +++-- main/utils.cpp | 7 ++++-- 6 files changed, 70 insertions(+), 38 deletions(-) diff --git a/main/leds.cpp b/main/leds.cpp index 378bd57..121ee29 100644 --- a/main/leds.cpp +++ b/main/leds.cpp @@ -37,8 +37,19 @@ void Pattern::step(Color pixels[], int len, int64_t &last_us, Fixed &offset) con last_us = now_us; //ESP_LOGI(TAG, "duration %d", duration_us); + /* + int period_us = 1000000 / 60; + int n_skipped = (duration_us - period_us / 2) / period_us; + if (n_skipped) { + ESP_LOGW(TAG, "Skipped %d frames", n_skipped); + } + */ + // Don't make a major animation jump if it's been terribly long + if (duration_us > 100000) duration_us = 100000; + + int offset_delta = (duration_us << shift) / (cycle_time_ms * 1000); - if (reverse) offset -= offset_delta; else offset += offset_delta; + if (reverse) offset += offset_delta; else offset -= offset_delta; Fixed off = march ? (((int)offset * cycle_length) & ~((1 << shift) - 1)) / cycle_length : offset; @@ -73,7 +84,7 @@ static std::string termcolor(Color c) { std::to_string((int)c.b) + "m"; } -void TerminalLEDs::show() const { +void TerminalLEDs::show() { std::string line; for (int i = 0; i < length; i++) { line += termcolor(pixels[i]) + " "; diff --git a/main/leds.hpp b/main/leds.hpp index dc57d7f..82589f0 100644 --- a/main/leds.hpp +++ b/main/leds.hpp @@ -36,7 +36,7 @@ public: void step(); - virtual void show() const = 0; + virtual void show() = 0; virtual void length_changing(int) {}; void setPattern(const Pattern *_pattern) { pattern = _pattern; } @@ -58,5 +58,5 @@ private: class TerminalLEDs : public LEDStrip { public: TerminalLEDs(int length = 0) : LEDStrip(length) {} - void show() const; + void show(); }; diff --git a/main/main.cpp b/main/main.cpp index dfe8d83..924987b 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -131,6 +131,7 @@ std::string gen_config(bool is_on, const std::string &effect) { constexpr char config_path[] = "/spiffs/blinky.json"; +//#define PROFILE // Entry Point @@ -214,16 +215,16 @@ extern "C" void app_main(void) { int period_us = 1000000 / 60; int64_t start = time_us(); int64_t target_us = start; -/* +#ifdef PROFILE int64_t total_delay_us = 0; int n_delays = 0; int64_t after_sleep_us = 0; int n_processes = 0; int64_t total_process_us = 0; -*/ +#endif // PROFILE while (true) { int64_t now = time_us(); -/* +#ifdef PROFILE if (after_sleep_us > 0) { total_process_us += now - after_sleep_us; ++n_processes; @@ -240,23 +241,23 @@ extern "C" void app_main(void) { start = now; } -*/ +#endif // PROFILE int64_t delay_us = target_us - now; 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; } -/* + + target_us = now + delay_us + period_us; +#ifdef PROFILE after_sleep_us = time_us(); total_delay_us += (after_sleep_us - now); ++n_delays; -*/ +#endif // PROFILE LEDs->step(); LEDs->show(); - - target_us += period_us; } } } diff --git a/main/spi_leds.cpp b/main/spi_leds.cpp index ff84d5e..647444c 100644 --- a/main/spi_leds.cpp +++ b/main/spi_leds.cpp @@ -16,7 +16,8 @@ static const char *TAG = "spi_leds"; SPI_LEDs::SPI_LEDs(int _gpio, int length) : LEDStrip(length), host(SPI3_HOST), gpio(_gpio), device(NULL), - out_buf(NULL) { + out_buf(NULL) + { int ret; spi_bus_config_t buscfg = { @@ -53,7 +54,7 @@ SPI_LEDs::SPI_LEDs(int _gpio, int length) : .cs_ena_posttrans = 0, // moot // Datasheet implies 3.2 MHz => 1.25 us / 4 // We can get away with 4.4_ MHz => 900 ns / 4 - .clock_speed_hz = 4444444, + .clock_speed_hz = 3200000, .input_delay_ns = 0, // moot .spics_io_num = -1, .flags = 0, // Keep it MSB first @@ -77,6 +78,19 @@ void SPI_LEDs::configure(int len) { if (len <= 0) return; out_buf = (uint8_t*)heap_caps_malloc(tx_len(len), MALLOC_CAP_DMA); + + memset(out_buf + tx_len(len) - reset_bytes, 0, reset_bytes); + + transaction = { + .flags = 0, + .cmd = 0, + .addr = 0, + .length = tx_len(len) * 8, + .rxlength = 0, + .user = NULL, + .tx_buffer = out_buf, + .rx_buffer = NULL, + }; } // Inverted @@ -92,7 +106,8 @@ static uint8_t* encode_byte(uint8_t *out, uint8_t in) { return out; } -void SPI_LEDs::show() const { +void SPI_LEDs::show() { + static int index = 0; int ret; const Color *pixel = pixels; uint8_t *out = out_buf; @@ -104,24 +119,24 @@ void SPI_LEDs::show() const { ++pixel; } - // TODO: Reset pad? Or should we check clocks? - - spi_transaction_ext_t transaction_ext = { - .base = { - .flags = 0, - .cmd = 0, - .addr = 0, - .length = tx_len(length) * 8, - .rxlength = 0, - .user = NULL, - .tx_buffer = out_buf, - .rx_buffer = NULL, - }, - .command_bits = 160, - .address_bits = 0, - .dummy_bits = 160, - }; + ++index; - ret = spi_device_transmit(device, &transaction_ext.base); - RET_ON_ERR(spi_device_transmit, ret); + ret = spi_device_queue_trans(device, &transaction, portMAX_DELAY); + RET_ON_ERR(spi_device_queue_trans, ret); + +/* + while ((ret = spi_device_queue_trans(device, &transaction, 0)) != ESP_OK) { + if (ret == ESP_ERR_TIMEOUT) { + // Wait for the previous transaction to finish + ESP_LOGI(TAG, "SPI transaction collision (# %d)", index); + spi_transaction_t *ret_trans; + ret = spi_device_get_trans_result(device, &ret_trans, portMAX_DELAY); + RET_ON_ERR(spi_device_get_trans_result, ret); + ESP_LOGI(TAG, "Transaction complete (# %d)", index); + + } else { + RET_ON_ERR(spi_device_queue_trans, ret); + } + } +*/ } diff --git a/main/spi_leds.hpp b/main/spi_leds.hpp index 786491c..216dc0a 100644 --- a/main/spi_leds.hpp +++ b/main/spi_leds.hpp @@ -8,7 +8,7 @@ class SPI_LEDs : public LEDStrip { public: SPI_LEDs(int gpio, int length = 0); - void show() const; + void show(); void length_changing(int len) { configure(len); } private: @@ -16,8 +16,10 @@ private: int gpio; spi_device_handle_t device; uint8_t *out_buf; + spi_transaction_t transaction; - static size_t tx_len(int len) { return (len * 3) * 4; } + static constexpr int reset_bytes = 30; + static size_t tx_len(int len) { return (len * 3) * 4 + reset_bytes; } void configure(int length); }; diff --git a/main/utils.cpp b/main/utils.cpp index d164c0c..2f6f399 100644 --- a/main/utils.cpp +++ b/main/utils.cpp @@ -4,7 +4,10 @@ int64_t time_us() { - struct timeval tv_now; + /*struct timeval tv_now; gettimeofday(&tv_now, NULL); - return (int64_t)tv_now.tv_sec * 1000000L + (int64_t)tv_now.tv_usec; + return (int64_t)tv_now.tv_sec * 1000000L + (int64_t)tv_now.tv_usec;*/ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (int64_t)ts.tv_sec * 1000000L + (int64_t)ts.tv_nsec / 1000L; } From 7f425f1e5f01d618b149e89fc254b30690d1ecda Mon Sep 17 00:00:00 2001 From: jrhoffa Date: Sat, 17 Sep 2022 10:37:53 -0700 Subject: [PATCH 03/16] 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); From 94797ebda979d1f12d7bfedd5eea919afa3b8f87 Mon Sep 17 00:00:00 2001 From: jrhoffa Date: Sun, 18 Sep 2022 00:13:15 -0700 Subject: [PATCH 04/16] Add random and sparkle animation patterns Includes slight refactor of pattern factory --- main/CMakeLists.txt | 9 ++- main/leds.cpp | 56 --------------- main/leds.hpp | 48 +------------ main/main.cpp | 11 +-- main/patterns/gradient.cpp | 138 +++++++++++++++++++++++++++++++++++++ main/patterns/random.cpp | 118 +++++++++++++++++++++++++++++++ main/patterns/sparkle.cpp | 132 +++++++++++++++++++++++++++++++++++ main/presets.cpp | 58 ++++++++-------- main/presets.hpp | 11 +++ main/presets.json | 2 + 10 files changed, 442 insertions(+), 141 deletions(-) create mode 100644 main/patterns/gradient.cpp create mode 100644 main/patterns/random.cpp create mode 100644 main/patterns/sparkle.cpp diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 952eed0..07abcfd 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,4 +1,5 @@ idf_component_register( + SRCS "main.cpp" "wifi.cpp" @@ -10,6 +11,9 @@ idf_component_register( "leds.cpp" "presets.cpp" "spi_leds.cpp" + "patterns/gradient.cpp" + "patterns/random.cpp" + "patterns/sparkle.cpp" INCLUDE_DIRS "." ) @@ -27,8 +31,3 @@ endfunction() function(export) endfunction() - -# Import tinyxml2 targets -#add_subdirectory(lib/tinyxml2) -# Link tinyxml2 to main component -#target_link_libraries(${COMPONENT_LIB} PUBLIC tinyxml2) diff --git a/main/leds.cpp b/main/leds.cpp index ffd9dc9..bf67917 100644 --- a/main/leds.cpp +++ b/main/leds.cpp @@ -5,62 +5,6 @@ static const char *TAG = "leds"; #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 GradientPattern::step(Color pixels[], int len, Pattern::State *_state) const { - GradientPattern::State *state = (GradientPattern::State*)_state; - - int64_t now_us = time_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); - - /* - int period_us = 1000000 / 60; - int n_skipped = (duration_us - period_us / 2) / period_us; - if (n_skipped) { - ESP_LOGW(TAG, "Skipped %d frames", n_skipped); - } - */ - // Don't make a major animation jump if it's been terribly long - if (duration_us > 100000) duration_us = 100000; - - - int offset_delta = (duration_us << shift) / (cycle_time_ms * 1000); - if (reverse) state->offset += offset_delta; else state->offset -= offset_delta; - Fixed off = march ? - (((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)); - } -} - - LEDStrip::LEDStrip(int _length) : length(_length), pattern(NULL), pixels(new Color[_length]), diff --git a/main/leds.hpp b/main/leds.hpp index e4a220a..3520c66 100644 --- a/main/leds.hpp +++ b/main/leds.hpp @@ -8,26 +8,12 @@ struct Color { uint8_t r, g, b; } __attribute__ ((packed)); + typedef uint16_t Fixed; constexpr int factor = 65536; 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[]; -}; - class Pattern { public: virtual ~Pattern() {}; @@ -39,38 +25,6 @@ public: 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; -}; class LEDStrip { public: diff --git a/main/main.cpp b/main/main.cpp index da7ac5a..5bc76a3 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -149,16 +149,17 @@ extern "C" void app_main(void) { if (pattern) { ESP_LOGI(TAG, "Setting pattern '%s'", effect.c_str()); LEDs->setPattern(pattern); + + char *config = cJSON_PrintUnformatted(json); + write_file(config_path, config); + ESP_LOGI(TAG, "Saved config"); + cJSON_free(config); + } else { ESP_LOGW(TAG, "Could not find pattern '%s'", effect.c_str()); pattern = LEDs->getPattern(); } - char *config = cJSON_PrintUnformatted(json); - write_file(config_path, config); - ESP_LOGI(TAG, "Saved config"); - - cJSON_free(config); cJSON_Delete(json); if (length <= 0 || !pattern || !is_on) { diff --git a/main/patterns/gradient.cpp b/main/patterns/gradient.cpp new file mode 100644 index 0000000..ebdf18c --- /dev/null +++ b/main/patterns/gradient.cpp @@ -0,0 +1,138 @@ +static const char *TAG = "gradient"; +#include + +#include "presets.hpp" +#include "utils.hpp" + + +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[]; +}; + +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; +}; + + +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 GradientPattern::step(Color pixels[], int len, Pattern::State *_state) const { + GradientPattern::State *state = (GradientPattern::State*)_state; + + int64_t now_us = time_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); + + /* + int period_us = 1000000 / 60; + int n_skipped = (duration_us - period_us / 2) / period_us; + if (n_skipped) { + ESP_LOGW(TAG, "Skipped %d frames", n_skipped); + } + */ + // Don't make a major animation jump if it's been terribly long + if (duration_us > 100000) duration_us = 100000; + + + int offset_delta = (duration_us << shift) / (cycle_time_ms * 1000); + if (reverse) state->offset += offset_delta; else state->offset -= offset_delta; + Fixed off = march ? + (((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)); + } +} + + +Pattern* json_gradient_pattern(cJSON *pattern, const Presets::ColorMap &colors) { + 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 in %s", pattern->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 = Presets::json_color(jcolor, colors); + gradient->colors[i++] = color; + } + + return new GradientPattern(gradient); +} diff --git a/main/patterns/random.cpp b/main/patterns/random.cpp new file mode 100644 index 0000000..e51db94 --- /dev/null +++ b/main/patterns/random.cpp @@ -0,0 +1,118 @@ +static const char *TAG = "random"; +#include + +#include + +#include "presets.hpp" +#include "utils.hpp" + + +static Color rand_color() { + static const Color colors[] = { + {255, 0, 0}, + { 78, 25, 0}, + {255, 160, 0}, + { 0, 255, 0}, + { 0, 0, 255}, + { 38, 10, 42}, + }; + return colors[esp_random() % 6]; +} + +class RandomPattern : public Pattern { +private: + struct State : Pattern::State { + State(int _len, int gap) : + last_us(0), step(0), offset(0), + n_colors((_len + gap) / (gap + 1)), colors(new Color[n_colors]) + { + for (Color *color = colors, *end = colors + n_colors; color < end; ++color) { + *color = rand_color(); + } + } + ~State() { delete[] colors; } + + int64_t last_us; + int step; + int offset; + int n_colors; + Color *colors; + }; + +public: + RandomPattern(int _delay_ms, int _gap) : delay_us(_delay_ms * 1000), gap(_gap) {} + + State* start(int len) const { return new State(len, gap); } + + void step(Color colors[], int len, Pattern::State *_state) const { + State *state = (State*)_state; + + int64_t now_us = time_us(); + int64_t elapsed_us = now_us - state->last_us; + bool should_step = (state->last_us == 0) || (elapsed_us >= delay_us && delay_us > 0); + if (should_step) { + state->last_us = (elapsed_us >= delay_us + 100000 ? now_us : state->last_us + delay_us); + + int step = state->step; + int offset = state->offset; + Color *inc = state->colors + offset; + Color *outc = colors; + + if ((gap + 1 - step) % (gap + 1) == 0) *inc = rand_color(); + + for (int i = 0; i < len; i++) { + if ((i - step + gap + 1) % (gap + 1) == 0) { + *outc = *inc--; + if (inc < state->colors) inc += state->n_colors; + } else { + *outc = {0, 0, 0}; + } + ++outc; + } + + step++; + if (step > gap) { + step -= gap + 1; + ++offset; + if (offset >= state->n_colors) offset -= state->n_colors; + state->offset = offset; + } + state->step = step; + } + } + +private: + int delay_us; + int gap; +}; + + +Pattern* json_random_pattern(cJSON *pattern, const Presets::ColorMap &colors) { + int delay_ms = 500; + int gap = 1; + + if (pattern->type != cJSON_Object) { + ESP_LOGE(TAG, "Not an object: %s", pattern->type); + return NULL; + } + + cJSON *jdelay = cJSON_GetObjectItem(pattern, "delay"); + if (jdelay) { + if (jdelay->type != cJSON_Number) { + ESP_LOGE(TAG, "Not a number: %s", jdelay->string); + } else { + delay_ms = jdelay->valuedouble * 1000.; + } + } + + cJSON *jgap = cJSON_GetObjectItem(pattern, "gap"); + if (jgap) { + if (jgap->type != cJSON_Number) { + ESP_LOGE(TAG, "Not a number: %s", jgap->string); + } else { + gap = jgap->valueint; + } + } + + return new RandomPattern(delay_ms, gap); +} diff --git a/main/patterns/sparkle.cpp b/main/patterns/sparkle.cpp new file mode 100644 index 0000000..7dfdf80 --- /dev/null +++ b/main/patterns/sparkle.cpp @@ -0,0 +1,132 @@ +static const char *TAG = "random"; +#include + +#include +#include + +#include + +#include "presets.hpp" +#include "utils.hpp" + + +static const int8_t ndist[64] = {-127, -104, -92, -84, -77, -72, -67, -62, -58, -55, -51, -48, -45, -42, -39, -37, -34, -32, -29, -27, -25, -22, -20, -18, -16, -14, -11, -9, -7, -5, -3, -1, 1, 3, 5, 7, 9, 11, 14, 16, 18, 20, 22, 25, 27, 29, 32, 34, 37, 39, 42, 45, 48, 51, 55, 58, 62, 67, 72, 77, 84, 92, 104, 127}; + +int norm(int mean) { + return (mean * 128) / ndist[esp_random() % 64]; +} + +struct Sparkle { + Sparkle(int64_t time_us, int mean_dur_us, int len): + start_us(time_us), end_us(time_us + (mean_dur_us)), + pos(esp_random() % len) {} + + bool expired(int64_t time_us) { return time_us >= end_us; } + + uint8_t intensity_at(int64_t time_us) { + if (time_us < start_us || time_us >= end_us) return 0; + int64_t midpoint_us = (start_us + end_us) / 2; + if (time_us >= midpoint_us) time_us = start_us + (end_us - time_us); + return (((time_us - start_us) * 255) * 2) / (end_us - start_us); + } + + int64_t start_us; + int end_us; + int pos; +}; + +Color fade(Color c, unsigned int i) { + return { + (uint8_t)((c.r * i) / 255), + (uint8_t)((c.g * i) / 255), + (uint8_t)((c.b * i) / 255) + }; +} + +class SparklePattern : public Pattern { +private: + struct State : Pattern::State { + std::list sparkles; + }; + +public: + SparklePattern(Color _color, int _duration_ms, int _delay_ms, int _density) : + color(_color), duration_us(_duration_ms * 1000), + delay_us(_delay_ms * 1000), density(_density) + {} + + State* start(int len) const { return new State(); } + + void step(Color colors[], int len, Pattern::State *_state) const { + State *state = (State*)_state; + + int64_t now_us = time_us(); + + int64_t start_us = now_us; + for ( int open = (len / density) + 1 - state->sparkles.size(); + open > 0; + --open, start_us += delay_us + ) { + state->sparkles.push_front(Sparkle(start_us, duration_us, len)); + } + + memset(colors, 0, sizeof(*colors) * len); + + for (auto iter = state->sparkles.begin(); iter != state->sparkles.end(); ) { + if (iter->expired(now_us)) { + state->sparkles.erase(iter++); + } else { + colors[iter->pos] = fade(color, iter->intensity_at(now_us)); + ++iter; + } + } + } + +private: + Color color; + int duration_us; + int delay_us; + int density; +}; + + +Pattern* json_sparkle_pattern(cJSON *pattern, const Presets::ColorMap &colors) { + int duration_ms = 250; + int delay_ms = 3000; + int density = 10; + Color color = {255, 255, 255}; + + if (pattern->type != cJSON_Object) { + ESP_LOGE(TAG, "Not an object: %s", pattern->type); + return NULL; + } + + cJSON *jnum = cJSON_GetObjectItem(pattern, "delay"); + if (jnum) { + if (jnum->type != cJSON_Number) { + ESP_LOGE(TAG, "Not a number: %s", jnum->string); + } else { + delay_ms = jnum->valuedouble * 1000.; + } + } + + jnum = cJSON_GetObjectItem(pattern, "duration"); + if (jnum) { + if (jnum->type != cJSON_Number) { + ESP_LOGE(TAG, "Not a number: %s", jnum->string); + } else { + duration_ms = jnum->valuedouble * 1000.; + } + } + + jnum = cJSON_GetObjectItem(pattern, "density"); + if (jnum) { + if (jnum->type != cJSON_Number) { + ESP_LOGE(TAG, "Not a number: %s", jnum->string); + } else { + density = jnum->valuedouble; + } + } + + return new SparklePattern(color, duration_ms, delay_ms, density); +} diff --git a/main/presets.cpp b/main/presets.cpp index 6931c83..1f7da36 100644 --- a/main/presets.cpp +++ b/main/presets.cpp @@ -7,8 +7,25 @@ static const char *TAG = "presets"; #include "utils.hpp" +/* Sadly, need to define these here */ + +extern Pattern* json_gradient_pattern(cJSON *pattern, const Presets::ColorMap &colors); +extern Pattern* json_random_pattern(cJSON *pattern, const Presets::ColorMap &colors); +extern Pattern* json_sparkle_pattern(cJSON *pattern, const Presets::ColorMap &colors); + namespace Presets { +/* Factory */ + +typedef Pattern* (*PatternGenerator)(cJSON*, const Presets::ColorMap&); + +static const std::map generators { + {"gradient", json_gradient_pattern}, + {"random", json_random_pattern}, + {"sparkle", json_sparkle_pattern}, +}; + + /* Preset Map */ std::map map; @@ -51,7 +68,7 @@ static Color json_raw_color(cJSON *json) { }; } -static Color json_color(cJSON *json, const std::map &colors) { +Color json_color(cJSON *json, const ColorMap &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); @@ -65,7 +82,8 @@ static Color json_color(cJSON *json, const std::map &colors return iter->second; } -static Pattern* json_pattern(cJSON *json, const std::map &colors) { + +static Pattern* json_pattern(cJSON *json, const ColorMap &colors) { if (json->type != cJSON_Object) { ESP_LOGE(TAG, "Not an object: %s", json->string); return NULL; @@ -74,38 +92,22 @@ static Pattern* json_pattern(cJSON *json, const std::map &c 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 { + auto iter = generators.find(type); + if (iter == generators.end()) { ESP_LOGE(TAG, "Unknown pattern type: %s", type.c_str()); + return NULL; } - return NULL; + return iter->second(pattern, colors); } void reload() { - std::map colors; + if (generators.empty()) { + ESP_LOGE(TAG, "No pattern generators defined!"); + return; + } + + ColorMap colors; cJSON *jcolors = load_json("/spiffs/colors.json"); if (!jcolors) { ESP_LOGW(TAG, "No preset colors!"); diff --git a/main/presets.hpp b/main/presets.hpp index fca5767..f87ca22 100644 --- a/main/presets.hpp +++ b/main/presets.hpp @@ -1,7 +1,10 @@ #pragma once + #include #include +#include + #include "leds.hpp" @@ -14,4 +17,12 @@ std::map::const_iterator map_end(); void reload(); + +// Helpers + +typedef std::map ColorMap; + +Color json_color(cJSON *json, const ColorMap &colors); + + } // Presets diff --git a/main/presets.json b/main/presets.json index d9dd401..5f44977 100644 --- a/main/presets.json +++ b/main/presets.json @@ -5,6 +5,8 @@ "peacock": {"gradient": {"colors": ["purple", "blue", "teal", "black"]}}, "pusheen": {"gradient": {"colors": ["teal", "pink"]}}, +"random": {"random": {"gap": 1, "delay": 0.5}}, +"snow": {"sparkle": {"color": "white", "duration": 0.25, "delay": 3, "density": 10}}, "love": {"gradient": {"colors": ["pink", "black", "red", "black"]}}, "irish": {"gradient": {"colors": ["dark_green", "black", "green", "black"]}}, From fcdf1766acfbe4d535595b993ceff22b3f1d28ec Mon Sep 17 00:00:00 2001 From: jrhoffa Date: Sun, 18 Sep 2022 15:41:17 -0700 Subject: [PATCH 05/16] Fix memory leak in pattern states Also simplified sparkle pattern memory --- main/device.cpp | 2 +- main/leds.hpp | 7 +++++-- main/main.cpp | 33 ++++++++++++++++++++------------- main/patterns/sparkle.cpp | 29 ++++++++++++++--------------- 4 files changed, 40 insertions(+), 31 deletions(-) diff --git a/main/device.cpp b/main/device.cpp index f58ee42..6aafeef 100644 --- a/main/device.cpp +++ b/main/device.cpp @@ -125,7 +125,7 @@ void Device::publish_state_locked() { ESP_ERROR_CHECK(esp_mqtt_client_publish( client, state_topic.c_str(), config, 0, 0, 0 )); - cJSON_Delete(json); cJSON_free(config); + cJSON_Delete(json); } } diff --git a/main/leds.hpp b/main/leds.hpp index 3520c66..3a23040 100644 --- a/main/leds.hpp +++ b/main/leds.hpp @@ -16,9 +16,11 @@ constexpr int shift = 16; class Pattern { public: - virtual ~Pattern() {}; + virtual ~Pattern() = default; - struct State {}; + struct State { + virtual ~State() = default; + }; virtual State* start(int) const = 0; @@ -29,6 +31,7 @@ public: class LEDStrip { public: LEDStrip(int length = 0); + ~LEDStrip() { if (state) delete state; } void step(); diff --git a/main/main.cpp b/main/main.cpp index 5bc76a3..9755e9e 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -88,7 +88,8 @@ static void log_dir(const std::string &path) { constexpr char config_path[] = "/spiffs/blinky.json"; -//#define PROFILE +//#define PROFILE_PERF +//#define PROFILE_MEM // Entry Point @@ -135,7 +136,7 @@ extern "C" void app_main(void) { ESP_LOGI(TAG, "Configuring LEDs"); device.lock(); - const std::string &effect = device.get_effect(); + const std::string effect = device.get_effect(); bool is_on = device.is_on(); int length = device.get_strip_length(); cJSON *json = device.make_json_config_locked(); @@ -174,24 +175,30 @@ extern "C" void app_main(void) { ESP_LOGI(TAG, "Starting animation"); int period_us = 1000000 / 60; - int64_t start = time_us(); - int64_t target_us = start; -#ifdef PROFILE + int64_t target_us = time_us(); + int64_t profile_start = target_us; +#ifdef PROFILE_PERF int64_t total_delay_us = 0; int n_delays = 0; int64_t after_sleep_us = 0; int n_processes = 0; int64_t total_process_us = 0; -#endif // PROFILE +#endif // PROFILE_PERF while (true) { int64_t now = time_us(); -#ifdef PROFILE +#ifdef PROFILE_PERF if (after_sleep_us > 0) { total_process_us += now - after_sleep_us; ++n_processes; } +#endif // PROFILE_PERF - if (now - start >= 5000000) { + if (now - profile_start >= 5000000) { +#ifdef PROFILE_MEM + heap_caps_print_heap_info(MALLOC_CAP_8BIT); +#endif // PROFILE_MEM + +#ifdef PROFILE_PERF ESP_LOGI(TAG, "Average delay: %f ms", (double)total_delay_us / (n_delays * 1000)); total_delay_us = 0; n_delays = 0; @@ -199,10 +206,11 @@ extern "C" void app_main(void) { ESP_LOGI(TAG, "Average processing: %f us", (double)total_process_us / n_processes); total_process_us = 0; n_processes = 0; +#endif // PROFILE_PERF - start = now; + profile_start = now; } -#endif // PROFILE + int64_t delay_us = target_us - now; if (delay_us < 0) delay_us = 0; if (device.wait(delay_us / 1000)) { @@ -211,12 +219,11 @@ extern "C" void app_main(void) { } target_us = now + delay_us + period_us; -#ifdef PROFILE +#ifdef PROFILE_PERF after_sleep_us = time_us(); - total_delay_us += (after_sleep_us - now); ++n_delays; -#endif // PROFILE +#endif // PROFILE_PERF LEDs->step(); LEDs->show(); } diff --git a/main/patterns/sparkle.cpp b/main/patterns/sparkle.cpp index 7dfdf80..871ef73 100644 --- a/main/patterns/sparkle.cpp +++ b/main/patterns/sparkle.cpp @@ -17,6 +17,7 @@ int norm(int mean) { } struct Sparkle { + Sparkle() : start_us(0), end_us(0), pos(0) {} Sparkle(int64_t time_us, int mean_dur_us, int len): start_us(time_us), end_us(time_us + (mean_dur_us)), pos(esp_random() % len) {} @@ -46,7 +47,11 @@ Color fade(Color c, unsigned int i) { class SparklePattern : public Pattern { private: struct State : Pattern::State { - std::list sparkles; + State(int n) : n_sparkles(n), sparkles(new Sparkle[n]) {} + ~State() { delete[] sparkles; } + + int n_sparkles; + Sparkle *sparkles; }; public: @@ -55,29 +60,23 @@ public: delay_us(_delay_ms * 1000), density(_density) {} - State* start(int len) const { return new State(); } + State* start(int len) const { return new State((len / density) + 1); } void step(Color colors[], int len, Pattern::State *_state) const { State *state = (State*)_state; int64_t now_us = time_us(); - int64_t start_us = now_us; - for ( int open = (len / density) + 1 - state->sparkles.size(); - open > 0; - --open, start_us += delay_us - ) { - state->sparkles.push_front(Sparkle(start_us, duration_us, len)); - } - memset(colors, 0, sizeof(*colors) * len); - for (auto iter = state->sparkles.begin(); iter != state->sparkles.end(); ) { - if (iter->expired(now_us)) { - state->sparkles.erase(iter++); + int64_t start_us = now_us; + for (int i = 0; i < state->n_sparkles; ++i, start_us += delay_us) { + Sparkle &sparkle = state->sparkles[i]; + + if (sparkle.expired(now_us)) { + sparkle = Sparkle(start_us, duration_us, len); } else { - colors[iter->pos] = fade(color, iter->intensity_at(now_us)); - ++iter; + colors[sparkle.pos] = fade(color, sparkle.intensity_at(now_us)); } } } From 14ddb4dfdcd3d87fa6d1faccb740b746e2358251 Mon Sep 17 00:00:00 2001 From: jrhoffa Date: Sun, 18 Sep 2022 20:35:53 -0700 Subject: [PATCH 06/16] Fix timestamp overflow in sparkle pattern --- main/patterns/sparkle.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/patterns/sparkle.cpp b/main/patterns/sparkle.cpp index 871ef73..66188e2 100644 --- a/main/patterns/sparkle.cpp +++ b/main/patterns/sparkle.cpp @@ -32,7 +32,7 @@ struct Sparkle { } int64_t start_us; - int end_us; + int64_t end_us; int pos; }; From 33d64e37e7e96451a4e8e5c24227010daac49390 Mon Sep 17 00:00:00 2001 From: jrhoffa Date: Wed, 21 Sep 2022 09:37:38 -1000 Subject: [PATCH 07/16] Add multi-color support to sparkle effect Also fix possible crash when changing presets --- main/main.cpp | 2 ++ main/patterns/sparkle.cpp | 74 ++++++++++++++++++++++++++++----------- main/presets.json | 3 +- 3 files changed, 57 insertions(+), 22 deletions(-) diff --git a/main/main.cpp b/main/main.cpp index 9755e9e..e738b0a 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -131,6 +131,8 @@ extern "C" void app_main(void) { LEDStrip *LEDs = new SPI_LEDs(39); //new TerminalLEDs(); while (true) { + // Trash the old preset in case we can't find the set effect + LEDs->setPattern(NULL); Presets::reload(); ESP_LOGI(TAG, "Configuring LEDs"); diff --git a/main/patterns/sparkle.cpp b/main/patterns/sparkle.cpp index 66188e2..951641a 100644 --- a/main/patterns/sparkle.cpp +++ b/main/patterns/sparkle.cpp @@ -9,18 +9,27 @@ static const char *TAG = "random"; #include "presets.hpp" #include "utils.hpp" - +/* static const int8_t ndist[64] = {-127, -104, -92, -84, -77, -72, -67, -62, -58, -55, -51, -48, -45, -42, -39, -37, -34, -32, -29, -27, -25, -22, -20, -18, -16, -14, -11, -9, -7, -5, -3, -1, 1, 3, 5, 7, 9, 11, 14, 16, 18, 20, 22, 25, 27, 29, 32, 34, 37, 39, 42, 45, 48, 51, 55, 58, 62, 67, 72, 77, 84, 92, 104, 127}; int norm(int mean) { return (mean * 128) / ndist[esp_random() % 64]; } +*/ + +static inline Color fade(Color c, unsigned int i) { + return { + (uint8_t)((c.r * i) / 255), + (uint8_t)((c.g * i) / 255), + (uint8_t)((c.b * i) / 255) + }; +} struct Sparkle { Sparkle() : start_us(0), end_us(0), pos(0) {} - Sparkle(int64_t time_us, int mean_dur_us, int len): + Sparkle(Color _color, int64_t time_us, int mean_dur_us, int len): start_us(time_us), end_us(time_us + (mean_dur_us)), - pos(esp_random() % len) {} + pos(esp_random() % len), color(_color) {} bool expired(int64_t time_us) { return time_us >= end_us; } @@ -31,18 +40,16 @@ struct Sparkle { return (((time_us - start_us) * 255) * 2) / (end_us - start_us); } + Color color_at(int64_t time_us) { + return fade(color, intensity_at(time_us)); + } + int64_t start_us; int64_t end_us; int pos; + Color color; }; -Color fade(Color c, unsigned int i) { - return { - (uint8_t)((c.r * i) / 255), - (uint8_t)((c.g * i) / 255), - (uint8_t)((c.b * i) / 255) - }; -} class SparklePattern : public Pattern { private: @@ -55,51 +62,71 @@ private: }; public: - SparklePattern(Color _color, int _duration_ms, int _delay_ms, int _density) : - color(_color), duration_us(_duration_ms * 1000), + SparklePattern(const Color *_colors, int _n_colors, int _duration_ms, int _delay_ms, int _density) : + n_colors(_n_colors), colors(_colors), + duration_us(_duration_ms * 1000), delay_us(_delay_ms * 1000), density(_density) {} + ~SparklePattern() { if (colors) delete[] colors; } State* start(int len) const { return new State((len / density) + 1); } - void step(Color colors[], int len, Pattern::State *_state) const { + void step(Color pixels[], int len, Pattern::State *_state) const { State *state = (State*)_state; int64_t now_us = time_us(); - memset(colors, 0, sizeof(*colors) * len); + memset(pixels, 0, sizeof(*pixels) * len); int64_t start_us = now_us; - for (int i = 0; i < state->n_sparkles; ++i, start_us += delay_us) { + for (int i = 0; i < state->n_sparkles; ++i, start_us += delay_us / state->n_sparkles) { Sparkle &sparkle = state->sparkles[i]; if (sparkle.expired(now_us)) { - sparkle = Sparkle(start_us, duration_us, len); + sparkle = Sparkle( + colors[esp_random() % n_colors], + start_us, duration_us, len + ); } else { - colors[sparkle.pos] = fade(color, sparkle.intensity_at(now_us)); + pixels[sparkle.pos] = sparkle.color_at(now_us); } } } private: - Color color; + int n_colors; + const Color* colors; int duration_us; int delay_us; int density; }; -Pattern* json_sparkle_pattern(cJSON *pattern, const Presets::ColorMap &colors) { +Pattern* json_sparkle_pattern(cJSON *pattern, const Presets::ColorMap &colormap) { int duration_ms = 250; int delay_ms = 3000; int density = 10; - Color color = {255, 255, 255}; + int n_colors = 0; + Color *colors = NULL; if (pattern->type != cJSON_Object) { ESP_LOGE(TAG, "Not an object: %s", pattern->type); return NULL; } + cJSON *jcolors = cJSON_GetObjectItem(pattern, "colors"); + if (!jcolors || jcolors->type != cJSON_Array) { + ESP_LOGW(TAG, "No colors in %s", pattern->string); + } else { + n_colors = cJSON_GetArraySize(jcolors); + colors = new Color[n_colors]; + int i = 0; + cJSON *jcolor; + cJSON_ArrayForEach(jcolor, jcolors) { + colors[i++] = Presets::json_color(jcolor, colormap); + } + } + cJSON *jnum = cJSON_GetObjectItem(pattern, "delay"); if (jnum) { if (jnum->type != cJSON_Number) { @@ -127,5 +154,10 @@ Pattern* json_sparkle_pattern(cJSON *pattern, const Presets::ColorMap &colors) { } } - return new SparklePattern(color, duration_ms, delay_ms, density); + if (n_colors == 0) { + n_colors = 1; + colors = new Color{255,255,255}; + } + + return new SparklePattern(colors, n_colors, duration_ms, delay_ms, density); } diff --git a/main/presets.json b/main/presets.json index 5f44977..3f0ea55 100644 --- a/main/presets.json +++ b/main/presets.json @@ -6,7 +6,8 @@ "peacock": {"gradient": {"colors": ["purple", "blue", "teal", "black"]}}, "pusheen": {"gradient": {"colors": ["teal", "pink"]}}, "random": {"random": {"gap": 1, "delay": 0.5}}, -"snow": {"sparkle": {"color": "white", "duration": 0.25, "delay": 3, "density": 10}}, +"stars": {"sparkle": {"colors": ["white"], "duration": 0.25, "delay": 3, "density": 20}}, +"pride": {"sparkle": {"colors": ["red", "orange", "yellow", "green", "blue", "purple"], "duration": 0.25, "delay": 3, "density": 1}}, "love": {"gradient": {"colors": ["pink", "black", "red", "black"]}}, "irish": {"gradient": {"colors": ["dark_green", "black", "green", "black"]}}, From 6efddbbc87a7600bfd0934b0faf81f33fb3ca8a2 Mon Sep 17 00:00:00 2001 From: jrhoffa Date: Fri, 23 Sep 2022 20:33:06 -1000 Subject: [PATCH 08/16] Add config files to default flash config --- CMakeLists.txt | 2 ++ files/blinky.json | 1 + {main => files}/colors.json | 0 {main => files}/presets.json | 0 4 files changed, 3 insertions(+) create mode 100644 files/blinky.json rename {main => files}/colors.json (100%) rename {main => files}/presets.json (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 12393e9..fa2060e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,3 +6,5 @@ include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(blinky) target_add_binary_data(blinky.elf "main/broker.pem" TEXT) + +spiffs_create_partition_image(storage files FLASH_IN_PROJECT) diff --git a/files/blinky.json b/files/blinky.json new file mode 100644 index 0000000..8967d0b --- /dev/null +++ b/files/blinky.json @@ -0,0 +1 @@ +{"length":50, "state": "ON", "effect": "rainbow"} diff --git a/main/colors.json b/files/colors.json similarity index 100% rename from main/colors.json rename to files/colors.json diff --git a/main/presets.json b/files/presets.json similarity index 100% rename from main/presets.json rename to files/presets.json From cb62d1f68546779745354ab711b3aee5bc32f44f Mon Sep 17 00:00:00 2001 From: jrhoffa Date: Mon, 26 Sep 2022 09:55:07 -1000 Subject: [PATCH 09/16] Small fixes for v4.4.2 esp-idf support --- .gitignore | 1 + main/main.cpp | 2 +- main/mqtt.cpp | 2 +- main/patterns/gradient.cpp | 2 +- main/patterns/random.cpp | 2 +- main/patterns/sparkle.cpp | 2 +- main/spi_leds.cpp | 2 +- main/wifi.cpp | 214 ++++++++++++++++++------------------- main/wifi.hpp | 6 +- 9 files changed, 117 insertions(+), 116 deletions(-) diff --git a/.gitignore b/.gitignore index dd95613..95cad48 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ CMakeCache.txt sdkconfig sdkconfig.old main/broker.pem +dependencies.lock diff --git a/main/main.cpp b/main/main.cpp index e738b0a..5cc1811 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -68,7 +68,7 @@ static void log_dir(const std::string &path) { if (err < 0) { ESP_LOGE(TAG, "%s %d", path.c_str(), errno); } else { - ESP_LOGI(TAG, "%s %d", path.c_str(), st.st_size); + ESP_LOGI(TAG, "%s %ld", path.c_str(), st.st_size); } DIR *dir; diff --git a/main/mqtt.cpp b/main/mqtt.cpp index 49788f5..eb936cf 100644 --- a/main/mqtt.cpp +++ b/main/mqtt.cpp @@ -85,10 +85,10 @@ esp_mqtt_client_handle_t start_mqtt_client( const esp_mqtt_client_config_t mqtt_cfg = { .uri = CONFIG_BROKER_URI, - .cert_pem = (const char *)broker_pem_start, .username = CONFIG_BROKER_USER, .password = CONFIG_BROKER_PASSWORD, .buffer_size = 4096, + .cert_pem = (const char *)broker_pem_start, }; esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqtt_cfg); diff --git a/main/patterns/gradient.cpp b/main/patterns/gradient.cpp index ebdf18c..4ffe95a 100644 --- a/main/patterns/gradient.cpp +++ b/main/patterns/gradient.cpp @@ -112,7 +112,7 @@ void GradientPattern::step(Color pixels[], int len, Pattern::State *_state) cons Pattern* json_gradient_pattern(cJSON *pattern, const Presets::ColorMap &colors) { if (pattern->type != cJSON_Object) { - ESP_LOGE(TAG, "Not an object: %s", pattern->type); + ESP_LOGE(TAG, "Not an object: %s", pattern->string); return NULL; } cJSON *jcolors = cJSON_GetObjectItem(pattern, "colors"); diff --git a/main/patterns/random.cpp b/main/patterns/random.cpp index e51db94..ba466b0 100644 --- a/main/patterns/random.cpp +++ b/main/patterns/random.cpp @@ -92,7 +92,7 @@ Pattern* json_random_pattern(cJSON *pattern, const Presets::ColorMap &colors) { int gap = 1; if (pattern->type != cJSON_Object) { - ESP_LOGE(TAG, "Not an object: %s", pattern->type); + ESP_LOGE(TAG, "Not an object: %s", pattern->string); return NULL; } diff --git a/main/patterns/sparkle.cpp b/main/patterns/sparkle.cpp index 951641a..b9b6543 100644 --- a/main/patterns/sparkle.cpp +++ b/main/patterns/sparkle.cpp @@ -110,7 +110,7 @@ Pattern* json_sparkle_pattern(cJSON *pattern, const Presets::ColorMap &colormap) Color *colors = NULL; if (pattern->type != cJSON_Object) { - ESP_LOGE(TAG, "Not an object: %s", pattern->type); + ESP_LOGE(TAG, "Not an object: %s", pattern->string); return NULL; } diff --git a/main/spi_leds.cpp b/main/spi_leds.cpp index 647444c..d312d60 100644 --- a/main/spi_leds.cpp +++ b/main/spi_leds.cpp @@ -81,7 +81,7 @@ void SPI_LEDs::configure(int len) { memset(out_buf + tx_len(len) - reset_bytes, 0, reset_bytes); - transaction = { + transaction = (spi_transaction_t){ .flags = 0, .cmd = 0, .addr = 0, diff --git a/main/wifi.cpp b/main/wifi.cpp index c4821b1..7cd2d2c 100644 --- a/main/wifi.cpp +++ b/main/wifi.cpp @@ -1,107 +1,107 @@ -static const char *TAG = "WiFi"; -#include "esp_log.h" - - -#include "wifi.hpp" - -// Defines SSID and PASS -#include "wifi_cfg.hpp" - -static const int CONFIG_ESP_MAXIMUM_RETRY = 3; - - -#include "esp_wifi.h" -#include "freertos/event_groups.h" - -static EventGroupHandle_t s_wifi_event_group; - -#define WIFI_CONNECTED_BIT BIT0 -#define WIFI_FAIL_BIT BIT1 - -static void event_handler(void* arg, esp_event_base_t event_base, - int32_t event_id, void* event_data) { - static int s_retry_num = 0; - - if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { - esp_wifi_connect(); - - } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { - ESP_LOGI(TAG, "Disconnected, retrying connection to AP"); - esp_wifi_connect(); - - } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { - ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data; - ESP_LOGI(TAG, "Got IP address: " IPSTR, IP2STR(&event->ip_info.ip)); - s_retry_num = 0; - xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); - } -} - -int config_wifi() { - ESP_LOGI(TAG, "Connecting to %s", SSID); - - s_wifi_event_group = xEventGroupCreate(); - - ESP_ERROR_CHECK(esp_netif_init()); - - ESP_ERROR_CHECK(esp_event_loop_create_default()); - esp_netif_create_default_wifi_sta(); - - wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); - ESP_ERROR_CHECK(esp_wifi_init(&cfg)); - - esp_event_handler_instance_t instance_any_id; - esp_event_handler_instance_t instance_got_ip; - ESP_ERROR_CHECK(esp_event_handler_instance_register( - WIFI_EVENT, - ESP_EVENT_ANY_ID, - &event_handler, - NULL, - &instance_any_id - )); - ESP_ERROR_CHECK(esp_event_handler_instance_register( - IP_EVENT, - IP_EVENT_STA_GOT_IP, - &event_handler, - NULL, - &instance_got_ip - )); - - wifi_config_t wifi_config = { - .sta = { - .ssid = SSID, - .password = PASS, - }, - }; - - ESP_ERROR_CHECK( esp_wifi_set_mode(WIFI_MODE_STA) ); - ESP_ERROR_CHECK( esp_wifi_set_config(WIFI_IF_STA, &wifi_config) ); - ESP_ERROR_CHECK( esp_wifi_start() ); - -/* - // Waiting until either the connection is established (WIFI_CONNECTED_BIT) - // or connection failed for the maximum number of re-tries (WIFI_FAIL_BIT). - // The bits are set by event_handler() (see above) - EventBits_t bits = xEventGroupWaitBits( - s_wifi_event_group, - WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, - pdFALSE, - pdFALSE, - portMAX_DELAY - ); - - // xEventGroupWaitBits() returns the bits before the call returned, hence we - // can test which event actually happened. - if (bits & WIFI_CONNECTED_BIT) { - ESP_LOGI(TAG, "Connected to AP: %s", SSID); - return 0; - - } else if (bits & WIFI_FAIL_BIT) { - ESP_LOGE(TAG, "Failed to connect to SSID %s", SSID); - } else { - ESP_LOGE(TAG, "UNEXPECTED EVENT"); - } -*/ - - return 0; -} +static const char *TAG = "WiFi"; +#include "esp_log.h" + + +#include "wifi.hpp" + +// Defines SSID and PASS +#include "wifi_cfg.hpp" + +static const int CONFIG_ESP_MAXIMUM_RETRY = 3; + + +#include "esp_wifi.h" +#include "freertos/event_groups.h" + +static EventGroupHandle_t s_wifi_event_group; + +#define WIFI_CONNECTED_BIT BIT0 +#define WIFI_FAIL_BIT BIT1 + +static void event_handler(void* arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) { + static int s_retry_num = 0; + + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { + esp_wifi_connect(); + + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { + ESP_LOGI(TAG, "Disconnected, retrying connection to AP"); + esp_wifi_connect(); + + } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { + ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data; + ESP_LOGI(TAG, "Got IP address: " IPSTR, IP2STR(&event->ip_info.ip)); + s_retry_num = 0; + xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); + } +} + +int config_wifi() { + ESP_LOGI(TAG, "Connecting to %s", SSID); + + s_wifi_event_group = xEventGroupCreate(); + + ESP_ERROR_CHECK(esp_netif_init()); + + ESP_ERROR_CHECK(esp_event_loop_create_default()); + esp_netif_create_default_wifi_sta(); + + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + esp_event_handler_instance_t instance_any_id; + esp_event_handler_instance_t instance_got_ip; + ESP_ERROR_CHECK(esp_event_handler_instance_register( + WIFI_EVENT, + ESP_EVENT_ANY_ID, + &event_handler, + NULL, + &instance_any_id + )); + ESP_ERROR_CHECK(esp_event_handler_instance_register( + IP_EVENT, + IP_EVENT_STA_GOT_IP, + &event_handler, + NULL, + &instance_got_ip + )); + + wifi_config_t wifi_config = { + .sta = { + {.ssid = SSID}, + {.password = PASS}, + }, + }; + + ESP_ERROR_CHECK( esp_wifi_set_mode(WIFI_MODE_STA) ); + ESP_ERROR_CHECK( esp_wifi_set_config(WIFI_IF_STA, &wifi_config) ); + ESP_ERROR_CHECK( esp_wifi_start() ); + +/* + // Waiting until either the connection is established (WIFI_CONNECTED_BIT) + // or connection failed for the maximum number of re-tries (WIFI_FAIL_BIT). + // The bits are set by event_handler() (see above) + EventBits_t bits = xEventGroupWaitBits( + s_wifi_event_group, + WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, + pdFALSE, + pdFALSE, + portMAX_DELAY + ); + + // xEventGroupWaitBits() returns the bits before the call returned, hence we + // can test which event actually happened. + if (bits & WIFI_CONNECTED_BIT) { + ESP_LOGI(TAG, "Connected to AP: %s", SSID); + return 0; + + } else if (bits & WIFI_FAIL_BIT) { + ESP_LOGE(TAG, "Failed to connect to SSID %s", SSID); + } else { + ESP_LOGE(TAG, "UNEXPECTED EVENT"); + } +*/ + + return 0; +} diff --git a/main/wifi.hpp b/main/wifi.hpp index 8e9a0a0..3afc9d3 100644 --- a/main/wifi.hpp +++ b/main/wifi.hpp @@ -1,3 +1,3 @@ -#pragma once - -int config_wifi(); \ No newline at end of file +#pragma once + +int config_wifi(); From fff36d84b67a6cf2a8f539a9cb5e9c33993c72f7 Mon Sep 17 00:00:00 2001 From: jrhoffa Date: Mon, 26 Sep 2022 09:30:21 -1000 Subject: [PATCH 10/16] Add LCD display virtual LED support --- CMakeLists.txt | 2 ++ main/CMakeLists.txt | 2 ++ main/display.cpp | 80 ++++++++++++++++++++++++++++++++++++++++++++ main/display.hpp | 28 ++++++++++++++++ main/main.cpp | 3 +- main/screen_leds.cpp | 79 +++++++++++++++++++++++++++++++++++++++++++ main/screen_leds.hpp | 15 +++++++++ 7 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 main/display.cpp create mode 100644 main/display.hpp create mode 100644 main/screen_leds.cpp create mode 100644 main/screen_leds.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index fa2060e..92d652a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,8 @@ cmake_minimum_required(VERSION 3.16) include($ENV{IDF_PATH}/tools/cmake/project.cmake) +include($ENV{IOT_SOLUTION_PATH}/component.cmake) + project(blinky) target_add_binary_data(blinky.elf "main/broker.pem" TEXT) diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 07abcfd..93d7397 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -11,6 +11,8 @@ idf_component_register( "leds.cpp" "presets.cpp" "spi_leds.cpp" + "screen_leds.cpp" + "display.cpp" "patterns/gradient.cpp" "patterns/random.cpp" "patterns/sparkle.cpp" diff --git a/main/display.cpp b/main/display.cpp new file mode 100644 index 0000000..5de5f03 --- /dev/null +++ b/main/display.cpp @@ -0,0 +1,80 @@ +static const char *TAG = "display"; +#include + + + +#include "display.hpp" + + +Display::Display(size_t w, size_t h) : + width(w), height(h), pitch(w), buffer(NULL), + bus(NULL), iface_drv(NULL), driver{.deinit = NULL} + { + esp_err_t ret; + + spi_config_t bus_cfg = { + .miso_io_num = GPIO_NUM_4, + .mosi_io_num = GPIO_NUM_35, + .sclk_io_num = GPIO_NUM_36, + .max_transfer_sz = (int)(height * width * sizeof(*buffer) + 8), + }; + + bus = spi_bus_create(SPI2_HOST, &bus_cfg); + if (NULL == bus) { + ESP_LOGE(TAG, "Failed to acquire bus"); + return; + } + + scr_interface_spi_config_t spi_dev_cfg = { + .spi_bus = bus, + .pin_num_cs = 34, + .pin_num_dc = 37, + .clk_freq = 40 * 1000 * 1000, + .swap_data = 1, + }; + + ret = scr_interface_create(SCREEN_IFACE_SPI, &spi_dev_cfg, &iface_drv); + if (ESP_OK != ret) { + ESP_LOGE(TAG, "Failed to create interface"); + return; + } + + ret = scr_find_driver(SCREEN_CONTROLLER_ST7789, &driver); + if (ESP_OK != ret) { + ESP_LOGE(TAG, "Failed to find driver"); + return; + } + + scr_controller_config_t lcd_cfg = { + .interface_drv = iface_drv, + .pin_num_rst = 38, + .pin_num_bckl = 33, + .rst_active_level = 0, + .bckl_active_level = 1, + .width = (uint16_t)width, + .height = (uint16_t)height, + .offset_hor = 52, + .offset_ver = 40, + .rotate = SCR_DIR_LRTB, + }; + + ret = driver.init(&lcd_cfg); + if (ESP_OK != ret) { + ESP_LOGE(TAG, "Failed to initialize driver"); + return; + } + + buffer = new uint16_t[height * pitch](); + refresh(); +} + +Display::~Display() { + if (buffer) delete[] buffer; + if (driver.deinit) driver.deinit(); + if (iface_drv) scr_interface_delete(iface_drv); + if (bus) spi_bus_delete(&bus); +} + +void Display::refresh() { + driver.draw_bitmap(0, 0, width, height, buffer); +} diff --git a/main/display.hpp b/main/display.hpp new file mode 100644 index 0000000..b458999 --- /dev/null +++ b/main/display.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include + + +class Display { +public: + Display(size_t w, size_t h); + ~Display(); + + void refresh(); + + size_t get_width() const { return width; } + size_t get_height() const { return height; } + size_t get_pitch() const { return pitch; } + size_t get_stride() const { return pitch * sizeof(*buffer); } + uint16_t* get_buffer() const { return buffer; } + +private: + size_t width; + size_t height; + size_t pitch; + uint16_t *buffer; + + spi_bus_handle_t bus; + scr_interface_driver_t *iface_drv; + scr_driver_t driver; +}; diff --git a/main/main.cpp b/main/main.cpp index 5cc1811..3e3b243 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -16,6 +16,7 @@ static const char *TAG = "blinky"; #include "presets.hpp" #include "utils.hpp" #include "spi_leds.hpp" +#include "screen_leds.hpp" // mDNS / NetBIOS @@ -176,7 +177,7 @@ extern "C" void app_main(void) { } else { ESP_LOGI(TAG, "Starting animation"); - int period_us = 1000000 / 60; + int period_us = 1000000 / 30; int64_t target_us = time_us(); int64_t profile_start = target_us; #ifdef PROFILE_PERF diff --git a/main/screen_leds.cpp b/main/screen_leds.cpp new file mode 100644 index 0000000..946e079 --- /dev/null +++ b/main/screen_leds.cpp @@ -0,0 +1,79 @@ +static const char *TAG = "screen_leds"; +#include + +#include "screen_leds.hpp" + + +static inline void fill(uint16_t *buffer, uint16_t value, int w, int h, int p = -1) { + if (p <= 0) p = w; + uint16_t *line = buffer; + while (--h >= 0) { + uint16_t *pixel = line; + for (int _w = w; _w > 0; --_w) { + *pixel = value; + ++pixel; + } + line += p; + } +} + +static inline void box(uint16_t *buffer, uint16_t value, int x, int y, int w, int h, int p = -1) { + if (p <= 0) p = w; + fill(buffer + x + (y * p), value, w, h, p); +} + +static inline uint16_t rgb16(int r, int g, int b) { + return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((uint8_t)b >> 3); +} + +static inline uint16_t color_to_565(Color c) { + return rgb16(c.r, c.g, c.b); +} + +ScreenLEDs::ScreenLEDs(int length) : LEDStrip(length), display(135, 240) {} + +void ScreenLEDs::length_changing(int length) { + fill(display.get_buffer(), COLOR_BLACK, display.get_width(), display.get_height()); +} + +void ScreenLEDs::show() { + const int width = display.get_width(); + const int height = display.get_height(); + const int pitch = display.get_pitch(); + auto buffer = display.get_buffer(); + + const size_t size = ((width + height) * 2) / (length + 4); + + int lx = 0, ly = 0; + int dx = 0, dy = size; + for (size_t l = 0; l < length; ++l) { + if (ly + dy > height) { + ly -= dy; + dx = size; + dy = 0; + lx += dx; + } else if (ly < 0) { + ly -= dy; + dx = -size; + dy = 0; + lx += dx; + } else if (lx + dx > width) { + lx -= dx; + dx = 0; + dy = -size; + ly += dy; + } else if (lx < 0) { + lx -= dx; + dx = 0; + dy = size; + ly += dy; + } + + box(buffer, color_to_565(pixels[l]), lx, ly, size, size, pitch); + + lx += dx; + ly += dy; + } + + display.refresh(); +} diff --git a/main/screen_leds.hpp b/main/screen_leds.hpp new file mode 100644 index 0000000..7277968 --- /dev/null +++ b/main/screen_leds.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include "leds.hpp" +#include "display.hpp" + + +class ScreenLEDs : public LEDStrip { +public: + ScreenLEDs(int length = 0); + void show(); + void length_changing(int length); + +private: + Display display; +}; From a772aba0127f65ac9ab5685a3a9ffcadc2247fbb Mon Sep 17 00:00:00 2001 From: jrhoffa Date: Thu, 3 Nov 2022 22:27:36 -0700 Subject: [PATCH 11/16] Add reboot command via MQTT --- main/device.cpp | 55 ++++++++++++++++++++++++++++++++++++------------- main/device.hpp | 6 +++--- 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/main/device.cpp b/main/device.cpp index 6aafeef..a0ae804 100644 --- a/main/device.cpp +++ b/main/device.cpp @@ -8,9 +8,7 @@ static const char *TAG = "device"; Device::Device(std::string _id) : id(_id), - state_topic("light/" + id + "/state"), - cmd_topic("light/" + id + "/cmd"), - data_topic("light/" + id + "/data/+"), + topic_prefix("light/" + id), ready(false), should_publish(false), mutex(xSemaphoreCreateMutex()), sem(xSemaphoreCreateBinary()), @@ -44,11 +42,9 @@ void Device::on_mqtt_connect(esp_mqtt_client_handle_t client) { ESP_LOGI(TAG, "Connected to MQTT"); - 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()); + std::string topic = topic_prefix + "/#"; + if (esp_mqtt_client_subscribe(client, topic.c_str(), 0) < 0) { + ESP_LOGE(TAG, "Failed to subscribe to %s", topic.c_str()); } // TODO: Re-announce when presets change @@ -58,8 +54,8 @@ void Device::on_mqtt_connect(esp_mqtt_client_handle_t client) { cJSON_AddStringToObject(device, "name", id.c_str()); cJSON *identifiers = cJSON_AddArrayToObject(device, "identifiers"); cJSON_AddItemToArray(identifiers, cJSON_CreateString(id.c_str())); - cJSON_AddStringToObject(json, "state_topic", state_topic.c_str()); - cJSON_AddStringToObject(json, "command_topic", cmd_topic.c_str()); + cJSON_AddStringToObject(json, "state_topic", (topic_prefix + "/state").c_str()); + cJSON_AddStringToObject(json, "command_topic", (topic_prefix + "/cmd").c_str()); cJSON_AddStringToObject(json, "schema", "json"); cJSON_AddBoolToObject(json, "effect", true); cJSON *effects = cJSON_AddArrayToObject(json, "effect_list"); @@ -86,10 +82,29 @@ void Device::on_mqtt_connect(esp_mqtt_client_handle_t client) { xSemaphoreGive(mutex); } +std::string Device::subtopic(const std::string &topic) { + auto slash_pos = topic.find('/', topic_prefix.length()); + if (slash_pos == std::string::npos) return ""; + auto another_slash_pos = topic.find('/', slash_pos + 1); + if (slash_pos == std::string::npos) { + // No further levels + return topic.substr(slash_pos + 1); + } + // First subtopic + return topic.substr(slash_pos + 1, another_slash_pos - (slash_pos + 1)); +} + 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"); - if (topic == cmd_topic) { + std::string command = subtopic(topic); + + ESP_LOGI(TAG, "Received command via MQTT: %s", command.c_str()); + + if (command == "state") { + // This is from us, just ignore it + return; + + } else if (command == "cmd") { cJSON *json = cJSON_ParseWithLength(event->data, event->data_len); if (json) { set_json_config(json); @@ -97,8 +112,14 @@ void Device::on_mqtt_message(esp_mqtt_client_handle_t, esp_mqtt_event_handle_t e } else { ESP_LOGE(TAG, "Invalid JSON data"); } + // Fall through & kick semaphore + + } else if (command == "reboot") { + ESP_LOGI(TAG, "Rebooting immediately"); + esp_restart(); + return; - } else /* data topic */ { + } else if (command == "data") { size_t slash = topic.rfind('/'); if (slash != std::string::npos) { write_file( @@ -106,7 +127,13 @@ void Device::on_mqtt_message(esp_mqtt_client_handle_t, esp_mqtt_event_handle_t e event->data, event->data_len ); } + // Fall through & kick semaphore + + } else { + ESP_LOGE(TAG, "Unhandled command: %s (%d bytes)", command.c_str(), event->data_len); + return; } + xSemaphoreGive(sem); } @@ -123,7 +150,7 @@ void Device::publish_state_locked() { cJSON *json = make_json_config_locked(); char *config = cJSON_PrintUnformatted(json); ESP_ERROR_CHECK(esp_mqtt_client_publish( - client, state_topic.c_str(), config, 0, 0, 0 + client, (topic_prefix + "/state").c_str(), config, 0, 0, 0 )); cJSON_free(config); cJSON_Delete(json); diff --git a/main/device.hpp b/main/device.hpp index a40ba95..ef3404c 100644 --- a/main/device.hpp +++ b/main/device.hpp @@ -33,9 +33,7 @@ public: private: std::string id; - std::string state_topic; - std::string cmd_topic; - std::string data_topic; + std::string topic_prefix; bool ready, should_publish; @@ -59,4 +57,6 @@ private: void on_mqtt_message(esp_mqtt_client_handle_t, esp_mqtt_event_handle_t); void publish_state_locked(); + + std::string subtopic(const std::string &topic); }; From 94f93d88e83072a4a7790f3e7177f46b5a47897b Mon Sep 17 00:00:00 2001 From: jrhoffa Date: Fri, 4 Nov 2022 19:25:21 -0700 Subject: [PATCH 12/16] Actually turn off LEDs when state set to off --- main/leds.cpp | 8 ++++++++ main/leds.hpp | 1 + main/main.cpp | 9 +++++++-- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/main/leds.cpp b/main/leds.cpp index bf67917..6ede02e 100644 --- a/main/leds.cpp +++ b/main/leds.cpp @@ -1,6 +1,8 @@ static const char *TAG = "leds"; #include +#include + #include "leds.hpp" #include "utils.hpp" @@ -18,6 +20,7 @@ void LEDStrip::update_state() { } void LEDStrip::setLength(int _length) { + clear(); length_changing(_length); delete[] pixels; length = _length; @@ -29,6 +32,11 @@ void LEDStrip::step() { if (pattern) pattern->step(pixels, length, state); } +void LEDStrip::clear() { + memset(pixels, 0, length * sizeof(*pixels)); + show(); +} + static std::string termcolor(Color c) { return "\x1b[48;2;" + std::to_string((int)c.r) + ";" + diff --git a/main/leds.hpp b/main/leds.hpp index 3a23040..2770ac9 100644 --- a/main/leds.hpp +++ b/main/leds.hpp @@ -34,6 +34,7 @@ public: ~LEDStrip() { if (state) delete state; } void step(); + void clear(); virtual void show() = 0; virtual void length_changing(int) {}; diff --git a/main/main.cpp b/main/main.cpp index 3e3b243..1795378 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -129,7 +129,11 @@ extern "C" void app_main(void) { cJSON_Delete(json); } - LEDStrip *LEDs = new SPI_LEDs(39); //new TerminalLEDs(); + // TerminalLEDs() + // ScreenLEDs() + // SPI_LEDs(39); + LEDStrip *LEDs = new SPI_LEDs(39); + int frequency = 60; while (true) { // Trash the old preset in case we can't find the set effect @@ -172,12 +176,13 @@ extern "C" void app_main(void) { if (!pattern) ESP_LOGW(TAG, "No LED pattern set"); ESP_LOGI(TAG, "Waiting for new config"); + LEDs->clear(); device.wait(); } else { ESP_LOGI(TAG, "Starting animation"); - int period_us = 1000000 / 30; + int period_us = 1000000 / frequency; int64_t target_us = time_us(); int64_t profile_start = target_us; #ifdef PROFILE_PERF From 19cda3ec5ad4c1ed7183830b5781c888dd6612e3 Mon Sep 17 00:00:00 2001 From: jrhoffa Date: Fri, 4 Nov 2022 22:23:25 -0700 Subject: [PATCH 13/16] Change OTA from HTTP to TFTP By doing this and eliminating mDNS, we save loads of RAM. --- main/main.cpp | 31 +------- main/ota.cpp | 208 ++++++++++++++++++++++++++------------------------ main/ota.hpp | 5 +- 3 files changed, 110 insertions(+), 134 deletions(-) diff --git a/main/main.cpp b/main/main.cpp index 1795378..895cfce 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -19,27 +19,6 @@ static const char *TAG = "blinky"; #include "screen_leds.hpp" -// mDNS / NetBIOS - -#include -#include - -static void start_mdns_service(const char *hostname) { - // Initialize mDNS service - ESP_ERROR_CHECK(mdns_init()); - - // Set hostname - mdns_hostname_set(hostname); - - // Set default instance - mdns_instance_name_set("Blinky Lights"); - - // NetBIOS too - netbiosns_init(); - netbiosns_set_name(hostname); -} - - // Dummy FS static void start_filesystem() { @@ -106,16 +85,10 @@ extern "C" void app_main(void) { // WiFi config_wifi(); - // mDNS - start_mdns_service("blinky-jr"); - - // HTTP Server - httpd_handle_t server = start_webserver(); - // OTA Server - start_ota_serv(server); + start_ota_serv(); - // Dummy Filesystem + // Flash Filesystem start_filesystem(); // TODO: Scrape this out log_dir("/spiffs"); diff --git a/main/ota.cpp b/main/ota.cpp index 13cf9a9..6dd399d 100644 --- a/main/ota.cpp +++ b/main/ota.cpp @@ -1,17 +1,16 @@ static const char *TAG = "ota"; #include -#include #include +#include #include #include -#include "ota.hpp" +#include +#include "ota.hpp" -// The real version probably lives somewhere else. Too lazy to find it -template static inline T MIN(T a, T b) { return a < b ? a : b; } // TODO: Stick this somewhere useful? static void reboot_soon_task(void*) { @@ -30,134 +29,141 @@ static void reboot_soon() { ); } -// HTTP Server Helpers. -// TODO: Share these? -static inline esp_err_t serv_err(httpd_req_t *req, const char *msg) { - ESP_LOGE(TAG, "%s", msg); - return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, msg); -} - -static inline esp_err_t serv_progress(httpd_req_t *req, int progress) { - ESP_LOGI(TAG, "%d%% complete", progress); - /* - char msg[100]; - snprintf(msg, sizeof(msg), "%d%% complete\n", progress); - httpd_resp_set_status(req, "102 Processing"); - httpd_resp_set_type(req, HTTPD_TYPE_TEXT); - return httpd_resp_send(req, msg, HTTPD_RESP_USE_STRLEN); - */ - return ESP_OK; -} - - -// OTA POST -static esp_err_t ota_post_handler(httpd_req_t *req) -{ - esp_err_t err; - - const esp_partition_t *part = esp_ota_get_next_update_partition(NULL); - if (!part) return serv_err(req, "Could not find update partition"); - int remaining = req->content_len; - ESP_LOGI(TAG, "Starting OTA: %d bytes", remaining); +class OTA_Session { +public: + OTA_Session() : + part(esp_ota_get_next_update_partition(NULL)), + ota(0), failed(false), total_written(0) + { + if (!part) { + ESP_LOGE(TAG, "Could not find update partition"); + failed = true; + return; + } - esp_ota_handle_t ota; - err = esp_ota_begin(part, remaining, &ota); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_ota_begin: %d", err); - return serv_err(req, "Could not start OTA"); + auto err = esp_ota_begin(part, OTA_SIZE_UNKNOWN, &ota); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ota_begin: %d", err); + failed = true; + return; + } } - constexpr int progress_step = 10; - int next_progress = 0; - constexpr size_t CHUNK_MAX = 4096; - char *chunk_buf = new char[CHUNK_MAX]; - while (remaining > 0) { - int chunk_size = MIN((unsigned int)remaining, CHUNK_MAX); - - int nread = httpd_req_recv(req, chunk_buf, chunk_size); - if (nread <= 0) { /* 0 return value indicates connection closed */ - ESP_LOGE(TAG, "httpd_req_recv(%d): %d", chunk_size, nread); + ~OTA_Session() { + if (failed) { + ESP_LOGE(TAG, "Aborting OTA due to previous failure"); esp_ota_abort(ota); - delete[] chunk_buf; - - /* Check if timeout occurred */ - if (nread == HTTPD_SOCK_ERR_TIMEOUT) { - /* In case of timeout one can choose to retry calling - * httpd_req_recv(), but to keep it simple, here we - * respond with an HTTP 408 (Request Timeout) error */ - httpd_resp_send_408(req); - } - - /* In case of error, returning ESP_FAIL will - * ensure that the underlying socket is closed */ - return ESP_FAIL; + return; } - err = esp_ota_write(ota, chunk_buf, nread); + ESP_LOGI(TAG, "Finalizing OTA (%d bytes)", total_written); + auto err = esp_ota_end(ota); if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_ota_write(%d): %d", nread, err); + ESP_LOGE(TAG, "esp_ota_end: %d", err); esp_ota_abort(ota); - delete[] chunk_buf; - return serv_err(req, "Failed to write OTA chunk"); + return; + } + + ESP_LOGI(TAG, "Setting new boot partition"); + err = esp_ota_set_boot_partition(part); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ota_set_boot_partition: %d", err); + return; } - remaining -= nread; + ESP_LOGI(TAG, "OTA Successful"); + + reboot_soon(); + } - int progress = 100 * (req->content_len - remaining) / req->content_len; - if (progress >= next_progress) { - serv_progress(req, progress); - next_progress += progress_step; + bool write(const void *data, int len) { + if (failed) return false; + auto err = esp_ota_write(ota, data, len); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ota_write(%d): %d", len, err); + failed = true; + return false; } + total_written += len; + return true; } - delete[] chunk_buf; - ESP_LOGI(TAG, "Finalizing OTA"); +private: + const esp_partition_t* part; + esp_ota_handle_t ota; + bool failed; + int total_written; +}; - err = esp_ota_end(ota); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_ota_end: %d", err); - esp_ota_abort(ota); - return serv_err(req, "Failed to finalize OTA"); + +static void* ota_open(const char *filename, const char *mode, u8_t write) { + if (strcmp(mode, "octet") != 0) { + ESP_LOGE(TAG, "Unexpected mode: %s", mode); + return NULL; } - ESP_LOGI(TAG, "Setting new boot partition"); - err = esp_ota_set_boot_partition(part); - if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_ota_set_boot_partition: %d", err); - return serv_err(req, "Failed to set partition"); + if (strcmp(filename, "blinky.bin") != 0) { + ESP_LOGE(TAG, "Unexpected filename: %s", mode); + return NULL; } - ESP_LOGI(TAG, "OTA Successful"); - httpd_resp_sendstr(req, "OTA Successful\n"); + if (!write) { + ESP_LOGE(TAG, "Illegal read attempt"); + return NULL; + } - reboot_soon(); + ESP_LOGI(TAG, "Starting OTA"); - return ESP_FAIL; + return new OTA_Session(); } +static void ota_close(void* handle) { + OTA_Session *session = (OTA_Session*)handle; + if (session) delete session; + else ESP_LOGE(TAG, "Attempt to close invalid session"); +} -int start_ota_serv(httpd_handle_t server) { - ESP_LOGI(TAG, "Starting OTA server"); +static int ota_read(void* handle, void* buf, int len) { + ESP_LOGE(TAG, "Super illegal read of %d bytes", len); + return -1; +} - // Register handlers (move these to components) - httpd_uri_t uri_ota_post = { - .uri = "/ota", - .method = HTTP_POST, - .handler = ota_post_handler, - .user_ctx = NULL - }; - ESP_ERROR_CHECK(httpd_register_uri_handler(server, &uri_ota_post)); +static int ota_write(void* handle, struct pbuf* p) { + OTA_Session *session = (OTA_Session*)handle; + if (!session) { + ESP_LOGE(TAG, "Attempt to write to invalid session"); + return -1; + } - // mDNS Entry - mdns_service_add(NULL, "_http", "_tcp", 80, NULL, 0); - mdns_service_instance_name_set("_http", "_tcp", "ESP32S2 OTA"); + for (; p; p = p->next) { + if (!session->write(p->payload, p->len)) { + ESP_LOGE(TAG, "Failed to write %d bytes", p->len); + return -1; + } + } + + return 0; +} + + +static tftp_context ota_context = { + .open = ota_open, + .close = ota_close, + .read = ota_read, + .write = ota_write +}; + +int start_ota_serv() { + ESP_LOGI(TAG, "Starting OTA server"); + + ESP_ERROR_CHECK(tftp_init(&ota_context)); return 0; } int stop_ota_serv() { - // TODO - return -1; + tftp_cleanup(); + return 0; } diff --git a/main/ota.hpp b/main/ota.hpp index ef126c5..143a136 100644 --- a/main/ota.hpp +++ b/main/ota.hpp @@ -1,7 +1,4 @@ #pragma once -#include - - -int start_ota_serv(httpd_handle_t); +int start_ota_serv(); int stop_ota_serv(); From b82f7900a364bfccfbcb495ddb24c817b22369ea Mon Sep 17 00:00:00 2001 From: jrhoffa Date: Fri, 4 Nov 2022 22:27:40 -0700 Subject: [PATCH 14/16] Avoid spurios SPI error with 0-length transactions --- main/spi_leds.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/main/spi_leds.cpp b/main/spi_leds.cpp index d312d60..174347d 100644 --- a/main/spi_leds.cpp +++ b/main/spi_leds.cpp @@ -107,6 +107,9 @@ static uint8_t* encode_byte(uint8_t *out, uint8_t in) { } void SPI_LEDs::show() { + // We generate a spurious SPI error with a 0-length transaction. + if (length <= 0) return; + static int index = 0; int ret; const Color *pixel = pixels; From 751d5f5751e1b067c27290ba9c487d70e582cbbf Mon Sep 17 00:00:00 2001 From: jrhoffa Date: Sun, 27 Nov 2022 18:16:51 -0800 Subject: [PATCH 15/16] Add lwIP TFTP support patch for esp-idf --- 0001-Enable-lwIP-TFTP-server-support.patch | 46 ++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 0001-Enable-lwIP-TFTP-server-support.patch diff --git a/0001-Enable-lwIP-TFTP-server-support.patch b/0001-Enable-lwIP-TFTP-server-support.patch new file mode 100644 index 0000000..211781e --- /dev/null +++ b/0001-Enable-lwIP-TFTP-server-support.patch @@ -0,0 +1,46 @@ +From 963e5981b7e9212068ee6589983f5cd0e217f7b1 Mon Sep 17 00:00:00 2001 +From: jrhoffa +Date: Sun, 27 Nov 2022 18:05:45 -0800 +Subject: [PATCH] Enable lwIP TFTP server support + +--- + components/lwip/CMakeLists.txt | 4 ++++ + components/lwip/Kconfig | 7 +++++++ + 2 files changed, 11 insertions(+) + +diff --git a/components/lwip/CMakeLists.txt b/components/lwip/CMakeLists.txt +index acd587de92..40dcb7f4f4 100644 +--- a/components/lwip/CMakeLists.txt ++++ b/components/lwip/CMakeLists.txt +@@ -148,6 +148,10 @@ if(CONFIG_LWIP_DHCPS) + list(APPEND srcs "apps/dhcpserver/dhcpserver.c") + endif() + ++if(CONFIG_LWIP_TFTPS) ++ list(APPEND srcs "lwip/src/apps/tftp/tftp_server.c") ++endif() ++ + idf_component_register(SRCS "${srcs}" + INCLUDE_DIRS "${include_dirs}" + LDFRAGMENTS linker.lf +diff --git a/components/lwip/Kconfig b/components/lwip/Kconfig +index 57e8c2b045..5016394397 100644 +--- a/components/lwip/Kconfig ++++ b/components/lwip/Kconfig +@@ -871,6 +871,13 @@ menu "LWIP" + + endmenu # LWIP RAW API + ++ menu "TFTP" ++ config LWIP_TFTPS ++ bool "Enable TFTP server" ++ default n ++ ++ endmenu # TFTP ++ + menu "SNTP" + + config LWIP_SNTP_MAX_SERVERS +-- +2.25.1 + From b90187af904c11aaa5277542e49f950c15c43692 Mon Sep 17 00:00:00 2001 From: Nathaniel Walizer Date: Sun, 27 Nov 2022 21:50:34 -0800 Subject: [PATCH 16/16] Fix high ouput timing to 417/833 ns widths This has the side effect of lengthening the low time proportionally, to a total of 417 ns longer than necessary. This does not appear to have any observable negative impact. --- main/main.cpp | 2 +- main/spi_leds.cpp | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/main/main.cpp b/main/main.cpp index 895cfce..35ff7c7 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -106,7 +106,7 @@ extern "C" void app_main(void) { // ScreenLEDs() // SPI_LEDs(39); LEDStrip *LEDs = new SPI_LEDs(39); - int frequency = 60; + int frequency = 30; while (true) { // Trash the old preset in case we can't find the set effect diff --git a/main/spi_leds.cpp b/main/spi_leds.cpp index 174347d..641fd96 100644 --- a/main/spi_leds.cpp +++ b/main/spi_leds.cpp @@ -54,7 +54,9 @@ SPI_LEDs::SPI_LEDs(int _gpio, int length) : .cs_ena_posttrans = 0, // moot // Datasheet implies 3.2 MHz => 1.25 us / 4 // We can get away with 4.4_ MHz => 900 ns / 4 - .clock_speed_hz = 3200000, + // HOWEVER, better implementations divide 1.25 us into thirds! + // Thus, 2.4 MHz has the same H times and slightly longer L times. + .clock_speed_hz = 2400000, .input_delay_ns = 0, // moot .spics_io_num = -1, .flags = 0, // Keep it MSB first