Community project
CYD Desk Buddy! with DHT11
James Blank (Jiffybytes)
Published July 25, 2026 · Updated July 26, 2026

CYD Desk Buddy is a smart desktop companion built around the Cheap Yellow Display (ESP32-2432S028R) that shows time, weather, and indoor environmental conditions. This guide transforms the display into a multi-function dashboard by removing the accelerometer and adding a DHT11 temperature and humidity sensor to monitor your workspace.
Following this guide, makers will receive a complete wiring diagram, parts list, and step-by-step assembly instructions to integrate the DHT11 sensor with the existing display firmware. The result is a touchscreen device that cycles through weather forecasts, astronomical data, and live indoor climate readings—all powered by a single ESP32 board.
Wiring diagram
Interactive · read-only
Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.
Parts list
Bill of materials| Component | Qty | Notes |
|---|---|---|
| DHT11DHT11 | 1 | Digital temperature and humidity sensor (lower accuracy than DHT22) |
Assembly
4 stepsPower the display board
Power the Cheap Yellow Display only through its onboard USB connector. Keep it on a non-conductive work surface while making the sensor connections.
- Tip: Use a good USB power source; the same USB connector is used by Schematik Deploy.
- ⚠ Disconnect USB power before changing any jumper wires.
Remove the MPU-6050
Disconnect the MPU-6050 completely: remove its VIN, GND, SDA, and SCL wires from the Cheap Yellow Display. The current DeskBuddy firmware no longer uses an IMU.
- Tip: GPIO0 and GPIO3 are now unused by this project after removing the MPU-6050.
- ⚠ Do not leave loose sensor wires touching the CYD or enclosure.
Wire the DHT11 indoor sensor
Connect DHT11 VCC to CYD 3V3, GND to CYD GND, and DATA to CYD GPIO27. For a bare 4-pin DHT11, add a 4.7–10 kΩ pull-up resistor from DATA to 3.3 V; common 3-pin modules normally include it.
- Tip: The indoor page always displays Fahrenheit and %RH.
- Tip: Keep the sensor away from the CYD voltage regulator, direct sunlight, and airflow from USB-powered equipment.
- ⚠ Do not power the DHT11 or pull its DATA line up to 5 V; ESP32 GPIO is 3.3 V only.
Check and deploy
Check the DHT11 3.3 V, ground, and GPIO27 connection, then close the enclosure only after the display, touch, and indoor sensor have been tested. Use Schematik’s Deploy button to flash the project.
- Tip: The 4-day forecast shows weekday labels. The sunset time is shifted farther left for better spacing.
- ⚠ Avoid placing the DHT11 against the display or in a sealed pocket; trapped heat will make its temperature reading high.
Pin assignments
Board wiring reference| Pin | Connection | Type |
|---|---|---|
| 3V3 | dht11-1 VCC | power |
| GND | dht11-1 GND | ground |
| GPIO 27 | dht11-1 DATA | data |
Firmware
ESP32// DeskBuddy for ESP32-2432S028R (Cheap Yellow Display), landscape
#include <Arduino.h>
#include <Arduino_GFX_Library.h>
#include <XPT2046_Touchscreen.h>
#include <ArduinoJson.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include <time.h>
#include <esp_sntp.h>
#include <math.h>
#include "secrets.h"
enum Page { FACE, CLOCK, WEATHER, FORECAST, SUN, MOON, INDOOR, SETUP, CONFIRM };
// Global state variables needed before function definitions
Page page = FACE;
bool dhtOK = false;
// Forward declarations
int dashboardPageCount();
bool isDashboardPage();
bool readDHT11();
void drawDht();
void centered(const String &s,int y,uint8_t n,uint16_t c);
void title(const String &s);
void footer();
String unitTemp();
String unitWind();
String format12Hour(const String &time24);
void applyTime();
void load();
void save();
String selected(const String &v,const String &wanted);
String form();
void setTimeZone(const String &choice);
void beginStation();
void startPortal();
bool getWeather();
void drawFace();
void drawClock();
void drawWeather();
void drawForecast();
void drawWeatherIcon(int x, int y, int code);
void drawSun();
void drawMoon();
void drawSetup();
void drawConfirm();
void render();
void readTouch();
int dashboardPageCount(){ return dhtOK ? 7 : 6; }
bool isDashboardPage(){ return (int)page >= (int)FACE && (int)page < dashboardPageCount(); }
constexpr int MY_TFT_CS=15, MY_TFT_DC=2, MY_TFT_SCLK=14, MY_TFT_MOSI=13, MY_TFT_MISO=12, MY_TFT_BL=21;
constexpr int TOUCH_CS=33, TOUCH_IRQ=36, TOUCH_SCK=25, TOUCH_MOSI=32, TOUCH_MISO=39;
constexpr int DHT_PIN=27, W=320, H=240;
constexpr uint16_t BG=0x0000, FG=0xFFFF, DIM=0x7BEF, CYAN=0x07FF;
Arduino_DataBus *bus=new Arduino_HWSPI(MY_TFT_DC,MY_TFT_CS,MY_TFT_SCLK,MY_TFT_MOSI,MY_TFT_MISO);
Arduino_GFX *gfx=new Arduino_ILI9341(bus,GFX_NOT_DEFINED,1);
SPIClass touchSPI(VSPI); XPT2046_Touchscreen touch(TOUCH_CS,TOUCH_IRQ);
WebServer web(80); Preferences store;
String ssid, pass, city, tzRule="CST6CDT,M3.2.0/2,M11.1.0/2", tzLabel="Central", units="imperial", weatherLabel="Waiting";
float lat=DESKBUDDY_DEFAULT_LAT, lon=DESKBUDDY_DEFAULT_LON, temperature=NAN;
float indoorTemperature=NAN, indoorHumidity=NAN;
String dayName[4], dayText[4]; float dayHigh[4], dayLow[4];
String sunriseTime="--:--", sunsetTime="--:--";
bool portal=false, webStarted=false, weatherOK=false, blink=false, dirty=true, wasConnected=false, timeSynced=false, reconnectPending=false; int renderedPage=-1;
String networkStatus="Starting Wi-Fi";
uint32_t lastWeather=0, lastWeatherAttempt=0, lastTimeAttempt=0, lastWiFiAttempt=0, wifiConnectStarted=0, reconnectAfter=0, lastBlink=0, lastPageCycle=0, lastDhtRead=0;
void centered(const String &s,int y,uint8_t n=1,uint16_t c=FG){gfx->setTextSize(n);gfx->setTextColor(c);gfx->setCursor((W-(int)s.length()*6*n)/2,y);gfx->print(s);}
void title(const String &s){gfx->fillScreen(BG);gfx->drawFastHLine(0,25,W,DIM);gfx->setTextSize(1);gfx->setTextColor(CYAN);gfx->setCursor(9,8);gfx->print(s);}
void footer(){
gfx->setTextSize(1); gfx->setTextColor(DIM); gfx->setCursor(8,226); gfx->print("Swipe pages");
const int gx=298,gy=220; gfx->drawCircle(gx,gy,9,CYAN); gfx->drawCircle(gx,gy,3,CYAN);
for(int i=0;i<8;i++){float a=i*PI/4;gfx->drawLine(gx+(int)(10*cos(a)),gy+(int)(10*sin(a)),gx+(int)(13*cos(a)),gy+(int)(13*sin(a)),CYAN);}
}
String unitTemp(){return units=="metric"?"C":"F";}
String unitWind(){return units=="metric"?"km/h":"mph";}
String format12Hour(const String &time24){
if(time24.length()!=5 || time24.charAt(2)!=':') return "--:--";
int hour=time24.substring(0,2).toInt();
int minute=time24.substring(3,5).toInt();
if(hour<0 || hour>23 || minute<0 || minute>59) return "--:--";
const char *suffix=hour>=12?"PM":"AM";
int hour12=hour%12;
if(hour12==0) hour12=12;
char out[12];
snprintf(out,sizeof(out),"%d:%02d %s",hour12,minute,suffix);
return String(out);
}
void applyTime(){
configTzTime(tzRule.c_str(), "time.cloudflare.com", "time.google.com", "pool.ntp.org");
sntp_set_sync_interval(300000UL);
lastTimeAttempt=millis();
}
void load(){
store.begin("deskbuddy",false); ssid=store.getString("ssid",DESKBUDDY_WIFI_SSID); pass=store.getString("pass",DESKBUDDY_WIFI_PASSWORD);
city=store.getString("city",DESKBUDDY_DEFAULT_CITY); lat=store.getFloat("lat",DESKBUDDY_DEFAULT_LAT); lon=store.getFloat("lon",DESKBUDDY_DEFAULT_LON);
tzRule=store.getString("tz",tzRule); tzLabel=store.getString("tzlabel",tzLabel); units=store.getString("units",units);
if(store.getUInt("weatherLocVer",0) < 2){
city="Bella Vista, AR";
lat=36.4637f;
lon=-94.2344f;
store.putString("city",city);
store.putFloat("lat",lat);
store.putFloat("lon",lon);
store.putUInt("weatherLocVer",2);
weatherOK=false;
lastWeather=0;
lastWeatherAttempt=0;
}
}
void save(){store.putString("ssid",ssid);store.putString("pass",pass);store.putString("city",city);store.putFloat("lat",lat);store.putFloat("lon",lon);store.putString("tz",tzRule);store.putString("tzlabel",tzLabel);store.putString("units",units);}
String selected(const String &v,const String &wanted){return v==wanted?" selected":"";}
String form(){
String h="<!doctype html><meta name=viewport content='width=device-width,initial-scale=1'><style>body{font-family:sans-serif;background:#101418;color:#eee;max-width:520px;margin:2em auto;padding:1em}input,select{width:100%;padding:.7em;box-sizing:border-box;margin:.35em 0}button{padding:.8em;background:#00bcd4;border:0;font-weight:bold}small{color:#bbb}</style><h1>DeskBuddy Setup Center</h1><p>Settings are stored on DeskBuddy.</p><form action=/save>Wi-Fi name<input name=ssid value='"+ssid+"'>Wi-Fi password <small>(leave blank to keep current password)</small><input type=password name=pass>City label<input name=city value='"+city+"'>Latitude<input name=lat value='"+String(lat,4)+"'>Longitude<input name=lon value='"+String(lon,4)+"'>Time zone<select name=tz>";
h+="<option value=central"+selected(tzLabel,"Central")+">US Central (DST automatic)</option><option value=eastern"+selected(tzLabel,"Eastern")+">US Eastern (DST automatic)</option><option value=mountain"+selected(tzLabel,"Mountain")+">US Mountain (DST automatic)</option><option value=pacific"+selected(tzLabel,"Pacific")+">US Pacific (DST automatic)</option><option value=utc"+selected(tzLabel,"UTC")+">UTC</option></select>Units<select name=units><option value=imperial"+selected(units,"imperial")+">Imperial (F, mph)</option><option value=metric"+selected(units,"metric")+">Metric (C, km/h)</option></select><button>Save settings</button></form>";
return h;
}
void setTimeZone(const String &choice){
if(choice=="eastern"){tzLabel="Eastern";tzRule="EST5EDT,M3.2.0/2,M11.1.0/2";}
else if(choice=="mountain"){tzLabel="Mountain";tzRule="MST7MDT,M3.2.0/2,M11.1.0/2";}
else if(choice=="pacific"){tzLabel="Pacific";tzRule="PST8PDT,M3.2.0/2,M11.1.0/2";}
else if(choice=="utc"){tzLabel="UTC";tzRule="UTC0";}
else {tzLabel="Central";tzRule="CST6CDT,M3.2.0/2,M11.1.0/2";}
}
void beginStation(){
if(ssid.isEmpty()){networkStatus="No Wi-Fi configured"; return;}
WiFi.mode(WIFI_STA);
WiFi.disconnect(false, false);
delay(100);
Serial.printf("Wi-Fi: joining '%s'\n", ssid.c_str());
networkStatus="Connecting to Wi-Fi";
WiFi.begin(ssid.c_str(),pass.c_str());
lastWiFiAttempt=millis();
if(wifiConnectStarted==0) wifiConnectStarted=lastWiFiAttempt;
}
void startPortal(){
reconnectPending=false;
portal=true;
WiFi.disconnect(true, false);
WiFi.softAPdisconnect(true);
WiFi.mode(WIFI_AP);
delay(100);
WiFi.softAP("DeskBuddy-Setup","deskbuddy");
networkStatus="Setup Wi-Fi ready: 192.168.4.1";
wifiConnectStarted=0;
page=SETUP;
if(!webStarted){
web.on("/",[](){web.send(200,"text/html",form());});
web.on("/save",[](){
ssid=web.arg("ssid"); String newPass=web.arg("pass"); if(!newPass.isEmpty())pass=newPass;
city=web.arg("city"); lat=web.arg("lat").toFloat(); lon=web.arg("lon").toFloat(); units=web.arg("units")=="metric"?"metric":"imperial";
store.putUInt("weatherLocVer",2);
setTimeZone(web.arg("tz")); save(); weatherOK=false; lastWeather=0; lastWeatherAttempt=0; applyTime(); page=CONFIRM; dirty=true;
web.send(200,"text/html","<h1>Saved</h1><p>DeskBuddy will reconnect in a moment. This setup Wi-Fi will turn off after it joins your saved network. Return to the gear screen for its local Wi-Fi address.</p>");
reconnectPending=true; reconnectAfter=millis()+1500;
});
web.begin();
webStarted=true;
}
dirty=true;
}
bool getWeather(){
if(WiFi.status()!=WL_CONNECTED)return false;
String tempUnit=units=="metric"?"celsius":"fahrenheit", windUnit=units=="metric"?"kmh":"mph";
String u="https://api.open-meteo.com/v1/forecast?latitude="+String(lat,4)+"&longitude="+String(lon,4)+"¤t=temperature_2m,weather_code&daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset&temperature_unit="+tempUnit+"&wind_speed_unit="+windUnit+"&timezone=auto&forecast_days=4";
WiFiClientSecure client;
client.setInsecure();
HTTPClient h;
h.setConnectTimeout(10000);
h.setTimeout(15000);
h.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
Serial.println("Weather: requesting Open-Meteo");
if(!h.begin(client,u)){networkStatus="Weather connection failed"; Serial.println("Weather: client begin failed"); return false;}
int status=h.GET();
if(status!=HTTP_CODE_OK){networkStatus="Weather HTTP error "+String(status); Serial.printf("Weather HTTP status: %d\n",status);h.end();return false;}
JsonDocument d;
String payload=h.getString();
bool ok=!deserializeJson(d,payload);
h.end();
if(!ok){networkStatus="Weather data parse failed"; Serial.println("Weather JSON parse failed");return false;}
temperature=d["current"]["temperature_2m"].as<float>(); int code=d["current"]["weather_code"].as<int>(); weatherLabel=code==0?"Clear":code<=3?"Cloudy":code<=67?"Rain / fog":code<=77?"Snow":"Showers";
JsonArray times=d["daily"]["time"].as<JsonArray>(), highs=d["daily"]["temperature_2m_max"].as<JsonArray>(), lows=d["daily"]["temperature_2m_min"].as<JsonArray>(), codes=d["daily"]["weather_code"].as<JsonArray>();
for(int i=0;i<4;i++){
struct tm dayTm;
time_t base=time(nullptr) + (time_t)i*86400;
localtime_r(&base,&dayTm);
static const char *weekdays[]={"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
dayName[i]=i==0?"Today":String(weekdays[dayTm.tm_wday]);
dayHigh[i]=highs[i]|NAN;dayLow[i]=lows[i]|NAN;int c=codes[i]|-1;dayText[i]=c==0?"Clear":c<=3?"Cloud":c<=67?"Rain":c<=77?"Snow":"Show";
}
String rise=d["daily"]["sunrise"][0] | "";
String set=d["daily"]["sunset"][0] | "";
sunriseTime=rise.length()>=16 ? format12Hour(rise.substring(11,16)) : "--:--";
sunsetTime=set.length()>=16 ? format12Hour(set.substring(11,16)) : "--:--";
weatherOK=true;
networkStatus="Weather updated";
Serial.printf("Weather: updated %.1f %s at %.4f, %.4f\n", temperature, unitTemp().c_str(), lat, lon);
lastWeather=millis();
dirty=true;
return true;
}
void drawFace(){title("DESKBUDDY / FACE");gfx->drawRoundRect(37,43,246,154,22,DIM);if(blink){gfx->drawFastHLine(76,106,60,FG);gfx->drawFastHLine(185,106,60,FG);}else{gfx->fillRoundRect(67,72,74,68,26,FG);gfx->fillRoundRect(179,72,74,68,26,FG);gfx->fillCircle(104,106,13,BG);gfx->fillCircle(216,106,13,BG);}gfx->drawCircle(160,157,24,FG);gfx->fillRect(136,139,48,18,BG);if(WiFi.status()!=WL_CONNECTED)centered(networkStatus,201,1,DIM);else if(!timeSynced)centered("Connected - syncing clock...",201,1,CYAN);else if(!weatherOK)centered(networkStatus,201,1,CYAN);else centered("DeskBuddy ready",201,1,DIM);footer();}
void drawClock(){title("CLOCK / DATE");struct tm t;if(getLocalTime(&t,10)){char a[16],b[32];strftime(a,sizeof(a),"%I:%M %p",&t);strftime(b,sizeof(b),"%A, %d %b %Y",&t);centered(a,66,5);centered(b,159,2,CYAN);centered(tzLabel+" time",195,1,DIM);}else if(WiFi.status()==WL_CONNECTED){centered("Connected to Wi-Fi",88,2,CYAN);centered("Syncing time from network...",126,2);centered("Please wait",164,1,DIM);}else{centered(networkStatus,98,2);centered("Configure Wi-Fi with the gear",136,1,DIM);}footer();}
void drawWeather(){title("OPEN-METEO / "+city);if(weatherOK){centered(String(temperature,1)+" "+unitTemp(),65,7);centered(weatherLabel,151,3,CYAN);centered("Location: "+city,194,1,DIM);}else centered(WiFi.status()==WL_CONNECTED?"Updating weather...":"Wi-Fi not connected",104,2);footer();}
void drawWeatherIcon(int x,int y,int code){
if(code==0){
gfx->drawCircle(x,y,11,CYAN);
for(int i=0;i<8;i++){float a=i*PI/4;gfx->drawLine(x+(int)(14*cos(a)),y+(int)(14*sin(a)),x+(int)(19*cos(a)),y+(int)(19*sin(a)),CYAN);}
}else if(code<=3){
gfx->fillCircle(x-7,y+3,7,FG);gfx->fillCircle(x+2,y-2,10,FG);gfx->fillCircle(x+12,y+4,6,FG);gfx->fillRect(x-13,y+4,31,7,FG);
}else if(code<=77){
gfx->fillCircle(x-7,y,7,FG);gfx->fillCircle(x+2,y-5,10,FG);gfx->fillCircle(x+12,y+1,6,FG);gfx->fillRect(x-13,y+1,31,7,FG);
gfx->drawLine(x-8,y+13,x-11,y+19,CYAN);gfx->drawLine(x+2,y+13,x-1,y+19,CYAN);gfx->drawLine(x+11,y+13,x+8,y+19,CYAN);
}else{
gfx->fillCircle(x-7,y,7,FG);gfx->fillCircle(x+2,y-5,10,FG);gfx->fillCircle(x+12,y+1,6,FG);gfx->fillRect(x-13,y+1,31,7,FG);
for(int i=-8;i<=10;i+=9){gfx->drawLine(x+i-3,y+14,x+i+3,y+20,CYAN);gfx->drawLine(x+i+3,y+14,x+i-3,y+20,CYAN);}
}
}
void drawForecast(){
title("4 DAY FORECAST / "+city);
if(!weatherOK){centered("Weather data is updating...",108,2);footer();return;}
for(int i=0;i<4;i++){
int x=4+i*79, c=x+36;
gfx->drawRoundRect(x,36,75,171,5,DIM);
gfx->setTextSize(1);gfx->setTextColor(CYAN);gfx->setCursor(x+8,43);gfx->print(dayName[i]);
int code=dayText[i]=="Clear"?0:dayText[i]=="Cloud"?2:dayText[i]=="Rain"?61:dayText[i]=="Snow"?71:80;
drawWeatherIcon(c,76,code);
gfx->setTextSize(1);gfx->setTextColor(FG);gfx->setCursor(x+11,105);gfx->print(dayText[i]);
gfx->setTextSize(2);gfx->setTextColor(FG);gfx->setCursor(x+11,124);gfx->print(String(dayHigh[i],0));
gfx->setTextSize(1);gfx->print(" "+unitTemp());
gfx->setTextSize(2);gfx->setTextColor(DIM);gfx->setCursor(x+11,148);gfx->print(String(dayLow[i],0));
gfx->setTextSize(1);gfx->print(" "+unitTemp());
}
centered("HIGH / LOW",214,1,DIM);footer();
}
void drawSun(){
title("SUNRISE / SUNSET / "+city);
if(!weatherOK){
centered("Weather data is updating...",104,2,CYAN);
footer();
return;
}
gfx->drawCircle(80,91,24,CYAN);
for(int i=0;i<8;i++){float a=i*PI/4;gfx->drawLine(80+(int)(29*cos(a)),91+(int)(29*sin(a)),80+(int)(37*cos(a)),91+(int)(37*sin(a)),CYAN);}
gfx->drawFastHLine(38,126,84,DIM);
gfx->setTextSize(2); gfx->setTextColor(CYAN); gfx->setCursor(36,145); gfx->print("SUNRISE");
gfx->setTextSize(3); gfx->setTextColor(FG); gfx->setCursor(18,171); gfx->print(sunriseTime);
gfx->fillCircle(240,91,24,FG);
gfx->fillCircle(250,82,24,BG);
gfx->drawFastHLine(198,126,84,DIM);
gfx->setTextSize(2); gfx->setTextColor(CYAN); gfx->setCursor(198,145); gfx->print("SUNSET");
gfx->setTextSize(3); gfx->setTextColor(FG); gfx->setCursor(188,171); gfx->print(sunsetTime);
centered("Today, local time",207,1,DIM);
footer();
}
bool readDHT11(){
uint8_t raw[5]={0,0,0,0,0};
pinMode(DHT_PIN, OUTPUT); digitalWrite(DHT_PIN, LOW); delay(20);
digitalWrite(DHT_PIN, HIGH); delayMicroseconds(40); pinMode(DHT_PIN, INPUT_PULLUP);
auto waitLevel=[](int level, uint32_t timeout){ uint32_t start=micros(); while(digitalRead(DHT_PIN)==level){ if((uint32_t)(micros()-start)>timeout) return false; } return true; };
if(!waitLevel(HIGH,120) || !waitLevel(LOW,120) || !waitLevel(HIGH,120)){ Serial.println("DHT11: no response"); return false; }
for(int i=0;i<40;i++){
if(!waitLevel(LOW,90)) return false;
uint32_t highStart=micros();
if(!waitLevel(HIGH,120)) return false;
if((uint32_t)(micros()-highStart)>50) raw[i/8]|=(1<<(7-(i%8)));
}
if((uint8_t)(raw[0]+raw[1]+raw[2]+raw[3])!=raw[4]){ Serial.println("DHT11: checksum failed"); return false; }
float humidity=raw[0], celsius=raw[2];
bool changed=!dhtOK || roundf(humidity)!=roundf(indoorHumidity) || roundf(celsius)!=roundf(indoorTemperature);
indoorHumidity=humidity; indoorTemperature=celsius; dhtOK=true;
if(changed && page==INDOOR) dirty=true;
return true;
}
void drawDht(){
title("INDOOR / DHT11");
if(!dhtOK){ centered("Reading indoor sensor...",88,2,CYAN); centered("Check DHT11 3.3V, GND, DATA",126,1,DIM); }
else{
float shown=indoorTemperature*9.0f/5.0f+32.0f;
centered("TEMPERATURE",46,1,CYAN); centered(String(shown,0)+" F",65,5,FG); centered("HUMIDITY",145,1,CYAN); centered(String(indoorHumidity,0)+" % RH",162,4,FG); centered("DHT11 indoor sensor",205,1,DIM); }
footer();
}
void drawMoon(){title("MOON PHASE");time_t n=time(nullptr);float phase=fmod((n/86400.0f-10957.0f)/29.530588f,1);if(phase<0)phase+=1;int sh=(int)((phase<.5?1:-1)*fabs(.5-phase)*220);gfx->fillCircle(160,112,60,FG);gfx->fillCircle(160+sh,112,60,BG);gfx->drawCircle(160,112,60,DIM);centered(String((int)(phase*29.53f))+" days into lunar cycle",192,2,CYAN);footer();}
void drawSetup(){title("SETUP CENTER");centered("DeskBuddy Setup",43,2,CYAN);if(WiFi.status()==WL_CONNECTED){centered("Wi-Fi connected",73,1,CYAN);centered("Open this address on Wi-Fi:",98,1);centered("http://"+WiFi.localIP().toString(),123,2,FG);centered("Any computer on this network can edit",153,1,DIM);}else{centered("Connect to DeskBuddy-Setup",83,2);centered("Password: deskbuddy",116,2,CYAN);centered("Then open 192.168.4.1",151,2,FG);}centered("Wi-Fi, location, time zone, units",188,1,DIM);footer();}
void drawConfirm(){title("SETTINGS SAVED");centered("Saved on DeskBuddy",62,3,CYAN);centered(city+" / "+tzLabel+" / "+units,111,1);centered("Return to gear for setup address",153,1);centered("Swipe to return",193,1,DIM);footer();}
void render(){switch(page){case FACE:drawFace();break;case CLOCK:drawClock();break;case WEATHER:drawWeather();break;case FORECAST:drawForecast();break;case SUN:drawSun();break;case MOON:drawMoon();break;case INDOOR:drawDht();break;case SETUP:drawSetup();break;default:drawConfirm();}}
void readTouch(){static bool down=false;static int sx,sy,lx,ly;if(touch.touched()){TS_Point p=touch.getPoint();int x=constrain(map(p.x,200,3800,0,W-1),0,W-1),y=constrain(map(p.y,200,3800,0,H-1),0,H-1);if(!down){down=true;sx=x;sy=y;}lx=x;ly=y;return;}if(!down)return;down=false;int dx=lx-sx,dy=ly-sy;if(abs(dx)<24&&abs(dy)<24&&lx>=276&&ly>=198){startPortal();page=SETUP;}else if(abs(dx)>55&&abs(dx)>abs(dy)){const int count=dashboardPageCount();if(isDashboardPage())page=(Page)(((int)page+(dx<0?1:count-1))%count);else page=FACE;}lastPageCycle=millis();dirty=true;}
void setup(){Serial.begin(115200);load();pinMode(MY_TFT_BL,OUTPUT);digitalWrite(MY_TFT_BL,HIGH);gfx->begin(40000000);gfx->invertDisplay(true);touchSPI.begin(TOUCH_SCK,TOUCH_MISO,TOUCH_MOSI,TOUCH_CS);touch.begin(touchSPI);touch.setRotation(1);pinMode(DHT_PIN, INPUT_PULLUP);applyTime();beginStation();}
void loop(){
if(portal)web.handleClient();
if(reconnectPending && (int32_t)(millis()-reconnectAfter)>=0){
reconnectPending=false;
portal=false;
WiFi.softAPdisconnect(true);
WiFi.mode(WIFI_STA);
wifiConnectStarted=0;
beginStation();
}
readTouch();
if(millis()-lastDhtRead>=2500){lastDhtRead=millis(); readDHT11();}
bool connected=WiFi.status()==WL_CONNECTED;
if(!connected && !portal && !ssid.isEmpty() && millis()-lastWiFiAttempt>=20000){
networkStatus="Retrying Wi-Fi";
beginStation();
dirty=true;
}
// With no saved network, immediately provide the recovery/setup access point.
// With a saved network, provide it after a failed 45-second join attempt.
if(!connected && !portal && (ssid.isEmpty() ||
(wifiConnectStarted!=0 && millis()-wifiConnectStarted>=45000))){
Serial.println(ssid.isEmpty() ? "No Wi-Fi configured; opening setup hotspot" :
"Wi-Fi join timed out; opening setup hotspot");
startPortal();
dirty=true;
}
if(connected!=wasConnected){
wasConnected=connected;
if(connected) wifiConnectStarted=0;
weatherOK=false;
timeSynced=false;
if(connected){
WiFi.softAPdisconnect(true);
WiFi.mode(WIFI_STA);
networkStatus="Connected - syncing clock";
Serial.print("Wi-Fi connected, IP: "); Serial.println(WiFi.localIP());
applyTime();
lastWeatherAttempt=0;
lastPageCycle=millis();
}
if(!connected){networkStatus="Wi-Fi unavailable (retrying)"; Serial.printf("Wi-Fi status: %d\n", WiFi.status());}
dirty=true;
}
if(connected){
const bool newTimeSynced = time(nullptr) > 1700000000;
if(newTimeSynced!=timeSynced){
timeSynced=newTimeSynced;
if(timeSynced){networkStatus="Clock updated"; Serial.println("NTP time: updated");}
dirty=true;
}
if(!timeSynced && millis()-lastTimeAttempt>=30000){
Serial.println("NTP: retrying initial sync");
applyTime();
}
if((!weatherOK || millis()-lastWeather>=300000) && millis()-lastWeatherAttempt>30000){lastWeatherAttempt=millis();getWeather();}
}
if(connected && isDashboardPage() && millis()-lastPageCycle>=15000){
page=(Page)(((int)page+1)%dashboardPageCount());
lastPageCycle=millis();
dirty=true;
}
if(millis()-lastBlink>2500){
blink=true;
lastBlink=millis();
if(page==FACE && !dirty){
gfx->fillRoundRect(67,72,74,68,26,FG);
gfx->fillRoundRect(179,72,74,68,26,FG);
gfx->drawFastHLine(76,106,60,FG);
gfx->drawFastHLine(185,106,60,FG);
}
}
if(blink&&millis()-lastBlink>130){
blink=false;
if(page==FACE && !dirty){
gfx->fillRoundRect(67,72,74,68,26,FG);
gfx->fillRoundRect(179,72,74,68,26,FG);
gfx->fillCircle(104,106,13,BG);
gfx->fillCircle(216,106,13,BG);
}
}
if(dirty||renderedPage!=(int)page){render();renderedPage=(int)page;dirty=false;}
}“Deploy to device” opens this project in Schematik, where you can flash it to your board over USB.
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.