#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include <HTTPClient.h>
#include <Arduino_JSON.h>
#include <Wire.h>
#include <U8g2lib.h>
#include <time.h>
// =====================================================
// PINS
// =====================================================
#define OLED_SDA 21
#define OLED_SCL 22
#define TOUCH_PIN 27
#define TOUCH_ACTIVE HIGH
// =====================================================
// OLED
// =====================================================
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(
U8G2_R0,
U8X8_PIN_NONE
);
// =====================================================
// CONFIG PORTAL
// =====================================================
const char* AP_NAME = "B3D-Setup";
const char* AP_PASSWORD = "12345678";
WebServer server(80);
Preferences prefs;
// =====================================================
// SAVED SETTINGS
// =====================================================
String wifiSSID = "";
String wifiPassword = "";
String apiKey = "";
String city = "Dublin";
String countryCode = "IE";
const char* timezoneDublin =
"GMT0IST,M3.5.0/1,M10.5.0";
// =====================================================
// WEATHER
// =====================================================
float temperature = 0.0;
int humidity = 0;
String weatherMain = "";
String weatherDescription = "Loading...";
bool weatherAvailable = false;
unsigned long lastWeatherUpdate = 0;
const unsigned long WEATHER_INTERVAL =
600000;
// =====================================================
// PAGES
// =====================================================
#define PAGE_FACE 0
#define PAGE_CLOCK 1
#define PAGE_TEMP 2
#define PAGE_WEATHER 3
int currentPage = PAGE_FACE;
// =====================================================
// MOODS
// =====================================================
#define MOOD_NORMAL 0
#define MOOD_HAPPY 1
#define MOOD_SAD 2
#define MOOD_ANGRY 3
int currentMood = MOOD_NORMAL;
// =====================================================
// TOUCH
// =====================================================
bool previousTouch = false;
unsigned long touchStarted = 0;
bool longPressDone = false;
const unsigned long LONG_PRESS_TIME = 900;
// =====================================================
// EYE ANIMATION
// =====================================================
int pupilOffsetX = 0;
int pupilOffsetY = 0;
unsigned long lastEyeMove = 0;
bool blinking = false;
unsigned long blinkStarted = 0;
unsigned long nextBlink = 0;
// =====================================================
// DISPLAY UPDATE
// =====================================================
unsigned long lastScreenUpdate = 0;
// =====================================================
// WIFI RETRY
// =====================================================
unsigned long lastWiFiAttempt = 0;
const unsigned long WIFI_RETRY = 15000;
bool wifiWasConnected = false;
// =====================================================
// CENTER TEXT
// =====================================================
void centerText(
String text,
int y,
const uint8_t* font
) {
u8g2.setFont(font);
int width =
u8g2.getStrWidth(
text.c_str()
);
int x =
(128 - width) / 2;
if (x < 0) x = 0;
u8g2.drawStr(
x,
y,
text.c_str()
);
}
// =====================================================
// LOAD SETTINGS
// =====================================================
void loadSettings() {
prefs.begin("b3d", true);
wifiSSID =
prefs.getString(
"ssid",
""
);
wifiPassword =
prefs.getString(
"pass",
""
);
apiKey =
prefs.getString(
"api",
""
);
city =
prefs.getString(
"city",
"Dublin"
);
countryCode =
prefs.getString(
"country",
"IE"
);
prefs.end();
Serial.println("Settings loaded");
}
// =====================================================
// SAVE SETTINGS
// =====================================================
void saveSettings(
String newSSID,
String newPassword,
String newAPI,
String newCity,
String newCountry
) {
prefs.begin("b3d", false);
prefs.putString(
"ssid",
newSSID
);
prefs.putString(
"pass",
newPassword
);
prefs.putString(
"api",
newAPI
);
prefs.putString(
"city",
newCity
);
prefs.putString(
"country",
newCountry
);
prefs.end();
}
// =====================================================
// SETUP WEB PAGE
// =====================================================
void handleRoot() {
String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta name="viewport"
content="width=device-width, initial-scale=1">
<title>B3D Setup</title>
<style>
body{
font-family:Arial;
background:#111827;
color:white;
padding:25px;
max-width:420px;
margin:auto;
}
h1{
color:#ff7214;
font-size:38px;
}
p{
color:#bbb;
}
label{
display:block;
margin-top:18px;
font-size:17px;
}
input{
width:100%;
box-sizing:border-box;
padding:13px;
margin-top:6px;
border-radius:8px;
border:1px solid #555;
font-size:17px;
}
button{
width:100%;
padding:15px;
margin-top:25px;
background:#ff7214;
color:white;
border:none;
border-radius:8px;
font-size:19px;
}
</style>
</head>
<body>
<h1>B3D Setup</h1>
<p>Enter Wi-Fi and Weather settings</p>
<form action="/save" method="POST">
<label>Wi-Fi Name</label>
<input
name="ssid"
value=")rawliteral";
html += wifiSSID;
html += R"rawliteral("
placeholder="Wi-Fi name">
<label>Wi-Fi Password</label>
<input
name="pass"
type="password"
placeholder="Wi-Fi password">
<label>OpenWeather API Key</label>
<input
name="api"
value=")rawliteral";
html += apiKey;
html += R"rawliteral("
placeholder="OpenWeather API Key">
<label>City</label>
<input
name="city"
value=")rawliteral";
html += city;
html += R"rawliteral(">
<label>Country Code</label>
<input
name="country"
value=")rawliteral";
html += countryCode;
html += R"rawliteral(">
<button type="submit">
SAVE & CONNECT
</button>
</form>
</body>
</html>
)rawliteral";
server.send(
200,
"text/html",
html
);
}
// =====================================================
// SAVE FROM WEB PAGE
// =====================================================
void handleSave() {
String newSSID =
server.arg("ssid");
String newPassword =
server.arg("pass");
String newAPI =
server.arg("api");
String newCity =
server.arg("city");
String newCountry =
server.arg("country");
if (
newSSID.length() == 0
) {
server.send(
400,
"text/plain",
"Wi-Fi name required"
);
return;
}
// اگر پسورد خالی باشد، قبلی بماند
if (
newPassword.length() == 0
) {
newPassword =
wifiPassword;
}
if (
newCity.length() == 0
) {
newCity = "Dublin";
}
if (
newCountry.length() == 0
) {
newCountry = "IE";
}
saveSettings(
newSSID,
newPassword,
newAPI,
newCity,
newCountry
);
server.send(
200,
"text/html",
"<html><body style='font-family:Arial;background:#111827;color:white;text-align:center;padding-top:60px;'>"
"<h1 style='color:#ff7214'>Saved!</h1>"
"<p>ESP32 is restarting...</p>"
"</body></html>"
);
delay(2000);
ESP.restart();
}
// =====================================================
// START SETUP WIFI
// =====================================================
void startSetupWiFi() {
WiFi.mode(
WIFI_AP_STA
);
WiFi.setSleep(
false
);
bool ok =
WiFi.softAP(
AP_NAME,
AP_PASSWORD
);
if (ok) {
Serial.println(
"B3D-Setup started"
);
Serial.print(
"Setup IP: "
);
Serial.println(
WiFi.softAPIP()
);
}
server.on(
"/",
HTTP_GET,
handleRoot
);
server.on(
"/save",
HTTP_POST,
handleSave
);
server.begin();
}
// =====================================================
// CONNECT HOME WIFI
// =====================================================
void connectHomeWiFi() {
if (
wifiSSID.length() == 0
) {
return;
}
Serial.print(
"Connecting to: "
);
Serial.println(
wifiSSID
);
WiFi.begin(
wifiSSID.c_str(),
wifiPassword.c_str()
);
unsigned long start =
millis();
while (
WiFi.status() !=
WL_CONNECTED &&
millis() - start <
15000
) {
server.handleClient();
delay(250);
Serial.print(".");
}
Serial.println();
if (
WiFi.status() ==
WL_CONNECTED
) {
Serial.println(
"WiFi Connected!"
);
Serial.println(
WiFi.localIP()
);
}
else {
Serial.println(
"Home WiFi failed"
);
}
}
// =====================================================
// CLOCK
// =====================================================
void startClock() {
if (
WiFi.status() !=
WL_CONNECTED
) {
return;
}
configTzTime(
timezoneDublin,
"pool.ntp.org",
"time.google.com"
);
struct tm t;
getLocalTime(
&t,
10000
);
}
// =====================================================
// WEATHER
// =====================================================
void getWeather() {
if (
WiFi.status() !=
WL_CONNECTED
) {
weatherAvailable =
false;
return;
}
if (
apiKey.length() == 0
) {
weatherAvailable =
false;
return;
}
HTTPClient http;
String url =
"http://api.openweathermap.org/data/2.5/weather?q=" +
city +
"," +
countryCode +
"&appid=" +
apiKey +
"&units=metric";
http.begin(url);
int code =
http.GET();
Serial.print(
"Weather HTTP: "
);
Serial.println(
code
);
if (
code == 200
) {
String payload =
http.getString();
JSONVar data =
JSON.parse(
payload
);
if (
JSON.typeof(data) !=
"undefined"
) {
temperature =
(double)
data["main"]["temp"];
humidity =
(int)
data["main"]["humidity"];
weatherMain =
(const char*)
data["weather"][0]["main"];
weatherDescription =
(const char*)
data["weather"][0]["description"];
weatherAvailable =
true;
}
}
http.end();
lastWeatherUpdate =
millis();
}
// =====================================================
// TOUCH
// =====================================================
void handleTouch() {
bool touched =
digitalRead(
TOUCH_PIN
) ==
TOUCH_ACTIVE;
if (
touched &&
!previousTouch
) {
touchStarted =
millis();
longPressDone =
false;
}
// LONG PRESS = CHANGE FACE
if (
touched &&
previousTouch &&
!longPressDone
) {
if (
millis() -
touchStarted >
LONG_PRESS_TIME
) {
currentMood++;
if (
currentMood >
MOOD_ANGRY
) {
currentMood =
MOOD_NORMAL;
}
currentPage =
PAGE_FACE;
longPressDone =
true;
}
}
// SHORT TAP = CHANGE PAGE
if (
!touched &&
previousTouch
) {
if (
!longPressDone
) {
currentPage++;
if (
currentPage >
PAGE_WEATHER
) {
currentPage =
PAGE_FACE;
}
}
}
previousTouch =
touched;
}
// =====================================================
// EYE ANIMATION
// =====================================================
void updateEyes() {
unsigned long now =
millis();
if (
now -
lastEyeMove >
1400
) {
lastEyeMove =
now;
pupilOffsetX =
random(
-5,
6
);
pupilOffsetY =
random(
-3,
4
);
}
if (
!blinking &&
now >
nextBlink
) {
blinking =
true;
blinkStarted =
now;
}
if (
blinking &&
now -
blinkStarted >
130
) {
blinking =
false;
nextBlink =
now +
random(
2000,
5500
);
}
}
// =====================================================
// NORMAL FACE
// =====================================================
void drawNormalFace() {
u8g2.drawRBox(
18,14,36,34,8
);
u8g2.drawRBox(
74,14,36,34,8
);
u8g2.setDrawColor(0);
u8g2.drawRBox(
30 + pupilOffsetX,
24 + pupilOffsetY,
13,
16,
4
);
u8g2.drawRBox(
86 + pupilOffsetX,
24 + pupilOffsetY,
13,
16,
4
);
u8g2.setDrawColor(1);
u8g2.drawDisc(
39 + pupilOffsetX,
27 + pupilOffsetY,
2
);
u8g2.drawDisc(
95 + pupilOffsetX,
27 + pupilOffsetY,
2
);
u8g2.drawHLine(
58,
56,
12
);
}
// =====================================================
// HAPPY FACE
// =====================================================
void drawHappyFace() {
u8g2.drawLine(
18,32,28,23
);
u8g2.drawLine(
28,23,38,32
);
u8g2.drawLine(
90,32,100,23
);
u8g2.drawLine(
100,23,110,32
);
u8g2.drawLine(
50,47,56,53
);
u8g2.drawLine(
56,53,64,56
);
u8g2.drawLine(
64,56,72,53
);
u8g2.drawLine(
72,53,78,47
);
}
// =====================================================
// SAD FACE
// =====================================================
void drawSadFace() {
u8g2.drawRBox(
18,18,34,30,8
);
u8g2.drawRBox(
76,18,34,30,8
);
u8g2.setDrawColor(0);
u8g2.drawDisc(
35,
31,
5
);
u8g2.drawDisc(
93,
31,
5
);
u8g2.setDrawColor(1);
u8g2.drawLine(
52,58,58,53
);
u8g2.drawLine(
58,53,64,51
);
u8g2.drawLine(
64,51,70,53
);
u8g2.drawLine(
70,53,76,58
);
}
// =====================================================
// ANGRY FACE
// =====================================================
void drawAngryFace() {
u8g2.drawRBox(
18,20,34,29,6
);
u8g2.drawRBox(
76,20,34,29,6
);
u8g2.drawLine(
16,12,51,22
);
u8g2.drawLine(
77,22,112,12
);
u8g2.setDrawColor(0);
u8g2.drawDisc(
36,
35,
5
);
u8g2.drawDisc(
92,
35,
5
);
u8g2.setDrawColor(1);
u8g2.drawHLine(
54,
57,
20
);
}
// =====================================================
// FACE PAGE
// =====================================================
void drawFacePage() {
updateEyes();
if (
blinking
) {
u8g2.drawRBox(
18,31,36,4,2
);
u8g2.drawRBox(
74,31,36,4,2
);
return;
}
if (
currentMood ==
MOOD_NORMAL
) {
drawNormalFace();
}
else if (
currentMood ==
MOOD_HAPPY
) {
drawHappyFace();
}
else if (
currentMood ==
MOOD_SAD
) {
drawSadFace();
}
else {
drawAngryFace();
}
}
// =====================================================
// CLOCK PAGE
// =====================================================
void drawClockPage() {
struct tm t;
if (
!getLocalTime(
&t,
50
)
) {
centerText(
"Syncing...",
34,
u8g2_font_6x12_tf
);
return;
}
char timeText[10];
char dateText[24];
strftime(
timeText,
sizeof(timeText),
"%H:%M",
&t
);
strftime(
dateText,
sizeof(dateText),
"%d %b %Y",
&t
);
centerText(
"DUBLIN",
10,
u8g2_font_5x8_tf
);
centerText(
timeText,
39,
u8g2_font_logisoso24_tf
);
centerText(
dateText,
60,
u8g2_font_6x12_tf
);
}
// =====================================================
// TEMP PAGE
// =====================================================
void drawTempPage() {
centerText(
"DUBLIN",
11,
u8g2_font_6x12_tf
);
if (
!weatherAvailable
) {
centerText(
"Waiting...",
37,
u8g2_font_6x12_tf
);
return;
}
char tempText[16];
snprintf(
tempText,
sizeof(tempText),
"%.1f C",
temperature
);
centerText(
tempText,
39,
u8g2_font_logisoso20_tf
);
char humText[20];
snprintf(
humText,
sizeof(humText),
"Humidity: %d%%",
humidity
);
centerText(
humText,
59,
u8g2_font_6x12_tf
);
}
// =====================================================
// WEATHER PAGE
// =====================================================
void drawWeatherPage() {
centerText(
"DUBLIN WEATHER",
11,
u8g2_font_5x8_tf
);
if (
!weatherAvailable
) {
centerText(
"Weather unavailable",
37,
u8g2_font_5x8_tf
);
return;
}
centerText(
weatherMain,
35,
u8g2_font_7x14B_tf
);
centerText(
weatherDescription,
59,
u8g2_font_6x12_tf
);
}
// =====================================================
// DRAW PAGE
// =====================================================
void drawCurrentPage() {
u8g2.clearBuffer();
if (
currentPage ==
PAGE_FACE
) {
drawFacePage();
}
else if (
currentPage ==
PAGE_CLOCK
) {
drawClockPage();
}
else if (
currentPage ==
PAGE_TEMP
) {
drawTempPage();
}
else {
drawWeatherPage();
}
u8g2.sendBuffer();
}
// =====================================================
// SETUP
// =====================================================
void setup() {
Serial.begin(115200);
delay(1000);
pinMode(
TOUCH_PIN,
INPUT
);
Wire.begin(
OLED_SDA,
OLED_SCL
);
u8g2.begin();
u8g2.setContrast(255);
randomSeed(micros());
loadSettings();
// --------------------------------
// ALWAYS START SETUP WIFI
// --------------------------------
startSetupWiFi();
// --------------------------------
// OLED SETUP INFO
// --------------------------------
u8g2.clearBuffer();
centerText(
"B3D SETUP",
12,
u8g2_font_6x12_tf
);
u8g2.setFont(
u8g2_font_5x8_tf
);
u8g2.drawStr(
0,
29,
"WiFi: B3D-Setup"
);
u8g2.drawStr(
0,
42,
"Pass: 12345678"
);
u8g2.drawStr(
0,
56,
"IP: 192.168.4.1"
);
u8g2.sendBuffer();
delay(1500);
// --------------------------------
// HOME WIFI
// --------------------------------
connectHomeWiFi();
if (
WiFi.status() ==
WL_CONNECTED
) {
startClock();
getWeather();
wifiWasConnected =
true;
}
nextBlink =
millis() +
3000;
currentPage =
PAGE_FACE;
}
// =====================================================
// LOOP
// =====================================================
void loop() {
// صفحه تنظیمات همیشه فعال
server.handleClient();
unsigned long now =
millis();
handleTouch();
// --------------------------------
// WiFi reconnect
// --------------------------------
bool connected =
WiFi.status() ==
WL_CONNECTED;
if (
!connected &&
wifiSSID.length() > 0 &&
now -
lastWiFiAttempt >
WIFI_RETRY
) {
lastWiFiAttempt =
now;
WiFi.begin(
wifiSSID.c_str(),
wifiPassword.c_str()
);
}
// تازه وصل شده
if (
connected &&
!wifiWasConnected
) {
wifiWasConnected =
true;
startClock();
getWeather();
}
if (
!connected
) {
wifiWasConnected =
false;
}
// Weather refresh
if (
connected &&
now -
lastWeatherUpdate >
WEATHER_INTERVAL
) {
getWeather();
}
// Display refresh
if (
now -
lastScreenUpdate >
50
) {
lastScreenUpdate =
now;
drawCurrentPage();
}
}