diff --git a/zephcore/helpers/ui-button/ui_pages.c b/zephcore/helpers/ui-button/ui_pages.c index da6a69f..792d0f1 100644 --- a/zephcore/helpers/ui-button/ui_pages.c +++ b/zephcore/helpers/ui-button/ui_pages.c @@ -56,6 +56,22 @@ LOG_MODULE_REGISTER(ui_pages, CONFIG_ZEPHCORE_BOARD_LOG_LEVEL); #define CONTENT_Y (SEP_Y + 4) /* below separator */ #define MAX_CHARS (DISP_W / (FONT_W ? FONT_W : 1)) +/* Compact RGB565 palette for small color TFTs. These are used only through + * mc_display_has_color(); monochrome displays keep the existing CFB path. */ +#define UI_COLOR_HEADER_BG 0x018c /* deep teal/blue */ +#define UI_COLOR_TITLE 0x5dff /* readable cyan */ +#define UI_COLOR_LABEL 0x8c71 /* muted blue-gray */ +#define UI_COLOR_VALUE MC_COLOR_WHITE +#define UI_COLOR_OK 0x07e0 +#define UI_COLOR_ACTIVE 0x05ff +#define UI_COLOR_WARN 0xffa0 +#define UI_COLOR_ERROR MC_COLOR_RED +#define UI_COLOR_DIM 0x4208 +#define UI_COLOR_DISABLED MC_COLOR_GRAY + +#define COLOR_FONT_W 6 +#define COLOR_FONT_H 8 + /* Vertically center `total` rows of text within the content area and * return the y pixel position for row `idx` (0-based). This replaces * the old hand-picked offsets (CONTENT_Y+16, +28, +34, ...) that were @@ -141,6 +157,7 @@ static char time_source_tag(enum time_sync_source src) static void render_top_bar(void) { + bool color = mc_display_has_color(); /* Right side: "HH:MM XX%" — clock (with 1-char source tag) then * battery, right-aligned. Built first so the node name can be clipped * to the space that remains on the left, preventing overlap. */ @@ -173,15 +190,33 @@ static void render_top_bar(void) int right_x = DISP_W; if (right[0]) { - right_x = DISP_W - ((int)strlen(right) * FONT_W); - mc_display_text(right_x, TOP_BAR_Y, right, false); + int fw = color ? COLOR_FONT_W : FONT_W; + uint16_t batt_color = UI_COLOR_VALUE; + + if (state.battery_mv > 0) { + uint8_t pct = state.battery_pct; + + if (pct == 0) { + pct = calc_battery_pct(state.battery_mv); + } + batt_color = (pct <= 15) ? UI_COLOR_ERROR : + (pct <= 30) ? UI_COLOR_WARN : UI_COLOR_OK; + } + + right_x = DISP_W - ((int)strlen(right) * fw); + if (color) { + mc_display_color_text(right_x, TOP_BAR_Y, right, batt_color); + } else { + mc_display_text(right_x, TOP_BAR_Y, right, false); + } } /* Node name on the left, clipped to the gap before the right block * (one char-width of padding so it never touches the clock). */ if (state.node_name[0]) { char name[16]; - int max_chars = (right_x - FONT_W) / FONT_W; + int fw = color ? COLOR_FONT_W : FONT_W; + int max_chars = (right_x - fw) / fw; if (max_chars > (int)sizeof(name) - 1) { max_chars = sizeof(name) - 1; @@ -191,7 +226,11 @@ static void render_top_bar(void) } strncpy(name, state.node_name, max_chars); name[max_chars] = '\0'; - mc_display_text(0, TOP_BAR_Y, name, false); + if (color) { + mc_display_color_text(0, TOP_BAR_Y, name, UI_COLOR_TITLE); + } else { + mc_display_text(0, TOP_BAR_Y, name, false); + } } } @@ -202,6 +241,7 @@ static void render_page_indicator(void) /* Centered dots matching Arduino layout: * Each dot spaced 10px apart, centered on screen. * Current page = filled 3x3 block, others = single pixel. */ + bool color = mc_display_has_color(); int total = ACTIVE_PAGE_COUNT; int dot_spacing = 10; int total_width = (total - 1) * dot_spacing; @@ -212,15 +252,29 @@ static void render_page_indicator(void) if (i == current_page_idx) { /* Current page: filled 3x3 block */ - mc_display_fill_rect(x - 1, DOTS_Y, 3, 3); + if (color) { + mc_display_color_fill_rect(x - 2, DOTS_Y - 1, 5, 5, + UI_COLOR_ACTIVE); + } else { + mc_display_fill_rect(x - 1, DOTS_Y, 3, 3); + } } else { /* Other pages: single pixel */ - mc_display_fill_rect(x, DOTS_Y + 1, 1, 1); + if (color) { + mc_display_color_fill_rect(x, DOTS_Y + 1, 1, 1, + UI_COLOR_DIM); + } else { + mc_display_fill_rect(x, DOTS_Y + 1, 1, 1); + } } } /* Separator line below dots */ - mc_display_hline(0, SEP_Y, DISP_W); + if (color) { + mc_display_color_fill_rect(0, SEP_Y, DISP_W, 1, UI_COLOR_DIM); + } else { + mc_display_hline(0, SEP_Y, DISP_W); + } } /* ========== Helper: Center text ========== */ @@ -236,14 +290,77 @@ static void draw_centered(int y, const char *text) mc_display_text(x, y, text, false); } +static void draw_centered_color(int y, const char *text, uint16_t color) +{ + int len = (int)strlen(text); + int x = (DISP_W - (len * COLOR_FONT_W)) / 2; + + if (x < 0) { + x = 0; + } + mc_display_color_text(x, y, text, color); +} + +static void draw_color_segments_at(int x, int y, const char *label, + const char *value, uint16_t value_color) +{ + mc_display_color_text(x, y, label, UI_COLOR_LABEL); + x += (int)strlen(label) * 6; + mc_display_color_text(x, y, value, value_color); +} + static void draw_color_segments(int y, const char *label, const char *value, uint16_t value_color) { - int x = 0; + draw_color_segments_at(0, y, label, value, value_color); +} - mc_display_color_text(x, y, label, MC_COLOR_CYAN); - x += (int)strlen(label) * 6; - mc_display_color_text(x, y, value, value_color); +static int color_text_width(const char *text) +{ + return (int)strlen(text) * COLOR_FONT_W; +} + +static void draw_badge(int x, int y, const char *text, uint16_t color) +{ + int w = color_text_width(text) + 4; + + mc_display_color_fill_rect(x, y - 1, w, COLOR_FONT_H + 2, UI_COLOR_DIM); + mc_display_color_text(x + 2, y, text, color); +} + +static void draw_metric_bar(int x, int y, int w, int h, int value, + int max_value, uint16_t color) +{ + if (w <= 0 || h <= 0) { + return; + } + if (max_value <= 0) { + max_value = 1; + } + if (value < 0) { + value = 0; + } + if (value > max_value) { + value = max_value; + } + + int filled = (w * value) / max_value; + + mc_display_color_fill_rect(x, y, w, h, UI_COLOR_DIM); + if (filled > 0) { + mc_display_color_fill_rect(x, y, filled, h, color); + } +} + +static void fmt_compact_count(char *buf, size_t len, uint32_t value) +{ + if (value >= 1000000U) { + snprintf(buf, len, "%luM", (unsigned long)(value / 1000000U)); + } else if (value >= 1000U) { + snprintf(buf, len, "%luk", (unsigned long)(value / 1000U)); + } else { + snprintf(buf, len, "%lu", (unsigned long)value); + } } /* ========== Page Renderers ========== */ @@ -253,6 +370,38 @@ static void render_messages(void) /* 3 centered rows: msg count, BLE status, offgrid status */ char buf[24]; + if (mc_display_has_color()) { + int y = CONTENT_Y; + uint16_t ble_color = state.ble_connected ? UI_COLOR_OK : UI_COLOR_WARN; + const char *ble = state.ble_connected ? "BLE OK" : "BLE ADV"; + + draw_badge(0, y, ble, ble_color); + draw_badge(DISP_W - color_text_width(state.offgrid_enabled ? "GRID" : "LOCAL") - 4, + y, state.offgrid_enabled ? "GRID" : "LOCAL", + state.offgrid_enabled ? UI_COLOR_ACTIVE : UI_COLOR_DISABLED); + y += LINE_H + 2; + + mc_display_color_text(0, y, "MESSAGES", UI_COLOR_LABEL); + snprintf(buf, sizeof(buf), "%u", state.msg_count); + mc_display_color_text(DISP_W - color_text_width(buf), y, buf, + state.msg_count ? UI_COLOR_WARN : UI_COLOR_VALUE); + y += LINE_H; + + if (state.ble_connected) { + draw_centered_color(y, "Connected", UI_COLOR_OK); + } else { + draw_centered_color(y, "Waiting for app", UI_COLOR_WARN); + } + y += LINE_H; + + snprintf(buf, sizeof(buf), "Offgrid %s", + state.offgrid_enabled ? "on" : "off"); + draw_centered_color(y, buf, + state.offgrid_enabled ? UI_COLOR_ACTIVE + : UI_COLOR_DISABLED); + return; + } + snprintf(buf, sizeof(buf), "MSG: %u", state.msg_count); draw_centered(centered_row(0, 3), buf); @@ -270,7 +419,12 @@ static void render_messages(void) static void render_recent(void) { if (state.recent_count == 0) { - draw_centered(centered_row(0, 1), "(no contacts heard)"); + if (mc_display_has_color()) { + draw_centered_color(centered_row(0, 1), "no contacts heard", + UI_COLOR_DISABLED); + } else { + draw_centered(centered_row(0, 1), "(no contacts heard)"); + } return; } @@ -299,7 +453,18 @@ static void render_recent(void) snprintf(buf, sizeof(buf), "%-13s %s", state.recent[i].name, time_str); - mc_display_text(0, y, buf, false); + if (mc_display_has_color()) { + uint16_t age_color = (age_s < 300) ? UI_COLOR_OK : + (age_s < 3600) ? UI_COLOR_WARN + : UI_COLOR_DISABLED; + + mc_display_color_text(0, y, state.recent[i].name, + UI_COLOR_VALUE); + mc_display_color_text(DISP_W - color_text_width(time_str), + y, time_str, age_color); + } else { + mc_display_text(0, y, buf, false); + } y += LINE_H; } } @@ -316,15 +481,30 @@ static void render_radio(void) (state.lora_in_rx ? "RX" : (state.lora_radio_ready ? "RDY" : "WAIT")); const char *rx_mode = state.lora_rx_duty_cycle ? "DC" : "CONT"; - uint16_t apc_color = state.lora_apc_enabled ? MC_COLOR_GREEN : MC_COLOR_GRAY; uint16_t warn_color = (state.lora_apc_enabled && state.lora_apc_reduction > 0) - ? MC_COLOR_ORANGE : MC_COLOR_GREEN; + ? UI_COLOR_WARN : UI_COLOR_OK; if (mc_display_has_color()) { - mc_display_color_fill_rect(0, y - 1, DISP_W, FONT_H + 2, MC_COLOR_BLUE); - snprintf(buf, sizeof(buf), "companion %s/%s", packet_state, rx_mode); - mc_display_color_text(2, y, "RAD", MC_COLOR_YELLOW); - mc_display_color_text(26, y, buf, MC_COLOR_WHITE); + uint16_t state_color = state.lora_tx_active ? UI_COLOR_WARN : + state.lora_in_rx ? UI_COLOR_ACTIVE : + state.lora_radio_ready ? UI_COLOR_OK + : UI_COLOR_DISABLED; + uint16_t tx_color = (state.lora_apc_enabled && + state.lora_apc_reduction > 0) + ? UI_COLOR_WARN : UI_COLOR_OK; + int max_tx = state.lora_tx_power > 0 ? state.lora_tx_power : 22; + int eff_tx = state.lora_effective_tx_power > 0 + ? state.lora_effective_tx_power : state.lora_tx_power; + int badge_x; + + mc_display_color_fill_rect(0, y - 1, DISP_W, COLOR_FONT_H + 2, + UI_COLOR_HEADER_BG); + mc_display_color_text(2, y, "RADIO", UI_COLOR_TITLE); + badge_x = DISP_W - color_text_width(rx_mode) - 6; + draw_badge(badge_x, y, rx_mode, + state.lora_rx_duty_cycle ? UI_COLOR_WARN : UI_COLOR_ACTIVE); + badge_x -= color_text_width(packet_state) + 8; + draw_badge(badge_x, y, packet_state, state_color); y += LINE_H; if (bw_frac) { @@ -334,22 +514,23 @@ static void render_radio(void) snprintf(buf, sizeof(buf), "%u.%03u BW%u", freq_mhz, freq_frac, bw_int); } - draw_color_segments(y, "RF ", buf, MC_COLOR_WHITE); + draw_color_segments(y, "RF ", buf, UI_COLOR_VALUE); y += LINE_H; snprintf(buf, sizeof(buf), "SF%u CR%u SW%02X P%u", state.lora_sf, state.lora_cr, state.lora_sync_word, state.lora_preamble_len); - draw_color_segments(y, "LoRa ", buf, MC_COLOR_WHITE); + draw_color_segments(y, "LoRa ", buf, UI_COLOR_VALUE); y += LINE_H; if (state.lora_apc_enabled) { - snprintf(buf, sizeof(buf), "%d/%ddBm APC on", + snprintf(buf, sizeof(buf), "%d/%ddBm", state.lora_effective_tx_power, state.lora_tx_power); } else { - snprintf(buf, sizeof(buf), "%ddBm APC off", state.lora_tx_power); + snprintf(buf, sizeof(buf), "%ddBm", state.lora_tx_power); } - draw_color_segments(y, "TX ", buf, apc_color); + draw_color_segments(y, "TX ", buf, tx_color); + draw_metric_bar(DISP_W - 50, y + 2, 48, 5, eff_tx, max_tx, tx_color); y += LINE_H; snprintf(buf, sizeof(buf), "red %d M%d.%d T%u", @@ -358,15 +539,23 @@ static void render_radio(void) abs(state.lora_apc_margin_x10 % 10), state.lora_apc_target_margin); draw_color_segments(y, "APC ", buf, warn_color); + draw_badge(DISP_W - color_text_width("APC") - 4, y, "APC", + !state.lora_apc_enabled ? UI_COLOR_DISABLED : + state.lora_apc_reduction > 0 ? UI_COLOR_WARN : UI_COLOR_OK); y += LINE_H; - snprintf(buf, sizeof(buf), "NF%d R%lu T%lu E%lu", - state.lora_noise_floor, - (unsigned long)state.lora_packets_rx, - (unsigned long)state.lora_packets_tx, - (unsigned long)state.lora_packets_err); + char rx_count[6]; + char tx_count[6]; + char err_count[6]; + + fmt_compact_count(rx_count, sizeof(rx_count), state.lora_packets_rx); + fmt_compact_count(tx_count, sizeof(tx_count), state.lora_packets_tx); + fmt_compact_count(err_count, sizeof(err_count), state.lora_packets_err); + snprintf(buf, sizeof(buf), "NF%d R%s T%s E%s", + state.lora_noise_floor, rx_count, tx_count, err_count); draw_color_segments(y, "PKT ", buf, - state.lora_packets_err ? MC_COLOR_RED : MC_COLOR_WHITE); + state.lora_packets_err ? UI_COLOR_ERROR + : UI_COLOR_VALUE); return; } @@ -413,6 +602,24 @@ static void render_radio(void) static void render_bluetooth(void) { + if (mc_display_has_color()) { + int y = CONTENT_Y; + uint16_t color = !state.ble_enabled ? UI_COLOR_DISABLED : + state.ble_connected ? UI_COLOR_OK : UI_COLOR_WARN; + + draw_badge(0, y, "BLE", color); + mc_display_color_text(32, y, + !state.ble_enabled ? "disabled" : + state.ble_connected ? "connected" : "advertising", + color); + y += LINE_H + 4; + draw_centered_color(y, + state.ble_enabled ? "Press to disable" + : "Press to enable", + UI_COLOR_VALUE); + return; + } + if (!state.ble_enabled) { draw_centered(centered_row(0, 2), "BLE: OFF"); draw_centered(centered_row(1, 2), "Press to Enable"); @@ -435,6 +642,25 @@ static void render_advert(void) bool just_sent = (state.advert_sent_time > 0 && (now - state.advert_sent_time) < 2000); + if (mc_display_has_color()) { + int y = CONTENT_Y; + + draw_badge(0, y, "ADV", just_sent ? UI_COLOR_OK : UI_COLOR_ACTIVE); + mc_display_color_text(32, y, "Zero-hop advert", UI_COLOR_VALUE); + y += LINE_H + 2; + + if (just_sent) { + draw_centered_color(y, + state.advert_was_flood ? "Flood sent" : "Sent", + UI_COLOR_OK); + } else { + draw_centered_color(y, "Press to send", UI_COLOR_VALUE); + } + y += LINE_H; + draw_centered_color(y, "2x press: flood", UI_COLOR_LABEL); + return; + } + draw_centered(centered_row(0, 3), "Send Zero-Hop Advert"); if (just_sent) { @@ -466,21 +692,39 @@ static void render_gps(void) { char buf[32]; int y = CONTENT_Y; + bool color = mc_display_has_color(); /* No GPS hardware on this board */ if (!state.gps_available) { - mc_display_text(0, y, "GPS: not detected", false); + if (color) { + draw_badge(0, y, "GPS", UI_COLOR_DISABLED); + mc_display_color_text(32, y, "not detected", UI_COLOR_DISABLED); + } else { + mc_display_text(0, y, "GPS: not detected", false); + } return; } /* GPS enabled/disabled state */ snprintf(buf, sizeof(buf), "GPS: %s", state.gps_enabled ? "on" : "off"); - mc_display_text(0, y, buf, false); + if (color) { + draw_badge(0, y, "GPS", state.gps_enabled ? UI_COLOR_OK + : UI_COLOR_DISABLED); + mc_display_color_text(32, y, state.gps_enabled ? "on" : "off", + state.gps_enabled ? UI_COLOR_OK + : UI_COLOR_DISABLED); + } else { + mc_display_text(0, y, buf, false); + } y += LINE_H; if (!state.gps_enabled) { - draw_centered(y + 8, "Press to Enable"); + if (color) { + draw_centered_color(y + 8, "Press to enable", UI_COLOR_VALUE); + } else { + draw_centered(y + 8, "Press to Enable"); + } return; } @@ -489,7 +733,11 @@ static void render_gps(void) /* ACQUIRING — actively searching for satellites */ snprintf(buf, sizeof(buf), "Searching... sat:%u", state.gps_satellites); - mc_display_text(0, y, buf, false); + if (color) { + draw_color_segments(y, "SAT ", buf, UI_COLOR_WARN); + } else { + mc_display_text(0, y, buf, false); + } y += LINE_H; /* Show last fix age if we have one */ @@ -498,9 +746,17 @@ static void render_gps(void) fmt_duration(tbuf, sizeof(tbuf), state.gps_last_fix_age_s); snprintf(buf, sizeof(buf), "Last fix: %s ago", tbuf); - mc_display_text(0, y, buf, false); + if (color) { + draw_color_segments(y, "FIX ", buf, UI_COLOR_VALUE); + } else { + mc_display_text(0, y, buf, false); + } } else { - mc_display_text(0, y, "No fix yet", false); + if (color) { + mc_display_color_text(0, y, "No fix yet", UI_COLOR_WARN); + } else { + mc_display_text(0, y, "No fix yet", false); + } } } else if (state.gps_state == 1) { /* STANDBY — sleeping between fix cycles */ @@ -509,10 +765,18 @@ static void render_gps(void) fmt_duration(tbuf, sizeof(tbuf), state.gps_last_fix_age_s); snprintf(buf, sizeof(buf), "Last fix: %s ago", tbuf); - mc_display_text(0, y, buf, false); + if (color) { + draw_color_segments(y, "FIX ", buf, UI_COLOR_VALUE); + } else { + mc_display_text(0, y, buf, false); + } y += LINE_H; } else { - mc_display_text(0, y, "No fix yet", false); + if (color) { + mc_display_color_text(0, y, "No fix yet", UI_COLOR_WARN); + } else { + mc_display_text(0, y, "No fix yet", false); + } y += LINE_H; } @@ -521,11 +785,19 @@ static void render_gps(void) fmt_duration(tbuf, sizeof(tbuf), state.gps_next_search_s); snprintf(buf, sizeof(buf), "Next search: %s", tbuf); - mc_display_text(0, y, buf, false); + if (color) { + draw_color_segments(y, "NEXT ", buf, UI_COLOR_LABEL); + } else { + mc_display_text(0, y, buf, false); + } } } else { /* OFF — shouldn't reach here if gps_enabled is true */ - mc_display_text(0, y, "GPS off", false); + if (color) { + mc_display_color_text(0, y, "GPS off", UI_COLOR_DISABLED); + } else { + mc_display_text(0, y, "GPS off", false); + } } } @@ -548,6 +820,21 @@ static void render_leds(void) char buf[24]; int y = CONTENT_Y; + if (mc_display_has_color()) { + draw_badge(0, y, "LED", state.leds_disabled ? UI_COLOR_DISABLED + : UI_COLOR_OK); + mc_display_color_text(32, y, + state.leds_disabled ? "off" : "on", + state.leds_disabled ? UI_COLOR_DISABLED + : UI_COLOR_OK); + y += LINE_H + 4; + draw_centered_color(y, + state.leds_disabled ? "Press to enable" + : "Press to disable", + UI_COLOR_VALUE); + return; + } + snprintf(buf, sizeof(buf), "LEDs: %s", state.leds_disabled ? "off" : "on"); mc_display_text(0, y, buf, false); @@ -561,6 +848,7 @@ static void render_sensors(void) { char buf[24]; int y = CONTENT_Y; + bool color = mc_display_has_color(); /* Lazy read: only fetch sensors when the user is actually looking at * this page. Render is event-driven (schedule_render only on real UI @@ -572,14 +860,22 @@ static void render_sensors(void) int16_t t10 = (int16_t)(edata.temperature_c * 10); snprintf(buf, sizeof(buf), "Temp: %d.%d C", t10 / 10, abs(t10 % 10)); - mc_display_text(0, y, buf, false); + if (color) { + draw_color_segments(y, "TMP ", buf, UI_COLOR_VALUE); + } else { + mc_display_text(0, y, buf, false); + } y += LINE_H; } if (have_env && edata.has_pressure) { snprintf(buf, sizeof(buf), "Press: %u hPa", (unsigned)edata.pressure_hpa); - mc_display_text(0, y, buf, false); + if (color) { + draw_color_segments(y, "BAR ", buf, UI_COLOR_VALUE); + } else { + mc_display_text(0, y, buf, false); + } y += LINE_H; } @@ -587,7 +883,11 @@ static void render_sensors(void) uint16_t h10 = (uint16_t)(edata.humidity_pct * 10); snprintf(buf, sizeof(buf), "Humid: %u.%u%%", h10 / 10, h10 % 10); - mc_display_text(0, y, buf, false); + if (color) { + draw_color_segments(y, "HUM ", buf, UI_COLOR_VALUE); + } else { + mc_display_text(0, y, buf, false); + } y += LINE_H; } @@ -596,7 +896,18 @@ static void render_sensors(void) state.battery_pct > 0 ? state.battery_pct : calc_battery_pct(state.battery_mv), state.battery_mv); - mc_display_text(0, y, buf, false); + if (color) { + uint8_t pct = state.battery_pct > 0 ? state.battery_pct + : calc_battery_pct(state.battery_mv); + uint16_t batt_color = (pct <= 15) ? UI_COLOR_ERROR : + (pct <= 30) ? UI_COLOR_WARN : UI_COLOR_OK; + + snprintf(buf, sizeof(buf), "%u%% %umV", pct, state.battery_mv); + draw_color_segments(y, "BAT ", buf, batt_color); + draw_metric_bar(DISP_W - 44, y + 2, 42, 5, pct, 100, batt_color); + } else { + mc_display_text(0, y, buf, false); + } } static void render_offgrid(void) @@ -607,10 +918,35 @@ static void render_offgrid(void) state.offgrid_confirm_time = 0; } - draw_centered(centered_row(0, 3), "Offgrid Mode"); - char buf[24]; + if (mc_display_has_color()) { + int y = CONTENT_Y; + + draw_badge(0, y, "GRID", state.offgrid_enabled ? UI_COLOR_ACTIVE + : UI_COLOR_DISABLED); + mc_display_color_text(38, y, "client repeat", + state.offgrid_enabled ? UI_COLOR_ACTIVE + : UI_COLOR_LABEL); + y += LINE_H + 4; + snprintf(buf, sizeof(buf), "Status %s", + state.offgrid_enabled ? "on" : "off"); + draw_centered_color(y, buf, + state.offgrid_enabled ? UI_COLOR_ACTIVE + : UI_COLOR_DISABLED); + y += LINE_H; + draw_centered_color(y, + state.offgrid_confirm_time != 0 ? + "Press to confirm" : + (state.offgrid_enabled ? "Press to disable" + : "Press to enable"), + state.offgrid_confirm_time != 0 ? UI_COLOR_WARN + : UI_COLOR_VALUE); + return; + } + + draw_centered(centered_row(0, 3), "Offgrid Mode"); + snprintf(buf, sizeof(buf), "Status: %s", state.offgrid_enabled ? "ON" : "OFF"); draw_centered(centered_row(1, 3), buf); @@ -632,6 +968,21 @@ static void render_dfu(void) state.dfu_confirm_time = 0; } + if (mc_display_has_color()) { + int y = CONTENT_Y; + + draw_badge(0, y, "DFU", state.dfu_confirm_time != 0 + ? UI_COLOR_WARN : UI_COLOR_ACTIVE); + mc_display_color_text(32, y, "BLE update", UI_COLOR_VALUE); + y += LINE_H + 4; + draw_centered_color(y, + state.dfu_confirm_time != 0 ? + "Press to confirm" : "Press to run", + state.dfu_confirm_time != 0 ? UI_COLOR_WARN + : UI_COLOR_VALUE); + return; + } + draw_centered(centered_row(0, 2), "BLE DFU Update"); if (state.dfu_confirm_time != 0) { draw_centered(centered_row(1, 2), "Press to confirm"); @@ -648,6 +999,21 @@ static void render_shutdown(void) state.shutdown_confirm_time = 0; } + if (mc_display_has_color()) { + int y = CONTENT_Y; + + draw_badge(0, y, "PWR", state.shutdown_confirm_time != 0 + ? UI_COLOR_WARN : UI_COLOR_ERROR); + mc_display_color_text(32, y, "power off", UI_COLOR_VALUE); + y += LINE_H + 4; + draw_centered_color(y, + state.shutdown_confirm_time != 0 ? + "Press to confirm" : "Press to run", + state.shutdown_confirm_time != 0 ? UI_COLOR_WARN + : UI_COLOR_VALUE); + return; + } + draw_centered(centered_row(0, 2), "Power Off"); if (state.shutdown_confirm_time != 0) { draw_centered(centered_row(1, 2), "Press to confirm"); @@ -660,9 +1026,15 @@ static void render_status(void) { char buf[28]; int y = CONTENT_Y; + bool color = mc_display_has_color(); /* Role label */ - draw_centered(y, "REPEATER"); + if (color) { + draw_badge(0, y, "MODE", UI_COLOR_ACTIVE); + mc_display_color_text(38, y, "repeater", UI_COLOR_VALUE); + } else { + draw_centered(y, "REPEATER"); + } y += LINE_H; /* Uptime */ @@ -672,7 +1044,11 @@ static void render_status(void) uint32_t mins = (up_s % 3600) / 60; snprintf(buf, sizeof(buf), "Up: %ud %uh %um", days, hours, mins); - mc_display_text(0, y, buf, false); + if (color) { + draw_color_segments(y, "UP ", buf, UI_COLOR_VALUE); + } else { + mc_display_text(0, y, buf, false); + } y += LINE_H; /* Clock — only if RTC has been synced (after Jan 1 2025) */ @@ -683,9 +1059,17 @@ static void render_status(void) uint8_t ss = day_sec % 60; snprintf(buf, sizeof(buf), "Time: %02u:%02u:%02u UTC", hh, mm, ss); - mc_display_text(0, y, buf, false); + if (color) { + draw_color_segments(y, "CLK ", buf, UI_COLOR_OK); + } else { + mc_display_text(0, y, buf, false); + } } else { - mc_display_text(0, y, "Time: not synced", false); + if (color) { + draw_color_segments(y, "CLK ", "not synced", UI_COLOR_WARN); + } else { + mc_display_text(0, y, "Time: not synced", false); + } } y += LINE_H; @@ -697,7 +1081,17 @@ static void render_status(void) pct = calc_battery_pct(state.battery_mv); } snprintf(buf, sizeof(buf), "Batt: %u%% (%umV)", pct, state.battery_mv); - mc_display_text(0, y, buf, false); + if (color) { + uint16_t batt_color = (pct <= 15) ? UI_COLOR_ERROR : + (pct <= 30) ? UI_COLOR_WARN : UI_COLOR_OK; + + snprintf(buf, sizeof(buf), "%u%% %umV", pct, state.battery_mv); + draw_color_segments(y, "BAT ", buf, batt_color); + draw_metric_bar(DISP_W - 44, y + 2, 42, 5, pct, 100, + batt_color); + } else { + mc_display_text(0, y, buf, false); + } } }