Desk32: The Ultimate DIY Smart Desk Hub for Deep Work
A dedicated "Focus Anchor" designed to reclaim your productivity from the distractions of the digital age. Powered by the ESP32-S3-BOX-3. Coded with Arduino IDE.
Devices & Components
1
Espressif ESP32 S3 BOX 3
1
3000mAh 18650 Li-on Battery
Software & Tools
Arduino IDE
1
SquareLine Studio
Project description
Code
Desk32
c
Complete Project Code
1/* 2 * Project Name: Desk32 3 * Designed For: ESP32 S3 BOX 3 4 * 5 * 6 * License: GPL3+ 7 * This project is licensed under the GNU General Public License v3.0 or later. 8 * You are free to use, modify, and distribute this software under the terms 9 * of the GPL, as long as you preserve the original license and credit the original 10 * author. For more details, see <https://www.gnu.org/licenses/gpl-3.0.en.html>. 11 * 12 * Copyright (C) 2026 Ameya Angadi 13 * 14 * Code Created And Maintained By: Ameya Angadi 15 * Last Modified On: February 10, 2026 16 * Version: 1.0.0 17 * 18 */ 19 20 21#include <Arduino.h> 22#include <WiFi.h> 23#include <HTTPClient.h> 24#include <ArduinoJson.h> 25#include <time.h> 26#include <esp_display_panel.hpp> 27#include <lvgl.h> 28#include <ui.h> 29#include "lvgl_v8_port.h" 30 31using namespace esp_panel::drivers; 32using namespace esp_panel::board; 33 34// --- USER CONFIGURATION (Update These) --- 35const char* ssid = "Excitel_SAP 2.4G"; // Update: Your WiFi SSID 36const char* password = "Sap180125"; // Update: Your WiFi Password 37const char* apiKey = "c1cdd20a0c278f22fd0e3c5c07e704cb"; // Update: OpenWeatherMap API Key 38const char* city = "Bengaluru"; // Update: Your City 39const char* country = "IN"; // Update: Your Country Code 40 41// Timezone Settings (India) 42const long gmtOffset_sec = 19800; 43const int daylightOffset_sec = 0; 44 45// Global Timers and State 46Board* board = nullptr; 47unsigned long lastWeatherTime = 0; 48const unsigned long weatherInterval = 900000; // 15 Minutes 49 50// Weather Variables 51String temperature = "--"; 52String humidity = "--"; 53 54// Pomodoro Globals 55bool pomoRunning = false; 56bool pomoPaused = false; 57bool isBreakMode = false; 58int totalTime = 1500; 59int timeLeft = 1500; 60unsigned long lastPomoTick = 0; 61const char* breakMessages[] = { "Good Job!", "Take a Walk", "Drink Water", "Meditate", "Stretch Now" }; 62 63// Keyboard and UI State Trackers 64bool isKeyboardActive = false; 65int prevDay = -1; 66bool isWaterReminderEnabled = true; // Default to ON 67unsigned long lastWaterAlertTime = 0; 68const unsigned long waterAlertInterval = 1800000; // 30 Minutes 69unsigned long lastWifiCheckTime = 0; 70 71// Brightness Tracker (Default 100) 72int currentBrightness = 100; 73 74// --- EVENT HANDLERS --- 75 76// Updates backlight level based on settings slider 77void onBrightnessChange(lv_event_t * e) { 78 lv_obj_t * slider = lv_event_get_target(e); 79 int val = lv_slider_get_value(slider); 80 81 // Ensure we don't go below 10 or above 100 82 if (val < 10) val = 10; 83 if (val > 100) val = 100; 84 85 currentBrightness = val; 86 board->getBacklight()->setBrightness(currentBrightness); 87} 88 89// Toggles the hydration alert system 90void onWaterSwitchToggle(lv_event_t * e) { 91 lv_obj_t * sw = lv_event_get_target(e); 92 if (lv_obj_has_state(sw, LV_STATE_CHECKED)) { 93 isWaterReminderEnabled = true; 94 lastWaterAlertTime = millis(); 95 Serial.println("Water Reminder: ON"); 96 } else { 97 isWaterReminderEnabled = false; 98 Serial.println("Water Reminder: OFF"); 99 } 100} 101 102// Triggers a manual weather API fetch 103void onUpdateWeatherClick(lv_event_t * e) { 104 lv_obj_t * btn = lv_event_get_target(e); 105 lv_obj_t * label = lv_obj_get_child(btn, 0); 106 fetchWeather(); 107} 108 109// Forces a resync with NTP time servers 110void onSyncTimeClick(lv_event_t * e) { 111 lv_obj_t * btn = lv_event_get_target(e); 112 lv_obj_t * label = lv_obj_get_child(btn, 0); 113 configTime(gmtOffset_sec, daylightOffset_sec, "pool.ntp.org"); 114 delay(300); 115} 116 117// Throttled UI update for WiFi signal/status 118void updateSettingsScreenUI() { 119 if (lv_scr_act() != ui_SettingScreen) return; 120 121 if (millis() - lastWifiCheckTime > 2000) { 122 lastWifiCheckTime = millis(); 123 124 String ssidStr = WiFi.SSID(); 125 if(ssidStr.isEmpty()) ssidStr = "No Network"; 126 lv_label_set_text(ui_LabelWifiSSID, ("SSID: " + ssidStr).c_str()); 127 128 if (WiFi.status() == WL_CONNECTED) { 129 lv_label_set_text(ui_LabelWifiStatus, "Status: Connected"); 130 lv_obj_set_style_text_color(ui_LabelWifiStatus, lv_color_hex(0x00FF00), LV_PART_MAIN); 131 } else { 132 lv_label_set_text(ui_LabelWifiStatus, "Status: Disconnected"); 133 lv_obj_set_style_text_color(ui_LabelWifiStatus, lv_color_hex(0xFF0000), LV_PART_MAIN); 134 } 135 } 136} 137 138// Dismisses health alert and returns to Home 139void onHydrateCloseClick(lv_event_t * e) { 140 lv_scr_load_anim(ui_HomeScreen, LV_SCR_LOAD_ANIM_FADE_ON, 500, 0, false); 141} 142 143// Task Manager Handlers 144static void textAreaFocusHandler(lv_event_t * e) { 145 lv_event_code_t code = lv_event_get_code(e); 146 if(code == LV_EVENT_FOCUSED || code == LV_EVENT_CLICKED) { 147 isKeyboardActive = true; 148 lv_obj_clear_flag(ui_Keyboard1, LV_OBJ_FLAG_HIDDEN); 149 } 150} 151 152// Manages keyboard visibility 153static void dismissKeyboardHandler(lv_event_t * e) { 154 lv_event_code_t code = lv_event_get_code(e); 155 if(code == LV_EVENT_CLICKED) { 156 lv_obj_add_flag(ui_Keyboard1, LV_OBJ_FLAG_HIDDEN); 157 lv_obj_clear_state(ui_TextAreaTaskInput, LV_STATE_FOCUSED); 158 isKeyboardActive = false; 159 } 160} 161 162// Handles task completion styling and keyboard conflict 163static void taskToggleHandler(lv_event_t * e) { 164 lv_event_code_t code = lv_event_get_code(e); 165 lv_obj_t * cb = lv_event_get_target(e); 166 167 if(code == LV_EVENT_VALUE_CHANGED) { 168 if (isKeyboardActive) { 169 lv_obj_add_flag(ui_Keyboard1, LV_OBJ_FLAG_HIDDEN); 170 lv_obj_clear_state(ui_TextAreaTaskInput, LV_STATE_FOCUSED); 171 isKeyboardActive = false; 172 // Undo click 173 if(lv_obj_has_state(cb, LV_STATE_CHECKED)) lv_obj_clear_state(cb, LV_STATE_CHECKED); 174 else lv_obj_add_state(cb, LV_STATE_CHECKED); 175 return; 176 } 177 // Normal toggle style 178 if(lv_obj_has_state(cb, LV_STATE_CHECKED)) { 179 lv_obj_set_style_text_color(cb, lv_color_hex(0x4c4d52), LV_PART_MAIN); 180 lv_obj_set_style_text_decor(cb, LV_TEXT_DECOR_STRIKETHROUGH, LV_PART_MAIN); 181 } else { 182 lv_obj_set_style_text_color(cb, lv_color_hex(0xFFFFFF), LV_PART_MAIN); 183 lv_obj_set_style_text_decor(cb, LV_TEXT_DECOR_NONE, LV_PART_MAIN); 184 } 185 } 186} 187 188// Deletes a task row from the list 189static void deleteTaskHandler(lv_event_t * e) { 190 lv_event_code_t code = lv_event_get_code(e); 191 if(code == LV_EVENT_CLICKED) { 192 if (isKeyboardActive) { 193 lv_obj_add_flag(ui_Keyboard1, LV_OBJ_FLAG_HIDDEN); 194 lv_obj_clear_state(ui_TextAreaTaskInput, LV_STATE_FOCUSED); 195 isKeyboardActive = false; 196 } 197 lv_obj_t* row_container = (lv_obj_t*)lv_event_get_user_data(e); 198 if(row_container) lv_obj_del(row_container); 199 } 200} 201 202// Dynamically creates a new task entry in the UI 203void addTaskToUI(const char* text) { 204 if (strlen(text) == 0) return; 205 lvgl_port_lock(-1); 206 207 lv_obj_t* row = lv_obj_create(ui_ContainerTaskList); 208 lv_obj_set_width(row, LV_PCT(100)); 209 lv_obj_set_height(row, LV_SIZE_CONTENT); 210 lv_obj_set_style_pad_all(row, 2, 0); 211 lv_obj_set_style_border_width(row, 0, 0); 212 lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, 0); 213 lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); 214 lv_obj_set_flex_align(row, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); 215 216 lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE); 217 lv_obj_add_event_cb(row, dismissKeyboardHandler, LV_EVENT_CLICKED, NULL); 218 219 lv_obj_t* cb = lv_checkbox_create(row); 220 lv_checkbox_set_text(cb, text); 221 lv_obj_set_flex_grow(cb, 1); 222 lv_obj_set_style_text_font(cb, &ui_font_OswaldLight20, LV_PART_MAIN); 223 lv_obj_set_style_width(cb, 20, LV_PART_INDICATOR); 224 lv_obj_set_style_height(cb, 20, LV_PART_INDICATOR); 225 lv_obj_set_style_text_font(cb, &lv_font_montserrat_14, LV_PART_INDICATOR); 226 lv_obj_set_style_pad_column(cb, 5, LV_PART_MAIN); 227 lv_obj_set_style_pad_top(cb, 0, LV_PART_MAIN); 228 lv_obj_set_style_pad_bottom(cb, 0, LV_PART_MAIN); 229 lv_obj_add_event_cb(cb, taskToggleHandler, LV_EVENT_VALUE_CHANGED, NULL); 230 231 lv_obj_t* btn_del = lv_btn_create(row); 232 lv_obj_set_size(btn_del, 35, 35); 233 lv_obj_set_style_bg_opa(btn_del, LV_OPA_TRANSP, 0); 234 lv_obj_set_style_shadow_width(btn_del, 0, 0); 235 lv_obj_t* lbl = lv_label_create(btn_del); 236 lv_label_set_text(lbl, "X"); 237 lv_obj_center(lbl); 238 lv_obj_set_style_text_font(lbl, &lv_font_montserrat_14, 0); 239 lv_obj_set_style_text_color(lbl, lv_color_hex(0xFF0000), 0); 240 lv_obj_add_event_cb(btn_del, deleteTaskHandler, LV_EVENT_CLICKED, row); 241 242 lvgl_port_unlock(); 243} 244 245// Processes "Enter" key from the task keyboard 246void onKeyboardReady(lv_event_t* e) { 247 lv_obj_t* kb = lv_event_get_target(e); 248 uint32_t code = lv_event_get_code(e); 249 250 if (code == LV_EVENT_READY) { 251 const char* txt = lv_textarea_get_text(ui_TextAreaTaskInput); 252 addTaskToUI(txt); 253 lv_textarea_set_text(ui_TextAreaTaskInput, ""); 254 lv_obj_add_flag(ui_Keyboard1, LV_OBJ_FLAG_HIDDEN); 255 lv_obj_clear_state(ui_TextAreaTaskInput, LV_STATE_FOCUSED); 256 isKeyboardActive = false; 257 } 258} 259 260// --- POMODORO AND WEATHER CORE --- 261 262// Updates Pomodoro countdown and progress arc 263void updatePomoUI() { 264 lvgl_port_lock(-1); 265 char buf[10]; 266 sprintf(buf, "%02d:%02d", timeLeft / 60, timeLeft % 60); 267 lv_label_set_text(ui_LabelPomoTime, buf); 268 if (totalTime > 0) { 269 int progress = (timeLeft * 100) / totalTime; 270 lv_arc_set_value(ui_ArcPomo, progress); 271 } 272 lvgl_port_unlock(); 273} 274 275// Handles Pomodoro Start/Pause/Resume states 276void startPomodoro(lv_event_t * e) { 277 if (!pomoRunning) { 278 pomoRunning = true; 279 pomoPaused = false; 280 lv_label_set_text(ui_LabelStart, "PAUSE"); 281 if (!isBreakMode) lv_label_set_text(ui_LabelPomoStatus, "Focus Mode"); 282 } else { 283 pomoPaused = !pomoPaused; 284 if (pomoPaused) { 285 lv_label_set_text(ui_LabelStart, "RESUME"); 286 lv_label_set_text(ui_LabelPomoStatus, "Paused"); 287 } else { 288 lv_label_set_text(ui_LabelStart, "PAUSE"); 289 lv_label_set_text(ui_LabelPomoStatus, isBreakMode ? "Relaxing..." : "Focus Mode"); 290 } 291 } 292} 293 294// Resets Pomodoro to initial state 295void stopPomodoro(lv_event_t * e) { 296 pomoRunning = false; 297 pomoPaused = false; 298 isBreakMode = false; 299 totalTime = 1500; 300 timeLeft = 1500; 301 lv_label_set_text(ui_LabelStart, "START"); 302 lv_label_set_text(ui_LabelPomoStatus, "Focus Mode"); 303 updatePomoUI(); 304} 305 306// Fetches real-time JSON weather data from OpenWeatherMap 307void fetchWeather() { 308 if (WiFi.status() == WL_CONNECTED) { 309 HTTPClient http; 310 String url = "http://api.openweathermap.org/data/2.5/weather?q=" + String(city) + "," + String(country) + "&units=metric&appid=" + String(apiKey); 311 http.begin(url); 312 int httpResponseCode = http.GET(); 313 if (httpResponseCode == 200) { 314 String payload = http.getString(); 315 DynamicJsonDocument doc(1024); 316 DeserializationError error = deserializeJson(doc, payload); 317 if (!error) { 318 float temp_val = doc["main"]["temp"]; 319 int hum_val = doc["main"]["humidity"]; 320 char temp_buf[16]; 321 sprintf(temp_buf, "%.1f °C", temp_val); 322 temperature = String(temp_buf); 323 humidity = String(hum_val) + " %"; 324 Serial.println("Weather Updated: " + temperature); 325 } 326 } 327 http.end(); 328 } 329} 330 331// --- SYSTEM SETUP --- 332 333void setup() { 334 Serial.begin(115200); 335 336 // Initialize Hardware 337 board = new Board(); 338 board->init(); 339 assert(board->begin()); 340 lvgl_port_init(board->getLCD(), board->getTouch()); 341 lvgl_port_lock(-1); 342 ui_init(); 343 344 // Dynamic Event Attachments 345 lv_obj_add_event_cb(ui_Keyboard1, onKeyboardReady, LV_EVENT_READY, NULL); 346 lv_obj_add_event_cb(ui_TextAreaTaskInput, textAreaFocusHandler, LV_EVENT_FOCUSED, NULL); 347 lv_obj_add_event_cb(ui_TextAreaTaskInput, textAreaFocusHandler, LV_EVENT_CLICKED, NULL); 348 lv_obj_add_flag(ui_ContainerTaskList, LV_OBJ_FLAG_CLICKABLE); 349 lv_obj_add_event_cb(ui_ContainerTaskList, dismissKeyboardHandler, LV_EVENT_CLICKED, NULL); 350 lv_obj_add_event_cb(lv_scr_act(), dismissKeyboardHandler, LV_EVENT_CLICKED, NULL); 351 352 353 if (ui_SwitchWater) { 354 lv_obj_add_state(ui_SwitchWater, LV_STATE_CHECKED); 355 isWaterReminderEnabled = true; 356 } 357 if (ui_SliderBrightness) { 358 lv_slider_set_range(ui_SliderBrightness, 10, 100); 359 lv_slider_set_value(ui_SliderBrightness, 100, LV_ANIM_OFF); 360 lv_obj_clear_flag(ui_SliderBrightness, LV_OBJ_FLAG_SCROLL_CHAIN); 361 lv_obj_clear_flag(ui_SliderBrightness, LV_OBJ_FLAG_GESTURE_BUBBLE); 362 } 363 364 lv_task_handler(); 365 lvgl_port_unlock(); 366 367// Initialization: WiFi and Time 368 WiFi.begin(ssid, password); 369 Serial.print("Connecting"); 370 int retry = 0; 371 while (WiFi.status() != WL_CONNECTED && retry < 10) { 372 delay(400); 373 Serial.print("."); 374 retry++; 375 } 376 Serial.println("\nConnected!"); 377 378 configTime(gmtOffset_sec, daylightOffset_sec, "pool.ntp.org"); 379 380 fetchWeather(); 381 lastWeatherTime = millis(); 382 lastWaterAlertTime = millis(); 383} 384 385// --- MAIN LOOP --- 386 387void loop() { 388 struct tm timeinfo; 389 390 // Pomodoro Tick Logic 391 if (pomoRunning && !pomoPaused) { 392 if (millis() - lastPomoTick > 1000) { 393 lastPomoTick = millis(); 394 if (timeLeft > 0) { 395 timeLeft--; 396 updatePomoUI(); 397 } else { 398 if (!isBreakMode) { 399 isBreakMode = true; 400 totalTime = 300; 401 timeLeft = 300; 402 int r = random(0, 5); 403 lvgl_port_lock(-1); 404 lv_label_set_text(ui_LabelPomoStatus, breakMessages[r]); 405 lvgl_port_unlock(); 406 } else { 407 stopPomodoro(NULL); 408 lvgl_port_lock(-1); 409 lv_label_set_text(ui_LabelPomoStatus, "Break Over!"); 410 lvgl_port_unlock(); 411 } 412 } 413 } 414 } 415 416 // Background Weather and Time Updation Every 15 Minutes 417 if (millis() - lastWeatherTime > weatherInterval) { 418 if (WiFi.status() == WL_CONNECTED) { 419 configTime(gmtOffset_sec, daylightOffset_sec, "pool.ntp.org"); 420 fetchWeather(); 421 } 422 lastWeatherTime = millis(); 423 } 424 425 // Water Reminder Logic 426 if (isWaterReminderEnabled) { 427 if (millis() - lastWaterAlertTime > waterAlertInterval) { 428 lvgl_port_lock(-1); 429 if (lv_scr_act() != ui_HydrateScreen) { 430 lv_scr_load_anim(ui_HydrateScreen, LV_SCR_LOAD_ANIM_FADE_ON, 500, 0, false); 431 } 432 lvgl_port_unlock(); 433 lastWaterAlertTime = millis(); 434 } 435 } 436 437 // Settings Screen UI Updates 438 if (lv_scr_act() == ui_SettingScreen) { 439 lvgl_port_lock(-1); 440 updateSettingsScreenUI(); 441 lvgl_port_unlock(); 442 } 443 444 // Update Time 445 if (getLocalTime(&timeinfo)) { 446 char buf_time[6], buf_ampm[3], buf_day[4], buf_date[12]; 447 strftime(buf_time, sizeof(buf_time), "%I:%M", &timeinfo); 448 strftime(buf_ampm, sizeof(buf_ampm), "%p", &timeinfo); 449 strftime(buf_day, sizeof(buf_day), "%a", &timeinfo); 450 strftime(buf_date, sizeof(buf_date), "%d %b %Y", &timeinfo); 451 452 lvgl_port_lock(-1); 453 lv_label_set_text(ui_LabelTime, buf_time); 454 lv_label_set_text(ui_LabelDay, buf_day); 455 lv_label_set_text(ui_LabelAMPM, buf_ampm); 456 lv_label_set_text(ui_LabelDate, buf_date); 457 458 if (timeinfo.tm_mday != prevDay) { 459 prevDay = timeinfo.tm_mday; 460 lv_calendar_set_today_date(ui_Calendar1, timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday); 461 lv_calendar_set_showed_date(ui_Calendar1, timeinfo.tm_year + 1900, timeinfo.tm_mon + 1); 462 } 463 464 lv_label_set_text(ui_LabelTemp, temperature.c_str()); 465 lv_label_set_text(ui_LabelHum, humidity.c_str()); 466 lvgl_port_unlock(); 467 } 468 469 delay(200); 470}
Desk32 Github Repository
Check this repository for detailled instructions
Downloadable files
Internal Circuit Diagram
Just for reference
pinlayout_box_3_dock.png

Desk32 Design Files
Squareline Studio UI Files
Desk32_Design_Files.zip
Files To Copy
Copy these files to Documents\Arduino\libraries\
Files_To_Copy.zip
Comments
Only logged in users can leave comments