#include <WiFi.h>
#include <WebServer.h>
#include "RTClib.h"
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// :::::::::::::::::::::::::::::::::::::::::::::::::::::::::
// Налаштування WiFi
// :::::::::::::::::::::::::::::::::::::::::::::::::::::::::
const char* SSID = "Wokwi-GUEST";
const char* password = "";
// ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
// API OpenAI (підстав свій ключ)
// ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
const char* openaiApiKey = "b453666eb3d04c1fa7c608375d88f7fc";
// ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
// Годинник + датчики
// ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
RTC_DS1307 rtc;
const int POT_PIN = 35; // симуляція пульсу (потенціометр)
const int NTC_PIN = 32; // аналоговий датчик температури
WebServer server(80);
// ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
// OLED екран
// ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
TwoWire I2COLED = TwoWire(1);
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &I2COLED);
// ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
// Дані для відображення
// ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
String dataValue = "Завантаження...";
String recommendation = "Ще нема поради";
float lastTemp = 25.0;
String history[10];
int historyIndex = 0;
// ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
// HTML сторінка (новий дизайн)
// ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
void sendHtml() {
String html = R"rawliteral(
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<title>Моніторинг ESP32</title>
<style>
body {
margin:0; padding:0;
font-family: 'Segoe UI', sans-serif;
background: #121212;
color: #f0f0f0;
display: flex;
flex-direction: column;
align-items: center;
}
header {
width: 100%;
padding: 15px;
background: #1e1e1e;
text-align: center;
font-size: 22px;
font-weight: bold;
letter-spacing: 1px;
color: #76c7c0;
box-shadow: 0 2px 5px rgba(0,0,0,0.5);
}
.container {
display: grid;
gap: 20px;
margin-top: 30px;
width: 90%;
max-width: 700px;
}
.box {
padding: 20px;
border-radius: 12px;
background: #1e1e1e;
box-shadow: 0 2px 10px rgba(0,0,0,0.7);
}
.box h3 { margin-top: 0; color: #76c7c0; }
pre { background:#2c2c2c; padding:15px; border-radius:8px; }
button {
margin-top: 15px;
padding: 12px 18px;
border:none;
border-radius:8px;
font-size: 15px;
background: linear-gradient(135deg, #76c7c0, #4e9e99);
color:white;
cursor:pointer;
transition: 0.3s;
}
button:hover { transform: scale(1.05); }
</style>
<script>
async function refreshPage() {
let res = await fetch('/');
let text = await res.text();
document.open();
document.write(text);
document.close();
}
async function getAdvice() {
let res = await fetch('/update?value=rec');
let text = await res.text();
document.open();
document.write(text);
document.close();
}
setInterval(refreshPage, 7000);
</script>
</head>
<body>
<header>📊 ESP32 Health Dashboard</header>
<div class="container">
<div class="box">
<h3>📌 Поточні показники</h3>
<pre>)rawliteral";
html += dataValue;
html += R"rawliteral(</pre>
</div>
<div class="box">
<h3>🤖 Порада ШІ</h3>
<p>)rawliteral" + recommendation + R"rawliteral(</p>
<button onclick="getAdvice()">Отримати нову пораду</button>
</div>
</div>
</body>
</html>
)rawliteral";
server.send(200, "text/html", html);
}
// ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
// Обробники веб-запитів
// ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
void handleRoot() { sendHtml(); }
void request_gpt(String prompt) {
DynamicJsonDocument jsonDocument(4096);
jsonDocument["model"] = "gpt-3.5-turbo";
JsonArray messages = jsonDocument.createNestedArray("messages");
JsonObject sys = messages.createNestedObject();
sys["role"] = "system";
sys["content"] = "Ти асистент, який аналізує дані здоров’я (температура, пульс). "
"Ігноруй дивні різкі стрибки (наприклад, -5C → 90C), бо це збої сенсора. "
"Пиши поради лаконічно українською.";
String historyPrompt = "Останні значення:\n";
for (int i = 0; i < 10; i++) {
if (history[i] != "") historyPrompt += history[i] + "\n";
}
JsonObject usr = messages.createNestedObject();
usr["role"] = "user";
usr["content"] = historyPrompt + "\nПоточні дані:\n" + prompt;
HTTPClient http;
String apiUrl = "https://artificialintelligence.openai.azure.com/openai/deployments/test/chat/completions?api-version=2023-05-15";
http.begin(apiUrl);
http.addHeader("Content-Type", "application/json");
http.addHeader("api-key", openaiApiKey);
String body;
serializeJson(jsonDocument, body);
int httpCode = http.POST(body);
if (httpCode == 200) {
String response = http.getString();
DynamicJsonDocument resDoc(8192);
deserializeJson(resDoc, response);
recommendation = resDoc["choices"][0]["message"]["content"].as<String>();
Serial.println("AI Advice: " + recommendation);
} else {
recommendation = "Помилка: " + String(httpCode);
}
http.end();
}
void handleUpdate() {
if (server.hasArg("value")) {
if (server.arg("value") == "rec") {
request_gpt(dataValue);
}
}
sendHtml();
}
// ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
// Зчитування датчика температури
// ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
float getTemperature() {
const float BETA = 3950;
int analogValue = analogRead(NTC_PIN);
float celsius = 1 / (log(1 / (4095. / analogValue - 1)) / BETA + 1.0 / 298.15) - 273.15;
return celsius;
}
// ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
// Ініціалізація
// ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
void setup() {
Serial.begin(115200);
pinMode(NTC_PIN, INPUT);
// RTC
Wire.begin(21, 22);
if (!rtc.begin()) Serial.println("RTC не знайдено!");
// OLED
I2COLED.begin(14, 12);
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) Serial.println("OLED error");
display.clearDisplay();
// WiFi
WiFi.begin(SSID, password);
while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
Serial.println("\nWiFi OK, IP: " + WiFi.localIP().toString());
// WebServer
server.on("/", handleRoot);
server.on("/update", handleUpdate);
server.begin();
}
// ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
// Основний цикл
// ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
void loop() {
server.handleClient();
// RTC
DateTime now = rtc.now();
String Time = String(now.hour()) + ":" + String(now.minute()) + ":" + String(now.second());
// Температура з перевіркою на аномалії
float temp = getTemperature();
bool anomaly = abs(temp - lastTemp) > 20.0;
if (!anomaly) lastTemp = temp;
String Temp = anomaly ? "АНОМАЛІЯ!" : String(temp, 1) + " C";
// Пульс з потенціометра
int potValue = analogRead(POT_PIN);
int bpm = map(potValue, 0, 4095, 60, 180);
String HeartRate = String(bpm) + " уд/хв";
// Формування даних
dataValue = "Час: " + Time + "\n" +
"Пульс: " + HeartRate + "\n" +
"Температура: " + Temp + "\n";
// Зберігаємо історію
history[historyIndex] = dataValue;
historyIndex = (historyIndex + 1) % 10;
// OLED вивід
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(WHITE);
display.setCursor(0,0);
display.println("ESP32 Monitor");
display.println("Time: " + Time);
display.println("Pulse: " + HeartRate);
display.println("Temp: " + Temp);
display.display();
delay(1000);
}