From 8332577f86344337d78118dbff1c77928d9fd2f4 Mon Sep 17 00:00:00 2001 From: 01trisha Date: Wed, 11 Mar 2026 05:34:53 +0700 Subject: [PATCH] upd main screen and settings --- .gitignore | 2 + main/gui_generated.c | 1526 +++++++++++++++++++++++++++--------------- 2 files changed, 1002 insertions(+), 526 deletions(-) diff --git a/.gitignore b/.gitignore index 123b0c5..f90b0c3 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ .directory .vscode/ dependencies.lock +.cache/ +MODBUS_SYSTEM.md # Temporary files *~ diff --git a/main/gui_generated.c b/main/gui_generated.c index ed88b56..0cd2a52 100644 --- a/main/gui_generated.c +++ b/main/gui_generated.c @@ -5,6 +5,7 @@ #include #include #include +#include /* Fonts */ LV_FONT_DECLARE(montserrat_16_ru_en) @@ -68,30 +69,30 @@ typedef struct { } SysState_t; static SysState_t state = { - .temp_room = 220, - .hum_room = 450, - .temp_channel = 200, - .hum_channel = 500, + .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, - .mode = 1, - .valve_inlet = 75, - .valve_recirc = 25, + .season = 0, /* will be recalculated from date in gui_init */ + .mode = 1, /* Дежурный по умолчанию */ + .valve_inlet = 0, + .valve_recirc = 0, .filter_g4_clean = 0, /* Фильтр загрязнен для демонстрации */ - .kws_valve = 50, + .kws_valve = 0, .kws_mode = 1, - .pww_valve = 30, - .pww_mode = 0, /* PWW в режиме охлаждения для демонстрации */ + .pww_valve = 0, + .pww_mode = 0, .heater_on = 0, - .fan_on = 1, + .fan_on = 0, .filter_h13_clean = 1, .year = 2026, - .month = 2, - .day = 26, + .month = 3, + .day = 10, .hour = 12, - .minute = 30 + .minute = 0 }; /* Module configuration for drag&drop */ @@ -116,28 +117,27 @@ typedef struct { lv_obj_t *img; /* указатель на изображение */ lv_obj_t *label_name; /* название модуля */ lv_obj_t *label_value; /* указатель на значение */ - lv_obj_t *pipe_placeholder; /* труба-заглушка когда модуль отключен */ } module_config_t; static module_config_t modules[MODULE_COUNT] = { - {MODULE_VALVE_INLET, 1, 0, NULL, NULL, NULL, NULL, NULL}, - {MODULE_VALVE_RECIRC, 1, 1, NULL, NULL, NULL, NULL, NULL}, - {MODULE_FILTER_G4, 1, 2, NULL, NULL, NULL, NULL, NULL}, - {MODULE_KWS, 1, 3, NULL, NULL, NULL, NULL, NULL}, - {MODULE_PWW, 1, 4, NULL, NULL, NULL, NULL, NULL}, - {MODULE_HEATER, 1, 5, NULL, NULL, NULL, NULL, NULL}, - {MODULE_FAN, 1, 6, NULL, NULL, NULL, NULL, NULL}, - {MODULE_FILTER_H13, 1, 7, NULL, NULL, NULL, NULL, NULL}, - {MODULE_CHANNEL, 1, 8, NULL, NULL, NULL, NULL, NULL}, + {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_ACTIVE_Y 20 -#define MODULES_INACTIVE_Y 175 -#define MODULE_SCALE 140 /* Scale factor: 256=100%, 140~55% for 64x64 images */ +#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; @@ -150,13 +150,16 @@ static lv_obj_t *lbl_stop_mode = NULL; static lv_obj_t *lbl_standby_mode = NULL; static lv_obj_t *lbl_work_mode = NULL; -/* Pipe placeholders for inactive modules in active line */ -static lv_obj_t *pipe_placeholders[MODULE_COUNT] = {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}; -/* Drag state for modules */ -static lv_obj_t *dragged_module = NULL; -static int dragged_module_idx = -1; -static lv_point_t drag_start_pos; +/* 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 @@ -255,6 +258,44 @@ static config_t config = { 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] = ""; @@ -284,12 +325,10 @@ static lv_obj_t *lbl_season = NULL; static lv_obj_t *lbl_mode = NULL; static lv_obj_t *lbl_datetime = NULL; -/* Equipment UI references */ -static lv_obj_t *panel_schema = 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; @@ -310,11 +349,17 @@ static void create_popup_param_editor(const char *name, float *param_ptr, int is 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 module_drag_cb(lv_event_t *e); 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); @@ -327,8 +372,16 @@ 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_module_positions(void); 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) { @@ -381,99 +434,92 @@ static const char* get_module_name(int module_idx) { } } -/* Module drag event handler */ -static void module_drag_cb(lv_event_t *e) { +/* 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); - lv_obj_t *obj = lv_event_get_target(e); int module_idx = (int)(intptr_t)lv_event_get_user_data(e); - - if (code == LV_EVENT_PRESSING) { - lv_indev_t *indev = lv_indev_active(); - if (indev == NULL) return; - - lv_point_t vect; - lv_indev_get_vect(indev, &vect); - - lv_coord_t x = lv_obj_get_x(obj) + vect.x; - lv_coord_t y = lv_obj_get_y(obj) + vect.y; - lv_obj_set_pos(obj, x, y); - } - else if (code == LV_EVENT_RELEASED) { - /* Check if module should be enabled/disabled based on Y position */ - lv_coord_t y = lv_obj_get_y(obj); - int threshold = (MODULES_ACTIVE_Y + MODULES_INACTIVE_Y) / 2; - - if (y < threshold) { - modules[module_idx].enabled = 1; - } else { - modules[module_idx].enabled = 0; + + if (code == LV_EVENT_CLICKED) { + /* If long press already fired, just consume the click */ + if (longpress_fired) { + longpress_fired = false; + return; } - - /* Reposition all modules */ - update_module_positions(); + /* 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); } } -/* Update positions of all modules and pipe placeholders */ -static void update_module_positions(void) { - int active_x = 50; /* Start after the inlet pipe */ - int inactive_x = 10; - - for (int i = 0; i < MODULE_COUNT; i++) { - if (modules[i].obj == NULL) continue; - - if (modules[i].enabled) { - /* Module is active - position it on the active line */ - lv_obj_set_pos(modules[i].obj, active_x, MODULES_ACTIVE_Y); - lv_obj_clear_flag(modules[i].obj, LV_OBJ_FLAG_HIDDEN); - - /* Hide pipe placeholder if exists */ - if (pipe_placeholders[i]) { - lv_obj_add_flag(pipe_placeholders[i], LV_OBJ_FLAG_HIDDEN); - } - - active_x += MODULE_WIDTH + MODULE_SPACING; - } else { - /* Module is inactive - move to inactive area */ - lv_obj_set_pos(modules[i].obj, inactive_x, MODULES_INACTIVE_Y); - lv_obj_clear_flag(modules[i].obj, LV_OBJ_FLAG_HIDDEN); - - /* Show pipe placeholder on active line */ - if (pipe_placeholders[i]) { - lv_obj_set_pos(pipe_placeholders[i], active_x, MODULES_ACTIVE_Y); - lv_obj_clear_flag(pipe_placeholders[i], LV_OBJ_FLAG_HIDDEN); - } - - active_x += MODULE_WIDTH + MODULE_SPACING; - inactive_x += MODULE_WIDTH + MODULE_SPACING; - } +/* 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); } -/* Create a module widget with image */ +/* 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 */ + /* 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_color(cont, lv_color_hex(COLOR_BG_MODULE), 0); - lv_obj_set_style_radius(cont, 5, 0); - lv_obj_set_style_border_width(cont, 1, 0); - lv_obj_set_style_border_color(cont, lv_color_hex(COLOR_BORDER), 0); + 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); - - /* Add drag flag */ - lv_obj_add_flag(cont, LV_OBJ_FLAG_CLICKABLE); - lv_obj_add_event_cb(cont, module_drag_cb, LV_EVENT_PRESSING, (void*)(intptr_t)module_idx); - lv_obj_add_event_cb(cont, module_drag_cb, LV_EVENT_RELEASED, (void*)(intptr_t)module_idx); - - /* Module name label at top */ - lv_obj_t *lbl_name = lv_label_create(cont); - lv_label_set_text(lbl_name, get_module_name(module_idx)); - lv_obj_align(lbl_name, LV_ALIGN_TOP_MID, 0, 0); - lv_obj_set_style_text_color(lbl_name, lv_color_hex(COLOR_TEXT_DIM), 0); - lv_obj_set_style_text_font(lbl_name, &montserrat_16_ru_en, 0); + lv_obj_clear_flag(cont, LV_OBJ_FLAG_CLICKABLE); /* Not clickable on main screen */ /* Image */ lv_obj_t *img = lv_image_create(cont); @@ -481,12 +527,37 @@ static lv_obj_t* create_module_widget(lv_obj_t *parent, int module_idx, int x, i lv_image_set_scale(img, MODULE_SCALE); lv_obj_align(img, LV_ALIGN_CENTER, 0, 0); - /* Value label at bottom */ + /* 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_align(lbl_value, LV_ALIGN_BOTTOM_MID, 0, -2); - lv_obj_set_style_text_color(lbl_value, lv_color_hex(COLOR_TEXT_ACCENT), 0); 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; @@ -500,34 +571,72 @@ 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 { /* Дежурный или Рабочий режим - применяем уставки */ - state.valve_inlet = (uint8_t)sp->valve_inlet; - state.valve_recirc = (uint8_t)sp->valve_recirc; + /* 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 = sp->fan; + /* Вентилятор - выключен если модуль отключён */ + state.fan_on = modules[MODULE_FAN].enabled ? sp->fan : 0; - /* Электронагреватель по уставке */ - if (sp->heater == 0) { - state.heater_on = 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; /* ВКЛ */ + state.heater_on = 1; } /* heater == 2 (АВТО) - управляется автоматикой */ /* KWS и PWW */ - if (!sp->kws_auto) { + if (modules[MODULE_KWS].enabled && !sp->kws_auto) { state.kws_valve = (uint8_t)sp->kws_manual; } - if (!sp->pww_auto) { + 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 */ @@ -585,12 +694,27 @@ static void btn_work_cb(lv_event_t *e) { static void btn_menu_cb(lv_event_t *e) { (void)e; - if (scr_screen1) lv_scr_load(scr_screen1); + if (!scr_screen1) { + create_screen_screen1(); + } + lv_scr_load(scr_screen1); } static void btn_back_to_main_cb(lv_event_t *e) { (void)e; - if (scr_main) lv_scr_load(scr_main); + /* 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) { @@ -737,8 +861,8 @@ static void btn_config_page_prev_cb(lv_event_t *e) { static void btn_config_page_next_cb(lv_event_t *e) { (void)e; - /* Total ~15 items, 6 per page = 3 pages */ - if (config_page < 2) { + /* Total 2 pages now */ + if (config_page < 1) { config_page++; if (scr_screen4) { lv_obj_del(scr_screen4); @@ -756,6 +880,13 @@ static void close_popup_cb(lv_event_t *e) { } 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)); @@ -769,12 +900,18 @@ static void close_popup_cb(lv_event_t *e) { static void btn_log_cb(lv_event_t *e) { (void)e; - if (scr_screen2) lv_scr_load(scr_screen2); + 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) lv_scr_load(scr_screen3); + if (!scr_screen3) { + create_screen_screen3(); + } + lv_scr_load(scr_screen3); } static void btn_config_cb(lv_event_t *e) { @@ -829,10 +966,8 @@ static void edit_standby_valve_recirc_cb(lv_event_t *e) { static void edit_work_heater_cb(lv_event_t *e) { (void)e; setpoint_work.heater = (setpoint_work.heater + 1) % 3; - /* Apply immediately if in work mode */ if (state.mode == 2) { - if (setpoint_work.heater == 0) state.heater_on = 0; - else if (setpoint_work.heater == 1) state.heater_on = 1; + apply_mode_setpoints(); gui_update_values(); } refresh_setpoints_display(); @@ -841,10 +976,8 @@ static void edit_work_heater_cb(lv_event_t *e) { static void edit_standby_heater_cb(lv_event_t *e) { (void)e; setpoint_standby.heater = (setpoint_standby.heater + 1) % 3; - /* Apply immediately if in standby mode */ if (state.mode == 1) { - if (setpoint_standby.heater == 0) state.heater_on = 0; - else if (setpoint_standby.heater == 1) state.heater_on = 1; + apply_mode_setpoints(); gui_update_values(); } refresh_setpoints_display(); @@ -854,9 +987,8 @@ static void edit_standby_heater_cb(lv_event_t *e) { static void edit_work_fan_cb(lv_event_t *e) { (void)e; setpoint_work.fan = !setpoint_work.fan; - /* Apply immediately if in work mode */ if (state.mode == 2) { - state.fan_on = setpoint_work.fan; + apply_mode_setpoints(); gui_update_values(); } refresh_setpoints_display(); @@ -865,9 +997,8 @@ static void edit_work_fan_cb(lv_event_t *e) { static void edit_standby_fan_cb(lv_event_t *e) { (void)e; setpoint_standby.fan = !setpoint_standby.fan; - /* Apply immediately if in standby mode */ if (state.mode == 1) { - state.fan_on = setpoint_standby.fan; + apply_mode_setpoints(); gui_update_values(); } refresh_setpoints_display(); @@ -879,9 +1010,8 @@ static void edit_work_kws_cb(lv_event_t *e) { if (setpoint_work.kws_auto) { setpoint_work.kws_auto = 0; setpoint_work.kws_manual = 50.0f; - /* Apply immediately if in work mode */ if (state.mode == 2) { - state.kws_valve = (uint8_t)setpoint_work.kws_manual; + apply_mode_setpoints(); gui_update_values(); } refresh_setpoints_display(); @@ -895,9 +1025,8 @@ static void edit_standby_kws_cb(lv_event_t *e) { if (setpoint_standby.kws_auto) { setpoint_standby.kws_auto = 0; setpoint_standby.kws_manual = 50.0f; - /* Apply immediately if in standby mode */ if (state.mode == 1) { - state.kws_valve = (uint8_t)setpoint_standby.kws_manual; + apply_mode_setpoints(); gui_update_values(); } refresh_setpoints_display(); @@ -912,9 +1041,8 @@ static void edit_work_pww_cb(lv_event_t *e) { if (setpoint_work.pww_auto) { setpoint_work.pww_auto = 0; setpoint_work.pww_manual = 50.0f; - /* Apply immediately if in work mode */ if (state.mode == 2) { - state.pww_valve = (uint8_t)setpoint_work.pww_manual; + apply_mode_setpoints(); gui_update_values(); } refresh_setpoints_display(); @@ -928,9 +1056,8 @@ static void edit_standby_pww_cb(lv_event_t *e) { if (setpoint_standby.pww_auto) { setpoint_standby.pww_auto = 0; setpoint_standby.pww_manual = 50.0f; - /* Apply immediately if in standby mode */ if (state.mode == 1) { - state.pww_valve = (uint8_t)setpoint_standby.pww_manual; + apply_mode_setpoints(); gui_update_values(); } refresh_setpoints_display(); @@ -948,13 +1075,21 @@ static void edit_config_offset_cb(lv_event_t *e) { static void toggle_recirc_valve_cb(lv_event_t *e) { (void)e; config.has_recirc_valve = !config.has_recirc_valve; - refresh_setpoints_display(); + /* 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; - refresh_setpoints_display(); + /* 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) { @@ -990,15 +1125,82 @@ static void edit_pid_kws_d_cb(lv_event_t *e) { static void toggle_force_mode_cb(lv_event_t *e) { (void)e; config.force_mode_enabled = !config.force_mode_enabled; - refresh_setpoints_display(); + /* 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; @@ -1205,7 +1407,10 @@ static void password_keypad_cb(lv_event_t *e) { /* Navigate to config screen */ if (password_target == 0) { - if (scr_screen4) lv_scr_load(scr_screen4); + if (!scr_screen4) { + create_screen_config(); + } + lv_scr_load(scr_screen4); } } else { /* Wrong password */ @@ -1358,7 +1563,20 @@ static void param_keypad_cb(lv_event_t *e) { } memset(param_value_str, 0, sizeof(param_value_str)); param_being_edited = NULL; - refresh_setpoints_display(); + + /* 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); @@ -1500,6 +1718,218 @@ static void create_popup_param_editor(const char *name, float *param_ptr, int is 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; @@ -1597,14 +2027,12 @@ static void add_alarm_to_log(const char *message, int active) { } } -/* Create alarm popup */ +/* 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); } - add_alarm_to_log(message, 1); - popup_alarm = lv_obj_create(lv_screen_active()); lv_obj_set_size(popup_alarm, 400, 240); lv_obj_center(popup_alarm); @@ -1680,7 +2108,12 @@ static void create_screen_main(void) { lv_obj_set_style_text_font(lbl_master_slave, &montserrat_16_ru_en, 0); lbl_season = lv_label_create(panel_status); - lv_label_set_text(lbl_season, "Сезон: Зима"); + { + 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); @@ -1724,7 +2157,7 @@ static void create_screen_main(void) { 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, "22.0°C"); + 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); @@ -1736,7 +2169,7 @@ static void create_screen_main(void) { 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, "45%"); + 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); @@ -1749,13 +2182,13 @@ static void create_screen_main(void) { 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, "20.0°C"); + 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, "50%"); + 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); @@ -1841,64 +2274,52 @@ static void create_screen_main(void) { lv_obj_set_style_border_width(schema_panel, 0, 0); lv_obj_clear_flag(schema_panel, LV_OBJ_FLAG_SCROLLABLE); - /* Hint labels */ + /* Title hint */ lv_obj_t *hint1 = lv_label_create(schema_panel); - lv_label_set_text(hint1, "Активные модули (перетащите вниз для отключения):"); - lv_obj_set_pos(hint1, 10, 2); + 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); - lv_obj_t *hint2 = lv_label_create(schema_panel); - lv_label_set_text(hint2, "Неактивные (перетащите вверх для включения):"); - lv_obj_set_pos(hint2, 10, 155); - lv_obj_set_style_text_color(hint2, lv_color_hex(COLOR_TEXT_DIM), 0); - lv_obj_set_style_text_font(hint2, &montserrat_16_ru_en, 0); - - /* Create continuous pipe background line FIRST (so modules appear on top) */ - /* Calculate total width needed for all module positions */ - int pipe_x = 5; - int pipe_y = MODULES_ACTIVE_Y + 25; - int pipe_segment_width = 35; /* Scaled pipe image width approximately */ - int total_modules_width = 50 + MODULE_COUNT * (MODULE_WIDTH + MODULE_SPACING) + 50; - - /* Create edge pipes (at the start and end of the line) */ - lv_obj_t *pipe_start = lv_image_create(schema_panel); - lv_image_set_src(pipe_start, &img_pipe); - lv_image_set_scale(pipe_start, MODULE_SCALE); - lv_obj_set_pos(pipe_start, 5, pipe_y); - - lv_obj_t *pipe_end = lv_image_create(schema_panel); - lv_image_set_src(pipe_end, &img_pipe); - lv_image_set_scale(pipe_end, MODULE_SCALE); - lv_obj_set_pos(pipe_end, total_modules_width - 40, pipe_y); - - /* Create all modules and pipe placeholders */ - int active_x = 50; /* Start after the inlet pipe */ - int inactive_x = 10; + /* 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++) { - /* Create pipe placeholder for this module slot */ - pipe_placeholders[i] = lv_image_create(schema_panel); - lv_image_set_src(pipe_placeholders[i], &img_pipe); - lv_image_set_scale(pipe_placeholders[i], MODULE_SCALE); - /* Will be positioned by update_module_positions */ + 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 x_pos, y_pos; - if (modules[i].enabled) { - x_pos = active_x; - y_pos = MODULES_ACTIVE_Y; - lv_obj_add_flag(pipe_placeholders[i], LV_OBJ_FLAG_HIDDEN); - active_x += MODULE_WIDTH + MODULE_SPACING; - } else { - /* Module inactive - show pipe placeholder on active line */ - lv_obj_set_pos(pipe_placeholders[i], active_x, MODULES_ACTIVE_Y); - active_x += MODULE_WIDTH + MODULE_SPACING; - - x_pos = inactive_x; - y_pos = MODULES_INACTIVE_Y; - inactive_x += MODULE_WIDTH + MODULE_SPACING; - } - create_module_widget(schema_panel, i, x_pos, y_pos); + 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 */ @@ -2608,6 +3029,7 @@ static void create_screen_screen3(void) { 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); @@ -2619,6 +3041,7 @@ static void create_screen_screen3(void) { 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); @@ -2646,21 +3069,132 @@ static void create_screen_config(void) { /* Header */ lv_obj_t *lbl = lv_label_create(scr); lv_label_set_text(lbl, "НАСТРОЙКИ"); - lv_obj_set_pos(lbl, 340, 10); + 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); - /* Single unified panel - NO SCROLL, with pagination */ + /* === 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, 40); - lv_obj_set_size(panel_main, 770, 310); + 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/3", config_page + 1); + 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); @@ -2670,7 +3204,7 @@ static void create_screen_config(void) { lv_obj_t *btn; int y = 35; - /* PAGE 0: Sensor corrections (4 параметра чтобы не налазить на кнопки) */ + /* PAGE 0: Sensor corrections - 2 columns */ if (config_page == 0) { lbl = lv_label_create(panel_main); lv_label_set_text(lbl, "Коррекция датчиков"); @@ -2679,10 +3213,10 @@ static void create_screen_config(void) { lv_obj_set_style_text_font(lbl, &montserrat_16_ru_en, 0); const char *labels[] = { - "Темп. помещения:", - "Влажн. помещения:", - "Темп. в канале:", - "Влажн. в канале:" + "Темп. помещ:", + "Влажн. помещ:", + "Темп. канала:", + "Влажн. канала:" }; float *offsets[] = { &config.temp_room_offset, @@ -2691,10 +3225,16 @@ static void create_screen_config(void) { &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, 10, y); - lv_obj_set_size(btn, 740, 40); + 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); @@ -2712,236 +3252,22 @@ static void create_screen_config(void) { 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]); - - y += 50; } } - /* PAGE 1: Remaining sensor corrections + Equipment and PID regulators */ + /* 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); - - /* Клапан притока */ - btn = lv_btn_create(panel_main); - lv_obj_set_pos(btn, 10, y); - lv_obj_set_size(btn, 360, 40); - 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, "Клапан притока:"); - 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", config.valve_inlet_offset); - 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, &config.valve_inlet_offset); - - /* Клапан рециркуляции */ - btn = lv_btn_create(panel_main); - lv_obj_set_pos(btn, 390, y); - lv_obj_set_size(btn, 360, 40); - lv_obj_set_style_bg_color(btn, lv_color_hex(COLOR_BG_BTN), 0); - lv_obj_set_style_radius(btn, 4, 0); - - lbl_name = lv_label_create(btn); - lv_label_set_text(lbl_name, "Клапан рецирк.:"); - 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", config.valve_recirc_offset); - 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, &config.valve_recirc_offset); - - y += 55; - - /* Оборудование */ - 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; - - /* Toggles for equipment presence */ - btn = lv_btn_create(panel_main); - lv_obj_set_pos(btn, 10, y); - lv_obj_set_size(btn, 360, 40); - 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.has_recirc_valve ? "ЕСТЬ" : "НЕТ"); - lv_obj_align(lbl, LV_ALIGN_RIGHT_MID, -15, 0); - lv_obj_set_style_text_color(lbl, config.has_recirc_valve ? 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_recirc_valve_cb, LV_EVENT_CLICKED, NULL); - - btn = lv_btn_create(panel_main); - lv_obj_set_pos(btn, 390, y); - lv_obj_set_size(btn, 360, 40); - 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.has_heater ? "ЕСТЬ" : "НЕТ"); - lv_obj_align(lbl, LV_ALIGN_RIGHT_MID, -15, 0); - lv_obj_set_style_text_color(lbl, config.has_heater ? 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_heater_cb, LV_EVENT_CLICKED, NULL); - } - /* PAGE 2: PID regulators, Mode settings and Date/Time */ - else { - /* PID regulators */ - 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); - - /* PWW PID */ - lbl = lv_label_create(panel_main); - lv_label_set_text(lbl, "PWW:"); - lv_obj_set_pos(lbl, 10, 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); - - y += 25; - /* PWW Kp */ - btn = lv_btn_create(panel_main); - lv_obj_set_pos(btn, 10, y); - lv_obj_set_size(btn, 120, 36); - 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), "Kp=%.2f", config.pid_pww_kp); - 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_pid_pww_kp_cb, LV_EVENT_CLICKED, NULL); - - /* PWW Ti */ - btn = lv_btn_create(panel_main); - lv_obj_set_pos(btn, 140, y); - lv_obj_set_size(btn, 120, 36); - 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), "Ti=%.2f", config.pid_pww_ti); - 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_pid_pww_ti_cb, LV_EVENT_CLICKED, NULL); - - /* PWW D */ - btn = lv_btn_create(panel_main); - lv_obj_set_pos(btn, 270, y); - lv_obj_set_size(btn, 120, 36); - 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), "D=%.2f", config.pid_pww_d); - 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_pid_pww_d_cb, LV_EVENT_CLICKED, NULL); - - y += 46; - - /* KWS PID */ - lbl = lv_label_create(panel_main); - lv_label_set_text(lbl, "KWS:"); - lv_obj_set_pos(lbl, 10, 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); - - y += 25; - /* KWS Kp */ - btn = lv_btn_create(panel_main); - lv_obj_set_pos(btn, 10, y); - lv_obj_set_size(btn, 120, 36); - 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), "Kp=%.2f", config.pid_kws_kp); - 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_pid_kws_kp_cb, LV_EVENT_CLICKED, NULL); - - /* KWS Ti */ - btn = lv_btn_create(panel_main); - lv_obj_set_pos(btn, 140, y); - lv_obj_set_size(btn, 120, 36); - 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), "Ti=%.2f", config.pid_kws_ti); - 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_pid_kws_ti_cb, LV_EVENT_CLICKED, NULL); - - /* KWS D */ - btn = lv_btn_create(panel_main); - lv_obj_set_pos(btn, 270, y); - lv_obj_set_size(btn, 120, 36); - 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), "D=%.2f", config.pid_kws_d); - 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_pid_kws_d_cb, LV_EVENT_CLICKED, NULL); - - y += 55; - lbl = lv_label_create(panel_main); lv_label_set_text(lbl, "Режим работы"); - lv_obj_set_pos(lbl, 10, y); + 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); - y += 25; + 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, 740, 40); + 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); @@ -2958,10 +3284,10 @@ static void create_screen_config(void) { 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); - y += 45; + /* Column 2: Key swap */ btn = lv_btn_create(panel_main); - lv_obj_set_pos(btn, 10, y); - lv_obj_set_size(btn, 740, 40); + 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); @@ -2978,17 +3304,18 @@ static void create_screen_config(void) { 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); - y += 55; + /* 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 += 30; + y += 25; btn = lv_btn_create(panel_main); lv_obj_set_pos(btn, 10, y); - lv_obj_set_size(btn, 740, 40); + 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); @@ -3004,8 +3331,8 @@ static void create_screen_config(void) { /* Pagination buttons - below content */ btn = lv_btn_create(scr); - lv_obj_set_pos(btn, 240, 365); - lv_obj_set_size(btn, 100, 38); + 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); @@ -3017,27 +3344,27 @@ static void create_screen_config(void) { /* Page indicator */ lbl = lv_label_create(scr); - snprintf(buf, sizeof(buf), "%d/3", config_page + 1); + snprintf(buf, sizeof(buf), "%d/2", config_page + 1); lv_label_set_text(lbl, buf); - lv_obj_set_pos(lbl, 365, 375); + 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, 430, 365); - lv_obj_set_size(btn, 100, 38); + 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 < 2 ? lv_color_hex(COLOR_TEXT) : lv_color_hex(COLOR_TEXT_DIM), 0); + 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, 420); + 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); @@ -3046,9 +3373,10 @@ static void create_screen_config(void) { 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, 420); + 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); @@ -3057,9 +3385,10 @@ static void create_screen_config(void) { 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, 420); + 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); @@ -3074,6 +3403,9 @@ static void create_screen_config(void) { 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); @@ -3147,6 +3479,13 @@ 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) { @@ -3198,49 +3537,90 @@ static void update_module_display(int module_idx) { } } +/* 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) { - /* Remember which screen was active */ - lv_obj_t *current_scr = lv_screen_active(); - int was_on_screen3 = (current_scr == scr_screen3); - int was_on_screen4 = (current_scr == scr_screen4); - - /* Recreate screen 3 to show updated values */ + /* 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); } - create_screen_screen3(); - /* Recreate screen 4 to show updated values */ + /* Invalidate screen4 so it rebuilds on next entry */ if (scr_screen4) { lv_obj_del(scr_screen4); scr_screen4 = NULL; } - create_screen_config(); - - /* Load appropriate screen */ - if (was_on_screen3) { - lv_scr_load(scr_screen3); - } else if (was_on_screen4) { - lv_scr_load(scr_screen4); - } } 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(); - create_screen_screen1(); - create_screen_screen3(); - create_screen_config(); lv_scr_load(scr_main); /* Проверка аварийных ситуаций после загрузки главного экрана */ if (!state.filter_g4_clean) { add_alarm_to_log("ФИЛЬТР G4 ЗАСОРИЛСЯ - Замените фильтр", 1); - state.mode = 0; - update_mode_button_colors(); } if (state.temp_channel < 50) { @@ -3249,18 +3629,143 @@ void gui_init(void) { update_mode_button_colors(); } - /* Create screen2 after alarms are added */ - create_screen_screen2(); - - /* Show alarm popup if needed */ + /* 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 ──────────────────────────────────────── */ +/* ── 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 */ @@ -3268,36 +3773,5 @@ void gui_modbus_init(void) { void gui_modbus_task(void *arg) { (void)arg; - /* Simulation: update with dummy values */ - while (1) { - state.temp_room = 220 + (rand() % 40); - state.hum_room = 450 + (rand() % 100); - state.temp_channel = 200 + (rand() % 30); - state.hum_channel = 500 + (rand() % 80); - - /* Simulate valve changes based on mode */ - if (state.mode == 2) { - state.valve_inlet = 70 + (rand() % 30); - state.valve_recirc = 10 + (rand() % 20); - state.kws_valve = 40 + (rand() % 30); - state.pww_valve = 30 + (rand() % 25); - state.fan_on = 1; - } else if (state.mode == 1) { - state.valve_inlet = 30 + (rand() % 20); - state.valve_recirc = 40 + (rand() % 20); - state.kws_valve = 20 + (rand() % 15); - state.pww_valve = 15 + (rand() % 15); - state.fan_on = 1; - } 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; - } - - gui_update_values(); - /* Sleep simulation - requires FreeRTOS or use SDL delay */ - } + /* Replaced by modbus_simulation_timer_cb (LVGL timer) */ }