- Исправление отображения
- 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
|
# 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,
|
||||||
|
|||||||
|
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
17
taskfile.yml
17
taskfile.yml
@@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user