# CodexBar-ESP32 — Feature Spec

## Overview

A standalone hardware display for Claude (and other LLM) usage quotas, running on an IdeaSpark ESP32 with integrated 1.9" ST7789 TFT LCD (170×320). Inspired by the macOS battery widget's clean arc-gauge aesthetic.

The device connects to WiFi, polls a lightweight Mac companion proxy for usage data (which in turn calls the Anthropic OAuth API using the local Claude CLI credentials), and renders arc-gauge widgets showing utilization and time-until-reset for each rate window.

---

## Architecture

```
Mac (companion proxy)                   ESP32 (display device)
┌────────────────────────────┐          ┌──────────────────────────┐
│ companion/server.py        │  HTTP    │ WiFi poll every 5 min    │
│  - reads ~/.claude keychain│ ◄──────► │ Renders arc gauges       │
│  - calls api.anthropic.com │          │ ST7789 170×320 display   │
│  - serves /usage JSON      │          └──────────────────────────┘
└────────────────────────────┘
```

The companion proxy is necessary because:
1. The ESP32 cannot access the macOS keychain for OAuth tokens
2. The OAuth access token needs periodic refresh (handled on the Mac)
3. The proxy provides a stable, simple HTTP interface regardless of upstream API changes

---

## Data Model

From `api.anthropic.com/api/oauth/usage` (via CodexBar's `ClaudeOAuthUsageFetcher`):

```json
{
  "five_hour": { "utilization": 0.42, "resets_at": "2026-05-25T22:00:00Z" },
  "seven_day":  { "utilization": 0.15, "resets_at": "2026-06-01T00:00:00Z" },
  "seven_day_opus": { "utilization": 0.80, "resets_at": "2026-06-01T00:00:00Z" }
}
```

Companion proxy `/usage` response (simplified for ESP32):

```json
{
  "ok": true,
  "updated_at": 1748210000,
  "windows": [
    { "label": "5h",  "pct": 42, "resets_in_sec": 7200 },
    { "label": "7d",  "pct": 15, "resets_in_sec": 518400 },
    { "label": "Opus","pct": 80, "resets_in_sec": 518400 }
  ]
}
```

---

## Display Layout (Portrait — 170×320)

```
┌──────────────┐  170px wide
│  CLAUDE  ●   │  header: provider name + status dot (green=ok, red=err)
│              │
│    ╭────╮    │
│   /  42% \   │  arc gauge — filled arc, color-coded
│  │  used  │  │  center: pct + "used" label
│   \      /   │
│    ╰────╯    │
│  resets 2h   │  time until reset
│   5-hr limit │  window label
│──────────────│  divider
│    ╭────╮    │
│   /  15% \   │
│  │  used  │  │
│   \      /   │
│    ╰────╯    │
│  resets 6d   │
│   7-day limit│
│──────────────│
│ Updated 2:34 │  footer: last refresh time
└──────────────┘
```

Color scale (arc fill color):
- 0–49%: Green `#34C759`
- 50–74%: Yellow `#FFD60A`
- 75–89%: Orange `#FF9F0A`
- 90–100%: Red `#FF3B30`

Background: near-black `#1C1C1E` (Apple dark UI)
Text: white `#FFFFFF` and secondary gray `#8E8E93`

---

## Rate Windows

| Window | Provider | Reset cycle | Notes |
|--------|----------|-------------|-------|
| 5-hour | Claude   | Rolling 5h  | Primary coding quota |
| 7-day  | Claude   | Weekly      | Overall usage cap |
| 7-day Opus | Claude | Weekly   | Shown if present |

Future: OpenAI, Gemini (once companion supports them)

---

## Companion Proxy

File: `companion/server.py`

- Reads OAuth access token from macOS keychain (`security find-generic-password -s "Claude Code-credentials" -w`)
- Parses JSON, extracts `claudeAiOauth.accessToken`
- Calls `https://api.anthropic.com/api/oauth/usage` with `anthropic-beta: oauth-2025-04-20`
- Caches response 60s, serves on `http://0.0.0.0:7842/usage`
- Token auto-refresh: uses `refreshToken` + `https://platform.claude.com/v1/oauth/token` when 401

---

## Hardware

**Board:** IdeaSpark ESP32 Development Board 16MB  
**Display:** Integrated 1.9" ST7789 TFT, 170×320 pixels  
**USB:** CH340 driver, USB Type-C  
**Connectivity:** WiFi 802.11 b/g/n  

**TFT_eSPI pin config** (IdeaSpark-specific — verify against board silkscreen):
```cpp
#define TFT_MOSI  23
#define TFT_SCLK  18
#define TFT_CS    5
#define TFT_DC    2
#define TFT_RST   4
#define TFT_BL    21   // backlight
#define TFT_WIDTH  170
#define TFT_HEIGHT 320
```

---

## Build Phases

### Phase 1 — Hardware Bringup
**Goal:** Confirm display works with correct pin config.  
**Deliverables:**
- `User_Setup_IdeaSpark.h` — TFT_eSPI pin config
- `codexbar_esp32.ino` — hello world: fills screen with dark bg, draws "CodexBar" header, two placeholder arc outlines
- Verified on physical hardware

**Libraries:**
- TFT_eSPI (Bodmer)
- ArduinoJson 7

---

### Phase 2 — WiFi + Companion Proxy
**Goal:** Live data flowing from Claude API to display (as text, no fancy graphics yet).  
**Deliverables:**
- `companion/server.py` — HTTP server with `/usage` endpoint
- `companion/requirements.txt`
- ESP32 sketch updated: WiFi connection, HTTP GET `/usage`, display raw `pct` + `resets_in_sec` as text
- `companion/README.md` — setup instructions (install deps, run, configure ESP32 with IP)

---

### Phase 3 — Arc Gauge Graphics
**Goal:** Full Apple-inspired widget UI.  
**Deliverables:**
- `drawArcGauge(cx, cy, r, pct, color)` — anti-aliased arc using TFT_eSPI's `drawArc`
- Color mapping function based on utilization %
- `formatTimeRemaining(sec)` → "2h 15m" / "3d 4h"
- Two widget panels stacked vertically, status dot, footer timestamp
- Screenshot / photo verification

---

### Phase 4 — Polish
**Goal:** Robust, always-on device.  
**Deliverables:**
- Auto-refresh every 5 minutes with non-blocking timer
- Error state screen (WiFi lost / proxy unreachable / API error)
- Graceful degradation: show last known data + "stale" indicator after 15 min
- Smooth animation: arc "fills in" on first draw after data arrives
- Optional: cycle between "% used" and "% remaining" views every 30s

---

## Config (in sketch, top of file)

```cpp
const char* WIFI_SSID     = "YourSSID";
const char* WIFI_PASS     = "YourPassword";
const char* PROXY_HOST    = "192.168.1.X";  // Mac's local IP
const int   PROXY_PORT    = 7842;
const int   REFRESH_SECS  = 300;           // 5 minutes
```

---

## Open Questions

1. **Remaining vs. Used:** Start with "% used" (matches battery widget convention where bar depletes). If it feels wrong after seeing it on hardware, flip to "% remaining".
2. **Portrait vs. Landscape:** Spec assumes portrait. Can flip if arc gauges look better side-by-side in landscape.
3. **Token refresh on proxy:** First implementation uses access token directly; if it expires (typically 1h) the proxy returns a stale/error state until the Mac re-authenticates. Phase 2 adds refresh logic.
