- Исправление отображения

- 16мб флеша
This commit is contained in:
Кобелев Андрей Андреевич
2026-03-12 12:28:44 +03:00
parent 6253480f30
commit 9a188663ee
17 changed files with 6111 additions and 774 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
# Name, Type, SubType, Offset, Size, Flags # Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000, nvs, data, nvs, 0x9000, 0x5000,
phy_init, data, phy, 0xE000, 0x1000, phy_init, data, phy, 0xE000, 0x1000,
factory, app, factory, 0x10000, 0x280000, factory, app, factory, 0x10000, 0x700000,
spiffs, data, spiffs, 0x290000,0x170000, spiffs, data, spiffs, 0x710000,0x8F0000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 phy_init data phy 0xE000 0x1000
4 factory app factory 0x10000 0x280000 0x700000
5 spiffs data spiffs 0x290000 0x710000 0x170000 0x8F0000

View File

@@ -4,10 +4,23 @@ board = esp32dev
framework = arduino framework = arduino
board_build.f_cpu = 240000000L board_build.f_cpu = 240000000L
board_build.partitions = partitions.csv board_build.partitions = partitions.csv
board_build.flash_size = 16MB
board_build.flash_mode = qio
board_upload.flash_size = 16MB
board_upload.maximum_size = 16777216
upload_speed = 460800
upload_flags =
--before
default_reset
--after
hard_reset
monitor_speed = 115200 monitor_speed = 115200
lib_deps = lib_deps =
gyverlibs/GyverNTP@^1.3.1 gyverlibs/GyverNTP@^1.3.1
bblanchon/ArduinoJson@^7.4.3 bblanchon/ArduinoJson@^7.4.3
https://github.com/vroland/epdiy.git
build_flags = build_flags =
-DBOARD_HAS_PSRAM -DBOARD_HAS_PSRAM
-mfix-esp32-psram-cache-issue
-DCONFIG_EPD_BOARD_REVISION_LILYGO_T5_47 -DCONFIG_EPD_BOARD_REVISION_LILYGO_T5_47
-DCONFIG_EPD_DISPLAY_TYPE_ED047TC2

View File

