ESP32/main/ui_screen.c
2026-06-23 20:38:23 +08:00

796 lines
27 KiB
C

#include "ui_screen.h"
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "lvgl.h"
#define CPU_HISTORY_POINTS 64
#define CPU_CHART_W 184
#define CPU_CHART_H 58
#define HALO_LABEL_COUNT 8
#define DISK_ROW_COUNT 4
#define CODEX_DAILY_BAR_W 8
#define CODEX_DAILY_BAR_GAP 5
#define CODEX_DAILY_BAR_BASE_Y 292
#define CODEX_DAILY_BAR_MAX_H 40
typedef enum {
CONN_USB = 0,
CONN_WIFI,
CONN_BLE,
} connection_slot_t;
static lv_obj_t *s_status_label;
static lv_obj_t *s_battery_label;
static lv_obj_t *s_conn_label;
static lv_obj_t *s_env_label;
static lv_obj_t *s_cpu_value_label;
static lv_obj_t *s_cpu_freq_label;
static lv_obj_t *s_cpu_value_halo[HALO_LABEL_COUNT];
static lv_obj_t *s_cpu_freq_halo[HALO_LABEL_COUNT];
static lv_obj_t *s_cpu_line;
static lv_point_precise_t s_cpu_points[CPU_HISTORY_POINTS];
static int s_cpu_history[CPU_HISTORY_POINTS];
static lv_obj_t *s_memory_value_label;
static lv_obj_t *s_memory_value_halo[HALO_LABEL_COUNT];
static lv_obj_t *s_memory_detail_label;
static lv_obj_t *s_memory_bar;
static lv_obj_t *s_disk_name_labels[DISK_ROW_COUNT];
static lv_obj_t *s_disk_rate_labels[DISK_ROW_COUNT];
static lv_obj_t *s_disk_bars[DISK_ROW_COUNT];
static lv_obj_t *s_net_line_label;
static lv_obj_t *s_codex_5h_label;
static lv_obj_t *s_codex_week_label;
static lv_obj_t *s_codex_5h_value_label;
static lv_obj_t *s_codex_week_value_label;
static lv_obj_t *s_codex_5h_reset_label;
static lv_obj_t *s_codex_week_reset_label;
static lv_obj_t *s_codex_token_label;
static lv_obj_t *s_codex_5h_bar;
static lv_obj_t *s_codex_week_bar;
static lv_obj_t *s_codex_daily_bars[UI_CODEX_DAILY_BAR_COUNT];
static lv_obj_t *s_zhipu_status_label;
static lv_obj_t *s_zhipu_5h_label;
static lv_obj_t *s_zhipu_week_label;
static lv_obj_t *s_zhipu_5h_value_label;
static lv_obj_t *s_zhipu_week_value_label;
static lv_obj_t *s_zhipu_5h_reset_label;
static lv_obj_t *s_zhipu_week_reset_label;
static lv_obj_t *s_zhipu_5h_bar;
static lv_obj_t *s_zhipu_week_bar;
static int s_age_seconds;
static connection_slot_t s_active_connection = CONN_USB;
typedef struct {
int x;
int y;
} halo_offset_t;
static const halo_offset_t s_halo_offsets[HALO_LABEL_COUNT] = {
{ -1, 0 },
{ 1, 0 },
{ 0, -1 },
{ 0, 1 },
{ -1, -1 },
{ 1, -1 },
{ -1, 1 },
{ 1, 1 },
};
static lv_color_t ink(void)
{
return lv_color_black();
}
static lv_color_t paper(void)
{
return lv_color_white();
}
static int clamp_percent(int value)
{
if (value < 0) {
return 0;
}
if (value > 100) {
return 100;
}
return value;
}
static lv_obj_t *make_colored_label(lv_obj_t *parent, const char *text, const lv_font_t *font, lv_color_t color)
{
lv_obj_t *label = lv_label_create(parent);
lv_label_set_text(label, text);
lv_label_set_long_mode(label, LV_LABEL_LONG_DOT);
lv_obj_set_style_text_font(label, font, 0);
lv_obj_set_style_text_color(label, color, 0);
lv_obj_set_style_pad_all(label, 0, 0);
return label;
}
static lv_obj_t *make_label(lv_obj_t *parent, const char *text, const lv_font_t *font)
{
return make_colored_label(parent, text, font, ink());
}
static void make_label_halo(lv_obj_t *parent,
lv_obj_t *halos[HALO_LABEL_COUNT],
const char *text,
const lv_font_t *font,
int width,
lv_align_t align,
int x,
int y,
int spread,
lv_text_align_t text_align)
{
for (int i = 0; i < HALO_LABEL_COUNT; i++) {
halos[i] = make_colored_label(parent, text, font, paper());
lv_obj_set_width(halos[i], width);
lv_obj_set_style_text_align(halos[i], text_align, 0);
lv_obj_align(halos[i], align, x + s_halo_offsets[i].x * spread, y + s_halo_offsets[i].y * spread);
}
}
static void set_label_with_halo(lv_obj_t *label, lv_obj_t *halos[HALO_LABEL_COUNT], const char *text)
{
for (int i = 0; i < HALO_LABEL_COUNT; i++) {
if (halos[i] != NULL) {
lv_label_set_text(halos[i], text);
}
}
lv_label_set_text(label, text);
}
static void set_bar_percent(lv_obj_t *bar, int percent)
{
if (bar == NULL) {
return;
}
lv_bar_set_value(bar, percent >= 0 ? clamp_percent(percent) : 0, LV_ANIM_OFF);
}
static lv_obj_t *make_rule(lv_obj_t *parent, int x, int y, int w, int h)
{
lv_obj_t *rule = lv_obj_create(parent);
lv_obj_set_size(rule, w, h);
lv_obj_align(rule, LV_ALIGN_TOP_LEFT, x, y);
lv_obj_set_style_bg_color(rule, ink(), 0);
lv_obj_set_style_bg_opa(rule, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(rule, 0, 0);
lv_obj_set_style_radius(rule, 0, 0);
lv_obj_set_style_pad_all(rule, 0, 0);
lv_obj_clear_flag(rule, LV_OBJ_FLAG_SCROLLABLE);
return rule;
}
static lv_obj_t *make_bar(lv_obj_t *parent, int w, int h)
{
lv_obj_t *bar = lv_bar_create(parent);
lv_obj_set_size(bar, w, h);
lv_bar_set_range(bar, 0, 100);
lv_bar_set_value(bar, 0, LV_ANIM_OFF);
lv_obj_set_style_bg_color(bar, paper(), 0);
lv_obj_set_style_border_color(bar, ink(), 0);
lv_obj_set_style_border_width(bar, 1, 0);
lv_obj_set_style_radius(bar, 1, 0);
lv_obj_set_style_bg_color(bar, ink(), LV_PART_INDICATOR);
lv_obj_set_style_radius(bar, 1, LV_PART_INDICATOR);
return bar;
}
static void format_capacity(int mb, char *out, size_t out_size)
{
if (mb <= 0) {
snprintf(out, out_size, "--");
return;
}
if (mb >= 1024) {
int gb10 = (mb * 10 + 512) / 1024;
snprintf(out, out_size, "%d.%dGB", gb10 / 10, gb10 % 10);
} else {
snprintf(out, out_size, "%dMB", mb);
}
}
static void format_rate_short(int kbps, char *out, size_t out_size)
{
if (kbps < 0) {
snprintf(out, out_size, "--");
return;
}
if (kbps >= 1024 * 1024) {
int gb10 = (kbps * 10 + (1024 * 512)) / (1024 * 1024);
if (gb10 >= 100) {
snprintf(out, out_size, "%dG", gb10 / 10);
} else {
snprintf(out, out_size, "%d.%dG", gb10 / 10, gb10 % 10);
}
} else if (kbps >= 1000) {
int mb10 = (kbps * 10 + 512) / 1024;
if (mb10 >= 100) {
snprintf(out, out_size, "%dM", mb10 / 10);
} else {
snprintf(out, out_size, "%d.%dM", mb10 / 10, mb10 % 10);
}
} else {
snprintf(out, out_size, "%dK", kbps);
}
}
static void format_reset_mins(int mins, char *out, size_t out_size)
{
if (mins < 0) {
snprintf(out, out_size, "--");
} else if (mins >= 1440) {
int days = mins / 1440;
int hours = (mins % 1440) / 60;
snprintf(out, out_size, "%dd%dh", days, hours);
} else if (mins >= 60) {
snprintf(out, out_size, "%dh%02d", mins / 60, mins % 60);
} else {
snprintf(out, out_size, "%dm", mins);
}
}
static void format_percent_compact(int percent, char *out, size_t out_size)
{
if (percent < 0) {
snprintf(out, out_size, "--");
} else {
snprintf(out, out_size, "%d%%", clamp_percent(percent));
}
}
static void format_tokens_m10(int tokens_m10, char *out, size_t out_size)
{
if (tokens_m10 < 0) {
snprintf(out, out_size, "--");
} else if (tokens_m10 >= 10000) {
snprintf(out, out_size, "%d.%dB", tokens_m10 / 10000, (tokens_m10 % 10000) / 1000);
} else if (tokens_m10 >= 1000) {
snprintf(out, out_size, "%dM", (tokens_m10 + 5) / 10);
} else {
snprintf(out, out_size, "%d.%dM", tokens_m10 / 10, tokens_m10 % 10);
}
}
static const char *zhipu_status_text(int status)
{
switch (status) {
case 1:
return "KEY";
case 2:
return "ERR";
default:
return "WAIT";
}
}
static const char *connection_symbol(connection_slot_t active)
{
switch (active) {
case CONN_BLE:
return LV_SYMBOL_BLUETOOTH;
case CONN_WIFI:
return LV_SYMBOL_WIFI;
case CONN_USB:
default:
return LV_SYMBOL_USB;
}
}
static void set_connection(connection_slot_t active)
{
s_active_connection = active;
if (s_conn_label != NULL) {
lv_label_set_text(s_conn_label, connection_symbol(active));
}
}
static connection_slot_t connection_from_source(const char *source)
{
if (source == NULL) {
return CONN_USB;
}
if (strstr(source, "BLE") != NULL) {
return CONN_BLE;
}
if (strstr(source, "WIFI") != NULL || strstr(source, "AP") != NULL) {
return CONN_WIFI;
}
return CONN_USB;
}
static void set_history_curve(int *history, lv_point_precise_t *points, int count,
int chart_w, int chart_h, int percent, lv_obj_t *line)
{
for (int i = 0; i < count - 1; i++) {
history[i] = history[i + 1];
}
history[count - 1] = clamp_percent(percent);
for (int i = 0; i < count; i++) {
points[i].x = (i * chart_w) / (count - 1);
points[i].y = chart_h - ((history[i] * chart_h) / 100);
}
lv_line_set_points_mutable(line, points, count);
lv_obj_invalidate(line);
}
static void set_cpu_curve(int cpu_percent)
{
set_history_curve(s_cpu_history, s_cpu_points, CPU_HISTORY_POINTS,
CPU_CHART_W, CPU_CHART_H, cpu_percent, s_cpu_line);
}
static int parse_int_field(char *field, int fallback)
{
if (field == NULL || field[0] == '\0') {
return fallback;
}
char *end = NULL;
long value = strtol(field, &end, 10);
if (end == field) {
return fallback;
}
return (int)value;
}
static int parse_disk_token(const char *token, char *name, size_t name_size,
int *percent, int *read_kbps, int *write_kbps)
{
char buffer[40];
snprintf(buffer, sizeof(buffer), "%s", token);
char *cursor = buffer;
char *disk_name = strsep(&cursor, ":");
char *usage = strsep(&cursor, ":");
char *read_rate = strsep(&cursor, ":");
char *write_rate = strsep(&cursor, ":");
if (disk_name == NULL || disk_name[0] == '\0' || usage == NULL) {
return 0;
}
size_t len = strlen(disk_name);
if (len >= name_size) {
len = name_size - 1;
}
memcpy(name, disk_name, len);
name[len] = '\0';
*percent = clamp_percent(parse_int_field(usage, 0));
*read_kbps = parse_int_field(read_rate, -1);
*write_kbps = parse_int_field(write_rate, -1);
return 1;
}
static void update_disk_rows(const char *disk_text)
{
char buffer[UI_MAX_DISK_TEXT];
char name[8];
int row = 0;
snprintf(buffer, sizeof(buffer), "%s", disk_text && disk_text[0] ? disk_text : "");
char *cursor = buffer;
char *token = strtok(cursor, " ");
while (token != NULL && row < DISK_ROW_COUNT) {
int percent = 0;
int read_kbps = -1;
int write_kbps = -1;
if (parse_disk_token(token, name, sizeof(name), &percent, &read_kbps, &write_kbps)) {
char text[28];
char read[10];
char write[10];
snprintf(text, sizeof(text), "%s", name);
lv_label_set_text(s_disk_name_labels[row], text);
format_rate_short(read_kbps, read, sizeof(read));
format_rate_short(write_kbps, write, sizeof(write));
snprintf(text, sizeof(text), "%s/%s", read, write);
lv_label_set_text(s_disk_rate_labels[row], text);
lv_bar_set_value(s_disk_bars[row], percent, LV_ANIM_OFF);
lv_obj_clear_flag(s_disk_name_labels[row], LV_OBJ_FLAG_HIDDEN);
lv_obj_clear_flag(s_disk_rate_labels[row], LV_OBJ_FLAG_HIDDEN);
lv_obj_clear_flag(s_disk_bars[row], LV_OBJ_FLAG_HIDDEN);
row++;
}
token = strtok(NULL, " ");
}
for (int i = row; i < DISK_ROW_COUNT; i++) {
lv_obj_add_flag(s_disk_name_labels[i], LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(s_disk_rate_labels[i], LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(s_disk_bars[i], LV_OBJ_FLAG_HIDDEN);
}
}
static void update_codex_panel(const ui_metrics_t *metrics)
{
char text[40];
char value[12];
char reset_5h[12];
char reset_week[12];
format_percent_compact(metrics->codex_5h_percent, value, sizeof(value));
lv_label_set_text(s_codex_5h_value_label, value);
set_bar_percent(s_codex_5h_bar, metrics->codex_5h_percent);
format_percent_compact(metrics->codex_week_percent, value, sizeof(value));
lv_label_set_text(s_codex_week_value_label, value);
set_bar_percent(s_codex_week_bar, metrics->codex_week_percent);
format_reset_mins(metrics->codex_5h_reset_mins, reset_5h, sizeof(reset_5h));
format_reset_mins(metrics->codex_week_reset_mins, reset_week, sizeof(reset_week));
lv_label_set_text(s_codex_5h_reset_label, reset_5h);
lv_label_set_text(s_codex_week_reset_label, reset_week);
format_tokens_m10(metrics->codex_today_tokens_m10, value, sizeof(value));
snprintf(text, sizeof(text), "TOK %s", value);
lv_label_set_text(s_codex_token_label, text);
for (int i = 0; i < UI_CODEX_DAILY_BAR_COUNT; i++) {
lv_obj_t *bar = s_codex_daily_bars[i];
if (bar == NULL) {
continue;
}
if (i >= metrics->codex_daily_count || metrics->codex_daily_percent[i] < 0) {
lv_obj_add_flag(bar, LV_OBJ_FLAG_HIDDEN);
continue;
}
int percent = clamp_percent(metrics->codex_daily_percent[i]);
int height = percent <= 0 ? 1 : 1 + ((percent * (CODEX_DAILY_BAR_MAX_H - 1)) + 50) / 100;
int x = 10 + i * (CODEX_DAILY_BAR_W + CODEX_DAILY_BAR_GAP);
int y = CODEX_DAILY_BAR_BASE_Y - height;
lv_obj_set_size(bar, CODEX_DAILY_BAR_W, height);
lv_obj_align(bar, LV_ALIGN_TOP_LEFT, x, y);
lv_obj_clear_flag(bar, LV_OBJ_FLAG_HIDDEN);
}
}
static void update_zhipu_panel(const ui_metrics_t *metrics)
{
char value[12];
char reset[12];
char text[24];
snprintf(text, sizeof(text), "STAT %s", zhipu_status_text(metrics->zhipu_status));
lv_label_set_text(s_zhipu_status_label, text);
format_percent_compact(metrics->zhipu_5h_percent, value, sizeof(value));
lv_label_set_text(s_zhipu_5h_value_label, value);
set_bar_percent(s_zhipu_5h_bar, metrics->zhipu_5h_percent);
format_reset_mins(metrics->zhipu_5h_reset_mins, reset, sizeof(reset));
lv_label_set_text(s_zhipu_5h_reset_label, reset);
format_percent_compact(metrics->zhipu_week_percent, value, sizeof(value));
lv_label_set_text(s_zhipu_week_value_label, value);
set_bar_percent(s_zhipu_week_bar, metrics->zhipu_week_percent);
format_reset_mins(metrics->zhipu_week_reset_mins, reset, sizeof(reset));
lv_label_set_text(s_zhipu_week_reset_label, reset);
}
static void update_net_line(const ui_metrics_t *metrics)
{
char rx[12];
char tx[12];
char text[44];
format_rate_short(metrics->rx_kbps, rx, sizeof(rx));
format_rate_short(metrics->tx_kbps, tx, sizeof(tx));
snprintf(text, sizeof(text), "D %s U %s", rx, tx);
size_t text_len = strlen(text);
const lv_font_t *font = &lv_font_montserrat_20;
if (text_len >= 18) {
font = &lv_font_montserrat_14;
}
lv_obj_set_style_text_font(s_net_line_label, font, 0);
lv_label_set_long_mode(s_net_line_label, LV_LABEL_LONG_CLIP);
lv_label_set_text(s_net_line_label, text);
}
void ui_screen_create(void)
{
lv_obj_t *screen = lv_screen_active();
lv_obj_set_style_bg_color(screen, paper(), 0);
lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, 0);
lv_obj_clear_flag(screen, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_clean(screen);
make_rule(screen, 200, 6, 1, 288);
make_rule(screen, 6, 174, 388, 1);
s_status_label = make_label(screen, "WAITING", &lv_font_montserrat_14);
lv_obj_add_flag(s_status_label, LV_OBJ_FLAG_HIDDEN);
s_cpu_line = lv_line_create(screen);
lv_obj_set_size(s_cpu_line, CPU_CHART_W + 2, CPU_CHART_H + 2);
lv_obj_align(s_cpu_line, LV_ALIGN_TOP_LEFT, 8, 112);
lv_obj_set_style_line_color(s_cpu_line, ink(), 0);
lv_obj_set_style_line_width(s_cpu_line, 3, 0);
for (int i = 0; i < CPU_HISTORY_POINTS; i++) {
s_cpu_history[i] = 0;
}
set_cpu_curve(0);
s_battery_label = make_label(screen, LV_SYMBOL_BATTERY_EMPTY " --%", &lv_font_montserrat_14);
lv_obj_set_width(s_battery_label, 54);
lv_obj_align(s_battery_label, LV_ALIGN_TOP_LEFT, 8, 8);
s_conn_label = make_label(screen, LV_SYMBOL_USB, &lv_font_montserrat_14);
lv_obj_set_width(s_conn_label, 22);
lv_obj_align(s_conn_label, LV_ALIGN_TOP_LEFT, 58, 8);
s_env_label = make_label(screen, "--C --%", &lv_font_montserrat_14);
lv_obj_set_width(s_env_label, 110);
lv_obj_align(s_env_label, LV_ALIGN_TOP_LEFT, 82, 8);
lv_obj_t *cpu_title = make_label(screen, "CPU", &lv_font_montserrat_14);
lv_obj_align(cpu_title, LV_ALIGN_TOP_LEFT, 8, 36);
make_label_halo(screen, s_cpu_value_halo, "--%", &lv_font_montserrat_48, 126,
LV_ALIGN_TOP_LEFT, 8, 52, 3, LV_TEXT_ALIGN_LEFT);
s_cpu_value_label = make_label(screen, "--%", &lv_font_montserrat_48);
lv_obj_set_width(s_cpu_value_label, 126);
lv_obj_set_style_text_align(s_cpu_value_label, LV_TEXT_ALIGN_LEFT, 0);
lv_obj_align(s_cpu_value_label, LV_ALIGN_TOP_LEFT, 8, 52);
make_label_halo(screen, s_cpu_freq_halo, "--GHz", &lv_font_montserrat_28, 128,
LV_ALIGN_TOP_LEFT, 8, 96, 2, LV_TEXT_ALIGN_LEFT);
s_cpu_freq_label = make_label(screen, "--GHz", &lv_font_montserrat_28);
lv_obj_set_width(s_cpu_freq_label, 128);
lv_obj_align(s_cpu_freq_label, LV_ALIGN_TOP_LEFT, 8, 96);
lv_obj_t *memory_title = make_label(screen, "MEM", &lv_font_montserrat_14);
lv_obj_align(memory_title, LV_ALIGN_TOP_LEFT, 208, 8);
make_label_halo(screen, s_memory_value_halo, "--%", &lv_font_montserrat_20, 50,
LV_ALIGN_TOP_RIGHT, -8, 8, 2, LV_TEXT_ALIGN_RIGHT);
s_memory_value_label = make_label(screen, "--%", &lv_font_montserrat_20);
lv_obj_set_width(s_memory_value_label, 50);
lv_obj_set_style_text_align(s_memory_value_label, LV_TEXT_ALIGN_RIGHT, 0);
lv_obj_align(s_memory_value_label, LV_ALIGN_TOP_RIGHT, -8, 8);
s_memory_detail_label = make_label(screen, "--/--", &lv_font_montserrat_14);
lv_obj_set_width(s_memory_detail_label, 178);
lv_obj_align(s_memory_detail_label, LV_ALIGN_TOP_LEFT, 208, 24);
s_memory_bar = make_bar(screen, 126, 10);
lv_obj_align(s_memory_bar, LV_ALIGN_TOP_LEFT, 262, 46);
lv_obj_t *disk_title = make_label(screen, "DSK", &lv_font_montserrat_14);
lv_obj_align(disk_title, LV_ALIGN_TOP_LEFT, 208, 64);
for (int i = 0; i < DISK_ROW_COUNT; i++) {
int y = 80 + i * 16;
s_disk_name_labels[i] = make_label(screen, "--", &lv_font_montserrat_14);
lv_obj_set_width(s_disk_name_labels[i], 16);
lv_obj_align(s_disk_name_labels[i], LV_ALIGN_TOP_LEFT, 208, y);
s_disk_bars[i] = make_bar(screen, 56, 8);
lv_obj_align(s_disk_bars[i], LV_ALIGN_TOP_LEFT, 226, y + 4);
s_disk_rate_labels[i] = make_label(screen, "--/--", &lv_font_montserrat_14);
lv_label_set_long_mode(s_disk_rate_labels[i], LV_LABEL_LONG_CLIP);
lv_obj_set_width(s_disk_rate_labels[i], 104);
lv_obj_align(s_disk_rate_labels[i], LV_ALIGN_TOP_LEFT, 288, y);
}
s_net_line_label = make_label(screen, "D -- U --", &lv_font_montserrat_20);
lv_label_set_long_mode(s_net_line_label, LV_LABEL_LONG_CLIP);
lv_obj_set_width(s_net_line_label, 184);
lv_obj_set_height(s_net_line_label, 24);
lv_obj_align(s_net_line_label, LV_ALIGN_TOP_LEFT, 208, 150);
lv_obj_t *codex_title = make_label(screen, "CODEX", &lv_font_montserrat_14);
lv_obj_align(codex_title, LV_ALIGN_TOP_LEFT, 8, 184);
s_codex_5h_label = make_label(screen, "5H", &lv_font_montserrat_14);
lv_obj_set_width(s_codex_5h_label, 28);
lv_obj_align(s_codex_5h_label, LV_ALIGN_TOP_LEFT, 8, 204);
s_codex_5h_bar = make_bar(screen, 76, 8);
lv_obj_align(s_codex_5h_bar, LV_ALIGN_TOP_LEFT, 36, 208);
s_codex_5h_value_label = make_label(screen, "--", &lv_font_montserrat_14);
lv_obj_set_width(s_codex_5h_value_label, 38);
lv_obj_align(s_codex_5h_value_label, LV_ALIGN_TOP_LEFT, 118, 204);
s_codex_5h_reset_label = make_label(screen, "--", &lv_font_montserrat_14);
lv_label_set_long_mode(s_codex_5h_reset_label, LV_LABEL_LONG_CLIP);
lv_obj_set_width(s_codex_5h_reset_label, 38);
lv_obj_align(s_codex_5h_reset_label, LV_ALIGN_TOP_LEFT, 158, 204);
s_codex_week_label = make_label(screen, "WK", &lv_font_montserrat_14);
lv_obj_set_width(s_codex_week_label, 28);
lv_obj_align(s_codex_week_label, LV_ALIGN_TOP_LEFT, 8, 224);
s_codex_week_bar = make_bar(screen, 76, 8);
lv_obj_align(s_codex_week_bar, LV_ALIGN_TOP_LEFT, 36, 228);
s_codex_week_value_label = make_label(screen, "--", &lv_font_montserrat_14);
lv_obj_set_width(s_codex_week_value_label, 38);
lv_obj_align(s_codex_week_value_label, LV_ALIGN_TOP_LEFT, 118, 224);
s_codex_week_reset_label = make_label(screen, "--", &lv_font_montserrat_14);
lv_label_set_long_mode(s_codex_week_reset_label, LV_LABEL_LONG_CLIP);
lv_obj_set_width(s_codex_week_reset_label, 38);
lv_obj_align(s_codex_week_reset_label, LV_ALIGN_TOP_LEFT, 158, 224);
s_codex_token_label = make_label(screen, "TOK --", &lv_font_montserrat_14);
lv_obj_set_width(s_codex_token_label, 96);
lv_obj_set_style_text_align(s_codex_token_label, LV_TEXT_ALIGN_RIGHT, 0);
lv_obj_align(s_codex_token_label, LV_ALIGN_TOP_RIGHT, -204, 184);
make_rule(screen, 8, CODEX_DAILY_BAR_BASE_Y, 188, 1);
for (int i = 0; i < UI_CODEX_DAILY_BAR_COUNT; i++) {
s_codex_daily_bars[i] = lv_obj_create(screen);
lv_obj_set_size(s_codex_daily_bars[i], CODEX_DAILY_BAR_W, 1);
lv_obj_align(s_codex_daily_bars[i], LV_ALIGN_TOP_LEFT,
10 + i * (CODEX_DAILY_BAR_W + CODEX_DAILY_BAR_GAP),
CODEX_DAILY_BAR_BASE_Y - 1);
lv_obj_set_style_bg_color(s_codex_daily_bars[i], ink(), 0);
lv_obj_set_style_bg_opa(s_codex_daily_bars[i], LV_OPA_COVER, 0);
lv_obj_set_style_border_width(s_codex_daily_bars[i], 0, 0);
lv_obj_set_style_radius(s_codex_daily_bars[i], 0, 0);
lv_obj_set_style_pad_all(s_codex_daily_bars[i], 0, 0);
lv_obj_clear_flag(s_codex_daily_bars[i], LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(s_codex_daily_bars[i], LV_OBJ_FLAG_HIDDEN);
}
lv_obj_t *zhipu_title = make_label(screen, "ZHIPU", &lv_font_montserrat_14);
lv_obj_align(zhipu_title, LV_ALIGN_TOP_LEFT, 208, 184);
s_zhipu_status_label = make_label(screen, "STAT WAIT", &lv_font_montserrat_14);
lv_obj_set_width(s_zhipu_status_label, 96);
lv_obj_set_style_text_align(s_zhipu_status_label, LV_TEXT_ALIGN_RIGHT, 0);
lv_obj_align(s_zhipu_status_label, LV_ALIGN_TOP_RIGHT, -8, 184);
s_zhipu_5h_label = make_label(screen, "5H", &lv_font_montserrat_14);
lv_obj_set_width(s_zhipu_5h_label, 28);
lv_obj_align(s_zhipu_5h_label, LV_ALIGN_TOP_LEFT, 208, 214);
s_zhipu_5h_bar = make_bar(screen, 74, 9);
lv_obj_align(s_zhipu_5h_bar, LV_ALIGN_TOP_LEFT, 238, 218);
s_zhipu_5h_value_label = make_label(screen, "--", &lv_font_montserrat_14);
lv_obj_set_width(s_zhipu_5h_value_label, 38);
lv_obj_align(s_zhipu_5h_value_label, LV_ALIGN_TOP_LEFT, 318, 214);
s_zhipu_5h_reset_label = make_label(screen, "--", &lv_font_montserrat_14);
lv_label_set_long_mode(s_zhipu_5h_reset_label, LV_LABEL_LONG_CLIP);
lv_obj_set_width(s_zhipu_5h_reset_label, 38);
lv_obj_align(s_zhipu_5h_reset_label, LV_ALIGN_TOP_LEFT, 358, 214);
s_zhipu_week_label = make_label(screen, "WK", &lv_font_montserrat_14);
lv_obj_set_width(s_zhipu_week_label, 28);
lv_obj_align(s_zhipu_week_label, LV_ALIGN_TOP_LEFT, 208, 242);
s_zhipu_week_bar = make_bar(screen, 74, 9);
lv_obj_align(s_zhipu_week_bar, LV_ALIGN_TOP_LEFT, 238, 246);
s_zhipu_week_value_label = make_label(screen, "--", &lv_font_montserrat_14);
lv_obj_set_width(s_zhipu_week_value_label, 38);
lv_obj_align(s_zhipu_week_value_label, LV_ALIGN_TOP_LEFT, 318, 242);
s_zhipu_week_reset_label = make_label(screen, "--", &lv_font_montserrat_14);
lv_label_set_long_mode(s_zhipu_week_reset_label, LV_LABEL_LONG_CLIP);
lv_obj_set_width(s_zhipu_week_reset_label, 38);
lv_obj_align(s_zhipu_week_reset_label, LV_ALIGN_TOP_LEFT, 358, 242);
}
void ui_screen_tick(void)
{
s_age_seconds++;
if (s_age_seconds > 5 && s_status_label != NULL) {
lv_label_set_text(s_status_label, "STALE");
}
}
void ui_screen_set_status(const char *status)
{
if (s_status_label == NULL || status == NULL) {
return;
}
lv_lock();
lv_label_set_text(s_status_label, status);
lv_unlock();
}
void ui_screen_update_battery(int percent, int millivolts)
{
if (s_battery_label == NULL) {
return;
}
const char *symbol = LV_SYMBOL_BATTERY_EMPTY;
percent = clamp_percent(percent);
if (percent >= 90) {
symbol = LV_SYMBOL_BATTERY_FULL;
} else if (percent >= 65) {
symbol = LV_SYMBOL_BATTERY_3;
} else if (percent >= 35) {
symbol = LV_SYMBOL_BATTERY_2;
} else if (percent >= 10) {
symbol = LV_SYMBOL_BATTERY_1;
}
char text[24];
if (millivolts > 0) {
snprintf(text, sizeof(text), "%s %d%%", symbol, percent);
} else {
snprintf(text, sizeof(text), "%s --%%", symbol);
}
lv_lock();
lv_label_set_text(s_battery_label, text);
lv_unlock();
}
void ui_screen_update_environment(int temperature_c10, int humidity_percent)
{
if (s_env_label == NULL) {
return;
}
if (humidity_percent < 0) {
humidity_percent = 0;
} else if (humidity_percent > 100) {
humidity_percent = 100;
}
char text[32];
int abs_temp_c10 = abs(temperature_c10);
if (temperature_c10 < 0) {
snprintf(text, sizeof(text), "-%d.%dC %d%%", abs_temp_c10 / 10, abs_temp_c10 % 10, humidity_percent);
} else {
snprintf(text, sizeof(text), "%d.%dC %d%%", abs_temp_c10 / 10, abs_temp_c10 % 10, humidity_percent);
}
lv_lock();
lv_label_set_text(s_env_label, text);
lv_unlock();
}
void ui_screen_update_metrics_from_source(const ui_metrics_t *metrics, const char *source)
{
if (metrics == NULL || s_cpu_value_label == NULL) {
return;
}
char text[96];
char used[16];
char total[16];
lv_lock();
s_age_seconds = 0;
lv_label_set_text(s_status_label, source ? source : "LIVE");
set_connection(connection_from_source(source));
snprintf(text, sizeof(text), "%d%%", clamp_percent(metrics->cpu_percent));
set_label_with_halo(s_cpu_value_label, s_cpu_value_halo, text);
if (metrics->cpu_mhz > 0) {
snprintf(text, sizeof(text), "%d.%02dGHz", metrics->cpu_mhz / 1000, (metrics->cpu_mhz % 1000) / 10);
} else {
snprintf(text, sizeof(text), "--GHz");
}
set_label_with_halo(s_cpu_freq_label, s_cpu_freq_halo, text);
set_cpu_curve(metrics->cpu_percent);
snprintf(text, sizeof(text), "%d%%", clamp_percent(metrics->memory_percent));
set_label_with_halo(s_memory_value_label, s_memory_value_halo, text);
format_capacity(metrics->memory_used_mb, used, sizeof(used));
format_capacity(metrics->memory_total_mb, total, sizeof(total));
snprintf(text, sizeof(text), "%s / %s", used, total);
lv_label_set_text(s_memory_detail_label, text);
set_bar_percent(s_memory_bar, metrics->memory_percent);
update_disk_rows(metrics->disk_text);
update_net_line(metrics);
update_codex_panel(metrics);
update_zhipu_panel(metrics);
lv_obj_invalidate(lv_screen_active());
lv_unlock();
}
void ui_screen_update_metrics(const ui_metrics_t *metrics)
{
ui_screen_update_metrics_from_source(metrics, "USB");
}