From 6253480f30bcfb42d46f1ddde5856ffa29c05c1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=BE=D0=B1=D0=B5=D0=BB=D0=B5=D0=B2=20=D0=90=D0=BD?= =?UTF-8?q?=D0=B4=D1=80=D0=B5=D0=B9=20=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B5?= =?UTF-8?q?=D0=B2=D0=B8=D1=87?= Date: Thu, 12 Mar 2026 00:40:06 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=B8=D1=81=D0=BF=D0=BB=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/extensions.json | 3 +- lib/epdiy/src/highlevel.c | 12 +--- lib/epdiy/src/render.c | 2 +- partitions.csv | 5 ++ platformio.ini | 2 +- src/app/application.cpp | 36 ++++++++++- src/app/application.h | 2 + src/app/services/display_service.cpp | 56 ++++++++++++++++- src/app/services/display_service.h | 1 + src/ui/dashboard_screen.cpp | 23 ++++--- src/ui/views/battery_view.cpp | 75 +++++++++++++++++++++- src/ui/views/battery_view.h | 7 ++- src/ui/views/calendar_view.cpp | 58 +++++++++-------- src/ui/views/weather_view.cpp | 75 +++++++++++++++++++++- src/ui/views/weather_view.h | 7 ++- taskfile.yml | 94 ++++++++++++++++++++++++++++ 16 files changed, 400 insertions(+), 58 deletions(-) create mode 100644 partitions.csv create mode 100644 taskfile.yml diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 080e70d..8057bc7 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,7 +1,6 @@ { - // See http://go.microsoft.com/fwlink/?LinkId=827846 - // for the documentation about the extensions.json format "recommendations": [ + "pioarduino.pioarduino-ide", "platformio.platformio-ide" ], "unwantedRecommendations": [ diff --git a/lib/epdiy/src/highlevel.c b/lib/epdiy/src/highlevel.c index f8eab6a..f212806 100644 --- a/lib/epdiy/src/highlevel.c +++ b/lib/epdiy/src/highlevel.c @@ -264,11 +264,6 @@ enum EpdDrawError epd_hl_update_area( uint32_t t1 = esp_timer_get_time() / 1000; - diff_area.x = 0; - diff_area.y = 0; - diff_area.width = epd_width(); - diff_area.height = epd_height(); - enum EpdDrawError err = EPD_DRAW_SUCCESS; err = epd_draw_base( epd_full_screen(), @@ -283,11 +278,6 @@ enum EpdDrawError epd_hl_update_area( uint32_t t2 = esp_timer_get_time() / 1000; - diff_area.x = 0; - diff_area.y = 0; - diff_area.width = epd_width(); - diff_area.height = epd_height(); - int buf_width = epd_width(); for (int l = diff_area.y; l < diff_area.y + diff_area.height; l++) { @@ -357,4 +347,4 @@ void epd_hl_waveform(EpdiyHighlevelState* state, const EpdWaveform* waveform) { waveform = epd_get_display()->default_waveform; } state->waveform = waveform; -} \ No newline at end of file +} diff --git a/lib/epdiy/src/render.c b/lib/epdiy/src/render.c index eaba217..6cbaf84 100644 --- a/lib/epdiy/src/render.c +++ b/lib/epdiy/src/render.c @@ -469,7 +469,7 @@ EpdRect epd_difference_image_base( break; } for (max_x = x_end - 1; max_x >= crop_to.x; max_x--) { - uint8_t mask = min_x % 2 ? 0xF0 : 0x0F; + uint8_t mask = max_x % 2 ? 0xF0 : 0x0F; if ((col_dirtyness[max_x / 2] & mask) != 0) break; } diff --git a/partitions.csv b/partitions.csv new file mode 100644 index 0000000..17878a6 --- /dev/null +++ b/partitions.csv @@ -0,0 +1,5 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +phy_init, data, phy, 0xE000, 0x1000, +factory, app, factory, 0x10000, 0x280000, +spiffs, data, spiffs, 0x290000,0x170000, diff --git a/platformio.ini b/platformio.ini index 0a82b6b..e6355e4 100644 --- a/platformio.ini +++ b/platformio.ini @@ -3,11 +3,11 @@ platform = https://github.com/pioarduino/platform-espressif32/releases/download/ board = esp32dev framework = arduino board_build.f_cpu = 240000000L +board_build.partitions = partitions.csv monitor_speed = 115200 lib_deps = gyverlibs/GyverNTP@^1.3.1 bblanchon/ArduinoJson@^7.4.3 build_flags = -DBOARD_HAS_PSRAM - -DCONFIG_EPD_DISPLAY_TYPE_ED047TC1 -DCONFIG_EPD_BOARD_REVISION_LILYGO_T5_47 diff --git a/src/app/application.cpp b/src/app/application.cpp index e40e5e4..b785a6e 100644 --- a/src/app/application.cpp +++ b/src/app/application.cpp @@ -76,6 +76,15 @@ void Application::run_update_cycle() { DashboardData data; RenderPlan plan; + if (force_full_refresh_once_) { + plan.full_refresh = true; + plan.redraw_calendar = true; + plan.redraw_weather = true; + plan.redraw_battery = true; + force_full_refresh_once_ = false; + Serial.println("Cold start: форсируем полное обновление экрана"); + } + const bool wifi_ok = connectivity_.connect_wifi(kWifiConnectTimeoutMs); if (!wifi_ok) { Serial.println("Wi-Fi недоступен, пропускаем NTP и погоду"); @@ -125,8 +134,26 @@ void Application::run_update_cycle() { Serial.println("Заряд не изменился"); } + const bool has_partial_redraw = !plan.full_refresh && (plan.redraw_weather || plan.redraw_battery); + if (FULL_REFRESH_EVERY_N_PARTIAL_UPDATES > 0 && has_partial_redraw && + (partial_updates_since_full_ + 1 >= FULL_REFRESH_EVERY_N_PARTIAL_UPDATES)) { + plan.full_refresh = true; + plan.redraw_calendar = true; + plan.redraw_weather = true; + plan.redraw_battery = true; + Serial.printf("Гигиенический full refresh: достигнут порог частичных обновлений (%u)\n", + (unsigned)FULL_REFRESH_EVERY_N_PARTIAL_UPDATES); + } + if (plan.full_refresh || plan.redraw_weather || plan.redraw_battery) { dashboard_.render_pass(data, plan); + + if (plan.full_refresh) { + partial_updates_since_full_ = 0; + } else { + partial_updates_since_full_++; + Serial.printf("Частичных обновлений с последнего full refresh: %u\n", (unsigned)partial_updates_since_full_); + } } if (data.weather.valid && (plan.full_refresh || plan.redraw_weather)) { @@ -149,9 +176,16 @@ void Application::setup() { log_separator(); SleepService::print_wakeup_reason(); + const esp_sleep_wakeup_cause_t wakeup_reason = esp_sleep_get_wakeup_cause(); + const bool woke_by_s2_button = (wakeup_reason == ESP_SLEEP_WAKEUP_EXT0); + force_full_refresh_once_ = !g_rtc_state.initialized || woke_by_s2_button; init_rtc_state_if_needed(); - display_.begin(EPD_ROT_PORTRAIT, EPD_BUILTIN_WAVEFORM, MODE_GC16, MODE_DU); + if (woke_by_s2_button) { + Serial.println("S2 wakeup: форсируем полную перерисовку дисплея"); + } + + display_.begin(EPD_ROT_PORTRAIT, EPD_BUILTIN_WAVEFORM, MODE_GC16, MODE_GC16); dashboard_.layout_pass(display_.width(), display_.height()); handle_wakeup_source(); diff --git a/src/app/application.h b/src/app/application.h index 0cf423a..4a008e4 100644 --- a/src/app/application.h +++ b/src/app/application.h @@ -32,6 +32,8 @@ private: WeatherService weather_service_; BatteryService battery_service_; ui::DashboardScreen dashboard_; + bool force_full_refresh_once_ = false; + uint16_t partial_updates_since_full_ = 0; const gpio_num_t wake_button_pin_ = GPIO_NUM_39; }; diff --git a/src/app/services/display_service.cpp b/src/app/services/display_service.cpp index 39fe384..68934eb 100644 --- a/src/app/services/display_service.cpp +++ b/src/app/services/display_service.cpp @@ -2,11 +2,50 @@ #include +#include "settings.h" + namespace app { +namespace { +EpdRect align_area_for_partial(const EpdRect& area, const int max_w, const int max_h) { + int x0 = area.x < 0 ? 0 : area.x; + int y0 = area.y < 0 ? 0 : area.y; + int x1 = area.x + area.width; + int y1 = area.y + area.height; + + if (x1 > max_w) x1 = max_w; + if (y1 > max_h) y1 = max_h; + if (x1 <= x0 || y1 <= y0) { + return {.x = x0, .y = y0, .width = 0, .height = 0}; + } + + // 4-bit packed framebuffer is more stable when partial regions are byte-aligned. + x0 = (x0 / 8) * 8; + x1 = ((x1 + 7) / 8) * 8; + if (x1 > max_w) x1 = max_w; + + return { + .x = x0, + .y = y0, + .width = x1 - x0, + .height = y1 - y0, + }; +} +} // namespace + void DisplayService::begin(EpdRotation orientation, const EpdWaveform* waveform, const EpdDrawMode full_mode, const EpdDrawMode partial_mode) { - epd_init(&epd_board_lilygo_t5_47, &ED047TC1, EPD_LUT_64K); +#if LILYGO_T5_47_PANEL_PROFILE == 1 + const EpdDisplay_t* display_profile = &ED047TC1; + const char* display_profile_name = "ED047TC1"; +#elif LILYGO_T5_47_PANEL_PROFILE == 2 + const EpdDisplay_t* display_profile = &ED047TC2; + const char* display_profile_name = "ED047TC2"; +#else +#error "LILYGO_T5_47_PANEL_PROFILE must be 1 (TC1) or 2 (TC2)" +#endif + + epd_init(&epd_board_lilygo_t5_47, display_profile, EPD_LUT_64K); hl_ = epd_hl_init(waveform); epd_set_rotation(orientation); @@ -19,6 +58,7 @@ void DisplayService::begin(EpdRotation orientation, const EpdWaveform* waveform, epd_hl_set_all_white(&hl_); + Serial.printf("Display profile: %s\n", display_profile_name); Serial.printf("Screen(rotated): %dx%d\n", screen_width_, screen_height_); } @@ -55,6 +95,13 @@ void DisplayService::clear_framebuffer_area(const EpdRect& area) { epd_fill_rect(area, 0xFF, fb_); } +void DisplayService::hard_full_clear() { + refresh_panel_temperature(); + epd_poweron(); + epd_fullclear(&hl_, panel_temperature_); + epd_poweroff(); +} + bool DisplayService::update_full_screen() { refresh_panel_temperature(); @@ -77,12 +124,17 @@ bool DisplayService::update_full_screen() { } bool DisplayService::update_area(const EpdRect& area) { + const EpdRect aligned_area = align_area_for_partial(area, screen_width_, screen_height_); + if (aligned_area.width <= 0 || aligned_area.height <= 0) { + return true; + } + refresh_panel_temperature(); epd_poweron(); int tries = 0; while (tries < 3) { - const EpdDrawError draw_err = epd_hl_update_area(&hl_, partial_mode_, panel_temperature_, area); + const EpdDrawError draw_err = epd_hl_update_area(&hl_, partial_mode_, panel_temperature_, aligned_area); if (draw_err == EPD_DRAW_SUCCESS) { epd_poweroff(); return true; diff --git a/src/app/services/display_service.h b/src/app/services/display_service.h index 3c2924b..9d195fc 100644 --- a/src/app/services/display_service.h +++ b/src/app/services/display_service.h @@ -17,6 +17,7 @@ public: bool clear_area_hw(const EpdRect& area); void clear_framebuffer_all(); void clear_framebuffer_area(const EpdRect& area); + void hard_full_clear(); bool update_full_screen(); bool update_area(const EpdRect& area); diff --git a/src/ui/dashboard_screen.cpp b/src/ui/dashboard_screen.cpp index a06e8db..04a6436 100644 --- a/src/ui/dashboard_screen.cpp +++ b/src/ui/dashboard_screen.cpp @@ -14,8 +14,7 @@ void DashboardScreen::layout_pass(int screen_width, int screen_height) { void DashboardScreen::render_pass(const app::DashboardData& data, const app::RenderPlan& plan) { if (plan.full_refresh) { - epd_clear(); - display_.clear_framebuffer_all(); + display_.hard_full_clear(); const CalendarProps calendar_props{ .valid = data.calendar.valid, @@ -43,26 +42,30 @@ void DashboardScreen::render_pass(const app::DashboardData& data, const app::Ren } if (plan.redraw_weather && data.weather.valid) { - display_.clear_area_hw(weather_view_.bounds()); - display_.clear_framebuffer_area(weather_view_.bounds()); const WeatherProps weather_props{ .valid = true, .state_ru = app::WeatherService::weather_state_to_ru(data.weather.state), .temperature = data.weather.temperature, }; - weather_view_.render(display_.framebuffer(), weather_props); - display_.update_area(weather_view_.bounds()); + const EpdRect weather_area = weather_view_.dirty_bounds(weather_props, 5, 6); + if (weather_area.width > 0 && weather_area.height > 0) { + display_.clear_framebuffer_area(weather_area); + weather_view_.render(display_.framebuffer(), weather_props); + display_.update_area(weather_area); + } } if (plan.redraw_battery && data.battery.valid) { - display_.clear_area_hw(battery_view_.bounds()); - display_.clear_framebuffer_area(battery_view_.bounds()); const BatteryProps battery_props{ .valid = true, .percent = data.battery.percent, }; - battery_view_.render(display_.framebuffer(), battery_props); - display_.update_area(battery_view_.bounds()); + const EpdRect battery_area = battery_view_.dirty_bounds(battery_props, 4, 4); + if (battery_area.width > 0 && battery_area.height > 0) { + display_.clear_framebuffer_area(battery_area); + battery_view_.render(display_.framebuffer(), battery_props); + display_.update_area(battery_area); + } } } diff --git a/src/ui/views/battery_view.cpp b/src/ui/views/battery_view.cpp index 07805bc..b5cbb49 100644 --- a/src/ui/views/battery_view.cpp +++ b/src/ui/views/battery_view.cpp @@ -6,6 +6,34 @@ namespace ui { +namespace { +EpdRect union_rect(const EpdRect& a, const EpdRect& b) { + const int left = a.x < b.x ? a.x : b.x; + const int top = a.y < b.y ? a.y : b.y; + const int right_a = a.x + a.width; + const int right_b = b.x + b.width; + const int bottom_a = a.y + a.height; + const int bottom_b = b.y + b.height; + const int right = right_a > right_b ? right_a : right_b; + const int bottom = bottom_a > bottom_b ? bottom_a : bottom_b; + return {.x = left, .y = top, .width = right - left, .height = bottom - top}; +} + +EpdRect clamp_to_rect(const EpdRect& rect, const EpdRect& limit) { + const int left = rect.x > limit.x ? rect.x : limit.x; + const int top = rect.y > limit.y ? rect.y : limit.y; + const int right = (rect.x + rect.width) < (limit.x + limit.width) ? (rect.x + rect.width) : (limit.x + limit.width); + const int bottom = + (rect.y + rect.height) < (limit.y + limit.height) ? (rect.y + rect.height) : (limit.y + limit.height); + + if (right <= left || bottom <= top) { + return {.x = limit.x, .y = limit.y, .width = 0, .height = 0}; + } + + return {.x = left, .y = top, .width = right - left, .height = bottom - top}; +} +} // namespace + void BatteryView::layout(int screen_width, int screen_height) { const int margin = 14; const int width = 120; @@ -22,8 +50,50 @@ void BatteryView::layout(int screen_width, int screen_height) { text_y_ = screen_height - margin; } -void BatteryView::render(uint8_t* framebuffer, const BatteryProps& props) const { +EpdRect BatteryView::measure_text_bounds(const BatteryProps& props) const { + char text[24]; + snprintf(text, sizeof(text), "%d %%", props.percent); + + EpdFontProperties font_props = epd_font_properties_default(); + font_props.flags = EPD_DRAW_ALIGN_RIGHT; + + int x1 = 0; + int y1 = 0; + int w = 0; + int h = 0; + epd_get_text_bounds(&MartianMono12, text, &text_x_, &text_y_, &x1, &y1, &w, &h, &font_props); + + return {.x = x1, .y = y1, .width = w, .height = h}; +} + +EpdRect BatteryView::dirty_bounds(const BatteryProps& props, const int pad_x, const int pad_y) const { + EpdRect area = {.x = bounds_.x, .y = bounds_.y, .width = 0, .height = 0}; + + if (has_last_text_bounds_) { + area = last_text_bounds_; + } + + if (props.valid) { + const EpdRect current_bounds = measure_text_bounds(props); + area = has_last_text_bounds_ ? union_rect(area, current_bounds) : current_bounds; + } + + if (area.width <= 0 || area.height <= 0) { + return area; + } + + const EpdRect expanded = { + .x = area.x - pad_x, + .y = area.y - pad_y, + .width = area.width + (pad_x * 2), + .height = area.height + (pad_y * 2), + }; + return clamp_to_rect(expanded, bounds_); +} + +void BatteryView::render(uint8_t* framebuffer, const BatteryProps& props) { if (!props.valid) { + has_last_text_bounds_ = false; return; } @@ -36,6 +106,9 @@ void BatteryView::render(uint8_t* framebuffer, const BatteryProps& props) const int x = text_x_; int y = text_y_; epd_write_string(&MartianMono12, text, &x, &y, framebuffer, &font_props); + + last_text_bounds_ = measure_text_bounds(props); + has_last_text_bounds_ = (last_text_bounds_.width > 0 && last_text_bounds_.height > 0); } const EpdRect& BatteryView::bounds() const { diff --git a/src/ui/views/battery_view.h b/src/ui/views/battery_view.h index a099e78..d3bdb1f 100644 --- a/src/ui/views/battery_view.h +++ b/src/ui/views/battery_view.h @@ -12,13 +12,18 @@ struct BatteryProps { class BatteryView { public: void layout(int screen_width, int screen_height); - void render(uint8_t* framebuffer, const BatteryProps& props) const; + EpdRect dirty_bounds(const BatteryProps& props, int pad_x = 4, int pad_y = 4) const; + void render(uint8_t* framebuffer, const BatteryProps& props); const EpdRect& bounds() const; private: + EpdRect measure_text_bounds(const BatteryProps& props) const; + EpdRect bounds_ = {0, 0, 0, 0}; int text_x_ = 0; int text_y_ = 0; + EpdRect last_text_bounds_ = {0, 0, 0, 0}; + bool has_last_text_bounds_ = false; }; } // namespace ui diff --git a/src/ui/views/calendar_view.cpp b/src/ui/views/calendar_view.cpp index 3a50490..8c43c29 100644 --- a/src/ui/views/calendar_view.cpp +++ b/src/ui/views/calendar_view.cpp @@ -1,46 +1,52 @@ #include "ui/views/calendar_view.h" -#include "MartianMono30.h" #include "MartianMono120.h" +#include "MartianMono30.h" namespace ui { int CalendarView::clamp_int(int value, int min_v, int max_v) { - if (value < min_v) return min_v; - if (value > max_v) return max_v; - return value; + if (value < min_v) + return min_v; + if (value > max_v) + return max_v; + return value; } void CalendarView::layout(int screen_width, int screen_height) { - center_x_ = screen_width / 2; - day_y_ = (screen_height * 38) / 100; + center_x_ = screen_width / 2; + day_y_ = (screen_height * 28) / 100; - const int month_spacing = clamp_int(screen_height / 6, 72, 108); - const int week_spacing = clamp_int(screen_height / 14, 30, 54); + const int month_spacing = clamp_int(screen_height / 6, 62, 108); + const int week_spacing = clamp_int(screen_height / 14, 30, 54); - month_y_ = day_y_ + month_spacing; - week_y_ = month_y_ + week_spacing; + month_y_ = day_y_ + month_spacing; + week_y_ = month_y_ + week_spacing; } -void CalendarView::render(uint8_t* framebuffer, const CalendarProps& props) const { - if (!props.valid) { - return; - } +void CalendarView::render(uint8_t *framebuffer, + const CalendarProps &props) const { + if (!props.valid) { + return; + } - EpdFontProperties font_props = epd_font_properties_default(); - font_props.flags = EPD_DRAW_ALIGN_CENTER; + EpdFontProperties font_props = epd_font_properties_default(); + font_props.flags = EPD_DRAW_ALIGN_CENTER; - int day_x = center_x_; - int day_y = day_y_; - epd_write_string(&MartianMono120, props.day, &day_x, &day_y, framebuffer, &font_props); + int day_x = center_x_; + int day_y = day_y_; + epd_write_string(&MartianMono120, props.day, &day_x, &day_y, framebuffer, + &font_props); - int month_x = center_x_; - int month_y = month_y_; - epd_write_string(&MartianMono30, props.month, &month_x, &month_y, framebuffer, &font_props); + int month_x = center_x_; + int month_y = month_y_; + epd_write_string(&MartianMono30, props.month, &month_x, &month_y, framebuffer, + &font_props); - int week_x = center_x_; - int week_y = week_y_; - epd_write_string(&MartianMono30, props.weekday, &week_x, &week_y, framebuffer, &font_props); + int week_x = center_x_; + int week_y = week_y_; + epd_write_string(&MartianMono30, props.weekday, &week_x, &week_y, framebuffer, + &font_props); } -} // namespace ui +} // namespace ui diff --git a/src/ui/views/weather_view.cpp b/src/ui/views/weather_view.cpp index 6c876cb..455c554 100644 --- a/src/ui/views/weather_view.cpp +++ b/src/ui/views/weather_view.cpp @@ -6,6 +6,34 @@ namespace ui { +namespace { +EpdRect union_rect(const EpdRect& a, const EpdRect& b) { + const int left = a.x < b.x ? a.x : b.x; + const int top = a.y < b.y ? a.y : b.y; + const int right_a = a.x + a.width; + const int right_b = b.x + b.width; + const int bottom_a = a.y + a.height; + const int bottom_b = b.y + b.height; + const int right = right_a > right_b ? right_a : right_b; + const int bottom = bottom_a > bottom_b ? bottom_a : bottom_b; + return {.x = left, .y = top, .width = right - left, .height = bottom - top}; +} + +EpdRect clamp_to_rect(const EpdRect& rect, const EpdRect& limit) { + const int left = rect.x > limit.x ? rect.x : limit.x; + const int top = rect.y > limit.y ? rect.y : limit.y; + const int right = (rect.x + rect.width) < (limit.x + limit.width) ? (rect.x + rect.width) : (limit.x + limit.width); + const int bottom = + (rect.y + rect.height) < (limit.y + limit.height) ? (rect.y + rect.height) : (limit.y + limit.height); + + if (right <= left || bottom <= top) { + return {.x = limit.x, .y = limit.y, .width = 0, .height = 0}; + } + + return {.x = left, .y = top, .width = right - left, .height = bottom - top}; +} +} // namespace + void WeatherView::layout(int screen_width, int screen_height) { const int top = (screen_height * 62) / 100; bounds_ = { @@ -19,8 +47,50 @@ void WeatherView::layout(int screen_width, int screen_height) { text_y_ = top + 36; } -void WeatherView::render(uint8_t* framebuffer, const WeatherProps& props) const { +EpdRect WeatherView::measure_text_bounds(const WeatherProps& props) const { + char text[128]; + snprintf(text, sizeof(text), "%s\n%d°C", props.state_ru, props.temperature); + + EpdFontProperties font_props = epd_font_properties_default(); + font_props.flags = EPD_DRAW_ALIGN_CENTER; + + int x1 = 0; + int y1 = 0; + int w = 0; + int h = 0; + epd_get_text_bounds(&MartianMono12, text, &text_x_, &text_y_, &x1, &y1, &w, &h, &font_props); + + return {.x = x1, .y = y1, .width = w, .height = h}; +} + +EpdRect WeatherView::dirty_bounds(const WeatherProps& props, const int pad_x, const int pad_y) const { + EpdRect area = {.x = bounds_.x, .y = bounds_.y, .width = 0, .height = 0}; + + if (has_last_text_bounds_) { + area = last_text_bounds_; + } + + if (props.valid) { + const EpdRect current_bounds = measure_text_bounds(props); + area = has_last_text_bounds_ ? union_rect(area, current_bounds) : current_bounds; + } + + if (area.width <= 0 || area.height <= 0) { + return area; + } + + const EpdRect expanded = { + .x = area.x - pad_x, + .y = area.y - pad_y, + .width = area.width + (pad_x * 2), + .height = area.height + (pad_y * 2), + }; + return clamp_to_rect(expanded, bounds_); +} + +void WeatherView::render(uint8_t* framebuffer, const WeatherProps& props) { if (!props.valid) { + has_last_text_bounds_ = false; return; } @@ -33,6 +103,9 @@ void WeatherView::render(uint8_t* framebuffer, const WeatherProps& props) const int x = text_x_; int y = text_y_; epd_write_string(&MartianMono12, text, &x, &y, framebuffer, &font_props); + + last_text_bounds_ = measure_text_bounds(props); + has_last_text_bounds_ = (last_text_bounds_.width > 0 && last_text_bounds_.height > 0); } const EpdRect& WeatherView::bounds() const { diff --git a/src/ui/views/weather_view.h b/src/ui/views/weather_view.h index 11c6698..458b663 100644 --- a/src/ui/views/weather_view.h +++ b/src/ui/views/weather_view.h @@ -13,13 +13,18 @@ struct WeatherProps { class WeatherView { public: void layout(int screen_width, int screen_height); - void render(uint8_t* framebuffer, const WeatherProps& props) const; + EpdRect dirty_bounds(const WeatherProps& props, int pad_x = 4, int pad_y = 4) const; + void render(uint8_t* framebuffer, const WeatherProps& props); const EpdRect& bounds() const; private: + EpdRect measure_text_bounds(const WeatherProps& props) const; + EpdRect bounds_ = {0, 0, 0, 0}; int text_x_ = 0; int text_y_ = 0; + EpdRect last_text_bounds_ = {0, 0, 0, 0}; + bool has_last_text_bounds_ = false; }; } // namespace ui diff --git a/taskfile.yml b/taskfile.yml new file mode 100644 index 0000000..ebe559a --- /dev/null +++ b/taskfile.yml @@ -0,0 +1,94 @@ +version: "3" + +vars: + PIO: pio + ENV: esp32dev + PORT: "" + BAUD: "115200" + PYTHON: python3.11 + EPDIY_SCRIPTS: lib/epdiy/scripts + MARTIAN_FONT: static/MartianMono-VariableFont_wdth,wght.ttf + MARTIAN_CHARS: '0123456789:;.,-+/!@\#^&*%°CABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя' + +tasks: + default: + desc: Показать список доступных задач + cmds: + - task --list + + build: + desc: Сборка прошивки (ENV=esp32dev) + cmds: + - "{{.PIO}} run -e {{.ENV}}" + + clean: + desc: Очистка артефактов сборки + cmds: + - "{{.PIO}} run -e {{.ENV}} -t clean" + + rebuild: + desc: Полная пересборка + deps: [clean] + cmds: + - "{{.PIO}} run -e {{.ENV}}" + + upload: + desc: Прошивка на устройство (опционально PORT=/dev/tty...) + cmds: + - "{{.PIO}} run -e {{.ENV}} -t upload {{if .PORT}}--upload-port {{.PORT}}{{end}}" + + monitor: + desc: Serial monitor (опционально PORT=/dev/tty...) + cmds: + - "{{.PIO}} device monitor -b {{.BAUD}} {{if .PORT}}-p {{.PORT}}{{end}}" + + run: + desc: Прошивка + monitor + deps: [upload] + cmds: + - "{{.PIO}} device monitor -b {{.BAUD}} {{if .PORT}}-p {{.PORT}}{{end}}" + + check: + desc: Статический анализ через pio check + cmds: + - "{{.PIO}} check -e {{.ENV}}" + + size: + desc: Отчёт по размеру бинарника + cmds: + - "{{.PIO}} run -e {{.ENV}} -t size" + + ports: + desc: Список доступных serial-портов + cmds: + - "{{.PIO}} device list" + + image: + desc: Генерация include/sun.h и include/moon.h из static/*.jpg + cmds: + - "{{.PYTHON}} {{.EPDIY_SCRIPTS}}/imgconvert.py -i static/sun.jpg -n sun -o include/sun.h" + - "{{.PYTHON}} {{.EPDIY_SCRIPTS}}/imgconvert.py -i static/moon.jpg -n moon -o include/moon.h" + + font:12: + desc: Генерация MartianMono12.h + cmds: + - "{{.PYTHON}} {{.EPDIY_SCRIPTS}}/fontconvert.py --compress MartianMono12 12 {{.MARTIAN_FONT}} --string '{{.MARTIAN_CHARS}}' > lib/MartianMono/MartianMono12.h" + + font:30: + desc: Генерация MartianMono30.h + cmds: + - "{{.PYTHON}} {{.EPDIY_SCRIPTS}}/fontconvert.py --compress MartianMono30 30 {{.MARTIAN_FONT}} --string '{{.MARTIAN_CHARS}}' > lib/MartianMono/MartianMono30.h" + + font:120: + desc: Генерация MartianMono120.h + cmds: + - "{{.PYTHON}} {{.EPDIY_SCRIPTS}}/fontconvert.py --compress MartianMono120 120 {{.MARTIAN_FONT}} --string '0123456789' > lib/MartianMono/MartianMono120.h" + + font:200: + desc: Генерация MartianMono200.h + cmds: + - "{{.PYTHON}} {{.EPDIY_SCRIPTS}}/fontconvert.py --compress MartianMono200 200 {{.MARTIAN_FONT}} --string '0123456789' > lib/MartianMono/MartianMono200.h" + + font:all: + desc: Генерация всех шрифтов + deps: [font:12, font:30, font:120, font:200]