@@ -38,7 +38,7 @@ int clamp_int(int value, int min_v, int max_v) {
Application::Application() Application::Application()
: time_service_(UTC_HOUR), : time_service_(UTC_HOUR),
weather_service_(connectivity_), weather_service_(connectivity_),
battery_service_(35), battery_service_(T5_47_BATT_PIN),
dashboard_(display_) {} dashboard_(display_) {}
void Application::safe_copy(char* dst, size_t dst_size, const char* src) { void Application::safe_copy(char* dst, size_t dst_size, const char* src) {

View File

@@ -7,11 +7,11 @@
namespace app { namespace app {
RTC_DATA_ATTR RtcState g_rtc_state = { RTC_DATA_ATTR RtcState g_rtc_state = {
.pressed_wakeup_btn_index = 0, 0,
.current_day = -1, -1,
.current_battery_percent = -1, -1,
.weather_last_updated = "", "",
.initialized = false, false,
}; };
void init_rtc_state_if_needed() { void init_rtc_state_if_needed() {

View File

@@ -1,6 +1,8 @@
#include "app/services/battery_service.h" #include "app/services/battery_service.h"
#include <Arduino.h> #include <Arduino.h>
#include <driver/adc.h>
#include <esp_adc_cal.h>
#include <math.h> #include <math.h>
#include "epdiy.h" #include "epdiy.h"
@@ -11,6 +13,25 @@ namespace {
constexpr double kBattMinVoltage = 3.30; constexpr double kBattMinVoltage = 3.30;
constexpr double kBattMaxVoltage = 4.20; constexpr double kBattMaxVoltage = 4.20;
constexpr double kBattVoltageDivider = 2.0; constexpr double kBattVoltageDivider = 2.0;
adc1_channel_t batt_pin_to_adc1_channel(const int batt_pin) {
switch (batt_pin) {
case 32:
return ADC1_CHANNEL_4;
case 33:
return ADC1_CHANNEL_5;
case 34:
return ADC1_CHANNEL_6;
case 35:
return ADC1_CHANNEL_7;
case 36:
return ADC1_CHANNEL_0;
case 39:
return ADC1_CHANNEL_3;
default:
return ADC1_CHANNEL_0;
}
}
} }
BatteryService::BatteryService(int batt_pin) : batt_pin_(batt_pin) {} BatteryService::BatteryService(int batt_pin) : batt_pin_(batt_pin) {}
@@ -22,10 +43,27 @@ int BatteryService::clamp_int(int value, int min_v, int max_v) {
} }
double BatteryService::read_battery_voltage() const { double BatteryService::read_battery_voltage() const {
static bool adc_initialized = false;
static esp_adc_cal_characteristics_t adc_chars;
const adc1_channel_t channel = batt_pin_to_adc1_channel(batt_pin_);
if (!adc_initialized) {
adc1_config_width(ADC_WIDTH_BIT_12);
adc1_config_channel_atten(channel, ADC_ATTEN_DB_11);
esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, 1100, &adc_chars);
adc_initialized = true;
}
epd_poweron(); epd_poweron();
delay(50); delay(50);
const uint32_t batt_mv = analogReadMilliVolts(batt_pin_); uint32_t raw = 0;
constexpr int kSamples = 32;
for (int i = 0; i < kSamples; i++) {
raw += static_cast<uint32_t>(adc1_get_raw(channel));
}
raw /= kSamples;
const uint32_t batt_mv = esp_adc_cal_raw_to_voltage(raw, &adc_chars);
Serial.print("Battery ADC: "); Serial.print("Battery ADC: ");
Serial.print(batt_mv); Serial.print(batt_mv);

View File

@@ -35,17 +35,18 @@ EpdRect align_area_for_partial(const EpdRect& area, const int max_w, const int m
void DisplayService::begin(EpdRotation orientation, const EpdWaveform* waveform, const EpdDrawMode full_mode, void DisplayService::begin(EpdRotation orientation, const EpdWaveform* waveform, const EpdDrawMode full_mode,
const EpdDrawMode partial_mode) { const EpdDrawMode partial_mode) {
#if LILYGO_T5_47_PANEL_PROFILE == 1 #if defined(CONFIG_EPD_DISPLAY_TYPE_ED047TC1)
const EpdDisplay_t* display_profile = &ED047TC1; const EpdDisplay_t* display_profile = &ED047TC1;
const char* display_profile_name = "ED047TC1"; const char* display_profile_name = "ED047TC1";
#elif LILYGO_T5_47_PANEL_PROFILE == 2 #elif defined(CONFIG_EPD_DISPLAY_TYPE_ED047TC2)
const EpdDisplay_t* display_profile = &ED047TC2; const EpdDisplay_t* display_profile = &ED047TC2;
const char* display_profile_name = "ED047TC2"; const char* display_profile_name = "ED047TC2";
#else #else
#error "LILYGO_T5_47_PANEL_PROFILE must be 1 (TC1) or 2 (TC2)" #error "Define CONFIG_EPD_DISPLAY_TYPE_ED047TC1 or CONFIG_EPD_DISPLAY_TYPE_ED047TC2 in build_flags"
#endif #endif
epd_init(&epd_board_lilygo_t5_47, display_profile, EPD_LUT_64K); epd_init(&epd_board_lilygo_t5_47, display_profile, EPD_LUT_64K);
epd_set_vcom(LILYGO_T5_47_VCOM_MV);
hl_ = epd_hl_init(waveform); hl_ = epd_hl_init(waveform);
epd_set_rotation(orientation); epd_set_rotation(orientation);

View File

@@ -1,7 +1,5 @@
#include "ui/dashboard_screen.h" #include "ui/dashboard_screen.h"
#include "app/services/weather_service.h"
namespace ui { namespace ui {
DashboardScreen::DashboardScreen(app::DisplayService& display) : display_(display) {} DashboardScreen::DashboardScreen(app::DisplayService& display) : display_(display) {}
@@ -26,7 +24,7 @@ void DashboardScreen::render_pass(const app::DashboardData& data, const app::Ren
const WeatherProps weather_props{ const WeatherProps weather_props{
.valid = data.weather.valid, .valid = data.weather.valid,
.state_ru = app::WeatherService::weather_state_to_ru(data.weather.state), .state_text = data.weather.state,
.temperature = data.weather.temperature, .temperature = data.weather.temperature,
}; };
weather_view_.render(display_.framebuffer(), weather_props); weather_view_.render(display_.framebuffer(), weather_props);
@@ -44,7 +42,7 @@ void DashboardScreen::render_pass(const app::DashboardData& data, const app::Ren
if (plan.redraw_weather && data.weather.valid) { if (plan.redraw_weather && data.weather.valid) {
const WeatherProps weather_props{ const WeatherProps weather_props{
.valid = true, .valid = true,
.state_ru = app::WeatherService::weather_state_to_ru(data.weather.state), .state_text = data.weather.state,
.temperature = data.weather.temperature, .temperature = data.weather.temperature,
}; };
const EpdRect weather_area = weather_view_.dirty_bounds(weather_props, 5, 6); const EpdRect weather_area = weather_view_.dirty_bounds(weather_props, 5, 6);

View File

@@ -2,6 +2,7 @@
#include <stdio.h> #include <stdio.h>
#include "IBMPlexMono24.h"
#include "MartianMono12.h" #include "MartianMono12.h"
namespace ui { namespace ui {
@@ -18,41 +19,45 @@ EpdRect union_rect(const EpdRect& a, const EpdRect& b) {
const int bottom = bottom_a > bottom_b ? bottom_a : bottom_b; const int bottom = bottom_a > bottom_b ? bottom_a : bottom_b;
return {.x = left, .y = top, .width = right - left, .height = bottom - top}; 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 } // namespace
void BatteryView::layout(int screen_width, int screen_height) { void BatteryView::layout(int screen_width, int screen_height) {
const int margin = 14; const int margin_x = 20;
const int width = 120; const int margin_y = 20;
const int height = 44; const int width = 170;
const int height = 68;
bounds_ = { bounds_ = {
.x = screen_width - width - margin, .x = screen_width - width - margin_x,
.y = screen_height - height - margin, .y = margin_y,
.width = width, .width = width,
.height = height, .height = height,
}; };
text_x_ = screen_width - margin; label_x_ = screen_width - margin_x;
text_y_ = screen_height - margin; label_y_ = margin_y + 16;
text_x_ = screen_width - margin_x;
text_y_ = margin_y + height - 8;
}
EpdRect BatteryView::measure_label_bounds() const {
EpdFontProperties font_props = epd_font_properties_default();
font_props.flags = EPD_DRAW_ALIGN_RIGHT;
int x = label_x_;
int y = label_y_;
int x1 = 0;
int y1 = 0;
int w = 0;
int h = 0;
epd_get_text_bounds(&MartianMono12, "BATTERY", &x, &y, &x1, &y1, &w, &h, &font_props);
return {.x = x1, .y = y1, .width = w, .height = h};
} }
EpdRect BatteryView::measure_text_bounds(const BatteryProps& props) const { EpdRect BatteryView::measure_text_bounds(const BatteryProps& props) const {
char text[24]; char text[24];
snprintf(text, sizeof(text), "%d %%", props.percent); snprintf(text, sizeof(text), "%d%%", props.percent);
EpdFontProperties font_props = epd_font_properties_default(); EpdFontProperties font_props = epd_font_properties_default();
font_props.flags = EPD_DRAW_ALIGN_RIGHT; font_props.flags = EPD_DRAW_ALIGN_RIGHT;
@@ -61,34 +66,21 @@ EpdRect BatteryView::measure_text_bounds(const BatteryProps& props) const {
int y1 = 0; int y1 = 0;
int w = 0; int w = 0;
int h = 0; int h = 0;
epd_get_text_bounds(&MartianMono12, text, &text_x_, &text_y_, &x1, &y1, &w, &h, &font_props); int x = text_x_;
int y = text_y_;
epd_get_text_bounds(&IBMPlexMono24, text, &x, &y, &x1, &y1, &w, &h, &font_props);
return {.x = x1, .y = y1, .width = w, .height = h}; 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 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}; (void)props;
(void)pad_x;
if (has_last_text_bounds_) { (void)pad_y;
area = last_text_bounds_; if (!has_last_text_bounds_) {
return {.x = bounds_.x, .y = bounds_.y, .width = bounds_.width, .height = bounds_.height};
} }
return 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) { void BatteryView::render(uint8_t* framebuffer, const BatteryProps& props) {
@@ -98,16 +90,22 @@ void BatteryView::render(uint8_t* framebuffer, const BatteryProps& props) {
} }
char text[24]; char text[24];
snprintf(text, sizeof(text), "%d %%", props.percent); snprintf(text, sizeof(text), "%d%%", props.percent);
EpdFontProperties font_props = epd_font_properties_default(); EpdFontProperties font_props = epd_font_properties_default();
font_props.flags = EPD_DRAW_ALIGN_RIGHT; font_props.flags = EPD_DRAW_ALIGN_RIGHT;
int label_x = label_x_;
int label_y = label_y_;
epd_write_string(&MartianMono12, "BATTERY", &label_x, &label_y, framebuffer, &font_props);
int x = text_x_; int x = text_x_;
int y = text_y_; int y = text_y_;
epd_write_string(&MartianMono12, text, &x, &y, framebuffer, &font_props); epd_write_string(&IBMPlexMono24, text, &x, &y, framebuffer, &font_props);
last_text_bounds_ = measure_text_bounds(props); const EpdRect label_bounds = measure_label_bounds();
const EpdRect value_bounds = measure_text_bounds(props);
last_text_bounds_ = union_rect(label_bounds, value_bounds);
has_last_text_bounds_ = (last_text_bounds_.width > 0 && last_text_bounds_.height > 0); has_last_text_bounds_ = (last_text_bounds_.width > 0 && last_text_bounds_.height > 0);
} }

View File

@@ -18,8 +18,11 @@ public:
private: private:
EpdRect measure_text_bounds(const BatteryProps& props) const; EpdRect measure_text_bounds(const BatteryProps& props) const;
EpdRect measure_label_bounds() const;
EpdRect bounds_ = {0, 0, 0, 0}; EpdRect bounds_ = {0, 0, 0, 0};
int label_x_ = 0;
int label_y_ = 0;
int text_x_ = 0; int text_x_ = 0;
int text_y_ = 0; int text_y_ = 0;
EpdRect last_text_bounds_ = {0, 0, 0, 0}; EpdRect last_text_bounds_ = {0, 0, 0, 0};

View File

@@ -1,7 +1,10 @@
#include "ui/views/calendar_view.h" #include "ui/views/calendar_view.h"
#include <stddef.h>
#include "IBMPlexMono24.h"
#include "MartianMono12.h"
#include "MartianMono120.h" #include "MartianMono120.h"
#include "MartianMono30.h"
namespace ui { namespace ui {
@@ -13,15 +16,46 @@ int CalendarView::clamp_int(int value, int min_v, int max_v) {
return value; return value;
} }
bool CalendarView::sanitize_ascii(const char* src, char* dst, int dst_size) {
if (!dst || dst_size <= 0) {
return false;
}
dst[0] = '\0';
if (!src) {
return false;
}
int out = 0;
bool has_visible = false;
for (int i = 0; src[i] != '\0' && out < (dst_size - 1); ++i) {
const unsigned char ch = static_cast<unsigned char>(src[i]);
if (ch >= 32 && ch <= 126) {
dst[out++] = static_cast<char>(ch);
if (ch != ' ') {
has_visible = true;
}
} else {
dst[out++] = ' ';
}
}
dst[out] = '\0';
return has_visible;
}
void CalendarView::layout(int screen_width, int screen_height) { void CalendarView::layout(int screen_width, int screen_height) {
const int margin_x = 16;
const int top = 16;
const int bottom = (screen_height * 48) / 100;
bounds_ = {.x = margin_x,
.y = top,
.width = screen_width - (margin_x * 2),
.height = bottom - top};
center_x_ = screen_width / 2; center_x_ = screen_width / 2;
day_y_ = (screen_height * 28) / 100; title_y_ = bounds_.y + 22;
day_y_ = bounds_.y + clamp_int(bounds_.height / 2, 110, 190);
const int month_spacing = clamp_int(screen_height / 6, 62, 108); month_y_ = day_y_ + clamp_int(screen_height / 12, 48, 82);
const int week_spacing = clamp_int(screen_height / 14, 30, 54); week_y_ = month_y_ + clamp_int(screen_height / 18, 30, 52);
month_y_ = day_y_ + month_spacing;
week_y_ = month_y_ + week_spacing;
} }
void CalendarView::render(uint8_t *framebuffer, void CalendarView::render(uint8_t *framebuffer,
@@ -33,20 +67,31 @@ void CalendarView::render(uint8_t *framebuffer,
EpdFontProperties font_props = epd_font_properties_default(); EpdFontProperties font_props = epd_font_properties_default();
font_props.flags = EPD_DRAW_ALIGN_CENTER; font_props.flags = EPD_DRAW_ALIGN_CENTER;
int title_x = center_x_;
int title_y = title_y_;
epd_write_string(&MartianMono12, "DATE", &title_x, &title_y, framebuffer,
&font_props);
int day_x = center_x_; int day_x = center_x_;
int day_y = day_y_; int day_y = day_y_;
epd_write_string(&MartianMono120, props.day, &day_x, &day_y, framebuffer, epd_write_string(&MartianMono120, props.day, &day_x, &day_y, framebuffer,
&font_props); &font_props);
int month_x = center_x_; char month_ascii[32];
int month_y = month_y_; if (sanitize_ascii(props.month, month_ascii, sizeof(month_ascii))) {
epd_write_string(&MartianMono30, props.month, &month_x, &month_y, framebuffer, int month_x = center_x_;
&font_props); int month_y = month_y_;
epd_write_string(&IBMPlexMono24, month_ascii, &month_x, &month_y, framebuffer,
&font_props);
}
int week_x = center_x_; char week_ascii[32];
int week_y = week_y_; if (sanitize_ascii(props.weekday, week_ascii, sizeof(week_ascii))) {
epd_write_string(&MartianMono30, props.weekday, &week_x, &week_y, framebuffer, int week_x = center_x_;
&font_props); int week_y = week_y_;
epd_write_string(&IBMPlexMono24, week_ascii, &week_x, &week_y, framebuffer,
&font_props);
}
} }
} // namespace ui } // namespace ui

View File

@@ -18,8 +18,11 @@ public:
private: private:
static int clamp_int(int value, int min_v, int max_v); static int clamp_int(int value, int min_v, int max_v);
static bool sanitize_ascii(const char* src, char* dst, int dst_size);
EpdRect bounds_ = {0, 0, 0, 0};
int center_x_ = 0; int center_x_ = 0;
int title_y_ = 0;
int day_y_ = 0; int day_y_ = 0;
int month_y_ = 0; int month_y_ = 0;
int week_y_ = 0; int week_y_ = 0;

View File

@@ -2,6 +2,8 @@
#include <stdio.h> #include <stdio.h>
#include "IBMPlexMono24.h"
#include "IBMPlexMono64.h"
#include "MartianMono12.h" #include "MartianMono12.h"
namespace ui { namespace ui {
@@ -19,73 +21,135 @@ EpdRect union_rect(const EpdRect& a, const EpdRect& b) {
return {.x = left, .y = top, .width = right - left, .height = bottom - top}; return {.x = left, .y = top, .width = right - left, .height = bottom - top};
} }
EpdRect clamp_to_rect(const EpdRect& rect, const EpdRect& limit) { const EpdFont* pick_state_font(const char* text, const int center_x, const int baseline_y, const int max_width) {
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_ = {
.x = 0,
.y = top,
.width = screen_width,
.height = screen_height - top,
};
text_x_ = screen_width / 2;
text_y_ = top + 36;
}
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(); EpdFontProperties font_props = epd_font_properties_default();
font_props.flags = EPD_DRAW_ALIGN_CENTER; font_props.flags = EPD_DRAW_ALIGN_CENTER;
int x = center_x;
int y = baseline_y;
int x1 = 0; int x1 = 0;
int y1 = 0; int y1 = 0;
int w = 0; int w = 0;
int h = 0; int h = 0;
epd_get_text_bounds(&MartianMono12, text, &text_x_, &text_y_, &x1, &y1, &w, &h, &font_props); epd_get_text_bounds(&IBMPlexMono24, text, &x, &y, &x1, &y1, &w, &h, &font_props);
if (w <= max_width) {
return &IBMPlexMono24;
}
return &MartianMono12;
}
return {.x = x1, .y = y1, .width = w, .height = h}; const EpdFont* pick_temp_font(const char* text, const int center_x, const int baseline_y, const int max_width) {
EpdFontProperties font_props = epd_font_properties_default();
font_props.flags = EPD_DRAW_ALIGN_CENTER;
int x = center_x;
int y = baseline_y;
int x1 = 0;
int y1 = 0;
int w = 0;
int h = 0;
epd_get_text_bounds(&IBMPlexMono64, text, &x, &y, &x1, &y1, &w, &h, &font_props);
if (w <= max_width) {
return &IBMPlexMono64;
}
return &IBMPlexMono24;
}
} // namespace
int WeatherView::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;
}
bool WeatherView::sanitize_ascii(const char* src, char* dst, int dst_size) {
if (!dst || dst_size <= 0) {
return false;
}
dst[0] = '\0';
if (!src) {
return false;
}
int out = 0;
bool has_visible = false;
for (int i = 0; src[i] != '\0' && out < (dst_size - 1); ++i) {
const unsigned char ch = static_cast<unsigned char>(src[i]);
if (ch >= 32 && ch <= 126) {
dst[out++] = static_cast<char>(ch);
if (ch != ' ') {
has_visible = true;
}
} else {
dst[out++] = ' ';
}
}
dst[out] = '\0';
return has_visible;
}
void WeatherView::layout(int screen_width, int screen_height) {
const int margin_x = 16;
const int margin_bottom = 16;
const int top = (screen_height * 52) / 100;
bounds_ = {
.x = margin_x,
.y = top,
.width = screen_width - (margin_x * 2),
.height = screen_height - top - margin_bottom,
};
center_x_ = screen_width / 2;
title_y_ = top + 24;
state_y_ = title_y_ + clamp_int(screen_height / 24, 30, 46);
temp_y_ = state_y_ + clamp_int(screen_height / 10, 72, 120);
}
EpdRect WeatherView::measure_text_bounds(const WeatherProps& props) const {
if (!props.valid) {
return {.x = bounds_.x, .y = bounds_.y, .width = 0, .height = 0};
}
EpdFontProperties font_props = epd_font_properties_default();
font_props.flags = EPD_DRAW_ALIGN_CENTER;
const int max_width = bounds_.width - 20;
char state_ascii[48];
const bool has_state = sanitize_ascii(props.state_text, state_ascii, sizeof(state_ascii));
const char* state_text = has_state ? state_ascii : "WEATHER";
const EpdFont* state_font = pick_state_font(state_text, center_x_, state_y_, max_width);
char temp_text[24];
snprintf(temp_text, sizeof(temp_text), "%dC", props.temperature);
const EpdFont* temp_font = pick_temp_font(temp_text, center_x_, temp_y_, max_width);
int sx = center_x_;
int sy = state_y_;
int sx1 = 0;
int sy1 = 0;
int sw = 0;
int sh = 0;
epd_get_text_bounds(state_font, state_text, &sx, &sy, &sx1, &sy1, &sw, &sh, &font_props);
int tx = center_x_;
int ty = temp_y_;
int tx1 = 0;
int ty1 = 0;
int tw = 0;
int th = 0;
epd_get_text_bounds(temp_font, temp_text, &tx, &ty, &tx1, &ty1, &tw, &th, &font_props);
const EpdRect state_rect = {.x = sx1, .y = sy1, .width = sw, .height = sh};
const EpdRect temp_rect = {.x = tx1, .y = ty1, .width = tw, .height = th};
return union_rect(state_rect, temp_rect);
} }
EpdRect WeatherView::dirty_bounds(const WeatherProps& props, const int pad_x, const int pad_y) const { 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}; (void)pad_x;
(void)pad_y;
if (has_last_text_bounds_) { if (!props.valid && !has_last_text_bounds_) {
area = last_text_bounds_; return {.x = bounds_.x, .y = bounds_.y, .width = 0, .height = 0};
} }
return 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) { void WeatherView::render(uint8_t* framebuffer, const WeatherProps& props) {
@@ -94,15 +158,30 @@ void WeatherView::render(uint8_t* framebuffer, const WeatherProps& props) {
return; return;
} }
char text[128];
snprintf(text, sizeof(text), "%s\n%d°C", props.state_ru, props.temperature);
EpdFontProperties font_props = epd_font_properties_default(); EpdFontProperties font_props = epd_font_properties_default();
font_props.flags = EPD_DRAW_ALIGN_CENTER; font_props.flags = EPD_DRAW_ALIGN_CENTER;
int x = text_x_; int title_x = center_x_;
int y = text_y_; int title_y = title_y_;
epd_write_string(&MartianMono12, text, &x, &y, framebuffer, &font_props); epd_write_string(&MartianMono12, "WEATHER", &title_x, &title_y, framebuffer, &font_props);
const int max_width = bounds_.width - 20;
char state_ascii[48];
const bool has_state = sanitize_ascii(props.state_text, state_ascii, sizeof(state_ascii));
const char* state_text = has_state ? state_ascii : "NO DATA";
const EpdFont* state_font = pick_state_font(state_text, center_x_, state_y_, max_width);
char temp_text[24];
snprintf(temp_text, sizeof(temp_text), "%dC", props.temperature);
const EpdFont* temp_font = pick_temp_font(temp_text, center_x_, temp_y_, max_width);
int sx = center_x_;
int sy = state_y_;
epd_write_string(state_font, state_text, &sx, &sy, framebuffer, &font_props);
int tx = center_x_;
int ty = temp_y_;
epd_write_string(temp_font, temp_text, &tx, &ty, framebuffer, &font_props);
last_text_bounds_ = measure_text_bounds(props); last_text_bounds_ = measure_text_bounds(props);
has_last_text_bounds_ = (last_text_bounds_.width > 0 && last_text_bounds_.height > 0); has_last_text_bounds_ = (last_text_bounds_.width > 0 && last_text_bounds_.height > 0);

View File

@@ -6,7 +6,7 @@ namespace ui {
struct WeatherProps { struct WeatherProps {
bool valid; bool valid;
const char* state_ru; const char* state_text;
int temperature; int temperature;
}; };
@@ -18,11 +18,15 @@ public:
const EpdRect& bounds() const; const EpdRect& bounds() const;
private: private:
static int clamp_int(int value, int min_v, int max_v);
EpdRect measure_text_bounds(const WeatherProps& props) const; EpdRect measure_text_bounds(const WeatherProps& props) const;
static bool sanitize_ascii(const char* src, char* dst, int dst_size);
EpdRect bounds_ = {0, 0, 0, 0}; EpdRect bounds_ = {0, 0, 0, 0};
int text_x_ = 0; int center_x_ = 0;
int text_y_ = 0; int title_y_ = 0;
int state_y_ = 0;
int temp_y_ = 0;
EpdRect last_text_bounds_ = {0, 0, 0, 0}; EpdRect last_text_bounds_ = {0, 0, 0, 0};
bool has_last_text_bounds_ = false; bool has_last_text_bounds_ = false;
}; };

View File

@@ -6,8 +6,9 @@ vars:
PORT: "" PORT: ""
BAUD: "115200" BAUD: "115200"
PYTHON: python3.11 PYTHON: python3.11
EPDIY_SCRIPTS: lib/epdiy/scripts EPDIY_SCRIPTS: .pio/libdeps/esp32dev/epdiy/scripts
MARTIAN_FONT: static/MartianMono-VariableFont_wdth,wght.ttf MARTIAN_FONT: static/MartianMono-VariableFont_wdth,wght.ttf
IBM_FONT: static/IBMPlexMono-SemiBold.ttf
MARTIAN_CHARS: '0123456789:;.,-+/!@\#^&*%°CABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя' MARTIAN_CHARS: '0123456789:;.,-+/!@\#^&*%°CABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя'
tasks: tasks:
@@ -92,3 +93,17 @@ tasks:
font:all: font:all:
desc: Генерация всех шрифтов desc: Генерация всех шрифтов
deps: [font:12, font:30, font:120, font:200] deps: [font:12, font:30, font:120, font:200]
font:ibm:24:
desc: Генерация IBMPlexMono24.h
cmds:
- "{{.PYTHON}} {{.EPDIY_SCRIPTS}}/fontconvert.py --compress IBMPlexMono24 24 {{.IBM_FONT}} --string '{{.MARTIAN_CHARS}}' > lib/IBMPlexMono/IBMPlexMono24.h"
font:ibm:64:
desc: Генерация IBMPlexMono64.h
cmds:
- "{{.PYTHON}} {{.EPDIY_SCRIPTS}}/fontconvert.py --compress IBMPlexMono64 64 {{.IBM_FONT}} --string '{{.MARTIAN_CHARS}}' > lib/IBMPlexMono/IBMPlexMono64.h"
font:ui:
desc: Генерация шрифтов для текущего UI
deps: [font:12, font:120, font:ibm:24, font:ibm:64]