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
+9
View File
@@ -0,0 +1,9 @@
# PlatformIO build artifacts
firmware/.pio/
firmware/.vscode/
# OS
.DS_Store
# Claude Code
.claude/
+96
View File
@@ -0,0 +1,96 @@
# Claude Usage Tracker - Panlee SC01 Plus
Displays your Claude Code usage limits on a Panlee SC01 Plus (ESP32-S3) 3.5" touchscreen.
![Demo](assets/demo.gif)
Shows your 5-hour session and 7-day weekly utilization.
## Hardware
- [Panlee SC01 Plus](http://en.smartpanle.com/product-item-15.html) (WT32-SC01 Plus) — ESP32-S3, 3.5" 480x320 IPS, capacitive touch
- USB-C cable for power and data)
## Prerequisites
- Linux (tested on my Ubuntu laptop)
- [PlatformIO CLI](https://docs.platformio.org/en/latest/core/installation/index.html)
- `curl`, `awk`, `inotify-tools` (pre-installed on most Linux distros, install `inotify-tools` if missing)
- Claude Code
- User in `dialout` group: `sudo adduser $USER dialout` (log out/in after)
## Flash the firmware
```bash
cd firmware
pio run -t upload
```
The SC01 Plus appears at `/dev/ttyACM0`.
## Run the daemon
The daemon polls your Claude usage every 30 seconds and sends it to the display over USB serial.
```bash
bash daemon/claude-usage-daemon.sh
```
## Install the systemd service
To auto-start the daemon on login:
```bash
./install.sh
```
Check status: `systemctl --user status claude-usage-daemon`
## How it works
1. The daemon reads your Claude Code OAuth token from `~/.claude/.credentials.json`
2. Makes a minimal API call to `api.anthropic.com/v1/messages` (1 token of Haiku, essentially free)
3. Extracts usage data from the response headers (`anthropic-ratelimit-unified-5h-utilization`, etc.)
4. Sends a JSON line over USB serial to the ESP32
5. The ESP32 parses it and updates the LVGL dashboard
## Recompiling fonts
The `firmware/src/font_*.c` files are pre-compiled LVGL bitmap fonts. If you need to regenerate them (e.g. to change sizes or add characters):
```bash
npm install -g lv_font_conv
```
Generate with `--no-compress` (required for LVGL 9):
```bash
# Tiempos Text (title)
lv_font_conv --font assets/TiemposText-400-Regular.otf -r 0x20-0x7E \
--size 34 --format lvgl --bpp 4 --no-compress \
-o firmware/src/font_tiempos_34.c --lv-include "lvgl.h"
# Styrene B (UI text)
lv_font_conv --font assets/StyreneB-Regular.otf -r 0x20-0x7E \
--size 28 --format lvgl --bpp 4 --no-compress \
-o firmware/src/font_styrene_28.c --lv-include "lvgl.h"
# DejaVu Sans Mono (animation, with spinner Unicode chars)
lv_font_conv --font assets/DejaVuSansMono.ttf \
-r 0x20-0x7E,0xB7,0x2026,0x2722,0x2733,0x2736,0x273B,0x273D \
--size 18 --format lvgl --bpp 4 --no-compress \
-o firmware/src/font_mono_18.c --lv-include "lvgl.h"
```
**Important:** `lv_font_conv` v1.5.3 outputs LVGL 8 format. Each generated file must be patched for LVGL 9 compatibility:
1. Remove `#if LVGL_VERSION_MAJOR >= 8` guards around `font_dsc` and the font struct
2. Remove the `.cache` field from `font_dsc`
3. Add `.release_glyph = NULL`, `.kerning = 0`, `.static_bitmap = 0` to the font struct
4. Add `.fallback = NULL`, `.user_data = NULL` to the font struct
Without these patches, fonts compile but render as invisible.
## Licensing gray area warning
The software in this repository uses and adheres to the Anthropic brand guidelines and uses the same proprietary fonts that Anthropic has a licnese for but this software uses without permission as well as using assets from Anthropic such as the copyrighted Claude Code mascot head logo so even though the code in this repo is non-proprietary I will not license it myself under a copyleft license since this repo includes proprietary fonts and copyrighted assets. Please be aware of this if you fork or copy the code from this repo. **You have been warned!**
Binary file not shown.

After

Width:  |  Height:  |  Size: 280 B

Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 MiB

+11
View File
@@ -0,0 +1,11 @@
[Unit]
Description=Claude Usage Tracker Daemon
[Service]
Type=simple
ExecStart=DAEMON_PATH
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
+128
View File
@@ -0,0 +1,128 @@
#!/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
+35
View File
@@ -0,0 +1,35 @@
[env:sc01plus]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
upload_speed = 921600
monitor_speed = 115200
build_flags =
-DARDUINO_USB_CDC_ON_BOOT=1
-DBOARD_HAS_PSRAM
; LVGL config via build flags
-DLV_CONF_SKIP
-DLV_COLOR_DEPTH=16
-DLV_USE_LOG=0
-DLV_FONT_MONTSERRAT_12=1
-DLV_FONT_MONTSERRAT_14=1
-DLV_FONT_MONTSERRAT_16=1
-DLV_FONT_MONTSERRAT_20=1
-DLV_FONT_MONTSERRAT_24=1
-DLV_FONT_MONTSERRAT_28=1
-DLV_USE_BAR=1
-DLV_USE_LABEL=1
-DLV_USE_ARC=1
-DLV_USE_BTN=1
-DLV_USE_LINE=1
-DLV_USE_OBJ=1
-DLV_USE_IMG=1
-DLV_USE_IMAGE=1
-DLV_USE_ANIMIMG=0
-DLV_TICK_CUSTOM=1
lib_deps =
lovyan03/LovyanGFX@^1.1.16
lvgl/lvgl@^9.2.0
bblanchon/ArduinoJson@^7.0.0
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include <Arduino.h>
struct UsageData {
float session_pct; // 5-hour window utilization (0-100)
int session_reset_mins; // minutes until session resets
float weekly_pct; // 7-day window utilization (0-100)
int weekly_reset_mins; // minutes until weekly resets
char status[16]; // "allowed" or "limited"
bool ok; // data parse succeeded
bool valid; // false until first successful parse
};
+93
View File
@@ -0,0 +1,93 @@
#pragma once
#define LGFX_USE_V1
#include <LovyanGFX.hpp>
class LGFX : public lgfx::LGFX_Device {
// 8-bit parallel bus for ST7796 display
lgfx::Bus_Parallel8 _bus_instance;
// ST7796 display panel
lgfx::Panel_ST7796 _panel_instance;
// PWM backlight
lgfx::Light_PWM _light_instance;
// FT6336U capacitive touch (FT5x06 compatible)
lgfx::Touch_FT5x06 _touch_instance;
public:
LGFX(void) {
// ---- Bus configuration ----
{
auto cfg = _bus_instance.config();
cfg.port = 0;
cfg.freq_write = 40000000; // 40 MHz
cfg.pin_wr = 47;
cfg.pin_rd = -1; // not connected
cfg.pin_rs = 0; // DC (data/command)
cfg.pin_d0 = 9;
cfg.pin_d1 = 46;
cfg.pin_d2 = 3;
cfg.pin_d3 = 8;
cfg.pin_d4 = 18;
cfg.pin_d5 = 17;
cfg.pin_d6 = 16;
cfg.pin_d7 = 15;
_bus_instance.config(cfg);
_panel_instance.setBus(&_bus_instance);
}
// ---- Panel configuration ----
{
auto cfg = _panel_instance.config();
cfg.pin_cs = -1; // directly driven
cfg.pin_rst = 4;
cfg.pin_busy = -1;
cfg.memory_width = 320;
cfg.memory_height = 480;
cfg.panel_width = 320;
cfg.panel_height = 480;
cfg.offset_x = 0;
cfg.offset_y = 0;
cfg.offset_rotation = 0;
cfg.dummy_read_pixel = 8;
cfg.dummy_read_bits = 1;
cfg.readable = false;
cfg.invert = true;
cfg.rgb_order = false;
cfg.dlen_16bit = false;
cfg.bus_shared = true;
_panel_instance.config(cfg);
}
// ---- Backlight configuration ----
{
auto cfg = _light_instance.config();
cfg.pin_bl = 45;
cfg.invert = false;
cfg.freq = 44100;
cfg.pwm_channel = 7;
_light_instance.config(cfg);
_panel_instance.setLight(&_light_instance);
}
// ---- Touch configuration ----
{
auto cfg = _touch_instance.config();
cfg.x_min = 0;
cfg.x_max = 319;
cfg.y_min = 0;
cfg.y_max = 479;
cfg.pin_int = 7;
cfg.bus_shared = false;
cfg.offset_rotation = 0;
cfg.i2c_port = 1;
cfg.i2c_addr = 0x38;
cfg.pin_sda = 6;
cfg.pin_scl = 5;
cfg.freq = 400000;
_touch_instance.config(cfg);
_panel_instance.setTouch(&_touch_instance);
}
setPanel(&_panel_instance);
}
};
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+152
View File
@@ -0,0 +1,152 @@
#pragma once
#include <stdint.h>
#define LOGO_WIDTH 48
#define LOGO_HEIGHT 48
static const uint16_t logo_data[2304] = {
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA,
0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0xDBAA,
0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA,
0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0xDBAA,
0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA,
0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0xDBAA,
0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA,
0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0xDBAA,
0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA,
0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0xDBAA,
0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA,
0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0xDBAA,
0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA,
0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0xDBAA, 0xDBAA, 0xDBAA, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2, 0x10A2,
};
+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);
}
+272
View File
@@ -0,0 +1,272 @@
#include "ui.h"
#include <lvgl.h>
#include "logo.h"
// Custom fonts
LV_FONT_DECLARE(font_tiempos_34);
LV_FONT_DECLARE(font_styrene_28);
LV_FONT_DECLARE(font_styrene_16);
LV_FONT_DECLARE(font_styrene_14);
LV_FONT_DECLARE(font_styrene_12);
LV_FONT_DECLARE(font_mono_18);
// Anthropic brand palette
#define COL_BG lv_color_hex(0x141413)
#define COL_PANEL lv_color_hex(0x1f1f1e)
#define COL_TEXT lv_color_hex(0xfaf9f5)
#define COL_DIM lv_color_hex(0xb0aea5)
#define COL_ACCENT lv_color_hex(0xd97757)
#define COL_GREEN lv_color_hex(0x788c5d)
#define COL_AMBER lv_color_hex(0xd97757)
#define COL_RED lv_color_hex(0xc0392b)
#define COL_BAR_BG lv_color_hex(0x2a2a28)
// Widget handles
static lv_obj_t* lbl_title;
static lv_obj_t* bar_session;
static lv_obj_t* lbl_session_pct;
static lv_obj_t* lbl_session_label;
static lv_obj_t* lbl_session_reset;
static lv_obj_t* bar_weekly;
static lv_obj_t* lbl_weekly_pct;
static lv_obj_t* lbl_weekly_label;
static lv_obj_t* lbl_weekly_reset;
static lv_obj_t* lbl_anim;
// Logo image descriptor for LVGL
static lv_image_dsc_t logo_dsc;
// Animation state
static uint32_t anim_last_ms = 0;
static uint8_t anim_spinner_idx = 0;
static uint8_t anim_msg_idx = 0;
static uint32_t anim_msg_start = 0;
#define ANIM_SPIN_MS 120 // spinner frame speed
#define ANIM_MSG_MS 4000 // how long each message stays
// Claude's spinner characters: · ✻ ✽ ✶ ✳ ✢
// UTF-8 encoded
static const char* const spinner_frames[] = {
"\xC2\xB7", // · (U+00B7)
"\xE2\x9C\xBB", // ✻ (U+273B)
"\xE2\x9C\xBD", // ✽ (U+273D)
"\xE2\x9C\xB6", // ✶ (U+2736)
"\xE2\x9C\xB3", // ✳ (U+2733)
"\xE2\x9C\xA2", // ✢ (U+2722)
};
#define SPINNER_COUNT 6
static const char* const anim_messages[] = {
"Accomplishing", "Elucidating", "Perusing",
"Actioning", "Enchanting", "Philosophising",
"Actualizing", "Envisioning", "Pondering",
"Baking", "Finagling", "Pontificating",
"Booping", "Flibbertigibbeting", "Processing",
"Brewing", "Forging", "Puttering",
"Calculating", "Forming", "Puzzling",
"Cerebrating", "Frolicking", "Reticulating",
"Channelling", "Generating", "Ruminating",
"Churning", "Germinating", "Scheming",
"Clauding", "Hatching", "Schlepping",
"Coalescing", "Herding", "Shimmying",
"Cogitating", "Honking", "Shucking",
"Combobulating", "Hustling", "Simmering",
"Computing", "Ideating", "Smooshing",
"Concocting", "Imagining", "Spelunking",
"Conjuring", "Incubating", "Spinning",
"Considering", "Inferring", "Stewing",
"Contemplating", "Jiving", "Sussing",
"Cooking", "Manifesting", "Synthesizing",
"Crafting", "Marinating", "Thinking",
"Creating", "Meandering", "Tinkering",
"Crunching", "Moseying", "Transmuting",
"Deciphering", "Mulling", "Unfurling",
"Deliberating", "Mustering", "Unravelling",
"Determining", "Musing", "Vibing",
"Discombobulating", "Noodling", "Wandering",
"Divining", "Percolating", "Whirring",
"Doing", "Wibbling",
"Effecting", "Wizarding",
"Working", "Wrangling",
};
#define ANIM_MSG_COUNT (sizeof(anim_messages) / sizeof(anim_messages[0]))
static lv_color_t pct_color(float pct) {
if (pct >= 80.0f) return COL_RED;
if (pct >= 50.0f) return COL_AMBER;
return COL_GREEN;
}
static void format_reset_time(int mins, char* buf, size_t len) {
if (mins < 0) {
snprintf(buf, len, "---");
} else if (mins < 60) {
snprintf(buf, len, "Resets in %dm", mins);
} else if (mins < 1440) {
snprintf(buf, len, "Resets in %dh %dm", mins / 60, mins % 60);
} else {
snprintf(buf, len, "Resets in %dd %dh", mins / 1440, (mins % 1440) / 60);
}
}
static lv_obj_t* make_panel(lv_obj_t* parent, int x, int y, int w, int h) {
lv_obj_t* panel = lv_obj_create(parent);
lv_obj_set_pos(panel, x, y);
lv_obj_set_size(panel, w, h);
lv_obj_set_style_bg_color(panel, COL_PANEL, 0);
lv_obj_set_style_bg_opa(panel, LV_OPA_COVER, 0);
lv_obj_set_style_radius(panel, 8, 0);
lv_obj_set_style_border_width(panel, 0, 0);
lv_obj_set_style_pad_left(panel, 12, 0);
lv_obj_set_style_pad_right(panel, 12, 0);
lv_obj_set_style_pad_top(panel, 10, 0);
lv_obj_set_style_pad_bottom(panel, 16, 0);
lv_obj_clear_flag(panel, LV_OBJ_FLAG_SCROLLABLE);
return panel;
}
static lv_obj_t* make_bar(lv_obj_t* parent, int x, int y, int w, int h) {
lv_obj_t* bar = lv_bar_create(parent);
lv_obj_set_pos(bar, x, y);
lv_obj_set_size(bar, w, h);
lv_bar_set_range(bar, 0, 100);
lv_bar_set_value(bar, 0, LV_ANIM_OFF);
lv_obj_set_style_bg_color(bar, COL_BAR_BG, LV_PART_MAIN);
lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_set_style_radius(bar, 6, LV_PART_MAIN);
lv_obj_set_style_bg_color(bar, COL_GREEN, LV_PART_INDICATOR);
lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, LV_PART_INDICATOR);
lv_obj_set_style_radius(bar, 6, LV_PART_INDICATOR);
return bar;
}
void ui_init(void) {
lv_obj_t* scr = lv_screen_active();
lv_obj_set_style_bg_color(scr, COL_BG, 0);
lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);
// ---- Logo image ----
logo_dsc.header.w = LOGO_WIDTH;
logo_dsc.header.h = LOGO_HEIGHT;
logo_dsc.header.cf = LV_COLOR_FORMAT_RGB565;
logo_dsc.header.stride = LOGO_WIDTH * 2;
logo_dsc.data = (const uint8_t*)logo_data;
logo_dsc.data_size = LOGO_WIDTH * LOGO_HEIGHT * 2;
lv_obj_t* img = lv_image_create(scr);
lv_image_set_src(img, &logo_dsc);
lv_obj_set_pos(img, 10, 4);
// ---- Title (vertically centered to 48px logo) ----
lbl_title = lv_label_create(scr);
lv_label_set_text(lbl_title, "Claude Usage Tracker");
lv_obj_set_style_text_font(lbl_title, &font_tiempos_34, 0);
lv_obj_set_style_text_color(lbl_title, COL_TEXT, 0);
lv_obj_set_pos(lbl_title, 66, 12);
// ---- Session panel ----
// Layout: percentage -> bar -> "5-Hour Session ... Resets in ..." below
lv_obj_t* p_session = make_panel(scr, 8, 56, 464, 100);
lbl_session_pct = lv_label_create(p_session);
lv_label_set_text(lbl_session_pct, "---%");
lv_obj_set_style_text_font(lbl_session_pct, &font_styrene_28, 0);
lv_obj_set_style_text_color(lbl_session_pct, COL_TEXT, 0);
lv_obj_set_pos(lbl_session_pct, 0, 0);
bar_session = make_bar(p_session, 0, 36, 440, 20);
lbl_session_label = lv_label_create(p_session);
lv_label_set_text(lbl_session_label, "Current Session");
lv_obj_set_style_text_font(lbl_session_label, &font_styrene_16, 0);
lv_obj_set_style_text_color(lbl_session_label, COL_DIM, 0);
lv_obj_set_pos(lbl_session_label, 0, 62);
lbl_session_reset = lv_label_create(p_session);
lv_label_set_text(lbl_session_reset, "---");
lv_obj_set_style_text_font(lbl_session_reset, &font_styrene_16, 0);
lv_obj_set_style_text_color(lbl_session_reset, COL_DIM, 0);
lv_obj_set_pos(lbl_session_reset, 440, 62);
lv_obj_set_style_text_align(lbl_session_reset, LV_TEXT_ALIGN_RIGHT, 0);
lv_obj_align(lbl_session_reset, LV_ALIGN_TOP_RIGHT, 0, 62);
// ---- Weekly panel ----
lv_obj_t* p_weekly = make_panel(scr, 8, 166, 464, 100);
lbl_weekly_pct = lv_label_create(p_weekly);
lv_label_set_text(lbl_weekly_pct, "---%");
lv_obj_set_style_text_font(lbl_weekly_pct, &font_styrene_28, 0);
lv_obj_set_style_text_color(lbl_weekly_pct, COL_TEXT, 0);
lv_obj_set_pos(lbl_weekly_pct, 0, 0);
bar_weekly = make_bar(p_weekly, 0, 36, 440, 20);
lbl_weekly_label = lv_label_create(p_weekly);
lv_label_set_text(lbl_weekly_label, "Current Week");
lv_obj_set_style_text_font(lbl_weekly_label, &font_styrene_16, 0);
lv_obj_set_style_text_color(lbl_weekly_label, COL_DIM, 0);
lv_obj_set_pos(lbl_weekly_label, 0, 62);
lbl_weekly_reset = lv_label_create(p_weekly);
lv_label_set_text(lbl_weekly_reset, "---");
lv_obj_set_style_text_font(lbl_weekly_reset, &font_styrene_16, 0);
lv_obj_set_style_text_color(lbl_weekly_reset, COL_DIM, 0);
lv_obj_align(lbl_weekly_reset, LV_ALIGN_TOP_RIGHT, 0, 62);
// ---- Animation at bottom ----
lbl_anim = lv_label_create(scr);
lv_label_set_text(lbl_anim, "");
lv_obj_set_style_text_font(lbl_anim, &font_mono_18, 0);
lv_obj_set_style_text_color(lbl_anim, COL_ACCENT, 0);
lv_obj_align(lbl_anim, LV_ALIGN_BOTTOM_MID, 0, -16);
}
void ui_update(const UsageData* data) {
if (!data->valid) return;
int s_pct = (int)(data->session_pct + 0.5f);
lv_label_set_text_fmt(lbl_session_pct, "%d%%", s_pct);
lv_bar_set_value(bar_session, s_pct, LV_ANIM_ON);
lv_obj_set_style_bg_color(bar_session, pct_color(data->session_pct), LV_PART_INDICATOR);
char buf[48];
format_reset_time(data->session_reset_mins, buf, sizeof(buf));
lv_label_set_text(lbl_session_reset, buf);
int w_pct = (int)(data->weekly_pct + 0.5f);
lv_label_set_text_fmt(lbl_weekly_pct, "%d%%", w_pct);
lv_bar_set_value(bar_weekly, w_pct, LV_ANIM_ON);
lv_obj_set_style_bg_color(bar_weekly, pct_color(data->weekly_pct), LV_PART_INDICATOR);
format_reset_time(data->weekly_reset_mins, buf, sizeof(buf));
lv_label_set_text(lbl_weekly_reset, buf);
}
void ui_tick_anim(void) {
uint32_t now = lv_tick_get();
// Switch to next message
if (now - anim_msg_start >= ANIM_MSG_MS) {
anim_msg_idx = (anim_msg_idx + 1) % ANIM_MSG_COUNT;
anim_msg_start = now;
}
// Spin the spinner
if (now - anim_last_ms >= ANIM_SPIN_MS) {
anim_last_ms = now;
anim_spinner_idx = (anim_spinner_idx + 1) % SPINNER_COUNT;
static char buf[80];
snprintf(buf, sizeof(buf), "%s %s\xE2\x80\xA6",
spinner_frames[anim_spinner_idx],
anim_messages[anim_msg_idx]);
lv_label_set_text(lbl_anim, buf);
}
}
void ui_set_status(const char* text) {
(void)text;
}
+7
View File
@@ -0,0 +1,7 @@
#pragma once
#include "data.h"
void ui_init(void);
void ui_update(const UsageData* data);
void ui_tick_anim(void);
void ui_set_status(const char* text);
Executable
+47
View File
@@ -0,0 +1,47 @@
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SERVICE_NAME="claude-usage-daemon"
SERVICE_FILE="$SCRIPT_DIR/daemon/$SERVICE_NAME.service"
USER_SERVICE_DIR="$HOME/.config/systemd/user"
echo "=== Claude Usage Tracker - Install ==="
echo ""
# Check dependencies
echo "[1/3] Checking dependencies..."
for cmd in curl stty inotifywait; do
command -v "$cmd" >/dev/null || { echo "Error: $cmd is required but not installed"; exit 1; }
done
echo " All dependencies found (curl, python3, stty)"
echo ""
# Install systemd user service with resolved path
echo "[2/3] Installing systemd user service..."
mkdir -p "$USER_SERVICE_DIR"
DAEMON_BIN="$SCRIPT_DIR/daemon/$SERVICE_NAME.sh"
sed "s|DAEMON_PATH|${DAEMON_BIN}|g" "$SERVICE_FILE" > "$USER_SERVICE_DIR/$SERVICE_NAME.service"
systemctl --user daemon-reload
# Enable (service starts automatically when device is plugged in)
echo "[3/3] Enabling service..."
systemctl --user enable "$SERVICE_NAME"
echo ""
echo "=== Done! ==="
echo ""
echo "The daemon will now start automatically when you log in"
echo "and the SC01 Plus is connected via USB."
echo ""
echo "Useful commands:"
echo " systemctl --user status $SERVICE_NAME # check status"
echo " journalctl --user -u $SERVICE_NAME -f # view logs"
echo " systemctl --user restart $SERVICE_NAME # restart"
echo " systemctl --user stop $SERVICE_NAME # stop"
echo ""
echo "To uninstall:"
echo " systemctl --user stop $SERVICE_NAME"
echo " systemctl --user disable $SERVICE_NAME"
echo " rm $USER_SERVICE_DIR/$SERVICE_NAME.service"
echo " systemctl --user daemon-reload"