Schematik build
DIY ESP Phone
Build a touchscreen mini phone powered by an ESP32 that runs a full suite of mobile-style apps. This project uses the built-in display, touch input, and wireless connectivity to create a pocket-sized device with a home screen, app launcher, and interactive applications including a clock, weather, news reader, and game.
This guide provides the complete wiring diagram, parts list, and firmware to get the device running. Simply connect USB power and start exploring the touch interface. The firmware uses LVGL for the graphical interface and connects to WiFi for internet-based apps, making this an excellent platform for learning embedded UI design and ESP32 development.
Wiring diagram
Assemble it in 3 steps
1. Leave the built-in parts alone
Use the display and touch panel already fitted to the Waveshare board. The screen, its backlight, and the touch panel are connected inside the board, so do not add jumper wires to GPIO26, GPIO27, GPIO7, or GPIO8.
- The project uses the board’s built-in display support, which sets up the fitted screen and touch panel for you.
- Do not connect external wires to the fitted screen or touch pins — a mistaken connection can stop the display or touch panel from working.
2. Connect USB power
Place the board on a non-conductive surface and connect a data-capable USB cable to its USB port, then to your computer. This cable powers the board and carries the firmware from Schematik.
- No battery, SD card, camera, wireless setup, microphone, or speaker is needed for this version.
- Keep loose metal objects away from the board while it is powered, because they can short exposed contacts.
3. Use the touch interface
After deployment, tap a tile to open it. Tap the Back button at the top or the warm-white bar at the bottom to return Home. In Calculator, tap the large keys; C clears an error or calculation, and the left-arrow key removes the last entered character.
- The calculator stays in its current state while you return Home and open it again during the same powered-on session.
- The first physical test should confirm that taps land on the tile or button you touched; if they do not, stop and report the direction and amount of the offset.
Deploy the firmware
#include <Arduino.h>
#include <lvgl.h>
#include "schematik_display.h"
#include <WiFi.h>
#include "clock_app.h"
#include "internet_apps.h"
#include "metro_run.h"
#include <math.h>
#include <string.h>
// The Waveshare BSP owns the ST7701/DSI panel, GPIO26 backlight, GT911 touch,
// LVGL ticks, and its GUI task. This sketch creates only the UI.
enum AppId {
PHONE, MESSAGES, CAMERA, PHOTOS, CALCULATOR, CLOCK, NOTES, FILES, WEATHER,
CALENDAR, SETTINGS, MAPS, CONTACTS, MAIL, BROWSER, TASKS, REMINDERS, HEALTH,
WALLET, COMPASS, BOOKS, NEWS, HELP, ABOUT, METRO_RUN
};
struct AppInfo { const char *name; const char *icon; bool orange; const char *description; };
// Forward declarations
// Forward declarations
static void showClock();
static void showWeather();
static void showNews();
static void showMetroRun();
static void homeEvent(lv_event_t *event);
static void backEvent(lv_event_t *event);
static void appEvent(lv_event_t *event);
static void pageEvent(lv_event_t *event);
static void homeSwipeEvent(lv_event_t *event);
// Wi-Fi uses the board's factory-programmed ESP32-C6 helper. Credentials stay
// in RAM for this session only; this project never updates the helper or saves
// a network profile to flash.
// ESP32-C6 ESP-Hosted SDIO wiring on this Waveshare ESP32-P4 board.
// It must be supplied to Arduino before the first Wi-Fi operation.
static constexpr int HOSTED_CLK = 18;
static constexpr int HOSTED_CMD = 19;
static constexpr int HOSTED_D0 = 14;
static constexpr int HOSTED_D1 = 15;
static constexpr int HOSTED_D2 = 16;
static constexpr int HOSTED_D3 = 17;
static constexpr int HOSTED_RESET = 54;
static bool hostedPinsConfigured = false;
static bool settingsVisible = false;
static bool scanPending = false;
static bool wifiConnecting = false;
static uint32_t wifiConnectStartedAt = 0;
static int scanResultCount = -2;
static String selectedNetwork;
static lv_obj_t *wifiStatusLabel = nullptr;
static lv_obj_t *wifiList = nullptr;
static lv_obj_t *passwordBox = nullptr;
static lv_obj_t *keyboard = nullptr;
static void configureHostedWifi();
static void startWifiScan();
static void pollWifi();
static void wifiScanEvent(lv_event_t *event);
static void wifiNetworkEvent(lv_event_t *event);
static void wifiConnectEvent(lv_event_t *event);
static void wifiPasswordEvent(lv_event_t *event);
static void wifiPasswordFocusEvent(lv_event_t *event);
namespace Theme {
const lv_color_t BG = lv_color_hex(0x181818);
const lv_color_t TILE = lv_color_hex(0x2C2B2C);
const lv_color_t TEXT = lv_color_hex(0xF1ECE3);
const lv_color_t MUTED = lv_color_hex(0x858081);
const lv_color_t ACCENT = lv_color_hex(0xFB7040);
const lv_color_t DIVIDER = lv_color_hex(0x373635);
}
const AppInfo APPS[] = {
{"Phone", "()", true, "Phone will be available in a future version."},
{"Messages", "[]", false, "Messages will be available in a future version."},
{"Camera", "O", false, "Camera will be available in a future version."},
{"Photos", "o", true, "Photos will be available in a future version."},
{"Calculator", "+", false, ""},
{"Clock", "@", true, "Clock will be available in a future version."},
{"Notes", "=", false, "Notes will be available in a future version."},
{"Files", "#", false, "Files will be available in a future version."},
{"Weather", "~", false, "Weather will be available in a future version."},
{"Calendar", "7", false, "Calendar will be available in a future version."},
{"Settings", "*", false, "Wi-Fi setup is not available on this firmware target yet."},
{"Maps", "><", false, "Maps will be available in a future version."},
{"Contacts", "@", false, "Contacts will be available in a future version."},
{"Mail", "[]", false, "Mail will be available in a future version."},
{"Browser", "o", false, "Browser will be available in a future version."},
{"Tasks", "+", false, "Tasks will be available in a future version."},
{"Reminders", "!", true, "Reminders will be available in a future version."},
{"Health", "+", false, "Health will be available in a future version."},
{"Wallet", "$", false, "Wallet will be available in a future version."},
{"Compass", "N", false, "Compass will be available in a future version."},
{"Books", "=", false, "Books will be available in a future version."},
{"News", "i", false, "News will be available in a future version."},
{"Help", "?", false, "Help will be available in a future version."},
{"About", "i", false, "About will be available in a future version."},
{"Metro Run", ">", true, ""}
};
static constexpr int APP_COUNT = sizeof(APPS) / sizeof(APPS[0]);
static constexpr int HOME_PAGE_COUNT = (APP_COUNT + 11) / 12;
static lv_obj_t *calcDisplay = nullptr;
static String entry = "0";
static double accumulator = 0.0;
static char pendingOp = 0;
static bool freshEntry = true;
static bool calcError = false;
static uint8_t homePage = 0;
static void showHome();
static void openApp(AppId app);
static void baseScreen();
static void styleLabel(lv_obj_t *label, lv_color_t color, const lv_font_t *font);
static void buttonStyle(lv_obj_t *button, lv_color_t normal, lv_color_t pressed, int radius);
static void makeHomeIndicator();
static void makeHeader(const char *title);
static void showPlaceholder(AppId app);
static void showSettings();
static void showWifiPasswordScreen();
static void makeWifiNetworkList();
static String formatNumber(double value);
static void updateCalcDisplay();
static void setError();
static bool applyPending();
static void calculatorEvent(lv_event_t *event);
static void calcKey(const char *text, int x, int y, int w, bool accent);
static void showCalculator();
static void baseScreen() {
clockAppDeactivate();
weatherAppDeactivate();
newsAppDeactivate();
metroRunDeactivate();
settingsVisible = false;
wifiStatusLabel = nullptr;
wifiList = nullptr;
passwordBox = nullptr;
keyboard = nullptr;
lv_obj_t *screen = lv_scr_act();
while (lv_obj_remove_event_cb(screen, homeSwipeEvent)) {}
lv_obj_clean(screen);
lv_obj_set_style_bg_color(screen, Theme::BG, 0);
lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, 0);
lv_obj_clear_flag(screen, LV_OBJ_FLAG_SCROLLABLE);
}
static void styleLabel(lv_obj_t *label, lv_color_t color, const lv_font_t *font) {
lv_obj_set_style_text_color(label, color, 0);
lv_obj_set_style_text_font(label, font, 0);
lv_obj_clear_flag(label, LV_OBJ_FLAG_CLICKABLE);
}
static void buttonStyle(lv_obj_t *button, lv_color_t normal, lv_color_t pressed, int radius) {
lv_obj_set_style_bg_color(button, normal, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(button, LV_OPA_COVER, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_bg_color(button, pressed, LV_PART_MAIN | LV_STATE_PRESSED);
lv_obj_set_style_radius(button, radius, 0);
lv_obj_set_style_border_width(button, 0, 0);
lv_obj_set_style_shadow_width(button, 0, 0);
lv_obj_clear_flag(button, LV_OBJ_FLAG_SCROLLABLE);
}
static void homeEvent(lv_event_t *event) { homePage = 0; showHome(); }
static void backEvent(lv_event_t *event) { homePage = 0; showHome(); }
static void appEvent(lv_event_t *event) { openApp((AppId)(uintptr_t)lv_event_get_user_data(event)); }
static void pageEvent(lv_event_t *event) { homePage = (homePage + 1) % HOME_PAGE_COUNT; showHome(); }
// LVGL recognizes the GT911 finger movement as a gesture. Page changes happen
// only for horizontal swipes, so ordinary taps still open the selected app.
static void homeSwipeEvent(lv_event_t *event) {
lv_indev_t *indev = lv_indev_get_act();
if (!indev) return;
lv_dir_t direction = lv_indev_get_gesture_dir(indev);
if (direction == LV_DIR_LEFT && homePage + 1 < HOME_PAGE_COUNT) {
homePage++;
showHome();
} else if (direction == LV_DIR_RIGHT && homePage > 0) {
homePage--;
showHome();
}
}
static void makeHomeIndicator() {
lv_obj_t *hit = lv_btn_create(lv_scr_act());
lv_obj_set_size(hit, 160, 48);
lv_obj_set_pos(hit, 160, 738);
lv_obj_set_style_bg_opa(hit, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(hit, 0, 0);
lv_obj_set_style_shadow_width(hit, 0, 0);
lv_obj_add_event_cb(hit, homeEvent, LV_EVENT_CLICKED, nullptr);
lv_obj_t *bar = lv_obj_create(hit);
lv_obj_set_size(bar, 86, 5);
lv_obj_center(bar);
lv_obj_set_style_bg_color(bar, Theme::TEXT, 0);
lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, 0);
lv_obj_set_style_radius(bar, 4, 0);
lv_obj_set_style_border_width(bar, 0, 0);
lv_obj_clear_flag(bar, LV_OBJ_FLAG_CLICKABLE);
}
static void makeHeader(const char *title) {
lv_obj_t *back = lv_btn_create(lv_scr_act());
lv_obj_set_size(back, 96, 52);
lv_obj_set_pos(back, 18, 20);
buttonStyle(back, Theme::TILE, lv_color_hex(0x454345), 16);
lv_obj_add_event_cb(back, backEvent, LV_EVENT_CLICKED, nullptr);
lv_obj_t *backText = lv_label_create(back);
lv_label_set_text(backText, "< Back");
styleLabel(backText, Theme::TEXT, &lv_font_montserrat_16);
lv_obj_center(backText);
lv_obj_t *heading = lv_label_create(lv_scr_act());
lv_label_set_text(heading, title);
styleLabel(heading, Theme::TEXT, &lv_font_montserrat_24);
lv_obj_align(heading, LV_ALIGN_TOP_MID, 0, 33);
lv_obj_t *line = lv_obj_create(lv_scr_act());
lv_obj_set_size(line, 444, 1);
lv_obj_set_pos(line, 18, 84);
lv_obj_set_style_bg_color(line, Theme::DIVIDER, 0);
lv_obj_set_style_border_width(line, 0, 0);
}
static void showHome() {
baseScreen();
// Receive swipes from empty Home-screen space and from the app tiles below.
lv_obj_add_event_cb(lv_scr_act(), homeSwipeEvent, LV_EVENT_GESTURE, nullptr);
lv_obj_t *title = lv_label_create(lv_scr_act());
lv_label_set_text(title, "Home");
styleLabel(title, Theme::TEXT, &lv_font_montserrat_28);
lv_obj_set_pos(title, 24, 27);
lv_obj_t *pageButton = lv_btn_create(lv_scr_act());
lv_obj_set_size(pageButton, 98, 46);
lv_obj_set_pos(pageButton, 358, 21);
buttonStyle(pageButton, Theme::TILE, lv_color_hex(0x454345), 16);
lv_obj_add_event_cb(pageButton, pageEvent, LV_EVENT_CLICKED, nullptr);
lv_obj_t *pageText = lv_label_create(pageButton);
lv_label_set_text_fmt(pageText, "%d / %d >", homePage + 1, HOME_PAGE_COUNT);
styleLabel(pageText, Theme::TEXT, &lv_font_montserrat_14);
lv_obj_center(pageText);
lv_obj_t *line = lv_obj_create(lv_scr_act());
lv_obj_set_size(line, 432, 1);
lv_obj_set_pos(line, 24, 78);
lv_obj_set_style_bg_color(line, Theme::DIVIDER, 0);
lv_obj_set_style_border_width(line, 0, 0);
const int xs[3] = {50, 192, 334};
const int ys[4] = {104, 260, 416, 572};
const int firstApp = homePage * 12;
for (int i = 0; i < 12 && firstApp + i < APP_COUNT; ++i) {
int col = i % 3;
int row = i / 3;
const AppInfo &app = APPS[firstApp + i];
lv_color_t tileColor = app.orange ? Theme::ACCENT : Theme::TILE;
lv_color_t ink = app.orange ? Theme::BG : Theme::TEXT;
lv_obj_t *tile = lv_btn_create(lv_scr_act());
lv_obj_set_size(tile, 96, 96);
lv_obj_set_pos(tile, xs[col], ys[row]);
buttonStyle(tile, tileColor, app.orange ? lv_color_hex(0xD95D34) : lv_color_hex(0x454345), 26);
lv_obj_add_event_cb(tile, appEvent, LV_EVENT_CLICKED, (void *)(uintptr_t)(firstApp + i));
lv_obj_add_flag(tile, LV_OBJ_FLAG_GESTURE_BUBBLE);
lv_obj_t *glyph = lv_label_create(tile);
lv_label_set_text(glyph, app.icon);
styleLabel(glyph, ink, &lv_font_montserrat_40);
lv_obj_center(glyph);
lv_obj_t *label = lv_label_create(lv_scr_act());
lv_label_set_text(label, app.name);
styleLabel(label, Theme::TEXT, &lv_font_montserrat_14);
lv_obj_set_width(label, 128);
lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_pos(label, xs[col] - 16, ys[row] + 106);
}
makeHomeIndicator();
}
static void showClock() {
baseScreen();
makeHeader("Clock");
showClockApp();
makeHomeIndicator();
}
static void showWeather() {
baseScreen();
makeHeader("Weather");
showWeatherApp();
makeHomeIndicator();
}
static void showNews() {
baseScreen();
makeHeader("News");
showNewsApp();
makeHomeIndicator();
}
static void showMetroRun() {
baseScreen();
makeHeader("Metro Run");
showMetroRunApp();
makeHomeIndicator();
}
static void showPlaceholder(AppId app) {
baseScreen();
makeHeader(APPS[app].name);
lv_obj_t *icon = lv_label_create(lv_scr_act());
lv_label_set_text(icon, APPS[app].icon);
styleLabel(icon, APPS[app].orange ? Theme::ACCENT : Theme::TEXT, &lv_font_montserrat_48);
lv_obj_align(icon, LV_ALIGN_TOP_MID, 0, 210);
lv_obj_t *soon = lv_label_create(lv_scr_act());
lv_label_set_text(soon, "Coming soon");
styleLabel(soon, Theme::TEXT, &lv_font_montserrat_28);
lv_obj_align(soon, LV_ALIGN_TOP_MID, 0, 302);
lv_obj_t *desc = lv_label_create(lv_scr_act());
lv_label_set_text(desc, APPS[app].description);
styleLabel(desc, Theme::MUTED, &lv_font_montserrat_16);
lv_obj_set_width(desc, 370);
lv_obj_set_style_text_align(desc, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_align(desc, LV_ALIGN_TOP_MID, 0, 354);
makeHomeIndicator();
}
static void makeWifiNetworkList() {
if (!wifiList) return;
lv_obj_clean(wifiList);
if (scanResultCount <= 0) {
lv_obj_t *empty = lv_label_create(wifiList);
lv_label_set_text(empty, scanResultCount == 0 ? "No networks found" : "Looking for nearby networks...");
styleLabel(empty, Theme::MUTED, &lv_font_montserrat_16);
lv_obj_center(empty);
return;
}
for (int i = 0; i < scanResultCount && i < 5; ++i) {
String ssid = WiFi.SSID(i);
if (!ssid.length()) continue;
lv_obj_t *network = lv_btn_create(wifiList);
lv_obj_set_size(network, 396, 45);
lv_obj_set_pos(network, 0, i * 51);
buttonStyle(network, Theme::TILE, lv_color_hex(0x454345), 12);
lv_obj_add_event_cb(network, wifiNetworkEvent, LV_EVENT_CLICKED, (void *)(intptr_t)i);
lv_obj_t *text = lv_label_create(network);
lv_label_set_text(text, ssid.c_str());
styleLabel(text, Theme::TEXT, &lv_font_montserrat_16);
lv_obj_align(text, LV_ALIGN_LEFT_MID, 14, 0);
}
}
static void configureHostedWifi() {
if (hostedPinsConfigured) return;
WiFi.setPins(HOSTED_CLK, HOSTED_CMD, HOSTED_D0, HOSTED_D1, HOSTED_D2, HOSTED_D3, HOSTED_RESET);
hostedPinsConfigured = true;
}
static void startWifiScan() {
configureHostedWifi();
wifiConnecting = false;
WiFi.mode(WIFI_STA);
WiFi.disconnect(false, false);
WiFi.scanDelete();
scanResultCount = -1;
scanPending = WiFi.scanNetworks(true, true) == -1;
if (wifiStatusLabel) lv_label_set_text(wifiStatusLabel, "Searching nearby networks...");
makeWifiNetworkList();
}
static void wifiScanEvent(lv_event_t *event) { startWifiScan(); }
static void wifiNetworkEvent(lv_event_t *event) {
int index = (int)(intptr_t)lv_event_get_user_data(event);
selectedNetwork = WiFi.SSID(index);
showWifiPasswordScreen();
}
static void wifiPasswordEvent(lv_event_t *event) {
const lv_event_code_t code = lv_event_get_code(event);
if (code != LV_EVENT_READY && code != LV_EVENT_CANCEL) return;
if (keyboard) lv_obj_add_flag(keyboard, LV_OBJ_FLAG_HIDDEN);
if (code == LV_EVENT_READY) wifiConnectEvent(event);
}
// Tapping the password box must always reveal the on-screen keyboard, even
// after a scan result has been selected or the keyboard was dismissed.
static void wifiPasswordFocusEvent(lv_event_t *event) {
if (!keyboard || !passwordBox) return;
lv_keyboard_set_textarea(keyboard, passwordBox);
lv_obj_clear_flag(keyboard, LV_OBJ_FLAG_HIDDEN);
}
static void wifiConnectEvent(lv_event_t *event) {
if (!selectedNetwork.length()) {
if (wifiStatusLabel) lv_label_set_text(wifiStatusLabel, "Choose a network first");
return;
}
if (wifiConnecting) return; // One connection attempt at a time.
configureHostedWifi();
const char *password = passwordBox ? lv_textarea_get_text(passwordBox) : "";
if (wifiStatusLabel) {
lv_label_set_text_fmt(wifiStatusLabel,
"Connection request sent for %s\nWaiting for the router (up to 20 seconds)...",
selectedNetwork.c_str());
}
WiFi.disconnect(false, false);
WiFi.begin(selectedNetwork.c_str(), password);
wifiConnecting = true;
wifiConnectStartedAt = millis();
}
static void pollWifi() {
if (wifiConnecting && millis() - wifiConnectStartedAt > 20000) {
wifiConnecting = false;
if (settingsVisible && schematikDisplayLock()) {
if (wifiStatusLabel) {
lv_label_set_text(wifiStatusLabel,
"Connection report: timed out after 20 seconds.\n"
"The board did not join the router. Check that this is a 2.4 GHz WPA2/WPA3 network, then try again.");
}
schematikDisplayUnlock();
}
}
if (scanPending) {
int result = WiFi.scanComplete();
if (result >= 0) {
scanPending = false;
scanResultCount = result;
if (schematikDisplayLock()) {
if (settingsVisible && wifiStatusLabel) lv_label_set_text_fmt(wifiStatusLabel, "%d network%s found", result, result == 1 ? "" : "s");
if (settingsVisible) makeWifiNetworkList();
schematikDisplayUnlock();
}
}
}
static wl_status_t lastStatus = WL_IDLE_STATUS;
wl_status_t status = WiFi.status();
if (status != lastStatus) {
lastStatus = status;
if (settingsVisible && schematikDisplayLock()) {
if (wifiStatusLabel) {
if (status == WL_CONNECTED) {
wifiConnecting = false;
lv_label_set_text_fmt(wifiStatusLabel, "Connected: %s", WiFi.localIP().toString().c_str());
} else if (status == WL_CONNECT_FAILED) {
wifiConnecting = false;
lv_label_set_text(wifiStatusLabel,
"Connection report: the router rejected the join request.\n"
"Check the password and router security mode.");
} else if (status == WL_NO_SSID_AVAIL) {
wifiConnecting = false;
lv_label_set_text(wifiStatusLabel,
"Connection report: this network is no longer visible.\n"
"Move closer to the router, scan again, then retry.");
}
}
schematikDisplayUnlock();
}
}
}
static void showSettings() {
baseScreen();
settingsVisible = true;
makeHeader("Settings");
lv_obj_t *section = lv_label_create(lv_scr_act());
lv_label_set_text(section, "Wi-Fi");
styleLabel(section, Theme::TEXT, &lv_font_montserrat_24);
lv_obj_set_pos(section, 28, 108);
wifiStatusLabel = lv_label_create(lv_scr_act());
lv_label_set_text(wifiStatusLabel, "Preparing Wi-Fi...");
styleLabel(wifiStatusLabel, Theme::MUTED, &lv_font_montserrat_16);
lv_obj_set_width(wifiStatusLabel, 400);
lv_obj_set_pos(wifiStatusLabel, 28, 145);
lv_obj_t *scan = lv_btn_create(lv_scr_act());
lv_obj_set_size(scan, 130, 42);
lv_obj_set_pos(scan, 326, 108);
buttonStyle(scan, Theme::ACCENT, lv_color_hex(0xD95D34), 14);
lv_obj_add_event_cb(scan, wifiScanEvent, LV_EVENT_CLICKED, nullptr);
lv_obj_t *scanText = lv_label_create(scan);
lv_label_set_text(scanText, "Scan");
styleLabel(scanText, Theme::BG, &lv_font_montserrat_16);
lv_obj_center(scanText);
wifiList = lv_obj_create(lv_scr_act());
lv_obj_set_size(wifiList, 412, 255);
lv_obj_set_pos(wifiList, 34, 178);
lv_obj_set_style_bg_color(wifiList, Theme::BG, 0);
lv_obj_set_style_border_width(wifiList, 0, 0);
lv_obj_set_style_pad_all(wifiList, 0, 0);
lv_obj_clear_flag(wifiList, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t *hint = lv_label_create(lv_scr_act());
lv_label_set_text(hint, "Tap a network, then enter its password.");
styleLabel(hint, Theme::MUTED, &lv_font_montserrat_16);
lv_obj_set_pos(hint, 28, 454);
makeHomeIndicator();
startWifiScan();
}
// A selected network gets its own screen. This avoids placing a small password
// field below a changing scan list and guarantees that both the field and the
// keyboard are visible on the 480 x 800 portrait panel.
static void showWifiPasswordScreen() {
baseScreen();
settingsVisible = true;
makeHeader("Wi-Fi password");
lv_obj_t *network = lv_label_create(lv_scr_act());
lv_label_set_text(network, selectedNetwork.c_str());
styleLabel(network, Theme::TEXT, &lv_font_montserrat_24);
lv_obj_set_width(network, 420);
lv_obj_set_style_text_align(network, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_pos(network, 30, 112);
lv_obj_t *instruction = lv_label_create(lv_scr_act());
lv_label_set_text(instruction, "Enter the password for this network");
styleLabel(instruction, Theme::MUTED, &lv_font_montserrat_16);
lv_obj_set_width(instruction, 420);
lv_obj_set_style_text_align(instruction, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_pos(instruction, 30, 154);
passwordBox = lv_textarea_create(lv_scr_act());
lv_obj_set_size(passwordBox, 432, 62);
lv_obj_set_pos(passwordBox, 24, 194);
lv_textarea_set_one_line(passwordBox, true);
lv_textarea_set_password_mode(passwordBox, true);
lv_textarea_set_placeholder_text(passwordBox, "Password");
lv_obj_set_style_bg_color(passwordBox, Theme::TILE, 0);
lv_obj_set_style_text_color(passwordBox, Theme::TEXT, 0);
lv_obj_set_style_text_font(passwordBox, &lv_font_montserrat_18, 0);
lv_obj_set_style_radius(passwordBox, 16, 0);
lv_obj_set_style_border_width(passwordBox, 2, 0);
lv_obj_set_style_border_color(passwordBox, Theme::ACCENT, LV_STATE_FOCUSED);
lv_obj_add_event_cb(passwordBox, wifiPasswordFocusEvent, LV_EVENT_FOCUSED, nullptr);
lv_obj_add_event_cb(passwordBox, wifiPasswordFocusEvent, LV_EVENT_CLICKED, nullptr);
lv_obj_t *connect = lv_btn_create(lv_scr_act());
lv_obj_set_size(connect, 432, 54);
lv_obj_set_pos(connect, 24, 272);
buttonStyle(connect, Theme::ACCENT, lv_color_hex(0xD95D34), 16);
lv_obj_add_event_cb(connect, wifiConnectEvent, LV_EVENT_CLICKED, nullptr);
lv_obj_t *connectText = lv_label_create(connect);
lv_label_set_text(connectText, "Connect");
styleLabel(connectText, Theme::BG, &lv_font_montserrat_18);
lv_obj_center(connectText);
// This report stays on the password screen so pressing Connect always gives
// a visible result instead of silently returning to the scan page.
wifiStatusLabel = lv_label_create(lv_scr_act());
lv_label_set_text(wifiStatusLabel, "Enter the password, then tap Connect.");
styleLabel(wifiStatusLabel, Theme::MUTED, &lv_font_montserrat_14);
lv_obj_set_width(wifiStatusLabel, 432);
lv_obj_set_style_text_align(wifiStatusLabel, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_pos(wifiStatusLabel, 24, 338);
keyboard = lv_keyboard_create(lv_scr_act());
lv_obj_set_size(keyboard, 480, 392);
lv_obj_align(keyboard, LV_ALIGN_TOP_LEFT, 0, 398);
lv_keyboard_set_textarea(keyboard, passwordBox);
lv_obj_add_event_cb(keyboard, wifiPasswordEvent, LV_EVENT_READY, nullptr);
lv_obj_add_event_cb(keyboard, wifiPasswordEvent, LV_EVENT_CANCEL, nullptr);
lv_obj_add_state(passwordBox, LV_STATE_FOCUSED);
}
static String formatNumber(double value) {
if (!isfinite(value)) return "Error";
if (fabs(value) < 0.000000001) value = 0;
char out[24];
snprintf(out, sizeof(out), "%.10g", value);
return String(out);
}
static void updateCalcDisplay() {
if (calcDisplay) lv_label_set_text(calcDisplay, calcError ? "Error" : entry.c_str());
}
static void setError() {
calcError = true;
entry = "Error";
pendingOp = 0;
freshEntry = true;
updateCalcDisplay();
}
static bool applyPending() {
double right = entry.toDouble();
if (pendingOp == '+') accumulator += right;
else if (pendingOp == '-') accumulator -= right;
else if (pendingOp == '*') accumulator *= right;
else if (pendingOp == '/') {
if (right == 0.0) { setError(); return false; }
accumulator /= right;
}
if (!isfinite(accumulator)) { setError(); return false; }
entry = formatNumber(accumulator);
return true;
}
static void calculatorEvent(lv_event_t *event) {
const char *key = (const char *)lv_event_get_user_data(event);
if (!strcmp(key, "C")) {
entry = "0"; accumulator = 0; pendingOp = 0; freshEntry = true; calcError = false;
} else if (calcError) {
return;
} else if (!strcmp(key, "BS")) {
if (!freshEntry && entry.length() > 1) entry.remove(entry.length() - 1);
else { entry = "0"; freshEntry = true; }
} else if (!strcmp(key, "+/-")) {
if (entry != "0") entry = entry.startsWith("-") ? entry.substring(1) : "-" + entry;
} else if (!strcmp(key, "%")) {
entry = formatNumber(entry.toDouble() / 100.0); freshEntry = true;
} else if (!strcmp(key, ".")) {
if (freshEntry) { entry = "0."; freshEntry = false; }
else if (entry.indexOf('.') < 0 && entry.length() < 14) entry += ".";
} else if (!strcmp(key, "=")) {
if (pendingOp) { if (!applyPending()) return; pendingOp = 0; freshEntry = true; }
} else if (!strcmp(key, "+") || !strcmp(key, "-") || !strcmp(key, "*") || !strcmp(key, "/")) {
char next = key[0];
if (pendingOp && !freshEntry) { if (!applyPending()) return; }
else if (!pendingOp) accumulator = entry.toDouble();
pendingOp = next; freshEntry = true;
} else {
if (freshEntry) { entry = key; freshEntry = false; }
else if (entry == "0") entry = key;
else if (entry == "-0") entry = "-" + String(key);
else if (entry.length() < 14) entry += key;
}
updateCalcDisplay();
}
static void calcKey(const char *text, int x, int y, int w, bool accent) {
lv_obj_t *key = lv_btn_create(lv_scr_act());
lv_obj_set_size(key, w, 82);
lv_obj_set_pos(key, x, y);
buttonStyle(key, accent ? Theme::ACCENT : Theme::TILE,
accent ? lv_color_hex(0xD95D34) : lv_color_hex(0x454345), 20);
lv_obj_add_event_cb(key, calculatorEvent, LV_EVENT_CLICKED, (void *)text);
lv_obj_t *label = lv_label_create(key);
lv_label_set_text(label, !strcmp(text, "BS") ? "<" :
(!strcmp(text, "*") ? "x" : (!strcmp(text, "/") ? "/" : text)));
styleLabel(label, accent ? Theme::BG : Theme::TEXT, &lv_font_montserrat_24);
lv_obj_center(label);
}
static void showCalculator() {
baseScreen();
makeHeader("Calculator");
calcDisplay = lv_label_create(lv_scr_act());
lv_obj_set_size(calcDisplay, 432, 86);
lv_obj_set_pos(calcDisplay, 24, 100);
styleLabel(calcDisplay, Theme::TEXT, &lv_font_montserrat_36);
lv_obj_set_style_text_align(calcDisplay, LV_TEXT_ALIGN_RIGHT, 0);
lv_obj_set_style_bg_color(calcDisplay, Theme::TILE, 0);
lv_obj_set_style_bg_opa(calcDisplay, LV_OPA_COVER, 0);
lv_obj_set_style_radius(calcDisplay, 18, 0);
lv_obj_set_style_pad_all(calcDisplay, 18, 0);
updateCalcDisplay();
const char *keys[5][4] = {
{"C", "+/-", "%", "/"}, {"7", "8", "9", "*"}, {"4", "5", "6", "-"},
{"1", "2", "3", "+"}, {"BS", "0", ".", "="}
};
const int xs[4] = {16, 131, 246, 361};
for (int r = 0; r < 5; r++)
for (int c = 0; c < 4; c++) calcKey(keys[r][c], xs[c], 206 + r * 94, 103, c == 3 || (r == 4 && c == 3));
makeHomeIndicator();
}
static void openApp(AppId app) {
if (app == CALCULATOR) showCalculator();
else if (app == CLOCK) showClock();
else if (app == WEATHER) showWeather();
else if (app == NEWS) showNews();
else if (app == METRO_RUN) showMetroRun();
else if (app == SETTINGS) showSettings();
else showPlaceholder(app);
}
void setup() {
Serial.begin(115200);
schematikDisplayBegin();
if (schematikDisplayLock()) {
showHome();
schematikDisplayUnlock();
}
}
void loop() {
pollWifi();
clockAppPoll();
weatherAppPoll();
newsAppPoll();
delay(10);
}Download project files
Remix this project
Make it yours in one click
Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.




