Files
clawdmeter/daemon/claude-usage-daemon.sh
T
Hermann Björgvin HaraldssonandClaude Opus 4.6 2611c70881 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>
2026-04-01 02:37:11 +00:00

129 lines
4.1 KiB
Bash
Executable File

#!/bin/bash
# Claude Usage Tracker Daemon
# Reads Claude Code OAuth token, polls usage via API, sends to ESP32 over serial.
# Survives USB plug/unplug cycles using inotifywait for instant device detection.
# Dependencies: curl, awk, inotify-tools
SERIAL_PORT="${SERIAL_PORT:-/dev/ttyACM0}"
SERIAL_DEV=$(basename "$SERIAL_PORT")
POLL_INTERVAL=30
BAUD=115200
SERIAL_FD=3
log() {
echo "[$(date '+%H:%M:%S')] $1"
}
read_token() {
grep -o '"accessToken":"[^"]*"' "$HOME/.claude/.credentials.json" | cut -d'"' -f4
}
device_connected() {
[ -e "$SERIAL_PORT" ]
}
open_serial() {
stty -F "$SERIAL_PORT" "$BAUD" raw -echo -echoe -echok 2>/dev/null || return 1
eval "exec ${SERIAL_FD}<>\"$SERIAL_PORT\"" 2>/dev/null || return 1
return 0
}
close_serial() {
eval "exec ${SERIAL_FD}>&-" 2>/dev/null
}
# Wait for device to appear using inotifywait (blocks, zero CPU)
wait_for_device() {
while ! device_connected; do
inotifywait -qq -e create /dev --include "$SERIAL_DEV" 2>/dev/null || sleep 1
done
}
poll() {
local token
token=$(read_token) || { log "Error: could not read token"; return 1; }
local now
now=$(date +%s)
local headers
headers=$(curl -s -D - -o /dev/null \
"https://api.anthropic.com/v1/messages" \
-H "Authorization: Bearer $token" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: oauth-2025-04-20" \
-H "Content-Type: application/json" \
-H "User-Agent: claude-code/2.1.5" \
-d '{"model":"claude-haiku-4-5-20251001","max_tokens":1,"messages":[{"role":"user","content":"hi"}]}' \
2>/dev/null) || { log "Error: API call failed"; return 1; }
local s5h_util s5h_reset s7d_util s7d_reset status
s5h_util=$(echo "$headers" | grep -i "anthropic-ratelimit-unified-5h-utilization" | tr -d '\r' | awk '{print $2}')
s5h_reset=$(echo "$headers" | grep -i "anthropic-ratelimit-unified-5h-reset" | tr -d '\r' | awk '{print $2}')
s7d_util=$(echo "$headers" | grep -i "anthropic-ratelimit-unified-7d-utilization" | tr -d '\r' | awk '{print $2}')
s7d_reset=$(echo "$headers" | grep -i "anthropic-ratelimit-unified-7d-reset" | tr -d '\r' | awk '{print $2}')
status=$(echo "$headers" | grep -i "anthropic-ratelimit-unified-5h-status" | tr -d '\r' | awk '{print $2}')
s5h_util=${s5h_util:-0}
s5h_reset=${s5h_reset:-0}
s7d_util=${s7d_util:-0}
s7d_reset=${s7d_reset:-0}
status=${status:-unknown}
# Build JSON payload in a single awk process (utilization * 100, timestamps to minutes)
local payload
payload=$(awk -v u5="$s5h_util" -v r5="$s5h_reset" -v u7="$s7d_util" -v r7="$s7d_reset" -v st="$status" -v now="$now" \
'BEGIN {
sp = sprintf("%.0f", u5 * 100);
sr = (r5 - now) / 60; sr = sr > 0 ? sprintf("%.0f", sr) : 0;
wp = sprintf("%.0f", u7 * 100);
wr = (r7 - now) / 60; wr = wr > 0 ? sprintf("%.0f", wr) : 0;
printf "{\"s\":%s,\"sr\":%s,\"w\":%s,\"wr\":%s,\"st\":\"%s\",\"ok\":true}", sp, sr, wp, wr, st;
}')
if ! echo "$payload" >&${SERIAL_FD} 2>/dev/null; then
log "Write failed - device disconnected"
return 1
fi
}
cleanup() {
close_serial
log "Daemon stopped"
exit 0
}
trap cleanup INT TERM
log "=== Claude Usage Tracker Daemon ==="
log "Serial port: $SERIAL_PORT"
log "Poll interval: ${POLL_INTERVAL}s"
while true; do
# Wait for device (instant detection via inotify, or already plugged in)
if ! device_connected; then
log "Waiting for device..."
wait_for_device
fi
log "Device connected"
sleep 2 # wait for ESP32 boot
if ! open_serial; then
log "Failed to open serial port"
sleep 2
continue
fi
log "Serial port opened"
# Poll loop - polls API every POLL_INTERVAL, exits on disconnect or write failure
while device_connected; do
poll || break
# Sleep POLL_INTERVAL but wake instantly if device disappears
inotifywait -qq -e delete /dev --include "$SERIAL_DEV" -t "$POLL_INTERVAL" 2>/dev/null
done
# Disconnected - clean up and loop back to wait
log "Device disconnected"
close_serial
done