Initial commit: Claude Usage Tracker for Panlee SC01 Plus

Physical desk monitor that displays Claude Code usage limits (5-hour session
and 7-day weekly utilization) on an ESP32-S3 touchscreen via USB serial.

Firmware: LVGL 9 dashboard with LovyanGFX on SC01 Plus, Anthropic brand
colors and custom fonts (Tiempos, Styrene B, DejaVu Sans Mono), Claude
spinner animation with rotating status words.

Daemon: Pure bash script that reads the Claude Code OAuth token, makes a
minimal Haiku API call, extracts usage from rate-limit response headers,
and sends JSON to the ESP32 over serial. Uses inotifywait for instant
USB plug/unplug detection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hermann Björgvin Haraldsson
2026-04-01 02:37:11 +00:00
co-authored by Claude Opus 4.6
commit 2611c70881
23 changed files with 14394 additions and 0 deletions
+126
View File
@@ -0,0 +1,126 @@
#include <Arduino.h>
#include <lvgl.h>
#include <ArduinoJson.h>
#include "display_cfg.h"
#include "data.h"
#include "ui.h"
static LGFX lcd;
static UsageData usage = {};
// LVGL draw buffers
#define BUF_LINES 40
static uint16_t buf1[480 * BUF_LINES];
static uint16_t buf2[480 * BUF_LINES];
// Serial line buffer
#define SERIAL_BUF_SIZE 512
static char serial_buf[SERIAL_BUF_SIZE];
static int serial_pos = 0;
// LVGL tick callback
static uint32_t my_tick(void) {
return millis();
}
// LVGL flush callback
static void my_flush_cb(lv_display_t* disp, const lv_area_t* area, uint8_t* px_map) {
uint32_t w = (area->x2 - area->x1 + 1);
uint32_t h = (area->y2 - area->y1 + 1);
lcd.startWrite();
lcd.setAddrWindow(area->x1, area->y1, w, h);
lcd.writePixels((uint16_t*)px_map, w * h, true);
lcd.endWrite();
lv_display_flush_ready(disp);
}
// LVGL touch callback
static void my_touch_cb(lv_indev_t* indev, lv_indev_data_t* data) {
uint16_t x, y;
if (lcd.getTouch(&x, &y)) {
data->point.x = x;
data->point.y = y;
data->state = LV_INDEV_STATE_PRESSED;
} else {
data->state = LV_INDEV_STATE_RELEASED;
}
}
// Parse a JSON line into UsageData
static bool parse_json(const char* json, UsageData* out) {
JsonDocument doc;
DeserializationError err = deserializeJson(doc, json);
if (err) {
Serial.printf("JSON parse error: %s\n", err.c_str());
return false;
}
out->session_pct = doc["s"] | 0.0f;
out->session_reset_mins = doc["sr"] | -1;
out->weekly_pct = doc["w"] | 0.0f;
out->weekly_reset_mins = doc["wr"] | -1;
strlcpy(out->status, doc["st"] | "unknown", sizeof(out->status));
out->ok = doc["ok"] | false;
out->valid = true;
return true;
}
// Read serial data, returns true when a complete line is ready
static bool read_serial_line() {
while (Serial.available()) {
char c = Serial.read();
if (c == '\n') {
serial_buf[serial_pos] = '\0';
serial_pos = 0;
return true;
}
if (serial_pos < SERIAL_BUF_SIZE - 1) {
serial_buf[serial_pos++] = c;
}
}
return false;
}
void setup() {
Serial.begin(115200);
delay(300);
Serial.println("{\"ready\":true}");
// Init display
lcd.init();
lcd.setRotation(1);
lcd.setBrightness(200);
lcd.fillScreen(TFT_BLACK);
// Init LVGL
lv_init();
lv_tick_set_cb(my_tick);
lv_display_t* disp = lv_display_create(480, 320);
lv_display_set_color_format(disp, LV_COLOR_FORMAT_RGB565);
lv_display_set_flush_cb(disp, my_flush_cb);
lv_display_set_buffers(disp, buf1, buf2, sizeof(buf1), LV_DISPLAY_RENDER_MODE_PARTIAL);
lv_indev_t* indev = lv_indev_create();
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(indev, my_touch_cb);
// Build dashboard
ui_init();
Serial.println("Dashboard ready, waiting for data on serial...");
}
void loop() {
lv_timer_handler();
ui_tick_anim();
if (read_serial_line()) {
if (parse_json(serial_buf, &usage)) {
ui_update(&usage);
Serial.println("{\"ack\":true}");
}
}
delay(5);
}