hardware-hmi-display-ui/main/gui_generated.c

3778 lines
148 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* AUTO-GENERATED by GUI Editor — do not edit manually */
#include "gui_generated.h"
#include "lvgl.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <stdbool.h>
/* 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) */
}