diff --git a/README.md b/README.md index 906d8981..977b9f56 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ - Added `bitmap_small` and `rainbow_bitmap_small` screen. - Added a pseudo-icon `blank` - empty icon, no display. - Added screen with scroll icon along with long text, `icon_text_screen`, `rainbow_icon_text_screen`. +- Added `bitmap_stack`screen. Screen that allows you to display from 1 to 64 icons described in the configuration. ### EspHoMaTriX 2023.9.0 - Added the ability to display graph as defined in the YAML file @@ -768,6 +769,7 @@ Numerous features are accessible with services from home assistant and lambdas t |`rainbow_bitmap_small`|"icon", "text", "lifetime", "screen_time", "default_font"|show 8x8 image as text, and text in rainbow colors| |`icon_text_screen`|"icon_name", "text", "lifetime", "screen_time", "default_font", "r", "g", "b"|show the specified icon with text and scroll icon along with long text| |`rainbow_icon_text_screen`|"icon_name", "text", "lifetime", "screen_time", "default_font"|show the specified icon with text in rainbow color and scroll icon along with long text| +|`bitmap_stack`|"icons", "lifetime", "screen_time"|show or scroll from 1 to 64 icons described in the configuration| #### Parameter description @@ -775,6 +777,7 @@ Numerous features are accessible with services from home assistant and lambdas t - **size**: The size of the rindicator or alarm, 1-3 - **percent**: values from 0..100 - **icon_name**: the id of the icon to show, as defined in the YAML file (or pseudo-icon `blank` - empty icon), it is also possible to set the arbitrary [screen identifier](#screen_id), for example `icon_name|screen_id` +- **icons**: the list of id of the icon to show, as defined in the YAML file, like: icon1,icon2. - **text**: a text message to display - **lifetime**: how long does this screen stay in the queue (minutes) - **screen_time**: how long is this screen display in the loop (seconds). For short text without scrolling it is shown the defined time, longer text is scrolled at least `scroll_count` times. @@ -1036,6 +1039,7 @@ For example, if you have multiple icons named weather_sunny, weather_rain & weat |MODE_RAINBOW_BITMAP_SMALL| 20| |MODE_ICON_TEXT_SCREEN| 21| |MODE_RAINBOW_ICON_TEXT_SCREEN| 22| +|MODE_BITMAP_STACK_SCREEN| 23| **(D)** Service **display_on** / **display_off** @@ -1345,6 +1349,31 @@ sensor:             } ``` +### bitmap_stack example + +``` +ehmtxv2: + icons: + - id: skull + lameid: 11241 +``` + +``` +service: esphome.esp_hall_pixel_clock_bitmap_stack +data: + icons: "skull,skull,skull" + lifetime: 1 + screen_time: 10 +``` + +``` +service: esphome.esp_hall_pixel_clock_bitmap_stack +data: + icons: "skull,skull,skull|two" + lifetime: 1 + screen_time: 10 +``` + ## Breaking changes ### 2023.8.0 diff --git a/components/ehmtxv2/EHMTX.cpp b/components/ehmtxv2/EHMTX.cpp index 25c9f849..6b10fb36 100644 --- a/components/ehmtxv2/EHMTX.cpp +++ b/components/ehmtxv2/EHMTX.cpp @@ -2,6 +2,7 @@ #include #include #include +#include namespace esphome { @@ -334,6 +335,101 @@ namespace esphome } screen->status(); } + + std::string trim(std::string const& s) + { + auto const first{ s.find_first_not_of(' ') }; + if (first == std::string::npos) + return {}; + auto const last{ s.find_last_not_of(' ') }; + return s.substr(first, (last - first + 1)); + } + + void EHMTX::bitmap_stack(std::string icons, int lifetime, int screen_time) + { + icons.erase(remove(icons.begin(), icons.end(), ' '), icons.end()); + + std::string ic = get_icon_name(icons); + std::string id = ""; + + if (icons.find("|") != std::string::npos) + { + id = get_screen_id(icons); + } + + std::stringstream stream(ic); + std::string icon; + std::vector tokens; + uint8_t count = 0; + + while (std::getline(stream, icon, ',')) + { + icon = trim(icon); + if (!icon.empty()) + { + tokens.push_back(icon); + count++; + } + if (count >= 64) + { + break; + } + } + if (count == 0) + { + ESP_LOGW(TAG, "bitmap stack: icons list [%s] empty", ic.c_str()); + return; + } + + EHMTX_queue *screen = this->find_mode_queue_element(MODE_BITMAP_STACK_SCREEN); + if (screen->sbitmap == NULL) + { + screen->sbitmap = new Color[64]; + } + + uint8_t real_count = 0; + for (uint8_t i = 0; i < count; i++) + { + uint8_t icon = this->find_icon(tokens[i].c_str()); + + if (icon == MAXICONS) + { + ESP_LOGW(TAG, "icon %d/%s not found => skip", icon, tokens[i].c_str()); + for (auto *t : on_icon_error_triggers_) + { + t->process(tokens[i]); + } + } + else + { + screen->sbitmap[real_count] = Color(127, 255, icon, 5); // int16_t 32767 = uint8_t(127,255) + real_count++; + } + } + if (real_count == 0) + { + delete [] screen->sbitmap; + screen->sbitmap = nullptr; + + ESP_LOGW(TAG, "bitmap stack: icons list [%s] does not contain any known icons.", ic.c_str()); + return; + } + + screen->icon = real_count; + screen->mode = MODE_BITMAP_STACK_SCREEN; + screen->icon_name = id; + screen->text = ic; + screen->progress = (id == "two") ? 1 : 0; // 0 - one side scroll (right to left), 1 - two side (outside to center) if supported + screen->default_font = false; + screen->calc_scroll_time(screen->icon, screen_time); + screen->endtime = this->get_tick() + (lifetime > 0 ? lifetime * 60000.0 : screen->screen_time_); + for (auto *t : on_add_screen_triggers_) + { + t->process(screen->text, (uint8_t)screen->mode); + } + ESP_LOGD(TAG, "bitmap stack: has %d icons from: [%s] screen_time: %d", screen->icon, icons.c_str(), screen_time); + screen->status(); + } #endif #ifdef USE_ESP8266 @@ -349,6 +445,10 @@ namespace esphome { ESP_LOGW(TAG, "bitmap_screen_rainbow is not available on ESP8266"); } + void EHMTX::bitmap_stack(std::string i, int l, int s) + { + ESP_LOGW(TAG, "bitmap_stack is not available on ESP8266"); + } #endif uint8_t EHMTX::find_icon(std::string name) @@ -534,6 +634,7 @@ namespace esphome register_service(&EHMTX::bitmap_screen, "bitmap_screen", {"icon", "lifetime", "screen_time"}); register_service(&EHMTX::bitmap_small, "bitmap_small", {"icon", "text", "lifetime", "screen_time", "default_font", "r", "g", "b"}); register_service(&EHMTX::rainbow_bitmap_small, "rainbow_bitmap_small", {"icon", "text", "lifetime", "screen_time", "default_font"}); + register_service(&EHMTX::bitmap_stack, "bitmap_stack", {"icons", "lifetime", "screen_time"}); #endif #ifdef USE_Fireplugin @@ -783,11 +884,14 @@ namespace esphome break; case MODE_BITMAP_SMALL: case MODE_RAINBOW_BITMAP_SMALL: - infotext = ("BITMAP_SMALL:" + this->queue[i]->icon_name).c_str(); + infotext = ("BITMAP_SMALL: " + this->queue[i]->icon_name).c_str(); break; case MODE_BITMAP_SCREEN: infotext = "BITMAP"; break; + case MODE_BITMAP_STACK_SCREEN: + infotext = ("BITMAP_STACK: " + this->queue[i]->text).c_str(); + break; case MODE_FIRE: infotext = "FIRE"; break; @@ -882,7 +986,16 @@ namespace esphome { this->queue[this->screen_pointer]->last_time = ts + this->queue[this->screen_pointer]->screen_time_; // todo nur bei animationen - if (this->queue[this->screen_pointer]->icon < this->icon_count) + if (this->queue[this->screen_pointer]->mode == MODE_BITMAP_STACK_SCREEN && this->queue[this->screen_pointer]->sbitmap != NULL) + { + for (uint8_t i = 0; i < this->queue[this->screen_pointer]->icon; i++) + { + this->icons[this->queue[this->screen_pointer]->sbitmap[i].b]->set_frame(0); + this->queue[this->screen_pointer]->sbitmap[i] = Color(127, 255, this->queue[this->screen_pointer]->sbitmap[i].b, 5); + this->queue[this->screen_pointer]->default_font = false; + } + } + else if (this->queue[this->screen_pointer]->icon < this->icon_count) { this->icons[this->queue[this->screen_pointer]->icon]->set_frame(0); } diff --git a/components/ehmtxv2/EHMTX.h b/components/ehmtxv2/EHMTX.h index 5948f524..e8caa0b7 100644 --- a/components/ehmtxv2/EHMTX.h +++ b/components/ehmtxv2/EHMTX.h @@ -53,7 +53,8 @@ enum show_mode : uint8_t MODE_ICON_PROGRESS = 19, MODE_RAINBOW_BITMAP_SMALL = 20, MODE_ICON_TEXT_SCREEN = 21, - MODE_RAINBOW_ICON_TEXT_SCREEN = 22 + MODE_RAINBOW_ICON_TEXT_SCREEN = 22, + MODE_BITMAP_STACK_SCREEN = 23 }; namespace esphome @@ -214,6 +215,8 @@ namespace esphome void icon_text_screen(std::string icon, std::string text, int lifetime = D_LIFETIME, int screen_time = D_SCREEN_TIME, bool default_font = true, int r = C_RED, int g = C_GREEN, int b = C_BLUE); void rainbow_icon_text_screen(std::string icon, std::string text, int lifetime = D_LIFETIME, int screen_time = D_SCREEN_TIME, bool default_font = true); + void bitmap_stack(std::string icons, int lifetime = D_LIFETIME, int screen_time = D_SCREEN_TIME); + void bitmap_screen(std::string text, int lifetime = D_LIFETIME, int screen_time = D_SCREEN_TIME); void color_gauge(std::string text); void bitmap_small(std::string icon, std::string text, int lifetime = D_LIFETIME, int screen_time = D_SCREEN_TIME, bool default_font = true, int r = C_RED, int g = C_GREEN, int b = C_BLUE); @@ -293,8 +296,12 @@ namespace esphome bool update_slot(uint8_t _icon); void update_screen(); void hold_slot(uint8_t _sec); - void calc_scroll_time(std::string, uint16_t); + void calc_scroll_time(std::string text, uint16_t screen_time); + void calc_scroll_time(uint8_t icon_count, uint16_t screen_time); int xpos(); + int xpos(uint8_t item); + int ypos(); + int ypos(uint8_t item); }; class EHMTXNextScreenTrigger : public Trigger diff --git a/components/ehmtxv2/EHMTX_queue.cpp b/components/ehmtxv2/EHMTX_queue.cpp index a5c862ee..b7cb9de2 100644 --- a/components/ehmtxv2/EHMTX_queue.cpp +++ b/components/ehmtxv2/EHMTX_queue.cpp @@ -159,6 +159,9 @@ namespace esphome case MODE_RAINBOW_BITMAP_SMALL: ESP_LOGD(TAG, "queue: rainbow small bitmap for: %.1f sec", this->screen_time_ / 1000.0); break; + case MODE_BITMAP_STACK_SCREEN : + ESP_LOGD(TAG, "queue: bitmap stack for: %.1f sec", this->screen_time_ / 1000.0); + break; #endif default: @@ -232,6 +235,132 @@ namespace esphome return result; } + void c16to8(int16_t t, uint8_t& r, uint8_t& g) + { + r = static_cast((t & 0xFF00) >> 8); + g = static_cast(t & 0x00FF); + } + + int16_t c8to16(uint8_t r, uint8_t g) + { + return (static_cast(r) << 8) | g; + } + + uint8_t is_tick(int step, uint8_t& state) + { + if (step % 2 == state) + { + return 0; + } + state = step % 2; + return 1; + } + + int EHMTX_queue::xpos(uint8_t item) + { + uint8_t width = 32; + int result = width - this->config_->scroll_step + item * 9; + + if (this->icon < 5) + { + int16_t item_pos = c8to16(this->sbitmap[item].r, this->sbitmap[item].g); + + uint8_t target = round(((static_cast(width) - 8 * static_cast(this->icon)) / static_cast(this->icon + 1)) * (item + 1) + 8 * item); + if ((this->progress == 1) && (this->icon == 2 || this->icon == 3)) + { + uint8_t reverse_steps = round(((static_cast(width) - 8 * static_cast(this->icon)) / static_cast(this->icon + 1)) + 8); + + if (ceil((this->config_->next_action_time - this->config_->get_tick()) / EHMTXv2_SCROLL_INTERVALL) > reverse_steps) + { + result = (item + 1 == this->icon) ? width - this->config_->scroll_step : -8 + this->config_->scroll_step; + if (item == 0 && (item_pos == 32767 || item_pos < target)) + { + item_pos = result < target ? result : target; + } + else if (item + 1 == this->icon && item_pos > target) + { + item_pos = result > target ? result : target; + } + else if (this->icon == 3 && item == 1) + { + item_pos = target; + } + } + else + { + if (item == 0) + { + item_pos -= is_tick(this->config_->scroll_step, this->sbitmap[item].w); + } + else if (item + 1 == this->icon) + { + item_pos += is_tick(this->config_->scroll_step, this->sbitmap[item].w); + } + else if (this->icon == 3 && item == 1) + { + item_pos = target; + } + } + } + else if ((this->progress == 1) && this->icon == 1) + { + item_pos = target; + } + else + { + uint8_t reverse_steps = round(((static_cast(width) - 8 * static_cast(this->icon)) / static_cast(this->icon + 1)) * this->icon + 8 * (this->icon + 1)) + 8; + + if (ceil((this->config_->next_action_time - this->config_->get_tick()) / EHMTXv2_SCROLL_INTERVALL) > reverse_steps) + { + if (item_pos > target) + { + item_pos = result > target ? result : target; + } + } + else + { + item_pos -= is_tick(this->config_->scroll_step, this->sbitmap[item].w); + } + } + + c16to8(item_pos, this->sbitmap[item].r, this->sbitmap[item].g); + result = item_pos; + } + + return result; + } + + int EHMTX_queue::ypos() + { + uint8_t height = 8; + if (this->config_->scroll_step > height) + { + return 0; + } + return this->config_->scroll_step - height; + } + + int EHMTX_queue::ypos(uint8_t item) + { + uint8_t height = 8; + + if ((this->progress == 1) && (this->icon == 1 || (this->icon == 3 && item == 1))) + { + if (ceil((this->config_->next_action_time - this->config_->get_tick()) / EHMTXv2_SCROLL_INTERVALL) > height) + { + if (this->default_font) + { + return 0; + } + this->default_font = this->config_->scroll_step >= height; + return this->config_->scroll_step - height; + } + return height - round((this->config_->next_action_time - this->config_->get_tick()) / EHMTXv2_SCROLL_INTERVALL); + } + + return 0; + } + void EHMTX_queue::update_screen() { if (millis() - this->config_->last_rainbow_time >= EHMTXv2_RAINBOW_INTERVALL) @@ -247,7 +376,25 @@ namespace esphome this->config_->last_rainbow_time = millis(); } - if (this->icon < this->config_->icon_count) + if (this->mode == MODE_BITMAP_STACK_SCREEN && this->sbitmap != NULL) + { + uint32_t average_frame_duration = 0; + for (uint8_t i = 0; i < this->icon; i++) + { + average_frame_duration += this->config_->icons[this->sbitmap[i].b]->frame_duration; + } + average_frame_duration = average_frame_duration / this->icon; + + if (millis() - this->config_->last_anim_time >= average_frame_duration) + { + for (uint8_t i = 0; i < this->icon; i++) + { + this->config_->icons[this->sbitmap[i].b]->next_frame(); + } + this->config_->last_anim_time = millis(); + } + } + else if (this->icon < this->config_->icon_count) { if (millis() - this->config_->last_anim_time >= this->config_->icons[this->icon]->frame_duration) { @@ -659,6 +806,21 @@ namespace esphome #endif break; + case MODE_BITMAP_STACK_SCREEN: +#ifndef USE_ESP8266 + if (this->sbitmap != NULL) + { + for (uint8_t i = 0; i < this->icon; i++) + { + if (this->sbitmap[i].b != BLANKICON) + { + this->config_->display->image(this->xpos(i), this->ypos(i), this->config_->icons[this->sbitmap[i].b]); + } + } + } +#endif + break; + #ifdef USE_Fireplugin case MODE_FIRE: { @@ -833,5 +995,33 @@ namespace esphome ESP_LOGD(TAG, "calc_scroll_time: mode: %d text: \"%s\" pixels %d calculated: %.1f defined: %d max_steps: %d", this->mode, text.c_str(), this->pixels_, this->screen_time_ / 1000.0, screen_time, this->scroll_reset); } - + + // Icons count, Screen time in seconds + void EHMTX_queue::calc_scroll_time(uint8_t icon_count, uint16_t screen_time) + { + float display_duration; + float requested_time = 1000.0 * screen_time; + + uint8_t width = 32; + uint8_t startx = 0; + uint16_t max_steps = 0; + + this->pixels_ = 9 * icon_count; + + if (this->pixels_ < 32) + { + this->screen_time_ = requested_time; + } + else + { + max_steps = EHMTXv2_SCROLL_COUNT * (width - startx) + EHMTXv2_SCROLL_COUNT * this->pixels_; + display_duration = static_cast(max_steps * EHMTXv2_SCROLL_INTERVALL); + this->screen_time_ = (display_duration > requested_time) ? display_duration : requested_time; + } + + this->scroll_reset = (width - startx) + this->pixels_; + + ESP_LOGD(TAG, "calc_scroll_time: mode: %d icons count: %d pixels %d calculated: %.1f defined: %d max_steps: %d", this->mode, icon_count, this->pixels_, this->screen_time_ / 1000.0, screen_time, this->scroll_reset); + } + } diff --git a/components/ehmtxv2/__init__.py b/components/ehmtxv2/__init__.py index 069b7acf..1dd18863 100644 --- a/components/ehmtxv2/__init__.py +++ b/components/ehmtxv2/__init__.py @@ -252,7 +252,7 @@ def rgb565_888(v565): } ), cv.Optional(CONF_NIGHT_MODE_SCREENS, default=DEFAULT_NIGHT_MODE_SCREENS): cv.All( - cv.ensure_list(cv.one_of(1, 2, 3, 4, 5, 9, 10, 12, 13, 14, 15, 16, 17, 18, 19)), cv.Length(min=1, max=5) + cv.ensure_list(cv.one_of(1, 2, 3, 4, 5, 9, 10, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23)), cv.Length(min=1, max=5) ), cv.Required(CONF_ICONS): cv.All( cv.ensure_list(