v2 Phase 4: Session screen — today's tokens and equivalent cost

Daemon: compute_today_usage() sums today's Claude Code token usage across the
local project transcripts (~/.claude/projects/**/*.jsonl), reading only files
modified today so the scan stays cheap, and prices it with Anthropic list rates
to show the equivalent API cost (the ccusage-style flex for subscription users).
The compact fields tk (total tokens), tc (cost in cents) and tn (message count)
piggyback onto the existing 60s BLE payload.

Firmware: UsageData gains tokens_today (64-bit) / cost_cents_today /
messages_today; parse_json reads tk/tc/tn; a new Session screen shows the cost
as the hero with total tokens and message count below, refreshed from ui_update
so it's current whenever opened. Wire SCREEN_SESSION to the real screen (was a
stub). Verified end to end: daemon sends e.g. tk=105374109 tc=26838 tn=663.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wenil
2026-06-20 15:03:23 +03:00
co-authored by Claude Opus 4.8
parent abe72eca0a
commit 56b49898a3
4 changed files with 159 additions and 1 deletions
+63 -1
View File
@@ -129,6 +129,10 @@ static lv_obj_t* bt_container; // BLE connection info
static lv_obj_t* bt_status_lbl;
static lv_obj_t* bt_name_lbl;
static lv_obj_t* bt_mac_lbl;
static lv_obj_t* session_container; // today's tokens / cost
static lv_obj_t* sess_cost_lbl;
static lv_obj_t* sess_tokens_lbl;
static lv_obj_t* sess_msgs_lbl;
// App registry — the launcher renders one tile per entry, so adding a screen is
// one line here plus its builder. Order = tile order. "Animations" reuses the
@@ -237,6 +241,13 @@ static void format_reset_time(int mins, char* buf, size_t len) {
}
}
// Format a token count compactly: 94972115 -> "95.0M", 960621 -> "960.6K".
static void format_tokens(long long n, char* buf, size_t len) {
if (n >= 1000000) snprintf(buf, len, "%.1fM", (double)n / 1000000.0);
else if (n >= 1000) snprintf(buf, len, "%.1fK", (double)n / 1000.0);
else snprintf(buf, len, "%lld", n);
}
// Forward decls — callbacks defined near ui_show_screen below
static void global_click_cb(lv_event_t* e);
static void logo_click_cb(lv_event_t* e);
@@ -676,6 +687,46 @@ static void init_soundpad_screen(lv_obj_t* scr) {
lv_obj_add_flag(soundpad_container, LV_OBJ_FLAG_HIDDEN);
}
// Session: today's Claude Code usage — equivalent API cost (hero), total tokens,
// and message count. Labels are refreshed from ui_update() as data arrives.
static void init_session_screen(lv_obj_t* scr) {
session_container = lv_obj_create(scr);
lv_obj_set_size(session_container, L.scr_w, L.scr_h);
lv_obj_set_pos(session_container, 0, 0);
lv_obj_set_style_bg_opa(session_container, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(session_container, 0, 0);
lv_obj_set_style_pad_all(session_container, 0, 0);
lv_obj_clear_flag(session_container, LV_OBJ_FLAG_SCROLLABLE);
make_screen_title(session_container, "Session");
sess_cost_lbl = lv_label_create(session_container);
lv_label_set_text(sess_cost_lbl, "$0.00");
lv_obj_set_style_text_font(sess_cost_lbl, &font_tiempos_56, 0);
lv_obj_set_style_text_color(sess_cost_lbl, COL_TEXT, 0);
lv_obj_align(sess_cost_lbl, LV_ALIGN_TOP_MID, 0, L.content_y + 30);
lv_obj_t* sub = lv_label_create(session_container);
lv_label_set_text(sub, "spent today");
lv_obj_set_style_text_font(sub, &font_styrene_20, 0);
lv_obj_set_style_text_color(sub, COL_DIM, 0);
lv_obj_align(sub, LV_ALIGN_TOP_MID, 0, L.content_y + 110);
sess_tokens_lbl = lv_label_create(session_container);
lv_label_set_text(sess_tokens_lbl, "\xE2\x80\x94 tokens");
lv_obj_set_style_text_font(sess_tokens_lbl, &font_styrene_28, 0);
lv_obj_set_style_text_color(sess_tokens_lbl, COL_ACCENT, 0);
lv_obj_align(sess_tokens_lbl, LV_ALIGN_TOP_MID, 0, L.content_y + 165);
sess_msgs_lbl = lv_label_create(session_container);
lv_label_set_text(sess_msgs_lbl, "\xE2\x80\x94 messages");
lv_obj_set_style_text_font(sess_msgs_lbl, &font_styrene_20, 0);
lv_obj_set_style_text_color(sess_msgs_lbl, COL_DIM, 0);
lv_obj_align(sess_msgs_lbl, LV_ALIGN_TOP_MID, 0, L.content_y + 215);
lv_obj_add_flag(session_container, LV_OBJ_FLAG_HIDDEN);
}
// ======== Public API ========
void ui_init(void) {
@@ -700,6 +751,7 @@ void ui_init(void) {
init_stub_screen(scr);
init_bt_screen(scr);
init_soundpad_screen(scr);
init_session_screen(scr);
logo_img = lv_image_create(scr);
lv_image_set_src(logo_img, &logo_dsc);
@@ -746,6 +798,15 @@ void ui_update(const UsageData* data) {
format_reset_time(data->weekly_reset_mins, buf, sizeof(buf));
lv_label_set_text(lbl_weekly_reset, buf);
// Session screen — refreshed here so it's current whenever it's opened.
if (sess_cost_lbl) {
lv_label_set_text_fmt(sess_cost_lbl, "$%d.%02d",
data->cost_cents_today / 100, data->cost_cents_today % 100);
format_tokens(data->tokens_today, buf, sizeof(buf));
lv_label_set_text_fmt(sess_tokens_lbl, "%s tokens", buf);
lv_label_set_text_fmt(sess_msgs_lbl, "%d messages", data->messages_today);
}
}
// Pick the usage-view sub-screen: pairing hint (BLE down), the idle "Zzz" screen
@@ -854,6 +915,7 @@ void ui_show_screen(screen_t screen) {
if (stub_container) lv_obj_add_flag(stub_container, LV_OBJ_FLAG_HIDDEN);
if (bt_container) lv_obj_add_flag(bt_container, LV_OBJ_FLAG_HIDDEN);
if (soundpad_container) lv_obj_add_flag(soundpad_container, LV_OBJ_FLAG_HIDDEN);
if (session_container) lv_obj_add_flag(session_container, LV_OBJ_FLAG_HIDDEN);
splash_hide();
switch (screen) {
@@ -861,8 +923,8 @@ void ui_show_screen(screen_t screen) {
case SCREEN_USAGE: lv_obj_clear_flag(usage_container, LV_OBJ_FLAG_HIDDEN); break;
case SCREEN_MENU: lv_obj_clear_flag(menu_container, LV_OBJ_FLAG_HIDDEN); break;
case SCREEN_SOUNDPAD: lv_obj_clear_flag(soundpad_container, LV_OBJ_FLAG_HIDDEN); break;
case SCREEN_SESSION: lv_obj_clear_flag(session_container, LV_OBJ_FLAG_HIDDEN); break;
case SCREEN_BLUETOOTH: bt_refresh(); lv_obj_clear_flag(bt_container, LV_OBJ_FLAG_HIDDEN); break;
case SCREEN_SESSION:
case SCREEN_NOWPLAYING:
case SCREEN_HOMEASSIST:
stub_show_for(screen);