- Исправление отображения
- 16мб флеша
This commit is contained in:
1502
lib/IBMPlexMono/IBMPlexMono24.h
Normal file
1502
lib/IBMPlexMono/IBMPlexMono24.h
Normal file
File diff suppressed because it is too large
Load Diff
3628
lib/IBMPlexMono/IBMPlexMono64.h
Normal file
3628
lib/IBMPlexMono/IBMPlexMono64.h
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,5 +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,
|
||||
factory, app, factory, 0x10000, 0x700000,
|
||||
spiffs, data, spiffs, 0x710000,0x8F0000,
|
||||
|
||||
|
@@ -4,10 +4,23 @@ board = esp32dev
|
||||
framework = arduino
|
||||
board_build.f_cpu = 240000000L
|
||||
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
|
||||
lib_deps =
|
||||
gyverlibs/GyverNTP@^1.3.1
|
||||
bblanchon/ArduinoJson@^7.4.3
|
||||
https://github.com/vroland/epdiy.git
|
||||
build_flags =
|
||||
-DBOARD_HAS_PSRAM
|
||||
-mfix-esp32-psram-cache-issue
|
||||
-DCONFIG_EPD_BOARD_REVISION_LILYGO_T5_47
|
||||
-DCONFIG_EPD_DISPLAY_TYPE_ED047TC2
|
||||
|
||||
@@ -38,7 +38,7 @@ int clamp_int(int value, int min_v, int max_v) {
|
||||
Application::Application()
|
||||
: time_service_(UTC_HOUR),
|
||||
weather_service_(connectivity_),
|
||||
battery_service_(35),
|
||||
battery_service_(T5_47_BATT_PIN),
|
||||
dashboard_(display_) {}
|
||||
|
||||
void Application::safe_copy(char* dst, size_t dst_size, const char* src) {
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
namespace app {
|
||||
|
||||
RTC_DATA_ATTR RtcState g_rtc_state = {
|
||||
.pressed_wakeup_btn_index = 0,
|
||||
.current_day = -1,
|
||||
.current_battery_percent = -1,
|
||||
.weather_last_updated = "",
|
||||
.initialized = false,
|
||||
0,
|
||||
-1,
|
||||
-1,
|
||||
"",
|
||||
false,
|
||||
};
|
||||
|
||||
void init_rtc_state_if_needed() {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "app/services/battery_service.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <driver/adc.h>
|
||||
#include <esp_adc_cal.h>
|
||||
#include <math.h>
|
||||
|
||||
#include "epdiy.h"
|
||||
@@ -11,6 +13,25 @@ namespace {
|
||||
constexpr double kBattMinVoltage = 3.30;
|
||||
constexpr double kBattMaxVoltage = 4.20;
|
||||
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) {}
|
||||
@@ -22,10 +43,27 @@ int BatteryService::clamp_int(int value, int min_v, int max_v) {
|
||||
}
|
||||
|
||||
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();
|
||||
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(batt_mv);
|
||||
|
||||
@@ -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,
|
||||
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 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 char* display_profile_name = "ED047TC2";
|
||||
#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
|
||||
|
||||
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);
|
||||
epd_set_rotation(orientation);
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#include "ui/dashboard_screen.h"
|
||||
|
||||
#include "app/services/weather_service.h"
|
||||
|
||||
namespace ui {
|
||||
|
||||
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{
|
||||
.valid = data.weather.valid,
|
||||
.state_ru = app::WeatherService::weather_state_to_ru(data.weather.state),
|
||||
.state_text = data.weather.state,
|
||||
.temperature = data.weather.temperature,
|
||||
};
|
||||
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) {
|
||||
const WeatherProps weather_props{
|
||||
.valid = true,
|
||||
.state_ru = app::WeatherService::weather_state_to_ru(data.weather.state),
|
||||
.state_text = data.weather.state,
|
||||
.temperature = data.weather.temperature,
|
||||
};
|
||||
const EpdRect weather_area = weather_view_.dirty_bounds(weather_props, 5, 6);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#include "IBMPlexMono24.h"
|
||||
#include "MartianMono12.h"
|
||||
|
||||
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;
|
||||
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;
|
||||
const int height = 44;
|
||||
const int margin_x = 20;
|
||||
const int margin_y = 20;
|
||||
const int width = 170;
|
||||
const int height = 68;
|
||||
|
||||
bounds_ = {
|
||||
.x = screen_width - width - margin,
|
||||
.y = screen_height - height - margin,
|
||||
.x = screen_width - width - margin_x,
|
||||
.y = margin_y,
|
||||
.width = width,
|
||||
.height = height,
|
||||
};
|
||||
|
||||
text_x_ = screen_width - margin;
|
||||
text_y_ = screen_height - margin;
|
||||
label_x_ = screen_width - margin_x;
|
||||
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 {
|
||||
char text[24];
|
||||
snprintf(text, sizeof(text), "%d %%", props.percent);
|
||||
snprintf(text, sizeof(text), "%d%%", props.percent);
|
||||
|
||||
EpdFontProperties font_props = epd_font_properties_default();
|
||||
font_props.flags = EPD_DRAW_ALIGN_RIGHT;
|
||||
@@ -61,34 +66,21 @@ EpdRect BatteryView::measure_text_bounds(const BatteryProps& props) const {
|
||||
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);
|
||||
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};
|
||||
}
|
||||
|
||||
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_;
|
||||
(void)props;
|
||||
(void)pad_x;
|
||||
(void)pad_y;
|
||||
if (!has_last_text_bounds_) {
|
||||
return {.x = bounds_.x, .y = bounds_.y, .width = bounds_.width, .height = bounds_.height};
|
||||
}
|
||||
|
||||
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_);
|
||||
return bounds_;
|
||||
}
|
||||
|
||||
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];
|
||||
snprintf(text, sizeof(text), "%d %%", props.percent);
|
||||
snprintf(text, sizeof(text), "%d%%", props.percent);
|
||||
|
||||
EpdFontProperties font_props = epd_font_properties_default();
|
||||
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 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,11 @@ public:
|
||||
|
||||
private:
|
||||
EpdRect measure_text_bounds(const BatteryProps& props) const;
|
||||
EpdRect measure_label_bounds() const;
|
||||
|
||||
EpdRect bounds_ = {0, 0, 0, 0};
|
||||
int label_x_ = 0;
|
||||
int label_y_ = 0;
|
||||
int text_x_ = 0;
|
||||
int text_y_ = 0;
|
||||
EpdRect last_text_bounds_ = {0, 0, 0, 0};
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#include "ui/views/calendar_view.h"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include "IBMPlexMono24.h"
|
||||
#include "MartianMono12.h"
|
||||
#include "MartianMono120.h"
|
||||
#include "MartianMono30.h"
|
||||
|
||||
namespace ui {
|
||||
|
||||
@@ -13,15 +16,46 @@ int CalendarView::clamp_int(int value, int min_v, int max_v) {
|
||||
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) {
|
||||
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;
|
||||
day_y_ = (screen_height * 28) / 100;
|
||||
|
||||
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;
|
||||
title_y_ = bounds_.y + 22;
|
||||
day_y_ = bounds_.y + clamp_int(bounds_.height / 2, 110, 190);
|
||||
month_y_ = day_y_ + clamp_int(screen_height / 12, 48, 82);
|
||||
week_y_ = month_y_ + clamp_int(screen_height / 18, 30, 52);
|
||||
}
|
||||
|
||||
void CalendarView::render(uint8_t *framebuffer,
|
||||
@@ -33,20 +67,31 @@ void CalendarView::render(uint8_t *framebuffer,
|
||||
EpdFontProperties font_props = epd_font_properties_default();
|
||||
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_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);
|
||||
char month_ascii[32];
|
||||
if (sanitize_ascii(props.month, month_ascii, sizeof(month_ascii))) {
|
||||
int month_x = center_x_;
|
||||
int month_y = month_y_;
|
||||
epd_write_string(&IBMPlexMono24, month_ascii, &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);
|
||||
char week_ascii[32];
|
||||
if (sanitize_ascii(props.weekday, week_ascii, sizeof(week_ascii))) {
|
||||
int week_x = center_x_;
|
||||
int week_y = week_y_;
|
||||
epd_write_string(&IBMPlexMono24, week_ascii, &week_x, &week_y, framebuffer,
|
||||
&font_props);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
|
||||
@@ -18,8 +18,11 @@ public:
|
||||
|
||||
private:
|
||||
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 title_y_ = 0;
|
||||
int day_y_ = 0;
|
||||
int month_y_ = 0;
|
||||
int week_y_ = 0;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#include "IBMPlexMono24.h"
|
||||
#include "IBMPlexMono64.h"
|
||||
#include "MartianMono12.h"
|
||||
|
||||
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};
|
||||
}
|
||||
|
||||
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_ = {
|
||||
.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);
|
||||
|
||||
const EpdFont* pick_state_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(&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 area = {.x = bounds_.x, .y = bounds_.y, .width = 0, .height = 0};
|
||||
|
||||
if (has_last_text_bounds_) {
|
||||
area = last_text_bounds_;
|
||||
(void)pad_x;
|
||||
(void)pad_y;
|
||||
if (!props.valid && !has_last_text_bounds_) {
|
||||
return {.x = bounds_.x, .y = bounds_.y, .width = 0, .height = 0};
|
||||
}
|
||||
|
||||
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_);
|
||||
return bounds_;
|
||||
}
|
||||
|
||||
void WeatherView::render(uint8_t* framebuffer, const WeatherProps& props) {
|
||||
@@ -94,15 +158,30 @@ void WeatherView::render(uint8_t* framebuffer, const WeatherProps& props) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 x = text_x_;
|
||||
int y = text_y_;
|
||||
epd_write_string(&MartianMono12, text, &x, &y, framebuffer, &font_props);
|
||||
int title_x = center_x_;
|
||||
int title_y = title_y_;
|
||||
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);
|
||||
has_last_text_bounds_ = (last_text_bounds_.width > 0 && last_text_bounds_.height > 0);
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace ui {
|
||||
|
||||
struct WeatherProps {
|
||||
bool valid;
|
||||
const char* state_ru;
|
||||
const char* state_text;
|
||||
int temperature;
|
||||
};
|
||||
|
||||
@@ -18,11 +18,15 @@ public:
|
||||
const EpdRect& bounds() const;
|
||||
|
||||
private:
|
||||
static int clamp_int(int value, int min_v, int max_v);
|
||||
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};
|
||||
int text_x_ = 0;
|
||||
int text_y_ = 0;
|
||||
int center_x_ = 0;
|
||||
int title_y_ = 0;
|
||||
int state_y_ = 0;
|
||||
int temp_y_ = 0;
|
||||
EpdRect last_text_bounds_ = {0, 0, 0, 0};
|
||||
bool has_last_text_bounds_ = false;
|
||||
};
|
||||
|
||||
17
taskfile.yml
17
taskfile.yml
@@ -6,8 +6,9 @@ vars:
|
||||
PORT: ""
|
||||
BAUD: "115200"
|
||||
PYTHON: python3.11
|
||||
EPDIY_SCRIPTS: lib/epdiy/scripts
|
||||
EPDIY_SCRIPTS: .pio/libdeps/esp32dev/epdiy/scripts
|
||||
MARTIAN_FONT: static/MartianMono-VariableFont_wdth,wght.ttf
|
||||
IBM_FONT: static/IBMPlexMono-SemiBold.ttf
|
||||
MARTIAN_CHARS: '0123456789:;.,-+/!@\#^&*%°CABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя'
|
||||
|
||||
tasks:
|
||||
@@ -92,3 +93,17 @@ tasks:
|
||||
font:all:
|
||||
desc: Генерация всех шрифтов
|
||||
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]
|
||||
|
||||
Reference in New Issue
Block a user