Add CYD host PC info panel with serial SI protocol and LVGL bars.

ESP32 shows CPU/GPU/RAM/disk from host/send_sysinfo.ps1 over USB; firmware packages for 2.8 and 3.5 included.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-27 20:13:09 +08:00
commit f724796359
32 changed files with 2664 additions and 0 deletions

152
src/host_info.cpp Normal file
View File

@@ -0,0 +1,152 @@
#include "host_info.h"
#include <Arduino.h>
#include <cstdio>
#include <cstring>
static HostInfo g_info = {};
static char g_line[384];
static size_t g_len = 0;
static void copy_trunc(char *dst, size_t dst_sz, const char *src) {
if (!dst || dst_sz == 0) {
return;
}
if (!src) {
dst[0] = '\0';
return;
}
strncpy(dst, src, dst_sz - 1);
dst[dst_sz - 1] = '\0';
}
static const char *find_field(const char *line, const char *key) {
// Look for |key=value| or start-of-string after "SI|"
const size_t klen = strlen(key);
const char *p = line;
while (p && *p) {
if (strncmp(p, key, klen) == 0 && p[klen] == '=') {
return p + klen + 1;
}
p = strchr(p, '|');
if (!p) {
break;
}
++p;
}
return nullptr;
}
static void copy_field(char *dst, size_t dst_sz, const char *line, const char *key) {
const char *v = find_field(line, key);
if (!v) {
dst[0] = '\0';
return;
}
size_t n = 0;
while (v[n] && v[n] != '|' && n + 1 < dst_sz) {
dst[n] = v[n];
++n;
}
dst[n] = '\0';
}
static float field_float(const char *line, const char *key, float fallback) {
const char *v = find_field(line, key);
if (!v || !*v) {
return fallback;
}
return (float)atof(v);
}
static void apply_line(const char *line) {
// SI|host=...|cpu=...|cpu%=..|gpu=...|gpu%=..|ram=used/total|disk=used/total
if (strncmp(line, "SI|", 3) != 0) {
return;
}
HostInfo next = g_info;
next.valid = true;
next.updated_ms = millis();
copy_field(next.host, sizeof(next.host), line, "host");
copy_field(next.cpu, sizeof(next.cpu), line, "cpu");
copy_field(next.gpu, sizeof(next.gpu), line, "gpu");
next.cpu_pct = field_float(line, "cpu%", next.cpu_pct);
next.gpu_pct = field_float(line, "gpu%", next.gpu_pct);
const char *ram = find_field(line, "ram");
if (ram) {
float u = 0, t = 0;
if (sscanf(ram, "%f/%f", &u, &t) == 2) {
next.ram_used_gb = u;
next.ram_total_gb = t;
}
}
const char *disk = find_field(line, "disk");
if (disk) {
float u = 0, t = 0;
if (sscanf(disk, "%f/%f", &u, &t) == 2) {
next.disk_used_gb = u;
next.disk_total_gb = t;
}
}
if (next.cpu_pct < 0) {
next.cpu_pct = 0;
}
if (next.cpu_pct > 100) {
next.cpu_pct = 100;
}
if (next.gpu_pct > 100) {
next.gpu_pct = 100;
}
g_info = next;
}
void host_info_begin() {
memset(&g_info, 0, sizeof(g_info));
g_info.gpu_pct = -1.0f;
g_info.valid = false;
g_len = 0;
g_line[0] = '\0';
}
bool host_info_poll() {
bool got = false;
while (Serial.available() > 0) {
const int c = Serial.read();
if (c < 0) {
break;
}
if (c == '\r') {
continue;
}
if (c == '\n') {
g_line[g_len] = '\0';
if (g_len > 0) {
apply_line(g_line);
got = g_info.valid;
}
g_len = 0;
continue;
}
if (g_len + 1 < sizeof(g_line)) {
g_line[g_len++] = (char)c;
} else {
// Overflow — drop line
g_len = 0;
}
}
return got;
}
const HostInfo &host_info_get() { return g_info; }
bool host_info_is_live(uint32_t stale_ms) {
if (!g_info.valid) {
return false;
}
return (millis() - g_info.updated_ms) <= stale_ms;
}

52
src/main.cpp Normal file
View File

