Add thermostat control for heat and fan with cycle protection
Turns the weather dashboard into an actual thermostat. Relays are not
fitted yet -- the GPIOs are driven and can be metered at the UART1-OUT
plug (idle 3.3V, energized 0V, active-low module assumed).
Control loop (main/control/thermostat.c) is pure logic: no ESP-IDF, no
network, no clock of its own. The caller passes a temperature and a
millisecond timestamp, which is what lets the simulator and the test
harness run the identical decision code.
Equipment protections, all verified by tools/thermostat_test.c:
deadband 1.0F stops the burner chattering at the setpoint
minimum on 3 min a shorter call is all ignition wear, no heat
minimum off 5 min back-to-back ignitions are what kill igniters
cycles/hour 5 standard gas default; catches a flapping sensor
boot lockout 60 s a boot loop must not machine-gun the gas valve
That test drives the state machine with synthetic time, so two hours of
furnace behaviour runs in milliseconds. It exits non-zero on failure and
is worth re-running after any edit to thermostat.c -- this switches a gas
valve.
Two deliberate decisions:
- G is never energized during a heat call. The furnace runs its own
blower with a fan-on delay that waits for the heat exchanger and a
fan-off delay that scavenges residual heat; driving G ourselves would
blow cold air at people and throw that heat away. G is manual
circulation only.
- The control loop starts before every early return in app_main. Those
returns are all "no WiFi" or "no HA token", and none of them is a
reason to leave a house unheated.
Relay pins are IO20 heat / IO19 fan on the UART1-OUT plug, measured on
this board since Elecrow documents the GPIO numbers but not the connector
pin order. Heat is on IO20 because it is USB D+, the line carrying the
hardware pull-up -- with an active-low module it fails toward "no heat"
rather than "heat stuck on" if the USB PHY is ever re-enabled. Both pins
were confirmed free of the onboard mic before use.
CONFIG_USJ_ENABLE_USB_SERIAL_JTAG and CONFIG_ESP_PHY_ENABLE_USB are now
off to release them; the console is on UART0 via the CH340 regardless.
UI: the indoor card gains a status dot, mode and fan buttons, and a
setpoint column with finger-sized up/down targets. The reading grid drops
from six cells to four to make room -- wind was the one to cut, since the
station anemometer has been dead since 2026-07-27 and that cell has never
shown a number. Wind now rides on the outdoor cell's sub-line and will
reappear on its own if the sensor is fixed. Sunrise and sunset share a
cell, showing whichever comes next.
Known weakness, documented in README and top of TODO: indoor temperature
comes from Home Assistant over the network, so an HA outage ages the
reading until the 15-minute failsafe forces heat off. A local I2C sensor
removes the failure mode entirely and is the next thing to add. The
setpoint also resets to 68F on reboot, because persisting it means an NVS
write and a flash write while the panel scans faults the bounce-buffer
ISR.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
ae5d12b3a2
commit
3aabcac90a
16 files changed
+1420
-53
No files matched your search
+243
-53
@@ -111,13 +111,18 @@ static lv_obj_t *temp_unit_label;
|
||||
static lv_obj_t *indoor_detail_label;
|
||||
static lv_obj_t *trend_indoor_icon;
|
||||
|
||||
/* 3x2 reading grid: value and caption per cell. */
|
||||
/*
|
||||
* 2x2 reading grid.
|
||||
*
|
||||
* Was 3x2. The thermostat controls need finger-sized targets and that space
|
||||
* had to come from somewhere; wind was the right thing to cut, since the
|
||||
* station anemometer has been dead since 2026-07-27 and the cell has never
|
||||
* shown a number. Sunrise and sunset share one cell now.
|
||||
*/
|
||||
enum {
|
||||
CELL_OUTDOOR = 0,
|
||||
CELL_WIND,
|
||||
CELL_RAIN,
|
||||
CELL_SUNRISE,
|
||||
CELL_SUNSET,
|
||||
CELL_SUN,
|
||||
CELL_PRESSURE,
|
||||
CELL_COUNT,
|
||||
};
|
||||
@@ -138,7 +143,17 @@ static lv_obj_t *fc_temp[HA_FORECAST_DAYS];
|
||||
|
||||
static lv_obj_t *status_label;
|
||||
|
||||
/* Thermostat card */
|
||||
static lv_obj_t *therm_state_label;
|
||||
static lv_obj_t *therm_state_dot;
|
||||
static lv_obj_t *setpoint_label;
|
||||
static lv_obj_t *mode_btn;
|
||||
static lv_obj_t *mode_btn_label;
|
||||
static lv_obj_t *fan_btn;
|
||||
static lv_obj_t *fan_btn_label;
|
||||
|
||||
static ui_light_cmd_cb_t light_cb;
|
||||
static ui_therm_cmd_cb_t therm_cb;
|
||||
|
||||
/* Per-room icons, riding in HA_LIGHT_LIST so order can never drift from the
|
||||
* entity table. */
|
||||
@@ -348,6 +363,55 @@ static void trend_render(lv_obj_t *icon, ha_trend_t t)
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------ thermostat controls */
|
||||
|
||||
/*
|
||||
* A flat touch button with a centred label. `out_label` is optional -- pass
|
||||
* it for buttons whose caption changes (mode, fan), omit for fixed ones.
|
||||
*
|
||||
* Sized by the caller rather than by content: these are finger targets on a
|
||||
* capacitive panel, so the hit area is the specification, not the text.
|
||||
*/
|
||||
static lv_obj_t *button_create(lv_obj_t *parent, int32_t w, int32_t h,
|
||||
const char *text, lv_obj_t **out_label)
|
||||
{
|
||||
lv_obj_t *btn = lv_obj_create(parent);
|
||||
lv_obj_set_size(btn, w, h);
|
||||
lv_obj_set_style_radius(btn, 8, LV_PART_MAIN);
|
||||
lv_obj_set_style_border_width(btn, 0, LV_PART_MAIN);
|
||||
lv_obj_set_style_pad_all(btn, 0, LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_color(btn, lv_color_hex(COL_PANEL_HI), LV_PART_MAIN);
|
||||
/* Visible press feedback matters more here than on the light tiles: these
|
||||
* act instantly and locally, so the flash is the whole acknowledgement. */
|
||||
lv_obj_set_style_bg_color(btn, lv_color_hex(COL_ACCENT), LV_STATE_PRESSED);
|
||||
lv_obj_remove_flag(btn, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_add_flag(btn, LV_OBJ_FLAG_CLICKABLE);
|
||||
|
||||
lv_obj_t *lbl = label_create(btn, text, &lv_font_montserrat_16, COL_TEXT);
|
||||
lv_obj_center(lbl);
|
||||
|
||||
if (out_label != NULL) {
|
||||
*out_label = lbl;
|
||||
}
|
||||
return btn;
|
||||
}
|
||||
|
||||
/*
|
||||
* Runs on the LVGL task with the lock held. Unlike the light commands there
|
||||
* is no network involved, so the callback applies the change and pushes the
|
||||
* new state straight back -- the LVGL lock is recursive, so re-entering
|
||||
* ui_update_thermostat() from here is fine.
|
||||
*/
|
||||
static void therm_button_clicked(lv_event_t *e)
|
||||
{
|
||||
const ui_therm_cmd_t cmd = (ui_therm_cmd_t)(intptr_t)lv_event_get_user_data(e);
|
||||
|
||||
if (therm_cb != NULL) {
|
||||
therm_cb(cmd);
|
||||
}
|
||||
schedule_repair();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ light tiles */
|
||||
|
||||
static void light_tile_update(int idx, const ha_light_status_t *st);
|
||||
@@ -713,9 +777,9 @@ static void build_body(lv_obj_t *scr)
|
||||
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE);
|
||||
|
||||
/* --- indoor card: the number a thermostat exists to show --- */
|
||||
lv_obj_t *card = panel_create(row, 296, LV_PCT(100), COL_PANEL);
|
||||
lv_obj_set_style_pad_all(card, 14, LV_PART_MAIN);
|
||||
/* --- thermostat card: current temperature and what the furnace is doing --- */
|
||||
lv_obj_t *card = panel_create(row, 300, LV_PCT(100), COL_PANEL);
|
||||
lv_obj_set_style_pad_all(card, 12, LV_PART_MAIN);
|
||||
|
||||
lv_obj_t *heading = label_create(card, "INDOOR", &lv_font_montserrat_14, COL_TEXT_DIM);
|
||||
lv_obj_set_style_text_letter_space(heading, 2, LV_PART_MAIN);
|
||||
@@ -724,11 +788,8 @@ static void build_body(lv_obj_t *scr)
|
||||
lv_obj_t *card_icon = icon_create(card, ICON_HOME_THERMO, icon_font_26, COL_TEXT_FAINT);
|
||||
lv_obj_align(card_icon, LV_ALIGN_TOP_RIGHT, 0, -2);
|
||||
|
||||
/* Centred vertically rather than pinned under the heading: the card is
|
||||
* 182px and a 48px number at the top leaves an obvious hole in the middle
|
||||
* of the most important element on the screen. */
|
||||
temp_label = label_create(card, "--", &lv_font_montserrat_48, COL_TEXT);
|
||||
lv_obj_align(temp_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
lv_obj_align(temp_label, LV_ALIGN_TOP_LEFT, 0, 22);
|
||||
|
||||
/* Degree sign as its own label so it can sit small and high beside a
|
||||
* 48px number instead of scaling with it. */
|
||||
@@ -740,10 +801,56 @@ static void build_body(lv_obj_t *scr)
|
||||
trend_indoor_icon = icon_create(card, "", icon_font_26, COL_TEXT_DIM);
|
||||
lv_obj_align_to(trend_indoor_icon, temp_label, LV_ALIGN_OUT_RIGHT_BOTTOM, 8, -4);
|
||||
|
||||
indoor_detail_label = label_create(card, "--", &lv_font_montserrat_16, COL_TEXT_DIM);
|
||||
lv_obj_align(indoor_detail_label, LV_ALIGN_BOTTOM_LEFT, 0, 0);
|
||||
indoor_detail_label = label_create(card, "--", &lv_font_montserrat_14, COL_TEXT_DIM);
|
||||
lv_obj_align(indoor_detail_label, LV_ALIGN_TOP_LEFT, 0, 78);
|
||||
|
||||
/* --- 3x2 reading grid --- */
|
||||
/* Status dot plus text: the dot carries the state as colour so it reads
|
||||
* from across the room without parsing a word. */
|
||||
therm_state_dot = lv_obj_create(card);
|
||||
lv_obj_set_size(therm_state_dot, 12, 12);
|
||||
lv_obj_set_style_radius(therm_state_dot, 6, LV_PART_MAIN);
|
||||
lv_obj_set_style_border_width(therm_state_dot, 0, LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_color(therm_state_dot, lv_color_hex(COL_TEXT_FAINT), LV_PART_MAIN);
|
||||
lv_obj_remove_flag(therm_state_dot, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_align(therm_state_dot, LV_ALIGN_TOP_LEFT, 2, 104);
|
||||
|
||||
therm_state_label = label_create(card, "--", &lv_font_montserrat_16, COL_TEXT_DIM);
|
||||
lv_obj_align(therm_state_label, LV_ALIGN_TOP_LEFT, 22, 100);
|
||||
|
||||
/* Two buttons across the bottom of the card. 134px each is a comfortable
|
||||
* finger target; the row fills the card's 276px inner width. */
|
||||
mode_btn = button_create(card, 134, 40, "Heat", &mode_btn_label);
|
||||
lv_obj_align(mode_btn, LV_ALIGN_BOTTOM_LEFT, 0, 0);
|
||||
lv_obj_add_event_cb(mode_btn, therm_button_clicked, LV_EVENT_CLICKED,
|
||||
(void *)(intptr_t)UI_THERM_MODE_TOGGLE);
|
||||
|
||||
fan_btn = button_create(card, 134, 40, "Fan auto", &fan_btn_label);
|
||||
lv_obj_align(fan_btn, LV_ALIGN_BOTTOM_RIGHT, 0, 0);
|
||||
lv_obj_add_event_cb(fan_btn, therm_button_clicked, LV_EVENT_CLICKED,
|
||||
(void *)(intptr_t)UI_THERM_FAN_TOGGLE);
|
||||
|
||||
/* --- setpoint column: up, value, down --- */
|
||||
lv_obj_t *setcol = panel_create(row, 120, LV_PCT(100), COL_PANEL);
|
||||
lv_obj_set_style_pad_all(setcol, 8, LV_PART_MAIN);
|
||||
|
||||
lv_obj_t *up_btn = button_create(setcol, LV_PCT(100), 52, "+", NULL);
|
||||
lv_obj_align(up_btn, LV_ALIGN_TOP_MID, 0, 0);
|
||||
lv_obj_add_event_cb(up_btn, therm_button_clicked, LV_EVENT_CLICKED,
|
||||
(void *)(intptr_t)UI_THERM_SETPOINT_UP);
|
||||
|
||||
lv_obj_t *set_cap = label_create(setcol, "SET", &lv_font_montserrat_12, COL_TEXT_FAINT);
|
||||
lv_obj_set_style_text_letter_space(set_cap, 1, LV_PART_MAIN);
|
||||
lv_obj_align(set_cap, LV_ALIGN_CENTER, 0, -14);
|
||||
|
||||
setpoint_label = label_create(setcol, "--", &lv_font_montserrat_32, COL_ACCENT_WARM);
|
||||
lv_obj_align(setpoint_label, LV_ALIGN_CENTER, 0, 8);
|
||||
|
||||
lv_obj_t *down_btn = button_create(setcol, LV_PCT(100), 52, "-", NULL);
|
||||
lv_obj_align(down_btn, LV_ALIGN_BOTTOM_MID, 0, 0);
|
||||
lv_obj_add_event_cb(down_btn, therm_button_clicked, LV_EVENT_CLICKED,
|
||||
(void *)(intptr_t)UI_THERM_SETPOINT_DOWN);
|
||||
|
||||
/* --- 2x2 reading grid --- */
|
||||
lv_obj_t *grid = lv_obj_create(row);
|
||||
lv_obj_set_size(grid, LV_GRID_CONTENT, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(grid, 1);
|
||||
@@ -755,11 +862,10 @@ static void build_body(lv_obj_t *scr)
|
||||
lv_obj_remove_flag(grid, LV_OBJ_FLAG_SCROLLABLE);
|
||||
|
||||
static const char *const captions[CELL_COUNT] = {
|
||||
"OUTDOOR", "WIND", "RAIN TODAY", "SUNRISE", "SUNSET", "PRESSURE",
|
||||
"OUTDOOR", "RAIN TODAY", "SUN", "PRESSURE",
|
||||
};
|
||||
static const char *const cell_icons[CELL_COUNT] = {
|
||||
ICON_THERMOMETER, ICON_W_WINDY, ICON_WATER,
|
||||
ICON_SUNRISE, ICON_SUNSET, ICON_GAUGE,
|
||||
ICON_THERMOMETER, ICON_WATER, ICON_SUNSET, ICON_GAUGE,
|
||||
};
|
||||
|
||||
/* Two rows of cells plus one gap must equal BAND_BODY_H exactly, or the
|
||||
@@ -767,7 +873,7 @@ static void build_body(lv_obj_t *scr)
|
||||
const int32_t cell_h = (BAND_BODY_H - BAND_GAP) / 2;
|
||||
|
||||
for (int i = 0; i < CELL_COUNT; i++) {
|
||||
lv_obj_t *cell = panel_create(grid, 156, cell_h, COL_PANEL);
|
||||
lv_obj_t *cell = panel_create(grid, 172, cell_h, COL_PANEL);
|
||||
lv_obj_set_style_pad_all(cell, 10, LV_PART_MAIN);
|
||||
|
||||
lv_obj_t *cap = label_create(cell, captions[i], &lv_font_montserrat_12, COL_TEXT_FAINT);
|
||||
@@ -877,8 +983,29 @@ static void update_body(const ha_data_t *d)
|
||||
lv_obj_align_to(cell_trend[CELL_OUTDOOR], cell_value[CELL_OUTDOOR],
|
||||
LV_ALIGN_OUT_RIGHT_MID, 6, 0);
|
||||
|
||||
if (d->outdoor_humidity.valid && d->dew_point_f.valid) {
|
||||
snprintf(sub, sizeof(sub), "%.0f%% dew %.0f°",
|
||||
/*
|
||||
* Wind lives on the outdoor cell's sub-line now that it has no cell of
|
||||
* its own -- the station anemometer has been offline since 2026-07-27 and
|
||||
* a dedicated cell showing "--" was not worth the space the thermostat
|
||||
* controls needed. It is still deliberately NOT backfilled from the
|
||||
* met.no forecast: regional modelled wind under a heading that means "my
|
||||
* yard" is worse than showing nothing.
|
||||
*
|
||||
* When the sensor comes back it displaces the dew point here, which is
|
||||
* both a useful reading and a visible sign the hardware is alive again.
|
||||
*/
|
||||
if (d->wind_mph.valid) {
|
||||
if (d->wind_dir_deg.valid) {
|
||||
snprintf(sub, sizeof(sub), "%.0f%% %.0f mph %s",
|
||||
d->outdoor_humidity.valid ? d->outdoor_humidity.value : 0.0f,
|
||||
d->wind_mph.value, ha_wind_cardinal(d->wind_dir_deg.value));
|
||||
} else {
|
||||
snprintf(sub, sizeof(sub), "%.0f%% %.0f mph",
|
||||
d->outdoor_humidity.valid ? d->outdoor_humidity.value : 0.0f,
|
||||
d->wind_mph.value);
|
||||
}
|
||||
} else if (d->outdoor_humidity.valid && d->dew_point_f.valid) {
|
||||
snprintf(sub, sizeof(sub), "%.0f%% dew %.0f°",
|
||||
d->outdoor_humidity.value, d->dew_point_f.value);
|
||||
} else if (d->outdoor_humidity.valid) {
|
||||
snprintf(sub, sizeof(sub), "%.0f%% humidity", d->outdoor_humidity.value);
|
||||
@@ -887,30 +1014,6 @@ static void update_body(const ha_data_t *d)
|
||||
}
|
||||
label_set_if_changed(cell_sub[CELL_OUTDOOR], sub);
|
||||
|
||||
/*
|
||||
* Wind. The station anemometer has been offline since 2026-07-27, so this
|
||||
* cell is expected to read "--" until the hardware is fixed. It is
|
||||
* deliberately NOT backfilled from the met.no forecast: showing regional
|
||||
* modelled wind under a heading that means "my yard" is worse than
|
||||
* showing nothing.
|
||||
*/
|
||||
format_value(&d->wind_mph, "%.0f", buf, sizeof(buf));
|
||||
if (d->wind_mph.valid) {
|
||||
const size_t n = strlen(buf);
|
||||
snprintf(buf + n, sizeof(buf) - n, " mph");
|
||||
}
|
||||
label_set_if_changed(cell_value[CELL_WIND], buf);
|
||||
|
||||
if (d->wind_dir_deg.valid && d->gust_mph.valid) {
|
||||
snprintf(sub, sizeof(sub), "%s gust %.0f",
|
||||
ha_wind_cardinal(d->wind_dir_deg.value), d->gust_mph.value);
|
||||
} else if (d->wind_dir_deg.valid) {
|
||||
snprintf(sub, sizeof(sub), "%s", ha_wind_cardinal(d->wind_dir_deg.value));
|
||||
} else {
|
||||
snprintf(sub, sizeof(sub), "sensor offline");
|
||||
}
|
||||
label_set_if_changed(cell_sub[CELL_WIND], sub);
|
||||
|
||||
/* Rain */
|
||||
format_value(&d->rain_today_in, "%.2f\"", buf, sizeof(buf));
|
||||
label_set_if_changed(cell_value[CELL_RAIN], buf);
|
||||
@@ -921,15 +1024,25 @@ static void update_body(const ha_data_t *d)
|
||||
}
|
||||
label_set_if_changed(cell_sub[CELL_RAIN], sub);
|
||||
|
||||
/* Sun. These strings are already local time from HA -- the board has no
|
||||
* clock of its own. */
|
||||
label_set_if_changed(cell_value[CELL_SUNRISE],
|
||||
d->sunrise[0] ? d->sunrise : NO_VALUE);
|
||||
label_set_if_changed(cell_sub[CELL_SUNRISE], d->sun_up ? "up now" : "before dawn");
|
||||
|
||||
label_set_if_changed(cell_value[CELL_SUNSET],
|
||||
d->sunset[0] ? d->sunset : NO_VALUE);
|
||||
label_set_if_changed(cell_sub[CELL_SUNSET], d->sun_up ? "still up" : "down");
|
||||
/*
|
||||
* Sun. One cell for both times now: the value line shows whichever is
|
||||
* next, and the sub-line the other, so the useful number is the big one.
|
||||
* These strings are already local time from HA -- the board has no clock.
|
||||
*/
|
||||
/* Plain hyphen as the separator, not a middot -- Montserrat has no
|
||||
* U+00B7 and it renders as a tofu box. See the note on NO_VALUE. */
|
||||
if (d->sun_up) {
|
||||
label_set_if_changed(cell_value[CELL_SUN],
|
||||
d->sunset[0] ? d->sunset : NO_VALUE);
|
||||
snprintf(sub, sizeof(sub), "sets - rose %s",
|
||||
d->sunrise[0] ? d->sunrise : NO_VALUE);
|
||||
} else {
|
||||
label_set_if_changed(cell_value[CELL_SUN],
|
||||
d->sunrise[0] ? d->sunrise : NO_VALUE);
|
||||
snprintf(sub, sizeof(sub), "rises - set %s",
|
||||
d->sunset[0] ? d->sunset : NO_VALUE);
|
||||
}
|
||||
label_set_if_changed(cell_sub[CELL_SUN], sub);
|
||||
|
||||
/* Pressure */
|
||||
format_value(&d->pressure_inhg, "%.2f", buf, sizeof(buf));
|
||||
@@ -1062,6 +1175,83 @@ void ui_set_light_cb(ui_light_cmd_cb_t cb)
|
||||
light_cb = cb;
|
||||
}
|
||||
|
||||
void ui_set_therm_cb(ui_therm_cmd_cb_t cb)
|
||||
{
|
||||
therm_cb = cb;
|
||||
}
|
||||
|
||||
void ui_update_thermostat(const therm_status_t *st)
|
||||
{
|
||||
if (st == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!lvgl_port_lock(UI_LOCK_TIMEOUT_MS)) {
|
||||
ESP_LOGW(TAG, "Dropped thermostat update: LVGL lock timed out");
|
||||
return;
|
||||
}
|
||||
|
||||
char buf[32];
|
||||
|
||||
snprintf(buf, sizeof(buf), "%d°", (int)lroundf(st->setpoint_f));
|
||||
label_set_if_changed(setpoint_label, buf);
|
||||
|
||||
thermostat_state_text(st, buf, sizeof(buf));
|
||||
label_set_if_changed(therm_state_label, buf);
|
||||
|
||||
/*
|
||||
* The dot colour is the at-a-glance channel: amber means the burner is
|
||||
* lit, red means something is wrong and no heat is coming, dim means
|
||||
* everything is fine and there is nothing to do.
|
||||
*/
|
||||
uint32_t dot = COL_TEXT_FAINT;
|
||||
uint32_t text = COL_TEXT_DIM;
|
||||
switch (st->state) {
|
||||
case THERM_STATE_HEATING:
|
||||
dot = COL_ON;
|
||||
text = COL_ON;
|
||||
break;
|
||||
case THERM_STATE_LOCKOUT:
|
||||
dot = COL_ACCENT_WARM;
|
||||
text = COL_ACCENT_WARM;
|
||||
break;
|
||||
case THERM_STATE_NO_SENSOR:
|
||||
dot = 0xE74C3C;
|
||||
text = 0xE74C3C;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
lv_obj_set_style_bg_color(therm_state_dot, lv_color_hex(dot), LV_PART_MAIN);
|
||||
lv_obj_set_style_text_color(therm_state_label, lv_color_hex(text), LV_PART_MAIN);
|
||||
|
||||
const bool heat_mode = (st->mode == THERM_MODE_HEAT);
|
||||
label_set_if_changed(mode_btn_label, heat_mode ? "Heat" : "Off");
|
||||
lv_obj_set_style_bg_color(mode_btn,
|
||||
lv_color_hex(heat_mode ? COL_ACCENT_WARM : COL_PANEL_HI),
|
||||
LV_PART_MAIN);
|
||||
lv_obj_set_style_text_color(mode_btn_label,
|
||||
lv_color_hex(heat_mode ? 0x2A1A05 : COL_TEXT_DIM),
|
||||
LV_PART_MAIN);
|
||||
|
||||
const bool fan_on = (st->fan == THERM_FAN_ON);
|
||||
label_set_if_changed(fan_btn_label, fan_on ? "Fan on" : "Fan auto");
|
||||
lv_obj_set_style_bg_color(fan_btn,
|
||||
lv_color_hex(fan_on ? COL_ACCENT : COL_PANEL_HI),
|
||||
LV_PART_MAIN);
|
||||
lv_obj_set_style_text_color(fan_btn_label,
|
||||
lv_color_hex(fan_on ? 0x05131F : COL_TEXT),
|
||||
LV_PART_MAIN);
|
||||
|
||||
/* Setpoint greys out when the mode is off -- it still adjusts, it just
|
||||
* is not controlling anything. */
|
||||
lv_obj_set_style_text_color(setpoint_label,
|
||||
lv_color_hex(heat_mode ? COL_ACCENT_WARM : COL_TEXT_FAINT),
|
||||
LV_PART_MAIN);
|
||||
|
||||
lvgl_port_unlock();
|
||||
}
|
||||
|
||||
void ui_init(void)
|
||||
{
|
||||
/* ui_init() runs from app_main, not the LVGL task, so it needs the lock
|
||||
|
||||
Reference in new issue
Block a user