Use CIE L* (psychometric lightness) mapped from luminance (assumes uniform luminance for R, G, and B channels) Predefined colors are assumed to be LED valuesS3
| @@ -0,0 +1,4 @@ | |||||
| { | |||||
| "gamma": [0,0,0,0,0,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,4,4,4,4,4,4,5,5,5,5,5,6,6,6,6,6,7,7,7,7,8,8,8,8,9,9,9,10,10,10,10,11,11,11,12,12,12,13,13,13,14,14,15,15,15,16,16,17,17,17,18,18,19,19,20,20,21,21,22,22,23,23,24,24,25,25,26,26,27,28,28,29,29,30,31,31,32,32,33,34,34,35,36,37,37,38,39,39,40,41,42,43,43,44,45,46,47,47,48,49,50,51,52,53,54,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,70,71,72,73,74,75,76,77,79,80,81,82,83,85,86,87,88,90,91,92,94,95,96,98,99,100,102,103,105,106,108,109,110,112,113,115,116,118,120,121,123,124,126,128,129,131,132,134,136,138,139,141,143,145,146,148,150,152,154,155,157,159,161,163,165,167,169,171,173,175,177,179,181,183,185,187,189,191,193,196,198,200,202,204,207,209,211,214,216,218,220,223,225,228,230,232,235,237,240,242,245,247,250,252,255], | |||||
| "inv_gamma": [0,9,18,26,33,39,44,48,52,56,60,63,66,69,72,74,77,79,81,84,86,88,90,92,94,96,97,99,101,103,104,106,107,109,110,112,113,115,116,117,119,120,121,123,124,125,126,128,129,130,131,132,133,134,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,151,152,153,154,155,156,157,158,159,159,160,161,162,163,163,164,165,166,167,167,168,169,170,171,171,172,173,174,174,175,176,176,177,178,179,179,180,181,181,182,183,183,184,185,185,186,187,187,188,189,189,190,191,191,192,192,193,194,194,195,196,196,197,197,198,198,199,200,200,201,201,202,203,203,204,204,205,205,206,206,207,208,208,209,209,210,210,211,211,212,212,213,213,214,215,215,216,216,217,217,218,218,219,219,220,220,221,221,222,222,223,223,224,224,225,225,225,226,226,227,227,228,228,229,229,230,230,231,231,232,232,232,233,233,234,234,235,235,236,236,236,237,237,238,238,239,239,240,240,240,241,241,242,242,242,243,243,244,244,245,245,245,246,246,247,247,247,248,248,249,249,249,250,250,251,251,251,252,252,253,253,253,254,254,255,255] | |||||
| } | |||||
| @@ -9,6 +9,7 @@ idf_component_register( | |||||
| "device.cpp" | "device.cpp" | ||||
| "utils.cpp" | "utils.cpp" | ||||
| "leds.cpp" | "leds.cpp" | ||||
| "gamma.cpp" | |||||
| "presets.cpp" | "presets.cpp" | ||||
| "spi_leds.cpp" | "spi_leds.cpp" | ||||
| # "screen_leds.cpp" | # "screen_leds.cpp" | ||||
| @@ -12,7 +12,7 @@ Device::Device(std::string _id) : | |||||
| ready(false), | ready(false), | ||||
| mutex(xSemaphoreCreateMutex()), | mutex(xSemaphoreCreateMutex()), | ||||
| sem(xSemaphoreCreateBinary()), | sem(xSemaphoreCreateBinary()), | ||||
| power_on(false), strip_length(0), | |||||
| power_on(false), strip_length(0), brightness(255), | |||||
| client(start_mqtt_client(on_mqtt_connect, on_mqtt_message, this)) | client(start_mqtt_client(on_mqtt_connect, on_mqtt_message, this)) | ||||
| {} | {} | ||||
| @@ -29,7 +29,11 @@ void Device::set_json_config(const cJSON *json) { | |||||
| } | } | ||||
| const cJSON *length = cJSON_GetObjectItem(json, "length"); | const cJSON *length = cJSON_GetObjectItem(json, "length"); | ||||
| if (length && cJSON_IsNumber(length)) { | if (length && cJSON_IsNumber(length)) { | ||||
| strip_length = length->valuedouble; | |||||
| strip_length = length->valueint; | |||||
| } | |||||
| const cJSON *jbrightness = cJSON_GetObjectItem(json, "brightness"); | |||||
| if (jbrightness && cJSON_IsNumber(jbrightness)) { | |||||
| brightness = jbrightness->valueint; | |||||
| } | } | ||||
| publish_state_locked(); | publish_state_locked(); | ||||
| @@ -55,6 +59,11 @@ void Device::advertise_locked() { | |||||
| cJSON *effects = cJSON_AddArrayToObject(json, "effect_list"); | cJSON *effects = cJSON_AddArrayToObject(json, "effect_list"); | ||||
| for (auto iter = Presets::map_begin(); iter != Presets::map_end(); ++iter) | for (auto iter = Presets::map_begin(); iter != Presets::map_end(); ++iter) | ||||
| cJSON_AddItemToArray(effects, cJSON_CreateString(iter->first.c_str())); | cJSON_AddItemToArray(effects, cJSON_CreateString(iter->first.c_str())); | ||||
| cJSON_AddBoolToObject(json, "brightness", true); | |||||
| cJSON_AddStringToObject(json, "brightness_command_topic", (topic_prefix + "/cmd").c_str()); | |||||
| cJSON_AddStringToObject(json, "brightness_state_topic", (topic_prefix + "/state").c_str()); | |||||
| cJSON_AddStringToObject(json, "brightness_template", "{{ value_json.brightness }}"); | |||||
| cJSON_AddStringToObject(json, "brightness_value_template", "{\"brightness\": {{ value }} }"); | |||||
| char *config = cJSON_PrintUnformatted(json); | char *config = cJSON_PrintUnformatted(json); | ||||
| @@ -193,6 +202,7 @@ cJSON* Device::make_json_config_locked() const { | |||||
| cJSON_AddStringToObject(json, "state", power_on ? "ON" : "OFF"); | cJSON_AddStringToObject(json, "state", power_on ? "ON" : "OFF"); | ||||
| cJSON_AddStringToObject(json, "effect", effect.c_str()); | cJSON_AddStringToObject(json, "effect", effect.c_str()); | ||||
| cJSON_AddNumberToObject(json, "length", strip_length); | cJSON_AddNumberToObject(json, "length", strip_length); | ||||
| cJSON_AddNumberToObject(json, "brightness", brightness); | |||||
| return json; | return json; | ||||
| } | } | ||||
| @@ -27,6 +27,7 @@ public: | |||||
| const std::string& get_effect() const { return effect; } | const std::string& get_effect() const { return effect; } | ||||
| bool is_on() const { return power_on; } | bool is_on() const { return power_on; } | ||||
| int get_strip_length() const { return strip_length; } | int get_strip_length() const { return strip_length; } | ||||
| int get_brightness() const { return brightness; } | |||||
| cJSON* make_json_config_locked() const; | cJSON* make_json_config_locked() const; | ||||
| @@ -43,6 +44,7 @@ private: | |||||
| bool power_on; | bool power_on; | ||||
| std::string effect; | std::string effect; | ||||
| int strip_length; | int strip_length; | ||||
| int brightness; | |||||
| esp_mqtt_client_handle_t client; | esp_mqtt_client_handle_t client; | ||||
| @@ -0,0 +1,98 @@ | |||||
| static const char *TAG = "gamma"; | |||||
| #include <esp_log.h> | |||||
| #include <cJSON.h> | |||||
| #include "gamma.hpp" | |||||
| #include "utils.hpp" | |||||
| namespace Gamma { | |||||
| static uint8_t gamma[256]; | |||||
| static uint8_t inv_gamma[256]; | |||||
| static void default_table(uint8_t *table) { | |||||
| for (int val = 0; val < 256; ++val) *table++ = val; | |||||
| } | |||||
| static void json_table(cJSON *jarray, uint8_t *table) { | |||||
| if (jarray->type != cJSON_Array) { | |||||
| ESP_LOGE(TAG, "Not an array: %s", jarray->string); | |||||
| } | |||||
| int count = cJSON_GetArraySize(jarray); | |||||
| if (count != 256) { | |||||
| ESP_LOGE(TAG, "Wrong size for %s: %d", jarray->string, count); | |||||
| } | |||||
| uint8_t *ptr = table; | |||||
| cJSON *jnumber; | |||||
| cJSON_ArrayForEach(jnumber, jarray) { | |||||
| if (jnumber->type != cJSON_Number) { | |||||
| ESP_LOGE(TAG, "Not a number: %s", jnumber->string); | |||||
| default_table(table); | |||||
| return; | |||||
| } | |||||
| *ptr++ = (uint8_t)jnumber->valueint; | |||||
| } | |||||
| } | |||||
| static inline cJSON* load_json(const char *path) { | |||||
| return cJSON_Parse(read_file(path).c_str()); | |||||
| } | |||||
| void reload() { | |||||
| default_table(gamma); | |||||
| default_table(inv_gamma); | |||||
| cJSON *json = load_json("/spiffs/gamma.json"); | |||||
| if (!json) { | |||||
| ESP_LOGE(TAG, "No gamma values!"); | |||||
| return; | |||||
| } | |||||
| cJSON *jgamma = cJSON_GetObjectItem(json, "gamma"); | |||||
| if (!jgamma) { | |||||
| ESP_LOGE(TAG, "No gamma values!"); | |||||
| cJSON_Delete(json); | |||||
| return; | |||||
| } | |||||
| cJSON *jinv_gamma = cJSON_GetObjectItem(json, "inv_gamma"); | |||||
| if (!jinv_gamma) { | |||||
| ESP_LOGE(TAG, "No inverse gamma values!"); | |||||
| cJSON_Delete(json); | |||||
| return; | |||||
| } | |||||
| json_table(jgamma, gamma); | |||||
| json_table(jinv_gamma, inv_gamma); | |||||
| cJSON_Delete(json); | |||||
| } | |||||
| /* | |||||
| static inline uint8_t scale(int val, int num, int dem) { | |||||
| if (dem == 0) return 0; | |||||
| int val = (val * num) / dem; | |||||
| return (val >= 256 ? 255 : val); | |||||
| } | |||||
| */ | |||||
| static inline Color correct_single(Color c, int b) { | |||||
| return {gamma[(b * c.r) / 255], gamma[(b * c.g) / 255], gamma[(b * c.b) / 255]}; | |||||
| } | |||||
| void correct(Color *dst, const Color *src, int n, int b) { | |||||
| while(n-- > 0) *dst++ = correct_single(*src++, b); | |||||
| } | |||||
| Color wrong_single(Color c) { | |||||
| return {inv_gamma[c.r], inv_gamma[c.g], inv_gamma[c.b]}; | |||||
| } | |||||
| } // Gamma | |||||
| @@ -0,0 +1,14 @@ | |||||
| #pragma once | |||||
| #include "leds.hpp" | |||||
| namespace Gamma { | |||||
| void reload(); | |||||
| void correct(Color *dst, const Color *src, int len, int brightness = 255); | |||||
| Color wrong_single(Color); | |||||
| } // Gamma | |||||
| @@ -3,13 +3,15 @@ static const char *TAG = "leds"; | |||||
| #include <cstring> | #include <cstring> | ||||
| #include "gamma.hpp" | |||||
| #include "leds.hpp" | #include "leds.hpp" | ||||
| #include "utils.hpp" | #include "utils.hpp" | ||||
| LEDStrip::LEDStrip(int _length) : | LEDStrip::LEDStrip(int _length) : | ||||
| length(_length), pattern(NULL), | |||||
| length(_length), brightness(255), pattern(NULL), | |||||
| pixels(new Color[_length]), | pixels(new Color[_length]), | ||||
| colors(new Color[_length]), | |||||
| state(NULL) | state(NULL) | ||||
| {} | {} | ||||
| @@ -23,13 +25,16 @@ void LEDStrip::setLength(int _length) { | |||||
| clear(); | clear(); | ||||
| length_changing(_length); | length_changing(_length); | ||||
| delete[] pixels; | delete[] pixels; | ||||
| delete[] colors; | |||||
| length = _length; | length = _length; | ||||
| pixels = new Color[_length]; | pixels = new Color[_length]; | ||||
| colors = new Color[_length]; | |||||
| update_state(); | update_state(); | ||||
| } | } | ||||
| void LEDStrip::step() { | void LEDStrip::step() { | ||||
| if (pattern) pattern->step(pixels, length, state); | |||||
| if (pattern) pattern->step(colors, length, state); | |||||
| Gamma::correct(pixels, colors, length, brightness); | |||||
| } | } | ||||
| void LEDStrip::clear() { | void LEDStrip::clear() { | ||||
| @@ -45,10 +45,14 @@ public: | |||||
| void setLength(int length); | void setLength(int length); | ||||
| int getLength() const { return length; } | int getLength() const { return length; } | ||||
| void setBrightness(int _brightness) { brightness = _brightness; } | |||||
| protected: | protected: | ||||
| int length; | int length; | ||||
| int brightness; | |||||
| const Pattern *pattern; | const Pattern *pattern; | ||||
| Color *pixels; | Color *pixels; | ||||
| Color *colors; | |||||
| private: | private: | ||||
| void update_state(); | void update_state(); | ||||
| @@ -13,6 +13,7 @@ static const char *TAG = "blinky"; | |||||
| #include "device.hpp" | #include "device.hpp" | ||||
| #include "utils.hpp" | #include "utils.hpp" | ||||
| #include "leds.hpp" | #include "leds.hpp" | ||||
| #include "gamma.hpp" | |||||
| #include "presets.hpp" | #include "presets.hpp" | ||||
| #include "utils.hpp" | #include "utils.hpp" | ||||
| #include "spi_leds.hpp" | #include "spi_leds.hpp" | ||||
| @@ -111,6 +112,8 @@ extern "C" void app_main(void) { | |||||
| int frequency = 30; | int frequency = 30; | ||||
| while (true) { | while (true) { | ||||
| Gamma::reload(); | |||||
| // Trash the old preset in case we can't find the set effect | // Trash the old preset in case we can't find the set effect | ||||
| LEDs->setPattern(NULL); | LEDs->setPattern(NULL); | ||||
| Presets::reload(); | Presets::reload(); | ||||
| @@ -121,9 +124,12 @@ extern "C" void app_main(void) { | |||||
| const std::string effect = device.get_effect(); | const std::string effect = device.get_effect(); | ||||
| bool is_on = device.is_on(); | bool is_on = device.is_on(); | ||||
| int length = device.get_strip_length(); | int length = device.get_strip_length(); | ||||
| int brightness = device.get_brightness(); | |||||
| cJSON *json = device.make_json_config_locked(); | cJSON *json = device.make_json_config_locked(); | ||||
| device.unlock(); | device.unlock(); | ||||
| LEDs->setBrightness(brightness); | |||||
| if (length != LEDs->getLength()) { | if (length != LEDs->getLength()) { | ||||
| LEDs->setLength(length); | LEDs->setLength(length); | ||||
| } | } | ||||
| @@ -26,9 +26,9 @@ public: | |||||
| Gradient *_gradient, | Gradient *_gradient, | ||||
| int _cycle_length=20, int _cycle_time_ms=-1, | int _cycle_length=20, int _cycle_time_ms=-1, | ||||
| bool _reverse=false, bool _march=false | bool _reverse=false, bool _march=false | ||||
| ) : cycle_length(_cycle_length), | |||||
| ) : cycle_length(_cycle_length > 0 ? _cycle_length : _gradient->n_colors), | |||||
| cycle_time_ms( _cycle_time_ms < 0 ? | cycle_time_ms( _cycle_time_ms < 0 ? | ||||
| _cycle_length * 500 : _cycle_time_ms | |||||
| cycle_length * 500 : _cycle_time_ms | |||||
| ), | ), | ||||
| reverse(_reverse), march(_march), gradient(_gradient) | reverse(_reverse), march(_march), gradient(_gradient) | ||||
| {} | {} | ||||
| @@ -97,8 +97,7 @@ void GradientPattern::step(Color pixels[], int len, Pattern::State *_state) cons | |||||
| // Don't make a major animation jump if it's been terribly long | // Don't make a major animation jump if it's been terribly long | ||||
| if (duration_us > 100000) duration_us = 100000; | if (duration_us > 100000) duration_us = 100000; | ||||
| int offset_delta = (duration_us << shift) / (cycle_time_ms * 1000); | |||||
| int offset_delta = (cycle_time_ms <= 0 ? 0 : (duration_us << shift) / (cycle_time_ms * 1000)); | |||||
| if (reverse) state->offset += offset_delta; else state->offset -= offset_delta; | if (reverse) state->offset += offset_delta; else state->offset -= offset_delta; | ||||
| Fixed off = march ? | Fixed off = march ? | ||||
| (((int)state->offset * cycle_length) & ~((1 << shift) - 1)) / cycle_length : | (((int)state->offset * cycle_length) & ~((1 << shift) - 1)) / cycle_length : | ||||
| @@ -134,5 +133,27 @@ Pattern* json_gradient_pattern(cJSON *pattern, const Presets::ColorMap &colors) | |||||
| gradient->colors[i++] = color; | gradient->colors[i++] = color; | ||||
| } | } | ||||
| return new GradientPattern(gradient); | |||||
| int cycle_length = 20; | |||||
| cJSON *jlength = cJSON_GetObjectItem(pattern, "length"); | |||||
| if (jlength) { | |||||
| if (jlength->type != cJSON_Number) { | |||||
| ESP_LOGE(TAG, "Not a number: %s", jlength->string); | |||||
| } else { | |||||
| cycle_length = jlength->valueint; | |||||
| } | |||||
| } | |||||
| int cycle_time_ms = -1; | |||||
| cJSON *jduration = cJSON_GetObjectItem(pattern, "duration"); | |||||
| if (jduration) { | |||||
| if (jduration->type != cJSON_Number) { | |||||
| ESP_LOGE(TAG, "Not a number: %s", jduration->string); | |||||
| } else { | |||||
| cycle_time_ms = jduration->valuedouble * 1000.; | |||||
| } | |||||
| } | |||||
| // TODO: Reverse, March | |||||
| return new GradientPattern(gradient, cycle_length, cycle_time_ms); | |||||
| } | } | ||||
| @@ -3,6 +3,7 @@ static const char *TAG = "presets"; | |||||
| #include <cJSON.h> | #include <cJSON.h> | ||||
| #include "gamma.hpp" | |||||
| #include "presets.hpp" | #include "presets.hpp" | ||||
| #include "utils.hpp" | #include "utils.hpp" | ||||
| @@ -115,7 +116,7 @@ void reload() { | |||||
| cJSON *jcolor; | cJSON *jcolor; | ||||
| cJSON_ArrayForEach(jcolor, jcolors) { | cJSON_ArrayForEach(jcolor, jcolors) { | ||||
| Color color = json_color(jcolor, colors); | Color color = json_color(jcolor, colors); | ||||
| colors[jcolor->string] = color; | |||||
| colors[jcolor->string] = Gamma::wrong_single(color); | |||||
| } | } | ||||
| cJSON_Delete(jcolors); | cJSON_Delete(jcolors); | ||||
| } | } | ||||