@@ -0,0 +1,52 @@
#include <Arduino.h>
#include <lvgl.h>
#include "host_info.h"
#include "pins.h"
#include "touch.h"
#include "ui.h"
static constexpr uint32_t DRAW_BUF_SIZE =
(SCREEN_WIDTH * SCREEN_HEIGHT / DRAW_BUF_DIV * (LV_COLOR_DEPTH / 8));
static uint32_t draw_buf[DRAW_BUF_SIZE / 4];
void setup() {
Serial.setRxBufferSize(2048);
Serial.begin(460800);
delay(200);
Serial.println();
Serial.printf("LVGL %d.%d.%d | CYD Server Info\n", lv_version_major(),
lv_version_minor(), lv_version_patch());
Serial.println("BOOT_OK");
Serial.println("PROTO SI|host=..|cpu=..|cpu%=..|gpu=..|gpu%=..|ram=u/t|disk=u/t");
Serial.printf("heap=%u BL=%d panel=%dx%d ui=%dx%d\n",
(unsigned)ESP.getFreeHeap(), TFT_BL_PIN, SCREEN_WIDTH,
SCREEN_HEIGHT, UI_HOR_RES, UI_VER_RES);
pinMode(TFT_BL_PIN, OUTPUT);
digitalWrite(TFT_BL_PIN, HIGH);
touch_init();
host_info_begin();
lv_init();
lv_display_t *disp =
lv_tft_espi_create(SCREEN_WIDTH, SCREEN_HEIGHT, draw_buf, sizeof(draw_buf));
lv_display_set_rotation(disp, LV_DISPLAY_ROTATION_270);
lv_indev_t *indev = lv_indev_create();
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(indev, touch_read_cb);
lv_indev_set_display(indev, disp);
ui_create();
Serial.printf("ui_ok heap=%u\n", (unsigned)ESP.getFreeHeap());
}
void loop() {
host_info_poll();
lv_timer_handler();
lv_tick_inc(5);
ui_tick();
delay(5);
}

284
src/touch.cpp Normal file
View File

