Дисплей
This commit is contained in:
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@@ -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": [
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
5
partitions.csv
Normal file
5
partitions.csv
Normal file
@@ -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,
|
||||
|
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -2,11 +2,50 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
94
taskfile.yml
Normal file
94
taskfile.yml
Normal file
@@ -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]
|
||||
Reference in New Issue
Block a user