Replace USB serial/HID with Bluetooth Low Energy
Move all communication from USB to BLE. The device now advertises as "Claude Controller" and acts as both a BLE HID keyboard (for touch gestures) and a GATT data server (for usage updates from the daemon). - Add NimBLE-Arduino BLE module with custom GATT service + HID keyboard - Add third screen (Bluetooth) with connection status, MAC, reset button - Rewrite daemon in bash using bluetoothctl/busctl for BLE GATT writes - Add screenshot capture via LVGL snapshot over serial - Remove TinyUSB mode — normal pio upload works again - Update README with BLE architecture, screenshots, gesture docs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@@ -1,58 +1,121 @@
|
|||||||
# Claude Usage Tracker - Panlee SC01 Plus
|
# Claude Usage Tracker - Panlee SC01 Plus
|
||||||
|
|
||||||
Displays your Claude Code usage limits on a Panlee SC01 Plus (ESP32-S3) 3.5" touchscreen.
|
Bluetooth-connected Claude Code usage monitor and touch controller on a Panlee SC01 Plus (ESP32-S3) 3.5" touchscreen.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Shows your 5-hour session and 7-day weekly utilization.
|
## Features
|
||||||
|
|
||||||
|
- **Usage dashboard** — Live 5-hour session and 7-day weekly utilization bars with color-coded thresholds
|
||||||
|
- **Touch controller** — Gesture-based Claude Code navigation (swipe, tap, hold) sent as BLE HID keyboard input
|
||||||
|
- **Bluetooth screen** — Connection status, device name, MAC address, bond management
|
||||||
|
- **Wireless** — All communication over Bluetooth Low Energy (USB only for charging and flashing)
|
||||||
|
- **Auto-reconnect** — Daemon discovers and reconnects automatically with exponential backoff
|
||||||
|
|
||||||
## Hardware
|
## Hardware
|
||||||
|
|
||||||
- [Panlee SC01 Plus](http://en.smartpanle.com/product-item-15.html) (WT32-SC01 Plus) — ESP32-S3, 3.5" 480x320 IPS, capacitive touch
|
- [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)
|
- USB-C cable for flashing firmware and charging
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- Linux (tested on my Ubuntu laptop)
|
- Linux (tested on Ubuntu)
|
||||||
- [PlatformIO CLI](https://docs.platformio.org/en/latest/core/installation/index.html)
|
- [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)
|
- `curl`, `bluetoothctl`, `busctl` (BlueZ Bluetooth stack)
|
||||||
- Claude Code
|
- Claude Code with an active subscription
|
||||||
- User in `dialout` group: `sudo adduser $USER dialout` (log out/in after)
|
|
||||||
|
|
||||||
## Flash the firmware
|
## Flash the firmware
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd firmware
|
cd firmware
|
||||||
pio run -t upload
|
pio run -t upload --upload-port /dev/ttyACM0
|
||||||
```
|
```
|
||||||
|
|
||||||
The SC01 Plus appears at `/dev/ttyACM0`.
|
## Bluetooth pairing
|
||||||
|
|
||||||
## Run the daemon
|
After flashing, the device advertises as **"Claude Controller"**. Pair it once:
|
||||||
|
|
||||||
The daemon polls your Claude usage every 30 seconds and sends it to the display over USB serial.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash daemon/claude-usage-daemon.sh
|
# Scan for the device
|
||||||
|
bluetoothctl scan le
|
||||||
|
|
||||||
|
# When "Claude Controller" appears, pair and trust it
|
||||||
|
bluetoothctl pair F4:12:FA:C0:8F:E5 # use your device's MAC
|
||||||
|
bluetoothctl trust F4:12:FA:C0:8F:E5
|
||||||
```
|
```
|
||||||
|
|
||||||
## Install the systemd service
|
The MAC address is shown on the Bluetooth screen (third screen, tap the logo to cycle).
|
||||||
|
|
||||||
To auto-start the daemon on login:
|
## Install the daemon
|
||||||
|
|
||||||
|
The daemon polls your Claude usage every 30 seconds and sends it to the display over BLE.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./install.sh
|
./install.sh
|
||||||
|
systemctl --user start claude-usage-daemon
|
||||||
```
|
```
|
||||||
|
|
||||||
Check status: `systemctl --user status claude-usage-daemon`
|
Check status: `systemctl --user status claude-usage-daemon`
|
||||||
|
|
||||||
|
View logs: `journalctl --user -u claude-usage-daemon -f`
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
1. The daemon reads your Claude Code OAuth token from `~/.claude/.credentials.json`
|
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)
|
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.)
|
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
|
4. Connects to the ESP32 over BLE and writes a JSON payload to the GATT RX characteristic
|
||||||
5. The ESP32 parses it and updates the LVGL dashboard
|
5. The ESP32 parses it and updates the LVGL dashboard
|
||||||
|
6. Touch gestures on the Controller screen are sent as BLE HID keyboard input to the host
|
||||||
|
|
||||||
|
## Screens
|
||||||
|
|
||||||
|
Tap the Claude logo (top-left) to cycle between screens:
|
||||||
|
|
||||||
|
| Usage | Controller | Bluetooth |
|
||||||
|
| :-----------------------------: | :---------------------------------------: | :-------------------------------------: |
|
||||||
|
|  |  |  |
|
||||||
|
| Session and weekly utilization | Touch zones and swipe gestures | Connection status and reset |
|
||||||
|
|
||||||
|
## Gesture controls
|
||||||
|
|
||||||
|
On the **Controller** screen:
|
||||||
|
|
||||||
|
| Gesture | Action |
|
||||||
|
| ------------- | ----------------------- |
|
||||||
|
| Swipe up | Arrow Up |
|
||||||
|
| Swipe down | Arrow Down |
|
||||||
|
| Swipe left | Shift+Tab (toggle mode) |
|
||||||
|
| Swipe right | Enter |
|
||||||
|
| Hold (center) | Space (voice dictation) |
|
||||||
|
|
||||||
|
Touch zones (tap):
|
||||||
|
|
||||||
|
| Zone | Action |
|
||||||
|
| ------------ | ----------- |
|
||||||
|
| Top-left | Escape |
|
||||||
|
| Bottom-left | Arrow Left |
|
||||||
|
| Top-right | Delete |
|
||||||
|
| Bottom-right | Arrow Right |
|
||||||
|
|
||||||
|
## BLE protocol
|
||||||
|
|
||||||
|
The device advertises a custom GATT service alongside the standard HID keyboard service:
|
||||||
|
|
||||||
|
| | UUID |
|
||||||
|
| -------------------------- | -------------------------------------- |
|
||||||
|
| **Data Service** | `4c41555a-4465-7669-6365-000000000001` |
|
||||||
|
| RX Characteristic (write) | `4c41555a-4465-7669-6365-000000000002` |
|
||||||
|
| TX Characteristic (notify) | `4c41555a-4465-7669-6365-000000000003` |
|
||||||
|
| **HID Service** | `00001812-0000-1000-8000-00805f9b34fb` |
|
||||||
|
|
||||||
|
JSON payload format (written to RX):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "s": 45, "sr": 120, "w": 28, "wr": 7200, "st": "allowed", "ok": true }
|
||||||
|
```
|
||||||
|
|
||||||
|
Fields: `s` = session %, `sr` = session reset (minutes), `w` = weekly %, `wr` = weekly reset (minutes), `st` = status, `ok` = success flag.
|
||||||
|
|
||||||
## Recompiling fonts
|
## Recompiling fonts
|
||||||
|
|
||||||
@@ -98,12 +161,14 @@ The controller screen uses [Lucide](https://lucide.dev) icons (the same icon set
|
|||||||
1. Get Lucide SVGs from [github.com/lucide-icons/lucide](https://github.com/lucide-icons/lucide) (`icons/` directory)
|
1. Get Lucide SVGs from [github.com/lucide-icons/lucide](https://github.com/lucide-icons/lucide) (`icons/` directory)
|
||||||
|
|
||||||
2. Convert SVG to PNG at desired size:
|
2. Convert SVG to PNG at desired size:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
inkscape icons/delete.svg --export-width=32 --export-height=32 \
|
inkscape icons/delete.svg --export-width=32 --export-height=32 \
|
||||||
--export-filename=assets/icon_delete.png --export-background-opacity=0
|
--export-filename=assets/icon_delete.png --export-background-opacity=0
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Convert PNG to RGB565 C array:
|
3. Convert PNG to RGB565 C array:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
@@ -125,7 +190,7 @@ for y in range(img.height):
|
|||||||
# Write as C array to firmware/src/icons.h
|
# Write as C array to firmware/src/icons.h
|
||||||
```
|
```
|
||||||
|
|
||||||
Current icons: `delete`, `arrow-left`, `arrow-right`, `circle-arrow-out-up-left` (escape), `hand` (gestures).
|
Current icons: `delete`, `arrow-left`, `arrow-right`, `circle-arrow-out-up-left` (escape), `hand` (gestures), `bluetooth`.
|
||||||
|
|
||||||
## Licensing gray area warning
|
## Licensing gray area warning
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 494 B |
@@ -0,0 +1,13 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="m7 7 10 10-5 5V2l5 5L7 17" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 249 B |
|
After Width: | Height: | Size: 519 B |
@@ -0,0 +1,17 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M10 11v6" />
|
||||||
|
<path d="M14 11v6" />
|
||||||
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" />
|
||||||
|
<path d="M3 6h18" />
|
||||||
|
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 389 B |
@@ -1,5 +1,7 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Claude Usage Tracker Daemon
|
Description=Claude Usage Tracker Daemon (BLE)
|
||||||
|
Requires=bluetooth.target
|
||||||
|
After=bluetooth.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Claude Usage Tracker Daemon
|
# Claude Usage Tracker Daemon (BLE)
|
||||||
# Reads Claude Code OAuth token, polls usage via API, sends to ESP32 over serial.
|
# Reads Claude Code OAuth token, polls usage via API, sends to ESP32 over BLE GATT.
|
||||||
# Survives USB plug/unplug cycles using inotifywait for instant device detection.
|
# Auto-connects and reconnects to the Claude Controller BLE device.
|
||||||
# Dependencies: curl, awk, inotify-tools
|
# Dependencies: curl, awk, bluetoothctl
|
||||||
|
|
||||||
SERIAL_PORT="${SERIAL_PORT:-/dev/ttyACM0}"
|
DEVICE_NAME="Claude Controller"
|
||||||
SERIAL_DEV=$(basename "$SERIAL_PORT")
|
DEVICE_MAC="${DEVICE_MAC:-}" # auto-discovered if empty
|
||||||
|
SERVICE_UUID="4c41555a-4465-7669-6365-000000000001"
|
||||||
|
RX_CHAR_UUID="4c41555a-4465-7669-6365-000000000002"
|
||||||
POLL_INTERVAL=30
|
POLL_INTERVAL=30
|
||||||
BAUD=115200
|
SAVED_MAC_FILE="$HOME/.config/claude-usage-monitor/ble-address"
|
||||||
SERIAL_FD=3
|
DBUS_DEST="org.bluez"
|
||||||
|
|
||||||
log() {
|
log() {
|
||||||
echo "[$(date '+%H:%M:%S')] $1"
|
echo "[$(date '+%H:%M:%S')] $1"
|
||||||
@@ -18,27 +20,112 @@ read_token() {
|
|||||||
grep -o '"accessToken":"[^"]*"' "$HOME/.claude/.credentials.json" | cut -d'"' -f4
|
grep -o '"accessToken":"[^"]*"' "$HOME/.claude/.credentials.json" | cut -d'"' -f4
|
||||||
}
|
}
|
||||||
|
|
||||||
device_connected() {
|
# Convert MAC to D-Bus path: AA:BB:CC:DD:EE:FF -> dev_AA_BB_CC_DD_EE_FF
|
||||||
[ -e "$SERIAL_PORT" ]
|
mac_to_dbus_path() {
|
||||||
|
local adapter
|
||||||
|
adapter=$(busctl call org.bluez / org.freedesktop.DBus.ObjectManager GetManagedObjects 2>/dev/null | grep -o '/org/bluez/hci[0-9]' | head -1)
|
||||||
|
adapter=${adapter:-/org/bluez/hci0}
|
||||||
|
echo "${adapter}/dev_$(echo "$1" | tr ':' '_')"
|
||||||
}
|
}
|
||||||
|
|
||||||
open_serial() {
|
# Check if device is connected via D-Bus
|
||||||
stty -F "$SERIAL_PORT" "$BAUD" raw -echo -echoe -echok 2>/dev/null || return 1
|
is_connected() {
|
||||||
eval "exec ${SERIAL_FD}<>\"$SERIAL_PORT\"" 2>/dev/null || return 1
|
local path
|
||||||
return 0
|
path=$(mac_to_dbus_path "$DEVICE_MAC")
|
||||||
|
busctl get-property "$DBUS_DEST" "$path" org.bluez.Device1 Connected 2>/dev/null | grep -q "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
close_serial() {
|
# Load saved MAC address
|
||||||
eval "exec ${SERIAL_FD}>&-" 2>/dev/null
|
load_mac() {
|
||||||
|
if [ -n "$DEVICE_MAC" ]; then return 0; fi
|
||||||
|
if [ -f "$SAVED_MAC_FILE" ]; then
|
||||||
|
DEVICE_MAC=$(cat "$SAVED_MAC_FILE")
|
||||||
|
[ -n "$DEVICE_MAC" ] && return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Wait for device to appear using inotifywait (blocks, zero CPU)
|
# Save MAC for fast reconnect
|
||||||
wait_for_device() {
|
save_mac() {
|
||||||
while ! device_connected; do
|
mkdir -p "$(dirname "$SAVED_MAC_FILE")"
|
||||||
inotifywait -qq -e create /dev --include "$SERIAL_DEV" 2>/dev/null || sleep 1
|
echo "$DEVICE_MAC" > "$SAVED_MAC_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Scan for Claude Controller
|
||||||
|
scan_for_device() {
|
||||||
|
log "Scanning for '$DEVICE_NAME'..."
|
||||||
|
# Start LE scan
|
||||||
|
bluetoothctl scan le &>/dev/null &
|
||||||
|
local scan_pid=$!
|
||||||
|
sleep 8
|
||||||
|
kill "$scan_pid" 2>/dev/null
|
||||||
|
wait "$scan_pid" 2>/dev/null
|
||||||
|
|
||||||
|
# Find the device
|
||||||
|
local found
|
||||||
|
found=$(bluetoothctl devices 2>/dev/null | grep "$DEVICE_NAME" | awk '{print $2}')
|
||||||
|
if [ -n "$found" ]; then
|
||||||
|
DEVICE_MAC="$found"
|
||||||
|
save_mac
|
||||||
|
log "Found: $DEVICE_MAC"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Connect to the device
|
||||||
|
connect_device() {
|
||||||
|
log "Connecting to $DEVICE_MAC..."
|
||||||
|
|
||||||
|
# Trust first (allows auto-reconnect)
|
||||||
|
bluetoothctl trust "$DEVICE_MAC" &>/dev/null
|
||||||
|
|
||||||
|
# Connect
|
||||||
|
bluetoothctl connect "$DEVICE_MAC" &>/dev/null
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
if is_connected; then
|
||||||
|
log "Connected"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
log "Connection failed"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Find the GATT characteristic handle via D-Bus
|
||||||
|
find_rx_char_path() {
|
||||||
|
local dev_path
|
||||||
|
dev_path=$(mac_to_dbus_path "$DEVICE_MAC")
|
||||||
|
|
||||||
|
# Search for our RX characteristic UUID in the device's GATT tree
|
||||||
|
busctl tree "$DBUS_DEST" 2>/dev/null | grep -o "${dev_path}/service[0-9a-f]*/char[0-9a-f]*" | while read -r char_path; do
|
||||||
|
local uuid
|
||||||
|
uuid=$(busctl get-property "$DBUS_DEST" "$char_path" org.bluez.GattCharacteristic1 UUID 2>/dev/null | tr -d '"' | awk '{print $2}')
|
||||||
|
if [ "$uuid" = "$RX_CHAR_UUID" ]; then
|
||||||
|
echo "$char_path"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Write data to the RX characteristic via D-Bus
|
||||||
|
write_gatt() {
|
||||||
|
local char_path="$1"
|
||||||
|
local data="$2"
|
||||||
|
|
||||||
|
# Convert string to byte array for D-Bus: "hi" -> 0x68 0x69
|
||||||
|
local bytes=""
|
||||||
|
for ((i = 0; i < ${#data}; i++)); do
|
||||||
|
local byte
|
||||||
|
byte=$(printf "0x%02x" "'${data:$i:1}")
|
||||||
|
bytes="$bytes $byte"
|
||||||
|
done
|
||||||
|
local count=${#data}
|
||||||
|
|
||||||
|
busctl call "$DBUS_DEST" "$char_path" org.bluez.GattCharacteristic1 \
|
||||||
|
WriteValue "aya{sv}" "$count" $bytes 0 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
poll() {
|
poll() {
|
||||||
local token
|
local token
|
||||||
token=$(read_token) || { log "Error: could not read token"; return 1; }
|
token=$(read_token) || { log "Error: could not read token"; return 1; }
|
||||||
@@ -69,7 +156,6 @@ poll() {
|
|||||||
s7d_reset=${s7d_reset:-0}
|
s7d_reset=${s7d_reset:-0}
|
||||||
status=${status:-unknown}
|
status=${status:-unknown}
|
||||||
|
|
||||||
# Build JSON payload in a single awk process (utilization * 100, timestamps to minutes)
|
|
||||||
local payload
|
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" \
|
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 {
|
'BEGIN {
|
||||||
@@ -80,49 +166,61 @@ poll() {
|
|||||||
printf "{\"s\":%s,\"sr\":%s,\"w\":%s,\"wr\":%s,\"st\":\"%s\",\"ok\":true}", sp, sr, wp, wr, st;
|
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 "Sending: $payload"
|
||||||
log "Write failed - device disconnected"
|
write_gatt "$RX_CHAR_PATH" "$payload" || { log "Write failed"; return 1; }
|
||||||
return 1
|
return 0
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
close_serial
|
|
||||||
log "Daemon stopped"
|
log "Daemon stopped"
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
trap cleanup INT TERM
|
trap cleanup INT TERM
|
||||||
|
|
||||||
log "=== Claude Usage Tracker Daemon ==="
|
log "=== Claude Usage Tracker Daemon (BLE) ==="
|
||||||
log "Serial port: $SERIAL_PORT"
|
|
||||||
log "Poll interval: ${POLL_INTERVAL}s"
|
log "Poll interval: ${POLL_INTERVAL}s"
|
||||||
|
|
||||||
|
BACKOFF=1
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
# Wait for device (instant detection via inotify, or already plugged in)
|
# Find the device
|
||||||
if ! device_connected; then
|
if ! load_mac; then
|
||||||
log "Waiting for device..."
|
scan_for_device || {
|
||||||
wait_for_device
|
log "Device not found, retrying in ${BACKOFF}s..."
|
||||||
|
sleep "$BACKOFF"
|
||||||
|
BACKOFF=$((BACKOFF < 60 ? BACKOFF * 2 : 60))
|
||||||
|
continue
|
||||||
|
}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log "Device connected"
|
# Connect if not connected
|
||||||
sleep 2 # wait for ESP32 boot
|
if ! is_connected; then
|
||||||
|
connect_device || {
|
||||||
|
log "Retrying in ${BACKOFF}s..."
|
||||||
|
sleep "$BACKOFF"
|
||||||
|
BACKOFF=$((BACKOFF < 60 ? BACKOFF * 2 : 60))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
if ! open_serial; then
|
# Find the GATT characteristic
|
||||||
log "Failed to open serial port"
|
RX_CHAR_PATH=$(find_rx_char_path)
|
||||||
sleep 2
|
if [ -z "$RX_CHAR_PATH" ]; then
|
||||||
|
log "Error: RX characteristic not found, retrying..."
|
||||||
|
sleep 5
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
log "Serial port opened"
|
log "GATT RX path: $RX_CHAR_PATH"
|
||||||
|
|
||||||
# Poll loop - polls API every POLL_INTERVAL, exits on disconnect or write failure
|
BACKOFF=1 # reset backoff on successful connection
|
||||||
while device_connected; do
|
|
||||||
poll || break
|
# Poll loop
|
||||||
# Sleep POLL_INTERVAL but wake instantly if device disappears
|
while is_connected; do
|
||||||
inotifywait -qq -e delete /dev --include "$SERIAL_DEV" -t "$POLL_INTERVAL" 2>/dev/null
|
poll || log "Poll failed, will retry"
|
||||||
|
sleep "$POLL_INTERVAL"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Disconnected - clean up and loop back to wait
|
log "Device disconnected, reconnecting..."
|
||||||
log "Device disconnected"
|
sleep 2
|
||||||
close_serial
|
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -7,8 +7,14 @@ monitor_speed = 115200
|
|||||||
|
|
||||||
build_flags =
|
build_flags =
|
||||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||||
-UARDUINO_USB_MODE -DARDUINO_USB_MODE=0
|
; HID is via BLE now, no TinyUSB needed — normal pio upload works
|
||||||
-DBOARD_HAS_PSRAM
|
-DBOARD_HAS_PSRAM
|
||||||
|
; NimBLE config — peripheral-only, single connection
|
||||||
|
-DCONFIG_BT_NIMBLE_ROLE_BROADCASTER=1
|
||||||
|
-DCONFIG_BT_NIMBLE_ROLE_PERIPHERAL=1
|
||||||
|
-DCONFIG_BT_NIMBLE_ROLE_CENTRAL=0
|
||||||
|
-DCONFIG_BT_NIMBLE_ROLE_OBSERVER=0
|
||||||
|
-DCONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
|
||||||
; LVGL config via build flags
|
; LVGL config via build flags
|
||||||
-DLV_CONF_SKIP
|
-DLV_CONF_SKIP
|
||||||
-DLV_COLOR_DEPTH=16
|
-DLV_COLOR_DEPTH=16
|
||||||
@@ -29,8 +35,10 @@ build_flags =
|
|||||||
-DLV_USE_IMAGE=1
|
-DLV_USE_IMAGE=1
|
||||||
-DLV_USE_ANIMIMG=0
|
-DLV_USE_ANIMIMG=0
|
||||||
-DLV_TICK_CUSTOM=1
|
-DLV_TICK_CUSTOM=1
|
||||||
|
-DLV_USE_SNAPSHOT=1
|
||||||
|
|
||||||
lib_deps =
|
lib_deps =
|
||||||
lovyan03/LovyanGFX@^1.1.16
|
lovyan03/LovyanGFX@^1.1.16
|
||||||
lvgl/lvgl@^9.2.0
|
lvgl/lvgl@^9.2.0
|
||||||
bblanchon/ArduinoJson@^7.0.0
|
bblanchon/ArduinoJson@^7.0.0
|
||||||
|
h2zero/NimBLE-Arduino@^2.1.1
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
#include "ble.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <NimBLEDevice.h>
|
||||||
|
#include <NimBLEHIDDevice.h>
|
||||||
|
|
||||||
|
#define DEVICE_NAME "Claude Controller"
|
||||||
|
|
||||||
|
// Custom GATT UUIDs for data channel
|
||||||
|
#define SERVICE_UUID "4c41555a-4465-7669-6365-000000000001"
|
||||||
|
#define RX_CHAR_UUID "4c41555a-4465-7669-6365-000000000002" // host writes here
|
||||||
|
#define TX_CHAR_UUID "4c41555a-4465-7669-6365-000000000003" // device notifies here
|
||||||
|
|
||||||
|
#define BLE_BUF_SIZE 512
|
||||||
|
|
||||||
|
// HID keyboard report descriptor
|
||||||
|
static const uint8_t HID_REPORT_MAP[] = {
|
||||||
|
0x05, 0x01, // Usage Page (Generic Desktop)
|
||||||
|
0x09, 0x06, // Usage (Keyboard)
|
||||||
|
0xA1, 0x01, // Collection (Application)
|
||||||
|
0x85, 0x01, // Report ID (1)
|
||||||
|
0x05, 0x07, // Usage Page (Key Codes)
|
||||||
|
0x19, 0xE0, // Usage Minimum (224) - Left Control
|
||||||
|
0x29, 0xE7, // Usage Maximum (231) - Right GUI
|
||||||
|
0x15, 0x00, // Logical Minimum (0)
|
||||||
|
0x25, 0x01, // Logical Maximum (1)
|
||||||
|
0x75, 0x01, // Report Size (1)
|
||||||
|
0x95, 0x08, // Report Count (8)
|
||||||
|
0x81, 0x02, // Input (Data, Variable, Absolute) - Modifier byte
|
||||||
|
0x95, 0x01, // Report Count (1)
|
||||||
|
0x75, 0x08, // Report Size (8)
|
||||||
|
0x81, 0x01, // Input (Constant) - Reserved byte
|
||||||
|
0x95, 0x06, // Report Count (6)
|
||||||
|
0x75, 0x08, // Report Size (8)
|
||||||
|
0x15, 0x00, // Logical Minimum (0)
|
||||||
|
0x25, 0x65, // Logical Maximum (101)
|
||||||
|
0x05, 0x07, // Usage Page (Key Codes)
|
||||||
|
0x19, 0x00, // Usage Minimum (0)
|
||||||
|
0x29, 0x65, // Usage Maximum (101)
|
||||||
|
0x81, 0x00, // Input (Data, Array) - Key array (6 keys)
|
||||||
|
0xC0, // End Collection
|
||||||
|
};
|
||||||
|
|
||||||
|
static NimBLEServer* server = nullptr;
|
||||||
|
static NimBLEHIDDevice* hid_dev = nullptr;
|
||||||
|
static NimBLECharacteristic* input_kbd = nullptr;
|
||||||
|
static NimBLECharacteristic* tx_char = nullptr;
|
||||||
|
static NimBLECharacteristic* rx_char = nullptr;
|
||||||
|
|
||||||
|
static ble_state_t state = BLE_STATE_INIT;
|
||||||
|
static bool need_advertise = false;
|
||||||
|
static char rx_buf[BLE_BUF_SIZE];
|
||||||
|
static volatile bool data_ready = false;
|
||||||
|
static char mac_str[18];
|
||||||
|
|
||||||
|
static void start_advertising() {
|
||||||
|
NimBLEAdvertising* adv = NimBLEDevice::getAdvertising();
|
||||||
|
adv->reset();
|
||||||
|
adv->addServiceUUID(SERVICE_UUID);
|
||||||
|
adv->setAppearance(HID_KEYBOARD);
|
||||||
|
adv->enableScanResponse(true);
|
||||||
|
adv->setName(DEVICE_NAME);
|
||||||
|
bool ok = adv->start();
|
||||||
|
state = BLE_STATE_ADVERTISING;
|
||||||
|
Serial.printf("BLE: advertising start=%s\n", ok ? "OK" : "FAILED");
|
||||||
|
}
|
||||||
|
|
||||||
|
class ServerCallbacks : public NimBLEServerCallbacks {
|
||||||
|
void onConnect(NimBLEServer* s, NimBLEConnInfo& info) override {
|
||||||
|
state = BLE_STATE_CONNECTED;
|
||||||
|
Serial.printf("BLE: connected from %s\n", info.getAddress().toString().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDisconnect(NimBLEServer* s, NimBLEConnInfo& info, int reason) override {
|
||||||
|
state = BLE_STATE_DISCONNECTED;
|
||||||
|
need_advertise = true;
|
||||||
|
Serial.printf("BLE: disconnected (reason=%d)\n", reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
class RxCallbacks : public NimBLECharacteristicCallbacks {
|
||||||
|
void onWrite(NimBLECharacteristic* chr, NimBLEConnInfo& info) override {
|
||||||
|
std::string val = chr->getValue();
|
||||||
|
size_t len = val.length();
|
||||||
|
if (len >= BLE_BUF_SIZE) len = BLE_BUF_SIZE - 1;
|
||||||
|
memcpy(rx_buf, val.c_str(), len);
|
||||||
|
rx_buf[len] = '\0';
|
||||||
|
data_ready = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void ble_init(void) {
|
||||||
|
NimBLEDevice::init(DEVICE_NAME);
|
||||||
|
NimBLEDevice::setSecurityAuth(true, false, true); // bonding, no MITM, SC
|
||||||
|
|
||||||
|
// Format MAC address
|
||||||
|
NimBLEAddress addr = NimBLEDevice::getAddress();
|
||||||
|
snprintf(mac_str, sizeof(mac_str), "%s", addr.toString().c_str());
|
||||||
|
for (int i = 0; mac_str[i]; i++) {
|
||||||
|
if (mac_str[i] >= 'a' && mac_str[i] <= 'f') mac_str[i] -= 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
server = NimBLEDevice::createServer();
|
||||||
|
static ServerCallbacks serverCb;
|
||||||
|
server->setCallbacks(&serverCb);
|
||||||
|
|
||||||
|
// --- HID keyboard service ---
|
||||||
|
hid_dev = new NimBLEHIDDevice(server);
|
||||||
|
hid_dev->setReportMap((uint8_t*)HID_REPORT_MAP, sizeof(HID_REPORT_MAP));
|
||||||
|
hid_dev->setManufacturer("Anthropic");
|
||||||
|
hid_dev->setPnp(0x02, 0x05AC, 0x820A, 0x0210); // BT SIG, generic keyboard
|
||||||
|
hid_dev->setHidInfo(0x00, 0x02); // country=0, flags=normally connectable
|
||||||
|
hid_dev->setBatteryLevel(100);
|
||||||
|
input_kbd = hid_dev->getInputReport(1); // report ID 1
|
||||||
|
|
||||||
|
// --- Custom data service ---
|
||||||
|
NimBLEService* svc = server->createService(SERVICE_UUID);
|
||||||
|
|
||||||
|
rx_char = svc->createCharacteristic(
|
||||||
|
RX_CHAR_UUID,
|
||||||
|
NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR
|
||||||
|
);
|
||||||
|
static RxCallbacks rxCb;
|
||||||
|
rx_char->setCallbacks(&rxCb);
|
||||||
|
|
||||||
|
tx_char = svc->createCharacteristic(
|
||||||
|
TX_CHAR_UUID,
|
||||||
|
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY
|
||||||
|
);
|
||||||
|
|
||||||
|
svc->start();
|
||||||
|
server->start();
|
||||||
|
start_advertising();
|
||||||
|
|
||||||
|
Serial.printf("BLE: init complete, MAC=%s\n", mac_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ble_tick(void) {
|
||||||
|
if (need_advertise) {
|
||||||
|
need_advertise = false;
|
||||||
|
start_advertising();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ble_state_t ble_get_state(void) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* ble_get_device_name(void) {
|
||||||
|
return DEVICE_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* ble_get_mac_address(void) {
|
||||||
|
return mac_str;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ble_clear_bonds(void) {
|
||||||
|
NimBLEDevice::deleteAllBonds();
|
||||||
|
Serial.println("BLE: bonds cleared");
|
||||||
|
if (state == BLE_STATE_CONNECTED) {
|
||||||
|
server->disconnect(server->getPeerInfo(0).getConnHandle());
|
||||||
|
}
|
||||||
|
need_advertise = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ble_has_data(void) {
|
||||||
|
return data_ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* ble_get_data(void) {
|
||||||
|
data_ready = false;
|
||||||
|
return rx_buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ble_send_ack(void) {
|
||||||
|
if (state == BLE_STATE_CONNECTED && tx_char) {
|
||||||
|
tx_char->setValue("{\"ack\":true}");
|
||||||
|
tx_char->notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ble_send_nack(void) {
|
||||||
|
if (state == BLE_STATE_CONNECTED && tx_char) {
|
||||||
|
tx_char->setValue("{\"err\":true}");
|
||||||
|
tx_char->notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ble_keyboard_press(uint8_t key, uint8_t modifier) {
|
||||||
|
if (state != BLE_STATE_CONNECTED || !input_kbd) return;
|
||||||
|
// HID report: [modifier, reserved, key1, key2, key3, key4, key5, key6]
|
||||||
|
uint8_t report[8] = {modifier, 0, key, 0, 0, 0, 0, 0};
|
||||||
|
input_kbd->setValue(report, sizeof(report));
|
||||||
|
input_kbd->notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ble_keyboard_release(void) {
|
||||||
|
if (state != BLE_STATE_CONNECTED || !input_kbd) return;
|
||||||
|
uint8_t report[8] = {0};
|
||||||
|
input_kbd->setValue(report, sizeof(report));
|
||||||
|
input_kbd->notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ble_is_connected(void) {
|
||||||
|
return state == BLE_STATE_CONNECTED;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
enum ble_state_t {
|
||||||
|
BLE_STATE_INIT,
|
||||||
|
BLE_STATE_ADVERTISING,
|
||||||
|
BLE_STATE_CONNECTED,
|
||||||
|
BLE_STATE_DISCONNECTED,
|
||||||
|
};
|
||||||
|
|
||||||
|
void ble_init(void);
|
||||||
|
void ble_tick(void);
|
||||||
|
ble_state_t ble_get_state(void);
|
||||||
|
const char* ble_get_device_name(void);
|
||||||
|
const char* ble_get_mac_address(void);
|
||||||
|
void ble_clear_bonds(void);
|
||||||
|
bool ble_has_data(void);
|
||||||
|
const char* ble_get_data(void);
|
||||||
|
void ble_send_ack(void);
|
||||||
|
void ble_send_nack(void);
|
||||||
|
|
||||||
|
// BLE HID keyboard
|
||||||
|
void ble_keyboard_press(uint8_t key, uint8_t modifier);
|
||||||
|
void ble_keyboard_release(void);
|
||||||
|
bool ble_is_connected(void);
|
||||||
|
|
||||||
@@ -1,10 +1,21 @@
|
|||||||
#include "hid.h"
|
#include "hid.h"
|
||||||
#include "USB.h"
|
#include "ble.h"
|
||||||
#include "USBHIDKeyboard.h"
|
|
||||||
|
|
||||||
static USBHIDKeyboard keyboard;
|
// HID usage codes (same for USB and BLE HID)
|
||||||
|
#define HID_KEY_ARROW_UP 0x52
|
||||||
|
#define HID_KEY_ARROW_DOWN 0x51
|
||||||
|
#define HID_KEY_ARROW_LEFT 0x50
|
||||||
|
#define HID_KEY_ARROW_RIGHT 0x4F
|
||||||
|
#define HID_KEY_ENTER 0x28
|
||||||
|
#define HID_KEY_ESCAPE 0x29
|
||||||
|
#define HID_KEY_BACKSPACE 0x2A
|
||||||
|
#define HID_KEY_TAB 0x2B
|
||||||
|
#define HID_KEY_SPACE 0x2C
|
||||||
|
|
||||||
|
// Modifier bits
|
||||||
|
#define MOD_LEFT_CTRL 0x01
|
||||||
|
#define MOD_LEFT_SHIFT 0x02
|
||||||
|
|
||||||
// Gesture-to-key mapping table. Edit this to change controls.
|
|
||||||
struct gesture_mapping_t {
|
struct gesture_mapping_t {
|
||||||
gesture_t gesture;
|
gesture_t gesture;
|
||||||
uint8_t key;
|
uint8_t key;
|
||||||
@@ -14,43 +25,40 @@ struct gesture_mapping_t {
|
|||||||
static const gesture_mapping_t mappings[] = {
|
static const gesture_mapping_t mappings[] = {
|
||||||
{ GESTURE_SWIPE_UP, HID_KEY_ARROW_UP, 0 },
|
{ GESTURE_SWIPE_UP, HID_KEY_ARROW_UP, 0 },
|
||||||
{ GESTURE_SWIPE_DOWN, HID_KEY_ARROW_DOWN, 0 },
|
{ GESTURE_SWIPE_DOWN, HID_KEY_ARROW_DOWN, 0 },
|
||||||
{ GESTURE_SWIPE_LEFT, HID_KEY_TAB, KEY_LEFT_SHIFT },
|
{ GESTURE_SWIPE_LEFT, HID_KEY_TAB, MOD_LEFT_SHIFT },
|
||||||
{ GESTURE_SWIPE_RIGHT, HID_KEY_ENTER, 0 },
|
{ GESTURE_SWIPE_RIGHT, HID_KEY_ENTER, 0 },
|
||||||
{ GESTURE_TAP_ESCAPE, HID_KEY_ESCAPE, 0 },
|
{ GESTURE_TAP_ESCAPE, HID_KEY_ESCAPE, 0 },
|
||||||
{ GESTURE_TAP_BACKSPACE, HID_KEY_BACKSPACE, 0 },
|
{ GESTURE_TAP_BACKSPACE, HID_KEY_BACKSPACE, 0 },
|
||||||
{ GESTURE_TAP_CTRL_SPACE, HID_KEY_SPACE, KEY_LEFT_CTRL },
|
{ GESTURE_TAP_CTRL_SPACE, HID_KEY_SPACE, MOD_LEFT_CTRL },
|
||||||
{ GESTURE_TAP_ARROW_LEFT, HID_KEY_ARROW_LEFT, 0 },
|
{ GESTURE_TAP_ARROW_LEFT, HID_KEY_ARROW_LEFT, 0 },
|
||||||
{ GESTURE_TAP_ARROW_RIGHT, HID_KEY_ARROW_RIGHT, 0 },
|
{ GESTURE_TAP_ARROW_RIGHT, HID_KEY_ARROW_RIGHT, 0 },
|
||||||
};
|
};
|
||||||
#define MAPPING_COUNT (sizeof(mappings) / sizeof(mappings[0]))
|
#define MAPPING_COUNT (sizeof(mappings) / sizeof(mappings[0]))
|
||||||
|
|
||||||
#define HOLD_KEY HID_KEY_SPACE
|
|
||||||
|
|
||||||
void hid_init(void) {
|
void hid_init(void) {
|
||||||
USB.begin();
|
// BLE HID is initialized in ble_init()
|
||||||
keyboard.begin();
|
|
||||||
keyboard.releaseAll();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void hid_on_gesture(gesture_t gesture) {
|
void hid_on_gesture(gesture_t gesture) {
|
||||||
|
if (!ble_is_connected()) return;
|
||||||
|
|
||||||
if (gesture == GESTURE_HOLD_START) {
|
if (gesture == GESTURE_HOLD_START) {
|
||||||
keyboard.pressRaw(HOLD_KEY);
|
ble_keyboard_press(HID_KEY_SPACE, 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (gesture == GESTURE_HOLD_END) {
|
if (gesture == GESTURE_HOLD_END) {
|
||||||
keyboard.releaseRaw(HOLD_KEY);
|
ble_keyboard_release();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (gesture == GESTURE_TOGGLE_SCREEN) {
|
if (gesture == GESTURE_TOGGLE_SCREEN) {
|
||||||
return; // handled by touch.cpp, not a keystroke
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < MAPPING_COUNT; i++) {
|
for (int i = 0; i < (int)MAPPING_COUNT; i++) {
|
||||||
if (mappings[i].gesture == gesture) {
|
if (mappings[i].gesture == gesture) {
|
||||||
if (mappings[i].modifier) keyboard.press(mappings[i].modifier);
|
ble_keyboard_press(mappings[i].key, mappings[i].modifier);
|
||||||
keyboard.pressRaw(mappings[i].key);
|
|
||||||
delay(20);
|
delay(20);
|
||||||
keyboard.releaseAll();
|
ble_keyboard_release();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -317,3 +317,76 @@ static const uint16_t icon_hand_data[576] = {
|
|||||||
0x73AD, 0x4228, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
0x73AD, 0x4228, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#define ICON_BLUETOOTH_W 32
|
||||||
|
#define ICON_BLUETOOTH_H 32
|
||||||
|
static const uint16_t icon_bluetooth_data[1024] = {
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x2104, 0x6B4C, 0x6B4C, 0x2104, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x4A28, 0xB574, 0xB574, 0x840F, 0x2104, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x4A69, 0xB574, 0xB574, 0xB574, 0x840F, 0x2104, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x4A69, 0xB574, 0xB574, 0xB574, 0xB574, 0x842F, 0x2104, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x4A69, 0xB574, 0xB574, 0xAD74, 0xB574, 0xB574, 0x840F, 0x2104, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x4A69, 0xB574, 0xB574, 0x630B, 0xA4F2, 0xB574, 0xB574, 0x842F, 0x2104, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x4A69, 0xB574, 0xB574, 0x4A69, 0x3186, 0xA4F2, 0xB574, 0xB574, 0x842F, 0x2104, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x73AE, 0xAD33, 0x52AA, 0x18E3, 0x18E3, 0x18E3, 0x4A69, 0xB574, 0xB574, 0x4A69, 0x18E3, 0x31A6, 0xA4F2, 0xB574, 0xB574, 0x7BEE, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0xAD33, 0xB574, 0xAD74, 0x52AA, 0x18E3, 0x18E3, 0x4A69, 0xB574, 0xB574, 0x4A69, 0x18E3, 0x18E3, 0x632C, 0xB574, 0xB574, 0xA533, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x52AA, 0xAD74, 0xB574, 0xAD74, 0x52AA, 0x18E3, 0x4A69, 0xB574, 0xB574, 0x4A69, 0x18E3, 0x52AA, 0xAD74, 0xB574, 0xAD74, 0x52AA, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x52AA, 0xAD74, 0xB574, 0xAD74, 0x5ACA, 0x4A69, 0xB574, 0xB574, 0x4A69, 0x52AA, 0xAD74, 0xB574, 0xAD74, 0x5ACA, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x52AA, 0xAD74, 0xB574, 0xAD74, 0x840F, 0xB574, 0xB574, 0x840F, 0xAD74, 0xB574, 0xAD74, 0x52AA, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x52AA, 0xAD74, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xAD74, 0x5ACA, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x52AA, 0xAD74, 0xB574, 0xB574, 0xB574, 0xB574, 0xAD74, 0x52AA, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x52AA, 0xAD74, 0xB574, 0xB574, 0xAD74, 0x52AA, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x52AA, 0xAD74, 0xB574, 0xB574, 0xAD74, 0x52AA, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x52AA, 0xAD74, 0xB574, 0xB574, 0xB574, 0xB574, 0xAD74, 0x52AA, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x52AA, 0xAD74, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xAD74, 0x5ACA, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x52AA, 0xAD74, 0xB574, 0xAD74, 0x840F, 0xB574, 0xB574, 0x840F, 0xAD74, 0xB574, 0xAD74, 0x52AA, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x52AA, 0xAD74, 0xB574, 0xAD74, 0x5ACA, 0x4A69, 0xB574, 0xB574, 0x4A69, 0x52AA, 0xAD74, 0xB574, 0xAD74, 0x5ACA, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x52AA, 0xAD74, 0xB574, 0xAD74, 0x52AA, 0x18E3, 0x4A69, 0xB574, 0xB574, 0x4A69, 0x18E3, 0x52AA, 0xAD74, 0xB574, 0xAD74, 0x52AA, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0xAD33, 0xB574, 0xAD74, 0x52AA, 0x18E3, 0x18E3, 0x4A69, 0xB574, 0xB574, 0x4A69, 0x18E3, 0x18E3, 0x632C, 0xB574, 0xB574, 0xA533, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x73AE, 0xAD33, 0x52AA, 0x18E3, 0x18E3, 0x18E3, 0x4A69, 0xB574, 0xB574, 0x4A69, 0x18E3, 0x31A6, 0xA4F2, 0xB574, 0xB574, 0x7BEE, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x4A69, 0xB574, 0xB574, 0x4A69, 0x3186, 0xA4F2, 0xB574, 0xB574, 0x842F, 0x2104, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x4A69, 0xB574, 0xB574, 0x630B, 0xA4F2, 0xB574, 0xB574, 0x842F, 0x2104, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x4A69, 0xB574, 0xB574, 0xAD74, 0xB574, 0xB574, 0x840F, 0x2104, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x4A69, 0xB574, 0xB574, 0xB574, 0xB574, 0x842F, 0x2104, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x4A69, 0xB574, 0xB574, 0xB574, 0x840F, 0x2104, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x4A28, 0xB574, 0xB574, 0x840F, 0x2104, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x2104, 0x6B4C, 0x6B4C, 0x2104, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
};
|
||||||
|
|
||||||
|
#define ICON_TRASH2_W 32
|
||||||
|
#define ICON_TRASH2_H 32
|
||||||
|
static const uint16_t icon_trash2_data[1024] = {
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x39C6, 0x632C, 0x7BEE, 0x7BEF, 0x7BEF, 0x7BEF, 0x7BEF, 0x7BEE, 0x632C, 0x39C6, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x5ACA, 0xAD54, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xAD54, 0x5ACA, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x39C6, 0xAD54, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xAD54, 0x39A6, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x632C, 0xB574, 0xB574, 0x52AA, 0x2103, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x2103, 0x5AAA, 0xB574, 0xB574, 0x632C, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x7BEE, 0xB574, 0xB574, 0x2103, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x2103, 0xB574, 0xB574, 0x7BEE, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x39C6, 0x4A69, 0x5289, 0x6B4C, 0x52AA, 0x4A69, 0x8C70, 0xB574, 0xB574, 0x4A69, 0x4A69, 0x4A69, 0x4A69, 0x4A69, 0x4A69, 0x4A69, 0x4A69, 0xB574, 0xB574, 0x8C70, 0x4A69, 0x52AA, 0x6B4C, 0x5289, 0x4A69, 0x39C6, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x39C6, 0xAD74, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xAD74, 0x39C6, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x39C6, 0xAD74, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xAD74, 0x39C6, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x39C6, 0x4A69, 0x8C70, 0xB574, 0xB574, 0x4A69, 0x5289, 0x6B4C, 0x52AA, 0x4A69, 0x4A69, 0x4A69, 0x4A69, 0x4A69, 0x4A69, 0x4A69, 0x4A69, 0x52AA, 0x6B4C, 0x5289, 0x4A69, 0xB574, 0xB574, 0x8C70, 0x4A69, 0x39C6, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x7BEE, 0xB574, 0xB574, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0xB574, 0xB574, 0x7BEE, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x7BEE, 0xB574, 0xB574, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0xB574, 0xB574, 0x7BEE, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x7BEE, 0xB574, 0xB574, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0xB574, 0xB574, 0x7BEE, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x7BEE, 0xB574, 0xB574, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x4A49, 0x7BAE, 0x3185, 0x18E3, 0x18E3, 0x3185, 0x7BAE, 0x4A49, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0xB574, 0xB574, 0x7BEE, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x7BEE, 0xB574, 0xB574, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0xAD33, 0xB574, 0x7BAE, 0x18E3, 0x18E3, 0x7BAE, 0xB574, 0xAD33, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0xB574, 0xB574, 0x7BEE, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x7BEE, 0xB574, 0xB574, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0xB574, 0xB574, 0x7BEE, 0x18E3, 0x18E3, 0x7BEE, 0xB574, 0xB574, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0xB574, 0xB574, 0x7BEE, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x7BEE, 0xB574, 0xB574, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0xB574, 0xB574, 0x7BEE, 0x18E3, 0x18E3, 0x7BEE, 0xB574, 0xB574, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0xB574, 0xB574, 0x7BEE, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x7BEE, 0xB574, 0xB574, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0xB574, 0xB574, 0x7BEE, 0x18E3, 0x18E3, 0x7BEE, 0xB574, 0xB574, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0xB574, 0xB574, 0x7BEE, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x7BEE, 0xB574, 0xB574, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0xB574, 0xB574, 0x7BEE, 0x18E3, 0x18E3, 0x7BEE, 0xB574, 0xB574, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0xB574, 0xB574, 0x7BEE, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x7BEE, 0xB574, 0xB574, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0xB574, 0xB574, 0x7BEE, 0x18E3, 0x18E3, 0x7BEE, 0xB574, 0xB574, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0xB574, 0xB574, 0x7BEE, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x7BEE, 0xB574, 0xB574, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0xB574, 0xB574, 0x7BEE, 0x18E3, 0x18E3, 0x7BEE, 0xB574, 0xB574, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0xB574, 0xB574, 0x7BEE, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x7BEE, 0xB574, 0xB574, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0xAD74, 0xB574, 0x7BEE, 0x18E3, 0x18E3, 0x7BEE, 0xB574, 0xAD74, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0xB574, 0xB574, 0x7BEE, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x7BEE, 0xB574, 0xB574, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x73AE, 0xAD33, 0x4A49, 0x18E3, 0x18E3, 0x4A49, 0xAD33, 0x73AE, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0xB574, 0xB574, 0x7BEE, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x7BEE, 0xB574, 0xB574, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0xB574, 0xB574, 0x7BEE, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x7BEE, 0xB574, 0xB574, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0xB574, 0xB574, 0x7BEE, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x7BEE, 0xB574, 0xB574, 0x2103, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x2103, 0xB574, 0xB574, 0x7BEE, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x632C, 0xB574, 0xB574, 0x5AAA, 0x2103, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x2103, 0x5ACA, 0xB574, 0xB574, 0x632C, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x39A6, 0xAD54, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xAD54, 0x31A6, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x5ACA, 0xAD54, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xB574, 0xAD54, 0x5ACA, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x39A6, 0x632C, 0x7BEE, 0x7BEF, 0x7BEF, 0x7BEF, 0x7BEF, 0x7BEF, 0x7BEF, 0x7BEF, 0x7BEF, 0x7BEF, 0x7BEF, 0x7BEF, 0x7BEF, 0x7BEE, 0x632C, 0x31A6, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3, 0x18E3,
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "ui.h"
|
#include "ui.h"
|
||||||
#include "hid.h"
|
#include "hid.h"
|
||||||
#include "touch.h"
|
#include "touch.h"
|
||||||
|
#include "ble.h"
|
||||||
|
|
||||||
LGFX lcd; // global - used by touch.cpp
|
LGFX lcd; // global - used by touch.cpp
|
||||||
static UsageData usage = {};
|
static UsageData usage = {};
|
||||||
@@ -15,11 +16,6 @@ static UsageData usage = {};
|
|||||||
static uint16_t buf1[480 * BUF_LINES];
|
static uint16_t buf1[480 * BUF_LINES];
|
||||||
static uint16_t buf2[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
|
// LVGL tick callback
|
||||||
static uint32_t my_tick(void) {
|
static uint32_t my_tick(void) {
|
||||||
return millis();
|
return millis();
|
||||||
@@ -67,20 +63,56 @@ static bool parse_json(const char* json, UsageData* out) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read serial data, returns true when a complete line is ready
|
// Serial command buffer
|
||||||
static bool read_serial_line() {
|
#define CMD_BUF_SIZE 64
|
||||||
|
static char cmd_buf[CMD_BUF_SIZE];
|
||||||
|
static int cmd_pos = 0;
|
||||||
|
|
||||||
|
static void send_screenshot() {
|
||||||
|
// Allocate buffer in PSRAM (internal RAM is too small for 307KB)
|
||||||
|
const uint32_t w = 480, h = 320;
|
||||||
|
const uint32_t row_bytes = w * 2;
|
||||||
|
const uint32_t buf_size = row_bytes * h;
|
||||||
|
uint8_t* buf = (uint8_t*)heap_caps_malloc(buf_size, MALLOC_CAP_SPIRAM);
|
||||||
|
if (!buf) {
|
||||||
|
Serial.println("SCREENSHOT_ERR");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use LVGL snapshot with caller-provided buffer
|
||||||
|
lv_draw_buf_t draw_buf;
|
||||||
|
lv_draw_buf_init(&draw_buf, w, h, LV_COLOR_FORMAT_RGB565, row_bytes, buf, buf_size);
|
||||||
|
|
||||||
|
lv_result_t res = lv_snapshot_take_to_draw_buf(lv_screen_active(), LV_COLOR_FORMAT_RGB565, &draw_buf);
|
||||||
|
if (res != LV_RESULT_OK) {
|
||||||
|
heap_caps_free(buf);
|
||||||
|
Serial.println("SCREENSHOT_ERR");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.printf("SCREENSHOT_START %lu %lu %lu\n", (unsigned long)w, (unsigned long)h, (unsigned long)buf_size);
|
||||||
|
Serial.flush();
|
||||||
|
Serial.write(buf, buf_size);
|
||||||
|
Serial.flush();
|
||||||
|
Serial.println();
|
||||||
|
Serial.println("SCREENSHOT_END");
|
||||||
|
|
||||||
|
heap_caps_free(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void check_serial_cmd() {
|
||||||
while (Serial.available()) {
|
while (Serial.available()) {
|
||||||
char c = Serial.read();
|
char c = Serial.read();
|
||||||
if (c == '\n') {
|
if (c == '\n' || c == '\r') {
|
||||||
serial_buf[serial_pos] = '\0';
|
cmd_buf[cmd_pos] = '\0';
|
||||||
serial_pos = 0;
|
if (strcmp(cmd_buf, "screenshot") == 0) {
|
||||||
return true;
|
send_screenshot();
|
||||||
}
|
}
|
||||||
if (serial_pos < SERIAL_BUF_SIZE - 1) {
|
cmd_pos = 0;
|
||||||
serial_buf[serial_pos++] = c;
|
} else if (cmd_pos < CMD_BUF_SIZE - 1) {
|
||||||
|
cmd_buf[cmd_pos++] = c;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
@@ -110,24 +142,46 @@ void setup() {
|
|||||||
// Init USB HID keyboard
|
// Init USB HID keyboard
|
||||||
hid_init();
|
hid_init();
|
||||||
|
|
||||||
|
// Init BLE data channel
|
||||||
|
ble_init();
|
||||||
|
|
||||||
// Init touch gesture detection
|
// Init touch gesture detection
|
||||||
touch_init();
|
touch_init();
|
||||||
|
|
||||||
// Build dashboard
|
// Build dashboard
|
||||||
ui_init();
|
ui_init();
|
||||||
|
|
||||||
Serial.println("Dashboard ready, waiting for data on serial...");
|
// Show initial BLE status on Bluetooth screen
|
||||||
|
ui_update_ble_status(ble_get_state(), ble_get_device_name(), ble_get_mac_address());
|
||||||
|
|
||||||
|
Serial.println("Dashboard ready, waiting for data on BLE...");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static ble_state_t last_ble_state = BLE_STATE_INIT;
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
lv_timer_handler();
|
lv_timer_handler();
|
||||||
ui_tick_anim();
|
ui_tick_anim();
|
||||||
touch_tick();
|
touch_tick();
|
||||||
|
ble_tick();
|
||||||
|
|
||||||
if (read_serial_line()) {
|
// Update BLE status on screen when state changes
|
||||||
if (parse_json(serial_buf, &usage)) {
|
ble_state_t bs = ble_get_state();
|
||||||
|
if (bs != last_ble_state) {
|
||||||
|
last_ble_state = bs;
|
||||||
|
ui_update_ble_status(bs, ble_get_device_name(), ble_get_mac_address());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for serial commands (screenshot, etc.)
|
||||||
|
check_serial_cmd();
|
||||||
|
|
||||||
|
// Process incoming BLE data
|
||||||
|
if (ble_has_data()) {
|
||||||
|
if (parse_json(ble_get_data(), &usage)) {
|
||||||
ui_update(&usage);
|
ui_update(&usage);
|
||||||
Serial.println("{\"ack\":true}");
|
ble_send_ack();
|
||||||
|
} else {
|
||||||
|
ble_send_nack();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "touch.h"
|
#include "touch.h"
|
||||||
#include "ui.h"
|
#include "ui.h"
|
||||||
|
#include "ble.h"
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include "display_cfg.h"
|
#include "display_cfg.h"
|
||||||
|
|
||||||
@@ -62,6 +63,11 @@ static bool is_in_tap_zone(uint16_t x, uint16_t y) {
|
|||||||
return classify_zone_tap(x, y) != GESTURE_NONE;
|
return classify_zone_tap(x, y) != GESTURE_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if tap is in the "Clear Bonds" zone on the Bluetooth screen
|
||||||
|
static bool in_ble_clear_zone(uint16_t x, uint16_t y) {
|
||||||
|
return x >= 8 && x <= 472 && y >= 190 && y < 250;
|
||||||
|
}
|
||||||
|
|
||||||
void touch_init(void) {
|
void touch_init(void) {
|
||||||
state = TS_IDLE;
|
state = TS_IDLE;
|
||||||
}
|
}
|
||||||
@@ -89,15 +95,19 @@ void touch_tick(void) {
|
|||||||
abs((int)last_y - (int)start_y) < TAP_MAX_MOVE_PX;
|
abs((int)last_y - (int)start_y) < TAP_MAX_MOVE_PX;
|
||||||
|
|
||||||
if (is_tap && in_logo(start_x, start_y)) {
|
if (is_tap && in_logo(start_x, start_y)) {
|
||||||
// Logo tap toggles screen (works on both screens)
|
// Logo tap cycles screens: Usage -> Controller -> Bluetooth -> ...
|
||||||
if (ui_is_controller_shown()) ui_show_usage();
|
ui_cycle_screen();
|
||||||
else ui_show_controller();
|
} else if (is_tap && ui_get_current_screen() == SCREEN_CONTROLLER) {
|
||||||
} else if (is_tap && ui_is_controller_shown()) {
|
|
||||||
// Zone taps only on controller screen
|
// Zone taps only on controller screen
|
||||||
gesture_t zone = classify_zone_tap(start_x, start_y);
|
gesture_t zone = classify_zone_tap(start_x, start_y);
|
||||||
if (zone != GESTURE_NONE) {
|
if (zone != GESTURE_NONE) {
|
||||||
hid_on_gesture(zone);
|
hid_on_gesture(zone);
|
||||||
}
|
}
|
||||||
|
} else if (is_tap && ui_get_current_screen() == SCREEN_BLUETOOTH) {
|
||||||
|
// "Clear Bonds" tap zone on Bluetooth screen
|
||||||
|
if (in_ble_clear_zone(start_x, start_y)) {
|
||||||
|
ble_clear_bonds();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
state = TS_IDLE;
|
state = TS_IDLE;
|
||||||
} else {
|
} else {
|
||||||
@@ -107,12 +117,12 @@ void touch_tick(void) {
|
|||||||
int dy = (int)y - (int)start_y;
|
int dy = (int)y - (int)start_y;
|
||||||
|
|
||||||
// Logo hold = Ctrl+Space (controller screen only)
|
// Logo hold = Ctrl+Space (controller screen only)
|
||||||
if (in_logo(start_x, start_y) && ui_is_controller_shown() && (now - down_time) >= LOGO_HOLD_TIME_MS) {
|
if (in_logo(start_x, start_y) && ui_get_current_screen() == SCREEN_CONTROLLER && (now - down_time) >= LOGO_HOLD_TIME_MS) {
|
||||||
hid_on_gesture(GESTURE_TAP_CTRL_SPACE);
|
hid_on_gesture(GESTURE_TAP_CTRL_SPACE);
|
||||||
state = TS_LOGO_HOLD;
|
state = TS_LOGO_HOLD;
|
||||||
}
|
}
|
||||||
// Everything below only on controller screen
|
// Everything below only on controller screen
|
||||||
else if (ui_is_controller_shown()) {
|
else if (ui_get_current_screen() == SCREEN_CONTROLLER) {
|
||||||
if (abs(dx) >= SWIPE_MIN_PX || abs(dy) >= SWIPE_MIN_PX) {
|
if (abs(dx) >= SWIPE_MIN_PX || abs(dy) >= SWIPE_MIN_PX) {
|
||||||
state = TS_SWIPING;
|
state = TS_SWIPING;
|
||||||
} else if (!in_logo(start_x, start_y) && !is_in_tap_zone(start_x, start_y) && (now - down_time) >= HOLD_TIME_MS) {
|
} else if (!in_logo(start_x, start_y) && !is_in_tap_zone(start_x, start_y) && (now - down_time) >= HOLD_TIME_MS) {
|
||||||
|
|||||||
@@ -43,9 +43,15 @@ static lv_obj_t* ctrl_session_bar;
|
|||||||
static lv_obj_t* ctrl_session_lbl;
|
static lv_obj_t* ctrl_session_lbl;
|
||||||
static lv_obj_t* ctrl_reset_lbl;
|
static lv_obj_t* ctrl_reset_lbl;
|
||||||
|
|
||||||
|
// ---- Bluetooth screen widgets ----
|
||||||
|
static lv_obj_t* ble_container;
|
||||||
|
static lv_obj_t* lbl_ble_status;
|
||||||
|
static lv_obj_t* lbl_ble_device;
|
||||||
|
static lv_obj_t* lbl_ble_mac;
|
||||||
|
|
||||||
// ---- Shared ----
|
// ---- Shared ----
|
||||||
static lv_image_dsc_t logo_dsc;
|
static lv_image_dsc_t logo_dsc;
|
||||||
static bool controller_shown = false;
|
static screen_t current_screen = SCREEN_USAGE;
|
||||||
|
|
||||||
// Animation state
|
// Animation state
|
||||||
static uint32_t anim_last_ms = 0;
|
static uint32_t anim_last_ms = 0;
|
||||||
@@ -388,6 +394,85 @@ static void init_controller_screen(lv_obj_t* scr) {
|
|||||||
lv_obj_add_flag(ctrl_container, LV_OBJ_FLAG_HIDDEN);
|
lv_obj_add_flag(ctrl_container, LV_OBJ_FLAG_HIDDEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void init_bluetooth_screen(lv_obj_t* scr) {
|
||||||
|
ble_container = lv_obj_create(scr);
|
||||||
|
lv_obj_set_size(ble_container, 480, 320);
|
||||||
|
lv_obj_set_pos(ble_container, 0, 0);
|
||||||
|
lv_obj_set_style_bg_opa(ble_container, LV_OPA_TRANSP, 0);
|
||||||
|
lv_obj_set_style_border_width(ble_container, 0, 0);
|
||||||
|
lv_obj_set_style_pad_all(ble_container, 0, 0);
|
||||||
|
lv_obj_clear_flag(ble_container, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
|
||||||
|
// Title
|
||||||
|
lv_obj_t* lbl_ble_title = lv_label_create(ble_container);
|
||||||
|
lv_label_set_text(lbl_ble_title, "Bluetooth");
|
||||||
|
lv_obj_set_style_text_font(lbl_ble_title, &font_tiempos_34, 0);
|
||||||
|
lv_obj_set_style_text_color(lbl_ble_title, COL_TEXT, 0);
|
||||||
|
lv_obj_set_pos(lbl_ble_title, 66, 12);
|
||||||
|
|
||||||
|
// Info panel
|
||||||
|
lv_obj_t* p_info = make_panel(ble_container, 8, 56, 464, 120);
|
||||||
|
|
||||||
|
// Bluetooth icon + status row
|
||||||
|
static lv_image_dsc_t icon_bt_dsc;
|
||||||
|
init_icon_dsc(&icon_bt_dsc, ICON_BLUETOOTH_W, ICON_BLUETOOTH_H, icon_bluetooth_data);
|
||||||
|
|
||||||
|
lv_obj_t* bt_img = lv_image_create(p_info);
|
||||||
|
lv_image_set_src(bt_img, &icon_bt_dsc);
|
||||||
|
lv_obj_set_pos(bt_img, 0, 0);
|
||||||
|
|
||||||
|
lbl_ble_status = lv_label_create(p_info);
|
||||||
|
lv_label_set_text(lbl_ble_status, "Initializing...");
|
||||||
|
lv_obj_set_style_text_font(lbl_ble_status, &font_styrene_28, 0);
|
||||||
|
lv_obj_set_style_text_color(lbl_ble_status, COL_DIM, 0);
|
||||||
|
lv_obj_set_pos(lbl_ble_status, 40, 2);
|
||||||
|
|
||||||
|
lbl_ble_device = lv_label_create(p_info);
|
||||||
|
lv_label_set_text(lbl_ble_device, "Device: ---");
|
||||||
|
lv_obj_set_style_text_font(lbl_ble_device, &font_styrene_16, 0);
|
||||||
|
lv_obj_set_style_text_color(lbl_ble_device, COL_DIM, 0);
|
||||||
|
lv_obj_set_pos(lbl_ble_device, 0, 50);
|
||||||
|
|
||||||
|
lbl_ble_mac = lv_label_create(p_info);
|
||||||
|
lv_label_set_text(lbl_ble_mac, "Address: ---");
|
||||||
|
lv_obj_set_style_text_font(lbl_ble_mac, &font_styrene_16, 0);
|
||||||
|
lv_obj_set_style_text_color(lbl_ble_mac, COL_DIM, 0);
|
||||||
|
lv_obj_set_pos(lbl_ble_mac, 0, 74);
|
||||||
|
|
||||||
|
// Reset Bluetooth tap zone with trash icon
|
||||||
|
lv_obj_t* reset_zone = lv_obj_create(ble_container);
|
||||||
|
lv_obj_set_pos(reset_zone, 8, 190);
|
||||||
|
lv_obj_set_size(reset_zone, 464, 60);
|
||||||
|
lv_obj_set_style_bg_color(reset_zone, COL_PANEL, 0);
|
||||||
|
lv_obj_set_style_bg_opa(reset_zone, LV_OPA_COVER, 0);
|
||||||
|
lv_obj_set_style_radius(reset_zone, 8, 0);
|
||||||
|
lv_obj_set_style_border_width(reset_zone, 0, 0);
|
||||||
|
lv_obj_set_style_pad_column(reset_zone, 10, 0);
|
||||||
|
lv_obj_set_flex_flow(reset_zone, LV_FLEX_FLOW_ROW);
|
||||||
|
lv_obj_set_flex_align(reset_zone, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||||
|
lv_obj_clear_flag(reset_zone, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
|
||||||
|
static lv_image_dsc_t icon_trash_dsc;
|
||||||
|
init_icon_dsc(&icon_trash_dsc, ICON_TRASH2_W, ICON_TRASH2_H, icon_trash2_data);
|
||||||
|
lv_obj_t* trash_img = lv_image_create(reset_zone);
|
||||||
|
lv_image_set_src(trash_img, &icon_trash_dsc);
|
||||||
|
|
||||||
|
lv_obj_t* reset_lbl = lv_label_create(reset_zone);
|
||||||
|
lv_label_set_text(reset_lbl, "Reset Bluetooth");
|
||||||
|
lv_obj_set_style_text_font(reset_lbl, &font_styrene_16, 0);
|
||||||
|
lv_obj_set_style_text_color(reset_lbl, COL_DIM, 0);
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
lv_obj_t* lbl_footer = lv_label_create(ble_container);
|
||||||
|
lv_label_set_text(lbl_footer, "Bluetooth Low Energy");
|
||||||
|
lv_obj_set_style_text_font(lbl_footer, &font_styrene_14, 0);
|
||||||
|
lv_obj_set_style_text_color(lbl_footer, COL_DIM, 0);
|
||||||
|
lv_obj_align(lbl_footer, LV_ALIGN_BOTTOM_MID, 0, -16);
|
||||||
|
|
||||||
|
// Start hidden
|
||||||
|
lv_obj_add_flag(ble_container, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
void ui_init(void) {
|
void ui_init(void) {
|
||||||
lv_obj_t* scr = lv_screen_active();
|
lv_obj_t* scr = lv_screen_active();
|
||||||
lv_obj_set_style_bg_color(scr, COL_BG, 0);
|
lv_obj_set_style_bg_color(scr, COL_BG, 0);
|
||||||
@@ -403,8 +488,9 @@ void ui_init(void) {
|
|||||||
|
|
||||||
init_usage_screen(scr);
|
init_usage_screen(scr);
|
||||||
init_controller_screen(scr);
|
init_controller_screen(scr);
|
||||||
|
init_bluetooth_screen(scr);
|
||||||
|
|
||||||
// Logo on top of both containers
|
// Logo on top of all containers
|
||||||
lv_obj_t* img = lv_image_create(scr);
|
lv_obj_t* img = lv_image_create(scr);
|
||||||
lv_image_set_src(img, &logo_dsc);
|
lv_image_set_src(img, &logo_dsc);
|
||||||
lv_obj_set_pos(img, 10, 4);
|
lv_obj_set_pos(img, 10, 4);
|
||||||
@@ -443,7 +529,7 @@ void ui_update(const UsageData* data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ui_tick_anim(void) {
|
void ui_tick_anim(void) {
|
||||||
if (controller_shown) return; // no animation on controller screen
|
if (current_screen != SCREEN_USAGE) return; // animation only on usage screen
|
||||||
|
|
||||||
uint32_t now = lv_tick_get();
|
uint32_t now = lv_tick_get();
|
||||||
|
|
||||||
@@ -464,22 +550,56 @@ void ui_tick_anim(void) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ui_show_controller(void) {
|
void ui_show_screen(screen_t screen) {
|
||||||
lv_obj_add_flag(usage_container, LV_OBJ_FLAG_HIDDEN);
|
lv_obj_add_flag(usage_container, LV_OBJ_FLAG_HIDDEN);
|
||||||
lv_obj_clear_flag(ctrl_container, LV_OBJ_FLAG_HIDDEN);
|
|
||||||
controller_shown = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ui_show_usage(void) {
|
|
||||||
lv_obj_add_flag(ctrl_container, LV_OBJ_FLAG_HIDDEN);
|
lv_obj_add_flag(ctrl_container, LV_OBJ_FLAG_HIDDEN);
|
||||||
lv_obj_clear_flag(usage_container, LV_OBJ_FLAG_HIDDEN);
|
lv_obj_add_flag(ble_container, LV_OBJ_FLAG_HIDDEN);
|
||||||
controller_shown = false;
|
|
||||||
|
switch (screen) {
|
||||||
|
case SCREEN_USAGE: lv_obj_clear_flag(usage_container, LV_OBJ_FLAG_HIDDEN); break;
|
||||||
|
case SCREEN_CONTROLLER: lv_obj_clear_flag(ctrl_container, LV_OBJ_FLAG_HIDDEN); break;
|
||||||
|
case SCREEN_BLUETOOTH: lv_obj_clear_flag(ble_container, LV_OBJ_FLAG_HIDDEN); break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
current_screen = screen;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ui_is_controller_shown(void) {
|
void ui_cycle_screen(void) {
|
||||||
return controller_shown;
|
ui_show_screen((screen_t)((current_screen + 1) % SCREEN_COUNT));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ui_set_status(const char* text) {
|
screen_t ui_get_current_screen(void) {
|
||||||
(void)text;
|
return current_screen;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ui_update_ble_status(ble_state_t state, const char* name, const char* mac) {
|
||||||
|
switch (state) {
|
||||||
|
case BLE_STATE_CONNECTED:
|
||||||
|
lv_label_set_text(lbl_ble_status, "Connected");
|
||||||
|
lv_obj_set_style_text_color(lbl_ble_status, COL_GREEN, 0);
|
||||||
|
break;
|
||||||
|
case BLE_STATE_ADVERTISING:
|
||||||
|
lv_label_set_text(lbl_ble_status, "Advertising...");
|
||||||
|
lv_obj_set_style_text_color(lbl_ble_status, COL_AMBER, 0);
|
||||||
|
break;
|
||||||
|
case BLE_STATE_DISCONNECTED:
|
||||||
|
lv_label_set_text(lbl_ble_status, "Disconnected");
|
||||||
|
lv_obj_set_style_text_color(lbl_ble_status, COL_RED, 0);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
lv_label_set_text(lbl_ble_status, "Initializing...");
|
||||||
|
lv_obj_set_style_text_color(lbl_ble_status, COL_DIM, 0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
static char nbuf[48];
|
||||||
|
snprintf(nbuf, sizeof(nbuf), "Device: %s", name);
|
||||||
|
lv_label_set_text(lbl_ble_device, nbuf);
|
||||||
|
}
|
||||||
|
if (mac) {
|
||||||
|
static char mbuf[48];
|
||||||
|
snprintf(mbuf, sizeof(mbuf), "Address: %s", mac);
|
||||||
|
lv_label_set_text(lbl_ble_mac, mbuf);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "data.h"
|
#include "data.h"
|
||||||
|
#include "ble.h"
|
||||||
|
|
||||||
|
enum screen_t {
|
||||||
|
SCREEN_USAGE,
|
||||||
|
SCREEN_CONTROLLER,
|
||||||
|
SCREEN_BLUETOOTH,
|
||||||
|
SCREEN_COUNT,
|
||||||
|
};
|
||||||
|
|
||||||
void ui_init(void);
|
void ui_init(void);
|
||||||
void ui_update(const UsageData* data);
|
void ui_update(const UsageData* data);
|
||||||
void ui_tick_anim(void);
|
void ui_tick_anim(void);
|
||||||
void ui_set_status(const char* text);
|
void ui_show_screen(screen_t screen);
|
||||||
void ui_show_controller(void);
|
void ui_cycle_screen(void);
|
||||||
void ui_show_usage(void);
|
screen_t ui_get_current_screen(void);
|
||||||
bool ui_is_controller_shown(void);
|
void ui_update_ble_status(ble_state_t state, const char* name, const char* mac);
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Flash helper for TinyUSB mode firmware.
|
# Build and flash firmware to the SC01 Plus.
|
||||||
# The built-in JTAG bootloader doesn't auto-reset with TinyUSB, so we
|
|
||||||
# trigger a reset via DTR/RTS on the CDC serial port before flashing.
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
@@ -11,34 +9,8 @@ echo "=== Flashing Claude Usage Tracker ==="
|
|||||||
echo "Port: $PORT"
|
echo "Port: $PORT"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Build first
|
|
||||||
echo "[1/3] Building firmware..."
|
|
||||||
cd "$SCRIPT_DIR/firmware"
|
cd "$SCRIPT_DIR/firmware"
|
||||||
~/.platformio/penv/bin/pio run
|
~/.platformio/penv/bin/pio run -t upload --upload-port "$PORT"
|
||||||
|
|
||||||
# Reset the device into bootloader via DTR/RTS
|
|
||||||
echo "[2/3] Resetting device into bootloader..."
|
|
||||||
python3 -c "
|
|
||||||
import serial, time
|
|
||||||
port = serial.Serial('$PORT', 115200)
|
|
||||||
port.dtr = False; port.rts = True; time.sleep(0.1)
|
|
||||||
port.dtr = True; port.rts = False; time.sleep(0.05)
|
|
||||||
port.dtr = False; time.sleep(0.1); port.close()
|
|
||||||
"
|
|
||||||
sleep 0.5
|
|
||||||
|
|
||||||
# Flash with no_reset (device is already in bootloader)
|
|
||||||
echo "[3/3] Flashing..."
|
|
||||||
python3 ~/.platformio/packages/tool-esptoolpy/esptool.py \
|
|
||||||
--chip esp32s3 \
|
|
||||||
--port "$PORT" \
|
|
||||||
--before no_reset \
|
|
||||||
--baud 921600 \
|
|
||||||
write_flash -z \
|
|
||||||
0x0 .pio/build/sc01plus/bootloader.bin \
|
|
||||||
0x8000 .pio/build/sc01plus/partitions.bin \
|
|
||||||
0xe000 ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin \
|
|
||||||
0x10000 .pio/build/sc01plus/firmware.bin
|
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Done! Device will reboot automatically. ==="
|
echo "=== Done! ==="
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ echo ""
|
|||||||
|
|
||||||
# Check dependencies
|
# Check dependencies
|
||||||
echo "[1/3] Checking dependencies..."
|
echo "[1/3] Checking dependencies..."
|
||||||
for cmd in curl stty inotifywait; do
|
for cmd in curl awk bluetoothctl busctl; do
|
||||||
command -v "$cmd" >/dev/null || { echo "Error: $cmd is required but not installed"; exit 1; }
|
command -v "$cmd" >/dev/null || { echo "Error: $cmd is required but not installed"; exit 1; }
|
||||||
done
|
done
|
||||||
echo " All dependencies found (curl, python3, stty)"
|
echo " All dependencies found"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Install systemd user service with resolved path
|
# Install systemd user service with resolved path
|
||||||
@@ -24,7 +24,7 @@ DAEMON_BIN="$SCRIPT_DIR/daemon/$SERVICE_NAME.sh"
|
|||||||
sed "s|DAEMON_PATH|${DAEMON_BIN}|g" "$SERVICE_FILE" > "$USER_SERVICE_DIR/$SERVICE_NAME.service"
|
sed "s|DAEMON_PATH|${DAEMON_BIN}|g" "$SERVICE_FILE" > "$USER_SERVICE_DIR/$SERVICE_NAME.service"
|
||||||
systemctl --user daemon-reload
|
systemctl --user daemon-reload
|
||||||
|
|
||||||
# Enable (service starts automatically when device is plugged in)
|
# Enable service
|
||||||
echo "[3/3] Enabling service..."
|
echo "[3/3] Enabling service..."
|
||||||
systemctl --user enable "$SERVICE_NAME"
|
systemctl --user enable "$SERVICE_NAME"
|
||||||
|
|
||||||
@@ -32,7 +32,15 @@ echo ""
|
|||||||
echo "=== Done! ==="
|
echo "=== Done! ==="
|
||||||
echo ""
|
echo ""
|
||||||
echo "The daemon will now start automatically when you log in"
|
echo "The daemon will now start automatically when you log in"
|
||||||
echo "and the SC01 Plus is connected via USB."
|
echo "and connect to the SC01 Plus over Bluetooth Low Energy."
|
||||||
|
echo ""
|
||||||
|
echo "First-time Bluetooth pairing:"
|
||||||
|
echo " 1. Power on the SC01 Plus"
|
||||||
|
echo " 2. Run: bluetoothctl scan le"
|
||||||
|
echo " 3. Find 'Claude Controller' and note the MAC address"
|
||||||
|
echo " 4. Run: bluetoothctl pair <MAC>"
|
||||||
|
echo " 5. Run: bluetoothctl trust <MAC>"
|
||||||
|
echo " 6. Start the daemon: systemctl --user start $SERVICE_NAME"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Useful commands:"
|
echo "Useful commands:"
|
||||||
echo " systemctl --user status $SERVICE_NAME # check status"
|
echo " systemctl --user status $SERVICE_NAME # check status"
|
||||||
@@ -40,8 +48,3 @@ echo " journalctl --user -u $SERVICE_NAME -f # view logs"
|
|||||||
echo " systemctl --user restart $SERVICE_NAME # restart"
|
echo " systemctl --user restart $SERVICE_NAME # restart"
|
||||||
echo " systemctl --user stop $SERVICE_NAME # stop"
|
echo " systemctl --user stop $SERVICE_NAME # stop"
|
||||||
echo ""
|
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"
|
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Take a screenshot from the SC01 Plus display via LVGL snapshot.
|
||||||
|
# Usage: ./screenshot.sh [output.png] [port]
|
||||||
|
|
||||||
|
OUTPUT="${1:-screenshot.png}"
|
||||||
|
PORT="${2:-/dev/ttyACM0}"
|
||||||
|
|
||||||
|
TMPRAW=$(mktemp /tmp/screenshot_XXXXXX.raw)
|
||||||
|
trap "rm -f '$TMPRAW'" EXIT
|
||||||
|
|
||||||
|
echo "Taking screenshot from $PORT..."
|
||||||
|
|
||||||
|
python3 - "$PORT" "$TMPRAW" << 'PYEOF'
|
||||||
|
import serial, sys
|
||||||
|
|
||||||
|
port_path, raw_path = sys.argv[1], sys.argv[2]
|
||||||
|
|
||||||
|
port = serial.Serial(port_path, 115200, timeout=10)
|
||||||
|
port.reset_input_buffer()
|
||||||
|
port.write(b"screenshot\n")
|
||||||
|
port.flush()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
line = port.readline().decode("utf-8", errors="replace").strip()
|
||||||
|
if line.startswith("SCREENSHOT_START"):
|
||||||
|
parts = line.split()
|
||||||
|
w, h, raw_size = int(parts[1]), int(parts[2]), int(parts[3])
|
||||||
|
break
|
||||||
|
if line == "SCREENSHOT_ERR":
|
||||||
|
print("Device reported screenshot error", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
data = b""
|
||||||
|
while len(data) < raw_size:
|
||||||
|
chunk = port.read(min(4096, raw_size - len(data)))
|
||||||
|
if not chunk:
|
||||||
|
print(f"Timeout: got {len(data)} of {raw_size} bytes", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
data += chunk
|
||||||
|
|
||||||
|
with open(raw_path, "wb") as f:
|
||||||
|
f.write(data)
|
||||||
|
|
||||||
|
for _ in range(10):
|
||||||
|
line = port.readline().decode("utf-8", errors="replace").strip()
|
||||||
|
if line == "SCREENSHOT_END":
|
||||||
|
break
|
||||||
|
|
||||||
|
port.close()
|
||||||
|
print(f"Captured {w}x{h} ({len(data)} bytes)")
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Screenshot capture failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ffmpeg -y -f rawvideo -pixel_format rgb565le -video_size 480x320 \
|
||||||
|
-i "$TMPRAW" -update 1 -frames:v 1 "$OUTPUT" 2>/dev/null || true
|
||||||
|
|
||||||
|
if [ -f "$OUTPUT" ]; then
|
||||||
|
echo "Saved: $OUTPUT"
|
||||||
|
else
|
||||||
|
echo "Error: conversion failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 8.3 KiB |