@@ -0,0 +1,284 @@
#include "touch.h"
#include "pins.h"
#include <Arduino.h>
#if defined(CYD_BOARD_3248S035)
#include <Wire.h>
#endif
/**
* 2.8": dedicated SoftSPI + BL/TR diagonal fix.
* 3.5C (3248S035C): capacitive GT911 on I2C SDA33/SCL32/INT21/RST25.
*/
int16_t g_dbg_phys_x = -1;
int16_t g_dbg_phys_y = -1;
int16_t g_dbg_logic_x = -1;
int16_t g_dbg_logic_y = -1;
bool g_dbg_touch_down = false;
static int16_t clampi(int32_t v, int32_t lo, int32_t hi) {
if (v < lo) {
return (int16_t)lo;
}
if (v > hi) {
return (int16_t)hi;
}
return (int16_t)v;
}
#if defined(CYD_BOARD_3248S035)
/* Minimal GT911 — no config rewrite (TAMC setResolution can hang Wire). */
static uint8_t s_gt_addr = 0x5D;
static constexpr uint16_t GT_POINT_INFO = 0x814E;
static constexpr uint16_t GT_POINT_1 = 0x814F;
static bool gt_write8(uint16_t reg, uint8_t val) {
Wire.beginTransmission(s_gt_addr);
Wire.write((uint8_t)(reg >> 8));
Wire.write((uint8_t)(reg & 0xFF));
Wire.write(val);
return Wire.endTransmission() == 0;
}
static bool gt_read(uint16_t reg, uint8_t *buf, uint8_t len) {
Wire.beginTransmission(s_gt_addr);
Wire.write((uint8_t)(reg >> 8));
Wire.write((uint8_t)(reg & 0xFF));
if (Wire.endTransmission(false) != 0) {
return false;
}
const uint8_t n = Wire.requestFrom(s_gt_addr, len);
if (n != len) {
return false;
}
for (uint8_t i = 0; i < len; i++) {
buf[i] = Wire.read();
}
return true;
}
static bool gt_probe(uint8_t addr) {
Wire.beginTransmission(addr);
return Wire.endTransmission() == 0;
}
/* TAMC-compatible address select: INT low => 0x5D, INT high => 0x14 */
static void gt_hw_reset(bool addr_0x14) {
pinMode(CYD_GT911_INT, OUTPUT);
pinMode(CYD_GT911_RST, OUTPUT);
digitalWrite(CYD_GT911_INT, LOW);
digitalWrite(CYD_GT911_RST, LOW);
delay(10);
digitalWrite(CYD_GT911_INT, addr_0x14 ? HIGH : LOW);
delay(1);
digitalWrite(CYD_GT911_RST, HIGH);
delay(5);
digitalWrite(CYD_GT911_INT, LOW);
delay(50);
/* Backlight is GPIO27 — leave INT as input for polling / optional IRQ. */
pinMode(CYD_GT911_INT, INPUT);
delay(50);
}
static bool gt_find_addr() {
for (uint8_t a : {uint8_t(0x5D), uint8_t(0x14)}) {
if (gt_probe(a)) {
s_gt_addr = a;
return true;
}
}
return false;
}
void touch_init() {
Serial.println("Touch: GT911 I2C capacitive (3.5C) minimal");
Wire.begin(CYD_GT911_SDA, CYD_GT911_SCL);
Wire.setClock(100000);
#if defined(WIRE_HAS_TIMEOUT) || defined(ESP32)
Wire.setTimeOut(50);
#endif
if (!gt_find_addr()) {
Serial.println("GT911 not found — hw reset 0x5D");
gt_hw_reset(false);
if (!gt_find_addr()) {
Serial.println("GT911 not found — hw reset 0x14");
gt_hw_reset(true);
gt_find_addr();
}
}
Serial.printf("GT911 probe 0x5D=%s 0x14=%s using=0x%02X\n",
gt_probe(0x5D) ? "OK" : "no", gt_probe(0x14) ? "OK" : "no",
s_gt_addr);
Serial.printf("GT911 SDA=%d SCL=%d INT=%d RST=%d\n", CYD_GT911_SDA,
CYD_GT911_SCL, CYD_GT911_INT, CYD_GT911_RST);
}
void touch_read_cb(lv_indev_t * /*indev*/, lv_indev_data_t *data) {
uint8_t info = 0;
static uint32_t last_log = 0;
const bool do_log = (millis() - last_log > 200);
if (!gt_read(GT_POINT_INFO, &info, 1)) {
data->state = LV_INDEV_STATE_RELEASED;
g_dbg_touch_down = false;
if (do_log) {
last_log = millis();
Serial.println("TouchProbe GT911 I2C err");
}
return;
}
const uint8_t touches = info & 0x0F;
const bool buffered = (info & 0x80) != 0;
if (!buffered || touches == 0) {
if (buffered) {
gt_write8(GT_POINT_INFO, 0);
}
data->state = LV_INDEV_STATE_RELEASED;
g_dbg_touch_down = false;
return;
}
uint8_t pt[7];
if (!gt_read(GT_POINT_1, pt, 7)) {
gt_write8(GT_POINT_INFO, 0);
data->state = LV_INDEV_STATE_RELEASED;
g_dbg_touch_down = false;
return;
}
gt_write8(GT_POINT_INFO, 0);
const int16_t raw_x = (int16_t)(pt[1] | (pt[2] << 8));
const int16_t raw_y = (int16_t)(pt[3] | (pt[4] << 8));
/* Native 320x480 → LVGL 270. Mirror long axis so L/R matches UI
* (else preset 3 hits 10). Resulting UI ≈ (H-1-raw_y, raw_x). */
const int16_t nx = clampi(raw_x, 0, SCREEN_WIDTH - 1);
const int16_t ny = clampi(raw_y, 0, SCREEN_HEIGHT - 1);
const int16_t feed_x = (int16_t)((SCREEN_WIDTH - 1) - nx);
const int16_t feed_y = (int16_t)((SCREEN_HEIGHT - 1) - ny);
const int16_t ui_x = (int16_t)((SCREEN_HEIGHT - 1) - ny);
const int16_t ui_y = nx;
g_dbg_phys_x = raw_x;
g_dbg_phys_y = raw_y;
g_dbg_logic_x = ui_x;
g_dbg_logic_y = ui_y;
g_dbg_touch_down = true;
if (do_log) {
last_log = millis();
Serial.printf("Touch HIT GT911 raw(%d,%d) ~ui(%d,%d)\n", raw_x, raw_y,
ui_x, ui_y);
}
data->point.x = feed_x;
data->point.y = feed_y;
data->state = LV_INDEV_STATE_PRESSED;
}
#else
static void soft_write(uint8_t data) {
for (int i = 7; i >= 0; --i) {
digitalWrite(CYD_TOUCH_MOSI, (data >> i) & 1);
digitalWrite(CYD_TOUCH_CLK, HIGH);
delayMicroseconds(2);
digitalWrite(CYD_TOUCH_CLK, LOW);
delayMicroseconds(2);
}
}
static uint16_t soft_read12() {
uint16_t v = 0;
digitalWrite(CYD_TOUCH_CLK, HIGH);
delayMicroseconds(2);
digitalWrite(CYD_TOUCH_CLK, LOW);
delayMicroseconds(2);
for (int i = 0; i < 12; ++i) {
digitalWrite(CYD_TOUCH_CLK, HIGH);
delayMicroseconds(2);
v = (uint16_t)((v << 1) | (digitalRead(CYD_TOUCH_MISO) ? 1u : 0u));
digitalWrite(CYD_TOUCH_CLK, LOW);
delayMicroseconds(2);
}
return v;
}
static uint16_t xpt_read(uint8_t cmd) {
digitalWrite(CYD_TOUCH_CS, LOW);
soft_write(cmd);
uint16_t v = soft_read12();
digitalWrite(CYD_TOUCH_CS, HIGH);
return v;
}
void touch_init() {
pinMode(CYD_TOUCH_CS, OUTPUT);
pinMode(CYD_TOUCH_CLK, OUTPUT);
pinMode(CYD_TOUCH_MOSI, OUTPUT);
pinMode(CYD_TOUCH_MISO, INPUT);
pinMode(CYD_TOUCH_IRQ, INPUT);
digitalWrite(CYD_TOUCH_CS, HIGH);
digitalWrite(CYD_TOUCH_CLK, LOW);
Serial.println("Touch: SoftSPI + diagonal fix (BL/TR)");
}
void touch_read_cb(lv_indev_t * /*indev*/, lv_indev_data_t *data) {
uint16_t adc_x = xpt_read(0x90);
uint16_t adc_y = xpt_read(0xD0);
adc_x = (uint16_t)((adc_x + xpt_read(0x90)) / 2);
adc_y = (uint16_t)((adc_y + xpt_read(0xD0)) / 2);
const bool irq = (digitalRead(CYD_TOUCH_IRQ) == LOW);
const bool in_range =
(adc_x > 80 && adc_x < 4000 && adc_y > 80 && adc_y < 4000);
if (!in_range || (!irq && (adc_x < 200 || adc_y < 200))) {
data->state = LV_INDEV_STATE_RELEASED;
g_dbg_touch_down = false;
return;
}
const int16_t p_x = (int16_t)adc_x;
const int16_t p_y = (int16_t)(4095 - adc_y);
int16_t x = map(p_x, 200, 3700, 1, SCREEN_WIDTH);
int16_t y = map(p_y, 240, 3800, 1, SCREEN_HEIGHT);
x = clampi(x, 0, SCREEN_WIDTH - 1);
y = clampi(y, 0, SCREEN_HEIGHT - 1);
const int32_t old_ux = y;
const int32_t old_uy = (SCREEN_WIDTH - 1) - x;
int16_t ui_x = clampi((old_uy * (UI_HOR_RES - 1)) / (UI_VER_RES - 1), 0,
UI_HOR_RES - 1);
int16_t ui_y = clampi((old_ux * (UI_VER_RES - 1)) / (UI_HOR_RES - 1), 0,
UI_VER_RES - 1);
const int16_t feed_x = (int16_t)((SCREEN_WIDTH - 1) - ui_y);
const int16_t feed_y = ui_x;
g_dbg_phys_x = x;
g_dbg_phys_y = y;
g_dbg_logic_x = ui_x;
g_dbg_logic_y = ui_y;
g_dbg_touch_down = true;
static uint32_t last_log = 0;
if (millis() - last_log > 80) {
last_log = millis();
Serial.printf("Touch UI(%d,%d)\n", ui_x, ui_y);
}
data->point.x = feed_x;
data->point.y = feed_y;
data->state = LV_INDEV_STATE_PRESSED;
}
#endif

