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>
This commit is contained in:
Hermann Björgvin Haraldsson
2026-04-04 19:30:19 +00:00
co-authored by Claude Opus 4.6
parent 930055170f
commit 10b8052bcc
22 changed files with 917 additions and 167 deletions
+9 -1
View File
@@ -7,8 +7,14 @@ monitor_speed = 115200
build_flags =
-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
; 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
-DLV_CONF_SKIP
-DLV_COLOR_DEPTH=16
@@ -29,8 +35,10 @@ build_flags =
-DLV_USE_IMAGE=1
-DLV_USE_ANIMIMG=0
-DLV_TICK_CUSTOM=1
-DLV_USE_SNAPSHOT=1
lib_deps =
lovyan03/LovyanGFX@^1.1.16
lvgl/lvgl@^9.2.0
bblanchon/ArduinoJson@^7.0.0
h2zero/NimBLE-Arduino@^2.1.1
+207
View File
@@ -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;
}
+26
View File
@@ -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);
+26 -18
View File
@@ -1,10 +1,21 @@
#include "hid.h"
#include "USB.h"
#include "USBHIDKeyboard.h"
#include "ble.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 {
gesture_t gesture;
uint8_t key;
@@ -14,43 +25,40 @@ struct gesture_mapping_t {
static const gesture_mapping_t mappings[] = {
{ GESTURE_SWIPE_UP, HID_KEY_ARROW_UP, 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_TAP_ESCAPE, HID_KEY_ESCAPE, 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_RIGHT, HID_KEY_ARROW_RIGHT, 0 },
};
#define MAPPING_COUNT (sizeof(mappings) / sizeof(mappings[0]))
#define HOLD_KEY HID_KEY_SPACE
void hid_init(void) {
USB.begin();
keyboard.begin();
keyboard.releaseAll();
// BLE HID is initialized in ble_init()
}
void hid_on_gesture(gesture_t gesture) {
if (!ble_is_connected()) return;
if (gesture == GESTURE_HOLD_START) {
keyboard.pressRaw(HOLD_KEY);
ble_keyboard_press(HID_KEY_SPACE, 0);
return;
}
if (gesture == GESTURE_HOLD_END) {
keyboard.releaseRaw(HOLD_KEY);
ble_keyboard_release();
return;
}
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].modifier) keyboard.press(mappings[i].modifier);
keyboard.pressRaw(mappings[i].key);
ble_keyboard_press(mappings[i].key, mappings[i].modifier);
delay(20);
keyboard.releaseAll();
ble_keyboard_release();
return;
}
}
+73
View File
@@ -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,
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,
};
+73 -19
View File
@@ -6,6 +6,7 @@
#include "ui.h"
#include "hid.h"
#include "touch.h"
#include "ble.h"
LGFX lcd; // global - used by touch.cpp
static UsageData usage = {};
@@ -15,11 +16,6 @@ static UsageData usage = {};
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();
@@ -67,20 +63,56 @@ static bool parse_json(const char* json, UsageData* out) {
return true;
}
// Read serial data, returns true when a complete line is ready
static bool read_serial_line() {
// Serial command buffer
#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()) {
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;
if (c == '\n' || c == '\r') {
cmd_buf[cmd_pos] = '\0';
if (strcmp(cmd_buf, "screenshot") == 0) {
send_screenshot();
}
cmd_pos = 0;
} else if (cmd_pos < CMD_BUF_SIZE - 1) {
cmd_buf[cmd_pos++] = c;
}
}
return false;
}
void setup() {
@@ -110,24 +142,46 @@ void setup() {
// Init USB HID keyboard
hid_init();
// Init BLE data channel
ble_init();
// Init touch gesture detection
touch_init();
// Build dashboard
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() {
lv_timer_handler();
ui_tick_anim();
touch_tick();
ble_tick();
if (read_serial_line()) {
if (parse_json(serial_buf, &usage)) {
// Update BLE status on screen when state changes
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);
Serial.println("{\"ack\":true}");
ble_send_ack();
} else {
ble_send_nack();
}
}
+16 -6
View File
@@ -1,5 +1,6 @@
#include "touch.h"
#include "ui.h"
#include "ble.h"
#include <Arduino.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;
}
// 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) {
state = TS_IDLE;
}
@@ -89,15 +95,19 @@ void touch_tick(void) {
abs((int)last_y - (int)start_y) < TAP_MAX_MOVE_PX;
if (is_tap && in_logo(start_x, start_y)) {
// Logo tap toggles screen (works on both screens)
if (ui_is_controller_shown()) ui_show_usage();
else ui_show_controller();
} else if (is_tap && ui_is_controller_shown()) {
// Logo tap cycles screens: Usage -> Controller -> Bluetooth -> ...
ui_cycle_screen();
} else if (is_tap && ui_get_current_screen() == SCREEN_CONTROLLER) {
// Zone taps only on controller screen
gesture_t zone = classify_zone_tap(start_x, start_y);
if (zone != GESTURE_NONE) {
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;
} else {
@@ -107,12 +117,12 @@ void touch_tick(void) {
int dy = (int)y - (int)start_y;
// 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);
state = TS_LOGO_HOLD;
}
// 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) {
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) {
+135 -15
View File
@@ -43,9 +43,15 @@ static lv_obj_t* ctrl_session_bar;
static lv_obj_t* ctrl_session_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 ----
static lv_image_dsc_t logo_dsc;
static bool controller_shown = false;
static screen_t current_screen = SCREEN_USAGE;
// Animation state
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);
}
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) {
lv_obj_t* scr = lv_screen_active();
lv_obj_set_style_bg_color(scr, COL_BG, 0);
@@ -403,8 +488,9 @@ void ui_init(void) {
init_usage_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_image_set_src(img, &logo_dsc);
lv_obj_set_pos(img, 10, 4);
@@ -443,7 +529,7 @@ void ui_update(const UsageData* data) {
}
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();
@@ -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_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_clear_flag(usage_container, LV_OBJ_FLAG_HIDDEN);
controller_shown = false;
lv_obj_add_flag(ble_container, LV_OBJ_FLAG_HIDDEN);
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) {
return controller_shown;
void ui_cycle_screen(void) {
ui_show_screen((screen_t)((current_screen + 1) % SCREEN_COUNT));
}
void ui_set_status(const char* text) {
(void)text;
screen_t ui_get_current_screen(void) {
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);
}
}
+12 -4
View File
@@ -1,10 +1,18 @@
#pragma once
#include "data.h"
#include "ble.h"
enum screen_t {
SCREEN_USAGE,
SCREEN_CONTROLLER,
SCREEN_BLUETOOTH,
SCREEN_COUNT,
};
void ui_init(void);
void ui_update(const UsageData* data);
void ui_tick_anim(void);
void ui_set_status(const char* text);
void ui_show_controller(void);
void ui_show_usage(void);
bool ui_is_controller_shown(void);
void ui_show_screen(screen_t screen);
void ui_cycle_screen(void);
screen_t ui_get_current_screen(void);
void ui_update_ble_status(ble_state_t state, const char* name, const char* mac);