diff --git a/main/metrics_protocol.c b/main/metrics_protocol.c index b9280dc..e059d42 100644 --- a/main/metrics_protocol.c +++ b/main/metrics_protocol.c @@ -72,6 +72,11 @@ static void set_optional_defaults(ui_metrics_t *metrics) metrics->zhipu_week_percent = -1; metrics->zhipu_5h_reset_mins = -1; metrics->zhipu_week_reset_mins = -1; + metrics->zhipu_today_tokens_m10 = -1; + metrics->zhipu_daily_count = 0; + for (int i = 0; i < UI_ZHIPU_DAILY_BAR_COUNT; i++) { + metrics->zhipu_daily_percent[i] = -1; + } } static void parse_codex_daily_bars(ui_metrics_t *metrics, const char *daily_text) @@ -106,6 +111,38 @@ static void parse_codex_daily_bars(ui_metrics_t *metrics, const char *daily_text } } +static void parse_zhipu_daily_bars(ui_metrics_t *metrics, const char *daily_text) +{ + metrics->zhipu_daily_count = 0; + for (int i = 0; i < UI_ZHIPU_DAILY_BAR_COUNT; i++) { + metrics->zhipu_daily_percent[i] = -1; + } + + if (daily_text == NULL || daily_text[0] == '\0') { + return; + } + + char buffer[96]; + snprintf(buffer, sizeof(buffer), "%s", daily_text); + + char *cursor = buffer; + char *token = NULL; + while ((token = strsep(&cursor, ":")) != NULL && metrics->zhipu_daily_count < UI_ZHIPU_DAILY_BAR_COUNT) { + if (token[0] == '\0') { + continue; + } + + char *end = NULL; + long value = strtol(token, &end, 10); + if (end == token || *end != '\0') { + continue; + } + + metrics->zhipu_daily_percent[metrics->zhipu_daily_count] = clamp_percent((int)value); + metrics->zhipu_daily_count++; + } +} + static bool parse_sys2_line(const char *line, ui_metrics_t *metrics) { char buffer[320]; @@ -601,6 +638,114 @@ static bool parse_sys7_line(const char *line, ui_metrics_t *metrics) return true; } +static bool parse_sys8_line(const char *line, ui_metrics_t *metrics) +{ + char buffer[768]; + snprintf(buffer, sizeof(buffer), "%s", line); + + char *cursor = buffer; + char *tag = strsep(&cursor, ","); + if (tag == NULL || strcmp(tag, "SYS8") != 0) { + return false; + } + + int cpu = 0; + int cpu_mhz = 0; + int mem = 0; + int mem_used = 0; + int mem_total = 0; + int disk_count = 0; + int disk_percent = 0; + int disk_read = 0; + int disk_write = 0; + int rx = 0; + int tx = 0; + int codex_5h = -1; + int codex_week = -1; + int codex_5h_reset = -1; + int codex_week_reset = -1; + int codex_credits = -1; + int codex_today_tokens_m10 = -1; + int zhipu_status = 0; + int zhipu_5h = -1; + int zhipu_week = -1; + int zhipu_5h_reset = -1; + int zhipu_week_reset = -1; + int zhipu_today_tokens_m10 = -1; + + if (!parse_int_token(&cursor, &cpu) || + !parse_int_token(&cursor, &cpu_mhz) || + !parse_int_token(&cursor, &mem) || + !parse_int_token(&cursor, &mem_used) || + !parse_int_token(&cursor, &mem_total) || + !parse_int_token(&cursor, &disk_count) || + !parse_int_token(&cursor, &disk_percent) || + !parse_int_token(&cursor, &disk_read) || + !parse_int_token(&cursor, &disk_write) || + !parse_int_token(&cursor, &rx) || + !parse_int_token(&cursor, &tx) || + !parse_int_token(&cursor, &codex_5h) || + !parse_int_token(&cursor, &codex_week) || + !parse_int_token(&cursor, &codex_5h_reset) || + !parse_int_token(&cursor, &codex_week_reset) || + !parse_int_token(&cursor, &codex_credits) || + !parse_int_token(&cursor, &codex_today_tokens_m10)) { + return false; + } + + char *codex_daily_text = strsep(&cursor, ","); + if (codex_daily_text == NULL || + !parse_int_token(&cursor, &zhipu_status) || + !parse_int_token(&cursor, &zhipu_5h) || + !parse_int_token(&cursor, &zhipu_week) || + !parse_int_token(&cursor, &zhipu_5h_reset) || + !parse_int_token(&cursor, &zhipu_week_reset) || + !parse_int_token(&cursor, &zhipu_today_tokens_m10)) { + return false; + } + + char *zhipu_daily_text = strsep(&cursor, ","); + if (zhipu_daily_text == NULL) { + return false; + } + + char *core_text = strsep(&cursor, ","); + if (core_text == NULL) { + return false; + } + + metrics->cpu_percent = clamp_percent(cpu); + metrics->cpu_mhz = cpu_mhz; + metrics->memory_percent = clamp_percent(mem); + metrics->memory_used_mb = mem_used; + metrics->memory_total_mb = mem_total; + metrics->disk_count = disk_count; + metrics->disk_percent = clamp_percent(disk_percent); + metrics->disk_read_kbps = disk_read; + metrics->disk_write_kbps = disk_write; + metrics->rx_kbps = rx; + metrics->tx_kbps = tx; + metrics->codex_5h_percent = (codex_5h >= 0) ? clamp_percent(codex_5h) : -1; + metrics->codex_week_percent = (codex_week >= 0) ? clamp_percent(codex_week) : -1; + metrics->codex_5h_reset_mins = codex_5h_reset; + metrics->codex_week_reset_mins = codex_week_reset; + metrics->codex_reset_credits = codex_credits; + metrics->codex_today_tokens_m10 = codex_today_tokens_m10; + parse_codex_daily_bars(metrics, codex_daily_text); + metrics->zhipu_status = zhipu_status; + metrics->zhipu_5h_percent = (zhipu_5h >= 0) ? clamp_percent(zhipu_5h) : -1; + metrics->zhipu_week_percent = (zhipu_week >= 0) ? clamp_percent(zhipu_week) : -1; + metrics->zhipu_5h_reset_mins = zhipu_5h_reset; + metrics->zhipu_week_reset_mins = zhipu_week_reset; + metrics->zhipu_today_tokens_m10 = zhipu_today_tokens_m10; + parse_zhipu_daily_bars(metrics, zhipu_daily_text); + metrics->zhipu_quota_percent = metrics->zhipu_5h_percent; + metrics->zhipu_latency_ms = -1; + set_cpu_core_text(metrics, core_text); + set_disk_text(metrics, cursor); + return true; +} + static bool parse_legacy_sys_line(const char *line, ui_metrics_t *metrics) { int cpu = 0; @@ -637,6 +782,10 @@ bool metrics_parse_line(const char *line, ui_metrics_t *metrics) memset(metrics, 0, sizeof(*metrics)); set_optional_defaults(metrics); + if (strncmp(line, "SYS8,", 5) == 0) { + return parse_sys8_line(line, metrics); + } + if (strncmp(line, "SYS7,", 5) == 0) { return parse_sys7_line(line, metrics); } diff --git a/main/ui_screen.c b/main/ui_screen.c index 402410f..0e2f704 100644 --- a/main/ui_screen.c +++ b/main/ui_screen.c @@ -17,6 +17,7 @@ #define CODEX_DAILY_BAR_GAP 5 #define CODEX_DAILY_BAR_BASE_Y 292 #define CODEX_DAILY_BAR_MAX_H 46 +#define ZHIPU_DAILY_BAR_BASE_X 210 typedef enum { CONN_USB = 0, @@ -56,7 +57,7 @@ static lv_obj_t *s_codex_today_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_today_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; @@ -65,6 +66,7 @@ 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 lv_obj_t *s_zhipu_daily_bars[UI_ZHIPU_DAILY_BAR_COUNT]; static int s_age_seconds; static connection_slot_t s_active_connection = CONN_USB; @@ -495,8 +497,12 @@ static void update_zhipu_panel(const ui_metrics_t *metrics) char reset[12]; char text[24]; - snprintf(text, sizeof(text), "%s", zhipu_status_text(metrics->zhipu_status)); - lv_label_set_text(s_zhipu_status_label, text); + if (metrics->zhipu_today_tokens_m10 >= 0) { + format_tokens_m10(metrics->zhipu_today_tokens_m10, text, sizeof(text)); + } else { + snprintf(text, sizeof(text), "%s", zhipu_status_text(metrics->zhipu_status)); + } + lv_label_set_text(s_zhipu_today_label, text); format_percent_compact(metrics->zhipu_5h_percent, value, sizeof(value)); lv_label_set_text(s_zhipu_5h_value_label, value); @@ -509,6 +515,26 @@ static void update_zhipu_panel(const ui_metrics_t *metrics) 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); + + for (int i = 0; i < UI_ZHIPU_DAILY_BAR_COUNT; i++) { + lv_obj_t *bar = s_zhipu_daily_bars[i]; + if (bar == NULL) { + continue; + } + + if (i >= metrics->zhipu_daily_count || metrics->zhipu_daily_percent[i] < 0) { + lv_obj_add_flag(bar, LV_OBJ_FLAG_HIDDEN); + continue; + } + + int percent = clamp_percent(metrics->zhipu_daily_percent[i]); + int height = percent <= 0 ? 1 : 1 + ((percent * (CODEX_DAILY_BAR_MAX_H - 1)) + 50) / 100; + int x = ZHIPU_DAILY_BAR_BASE_X + 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_cpu_core_labels(const char *core_text) @@ -723,10 +749,11 @@ void ui_screen_create(void) 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, "WAIT", &lv_font_montserrat_14); - lv_obj_set_width(s_zhipu_status_label, 78); - 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_today_label = make_label(screen, "--", &lv_font_montserrat_14); + lv_label_set_long_mode(s_zhipu_today_label, LV_LABEL_LONG_CLIP); + lv_obj_set_width(s_zhipu_today_label, 70); + lv_obj_set_style_text_align(s_zhipu_today_label, LV_TEXT_ALIGN_RIGHT, 0); + lv_obj_align(s_zhipu_today_label, LV_ALIGN_TOP_LEFT, 326, 184); s_zhipu_5h_label = make_label(screen, "5H", &lv_font_montserrat_14); lv_obj_set_width(s_zhipu_5h_label, 28); @@ -753,6 +780,22 @@ void ui_screen_create(void) 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, 224); + + make_rule(screen, 208, CODEX_DAILY_BAR_BASE_Y, 188, 1); + for (int i = 0; i < UI_ZHIPU_DAILY_BAR_COUNT; i++) { + s_zhipu_daily_bars[i] = lv_obj_create(screen); + lv_obj_set_size(s_zhipu_daily_bars[i], CODEX_DAILY_BAR_W, 1); + lv_obj_align(s_zhipu_daily_bars[i], LV_ALIGN_TOP_LEFT, + ZHIPU_DAILY_BAR_BASE_X + i * (CODEX_DAILY_BAR_W + CODEX_DAILY_BAR_GAP), + CODEX_DAILY_BAR_BASE_Y - 1); + lv_obj_set_style_bg_color(s_zhipu_daily_bars[i], ink(), 0); + lv_obj_set_style_bg_opa(s_zhipu_daily_bars[i], LV_OPA_COVER, 0); + lv_obj_set_style_border_width(s_zhipu_daily_bars[i], 0, 0); + lv_obj_set_style_radius(s_zhipu_daily_bars[i], 0, 0); + lv_obj_set_style_pad_all(s_zhipu_daily_bars[i], 0, 0); + lv_obj_clear_flag(s_zhipu_daily_bars[i], LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(s_zhipu_daily_bars[i], LV_OBJ_FLAG_HIDDEN); + } } void ui_screen_tick(void) diff --git a/main/ui_screen.h b/main/ui_screen.h index f9c97bd..deaecd0 100644 --- a/main/ui_screen.h +++ b/main/ui_screen.h @@ -7,6 +7,7 @@ extern "C" { #define UI_MAX_DISK_TEXT 128 #define UI_MAX_CPU_CORE_TEXT 48 #define UI_CODEX_DAILY_BAR_COUNT 14 +#define UI_ZHIPU_DAILY_BAR_COUNT UI_CODEX_DAILY_BAR_COUNT typedef struct { int cpu_percent; @@ -35,6 +36,9 @@ typedef struct { int zhipu_week_percent; int zhipu_5h_reset_mins; int zhipu_week_reset_mins; + int zhipu_today_tokens_m10; + int zhipu_daily_count; + int zhipu_daily_percent[UI_ZHIPU_DAILY_BAR_COUNT]; char cpu_core_text[UI_MAX_CPU_CORE_TEXT]; char disk_text[UI_MAX_DISK_TEXT]; } ui_metrics_t; diff --git a/tools/pc-metrics/push-metrics-ble.py b/tools/pc-metrics/push-metrics-ble.py index 9abeeb6..5691d09 100644 --- a/tools/pc-metrics/push-metrics-ble.py +++ b/tools/pc-metrics/push-metrics-ble.py @@ -11,6 +11,7 @@ import sys import threading import time import urllib.error +import urllib.parse import urllib.request from ctypes import wintypes @@ -26,6 +27,8 @@ CODEX_DAILY_BAR_COUNT = 14 CODEX_REFRESH_SECONDS = 60.0 CODEX_USAGE_REFRESH_SECONDS = 300.0 ZHIPU_REFRESH_SECONDS = 60.0 +ZHIPU_USAGE_REFRESH_SECONDS = 600.0 +ZHIPU_DAILY_BAR_COUNT = 14 def clamp_percent(value: float) -> int: @@ -627,19 +630,38 @@ class CodexRateLimitReader: class ZhipuStatusReader: - def __init__(self, refresh_seconds: float = ZHIPU_REFRESH_SECONDS) -> None: + def __init__(self, + refresh_seconds: float = ZHIPU_REFRESH_SECONDS, + usage_refresh_seconds: float = ZHIPU_USAGE_REFRESH_SECONDS) -> None: self.refresh_seconds = refresh_seconds - self.next_refresh = 0.0 - self.cached = (0, -1, -1, -1, -1) + self.usage_refresh_seconds = usage_refresh_seconds + self.next_quota_refresh = 0.0 + self.next_usage_refresh = 0.0 + self.quota_cached = (0, -1, -1, -1, -1) + self.usage_cached = (-1, "") @staticmethod - def _quota_url() -> str: + def _base_host() -> str: base_url = os.environ.get("ZHIPU_BASE_URL", "https://open.bigmodel.cn").strip() if "bigmodel.cn" in base_url.lower(): - host = "https://open.bigmodel.cn" - else: - host = "https://api.z.ai" - return f"{host}/api/monitor/usage/quota/limit" + return "https://open.bigmodel.cn" + return "https://api.z.ai" + + @classmethod + def _quota_url(cls) -> str: + return f"{cls._base_host()}/api/monitor/usage/quota/limit" + + @classmethod + def _model_usage_url(cls) -> str: + now = time.localtime() + end = time.strftime("%Y-%m-%d %H:%M:%S", now) + start_date = date.today() - timedelta(days=ZHIPU_DAILY_BAR_COUNT - 1) + start = f"{start_date.isoformat()} 00:00:00" + query = urllib.parse.urlencode({ + "startTime": start, + "endTime": end, + }) + return f"{cls._base_host()}/api/monitor/usage/model-usage?{query}" @staticmethod def _mins_until(epoch_ms) -> int: @@ -656,9 +678,48 @@ class ZhipuStatusReader: except (TypeError, ValueError): return -1 - def _query(self, api_key: str) -> tuple[int, int, int, int, int]: + @staticmethod + def _tokens_to_m10(tokens) -> int: + if not isinstance(tokens, (int, float)) or tokens < 0: + return -1 + return max(0, min(2147483647, int(round(tokens / 100000)))) + + @staticmethod + def _usage_to_daily_snapshot(data) -> tuple[int, str]: + if not isinstance(data, dict): + return (-1, "") + + x_time = data.get("x_time") + tokens_usage = data.get("tokensUsage") + if not isinstance(x_time, list) or not isinstance(tokens_usage, list): + return (-1, "") + + today = date.today() + wanted_days = [(today - timedelta(days=offset)).isoformat() for offset in range(ZHIPU_DAILY_BAR_COUNT - 1, -1, -1)] + tokens_by_day: dict[str, int] = {} + + for stamp, tokens in zip(x_time, tokens_usage): + if not isinstance(stamp, str) or len(stamp) < 10: + continue + if not isinstance(tokens, (int, float)) or tokens < 0: + continue + day = stamp[:10] + tokens_by_day[day] = tokens_by_day.get(day, 0) + int(tokens) + + values = [tokens_by_day.get(day, 0) for day in wanted_days] + max_value = max(values) if values else 0 + if max_value > 0: + percents = [max(0, min(100, int(round(value * 100 / max_value)))) for value in values] + else: + percents = [0 for _ in values] + + today_tokens_m10 = ZhipuStatusReader._tokens_to_m10(tokens_by_day.get(today.isoformat(), -1)) + return today_tokens_m10, ":".join(str(value) for value in percents) + + @staticmethod + def _read_json(url: str, api_key: str): request = urllib.request.Request( - self._quota_url(), + url, headers={ "Authorization": api_key, "Content-Type": "application/json", @@ -672,14 +733,19 @@ class ZhipuStatusReader: body = response.read().decode("utf-8", errors="replace") except urllib.error.HTTPError as exc: if exc.code in (401, 403): - return (2, -1, -1, -1, -1) - return (2, -1, -1, -1, -1) + return None + return None except OSError: - return (2, -1, -1, -1, -1) + return None try: - payload = json.loads(body) + return json.loads(body) except json.JSONDecodeError: + return None + + def _query_quota(self, api_key: str) -> tuple[int, int, int, int, int]: + payload = self._read_json(self._quota_url(), api_key) + if not isinstance(payload, dict): return (2, -1, -1, -1, -1) if payload.get("success") is False: @@ -714,20 +780,32 @@ class ZhipuStatusReader: return (1, five_percent, week_percent, five_reset, week_reset) - def read(self) -> tuple[int, int, int, int, int]: + def _query_usage(self, api_key: str) -> tuple[int, str]: + payload = self._read_json(self._model_usage_url(), api_key) + if not isinstance(payload, dict) or payload.get("success") is False: + return self.usage_cached + + return self._usage_to_daily_snapshot(payload.get("data")) + + def read(self) -> tuple[int, int, int, int, int, int, str]: api_key = os.environ.get("ZHIPU_API_KEY", "").strip() if not api_key: - self.cached = (0, -1, -1, -1, -1) - self.next_refresh = time.monotonic() + self.refresh_seconds - return self.cached + self.quota_cached = (0, -1, -1, -1, -1) + self.usage_cached = (-1, "") + self.next_quota_refresh = time.monotonic() + self.refresh_seconds + self.next_usage_refresh = time.monotonic() + self.usage_refresh_seconds + return (*self.quota_cached, *self.usage_cached) now = time.monotonic() - if now < self.next_refresh: - return self.cached + if now >= self.next_quota_refresh: + self.next_quota_refresh = now + self.refresh_seconds + self.quota_cached = self._query_quota(api_key) - self.next_refresh = now + self.refresh_seconds - self.cached = self._query(api_key) - return self.cached + if now >= self.next_usage_refresh: + self.next_usage_refresh = now + self.usage_refresh_seconds + self.usage_cached = self._query_usage(api_key) + + return (*self.quota_cached, *self.usage_cached) class MetricsSampler: @@ -844,15 +922,16 @@ class MetricsSampler: disk_count, disk_percent, disk_text = self._disk_summary(disk_rates) codex_5h, codex_week, codex_5h_reset, codex_week_reset, codex_credits = self.codex.read() codex_today_tokens_m10, codex_daily_bars = self.codex.read_usage_snapshot() - zhipu_status, zhipu_5h, zhipu_week, zhipu_5h_reset, zhipu_week_reset = self.zhipu.read() + zhipu_status, zhipu_5h, zhipu_week, zhipu_5h_reset, zhipu_week_reset, zhipu_today_tokens_m10, zhipu_daily_bars = self.zhipu.read() return ( - f"SYS7,{cpu},{cpu_mhz},{mem_percent},{mem_used_mb},{mem_total_mb}," + f"SYS8,{cpu},{cpu_mhz},{mem_percent},{mem_used_mb},{mem_total_mb}," f"{disk_count},{disk_percent},{disk_read_kbps},{disk_write_kbps}," f"{rx_kbps},{tx_kbps}," f"{codex_5h},{codex_week},{codex_5h_reset},{codex_week_reset},{codex_credits},{codex_today_tokens_m10}," f"{codex_daily_bars}," - f"{zhipu_status},{zhipu_5h},{zhipu_week},{zhipu_5h_reset},{zhipu_week_reset},{cpu_core_text},{disk_text}" + f"{zhipu_status},{zhipu_5h},{zhipu_week},{zhipu_5h_reset},{zhipu_week_reset},{zhipu_today_tokens_m10}," + f"{zhipu_daily_bars},{cpu_core_text},{disk_text}" )