223
src/ui.cpp Normal file
View File

@@ -0,0 +1,223 @@
#include "ui.h"
#include "host_info.h"
#include "pins.h"
#include <Arduino.h>
#include <cstdio>
#include <lvgl.h>
static constexpr uint32_t COL_BG = 0x0B121A;
static constexpr uint32_t COL_INK = 0xE8EEF4;
static constexpr uint32_t COL_MUTED = 0x7A8B9C;
static constexpr uint32_t COL_LINE = 0x1A2734;
static constexpr uint32_t COL_OK = 0x3DDC97;
static constexpr uint32_t COL_WARN = 0xE8B84A;
static constexpr uint32_t COL_BAD = 0xE85D4C;
static constexpr uint32_t COL_ACCENT = 0x4FC3F7;
static constexpr uint32_t COL_BAR_BG = 0x162230;
struct MetricRow {
lv_obj_t *lbl_tag;
lv_obj_t *lbl_name;
lv_obj_t *lbl_pct;
lv_obj_t *bar;
};
static lv_obj_t *scr = nullptr;
static lv_obj_t *lbl_title = nullptr;
static lv_obj_t *lbl_host = nullptr;
static lv_obj_t *lbl_status = nullptr;
static MetricRow row_cpu = {};
static MetricRow row_gpu = {};
static MetricRow row_ram = {};
static MetricRow row_disk = {};
static uint32_t last_paint_ms = 0;
static int pct_of(float used, float total) {
if (total <= 0.01f) {
return 0;
}
int p = (int)((used * 100.0f) / total + 0.5f);
if (p < 0) {
p = 0;
}
if (p > 100) {
p = 100;
}
return p;
}
static uint32_t color_for_pct(int pct) {
if (pct >= 85) {
return COL_BAD;
}
if (pct >= 65) {
return COL_WARN;
}
return COL_OK;
}
static void style_bar(lv_obj_t *bar) {
lv_obj_set_style_bg_color(bar, lv_color_hex(COL_BAR_BG), LV_PART_MAIN);
lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_set_style_radius(bar, 4, LV_PART_MAIN);
lv_obj_set_style_pad_all(bar, 0, LV_PART_MAIN);
lv_obj_set_style_bg_color(bar, lv_color_hex(COL_OK), LV_PART_INDICATOR);
lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, LV_PART_INDICATOR);
lv_obj_set_style_radius(bar, 4, LV_PART_INDICATOR);
lv_bar_set_range(bar, 0, 100);
lv_bar_set_value(bar, 0, LV_ANIM_OFF);
}
static MetricRow make_row(lv_obj_t *parent, const char *tag, int y) {
MetricRow r = {};
r.lbl_tag = lv_label_create(parent);
lv_label_set_text(r.lbl_tag, tag);
lv_obj_set_style_text_color(r.lbl_tag, lv_color_hex(COL_ACCENT), 0);
lv_obj_set_style_text_font(r.lbl_tag, &lv_font_montserrat_14, 0);
lv_obj_set_pos(r.lbl_tag, UX(8), UY(y));
r.lbl_name = lv_label_create(parent);
lv_label_set_text(r.lbl_name, "—");
lv_label_set_long_mode(r.lbl_name, LV_LABEL_LONG_DOT);
lv_obj_set_width(r.lbl_name, UW(190));
lv_obj_set_style_text_color(r.lbl_name, lv_color_hex(COL_INK), 0);
lv_obj_set_style_text_font(r.lbl_name, &lv_font_montserrat_14, 0);
lv_obj_set_pos(r.lbl_name, UX(52), UY(y));
r.lbl_pct = lv_label_create(parent);
lv_label_set_text(r.lbl_pct, "--%");
lv_obj_set_style_text_color(r.lbl_pct, lv_color_hex(COL_MUTED), 0);
lv_obj_set_style_text_font(r.lbl_pct, &lv_font_montserrat_14, 0);
lv_obj_align(r.lbl_pct, LV_ALIGN_TOP_RIGHT, UX(-8), UY(y));
r.bar = lv_bar_create(parent);
lv_obj_set_size(r.bar, UI_HOR_RES - UW(16), UH(10));
lv_obj_set_pos(r.bar, UX(8), UY(y + 18));
style_bar(r.bar);
return r;
}
static void set_row_pct(MetricRow &r, int pct, const char *name, const char *right) {
if (r.lbl_name && name) {
lv_label_set_text(r.lbl_name, name);
}
if (r.lbl_pct && right) {
lv_label_set_text(r.lbl_pct, right);
lv_obj_set_style_text_color(r.lbl_pct, lv_color_hex(color_for_pct(pct)), 0);
}
if (r.bar) {
lv_bar_set_value(r.bar, pct, LV_ANIM_OFF);
lv_obj_set_style_bg_color(r.bar, lv_color_hex(color_for_pct(pct)),
LV_PART_INDICATOR);
}
}
void ui_create() {
scr = lv_obj_create(nullptr);
lv_obj_set_style_bg_color(scr, lv_color_hex(COL_BG), 0);
lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);
lv_obj_clear_flag(scr, LV_OBJ_FLAG_SCROLLABLE);
lv_screen_load(scr);
lbl_title = lv_label_create(scr);
lv_label_set_text(lbl_title, "PC Monitor");
lv_obj_set_style_text_color(lbl_title, lv_color_hex(COL_INK), 0);
lv_obj_set_style_text_font(lbl_title, &lv_font_montserrat_18, 0);
lv_obj_set_pos(lbl_title, UX(8), UY(6));
lbl_status = lv_label_create(scr);
lv_label_set_text(lbl_status, "waiting");
lv_obj_set_style_text_color(lbl_status, lv_color_hex(COL_WARN), 0);
lv_obj_set_style_text_font(lbl_status, &lv_font_montserrat_14, 0);
lv_obj_align(lbl_status, LV_ALIGN_TOP_RIGHT, UX(-8), UY(8));
lbl_host = lv_label_create(scr);
lv_label_set_text(lbl_host, "Connect USB + run host script");
lv_label_set_long_mode(lbl_host, LV_LABEL_LONG_DOT);
lv_obj_set_width(lbl_host, UI_HOR_RES - UW(16));
lv_obj_set_style_text_color(lbl_host, lv_color_hex(COL_MUTED), 0);
lv_obj_set_style_text_font(lbl_host, &lv_font_montserrat_14, 0);
lv_obj_set_pos(lbl_host, UX(8), UY(28));
lv_obj_t *line = lv_obj_create(scr);
lv_obj_set_size(line, UI_HOR_RES - UW(16), 1);
lv_obj_set_pos(line, UX(8), UY(48));
lv_obj_set_style_bg_color(line, lv_color_hex(COL_LINE), 0);
lv_obj_set_style_bg_opa(line, LV_OPA_COVER, 0);
lv_obj_set_style_border_width(line, 0, 0);
lv_obj_set_style_pad_all(line, 0, 0);
lv_obj_clear_flag(line, LV_OBJ_FLAG_SCROLLABLE);
row_cpu = make_row(scr, "CPU", 56);
row_gpu = make_row(scr, "GPU", 96);
row_ram = make_row(scr, "RAM", 136);
row_disk = make_row(scr, "DISK", 176);
last_paint_ms = 0;
}
static void paint() {
const HostInfo &h = host_info_get();
const bool live = host_info_is_live(5000);
if (lbl_status) {
if (!h.valid) {
lv_label_set_text(lbl_status, "waiting");
lv_obj_set_style_text_color(lbl_status, lv_color_hex(COL_WARN), 0);
} else if (live) {
lv_label_set_text(lbl_status, "live");
lv_obj_set_style_text_color(lbl_status, lv_color_hex(COL_OK), 0);
} else {
lv_label_set_text(lbl_status, "stale");
lv_obj_set_style_text_color(lbl_status, lv_color_hex(COL_BAD), 0);
}
}
if (!h.valid) {
return;
}
if (lbl_host) {
lv_label_set_text(lbl_host, h.host[0] ? h.host : "(unknown host)");
}
char buf[48];
int cpu_pct = (int)(h.cpu_pct + 0.5f);
snprintf(buf, sizeof(buf), "%d%%", cpu_pct);
set_row_pct(row_cpu, cpu_pct, h.cpu[0] ? h.cpu : "CPU", buf);
if (h.gpu_pct < 0) {
set_row_pct(row_gpu, 0, h.gpu[0] ? h.gpu : "GPU", "n/a");
if (row_gpu.lbl_pct) {
lv_obj_set_style_text_color(row_gpu.lbl_pct, lv_color_hex(COL_MUTED), 0);
}
} else {
int gpu_pct = (int)(h.gpu_pct + 0.5f);
snprintf(buf, sizeof(buf), "%d%%", gpu_pct);
set_row_pct(row_gpu, gpu_pct, h.gpu[0] ? h.gpu : "GPU", buf);
}
int ram_pct = pct_of(h.ram_used_gb, h.ram_total_gb);
snprintf(buf, sizeof(buf), "%.1f/%.0fG", (double)h.ram_used_gb,
(double)h.ram_total_gb);
set_row_pct(row_ram, ram_pct, "Memory", buf);
int disk_pct = pct_of(h.disk_used_gb, h.disk_total_gb);
snprintf(buf, sizeof(buf), "%.0f/%.0fG", (double)h.disk_used_gb,
(double)h.disk_total_gb);
set_row_pct(row_disk, disk_pct, "System drive", buf);
}
void ui_tick() {
const uint32_t now = millis();
if (now - last_paint_ms < 200) {
return;
}
last_paint_ms = now;
paint();
}