/* AUTO-GENERATED by GUI Editor — do not edit manually */ #include "gui_generated.h" #include "lvgl.h" #include #include #include #include #include /* Fonts */ LV_FONT_DECLARE(montserrat_16_ru_en) /* Images */ LV_IMG_DECLARE(img_valve) LV_IMG_DECLARE(img_fan) LV_IMG_DECLARE(img_channel) LV_IMG_DECLARE(img_heater) LV_IMG_DECLARE(img_cooler) LV_IMG_DECLARE(img_pipe) LV_IMG_DECLARE(img_electric_heater) LV_IMG_DECLARE(img_filter_g4) LV_IMG_DECLARE(img_filter_h13) /* Color scheme - minimalistic dark theme */ #define COLOR_BG_DARK 0x0F1419 /* Очень темный фон */ #define COLOR_BG_PANEL 0x1A1F26 /* Фон панелей */ #define COLOR_BG_MODULE 0x252B33 /* Фон модулей */ #define COLOR_BG_BTN 0x2D333B /* Фон кнопок */ #define COLOR_BORDER 0x3D444D /* Границы */ #define COLOR_TEXT 0xD0D4D8 /* Светлый текст */ #define COLOR_TEXT_DIM 0x6E7681 /* Приглушенный текст */ #define COLOR_TEXT_ACCENT 0x58A6FF /* Акцентный текст */ #define COLOR_ACCENT 0x3FB950 /* Зеленый для активных */ #define COLOR_WARNING 0xD29922 /* Желтый для предупреждений */ #define COLOR_DANGER 0xF85149 /* Красный для аварий */ lv_obj_t *scr_main = NULL; lv_obj_t *scr_screen1 = NULL; /* Меню */ lv_obj_t *scr_screen2 = NULL; /* Журнал */ lv_obj_t *scr_screen3 = NULL; /* Уставка параметров */ lv_obj_t *scr_screen4 = NULL; /* Настройки (после пароля) */ /* System state structure (like ModBus data) */ typedef struct { int16_t temp_room; /* Температура комнаты x10 */ int16_t hum_room; /* Влажность комнаты x10 */ int16_t temp_channel; /* Температура канала x10 */ int16_t hum_channel; /* Влажность канала x10 */ uint16_t room_num; /* Номер комнаты */ uint8_t is_master; /* 1=Мастер, 0=Слейв */ uint8_t has_signal; /* Есть ли сигнал при слейве */ uint8_t season; /* 0=Зима, 1=Лето, 2=Переход */ uint8_t mode; /* 0=Остановка, 1=Дежурный, 2=Рабочий */ uint8_t valve_inlet; /* Входной клапан 0-100% */ uint8_t valve_recirc; /* Клапан рециркуляции 0-100% */ uint8_t filter_g4_clean; /* 0=Грязный, 1=Чистый */ uint8_t kws_valve; /* KWS клапан 0-100% */ uint8_t kws_mode; /* 0=Охлаждение, 1=Нагрев */ uint8_t pww_valve; /* PWW клапан 0-100% */ uint8_t pww_mode; /* 0=Охлаждение, 1=Нагрев */ uint8_t heater_on; /* Электронагреватель 0=Выкл, 1=Вкл */ uint8_t fan_on; /* Вентилятор 0=Выкл, 1=Вкл */ uint8_t filter_h13_clean; /* 0=Грязный, 1=Чистый */ uint16_t year; uint8_t month; uint8_t day; uint8_t hour; uint8_t minute; } SysState_t; static SysState_t state = { .temp_room = 0, .hum_room = 0, .temp_channel = 200, /* 20.0°C default - prevents false freezing alarm */ .hum_channel = 0, .room_num = 101, .is_master = 1, .has_signal = 1, .season = 0, /* will be recalculated from date in gui_init */ .mode = 1, /* Дежурный по умолчанию */ .valve_inlet = 0, .valve_recirc = 0, .filter_g4_clean = 0, /* Фильтр загрязнен для демонстрации */ .kws_valve = 0, .kws_mode = 1, .pww_valve = 0, .pww_mode = 0, .heater_on = 0, .fan_on = 0, .filter_h13_clean = 1, .year = 2026, .month = 3, .day = 10, .hour = 12, .minute = 0 }; /* Module configuration for drag&drop */ typedef enum { MODULE_VALVE_INLET = 0, MODULE_VALVE_RECIRC, MODULE_FILTER_G4, MODULE_KWS, MODULE_PWW, MODULE_HEATER, MODULE_FAN, MODULE_FILTER_H13, MODULE_CHANNEL, MODULE_COUNT } module_type_t; typedef struct { module_type_t type; int enabled; /* 1=активен (на линии), 0=неактивен (внизу) */ int position; /* позиция в ряду (0-7) */ lv_obj_t *obj; /* указатель на объект */ lv_obj_t *img; /* указатель на изображение */ lv_obj_t *label_name; /* название модуля */ lv_obj_t *label_value; /* указатель на значение */ } module_config_t; static module_config_t modules[MODULE_COUNT] = { {MODULE_VALVE_INLET, 1, 0, NULL, NULL, NULL, NULL}, {MODULE_VALVE_RECIRC, 1, 1, NULL, NULL, NULL, NULL}, {MODULE_FILTER_G4, 1, 2, NULL, NULL, NULL, NULL}, {MODULE_KWS, 1, 3, NULL, NULL, NULL, NULL}, {MODULE_PWW, 1, 4, NULL, NULL, NULL, NULL}, {MODULE_HEATER, 1, 5, NULL, NULL, NULL, NULL}, {MODULE_FAN, 1, 6, NULL, NULL, NULL, NULL}, {MODULE_FILTER_H13, 1, 7, NULL, NULL, NULL, NULL}, {MODULE_CHANNEL, 1, 8, NULL, NULL, NULL, NULL}, }; /* Module dimensions and positions */ #define MODULE_WIDTH 70 #define MODULE_HEIGHT 95 #define MODULE_SPACING 3 #define MODULES_Y 40 /* Fixed Y position for all modules */ /* Scale factor: 256=100% (no transform, saves memory), 140~55% */ #define MODULE_SCALE 256 /* Changed from 140 to avoid out-of-memory issues */ /* Panel for schema (needed globally for drag) */ static lv_obj_t *schema_panel = NULL; /* Mode control buttons */ static lv_obj_t *btn_stop_mode = NULL; static lv_obj_t *btn_standby_mode = NULL; static lv_obj_t *btn_work_mode = NULL; static lv_obj_t *lbl_stop_mode = NULL; static lv_obj_t *lbl_standby_mode = NULL; static lv_obj_t *lbl_work_mode = NULL; /* Settings screen module objects (separate from main screen) */ static lv_obj_t *settings_module_obj[MODULE_COUNT] = {NULL}; static lv_obj_t *settings_module_img[MODULE_COUNT] = {NULL}; static lv_obj_t *settings_module_label_name[MODULE_COUNT] = {NULL}; static lv_obj_t *settings_module_label_value[MODULE_COUNT] = {NULL}; /* Long press timer for settings screen */ static lv_timer_t *longpress_timer = NULL; static int longpress_module_idx = -1; static bool longpress_fired = false; /* suppress click after long press */ /* Alarm log structure */ #define MAX_ALARMS 20 typedef struct { char message[80]; char timestamp[20]; int active; int confirmed; /* 1=подтверждена */ char confirm_timestamp[20]; /* дата подтверждения */ } alarm_entry_t; static alarm_entry_t alarm_log[MAX_ALARMS]; static int alarm_count = 0; /* Pagination state for alarm log */ static int alarm_page = 0; #define ALARMS_PER_PAGE 4 /* Pagination state for setpoints (Work mode) */ static int setpoint_work_page = 0; #define SETPOINTS_PER_PAGE 4 /* Pagination state for setpoints (Standby mode) */ static int setpoint_standby_page = 0; /* Pagination state for config screen */ static int config_page = 0; #define CONFIG_ITEMS_PER_PAGE 6 /* Settings/Setpoints for Work and Standby modes */ typedef struct { float temp; /* Температура */ float hum; /* Влажность */ float valve_inlet; /* Клапан притока */ float valve_recirc; /* Клапан рециркуляции */ uint8_t heater; /* 0=ВЫКЛ, 1=ВКЛ, 2=АВТО */ uint8_t fan; /* 0=ВЫКЛ, 1=ВКЛ */ uint8_t kws_auto; /* 1=АВТО, 0=ручной режим */ float kws_manual; /* Значение для ручного режима */ uint8_t pww_auto; /* 1=АВТО, 0=ручной режим */ float pww_manual; /* Значение для ручного режима */ } setpoint_mode_t; static setpoint_mode_t setpoint_work = { .temp = 22.0f, .hum = 50.0f, .valve_inlet = 75.0f, .valve_recirc = 25.0f, .heater = 2, .fan = 1, .kws_auto = 1, .kws_manual = 0, .pww_auto = 1, .pww_manual = 0 }; static setpoint_mode_t setpoint_standby = { .temp = 18.0f, .hum = 45.0f, .valve_inlet = 35.0f, .valve_recirc = 40.0f, .heater = 0, .fan = 1, .kws_auto = 1, .kws_manual = 0, .pww_auto = 1, .pww_manual = 0 }; /* Configuration parameters (Настройки - с паролем) */ typedef struct { float temp_room_offset; float hum_room_offset; float temp_channel_offset; float hum_channel_offset; float valve_inlet_offset; float valve_recirc_offset; uint8_t has_recirc_valve; uint8_t has_heater; float pid_pww_kp; float pid_pww_ti; float pid_pww_d; float pid_kws_kp; float pid_kws_ti; float pid_kws_d; uint8_t force_mode_enabled; uint8_t force_mode_value; uint8_t key_swap_enabled; } config_t; static config_t config = { .temp_room_offset = 0.0f, .hum_room_offset = 0.0f, .temp_channel_offset = 0.0f, .hum_channel_offset = 0.0f, .valve_inlet_offset = 0.0f, .valve_recirc_offset = 0.0f, .has_recirc_valve = 1, .has_heater = 1, .pid_pww_kp = 1.0f, .pid_pww_ti = 10.0f, .pid_pww_d = 0.0f, .pid_kws_kp = 1.0f, .pid_kws_ti = 10.0f, .pid_kws_d = 0.0f, .force_mode_enabled = 0, .force_mode_value = 1, .key_swap_enabled = 1 }; /* Filter replacement dates */ static char filter_g4_date[12] = "01.01.2026"; static char filter_h13_date[12] = "01.01.2026"; /* ── Modbus Slave Emulation ──────────────────────── */ typedef struct { /* Command registers (written by master/UI setpoints) */ float target_temp; /* Target room temperature °C */ float target_hum; /* Target room humidity % */ uint8_t cmd_valve_inlet; /* Commanded inlet valve 0-100% */ uint8_t cmd_valve_recirc; /* Commanded recirc valve 0-100% */ uint8_t cmd_kws_valve; /* Commanded KWS valve 0-100% */ uint8_t cmd_pww_valve; /* Commanded PWW valve 0-100% */ uint8_t cmd_heater; /* Commanded heater 0/1 */ uint8_t cmd_fan; /* Commanded fan 0/1 */ uint8_t kws_auto; /* KWS auto mode flag */ uint8_t pww_auto; /* PWW auto mode flag */ /* Feedback registers (simulated sensor readings) */ int16_t fb_temp_room; /* Simulated room temp x10 */ int16_t fb_hum_room; /* Simulated room humidity x10 */ int16_t fb_temp_channel; /* Simulated channel temp x10 */ int16_t fb_hum_channel; /* Simulated channel humidity x10 */ } modbus_slave_t; static modbus_slave_t slave = { .target_temp = 22.0f, .target_hum = 50.0f, .cmd_valve_inlet = 0, .cmd_valve_recirc = 0, .cmd_kws_valve = 0, .cmd_pww_valve = 0, .cmd_heater = 0, .cmd_fan = 0, .kws_auto = 1, .pww_auto = 1, .fb_temp_room = 200, .fb_hum_room = 450, .fb_temp_channel = 200, .fb_hum_channel = 500, }; /* Password and settings */ #define PASSWORD "1234" static char password_input[10] = ""; static lv_obj_t *popup_password = NULL; static lv_obj_t *lbl_password_display = NULL; static int password_target = 0; /* 0=настройки, 1=очистка журнала фильтров */ /* Parameter editor */ static lv_obj_t *popup_param_editor = NULL; static lv_obj_t *lbl_param_value = NULL; static char param_value_str[20] = ""; static float *param_being_edited = NULL; static char param_name[50] = ""; static int param_is_percentage = 0; /* Configuration parameters */ /* (moved to config_t structure above) */ /* UI element references for updates */ static lv_obj_t *lbl_temp_room = NULL; static lv_obj_t *lbl_hum_room = NULL; static lv_obj_t *lbl_temp_channel = NULL; static lv_obj_t *lbl_hum_channel = NULL; static lv_obj_t *lbl_room_num = NULL; static lv_obj_t *lbl_master_slave = NULL; static lv_obj_t *lbl_season = NULL; static lv_obj_t *lbl_mode = NULL; static lv_obj_t *lbl_datetime = NULL; /* Popup windows */ static lv_obj_t *popup_readings = NULL; static lv_obj_t *popup_alarm = NULL; static lv_obj_t *popup_module_config = NULL; /* Setpoints screen UI */ static lv_obj_t *lbl_work_temp = NULL; static lv_obj_t *lbl_work_hum = NULL; static lv_obj_t *lbl_standby_temp = NULL; static lv_obj_t *lbl_standby_hum = NULL; /* Forward declarations */ static void create_popup_readings(void); static void create_popup_alarm(const char *message); static void add_alarm_to_log(const char *message, int active); static void btn_log_cb(lv_event_t *e); static void btn_settings_cb(lv_event_t *e); static void btn_config_cb(lv_event_t *e); static void create_popup_password(int target); static void password_keypad_cb(lv_event_t *e); static void create_popup_param_editor(const char *name, float *param_ptr, int is_percentage); static void param_keypad_cb(lv_event_t *e); static void close_popup_cb(lv_event_t *e); static void btn_back_to_main_cb(lv_event_t *e); static void create_screen_main(void); static void create_screen_screen1(void); static void create_screen_config(void); static void create_screen_screen2(void); static void update_module_display(int module_idx); static void update_settings_module_display(int module_idx); static void refresh_setpoints_display(void); static void module_settings_click_cb(lv_event_t *e); static void module_settings_press_cb(lv_event_t *e); static void module_settings_longpress_timer_cb(lv_timer_t *timer); static void create_popup_module_config(int module_idx); static void btn_confirm_alarm_cb(lv_event_t *e); static void btn_alarm_page_prev_cb(lv_event_t *e); static void btn_alarm_page_next_cb(lv_event_t *e); static void btn_clear_log_cb(lv_event_t *e); static void btn_setpoint_work_prev_cb(lv_event_t *e); static void btn_setpoint_work_next_cb(lv_event_t *e); static void btn_setpoint_standby_prev_cb(lv_event_t *e); static void btn_setpoint_standby_next_cb(lv_event_t *e); static void btn_config_page_prev_cb(lv_event_t *e); static void btn_config_page_next_cb(lv_event_t *e); static void create_screen_screen3(void); static lv_obj_t* create_module_widget(lv_obj_t *parent, int module_idx, int x, int y); static void update_mode_button_colors(void); static void sync_setpoints_to_slave(void); static void modbus_simulation_timer_cb(lv_timer_t *timer); /* Calculate season from month: 0=Winter, 1=Summer, 2=Transition */ static uint8_t calc_season_from_month(uint8_t month) { if (month == 12 || month == 1 || month == 2) return 0; /* Зима */ if (month == 6 || month == 7 || month == 8) return 1; /* Лето */ return 2; /* Переход (весна/осень) */ } /* Get image for module type */ static const lv_image_dsc_t* get_module_image(int module_idx) { switch (module_idx) { case MODULE_VALVE_INLET: case MODULE_VALVE_RECIRC: return &img_valve; case MODULE_FILTER_G4: return &img_filter_g4; case MODULE_KWS: return state.kws_mode ? &img_heater : &img_cooler; case MODULE_PWW: return state.pww_mode ? &img_heater : &img_cooler; case MODULE_HEATER: return &img_electric_heater; case MODULE_FAN: return &img_fan; case MODULE_FILTER_H13: return &img_filter_h13; case MODULE_CHANNEL: return &img_channel; default: return &img_pipe; } } /* Get name for module type */ static const char* get_module_name(int module_idx) { switch (module_idx) { case MODULE_VALVE_INLET: return "Вход"; case MODULE_VALVE_RECIRC: return "Рецирк"; case MODULE_FILTER_G4: return "G4"; case MODULE_KWS: return "KWS"; case MODULE_PWW: return "PWW"; case MODULE_HEATER: return "Нагрев"; case MODULE_FAN: return "Вент"; case MODULE_FILTER_H13: return "H13"; case MODULE_CHANNEL: return "Канал"; default: return ""; } } /* Settings screen: Module click handler (short press) */ static void module_settings_click_cb(lv_event_t *e) { lv_event_code_t code = lv_event_get_code(e); int module_idx = (int)(intptr_t)lv_event_get_user_data(e); if (code == LV_EVENT_CLICKED) { /* If long press already fired, just consume the click */ if (longpress_fired) { longpress_fired = false; return; } /* Cancel long press timer - this was a short click, not a long press */ if (longpress_timer) { lv_timer_del(longpress_timer); longpress_timer = NULL; longpress_module_idx = -1; } /* Short click - open module config popup */ create_popup_module_config(module_idx); } } /* Settings screen: Module press handler (start long press timer) */ static void module_settings_press_cb(lv_event_t *e) { int module_idx = (int)(intptr_t)lv_event_get_user_data(e); /* Reset long press flag */ longpress_fired = false; /* Cancel existing timer if any */ if (longpress_timer) { lv_timer_del(longpress_timer); longpress_timer = NULL; } /* Start long press timer (500ms) */ longpress_module_idx = module_idx; longpress_timer = lv_timer_create(module_settings_longpress_timer_cb, 500, NULL); lv_timer_set_repeat_count(longpress_timer, 1); } /* Settings screen: Long press timer callback */ static void module_settings_longpress_timer_cb(lv_timer_t *timer) { (void)timer; if (longpress_module_idx >= 0 && longpress_module_idx < MODULE_COUNT) { /* Toggle module enabled state */ modules[longpress_module_idx].enabled = !modules[longpress_module_idx].enabled; int enabled = modules[longpress_module_idx].enabled; /* Update image opacity */ if (settings_module_img[longpress_module_idx]) { lv_obj_set_style_img_opa(settings_module_img[longpress_module_idx], enabled ? LV_OPA_COVER : LV_OPA_30, 0); } /* Update label colors (grey when disabled) */ if (settings_module_label_name[longpress_module_idx]) { lv_obj_set_style_text_color(settings_module_label_name[longpress_module_idx], lv_color_hex(enabled ? COLOR_TEXT_DIM : 0x555555), 0); } if (settings_module_label_value[longpress_module_idx]) { lv_obj_set_style_text_color(settings_module_label_value[longpress_module_idx], lv_color_hex(enabled ? COLOR_TEXT_ACCENT : 0x555555), 0); } update_settings_module_display(longpress_module_idx); } longpress_timer = NULL; longpress_module_idx = -1; longpress_fired = true; /* prevent click callback from opening popup */ } /* Create a module widget with image - simplified, no container box */ static lv_obj_t* create_module_widget(lv_obj_t *parent, int module_idx, int x, int y) { /* Container for module - transparent, no border */ lv_obj_t *cont = lv_obj_create(parent); lv_obj_set_pos(cont, x, y); lv_obj_set_size(cont, MODULE_WIDTH, MODULE_HEIGHT); lv_obj_set_style_bg_opa(cont, LV_OPA_TRANSP, 0); /* Transparent background */ lv_obj_set_style_border_width(cont, 0, 0); /* No border */ lv_obj_set_style_pad_all(cont, 2, 0); lv_obj_clear_flag(cont, LV_OBJ_FLAG_SCROLLABLE); lv_obj_clear_flag(cont, LV_OBJ_FLAG_CLICKABLE); /* Not clickable on main screen */ /* Image */ lv_obj_t *img = lv_image_create(cont); lv_image_set_src(img, get_module_image(module_idx)); lv_image_set_scale(img, MODULE_SCALE); lv_obj_align(img, LV_ALIGN_CENTER, 0, 0); /* Module name label */ lv_obj_t *lbl_name = lv_label_create(cont); lv_label_set_text(lbl_name, get_module_name(module_idx)); lv_obj_set_style_text_font(lbl_name, &montserrat_16_ru_en, 0); /* Value label */ lv_obj_t *lbl_value = lv_label_create(cont); lv_obj_set_style_text_font(lbl_value, &montserrat_16_ru_en, 0); if (module_idx == MODULE_CHANNEL) { /* Channel: name at top (same as others), value centered on black square */ lv_obj_set_style_text_color(lbl_name, lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_align(lbl_name, LV_ALIGN_TOP_MID, 0, -5); lv_obj_set_style_text_color(lbl_value, lv_color_hex(COLOR_TEXT_ACCENT), 0); lv_obj_align(lbl_value, LV_ALIGN_CENTER, 0, 5); lv_obj_set_style_text_align(lbl_value, LV_TEXT_ALIGN_CENTER, 0); } else { /* Standard: name at top, value at bottom */ lv_obj_set_style_text_color(lbl_name, lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_align(lbl_name, LV_ALIGN_TOP_MID, 0, -5); lv_obj_set_style_text_color(lbl_value, lv_color_hex(COLOR_TEXT_ACCENT), 0); lv_obj_align(lbl_value, LV_ALIGN_BOTTOM_MID, 0, -2); } /* Grey out disabled modules on main screen too */ if (!modules[module_idx].enabled) { lv_obj_set_style_img_opa(img, LV_OPA_30, 0); lv_obj_set_style_text_color(lbl_name, lv_color_hex(0x555555), 0); lv_obj_set_style_text_color(lbl_value, lv_color_hex(0x555555), 0); } modules[module_idx].obj = cont; modules[module_idx].img = img; modules[module_idx].label_name = lbl_name; modules[module_idx].label_value = lbl_value; return cont; } /* Apply setpoints to system state based on current mode */ static void apply_mode_setpoints(void) { setpoint_mode_t *sp = (state.mode == 2) ? &setpoint_work : &setpoint_standby; if (state.mode == 0) { /* Остановка - выключаем активные элементы */ state.heater_on = 0; state.fan_on = 0; } else { /* Дежурный или Рабочий режим - применяем уставки */ /* Only apply if module is enabled */ if (modules[MODULE_VALVE_INLET].enabled) state.valve_inlet = (uint8_t)sp->valve_inlet; if (modules[MODULE_VALVE_RECIRC].enabled) state.valve_recirc = (uint8_t)sp->valve_recirc; /* Вентилятор - выключен если модуль отключён */ state.fan_on = modules[MODULE_FAN].enabled ? sp->fan : 0; /* Электронагреватель */ if (!modules[MODULE_HEATER].enabled) { state.heater_on = 0; } else if (sp->heater == 0) { state.heater_on = 0; } else if (sp->heater == 1) { state.heater_on = 1; } /* heater == 2 (АВТО) - управляется автоматикой */ /* KWS и PWW */ if (modules[MODULE_KWS].enabled && !sp->kws_auto) { state.kws_valve = (uint8_t)sp->kws_manual; } if (modules[MODULE_PWW].enabled && !sp->pww_auto) { state.pww_valve = (uint8_t)sp->pww_manual; } } /* Sync setpoints to modbus slave registers */ sync_setpoints_to_slave(); } /* Write current setpoints to modbus slave command registers */ static void sync_setpoints_to_slave(void) { setpoint_mode_t *sp = (state.mode == 2) ? &setpoint_work : &setpoint_standby; if (state.mode == 0) { slave.target_temp = 15.0f; /* maintain minimum when stopped */ slave.target_hum = 40.0f; slave.cmd_valve_inlet = 0; slave.cmd_valve_recirc = 0; slave.cmd_kws_valve = 0; slave.cmd_pww_valve = 0; slave.cmd_heater = 0; slave.cmd_fan = 0; slave.kws_auto = 1; slave.pww_auto = 1; } else { slave.target_temp = sp->temp; slave.target_hum = sp->hum; slave.cmd_valve_inlet = (uint8_t)sp->valve_inlet; slave.cmd_valve_recirc = (uint8_t)sp->valve_recirc; slave.cmd_fan = modules[MODULE_FAN].enabled ? sp->fan : 0; slave.cmd_heater = (modules[MODULE_HEATER].enabled && sp->heater == 1) ? 1 : 0; slave.kws_auto = sp->kws_auto; slave.pww_auto = sp->pww_auto; if (!sp->kws_auto) slave.cmd_kws_valve = (uint8_t)sp->kws_manual; if (!sp->pww_auto) slave.cmd_pww_valve = (uint8_t)sp->pww_manual; } } /* Update mode button colors based on current mode */ static void update_mode_button_colors(void) { if (!btn_stop_mode || !btn_standby_mode || !btn_work_mode) return; if (!lbl_stop_mode || !lbl_standby_mode || !lbl_work_mode) return; /* Reset all buttons to default color */ lv_obj_set_style_bg_color(btn_stop_mode, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_bg_color(btn_standby_mode, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_bg_color(btn_work_mode, lv_color_hex(COLOR_BG_BTN), 0); /* Reset text colors to original */ lv_obj_set_style_text_color(lbl_stop_mode, lv_color_hex(COLOR_DANGER), 0); lv_obj_set_style_text_color(lbl_standby_mode, lv_color_hex(COLOR_WARNING), 0); lv_obj_set_style_text_color(lbl_work_mode, lv_color_hex(COLOR_ACCENT), 0); /* Highlight active mode button with background and change text to black */ if (state.mode == 0) { lv_obj_set_style_bg_color(btn_stop_mode, lv_color_hex(COLOR_DANGER), 0); lv_obj_set_style_text_color(lbl_stop_mode, lv_color_hex(0x000000), 0); } else if (state.mode == 1) { lv_obj_set_style_bg_color(btn_standby_mode, lv_color_hex(COLOR_WARNING), 0); lv_obj_set_style_text_color(lbl_standby_mode, lv_color_hex(0x000000), 0); } else if (state.mode == 2) { lv_obj_set_style_bg_color(btn_work_mode, lv_color_hex(COLOR_ACCENT), 0); lv_obj_set_style_text_color(lbl_work_mode, lv_color_hex(0x000000), 0); } } /* Button callbacks */ static void btn_stop_cb(lv_event_t *e) { (void)e; state.mode = 0; apply_mode_setpoints(); update_mode_button_colors(); gui_update_values(); } static void btn_standby_cb(lv_event_t *e) { (void)e; state.mode = 1; apply_mode_setpoints(); update_mode_button_colors(); gui_update_values(); } static void btn_work_cb(lv_event_t *e) { (void)e; state.mode = 2; apply_mode_setpoints(); update_mode_button_colors(); gui_update_values(); } static void btn_menu_cb(lv_event_t *e) { (void)e; if (!scr_screen1) { create_screen_screen1(); } lv_scr_load(scr_screen1); } static void btn_back_to_main_cb(lv_event_t *e) { (void)e; /* Free non-main screens to save memory */ if (scr_screen1) { lv_obj_del(scr_screen1); scr_screen1 = NULL; } if (scr_screen2) { lv_obj_del(scr_screen2); scr_screen2 = NULL; } if (scr_screen3) { lv_obj_del(scr_screen3); scr_screen3 = NULL; } if (scr_screen4) { lv_obj_del(scr_screen4); scr_screen4 = NULL; } /* Rebuild main screen so it picks up all state changes */ if (scr_main) { lv_obj_del(scr_main); scr_main = NULL; } create_screen_main(); lv_scr_load(scr_main); } static void btn_readings_cb(lv_event_t *e) { (void)e; create_popup_readings(); } static void btn_confirm_alarm_cb(lv_event_t *e) { int alarm_idx = (int)(intptr_t)lv_event_get_user_data(e); if (alarm_idx >= 0 && alarm_idx < alarm_count) { alarm_log[alarm_idx].confirmed = 1; alarm_log[alarm_idx].active = 0; snprintf(alarm_log[alarm_idx].confirm_timestamp, 20, "%02d.%02d.%04d %02d:%02d", state.day, state.month, state.year, state.hour, state.minute); } /* Refresh journal screen */ if (scr_screen2) { lv_obj_del(scr_screen2); scr_screen2 = NULL; } create_screen_screen2(); lv_scr_load(scr_screen2); } static void btn_alarm_page_prev_cb(lv_event_t *e) { (void)e; if (alarm_page > 0) { alarm_page--; if (scr_screen2) { lv_obj_del(scr_screen2); scr_screen2 = NULL; } create_screen_screen2(); lv_scr_load(scr_screen2); } } static void btn_alarm_page_next_cb(lv_event_t *e) { (void)e; int total_pages = (alarm_count + ALARMS_PER_PAGE - 1) / ALARMS_PER_PAGE; if (alarm_page < total_pages - 1) { alarm_page++; if (scr_screen2) { lv_obj_del(scr_screen2); scr_screen2 = NULL; } create_screen_screen2(); lv_scr_load(scr_screen2); } } static void btn_clear_log_cb(lv_event_t *e) { (void)e; /* Check if any active alarms */ int has_active = 0; for (int i = 0; i < alarm_count; i++) { if (alarm_log[i].active) { has_active = 1; break; } } if (!has_active) { alarm_count = 0; alarm_page = 0; /* Refresh journal screen */ if (scr_screen2) { lv_obj_del(scr_screen2); scr_screen2 = NULL; } create_screen_screen2(); lv_scr_load(scr_screen2); } } /* Setpoints pagination callbacks - Work mode */ static void btn_setpoint_work_prev_cb(lv_event_t *e) { (void)e; if (setpoint_work_page > 0) { setpoint_work_page--; if (scr_screen3) { lv_obj_del(scr_screen3); scr_screen3 = NULL; } create_screen_screen3(); lv_scr_load(scr_screen3); } } static void btn_setpoint_work_next_cb(lv_event_t *e) { (void)e; /* 8 parameters total, 4 per page = 2 pages */ if (setpoint_work_page < 1) { setpoint_work_page++; if (scr_screen3) { lv_obj_del(scr_screen3); scr_screen3 = NULL; } create_screen_screen3(); lv_scr_load(scr_screen3); } } /* Setpoints pagination callbacks - Standby mode */ static void btn_setpoint_standby_prev_cb(lv_event_t *e) { (void)e; if (setpoint_standby_page > 0) { setpoint_standby_page--; if (scr_screen3) { lv_obj_del(scr_screen3); scr_screen3 = NULL; } create_screen_screen3(); lv_scr_load(scr_screen3); } } static void btn_setpoint_standby_next_cb(lv_event_t *e) { (void)e; /* 8 parameters total, 4 per page = 2 pages */ if (setpoint_standby_page < 1) { setpoint_standby_page++; if (scr_screen3) { lv_obj_del(scr_screen3); scr_screen3 = NULL; } create_screen_screen3(); lv_scr_load(scr_screen3); } } /* Config pagination callbacks */ static void btn_config_page_prev_cb(lv_event_t *e) { (void)e; if (config_page > 0) { config_page--; if (scr_screen4) { lv_obj_del(scr_screen4); scr_screen4 = NULL; } create_screen_config(); lv_scr_load(scr_screen4); } } static void btn_config_page_next_cb(lv_event_t *e) { (void)e; /* Total 2 pages now */ if (config_page < 1) { config_page++; if (scr_screen4) { lv_obj_del(scr_screen4); scr_screen4 = NULL; } create_screen_config(); lv_scr_load(scr_screen4); } } static void close_popup_cb(lv_event_t *e) { lv_obj_t *popup = lv_event_get_user_data(e); if (popup) { lv_obj_del(popup); } if (popup == popup_readings) popup_readings = NULL; if (popup == popup_alarm) popup_alarm = NULL; if (popup == popup_module_config) { popup_module_config = NULL; /* Just update module displays, don't rebuild entire screen */ for (int i = 0; i < MODULE_COUNT; i++) { update_settings_module_display(i); } } if (popup == popup_password) { popup_password = NULL; memset(password_input, 0, sizeof(password_input)); } if (popup == popup_param_editor) { popup_param_editor = NULL; memset(param_value_str, 0, sizeof(param_value_str)); param_being_edited = NULL; } } static void btn_log_cb(lv_event_t *e) { (void)e; if (!scr_screen2) { create_screen_screen2(); } lv_scr_load(scr_screen2); } static void btn_settings_cb(lv_event_t *e) { (void)e; if (!scr_screen3) { create_screen_screen3(); } lv_scr_load(scr_screen3); } static void btn_config_cb(lv_event_t *e) { (void)e; password_target = 0; create_popup_password(0); } /* Edit parameter callbacks - Work mode */ static void edit_work_temp_cb(lv_event_t *e) { (void)e; create_popup_param_editor("Темп.", &setpoint_work.temp, 0); } static void edit_work_hum_cb(lv_event_t *e) { (void)e; create_popup_param_editor("Влажность (Рабочий)", &setpoint_work.hum, 1); } static void edit_work_valve_inlet_cb(lv_event_t *e) { (void)e; create_popup_param_editor("Клапан притока (Рабочий)", &setpoint_work.valve_inlet, 1); } static void edit_work_valve_recirc_cb(lv_event_t *e) { (void)e; create_popup_param_editor("Клапан рециркуляции (Рабочий)", &setpoint_work.valve_recirc, 1); } /* Edit parameter callbacks - Standby mode */ static void edit_standby_temp_cb(lv_event_t *e) { (void)e; create_popup_param_editor("Температура (Дежурный)", &setpoint_standby.temp, 0); } static void edit_standby_hum_cb(lv_event_t *e) { (void)e; create_popup_param_editor("Влажность (Дежурный)", &setpoint_standby.hum, 1); } static void edit_standby_valve_inlet_cb(lv_event_t *e) { (void)e; create_popup_param_editor("Клапан притока (Дежурный)", &setpoint_standby.valve_inlet, 1); } static void edit_standby_valve_recirc_cb(lv_event_t *e) { (void)e; create_popup_param_editor("Клапан рециркуляции (Дежурный)", &setpoint_standby.valve_recirc, 1); } /* Heater mode callbacks */ static void edit_work_heater_cb(lv_event_t *e) { (void)e; setpoint_work.heater = (setpoint_work.heater + 1) % 3; if (state.mode == 2) { apply_mode_setpoints(); gui_update_values(); } refresh_setpoints_display(); } static void edit_standby_heater_cb(lv_event_t *e) { (void)e; setpoint_standby.heater = (setpoint_standby.heater + 1) % 3; if (state.mode == 1) { apply_mode_setpoints(); gui_update_values(); } refresh_setpoints_display(); } /* Fan toggle callbacks */ static void edit_work_fan_cb(lv_event_t *e) { (void)e; setpoint_work.fan = !setpoint_work.fan; if (state.mode == 2) { apply_mode_setpoints(); gui_update_values(); } refresh_setpoints_display(); } static void edit_standby_fan_cb(lv_event_t *e) { (void)e; setpoint_standby.fan = !setpoint_standby.fan; if (state.mode == 1) { apply_mode_setpoints(); gui_update_values(); } refresh_setpoints_display(); } /* KWS mode callbacks */ static void edit_work_kws_cb(lv_event_t *e) { (void)e; if (setpoint_work.kws_auto) { setpoint_work.kws_auto = 0; setpoint_work.kws_manual = 50.0f; if (state.mode == 2) { apply_mode_setpoints(); gui_update_values(); } refresh_setpoints_display(); } else { create_popup_param_editor("KWS ручной (Рабочий)", &setpoint_work.kws_manual, 1); } } static void edit_standby_kws_cb(lv_event_t *e) { (void)e; if (setpoint_standby.kws_auto) { setpoint_standby.kws_auto = 0; setpoint_standby.kws_manual = 50.0f; if (state.mode == 1) { apply_mode_setpoints(); gui_update_values(); } refresh_setpoints_display(); } else { create_popup_param_editor("KWS ручной (Дежурный)", &setpoint_standby.kws_manual, 1); } } /* PWW mode callbacks */ static void edit_work_pww_cb(lv_event_t *e) { (void)e; if (setpoint_work.pww_auto) { setpoint_work.pww_auto = 0; setpoint_work.pww_manual = 50.0f; if (state.mode == 2) { apply_mode_setpoints(); gui_update_values(); } refresh_setpoints_display(); } else { create_popup_param_editor("PWW ручной (Рабочий)", &setpoint_work.pww_manual, 1); } } static void edit_standby_pww_cb(lv_event_t *e) { (void)e; if (setpoint_standby.pww_auto) { setpoint_standby.pww_auto = 0; setpoint_standby.pww_manual = 50.0f; if (state.mode == 1) { apply_mode_setpoints(); gui_update_values(); } refresh_setpoints_display(); } else { create_popup_param_editor("PWW ручной (Дежурный)", &setpoint_standby.pww_manual, 1); } } /* Configuration edit callbacks */ static void edit_config_offset_cb(lv_event_t *e) { float *offset_ptr = (float*)lv_event_get_user_data(e); create_popup_param_editor("Коррекция", offset_ptr, 0); } static void toggle_recirc_valve_cb(lv_event_t *e) { (void)e; config.has_recirc_valve = !config.has_recirc_valve; /* Invalidate screen3 so it rebuilds on next visit */ if (scr_screen3) { lv_obj_del(scr_screen3); scr_screen3 = NULL; } } static void toggle_heater_cb(lv_event_t *e) { (void)e; config.has_heater = !config.has_heater; /* Invalidate screen3 so it rebuilds on next visit */ if (scr_screen3) { lv_obj_del(scr_screen3); scr_screen3 = NULL; } } static void edit_pid_pww_kp_cb(lv_event_t *e) { (void)e; create_popup_param_editor("PWW Kp", &config.pid_pww_kp, 0); } static void edit_pid_pww_ti_cb(lv_event_t *e) { (void)e; create_popup_param_editor("PWW Ti", &config.pid_pww_ti, 0); } static void edit_pid_pww_d_cb(lv_event_t *e) { (void)e; create_popup_param_editor("PWW D", &config.pid_pww_d, 0); } static void edit_pid_kws_kp_cb(lv_event_t *e) { (void)e; create_popup_param_editor("KWS Kp", &config.pid_kws_kp, 0); } static void edit_pid_kws_ti_cb(lv_event_t *e) { (void)e; create_popup_param_editor("KWS Ti", &config.pid_kws_ti, 0); } static void edit_pid_kws_d_cb(lv_event_t *e) { (void)e; create_popup_param_editor("KWS D", &config.pid_kws_d, 0); } static void toggle_force_mode_cb(lv_event_t *e) { (void)e; config.force_mode_enabled = !config.force_mode_enabled; /* Rebuild config screen to reflect toggle change */ if (scr_screen4) { lv_obj_del(scr_screen4); scr_screen4 = NULL; } create_screen_config(); lv_scr_load(scr_screen4); } static void toggle_key_swap_cb(lv_event_t *e) { (void)e; config.key_swap_enabled = !config.key_swap_enabled; /* Rebuild config screen to reflect toggle change */ if (scr_screen4) { lv_obj_del(scr_screen4); scr_screen4 = NULL; } create_screen_config(); lv_scr_load(scr_screen4); } /* Save setpoints callback (screen3) */ static void btn_save_setpoints_cb(lv_event_t *e) { (void)e; apply_mode_setpoints(); gui_update_values(); } /* Default setpoints callback (screen3) */ static void btn_default_setpoints_cb(lv_event_t *e) { (void)e; setpoint_work.temp = 22.0f; setpoint_work.hum = 50.0f; setpoint_work.valve_inlet = 75.0f; setpoint_work.valve_recirc = 25.0f; setpoint_work.heater = 2; setpoint_work.fan = 1; setpoint_work.kws_auto = 1; setpoint_work.kws_manual = 0; setpoint_work.pww_auto = 1; setpoint_work.pww_manual = 0; setpoint_standby.temp = 18.0f; setpoint_standby.hum = 45.0f; setpoint_standby.valve_inlet = 35.0f; setpoint_standby.valve_recirc = 40.0f; setpoint_standby.heater = 0; setpoint_standby.fan = 1; setpoint_standby.kws_auto = 1; setpoint_standby.kws_manual = 0; setpoint_standby.pww_auto = 1; setpoint_standby.pww_manual = 0; apply_mode_setpoints(); refresh_setpoints_display(); } /* Save config callback (screen4) */ static void btn_save_config_cb(lv_event_t *e) { (void)e; apply_mode_setpoints(); gui_update_values(); } /* Default config callback (screen4) */ static void btn_default_config_cb(lv_event_t *e) { (void)e; config.temp_room_offset = 0.0f; config.hum_room_offset = 0.0f; config.temp_channel_offset = 0.0f; config.hum_channel_offset = 0.0f; config.valve_inlet_offset = 0.0f; config.valve_recirc_offset = 0.0f; config.pid_pww_kp = 1.0f; config.pid_pww_ti = 10.0f; config.pid_pww_d = 0.0f; config.pid_kws_kp = 1.0f; config.pid_kws_ti = 10.0f; config.pid_kws_d = 0.0f; config.force_mode_enabled = 0; config.force_mode_value = 1; config.key_swap_enabled = 1; if (scr_screen4) { lv_obj_del(scr_screen4); scr_screen4 = NULL; } create_screen_config(); lv_scr_load(scr_screen4); } /* Date/Time editor popup */ static lv_obj_t *popup_datetime = NULL; static lv_obj_t *roller_day = NULL; static lv_obj_t *roller_month = NULL; static lv_obj_t *roller_year = NULL; static lv_obj_t *roller_hour = NULL; static lv_obj_t *roller_minute = NULL; static void datetime_ok_cb(lv_event_t *e) { (void)e; if (roller_day && roller_month && roller_year && roller_hour && roller_minute) { state.day = lv_roller_get_selected(roller_day) + 1; state.month = lv_roller_get_selected(roller_month) + 1; state.year = 2024 + lv_roller_get_selected(roller_year); state.hour = lv_roller_get_selected(roller_hour); state.minute = lv_roller_get_selected(roller_minute); } if (popup_datetime) { lv_obj_del(popup_datetime); popup_datetime = NULL; roller_day = roller_month = roller_year = roller_hour = roller_minute = NULL; } /* Recreate config screen to show updated datetime */ if (scr_screen4) { lv_obj_del(scr_screen4); scr_screen4 = NULL; } create_screen_config(); lv_scr_load(scr_screen4); } static void datetime_close_cb(lv_event_t *e) { (void)e; if (popup_datetime) { lv_obj_del(popup_datetime); popup_datetime = NULL; roller_day = roller_month = roller_year = roller_hour = roller_minute = NULL; } } static void create_popup_datetime(void) { if (popup_datetime) return; popup_datetime = lv_obj_create(lv_screen_active()); lv_obj_set_size(popup_datetime, 420, 280); lv_obj_center(popup_datetime); lv_obj_set_style_bg_color(popup_datetime, lv_color_hex(COLOR_BG_PANEL), 0); lv_obj_set_style_radius(popup_datetime, 8, 0); lv_obj_set_style_border_width(popup_datetime, 1, 0); lv_obj_set_style_border_color(popup_datetime, lv_color_hex(COLOR_BORDER), 0); lv_obj_clear_flag(popup_datetime, LV_OBJ_FLAG_SCROLLABLE); /* Title */ lv_obj_t *lbl = lv_label_create(popup_datetime); lv_label_set_text(lbl, "Установка даты и времени"); lv_obj_set_pos(lbl, 20, 10); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); /* Close button (X) */ lv_obj_t *btn_close = lv_btn_create(popup_datetime); lv_obj_set_pos(btn_close, 370, 6); lv_obj_set_size(btn_close, 32, 32); lv_obj_set_style_bg_color(btn_close, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn_close, 4, 0); lbl = lv_label_create(btn_close); lv_label_set_text(lbl, "X"); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_DANGER), 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn_close, datetime_close_cb, LV_EVENT_CLICKED, NULL); /* Date section label */ lbl = lv_label_create(popup_datetime); lv_label_set_text(lbl, "Дата:"); lv_obj_set_pos(lbl, 20, 50); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); /* Day roller */ char days[31*4]; days[0] = '\0'; for (int i = 1; i <= 31; i++) { char tmp[5]; snprintf(tmp, sizeof(tmp), "%02d\n", i); strcat(days, tmp); } days[strlen(days)-1] = '\0'; roller_day = lv_roller_create(popup_datetime); lv_roller_set_options(roller_day, days, LV_ROLLER_MODE_NORMAL); lv_roller_set_selected(roller_day, state.day - 1, LV_ANIM_OFF); lv_obj_set_pos(roller_day, 80, 45); lv_obj_set_size(roller_day, 60, 80); lv_obj_set_style_text_font(roller_day, &montserrat_16_ru_en, 0); lv_obj_set_style_bg_color(roller_day, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_text_color(roller_day, lv_color_hex(0xFFFFFF), LV_PART_SELECTED); /* Month roller */ roller_month = lv_roller_create(popup_datetime); lv_roller_set_options(roller_month, "01\n02\n03\n04\n05\n06\n07\n08\n09\n10\n11\n12", LV_ROLLER_MODE_NORMAL); lv_roller_set_selected(roller_month, state.month - 1, LV_ANIM_OFF); lv_obj_set_pos(roller_month, 150, 45); lv_obj_set_size(roller_month, 60, 80); lv_obj_set_style_text_font(roller_month, &montserrat_16_ru_en, 0); lv_obj_set_style_bg_color(roller_month, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_text_color(roller_month, lv_color_hex(0xFFFFFF), LV_PART_SELECTED); /* Year roller (2024-2035) */ roller_year = lv_roller_create(popup_datetime); lv_roller_set_options(roller_year, "2024\n2025\n2026\n2027\n2028\n2029\n2030\n2031\n2032\n2033\n2034\n2035", LV_ROLLER_MODE_NORMAL); lv_roller_set_selected(roller_year, state.year - 2024, LV_ANIM_OFF); lv_obj_set_pos(roller_year, 220, 45); lv_obj_set_size(roller_year, 80, 80); lv_obj_set_style_text_font(roller_year, &montserrat_16_ru_en, 0); lv_obj_set_style_bg_color(roller_year, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_text_color(roller_year, lv_color_hex(0xFFFFFF), LV_PART_SELECTED); /* Time section label */ lbl = lv_label_create(popup_datetime); lv_label_set_text(lbl, "Время:"); lv_obj_set_pos(lbl, 20, 140); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); /* Hour roller */ char hours[24*4]; hours[0] = '\0'; for (int i = 0; i < 24; i++) { char tmp[5]; snprintf(tmp, sizeof(tmp), "%02d\n", i); strcat(hours, tmp); } hours[strlen(hours)-1] = '\0'; roller_hour = lv_roller_create(popup_datetime); lv_roller_set_options(roller_hour, hours, LV_ROLLER_MODE_NORMAL); lv_roller_set_selected(roller_hour, state.hour, LV_ANIM_OFF); lv_obj_set_pos(roller_hour, 80, 135); lv_obj_set_size(roller_hour, 60, 80); lv_obj_set_style_text_font(roller_hour, &montserrat_16_ru_en, 0); lv_obj_set_style_bg_color(roller_hour, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_text_color(roller_hour, lv_color_hex(0xFFFFFF), LV_PART_SELECTED); lbl = lv_label_create(popup_datetime); lv_label_set_text(lbl, ":"); lv_obj_set_pos(lbl, 145, 160); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); /* Minute roller */ char minutes[60*4]; minutes[0] = '\0'; for (int i = 0; i < 60; i++) { char tmp[5]; snprintf(tmp, sizeof(tmp), "%02d\n", i); strcat(minutes, tmp); } minutes[strlen(minutes)-1] = '\0'; roller_minute = lv_roller_create(popup_datetime); lv_roller_set_options(roller_minute, minutes, LV_ROLLER_MODE_NORMAL); lv_roller_set_selected(roller_minute, state.minute, LV_ANIM_OFF); lv_obj_set_pos(roller_minute, 160, 135); lv_obj_set_size(roller_minute, 60, 80); lv_obj_set_style_text_font(roller_minute, &montserrat_16_ru_en, 0); lv_obj_set_style_bg_color(roller_minute, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_text_color(roller_minute, lv_color_hex(0xFFFFFF), LV_PART_SELECTED); /* OK button */ lv_obj_t *btn_ok = lv_btn_create(popup_datetime); lv_obj_set_pos(btn_ok, 160, 230); lv_obj_set_size(btn_ok, 100, 40); lv_obj_set_style_bg_color(btn_ok, lv_color_hex(COLOR_ACCENT), 0); lv_obj_set_style_radius(btn_ok, 6, 0); lbl = lv_label_create(btn_ok); lv_label_set_text(lbl, "OK"); lv_obj_set_style_text_color(lbl, lv_color_hex(0xFFFFFF), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn_ok, datetime_ok_cb, LV_EVENT_CLICKED, NULL); } static void edit_datetime_cb(lv_event_t *e) { (void)e; create_popup_datetime(); } /* Password keypad callback */ static void password_keypad_cb(lv_event_t *e) { const char *key = lv_event_get_user_data(e); int len = strlen(password_input); if (strcmp(key, "C") == 0) { memset(password_input, 0, sizeof(password_input)); } else if (strcmp(key, "OK") == 0) { if (strcmp(password_input, PASSWORD) == 0) { /* Correct password - close popup */ if (popup_password) { lv_obj_del(popup_password); popup_password = NULL; lbl_password_display = NULL; } memset(password_input, 0, sizeof(password_input)); /* Navigate to config screen */ if (password_target == 0) { if (!scr_screen4) { create_screen_config(); } lv_scr_load(scr_screen4); } } else { /* Wrong password */ if (lbl_password_display) { lv_label_set_text(lbl_password_display, "НЕВЕРНЫЙ ПАРОЛЬ!"); lv_obj_set_style_text_color(lbl_password_display, lv_color_hex(COLOR_DANGER), 0); } memset(password_input, 0, sizeof(password_input)); } } else if (len < 8) { strcat(password_input, key); } /* Update display */ if (lbl_password_display) { char display[20] = ""; for (int i = 0; i < (int)strlen(password_input); i++) { strcat(display, "*"); } if (strlen(display) == 0) { lv_label_set_text(lbl_password_display, "Введите пароль"); lv_obj_set_style_text_color(lbl_password_display, lv_color_hex(COLOR_TEXT_DIM), 0); } else { lv_label_set_text(lbl_password_display, display); lv_obj_set_style_text_color(lbl_password_display, lv_color_hex(COLOR_ACCENT), 0); } } } /* Create password popup with keypad */ static void create_popup_password(int target) { if (popup_password) return; password_target = target; memset(password_input, 0, sizeof(password_input)); popup_password = lv_obj_create(lv_screen_active()); lv_obj_set_size(popup_password, 300, 420); lv_obj_center(popup_password); lv_obj_set_style_bg_color(popup_password, lv_color_hex(COLOR_BG_PANEL), 0); lv_obj_set_style_radius(popup_password, 8, 0); lv_obj_set_style_border_width(popup_password, 1, 0); lv_obj_set_style_border_color(popup_password, lv_color_hex(COLOR_BORDER), 0); lv_obj_clear_flag(popup_password, LV_OBJ_FLAG_SCROLLABLE); /* Title */ lv_obj_t *lbl = lv_label_create(popup_password); lv_label_set_text(lbl, "Введите пароль"); lv_obj_set_pos(lbl, 20, 10); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); /* Close button (X) - справа */ lv_obj_t *btn_close = lv_btn_create(popup_password); lv_obj_set_pos(btn_close, 230, 6); lv_obj_set_size(btn_close, 32, 32); lv_obj_set_style_bg_color(btn_close, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn_close, 4, 0); lbl = lv_label_create(btn_close); lv_label_set_text(lbl, "X"); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_DANGER), 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn_close, close_popup_cb, LV_EVENT_CLICKED, popup_password); /* Password display */ lbl_password_display = lv_label_create(popup_password); lv_label_set_text(lbl_password_display, "_ _ _ _"); lv_obj_set_pos(lbl_password_display, 105, 50); lv_obj_set_style_text_color(lbl_password_display, lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl_password_display, &montserrat_16_ru_en, 0); /* Numeric keypad 3x4 */ const char *keys[12] = {"1", "2", "3", "4", "5", "6", "7", "8", "9", "C", "0", "OK"}; int key_size = 60; int key_spacing = 8; int start_x = 30; int start_y = 90; for (int i = 0; i < 12; i++) { int row = i / 3; int col = i % 3; lv_obj_t *btn = lv_btn_create(popup_password); lv_obj_set_pos(btn, start_x + col * (key_size + key_spacing), start_y + row * (key_size + key_spacing)); lv_obj_set_size(btn, key_size, key_size); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 6, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, keys[i]); if (strcmp(keys[i], "C") == 0) { lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_WARNING), 0); } else if (strcmp(keys[i], "OK") == 0) { lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_ACCENT), 0); } else { lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); } lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn, password_keypad_cb, LV_EVENT_CLICKED, (void*)keys[i]); } } /* Parameter editor keypad callback */ static void param_keypad_cb(lv_event_t *e) { const char *key = lv_event_get_user_data(e); int len = strlen(param_value_str); if (strcmp(key, "C") == 0) { memset(param_value_str, 0, sizeof(param_value_str)); } else if (strcmp(key, "<") == 0) { /* Backspace - remove last character */ if (len > 0) { param_value_str[len - 1] = '\0'; } } else if (strcmp(key, ".") == 0) { if (strchr(param_value_str, '.') == NULL) { if (len == 0 || (len == 1 && param_value_str[0] == '-')) { strcat(param_value_str, "0."); } else { strcat(param_value_str, "."); } } } else if (strcmp(key, "-") == 0) { if (len == 0) { strcat(param_value_str, "-"); } else if (param_value_str[0] == '-') { /* Remove minus */ memmove(param_value_str, param_value_str + 1, strlen(param_value_str)); } else { /* Add minus at beginning */ memmove(param_value_str + 1, param_value_str, strlen(param_value_str) + 1); param_value_str[0] = '-'; } } else if (strcmp(key, "OK") == 0) { if (param_being_edited && strlen(param_value_str) > 0) { float val = (float)atof(param_value_str); if (param_is_percentage) { if (val < 0.0f) val = 0.0f; if (val > 100.0f) val = 100.0f; } *param_being_edited = val; } if (popup_param_editor) { lv_obj_del(popup_param_editor); popup_param_editor = NULL; lbl_param_value = NULL; } memset(param_value_str, 0, sizeof(param_value_str)); param_being_edited = NULL; /* Apply setpoints to state so main screen reflects changes */ apply_mode_setpoints(); /* Refresh the current screen */ if (lv_screen_active() == scr_screen3) { refresh_setpoints_display(); } else if (lv_screen_active() == scr_screen4) { /* Rebuild config screen to show updated value */ lv_obj_del(scr_screen4); scr_screen4 = NULL; create_screen_config(); lv_scr_load(scr_screen4); } return; } else if (len < 10) { strcat(param_value_str, key); } if (lbl_param_value) { if (strlen(param_value_str) == 0) { lv_label_set_text(lbl_param_value, "0"); } else { lv_label_set_text(lbl_param_value, param_value_str); } } } /* Create parameter editor popup */ static void create_popup_param_editor(const char *name, float *param_ptr, int is_percentage) { if (popup_param_editor) return; param_being_edited = param_ptr; param_is_percentage = is_percentage; strncpy(param_name, name, 49); param_name[49] = '\0'; memset(param_value_str, 0, sizeof(param_value_str)); snprintf(param_value_str, sizeof(param_value_str), "%.1f", *param_ptr); popup_param_editor = lv_obj_create(lv_screen_active()); lv_obj_set_size(popup_param_editor, 360, 380); lv_obj_center(popup_param_editor); lv_obj_set_style_bg_color(popup_param_editor, lv_color_hex(COLOR_BG_PANEL), 0); lv_obj_set_style_radius(popup_param_editor, 8, 0); lv_obj_set_style_border_width(popup_param_editor, 1, 0); lv_obj_set_style_border_color(popup_param_editor, lv_color_hex(COLOR_BORDER), 0); lv_obj_clear_flag(popup_param_editor, LV_OBJ_FLAG_SCROLLABLE); /* Title - shorter */ lv_obj_t *lbl = lv_label_create(popup_param_editor); lv_label_set_text(lbl, param_name); lv_obj_set_pos(lbl, 10, 10); lv_obj_set_width(lbl, 260); lv_label_set_long_mode(lbl, LV_LABEL_LONG_CLIP); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); /* Close button (X) - справа */ lv_obj_t *btn_close = lv_btn_create(popup_param_editor); lv_obj_set_pos(btn_close, 290, 8); lv_obj_set_size(btn_close, 32, 32); lv_obj_set_style_bg_color(btn_close, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn_close, 4, 0); lbl = lv_label_create(btn_close); lv_label_set_text(lbl, "X"); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_DANGER), 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn_close, close_popup_cb, LV_EVENT_CLICKED, popup_param_editor); /* Value display with unit */ lbl_param_value = lv_label_create(popup_param_editor); lv_label_set_text(lbl_param_value, param_value_str); lv_obj_set_pos(lbl_param_value, 130, 50); lv_obj_set_style_text_color(lbl_param_value, lv_color_hex(COLOR_ACCENT), 0); lv_obj_set_style_text_font(lbl_param_value, &montserrat_16_ru_en, 0); /* Unit label */ lbl = lv_label_create(popup_param_editor); lv_label_set_text(lbl, is_percentage ? "%" : "°C"); lv_obj_set_pos(lbl, 210, 50); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); /* Numeric keypad 4x4 with decimal, minus, backspace */ const char *keys[16] = {"1", "2", "3", "C", "4", "5", "6", "<", "7", "8", "9", "-", "0", ".", "0", "."}; int key_size = 55; int key_spacing = 6; int start_x = 25; int start_y = 85; /* First 3 rows (12 buttons) */ for (int i = 0; i < 12; i++) { int row = i / 4; int col = i % 4; lv_obj_t *btn = lv_btn_create(popup_param_editor); lv_obj_set_pos(btn, start_x + col * (key_size + key_spacing), start_y + row * (key_size + key_spacing)); lv_obj_set_size(btn, key_size, key_size); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 6, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, keys[i]); if (strcmp(keys[i], "C") == 0) { lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_DANGER), 0); } else if (strcmp(keys[i], "<") == 0 || strcmp(keys[i], ".") == 0 || strcmp(keys[i], "-") == 0) { lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_WARNING), 0); } else { lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); } lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn, param_keypad_cb, LV_EVENT_CLICKED, (void*)keys[i]); } /* Bottom row: 0, dot, OK */ lv_obj_t *btn = lv_btn_create(popup_param_editor); lv_obj_set_pos(btn, start_x, start_y + 3 * (key_size + key_spacing)); lv_obj_set_size(btn, key_size, key_size); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 6, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, "0"); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn, param_keypad_cb, LV_EVENT_CLICKED, (void*)"0"); btn = lv_btn_create(popup_param_editor); lv_obj_set_pos(btn, start_x + (key_size + key_spacing), start_y + 3 * (key_size + key_spacing)); lv_obj_set_size(btn, key_size, key_size); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 6, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, "."); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_WARNING), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn, param_keypad_cb, LV_EVENT_CLICKED, (void*)"."); btn = lv_btn_create(popup_param_editor); lv_obj_set_pos(btn, start_x + 2 * (key_size + key_spacing), start_y + 3 * (key_size + key_spacing)); lv_obj_set_size(btn, key_size * 2 + key_spacing, key_size); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_ACCENT), 0); lv_obj_set_style_radius(btn, 6, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, "OK"); lv_obj_set_style_text_color(lbl, lv_color_hex(0xFFFFFF), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn, param_keypad_cb, LV_EVENT_CLICKED, (void*)"OK"); } /* ── Slider infrastructure for module config popup ── */ typedef struct { float *target; lv_obj_t *lbl_value; float scale; /* slider_val * scale = real_val (e.g. 0.1 or 0.01) */ const char *fmt; /* printf format */ } slider_ud_t; #define MAX_SLIDER_UD 6 static slider_ud_t slider_ud_pool[MAX_SLIDER_UD]; static int slider_ud_idx = 0; static void config_slider_cb(lv_event_t *e) { slider_ud_t *ud = (slider_ud_t*)lv_event_get_user_data(e); lv_obj_t *slider = lv_event_get_target(e); int val = lv_slider_get_value(slider); float fval = val * ud->scale; *ud->target = fval; if (ud->lbl_value) { char buf[24]; snprintf(buf, sizeof(buf), ud->fmt, fval); lv_label_set_text(ud->lbl_value, buf); } } /* Helper: create a label + slider + value row inside parent */ static void add_slider_row(lv_obj_t *parent, int y, const char *name, float *target, float min_f, float max_f, float scale, const char *fmt) { if (slider_ud_idx >= MAX_SLIDER_UD) return; slider_ud_t *ud = &slider_ud_pool[slider_ud_idx++]; ud->target = target; ud->scale = scale; ud->fmt = fmt; /* Label */ lv_obj_t *lbl = lv_label_create(parent); lv_label_set_text(lbl, name); lv_obj_set_pos(lbl, 15, y + 2); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); /* Value label */ lv_obj_t *lbl_val = lv_label_create(parent); char buf[24]; snprintf(buf, sizeof(buf), fmt, *target); lv_label_set_text(lbl_val, buf); lv_obj_set_pos(lbl_val, 300, y + 2); lv_obj_set_style_text_color(lbl_val, lv_color_hex(COLOR_ACCENT), 0); lv_obj_set_style_text_font(lbl_val, &montserrat_16_ru_en, 0); ud->lbl_value = lbl_val; /* Slider */ lv_obj_t *slider = lv_slider_create(parent); lv_obj_set_pos(slider, 15, y + 26); lv_obj_set_size(slider, 340, 12); lv_slider_set_range(slider, (int32_t)(min_f / scale), (int32_t)(max_f / scale)); lv_slider_set_value(slider, (int32_t)(*target / scale), LV_ANIM_OFF); lv_obj_set_style_bg_color(slider, lv_color_hex(COLOR_BG_BTN), LV_PART_MAIN); lv_obj_set_style_bg_color(slider, lv_color_hex(COLOR_ACCENT), LV_PART_INDICATOR); lv_obj_set_style_bg_color(slider, lv_color_hex(COLOR_TEXT), LV_PART_KNOB); lv_obj_set_style_pad_all(slider, 4, LV_PART_KNOB); lv_obj_add_event_cb(slider, config_slider_cb, LV_EVENT_VALUE_CHANGED, ud); } /* Create popup window for module configuration (settings screen) - slider-based */ static void create_popup_module_config(int module_idx) { if (popup_module_config) { lv_obj_del(popup_module_config); } slider_ud_idx = 0; /* reset slider userdata pool */ popup_module_config = lv_obj_create(lv_screen_active()); lv_obj_set_size(popup_module_config, 400, 350); lv_obj_center(popup_module_config); lv_obj_set_style_bg_color(popup_module_config, lv_color_hex(COLOR_BG_PANEL), 0); lv_obj_set_style_radius(popup_module_config, 12, 0); lv_obj_set_style_border_width(popup_module_config, 3, 0); lv_obj_set_style_border_color(popup_module_config, lv_color_hex(COLOR_TEXT_ACCENT), 0); lv_obj_clear_flag(popup_module_config, LV_OBJ_FLAG_SCROLLABLE); /* Title */ char title[50]; snprintf(title, sizeof(title), "НАСТРОЙКА: %s", get_module_name(module_idx)); lv_obj_t *lbl = lv_label_create(popup_module_config); lv_label_set_text(lbl, title); lv_obj_set_pos(lbl, 20, 15); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); /* Close button (X) */ lv_obj_t *btn_close = lv_btn_create(popup_module_config); lv_obj_set_pos(btn_close, 330, 10); lv_obj_set_size(btn_close, 35, 35); lv_obj_set_style_bg_color(btn_close, lv_color_hex(COLOR_DANGER), 0); lv_obj_set_style_radius(btn_close, 6, 0); lbl = lv_label_create(btn_close); lv_label_set_text(lbl, "X"); lv_obj_set_style_text_color(lbl, lv_color_hex(0xFFFFFF), 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn_close, close_popup_cb, LV_EVENT_CLICKED, popup_module_config); int y = 55; char buf[100]; switch (module_idx) { case MODULE_VALVE_INLET: add_slider_row(popup_module_config, y, "Коррекция:", &config.valve_inlet_offset, -10.0f, 10.0f, 0.1f, "%+.1f%%"); break; case MODULE_VALVE_RECIRC: add_slider_row(popup_module_config, y, "Коррекция:", &config.valve_recirc_offset, -10.0f, 10.0f, 0.1f, "%+.1f%%"); y += 60; /* Toggle has_recirc_valve */ lbl = lv_label_create(popup_module_config); snprintf(buf, sizeof(buf), "Наличие: %s", config.has_recirc_valve ? "ЕСТЬ" : "НЕТ"); lv_label_set_text(lbl, buf); lv_obj_set_pos(lbl, 30, y); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); { lv_obj_t *btn = lv_btn_create(popup_module_config); lv_obj_set_pos(btn, 250, y - 5); lv_obj_set_size(btn, 100, 35); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, "Переключ"); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_WARNING), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn, toggle_recirc_valve_cb, LV_EVENT_CLICKED, NULL); } break; case MODULE_HEATER: lbl = lv_label_create(popup_module_config); snprintf(buf, sizeof(buf), "Наличие: %s", config.has_heater ? "ЕСТЬ" : "НЕТ"); lv_label_set_text(lbl, buf); lv_obj_set_pos(lbl, 30, y); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); { lv_obj_t *btn = lv_btn_create(popup_module_config); lv_obj_set_pos(btn, 250, y - 5); lv_obj_set_size(btn, 100, 35); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, "Переключ"); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_WARNING), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn, toggle_heater_cb, LV_EVENT_CLICKED, NULL); } break; case MODULE_KWS: lbl = lv_label_create(popup_module_config); lv_label_set_text(lbl, "ПИД регулятор KWS:"); lv_obj_set_pos(lbl, 15, y); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_WARNING), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); y += 25; add_slider_row(popup_module_config, y, "Kp:", &config.pid_kws_kp, 0.0f, 10.0f, 0.01f, "%.2f"); y += 50; add_slider_row(popup_module_config, y, "Ti:", &config.pid_kws_ti, 0.0f, 100.0f, 0.1f, "%.1f"); y += 50; add_slider_row(popup_module_config, y, "D:", &config.pid_kws_d, 0.0f, 10.0f, 0.01f, "%.2f"); break; case MODULE_PWW: lbl = lv_label_create(popup_module_config); lv_label_set_text(lbl, "ПИД регулятор PWW:"); lv_obj_set_pos(lbl, 15, y); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_WARNING), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); y += 25; add_slider_row(popup_module_config, y, "Kp:", &config.pid_pww_kp, 0.0f, 10.0f, 0.01f, "%.2f"); y += 50; add_slider_row(popup_module_config, y, "Ti:", &config.pid_pww_ti, 0.0f, 100.0f, 0.1f, "%.1f"); y += 50; add_slider_row(popup_module_config, y, "D:", &config.pid_pww_d, 0.0f, 10.0f, 0.01f, "%.2f"); break; case MODULE_CHANNEL: add_slider_row(popup_module_config, y, "Темп. коррекция:", &config.temp_channel_offset, -10.0f, 10.0f, 0.1f, "%+.1f°C"); y += 60; add_slider_row(popup_module_config, y, "Влажн. коррекция:", &config.hum_channel_offset, -10.0f, 10.0f, 0.1f, "%+.1f%%"); break; default: lbl = lv_label_create(popup_module_config); lv_label_set_text(lbl, "Нет доступных настроек\nдля этого модуля"); lv_obj_set_pos(lbl, 100, 150); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_set_style_text_align(lbl, LV_TEXT_ALIGN_CENTER, 0); break; } } /* Create popup window for sensor readings */ static void create_popup_readings(void) { if (popup_readings) return; popup_readings = lv_obj_create(lv_screen_active()); lv_obj_set_size(popup_readings, 400, 280); lv_obj_center(popup_readings); lv_obj_set_style_bg_color(popup_readings, lv_color_hex(COLOR_BG_PANEL), 0); lv_obj_set_style_radius(popup_readings, 12, 0); lv_obj_set_style_border_width(popup_readings, 3, 0); lv_obj_set_style_border_color(popup_readings, lv_color_hex(COLOR_TEXT_ACCENT), 0); lv_obj_clear_flag(popup_readings, LV_OBJ_FLAG_SCROLLABLE); /* Title */ lv_obj_t *lbl = lv_label_create(popup_readings); lv_label_set_text(lbl, "ПОКАЗАНИЯ ДАТЧИКОВ"); lv_obj_set_pos(lbl, 10, 15); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); /* Close button (X) - справа */ lv_obj_t *btn_close = lv_btn_create(popup_readings); lv_obj_set_pos(btn_close, 330, 10); lv_obj_set_size(btn_close, 35, 35); lv_obj_set_style_bg_color(btn_close, lv_color_hex(COLOR_DANGER), 0); lv_obj_set_style_radius(btn_close, 6, 0); lbl = lv_label_create(btn_close); lv_label_set_text(lbl, "X"); lv_obj_set_style_text_color(lbl, lv_color_hex(0xFFFFFF), 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn_close, close_popup_cb, LV_EVENT_CLICKED, popup_readings); /* Room sensors */ int y = 55; char buf[64]; lbl = lv_label_create(popup_readings); lv_label_set_text(lbl, "КОМНАТА:"); lv_obj_set_pos(lbl, 30, y); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_WARNING), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); y += 28; lbl = lv_label_create(popup_readings); snprintf(buf, sizeof(buf), "Температура: %.1f°C", state.temp_room / 10.0f); lv_label_set_text(lbl, buf); lv_obj_set_pos(lbl, 40, y); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_DANGER), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); y += 26; lbl = lv_label_create(popup_readings); snprintf(buf, sizeof(buf), "Влажность: %.1f%%", state.hum_room / 10.0f); lv_label_set_text(lbl, buf); lv_obj_set_pos(lbl, 40, y); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT_ACCENT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); /* Channel sensors */ y += 35; lbl = lv_label_create(popup_readings); lv_label_set_text(lbl, "КАНАЛ ПРИТОКА:"); lv_obj_set_pos(lbl, 30, y); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_WARNING), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); y += 28; lbl = lv_label_create(popup_readings); snprintf(buf, sizeof(buf), "Температура: %.1f°C", state.temp_channel / 10.0f); lv_label_set_text(lbl, buf); lv_obj_set_pos(lbl, 40, y); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_DANGER), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); y += 26; lbl = lv_label_create(popup_readings); snprintf(buf, sizeof(buf), "Влажность: %.1f%%", state.hum_channel / 10.0f); lv_label_set_text(lbl, buf); lv_obj_set_pos(lbl, 40, y); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT_ACCENT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); } /* Add alarm to log */ static void add_alarm_to_log(const char *message, int active) { if (alarm_count < MAX_ALARMS) { strncpy(alarm_log[alarm_count].message, message, 79); alarm_log[alarm_count].message[79] = '\0'; snprintf(alarm_log[alarm_count].timestamp, 20, "%02d.%02d.%04d %02d:%02d", state.day, state.month, state.year, state.hour, state.minute); alarm_log[alarm_count].active = active; alarm_log[alarm_count].confirmed = 0; alarm_log[alarm_count].confirm_timestamp[0] = '\0'; alarm_count++; } } /* Create alarm popup (does NOT add to log - caller must add separately) */ static void create_popup_alarm(const char *message) { if (popup_alarm) { lv_obj_del(popup_alarm); } popup_alarm = lv_obj_create(lv_screen_active()); lv_obj_set_size(popup_alarm, 400, 240); lv_obj_center(popup_alarm); lv_obj_set_style_bg_color(popup_alarm, lv_color_hex(COLOR_DANGER), 0); lv_obj_set_style_radius(popup_alarm, 12, 0); lv_obj_set_style_border_width(popup_alarm, 4, 0); lv_obj_set_style_border_color(popup_alarm, lv_color_hex(0xFF0000), 0); lv_obj_clear_flag(popup_alarm, LV_OBJ_FLAG_SCROLLABLE); /* Title */ lv_obj_t *lbl = lv_label_create(popup_alarm); lv_label_set_text(lbl, "АВАРИЯ"); lv_obj_set_pos(lbl, 20, 15); lv_obj_set_style_text_color(lbl, lv_color_hex(0xFFFFFF), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); /* Close button (X) - справа в углу */ lv_obj_t *btn_close = lv_btn_create(popup_alarm); lv_obj_set_pos(btn_close, 330, 10); lv_obj_set_size(btn_close, 35, 35); lv_obj_set_style_bg_color(btn_close, lv_color_hex(0x800000), 0); lv_obj_set_style_radius(btn_close, 6, 0); lbl = lv_label_create(btn_close); lv_label_set_text(lbl, "X"); lv_obj_set_style_text_color(lbl, lv_color_hex(0xFFFFFF), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn_close, close_popup_cb, LV_EVENT_CLICKED, popup_alarm); /* Message */ lbl = lv_label_create(popup_alarm); lv_label_set_text(lbl, message); lv_obj_set_pos(lbl, 30, 70); lv_obj_set_width(lbl, 340); lv_obj_set_style_text_color(lbl, lv_color_hex(0xFFFFFF), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_label_set_long_mode(lbl, LV_LABEL_LONG_WRAP); lv_obj_set_style_text_align(lbl, LV_TEXT_ALIGN_CENTER, 0); } static void create_screen_main(void) { lv_obj_t *scr = lv_obj_create(NULL); scr_main = scr; lv_obj_set_style_bg_color(scr, lv_color_hex(COLOR_BG_DARK), 0); lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0); lv_obj_clear_flag(scr, LV_OBJ_FLAG_SCROLLABLE); /* === STATUS PANEL === (нижняя часть экрана) */ lv_obj_t *panel_status = lv_obj_create(scr); lv_obj_set_pos(panel_status, 10, 300); lv_obj_set_size(panel_status, 240, 170); lv_obj_set_style_bg_color(panel_status, lv_color_hex(COLOR_BG_PANEL), 0); lv_obj_set_style_radius(panel_status, 6, 0); lv_obj_set_style_border_width(panel_status, 0, 0); lv_obj_clear_flag(panel_status, LV_OBJ_FLAG_SCROLLABLE); lv_obj_t *lbl = lv_label_create(panel_status); lv_label_set_text(lbl, "Состояние"); lv_obj_set_pos(lbl, 10, 5); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lbl_room_num = lv_label_create(panel_status); lv_label_set_text(lbl_room_num, "Комната: 101"); lv_obj_set_pos(lbl_room_num, 10, 30); lv_obj_set_style_text_color(lbl_room_num, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl_room_num, &montserrat_16_ru_en, 0); lbl_master_slave = lv_label_create(panel_status); lv_label_set_text(lbl_master_slave, "Мастер"); lv_obj_set_pos(lbl_master_slave, 10, 55); lv_obj_set_style_text_color(lbl_master_slave, lv_color_hex(COLOR_ACCENT), 0); lv_obj_set_style_text_font(lbl_master_slave, &montserrat_16_ru_en, 0); lbl_season = lv_label_create(panel_status); { const char *seasons[] = {"Зима", "Лето", "Переход"}; char sbuf[32]; snprintf(sbuf, sizeof(sbuf), "Сезон: %s", seasons[state.season % 3]); lv_label_set_text(lbl_season, sbuf); } lv_obj_set_pos(lbl_season, 10, 80); lv_obj_set_style_text_color(lbl_season, lv_color_hex(COLOR_TEXT_ACCENT), 0); lv_obj_set_style_text_font(lbl_season, &montserrat_16_ru_en, 0); lbl_mode = lv_label_create(panel_status); lv_label_set_text(lbl_mode, "Режим: Дежурный"); lv_obj_set_pos(lbl_mode, 10, 105); lv_obj_set_style_text_color(lbl_mode, lv_color_hex(COLOR_WARNING), 0); lv_obj_set_style_text_font(lbl_mode, &montserrat_16_ru_en, 0); lbl_datetime = lv_label_create(panel_status); { char dt_buf[32]; snprintf(dt_buf, sizeof(dt_buf), "%02d.%02d.%04d %02d:%02d", state.day, state.month, state.year, state.hour, state.minute); lv_label_set_text(lbl_datetime, dt_buf); } lv_obj_set_pos(lbl_datetime, 10, 130); lv_obj_set_style_text_color(lbl_datetime, lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl_datetime, &montserrat_16_ru_en, 0); /* === CLIMATE PANEL === */ lv_obj_t *panel_climate = lv_obj_create(scr); lv_obj_set_pos(panel_climate, 260, 300); lv_obj_set_size(panel_climate, 230, 170); lv_obj_set_style_bg_color(panel_climate, lv_color_hex(COLOR_BG_PANEL), 0); lv_obj_set_style_radius(panel_climate, 6, 0); lv_obj_set_style_border_width(panel_climate, 0, 0); lv_obj_clear_flag(panel_climate, LV_OBJ_FLAG_SCROLLABLE); lbl = lv_label_create(panel_climate); lv_label_set_text(lbl, "Комната"); lv_obj_set_pos(lbl, 10, 5); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lbl = lv_label_create(panel_climate); lv_label_set_text(lbl, "Температура:"); lv_obj_set_pos(lbl, 10, 35); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lbl_temp_room = lv_label_create(panel_climate); lv_label_set_text(lbl_temp_room, "--.-°C"); lv_obj_set_pos(lbl_temp_room, 130, 35); lv_obj_set_style_text_color(lbl_temp_room, lv_color_hex(COLOR_DANGER), 0); lv_obj_set_style_text_font(lbl_temp_room, &montserrat_16_ru_en, 0); lbl = lv_label_create(panel_climate); lv_label_set_text(lbl, "Влажность:"); lv_obj_set_pos(lbl, 10, 65); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lbl_hum_room = lv_label_create(panel_climate); lv_label_set_text(lbl_hum_room, "--.-%"); lv_obj_set_pos(lbl_hum_room, 130, 65); lv_obj_set_style_text_color(lbl_hum_room, lv_color_hex(COLOR_TEXT_ACCENT), 0); lv_obj_set_style_text_font(lbl_hum_room, &montserrat_16_ru_en, 0); /* Channel info */ lbl = lv_label_create(panel_climate); lv_label_set_text(lbl, "Канал:"); lv_obj_set_pos(lbl, 10, 100); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lbl_temp_channel = lv_label_create(panel_climate); lv_label_set_text(lbl_temp_channel, "--.-°C"); lv_obj_set_pos(lbl_temp_channel, 70, 100); lv_obj_set_style_text_color(lbl_temp_channel, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl_temp_channel, &montserrat_16_ru_en, 0); lbl_hum_channel = lv_label_create(panel_climate); lv_label_set_text(lbl_hum_channel, "--%"); lv_obj_set_pos(lbl_hum_channel, 150, 100); lv_obj_set_style_text_color(lbl_hum_channel, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl_hum_channel, &montserrat_16_ru_en, 0); /* === CONTROL BUTTONS === (правая часть нижней половины) */ int btn_x = 500; int btn_y = 300; /* Stop button */ btn_stop_mode = lv_btn_create(scr); lv_obj_set_pos(btn_stop_mode, btn_x, btn_y); lv_obj_set_size(btn_stop_mode, 140, 100); lv_obj_set_style_bg_color(btn_stop_mode, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn_stop_mode, 6, 0); lbl_stop_mode = lv_label_create(btn_stop_mode); lv_label_set_text(lbl_stop_mode, "ОСТАНОВ"); lv_obj_set_style_text_color(lbl_stop_mode, lv_color_hex(COLOR_DANGER), 0); lv_obj_set_style_text_font(lbl_stop_mode, &montserrat_16_ru_en, 0); lv_obj_center(lbl_stop_mode); lv_obj_add_event_cb(btn_stop_mode, btn_stop_cb, LV_EVENT_CLICKED, NULL); /* Standby button */ btn_standby_mode = lv_btn_create(scr); lv_obj_set_pos(btn_standby_mode, btn_x + 150, btn_y); lv_obj_set_size(btn_standby_mode, 140, 48); lv_obj_set_style_bg_color(btn_standby_mode, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn_standby_mode, 6, 0); lbl_standby_mode = lv_label_create(btn_standby_mode); lv_label_set_text(lbl_standby_mode, "Дежурный"); lv_obj_set_style_text_color(lbl_standby_mode, lv_color_hex(COLOR_WARNING), 0); lv_obj_set_style_text_font(lbl_standby_mode, &montserrat_16_ru_en, 0); lv_obj_center(lbl_standby_mode); lv_obj_add_event_cb(btn_standby_mode, btn_standby_cb, LV_EVENT_CLICKED, NULL); /* Work button */ btn_work_mode = lv_btn_create(scr); lv_obj_set_pos(btn_work_mode, btn_x + 150, btn_y + 52); lv_obj_set_size(btn_work_mode, 140, 48); lv_obj_set_style_bg_color(btn_work_mode, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn_work_mode, 6, 0); lbl_work_mode = lv_label_create(btn_work_mode); lv_label_set_text(lbl_work_mode, "Рабочий"); lv_obj_set_style_text_color(lbl_work_mode, lv_color_hex(COLOR_ACCENT), 0); lv_obj_set_style_text_font(lbl_work_mode, &montserrat_16_ru_en, 0); lv_obj_center(lbl_work_mode); lv_obj_add_event_cb(btn_work_mode, btn_work_cb, LV_EVENT_CLICKED, NULL); /* Menu button */ lv_obj_t *btn = lv_btn_create(scr); lv_obj_set_pos(btn, btn_x, btn_y + 108); lv_obj_set_size(btn, 140, 48); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 6, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, "Меню"); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn, btn_menu_cb, LV_EVENT_CLICKED, NULL); /* Readings button */ btn = lv_btn_create(scr); lv_obj_set_pos(btn, btn_x + 150, btn_y + 108); lv_obj_set_size(btn, 140, 48); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 6, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, "Показания"); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT_ACCENT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn, btn_readings_cb, LV_EVENT_CLICKED, NULL); /* Update mode button colors based on current state */ update_mode_button_colors(); /* === EQUIPMENT SCHEMA AREA === (верхняя половина экрана) */ schema_panel = lv_obj_create(scr); lv_obj_set_pos(schema_panel, 10, 10); lv_obj_set_size(schema_panel, 780, 280); lv_obj_set_style_bg_color(schema_panel, lv_color_hex(COLOR_BG_PANEL), 0); lv_obj_set_style_radius(schema_panel, 6, 0); lv_obj_set_style_border_width(schema_panel, 0, 0); lv_obj_clear_flag(schema_panel, LV_OBJ_FLAG_SCROLLABLE); /* Title hint */ lv_obj_t *hint1 = lv_label_create(schema_panel); lv_label_set_text(hint1, "Схема установки:"); lv_obj_set_pos(hint1, 10, 5); lv_obj_set_style_text_color(hint1, lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(hint1, &montserrat_16_ru_en, 0); /* Layout: pipe_img + module + pipe_img + ... + pipe_img * Use pipe image (64x64) cropped into narrow containers between modules. * Container clips image to show only a horizontal strip. */ #define PIPE_W 12 #define PIPE_H 64 int total_w = MODULE_COUNT * MODULE_WIDTH + (MODULE_COUNT + 1) * PIPE_W; int start_x = (780 - total_w) / 2; int pipe_y = MODULES_Y + (MODULE_HEIGHT - PIPE_H) / 2; for (int i = 0; i < MODULE_COUNT; i++) { int pipe_x = start_x + i * (MODULE_WIDTH + PIPE_W); /* Pipe image in a clipping container */ lv_obj_t *pcont = lv_obj_create(schema_panel); lv_obj_set_pos(pcont, pipe_x, pipe_y); lv_obj_set_size(pcont, PIPE_W, PIPE_H); lv_obj_set_style_pad_all(pcont, 0, 0); lv_obj_set_style_bg_opa(pcont, LV_OPA_TRANSP, 0); lv_obj_set_style_border_width(pcont, 0, 0); lv_obj_clear_flag(pcont, LV_OBJ_FLAG_SCROLLABLE | LV_OBJ_FLAG_CLICKABLE); lv_obj_t *pimg = lv_image_create(pcont); lv_image_set_src(pimg, &img_pipe); lv_obj_align(pimg, LV_ALIGN_CENTER, 0, 0); int mod_x = pipe_x + PIPE_W; create_module_widget(schema_panel, i, mod_x, MODULES_Y); } /* Final pipe after last module */ { int pipe_x = start_x + MODULE_COUNT * (MODULE_WIDTH + PIPE_W); lv_obj_t *pcont = lv_obj_create(schema_panel); lv_obj_set_pos(pcont, pipe_x, pipe_y); lv_obj_set_size(pcont, PIPE_W, PIPE_H); lv_obj_set_style_pad_all(pcont, 0, 0); lv_obj_set_style_bg_opa(pcont, LV_OPA_TRANSP, 0); lv_obj_set_style_border_width(pcont, 0, 0); lv_obj_clear_flag(pcont, LV_OBJ_FLAG_SCROLLABLE | LV_OBJ_FLAG_CLICKABLE); lv_obj_t *pimg = lv_image_create(pcont); lv_image_set_src(pimg, &img_pipe); lv_obj_align(pimg, LV_ALIGN_CENTER, 0, 0); } /* Update all module displays with current values */ for (int i = 0; i < MODULE_COUNT; i++) { update_module_display(i); } /* Update mode button colors to reflect initial state */ update_mode_button_colors(); } static void create_screen_screen1(void) { lv_obj_t *scr = lv_obj_create(NULL); scr_screen1 = scr; lv_obj_set_style_bg_color(scr, lv_color_hex(COLOR_BG_DARK), 0); lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0); lv_obj_clear_flag(scr, LV_OBJ_FLAG_SCROLLABLE); /* Title */ lv_obj_t *lbl = lv_label_create(scr); lv_label_set_text(lbl, "МЕНЮ"); lv_obj_set_pos(lbl, 370, 30); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); int btn_width = 300; int btn_height = 55; int x = 250; int y = 90; int spacing = 15; /* Button: Back to Main Screen */ lv_obj_t *btn = lv_btn_create(scr); lv_obj_set_pos(btn, x, y); lv_obj_set_size(btn, btn_width, btn_height); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 6, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, "Главный экран"); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_ACCENT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn, btn_back_to_main_cb, LV_EVENT_CLICKED, NULL); y += btn_height + spacing; /* Button: Log (Журнал) */ btn = lv_btn_create(scr); lv_obj_set_pos(btn, x, y); lv_obj_set_size(btn, btn_width, btn_height); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 6, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, "Журнал аварий"); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_WARNING), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn, btn_log_cb, LV_EVENT_CLICKED, NULL); y += btn_height + spacing; /* Button: Settings Parameters (Уставка параметров) */ btn = lv_btn_create(scr); lv_obj_set_pos(btn, x, y); lv_obj_set_size(btn, btn_width, btn_height); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 6, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, "Уставка параметров"); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn, btn_settings_cb, LV_EVENT_CLICKED, NULL); y += btn_height + spacing; /* Button: Configuration (Настройки - требует пароль) */ btn = lv_btn_create(scr); lv_obj_set_pos(btn, x, y); lv_obj_set_size(btn, btn_width, btn_height); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 6, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, "Настройки"); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_DANGER), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn, btn_config_cb, LV_EVENT_CLICKED, NULL); } static void create_screen_screen2(void) { lv_obj_t *scr = lv_obj_create(NULL); scr_screen2 = scr; lv_obj_set_style_bg_color(scr, lv_color_hex(COLOR_BG_DARK), 0); lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0); lv_obj_clear_flag(scr, LV_OBJ_FLAG_SCROLLABLE); /* Title */ lv_obj_t *lbl = lv_label_create(scr); lv_label_set_text(lbl, "ЖУРНАЛ АВАРИЙ"); lv_obj_set_pos(lbl, 300, 15); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); /* Date/Time display */ char datetime_buf[32]; snprintf(datetime_buf, sizeof(datetime_buf), "%02d.%02d.%04d %02d:%02d", state.day, state.month, state.year, state.hour, state.minute); lbl = lv_label_create(scr); lv_label_set_text(lbl, datetime_buf); lv_obj_set_pos(lbl, 620, 15); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); /* Log panel */ lv_obj_t *log_panel = lv_obj_create(scr); lv_obj_set_pos(log_panel, 20, 50); lv_obj_set_size(log_panel, 760, 310); lv_obj_set_style_bg_color(log_panel, lv_color_hex(COLOR_BG_PANEL), 0); lv_obj_set_style_radius(log_panel, 8, 0); lv_obj_set_style_border_width(log_panel, 1, 0); lv_obj_set_style_border_color(log_panel, lv_color_hex(COLOR_BORDER), 0); lv_obj_clear_flag(log_panel, LV_OBJ_FLAG_SCROLLABLE); /* Calculate pagination */ int total_pages = (alarm_count + ALARMS_PER_PAGE - 1) / ALARMS_PER_PAGE; if (total_pages == 0) total_pages = 1; if (alarm_page >= total_pages) alarm_page = total_pages - 1; if (alarm_page < 0) alarm_page = 0; int start_idx = alarm_page * ALARMS_PER_PAGE; int end_idx = start_idx + ALARMS_PER_PAGE; if (end_idx > alarm_count) end_idx = alarm_count; /* Display log entries for current page */ int y = 10; if (alarm_count == 0) { lbl = lv_label_create(log_panel); lv_label_set_text(lbl, "Аварий нет"); lv_obj_set_pos(lbl, 320, 150); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_ACCENT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); } else { for (int i = start_idx; i < end_idx; i++) { /* Alarm entry container */ lv_obj_t *entry_cont = lv_obj_create(log_panel); lv_obj_set_pos(entry_cont, 5, y); lv_obj_set_size(entry_cont, 740, 75); lv_obj_set_style_bg_color(entry_cont, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(entry_cont, 6, 0); lv_obj_set_style_border_width(entry_cont, 1, 0); lv_obj_set_style_border_color(entry_cont, alarm_log[i].active ? lv_color_hex(COLOR_DANGER) : lv_color_hex(COLOR_BORDER), 0); lv_obj_clear_flag(entry_cont, LV_OBJ_FLAG_SCROLLABLE); /* Alarm message */ char buf[120]; snprintf(buf, sizeof(buf), "%s %s", alarm_log[i].message, alarm_log[i].timestamp); lbl = lv_label_create(entry_cont); lv_label_set_text(lbl, buf); lv_obj_set_pos(lbl, 10, 8); lv_obj_set_width(lbl, 550); lv_label_set_long_mode(lbl, LV_LABEL_LONG_CLIP); if (alarm_log[i].active) { lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_DANGER), 0); } else { lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); } lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); /* Confirm button or confirmed status */ if (alarm_log[i].confirmed) { /* Show confirmed status */ lbl = lv_label_create(entry_cont); lv_label_set_text(lbl, "Подтверждено"); lv_obj_set_pos(lbl, 580, 10); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_ACCENT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lbl = lv_label_create(entry_cont); lv_label_set_text(lbl, alarm_log[i].confirm_timestamp); lv_obj_set_pos(lbl, 580, 35); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); } else { /* Confirm button - centered vertically */ lv_obj_t *btn_confirm = lv_btn_create(entry_cont); lv_obj_set_pos(btn_confirm, 580, 17); lv_obj_set_size(btn_confirm, 130, 40); lv_obj_set_style_bg_color(btn_confirm, lv_color_hex(COLOR_BG_MODULE), 0); lv_obj_set_style_radius(btn_confirm, 4, 0); lbl = lv_label_create(btn_confirm); lv_label_set_text(lbl, "Подтвердить"); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_ACCENT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn_confirm, btn_confirm_alarm_cb, LV_EVENT_CLICKED, (void*)(intptr_t)i); } y += 80; } } /* Buttons row */ int btn_y = 370; /* Left arrow button */ lv_obj_t *btn = lv_btn_create(scr); lv_obj_set_pos(btn, 300, btn_y); lv_obj_set_size(btn, 60, 40); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 6, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, "<"); lv_obj_set_style_text_color(lbl, alarm_page > 0 ? lv_color_hex(COLOR_TEXT) : lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn, btn_alarm_page_prev_cb, LV_EVENT_CLICKED, NULL); /* Page indicator */ char page_buf[16]; snprintf(page_buf, sizeof(page_buf), "%d / %d", alarm_page + 1, total_pages); lbl = lv_label_create(scr); lv_label_set_text(lbl, page_buf); lv_obj_set_pos(lbl, 380, btn_y + 10); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); /* Right arrow button */ btn = lv_btn_create(scr); lv_obj_set_pos(btn, 440, btn_y); lv_obj_set_size(btn, 60, 40); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 6, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, ">"); lv_obj_set_style_text_color(lbl, alarm_page < total_pages - 1 ? lv_color_hex(COLOR_TEXT) : lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn, btn_alarm_page_next_cb, LV_EVENT_CLICKED, NULL); /* Bottom buttons row */ btn_y = 420; /* Clear Log button */ btn = lv_btn_create(scr); lv_obj_set_pos(btn, 250, btn_y); lv_obj_set_size(btn, 160, 40); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 6, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, "Очистить журнал"); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_DANGER), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn, btn_clear_log_cb, LV_EVENT_CLICKED, NULL); /* Back to Menu button */ btn = lv_btn_create(scr); lv_obj_set_pos(btn, 430, btn_y); lv_obj_set_size(btn, 120, 40); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 6, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, "Меню"); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn, btn_menu_cb, LV_EVENT_CLICKED, NULL); } static void create_screen_screen3(void) { lv_obj_t *scr = lv_obj_create(NULL); scr_screen3 = scr; lv_obj_set_style_bg_color(scr, lv_color_hex(COLOR_BG_DARK), 0); lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0); lv_obj_clear_flag(scr, LV_OBJ_FLAG_SCROLLABLE); /* Title */ lv_obj_t *lbl = lv_label_create(scr); lv_label_set_text(lbl, "УСТАВКА ПАРАМЕТРОВ"); lv_obj_set_pos(lbl, 290, 10); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); char buf[64]; int row_h = 38; const char *heater_modes[] = {"ВЫКЛ", "ВКЛ", "АВТО"}; /* Left panel - Work mode - NO SCROLL, with pagination */ lv_obj_t *panel_work = lv_obj_create(scr); lv_obj_set_pos(panel_work, 15, 40); lv_obj_set_size(panel_work, 380, 270); lv_obj_set_style_bg_color(panel_work, lv_color_hex(COLOR_BG_PANEL), 0); lv_obj_set_style_radius(panel_work, 6, 0); lv_obj_set_style_border_width(panel_work, 0, 0); lv_obj_clear_flag(panel_work, LV_OBJ_FLAG_SCROLLABLE); lbl = lv_label_create(panel_work); lv_label_set_text(lbl, "РАБОЧИЙ"); lv_obj_set_pos(lbl, 10, 5); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_ACCENT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); /* Page indicator for Work mode */ snprintf(buf, sizeof(buf), "%d/2", setpoint_work_page + 1); lbl = lv_label_create(panel_work); lv_label_set_text(lbl, buf); lv_obj_set_pos(lbl, 330, 5); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); int y = 32; lv_obj_t *btn; /* Work mode parameters - page 0: Temp, Hum, Valve inlet, Valve recirc */ /* Work mode parameters - page 1: Heater, Fan, KWS, PWW */ if (setpoint_work_page == 0) { /* Temperature */ btn = lv_btn_create(panel_work); lv_obj_set_pos(btn, 5, y); lv_obj_set_size(btn, 350, row_h); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); snprintf(buf, sizeof(buf), "Температура: %.1f°C", setpoint_work.temp); lbl_work_temp = lv_label_create(btn); lv_label_set_text(lbl_work_temp, buf); lv_obj_center(lbl_work_temp); lv_obj_set_style_text_color(lbl_work_temp, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl_work_temp, &montserrat_16_ru_en, 0); lv_obj_add_event_cb(btn, edit_work_temp_cb, LV_EVENT_CLICKED, NULL); y += row_h + 6; /* Humidity */ btn = lv_btn_create(panel_work); lv_obj_set_pos(btn, 5, y); lv_obj_set_size(btn, 350, row_h); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); snprintf(buf, sizeof(buf), "Влажность: %.1f%%", setpoint_work.hum); lbl_work_hum = lv_label_create(btn); lv_label_set_text(lbl_work_hum, buf); lv_obj_center(lbl_work_hum); lv_obj_set_style_text_color(lbl_work_hum, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl_work_hum, &montserrat_16_ru_en, 0); lv_obj_add_event_cb(btn, edit_work_hum_cb, LV_EVENT_CLICKED, NULL); y += row_h + 6; /* Valve inlet */ btn = lv_btn_create(panel_work); lv_obj_set_pos(btn, 5, y); lv_obj_set_size(btn, 350, row_h); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); snprintf(buf, sizeof(buf), "Клапан притока: %.1f%%", setpoint_work.valve_inlet); lbl = lv_label_create(btn); lv_label_set_text(lbl, buf); lv_obj_center(lbl); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_add_event_cb(btn, edit_work_valve_inlet_cb, LV_EVENT_CLICKED, NULL); y += row_h + 6; /* Valve recirc */ btn = lv_btn_create(panel_work); lv_obj_set_pos(btn, 5, y); lv_obj_set_size(btn, 350, row_h); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); snprintf(buf, sizeof(buf), "Клапан рецирк: %.1f%%", setpoint_work.valve_recirc); lbl = lv_label_create(btn); lv_label_set_text(lbl, buf); lv_obj_center(lbl); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_add_event_cb(btn, edit_work_valve_recirc_cb, LV_EVENT_CLICKED, NULL); } else { /* Page 1: Heater, Fan, KWS, PWW */ /* Heater mode */ btn = lv_btn_create(panel_work); lv_obj_set_pos(btn, 5, y); lv_obj_set_size(btn, 350, row_h); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); snprintf(buf, sizeof(buf), "Электронагрев: %s", heater_modes[setpoint_work.heater]); lbl = lv_label_create(btn); lv_label_set_text(lbl, buf); lv_obj_center(lbl); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_add_event_cb(btn, edit_work_heater_cb, LV_EVENT_CLICKED, NULL); y += row_h + 6; /* Fan toggle */ btn = lv_btn_create(panel_work); lv_obj_set_pos(btn, 5, y); lv_obj_set_size(btn, 350, row_h); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); snprintf(buf, sizeof(buf), "Вентилятор: %s", setpoint_work.fan ? "ВКЛ" : "ВЫКЛ"); lbl = lv_label_create(btn); lv_label_set_text(lbl, buf); lv_obj_center(lbl); lv_obj_set_style_text_color(lbl, setpoint_work.fan ? lv_color_hex(COLOR_ACCENT) : lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_add_event_cb(btn, edit_work_fan_cb, LV_EVENT_CLICKED, NULL); y += row_h + 6; /* KWS mode */ btn = lv_btn_create(panel_work); lv_obj_set_pos(btn, 5, y); lv_obj_set_size(btn, 350, row_h); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); if (setpoint_work.kws_auto) { snprintf(buf, sizeof(buf), "KWS: АВТО"); } else { snprintf(buf, sizeof(buf), "KWS: %.1f%%", setpoint_work.kws_manual); } lbl = lv_label_create(btn); lv_label_set_text(lbl, buf); lv_obj_center(lbl); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_add_event_cb(btn, edit_work_kws_cb, LV_EVENT_CLICKED, NULL); y += row_h + 6; /* PWW mode */ btn = lv_btn_create(panel_work); lv_obj_set_pos(btn, 5, y); lv_obj_set_size(btn, 350, row_h); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); if (setpoint_work.pww_auto) { snprintf(buf, sizeof(buf), "PWW: АВТО"); } else { snprintf(buf, sizeof(buf), "PWW: %.1f%%", setpoint_work.pww_manual); } lbl = lv_label_create(btn); lv_label_set_text(lbl, buf); lv_obj_center(lbl); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_add_event_cb(btn, edit_work_pww_cb, LV_EVENT_CLICKED, NULL); } /* Pagination buttons for Work panel */ btn = lv_btn_create(panel_work); lv_obj_set_pos(btn, 80, 216); lv_obj_set_size(btn, 80, 38); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, "<"); lv_obj_set_style_text_color(lbl, setpoint_work_page > 0 ? lv_color_hex(COLOR_TEXT) : lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn, btn_setpoint_work_prev_cb, LV_EVENT_CLICKED, NULL); /* Page indicator */ snprintf(buf, sizeof(buf), "%d/2", setpoint_work_page + 1); lbl = lv_label_create(panel_work); lv_label_set_text(lbl, buf); lv_obj_set_pos(lbl, 180, 226); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); btn = lv_btn_create(panel_work); lv_obj_set_pos(btn, 210, 216); lv_obj_set_size(btn, 80, 38); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, ">"); lv_obj_set_style_text_color(lbl, setpoint_work_page < 1 ? lv_color_hex(COLOR_TEXT) : lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn, btn_setpoint_work_next_cb, LV_EVENT_CLICKED, NULL); /* Right panel - Standby mode - NO SCROLL, with pagination */ lv_obj_t *panel_standby = lv_obj_create(scr); lv_obj_set_pos(panel_standby, 405, 40); lv_obj_set_size(panel_standby, 380, 270); lv_obj_set_style_bg_color(panel_standby, lv_color_hex(COLOR_BG_PANEL), 0); lv_obj_set_style_radius(panel_standby, 6, 0); lv_obj_set_style_border_width(panel_standby, 0, 0); lv_obj_clear_flag(panel_standby, LV_OBJ_FLAG_SCROLLABLE); lbl = lv_label_create(panel_standby); lv_label_set_text(lbl, "ДЕЖУРНЫЙ"); lv_obj_set_pos(lbl, 10, 5); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_WARNING), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); /* Page indicator for Standby mode */ snprintf(buf, sizeof(buf), "%d/2", setpoint_standby_page + 1); lbl = lv_label_create(panel_standby); lv_label_set_text(lbl, buf); lv_obj_set_pos(lbl, 330, 5); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); y = 32; /* Standby mode parameters - page 0: Temp, Hum, Valve inlet, Valve recirc */ /* Standby mode parameters - page 1: Heater, Fan, KWS, PWW */ if (setpoint_standby_page == 0) { /* Standby Temperature */ btn = lv_btn_create(panel_standby); lv_obj_set_pos(btn, 5, y); lv_obj_set_size(btn, 350, row_h); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); snprintf(buf, sizeof(buf), "Температура: %.1f°C", setpoint_standby.temp); lbl_standby_temp = lv_label_create(btn); lv_label_set_text(lbl_standby_temp, buf); lv_obj_center(lbl_standby_temp); lv_obj_set_style_text_color(lbl_standby_temp, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl_standby_temp, &montserrat_16_ru_en, 0); lv_obj_add_event_cb(btn, edit_standby_temp_cb, LV_EVENT_CLICKED, NULL); y += row_h + 6; /* Standby Humidity */ btn = lv_btn_create(panel_standby); lv_obj_set_pos(btn, 5, y); lv_obj_set_size(btn, 350, row_h); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); snprintf(buf, sizeof(buf), "Влажность: %.1f%%", setpoint_standby.hum); lbl_standby_hum = lv_label_create(btn); lv_label_set_text(lbl_standby_hum, buf); lv_obj_center(lbl_standby_hum); lv_obj_set_style_text_color(lbl_standby_hum, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl_standby_hum, &montserrat_16_ru_en, 0); lv_obj_add_event_cb(btn, edit_standby_hum_cb, LV_EVENT_CLICKED, NULL); y += row_h + 6; /* Standby Valve inlet */ btn = lv_btn_create(panel_standby); lv_obj_set_pos(btn, 5, y); lv_obj_set_size(btn, 350, row_h); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); snprintf(buf, sizeof(buf), "Клапан притока: %.1f%%", setpoint_standby.valve_inlet); lbl = lv_label_create(btn); lv_label_set_text(lbl, buf); lv_obj_center(lbl); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_add_event_cb(btn, edit_standby_valve_inlet_cb, LV_EVENT_CLICKED, NULL); y += row_h + 6; /* Standby Valve recirc */ btn = lv_btn_create(panel_standby); lv_obj_set_pos(btn, 5, y); lv_obj_set_size(btn, 350, row_h); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); snprintf(buf, sizeof(buf), "Клапан рецирк: %.1f%%", setpoint_standby.valve_recirc); lbl = lv_label_create(btn); lv_label_set_text(lbl, buf); lv_obj_center(lbl); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_add_event_cb(btn, edit_standby_valve_recirc_cb, LV_EVENT_CLICKED, NULL); } else { /* Page 1: Heater, Fan, KWS, PWW */ /* Heater mode */ btn = lv_btn_create(panel_standby); lv_obj_set_pos(btn, 5, y); lv_obj_set_size(btn, 350, row_h); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); snprintf(buf, sizeof(buf), "Электронагрев: %s", heater_modes[setpoint_standby.heater]); lbl = lv_label_create(btn); lv_label_set_text(lbl, buf); lv_obj_center(lbl); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_add_event_cb(btn, edit_standby_heater_cb, LV_EVENT_CLICKED, NULL); y += row_h + 6; /* Fan toggle */ btn = lv_btn_create(panel_standby); lv_obj_set_pos(btn, 5, y); lv_obj_set_size(btn, 350, row_h); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); snprintf(buf, sizeof(buf), "Вентилятор: %s", setpoint_standby.fan ? "ВКЛ" : "ВЫКЛ"); lbl = lv_label_create(btn); lv_label_set_text(lbl, buf); lv_obj_center(lbl); lv_obj_set_style_text_color(lbl, setpoint_standby.fan ? lv_color_hex(COLOR_ACCENT) : lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_add_event_cb(btn, edit_standby_fan_cb, LV_EVENT_CLICKED, NULL); y += row_h + 6; /* KWS mode */ btn = lv_btn_create(panel_standby); lv_obj_set_pos(btn, 5, y); lv_obj_set_size(btn, 350, row_h); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); if (setpoint_standby.kws_auto) { snprintf(buf, sizeof(buf), "KWS: АВТО"); } else { snprintf(buf, sizeof(buf), "KWS: %.1f%%", setpoint_standby.kws_manual); } lbl = lv_label_create(btn); lv_label_set_text(lbl, buf); lv_obj_center(lbl); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_add_event_cb(btn, edit_standby_kws_cb, LV_EVENT_CLICKED, NULL); y += row_h + 6; /* PWW mode */ btn = lv_btn_create(panel_standby); lv_obj_set_pos(btn, 5, y); lv_obj_set_size(btn, 350, row_h); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); if (setpoint_standby.pww_auto) { snprintf(buf, sizeof(buf), "PWW: АВТО"); } else { snprintf(buf, sizeof(buf), "PWW: %.1f%%", setpoint_standby.pww_manual); } lbl = lv_label_create(btn); lv_label_set_text(lbl, buf); lv_obj_center(lbl); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_add_event_cb(btn, edit_standby_pww_cb, LV_EVENT_CLICKED, NULL); } /* Pagination buttons for Standby panel */ btn = lv_btn_create(panel_standby); lv_obj_set_pos(btn, 80, 216); lv_obj_set_size(btn, 80, 38); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, "<"); lv_obj_set_style_text_color(lbl, setpoint_standby_page > 0 ? lv_color_hex(COLOR_TEXT) : lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn, btn_setpoint_standby_prev_cb, LV_EVENT_CLICKED, NULL); /* Page indicator */ snprintf(buf, sizeof(buf), "%d/2", setpoint_standby_page + 1); lbl = lv_label_create(panel_standby); lv_label_set_text(lbl, buf); lv_obj_set_pos(lbl, 180, 226); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); btn = lv_btn_create(panel_standby); lv_obj_set_pos(btn, 210, 216); lv_obj_set_size(btn, 80, 38); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, ">"); lv_obj_set_style_text_color(lbl, setpoint_standby_page < 1 ? lv_color_hex(COLOR_TEXT) : lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn, btn_setpoint_standby_next_cb, LV_EVENT_CLICKED, NULL); /* Common parameters section */ lv_obj_t *panel_common = lv_obj_create(scr); lv_obj_set_pos(panel_common, 15, 320); lv_obj_set_size(panel_common, 770, 60); lv_obj_set_style_bg_color(panel_common, lv_color_hex(COLOR_BG_PANEL), 0); lv_obj_set_style_radius(panel_common, 6, 0); lv_obj_set_style_border_width(panel_common, 0, 0); lv_obj_clear_flag(panel_common, LV_OBJ_FLAG_SCROLLABLE); lbl = lv_label_create(panel_common); lv_label_set_text(lbl, "Общие:"); lv_obj_set_pos(lbl, 10, 3); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); snprintf(buf, sizeof(buf), "Фильтр G4: %s", filter_g4_date); lbl = lv_label_create(panel_common); lv_label_set_text(lbl, buf); lv_obj_set_pos(lbl, 10, 28); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); snprintf(buf, sizeof(buf), "Фильтр H13: %s", filter_h13_date); lbl = lv_label_create(panel_common); lv_label_set_text(lbl, buf); lv_obj_set_pos(lbl, 250, 28); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); /* Buttons row */ int btn_y = 395; btn = lv_btn_create(scr); lv_obj_set_pos(btn, 200, btn_y); lv_obj_set_size(btn, 110, 38); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, "Сохранить"); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_ACCENT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn, btn_save_setpoints_cb, LV_EVENT_CLICKED, NULL); btn = lv_btn_create(scr); lv_obj_set_pos(btn, 330, btn_y); lv_obj_set_size(btn, 140, 38); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, "По умолчанию"); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_WARNING), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn, btn_default_setpoints_cb, LV_EVENT_CLICKED, NULL); btn = lv_btn_create(scr); lv_obj_set_pos(btn, 490, btn_y); lv_obj_set_size(btn, 100, 38); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, "Меню"); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn, btn_menu_cb, LV_EVENT_CLICKED, NULL); } /* ============ SCREEN 4: НАСТРОЙКИ (config, after password) ============ */ static void create_screen_config(void) { lv_obj_t *scr = lv_obj_create(NULL); scr_screen4 = scr; lv_obj_set_style_bg_color(scr, lv_color_hex(COLOR_BG_DARK), 0); lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0); lv_obj_clear_flag(scr, LV_OBJ_FLAG_SCROLLABLE); char buf[64]; /* Header */ lv_obj_t *lbl = lv_label_create(scr); lv_label_set_text(lbl, "НАСТРОЙКИ"); lv_obj_set_pos(lbl, 340, 5); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); /* === MODULE SCHEMA AREA === (top part, like main screen) */ lv_obj_t *schema_panel = lv_obj_create(scr); lv_obj_set_pos(schema_panel, 10, 30); lv_obj_set_size(schema_panel, 780, 150); lv_obj_set_style_bg_color(schema_panel, lv_color_hex(COLOR_BG_PANEL), 0); lv_obj_set_style_radius(schema_panel, 6, 0); lv_obj_set_style_border_width(schema_panel, 0, 0); lv_obj_clear_flag(schema_panel, LV_OBJ_FLAG_SCROLLABLE); lbl = lv_label_create(schema_panel); lv_label_set_text(lbl, "Модули (нажмите для настройки, удерживайте для вкл/выкл):"); lv_obj_set_pos(lbl, 10, 5); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); /* Create all modules with pipe image connectors (centered) */ int total_w2 = MODULE_COUNT * MODULE_WIDTH + (MODULE_COUNT + 1) * PIPE_W; int start_x = (780 - total_w2) / 2; int modules_y = 30; int pipe_sy = modules_y + (MODULE_HEIGHT - PIPE_H) / 2; for (int i = 0; i < MODULE_COUNT; i++) { int pipe_x = start_x + i * (MODULE_WIDTH + PIPE_W); /* Pipe image in clipping container */ lv_obj_t *p = lv_obj_create(schema_panel); lv_obj_set_pos(p, pipe_x, pipe_sy); lv_obj_set_size(p, PIPE_W, PIPE_H); lv_obj_set_style_pad_all(p, 0, 0); lv_obj_set_style_bg_opa(p, LV_OPA_TRANSP, 0); lv_obj_set_style_border_width(p, 0, 0); lv_obj_clear_flag(p, LV_OBJ_FLAG_SCROLLABLE | LV_OBJ_FLAG_CLICKABLE); lv_obj_t *pimg = lv_image_create(p); lv_image_set_src(pimg, &img_pipe); lv_obj_align(pimg, LV_ALIGN_CENTER, 0, 0); int x_pos = pipe_x + PIPE_W; /* Container for module - transparent */ lv_obj_t *cont = lv_obj_create(schema_panel); lv_obj_set_pos(cont, x_pos, modules_y); lv_obj_set_size(cont, MODULE_WIDTH, MODULE_HEIGHT); lv_obj_set_style_bg_opa(cont, LV_OPA_TRANSP, 0); lv_obj_set_style_border_width(cont, 0, 0); lv_obj_set_style_pad_all(cont, 2, 0); lv_obj_clear_flag(cont, LV_OBJ_FLAG_SCROLLABLE); lv_obj_add_flag(cont, LV_OBJ_FLAG_CLICKABLE); /* Add click and press event handlers */ lv_obj_add_event_cb(cont, module_settings_click_cb, LV_EVENT_CLICKED, (void*)(intptr_t)i); lv_obj_add_event_cb(cont, module_settings_press_cb, LV_EVENT_PRESSED, (void*)(intptr_t)i); /* Image */ lv_obj_t *img = lv_image_create(cont); lv_image_set_src(img, get_module_image(i)); lv_image_set_scale(img, MODULE_SCALE); lv_obj_align(img, LV_ALIGN_CENTER, 0, 0); /* Module name label */ lv_obj_t *lbl_name = lv_label_create(cont); lv_label_set_text(lbl_name, get_module_name(i)); lv_obj_set_style_text_font(lbl_name, &montserrat_16_ru_en, 0); /* Value label */ lv_obj_t *lbl_value = lv_label_create(cont); lv_obj_set_style_text_font(lbl_value, &montserrat_16_ru_en, 0); if (i == MODULE_CHANNEL) { /* Channel: name at top (same as others), value centered on black square */ lv_obj_set_style_text_color(lbl_name, lv_color_hex(modules[i].enabled ? COLOR_TEXT_DIM : 0x555555), 0); lv_obj_align(lbl_name, LV_ALIGN_TOP_MID, 0, -5); lv_obj_set_style_text_color(lbl_value, lv_color_hex(modules[i].enabled ? COLOR_TEXT_ACCENT : 0x555555), 0); lv_obj_align(lbl_value, LV_ALIGN_CENTER, 0, 5); lv_obj_set_style_text_align(lbl_value, LV_TEXT_ALIGN_CENTER, 0); } else { /* Standard: name at top, value at bottom */ lv_obj_set_style_text_color(lbl_name, lv_color_hex(modules[i].enabled ? COLOR_TEXT_DIM : 0x555555), 0); lv_obj_align(lbl_name, LV_ALIGN_TOP_MID, 0, -5); lv_obj_set_style_text_color(lbl_value, lv_color_hex(modules[i].enabled ? COLOR_TEXT_ACCENT : 0x555555), 0); lv_obj_align(lbl_value, LV_ALIGN_BOTTOM_MID, 0, -2); } /* Set opacity based on enabled state */ if (!modules[i].enabled) { lv_obj_set_style_img_opa(img, LV_OPA_30, 0); } settings_module_obj[i] = cont; settings_module_img[i] = img; settings_module_label_name[i] = lbl_name; settings_module_label_value[i] = lbl_value; } /* Final pipe connector after last module */ { int pipe_x = start_x + MODULE_COUNT * (MODULE_WIDTH + PIPE_W); lv_obj_t *p = lv_obj_create(schema_panel); lv_obj_set_pos(p, pipe_x, pipe_sy); lv_obj_set_size(p, PIPE_W, PIPE_H); lv_obj_set_style_pad_all(p, 0, 0); lv_obj_set_style_bg_opa(p, LV_OPA_TRANSP, 0); lv_obj_set_style_border_width(p, 0, 0); lv_obj_clear_flag(p, LV_OBJ_FLAG_SCROLLABLE | LV_OBJ_FLAG_CLICKABLE); lv_obj_t *pimg = lv_image_create(p); lv_image_set_src(pimg, &img_pipe); lv_obj_align(pimg, LV_ALIGN_CENTER, 0, 0); } /* Update all settings module displays with current values */ for (int i = 0; i < MODULE_COUNT; i++) { update_settings_module_display(i); } /* === CONFIGURATION PANEL === (bottom part) */ lv_obj_t *panel_main = lv_obj_create(scr); lv_obj_set_pos(panel_main, 15, 190); lv_obj_set_size(panel_main, 770, 160); lv_obj_set_style_bg_color(panel_main, lv_color_hex(COLOR_BG_PANEL), 0); lv_obj_set_style_radius(panel_main, 6, 0); lv_obj_set_style_border_width(panel_main, 0, 0); lv_obj_clear_flag(panel_main, LV_OBJ_FLAG_SCROLLABLE); /* Page indicator */ snprintf(buf, sizeof(buf), "%d/2", config_page + 1); lbl = lv_label_create(panel_main); lv_label_set_text(lbl, buf); lv_obj_set_pos(lbl, 720, 5); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_t *btn; int y = 35; /* PAGE 0: Sensor corrections - 2 columns */ if (config_page == 0) { lbl = lv_label_create(panel_main); lv_label_set_text(lbl, "Коррекция датчиков"); lv_obj_set_pos(lbl, 10, 5); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_WARNING), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); const char *labels[] = { "Темп. помещ:", "Влажн. помещ:", "Темп. канала:", "Влажн. канала:" }; float *offsets[] = { &config.temp_room_offset, &config.hum_room_offset, &config.temp_channel_offset, &config.hum_channel_offset }; int col_w = 370; for (int i = 0; i < 4; i++) { int col = i % 2; int row = i / 2; int bx = 10 + col * (col_w + 10); int by = y + row * 32; btn = lv_btn_create(panel_main); lv_obj_set_pos(btn, bx, by); lv_obj_set_size(btn, col_w, 28); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); lv_obj_t *lbl_name = lv_label_create(btn); lv_label_set_text(lbl_name, labels[i]); lv_obj_align(lbl_name, LV_ALIGN_LEFT_MID, 15, 0); lv_obj_set_style_text_color(lbl_name, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl_name, &montserrat_16_ru_en, 0); snprintf(buf, sizeof(buf), "%+.1f", *offsets[i]); lv_obj_t *lbl_val = lv_label_create(btn); lv_label_set_text(lbl_val, buf); lv_obj_align(lbl_val, LV_ALIGN_RIGHT_MID, -15, 0); lv_obj_set_style_text_color(lbl_val, lv_color_hex(COLOR_ACCENT), 0); lv_obj_set_style_text_font(lbl_val, &montserrat_16_ru_en, 0); lv_obj_add_event_cb(btn, edit_config_offset_cb, LV_EVENT_CLICKED, offsets[i]); } } /* PAGE 1: Режим работы и Дата/время - 2 columns */ else if (config_page == 1) { lbl = lv_label_create(panel_main); lv_label_set_text(lbl, "Режим работы"); lv_obj_set_pos(lbl, 10, 5); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_WARNING), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); int col_w = 370; /* Column 1: Force mode */ btn = lv_btn_create(panel_main); lv_obj_set_pos(btn, 10, y); lv_obj_set_size(btn, col_w, 28); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, "Принуд. режим:"); lv_obj_align(lbl, LV_ALIGN_LEFT_MID, 15, 0); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, config.force_mode_enabled ? "ВКЛ" : "ВЫКЛ"); lv_obj_align(lbl, LV_ALIGN_RIGHT_MID, -15, 0); lv_obj_set_style_text_color(lbl, config.force_mode_enabled ? lv_color_hex(COLOR_WARNING) : lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_add_event_cb(btn, toggle_force_mode_cb, LV_EVENT_CLICKED, NULL); /* Column 2: Key swap */ btn = lv_btn_create(panel_main); lv_obj_set_pos(btn, 10 + col_w + 10, y); lv_obj_set_size(btn, col_w, 28); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, "Перемена ключа:"); lv_obj_align(lbl, LV_ALIGN_LEFT_MID, 15, 0); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, config.key_swap_enabled ? "ВКЛ" : "ВЫКЛ"); lv_obj_align(lbl, LV_ALIGN_RIGHT_MID, -15, 0); lv_obj_set_style_text_color(lbl, config.key_swap_enabled ? lv_color_hex(COLOR_ACCENT) : lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_add_event_cb(btn, toggle_key_swap_cb, LV_EVENT_CLICKED, NULL); /* Date/time - row 2 spanning full width */ y += 42; lbl = lv_label_create(panel_main); lv_label_set_text(lbl, "Дата и время"); lv_obj_set_pos(lbl, 10, y); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_WARNING), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); y += 25; btn = lv_btn_create(panel_main); lv_obj_set_pos(btn, 10, y); lv_obj_set_size(btn, col_w, 28); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); snprintf(buf, sizeof(buf), "%02d.%02d.%04d %02d:%02d", state.day, state.month, state.year, state.hour, state.minute); lbl = lv_label_create(btn); lv_label_set_text(lbl, buf); lv_obj_center(lbl); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT_ACCENT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_add_event_cb(btn, edit_datetime_cb, LV_EVENT_CLICKED, NULL); } /* Pagination buttons - below content */ btn = lv_btn_create(scr); lv_obj_set_pos(btn, 300, 365); lv_obj_set_size(btn, 80, 30); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, "<"); lv_obj_set_style_text_color(lbl, config_page > 0 ? lv_color_hex(COLOR_TEXT) : lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn, btn_config_page_prev_cb, LV_EVENT_CLICKED, NULL); /* Page indicator */ lbl = lv_label_create(scr); snprintf(buf, sizeof(buf), "%d/2", config_page + 1); lv_label_set_text(lbl, buf); lv_obj_set_pos(lbl, 395, 372); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); btn = lv_btn_create(scr); lv_obj_set_pos(btn, 445, 365); lv_obj_set_size(btn, 80, 30); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, ">"); lv_obj_set_style_text_color(lbl, config_page < 1 ? lv_color_hex(COLOR_TEXT) : lv_color_hex(COLOR_TEXT_DIM), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn, btn_config_page_next_cb, LV_EVENT_CLICKED, NULL); /* Bottom buttons - using screen3 style colors */ btn = lv_btn_create(scr); lv_obj_set_pos(btn, 200, 410); lv_obj_set_size(btn, 110, 38); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, "Сохранить"); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_ACCENT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn, btn_save_config_cb, LV_EVENT_CLICKED, NULL); btn = lv_btn_create(scr); lv_obj_set_pos(btn, 330, 410); lv_obj_set_size(btn, 140, 38); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, "По умолчанию"); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_WARNING), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn, btn_default_config_cb, LV_EVENT_CLICKED, NULL); btn = lv_btn_create(scr); lv_obj_set_pos(btn, 490, 410); lv_obj_set_size(btn, 100, 38); lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); lv_obj_set_style_radius(btn, 4, 0); lbl = lv_label_create(btn); lv_label_set_text(lbl, "Меню"); lv_obj_set_style_text_color(lbl, lv_color_hex(COLOR_TEXT), 0); lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); lv_obj_center(lbl); lv_obj_add_event_cb(btn, btn_menu_cb, LV_EVENT_CLICKED, NULL); } void gui_update_values(void) { char buf[64]; /* Recalculate season from date */ state.season = calc_season_from_month(state.month); /* Update room climate */ if (lbl_temp_room) { snprintf(buf, sizeof(buf), "%.1f°C", state.temp_room / 10.0f); lv_label_set_text(lbl_temp_room, buf); } if (lbl_hum_room) { snprintf(buf, sizeof(buf), "%.1f%%", state.hum_room / 10.0f); lv_label_set_text(lbl_hum_room, buf); } /* Update channel climate */ if (lbl_temp_channel) { snprintf(buf, sizeof(buf), "%.1f°C", state.temp_channel / 10.0f); lv_label_set_text(lbl_temp_channel, buf); } if (lbl_hum_channel) { snprintf(buf, sizeof(buf), "%.1f%%", state.hum_channel / 10.0f); lv_label_set_text(lbl_hum_channel, buf); } /* Update system status */ if (lbl_room_num) { snprintf(buf, sizeof(buf), "Комната: %d", state.room_num); lv_label_set_text(lbl_room_num, buf); } if (lbl_master_slave) { if (state.is_master) { lv_label_set_text(lbl_master_slave, "Мастер"); lv_obj_set_style_text_color(lbl_master_slave, lv_color_hex(COLOR_ACCENT), 0); } else { snprintf(buf, sizeof(buf), "Слейв %s", state.has_signal ? "(OK)" : "(НЕТ)"); lv_label_set_text(lbl_master_slave, buf); lv_obj_set_style_text_color(lbl_master_slave, state.has_signal ? lv_color_hex(COLOR_WARNING) : lv_color_hex(COLOR_DANGER), 0); } } if (lbl_season) { const char *seasons[] = {"Зима", "Лето", "Переход"}; const uint32_t colors[] = {COLOR_TEXT_ACCENT, COLOR_DANGER, COLOR_WARNING}; snprintf(buf, sizeof(buf), "Сезон: %s", seasons[state.season % 3]); lv_label_set_text(lbl_season, buf); lv_obj_set_style_text_color(lbl_season, lv_color_hex(colors[state.season % 3]), 0); } if (lbl_mode) { const char *modes[] = {"Остановка", "Дежурный", "Рабочий"}; const uint32_t colors[] = {COLOR_DANGER, COLOR_WARNING, COLOR_ACCENT}; snprintf(buf, sizeof(buf), "Режим: %s", modes[state.mode % 3]); lv_label_set_text(lbl_mode, buf); lv_obj_set_style_text_color(lbl_mode, lv_color_hex(colors[state.mode % 3]), 0); } if (lbl_datetime) { snprintf(buf, sizeof(buf), "%02d.%02d.%04d %02d:%02d", state.day, state.month, state.year, state.hour, state.minute); lv_label_set_text(lbl_datetime, buf); } /* Update equipment modules */ for (int i = 0; i < MODULE_COUNT; i++) { update_module_display(i); } } /* Update display of a single module */ static void update_module_display(int module_idx) { if (module_idx < 0 || module_idx >= MODULE_COUNT) return; if (!modules[module_idx].label_value) return; /* If module is disabled, show "ВЫКЛ" in grey */ if (!modules[module_idx].enabled) { lv_label_set_text(modules[module_idx].label_value, "ВЫКЛ"); lv_obj_set_style_text_color(modules[module_idx].label_value, lv_color_hex(0x555555), 0); return; } char buf[32]; switch (module_idx) { case MODULE_VALVE_INLET: snprintf(buf, sizeof(buf), "%d%%", state.valve_inlet); lv_label_set_text(modules[module_idx].label_value, buf); break; case MODULE_VALVE_RECIRC: snprintf(buf, sizeof(buf), "%d%%", state.valve_recirc); lv_label_set_text(modules[module_idx].label_value, buf); break; case MODULE_FILTER_G4: lv_label_set_text(modules[module_idx].label_value, state.filter_g4_clean ? "OK" : "!"); lv_obj_set_style_text_color(modules[module_idx].label_value, state.filter_g4_clean ? lv_color_hex(COLOR_ACCENT) : lv_color_hex(COLOR_DANGER), 0); break; case MODULE_KWS: snprintf(buf, sizeof(buf), "%d%%", state.kws_valve); lv_label_set_text(modules[module_idx].label_value, buf); break; case MODULE_PWW: snprintf(buf, sizeof(buf), "%d%%", state.pww_valve); lv_label_set_text(modules[module_idx].label_value, buf); break; case MODULE_HEATER: lv_label_set_text(modules[module_idx].label_value, state.heater_on ? "ВКЛ" : "ВЫКЛ"); lv_obj_set_style_text_color(modules[module_idx].label_value, state.heater_on ? lv_color_hex(COLOR_DANGER) : lv_color_hex(COLOR_TEXT_DIM), 0); break; case MODULE_FAN: lv_label_set_text(modules[module_idx].label_value, state.fan_on ? "ВКЛ" : "ВЫКЛ"); lv_obj_set_style_text_color(modules[module_idx].label_value, state.fan_on ? lv_color_hex(COLOR_ACCENT) : lv_color_hex(COLOR_DANGER), 0); break; case MODULE_FILTER_H13: lv_label_set_text(modules[module_idx].label_value, state.filter_h13_clean ? "OK" : "!"); lv_obj_set_style_text_color(modules[module_idx].label_value, state.filter_h13_clean ? lv_color_hex(COLOR_ACCENT) : lv_color_hex(COLOR_DANGER), 0); break; case MODULE_CHANNEL: /* Display channel temperature and humidity */ snprintf(buf, sizeof(buf), "%.1f°\n%.0f%%", state.temp_channel / 10.0f, state.hum_channel / 10.0f); lv_label_set_text(modules[module_idx].label_value, buf); break; } } /* Update display of a single module on settings screen */ static void update_settings_module_display(int module_idx) { if (module_idx < 0 || module_idx >= MODULE_COUNT) return; if (!settings_module_label_value[module_idx]) return; char buf[32]; switch (module_idx) { case MODULE_VALVE_INLET: snprintf(buf, sizeof(buf), "%d%%", state.valve_inlet); lv_label_set_text(settings_module_label_value[module_idx], buf); break; case MODULE_VALVE_RECIRC: snprintf(buf, sizeof(buf), "%d%%", state.valve_recirc); lv_label_set_text(settings_module_label_value[module_idx], buf); break; case MODULE_FILTER_G4: lv_label_set_text(settings_module_label_value[module_idx], state.filter_g4_clean ? "OK" : "!"); lv_obj_set_style_text_color(settings_module_label_value[module_idx], state.filter_g4_clean ? lv_color_hex(COLOR_ACCENT) : lv_color_hex(COLOR_DANGER), 0); break; case MODULE_KWS: snprintf(buf, sizeof(buf), "%d%%", state.kws_valve); lv_label_set_text(settings_module_label_value[module_idx], buf); break; case MODULE_PWW: snprintf(buf, sizeof(buf), "%d%%", state.pww_valve); lv_label_set_text(settings_module_label_value[module_idx], buf); break; case MODULE_HEATER: lv_label_set_text(settings_module_label_value[module_idx], state.heater_on ? "ВКЛ" : "ВЫКЛ"); lv_obj_set_style_text_color(settings_module_label_value[module_idx], state.heater_on ? lv_color_hex(COLOR_DANGER) : lv_color_hex(COLOR_TEXT_DIM), 0); break; case MODULE_FAN: lv_label_set_text(settings_module_label_value[module_idx], state.fan_on ? "ВКЛ" : "ВЫКЛ"); lv_obj_set_style_text_color(settings_module_label_value[module_idx], state.fan_on ? lv_color_hex(COLOR_ACCENT) : lv_color_hex(COLOR_DANGER), 0); break; case MODULE_FILTER_H13: lv_label_set_text(settings_module_label_value[module_idx], state.filter_h13_clean ? "OK" : "!"); lv_obj_set_style_text_color(settings_module_label_value[module_idx], state.filter_h13_clean ? lv_color_hex(COLOR_ACCENT) : lv_color_hex(COLOR_DANGER), 0); break; case MODULE_CHANNEL: /* Display channel temperature and humidity */ snprintf(buf, sizeof(buf), "%.1f°\n%.0f%%", state.temp_channel / 10.0f, state.hum_channel / 10.0f); lv_label_set_text(settings_module_label_value[module_idx], buf); break; } } /* Refresh setpoints display on screen 3 */ static void refresh_setpoints_display(void) { /* Recreate screen 3 only if it exists */ if (scr_screen3) { lv_obj_del(scr_screen3); scr_screen3 = NULL; create_screen_screen3(); lv_scr_load(scr_screen3); } /* Invalidate screen4 so it rebuilds on next entry */ if (scr_screen4) { lv_obj_del(scr_screen4); scr_screen4 = NULL; } } void gui_init(void) { /* Calculate season from current month */ state.season = calc_season_from_month(state.month); /* Create and load only main screen initially (lazy loading for memory) */ create_screen_main(); lv_scr_load(scr_main); /* Проверка аварийных ситуаций после загрузки главного экрана */ if (!state.filter_g4_clean) { add_alarm_to_log("ФИЛЬТР G4 ЗАСОРИЛСЯ - Замените фильтр", 1); } if (state.temp_channel < 50) { add_alarm_to_log("УГРОЗА ЗАМОРОЗКИ - Установка остановлена", 1); state.mode = 0; update_mode_button_colors(); } /* Show alarm popup if needed (only one at a time) */ if (!state.filter_g4_clean) { create_popup_alarm("ФИЛЬТР G4 ЗАСОРИЛСЯ - Замените фильтр"); } else if (state.temp_channel < 50) { create_popup_alarm("УГРОЗА ЗАМОРОЗКИ - Установка остановлена"); } /* Update all module displays with initial data */ gui_update_values(); /* Start Modbus simulation timer (every 2 seconds) */ sync_setpoints_to_slave(); lv_timer_create(modbus_simulation_timer_cb, 2000, NULL); /* Other screens created on demand when user navigates to them */ } /* ── ModBus Simulation ────────────────────────────── */ /* Timer-based Modbus slave simulation */ static void modbus_simulation_timer_cb(lv_timer_t *timer) { (void)timer; /* === Slave simulation: process commands and generate sensor feedback === */ int16_t target_temp_x10 = (int16_t)(slave.target_temp * 10); int16_t target_hum_x10 = (int16_t)(slave.target_hum * 10); /* Room temperature drifts toward target */ if (slave.fb_temp_room < target_temp_x10) { slave.fb_temp_room += 1 + (rand() % 2); if (slave.fb_temp_room > target_temp_x10) slave.fb_temp_room = target_temp_x10; } else if (slave.fb_temp_room > target_temp_x10) { slave.fb_temp_room -= 1 + (rand() % 2); if (slave.fb_temp_room < target_temp_x10) slave.fb_temp_room = target_temp_x10; } else { slave.fb_temp_room += (rand() % 3) - 1; } /* Room humidity drifts toward target */ if (slave.fb_hum_room < target_hum_x10) { slave.fb_hum_room += 1 + (rand() % 3); if (slave.fb_hum_room > target_hum_x10) slave.fb_hum_room = target_hum_x10; } else if (slave.fb_hum_room > target_hum_x10) { slave.fb_hum_room -= 1 + (rand() % 3); if (slave.fb_hum_room < target_hum_x10) slave.fb_hum_room = target_hum_x10; } else { slave.fb_hum_room += (rand() % 3) - 1; } /* Channel temperature: slightly above room target, influenced by KWS/PWW */ int16_t ch_target = target_temp_x10; if (state.mode != 0) { ch_target += 20; if (slave.cmd_kws_valve > 0 && state.kws_mode == 0) ch_target -= slave.cmd_kws_valve / 5; if (slave.cmd_pww_valve > 0 && state.pww_mode == 1) ch_target += slave.cmd_pww_valve / 5; if (slave.cmd_heater) ch_target += 30; } else { ch_target = 150; } if (slave.fb_temp_channel < ch_target) { slave.fb_temp_channel += 1 + (rand() % 2); if (slave.fb_temp_channel > ch_target) slave.fb_temp_channel = ch_target; } else if (slave.fb_temp_channel > ch_target) { slave.fb_temp_channel -= 1 + (rand() % 2); if (slave.fb_temp_channel < ch_target) slave.fb_temp_channel = ch_target; } /* Channel humidity */ slave.fb_hum_channel = target_hum_x10 + 50 + (rand() % 30) - 15; /* === Master reads feedback from slave === */ state.temp_room = slave.fb_temp_room; state.hum_room = slave.fb_hum_room; state.temp_channel = slave.fb_temp_channel; state.hum_channel = slave.fb_hum_channel; /* Valve positions reflect slave commands */ if (state.mode != 0) { state.valve_inlet = slave.cmd_valve_inlet; state.valve_recirc = slave.cmd_valve_recirc; state.fan_on = slave.cmd_fan; state.heater_on = slave.cmd_heater; /* Auto KWS/PWW: simple proportional response */ if (slave.kws_auto && modules[MODULE_KWS].enabled) { int16_t err = state.temp_room - target_temp_x10; if (state.kws_mode == 0) { /* cooling */ state.kws_valve = (err > 0) ? (uint8_t)(err > 100 ? 100 : err) : 0; } else { /* heating */ state.kws_valve = (err < 0) ? (uint8_t)((-err) > 100 ? 100 : (-err)) : 0; } } else if (!slave.kws_auto) { state.kws_valve = slave.cmd_kws_valve; } if (slave.pww_auto && modules[MODULE_PWW].enabled) { int16_t err = state.temp_room - target_temp_x10; if (state.pww_mode == 0) { /* cooling */ state.pww_valve = (err > 0) ? (uint8_t)(err > 100 ? 100 : err) : 0; } else { /* heating */ state.pww_valve = (err < 0) ? (uint8_t)((-err) > 100 ? 100 : (-err)) : 0; } } else if (!slave.pww_auto) { state.pww_valve = slave.cmd_pww_valve; } } else { state.valve_inlet = 0; state.valve_recirc = 0; state.kws_valve = 0; state.pww_valve = 0; state.fan_on = 0; state.heater_on = 0; } /* Increment simulated clock */ state.minute++; if (state.minute >= 60) { state.minute = 0; state.hour++; if (state.hour >= 24) { state.hour = 0; state.day++; if (state.day > 28) { state.day = 1; state.month++; if (state.month > 12) { state.month = 1; state.year++; } } } } gui_update_values(); } void gui_modbus_init(void) { /* No-op for simulation */ } void gui_modbus_task(void *arg) { (void)arg; /* Replaced by modbus_simulation_timer_cb (LVGL timer) */ }