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:
co-authored by
Claude Opus 4.6
commit
2611c70881
@@ -0,0 +1,9 @@
|
|||||||
|
# PlatformIO build artifacts
|
||||||
|
firmware/.pio/
|
||||||
|
firmware/.vscode/
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Claude Code
|
||||||
|
.claude/
|
||||||
@@ -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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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.
Binary file not shown.
|
After Width: | Height: | Size: 6.0 MiB |
@@ -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
|
||||||
Executable
+128
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
};
|
||||||
@@ -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
@@ -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,
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
@@ -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"
|
||||||
Reference in New Issue
Block a user