#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <ESP32Servo.h>
#include <DHT.h>
#include "RTClib.h"
#include <math.h>
//==================== OLED ====================
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 OLED(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
//==================== SERVO ===================
const int servoPin = 2;
Servo servo;
//==================== DHT =====================
#define dhtPin 15
#define dhttype DHT22
DHT dht(dhtPin, dhttype);
//==================== HC-SR04 =================
const int trigPin = 12;
const int echoPin = 14;
//==================== POT (0..4095 -> 0..180deg)
#define POT_PIN 26
//==================== JOYSTICK ================
#define JOY_X 32
#define JOY_Y 33
#define JOY_SW 25
const int ADC_MID = 2048;
const int DEAD = 400;
//==================== LDR =====================
#define LDR_OUT 27 // ADC2
//====== LDR -> lux (ปรับได้ตามเซ็นเซอร์จริง) ======
const float VREF = 3.3;
const float R_FIXED= 10000.0; // ตัวต้านทานคงที่ในวงจรแบ่งแรงดัน (ประมาณ 10k)
const float RL10 = 50000.0; // ความต้านทาน LDR ที่ 10 lux
const float GAMMA = 0.7; // เอ็กซ์โปเนนต์ของ LDR
// LDR ต่อบน (3.3V) / R_FIXED ต่อกราวด์ -> สว่างมาก Vout ต่ำ
float adcToLux(int adc) {
if (adc <= 0) return 100000.0;
if (adc >= 4095) return 0.0;
float vout = (adc / 4095.0f) * VREF;
float r_ldr = (R_FIXED * vout) / (VREF - vout);
float lux = pow(RL10 / r_ldr, 1.0f / GAMMA) * 10.0f;
if (!isfinite(lux) || lux < 0) lux = 0;
return lux;
}
//==================== RTC =====================
RTC_DS1307 rtc;
bool showClock = true; // เริ่มที่นาฬิกา
//==================== MENU STATE ==============
byte menuIndex = 1; // 1..5
bool inPage = false;
//==================== INACTIVITY (ทุกหน้า) ====
const unsigned long GLOBAL_IDLE_MS = 10000; // 10s
unsigned long lastAction = 0; // อัปเดตเมื่อมี interaction
unsigned long lastNav = 0;
const unsigned long navGap = 180; // กันรัวจอย
//==================== FORWARD DECL ============
void drawMenu(byte sel);
void showTemperature();
void showHumidity();
void showDistance();
void showDegree();
void showIllumination();
void drawClockScreen();
//==================== HELPERS =================
inline bool joyClickRaw() { return digitalRead(JOY_SW) == LOW; }
bool prevUp=false, prevDown=false, prevClick=false;
inline bool joyUp() { return analogRead(JOY_Y) > (ADC_MID + DEAD); }
inline bool joyDown() { return analogRead(JOY_Y) < (ADC_MID - DEAD); }
inline void markAction() { lastAction = millis(); }
// edge-detect ปุ่มคลิก (ใช้ในหน้าเพจ)
bool clickTriggeredOnce() {
bool c = joyClickRaw();
bool fire = (c && !prevClick);
prevClick = c;
if (fire) markAction();
return fire;
}
// ---------- UI helpers ----------
void uiHeader(const char* title) {
OLED.clearDisplay();
OLED.setTextColor(WHITE, BLACK);
OLED.setTextSize(1);
int16_t x1, y1; uint16_t w, h;
OLED.getTextBounds(title, 0, 0, &x1, &y1, &w, &h);
int cx = (SCREEN_WIDTH - w) / 2;
OLED.setCursor(cx, 2);
OLED.print(title);
OLED.drawLine(6, 14, SCREEN_WIDTH-6, 14, WHITE);
}
// ตัวเลขชิดซ้าย + หน่วยชิดขวา (auto scale)
void uiValueLeftRight(const char* value, const char* unit, int yVal=20) {
// หน่วยขวา
OLED.setTextSize(1);
int16_t ux1, uy1; uint16_t uw, uh;
OLED.getTextBounds(unit, 0, 0, &ux1, &uy1, &uw, &uh);
int unitX = SCREEN_WIDTH - 6 - (int)uw;
OLED.setCursor(unitX, yVal);
OLED.print(unit);
// ค่าซ้าย
OLED.setTextSize(2);
int16_t vx1, vy1; uint16_t vw, vh;
OLED.getTextBounds(value, 0, 0, &vx1, &vy1, &vw, &vh);
int valueX = 6;
int maxW = unitX - 4 - valueX;
if ((int)vw > maxW) {
OLED.setTextSize(1);
OLED.getTextBounds(value, 0, 0, &vx1, &vy1, &vw, &vh);
}
OLED.setCursor(valueX, yVal);
OLED.print(value);
}
void uiBarPercent(int percent) {
percent = constrain(percent, 0, 100);
const int bx = 8, by = 44, bw = 112, bh = 12;
OLED.drawRect(bx, by, bw, bh, WHITE);
int fillw = map(percent, 0, 100, 0, bw - 2);
if (fillw > 0) OLED.fillRect(bx + 1, by + 1, fillw, bh - 2, WHITE);
}
bool isValid(float v) { return !(isnan(v) || isinf(v)); }
//==================== SETUP ===================
void setup() {
Serial.begin(115200);
pinMode(trigPin, OUTPUT);
pinMode(echoPin, INPUT);
pinMode(JOY_SW, INPUT_PULLUP);
OLED.begin(SSD1306_SWITCHCAPVCC, 0x3C);
OLED.clearDisplay();
OLED.display();
dht.begin();
servo.attach(servoPin, 500, 2400);
if (!rtc.begin()) {
Serial.println("ไม่พบ DS1307!");
}
if (!rtc.isrunning()) {
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
}
markAction(); // เริ่มจับเวลา inactivity
drawClockScreen(); // แสดงเวลาเริ่มต้น
}
//==================== LOOP ====================
void loop() {
// --------- CLOCK MODE ---------
if (showClock) {
static unsigned long lastUpdate = 0;
if (millis() - lastUpdate > 250) {
drawClockScreen();
lastUpdate = millis();
}
bool click = joyClickRaw();
if (click && !prevClick) {
showClock = false;
inPage = false;
markAction();
drawMenu(menuIndex);
}
prevClick = click;
delay(10);
return;
}
// --------- MENU / PAGES ---------
bool up = joyUp();
bool down = joyDown();
bool click = joyClickRaw();
unsigned long now = millis();
// ถ้า inactivity เกินกำหนดจาก "ทุกหน้า" -> กลับนาฬิกา
if (now - lastAction >= GLOBAL_IDLE_MS) {
showClock = true;
drawClockScreen();
prevUp = prevDown = prevClick = false; // reset edges
delay(10);
return;
}
if (!inPage) {
if (now - lastNav > navGap) {
if (up && !prevUp) { menuIndex = (menuIndex <= 1) ? 5 : menuIndex - 1; lastNav = now; markAction(); drawMenu(menuIndex); }
if (down && !prevDown){ menuIndex = (menuIndex >= 5) ? 1 : menuIndex + 1; lastNav = now; markAction(); drawMenu(menuIndex); }
if (click && !prevClick) {
lastNav = now; markAction();
inPage = true;
switch (menuIndex) {
case 1: showTemperature(); break;
case 2: showHumidity(); break;
case 3: showDistance(); break;
case 4: showDegree(); break;
case 5: showIllumination(); break;
}
inPage = false;
// ถ้าหน้าเพจ set ให้กลับนาฬิกาไว้ ก็วาดนาฬิกาเลย
if (showClock) { drawClockScreen(); }
else { drawMenu(menuIndex); }
markAction(); // กลับออกมาแล้วรีเซ็ตเวลาใหม่
}
}
prevUp = up; prevDown = down; prevClick = click;
delay(10);
return;
}
// inPage: เพจจะจัดการเอง (รวม timeout ภายในด้วย)
delay(10);
}
//==================== CLOCK ===================
void drawClockScreen() {
DateTime t = rtc.now();
char timeStr[16], dateStr[16];
snprintf(timeStr, sizeof(timeStr), "%02d:%02d:%02d", t.hour(), t.minute(), t.second());
snprintf(dateStr, sizeof(dateStr), "%02d/%02d/%04d", t.day(), t.month(), t.year());
OLED.clearDisplay();
OLED.setTextColor(WHITE, BLACK);
OLED.setTextSize(2);
OLED.setCursor(18, 14);
OLED.print(timeStr);
OLED.setTextSize(1);
OLED.setCursor(28, 48);
OLED.print(dateStr);
OLED.display();
}
//==================== MENU ====================
void drawMenu(byte sel) {
OLED.clearDisplay();
OLED.setTextColor(WHITE, BLACK);
OLED.setTextSize(1);
OLED.setCursor(35, 0);
OLED.print("MENU");
OLED.setCursor(5, 15);
OLED.setTextColor(sel==1 ? BLACK : WHITE, sel==1 ? WHITE : BLACK);
OLED.print("1. Temperature");
OLED.setCursor(5, 25);
OLED.setTextColor(sel==2 ? BLACK : WHITE, sel==2 ? WHITE : BLACK);
OLED.print("2. Humidity");
OLED.setCursor(5, 35);
OLED.setTextColor(sel==3 ? BLACK : WHITE, sel==3 ? WHITE : BLACK);
OLED.print("3. Distance");
OLED.setCursor(5, 45);
OLED.setTextColor(sel==4 ? BLACK : WHITE, sel==4 ? WHITE : BLACK);
OLED.print("4. position");
OLED.setCursor(5, 55);
OLED.setTextColor(sel==5 ? BLACK : WHITE, sel==5 ? WHITE : BLACK);
OLED.print("5. Illumination");
OLED.display();
OLED.setTextColor(WHITE, BLACK);
}
//==================== PAGES ===================
void showTemperature() {
while (joyClickRaw()) delay(5);
markAction();
while (true) {
uiHeader("TEMPERATURE");
float t = dht.readTemperature();
char val[16];
if (isValid(t)) {
dtostrf(t, 0, 1, val);
uiValueLeftRight(val, "deg C");
int p = map((long)(t*10), -400, 800, 0, 100); // -40..80C
uiBarPercent(constrain(p,0,100));
} else {
uiValueLeftRight("--", "sensor");
uiBarPercent(0);
}
OLED.display();
if (clickTriggeredOnce()) break;
if (millis() - lastAction >= GLOBAL_IDLE_MS) { showClock = true; break; }
delay(80);
}
}
void showHumidity() {
while (joyClickRaw()) delay(5);
markAction();
while (true) {
uiHeader("HUMIDITY");
float h = dht.readHumidity();
char val[16];
if (isValid(h)) {
dtostrf(h, 0, 1, val);
uiValueLeftRight(val, "% RH");
uiBarPercent(constrain((int)h,0,100));
} else {
uiValueLeftRight("--", "sensor");
uiBarPercent(0);
}
OLED.display();
if (clickTriggeredOnce()) break;
if (millis() - lastAction >= GLOBAL_IDLE_MS) { showClock = true; break; }
delay(80);
}
}
void showDistance() {
while (joyClickRaw()) delay(5);
markAction();
while (true) {
uiHeader("DISTANCE");
digitalWrite(trigPin, LOW); delayMicroseconds(2);
digitalWrite(trigPin, HIGH); delayMicroseconds(10);
digitalWrite(trigPin, LOW);
long dur = pulseIn(echoPin, HIGH, 30000UL);
if (dur == 0) {
uiValueLeftRight("--", "no echo");
uiBarPercent(0);
} else {
int cm = (int)((dur/2.0) * 0.0343);
cm = max(cm, 0);
char val[16]; snprintf(val, sizeof(val), "%d", cm);
uiValueLeftRight(val, "cm");
int p = map(cm, 0, 400, 0, 100);
uiBarPercent(constrain(p,0,100));
}
OLED.display();
if (clickTriggeredOnce()) break;
if (millis() - lastAction >= GLOBAL_IDLE_MS) { showClock = true; break; }
delay(80);
}
}
void showDegree() {
while (joyClickRaw()) delay(5);
markAction();
while (true) {
uiHeader("POSITION");
int deg = map(analogRead(POT_PIN), 0, 4095, 0, 180);
servo.write(deg);
char val[16]; snprintf(val, sizeof(val), "%d", deg);
uiValueLeftRight(val, "deg");
int p = map(deg, 0, 180, 0, 100);
uiBarPercent(constrain(p,0,100));
OLED.display();
if (clickTriggeredOnce()) break;
if (millis() - lastAction >= GLOBAL_IDLE_MS) { showClock = true; break; }
delay(50);
}
}
void showIllumination() {
while (joyClickRaw()) delay(5);
markAction();
while (true) {
uiHeader("ILLUMINATION");
const int N = 10; long sum = 0;
for (int i=0;i<N;i++) { sum += analogRead(LDR_OUT); delay(2); }
int raw = sum / N;
float lux = adcToLux(raw);
if (!isfinite(lux) || lux < 0) lux = 0;
char val[16]; const char* unit = "lux";
if (lux >= 10000.0f) { dtostrf(lux/1000.0f, 0, 2, val); unit = "klux"; }
else { dtostrf(lux, 0, 1, val); }
uiValueLeftRight(val, unit);
// log-ish bar: 1..2000 lux -> 0..100%
float lc = lux; if (lc < 1) lc = 1; if (lc > 2000) lc = 2000;
int p = map((int)(log10(lc) * 100), 0, 300, 0, 100);
uiBarPercent(constrain(p,0,100));
OLED.display();
if (clickTriggeredOnce()) break;
if (millis() - lastAction >= GLOBAL_IDLE_MS) { showClock = true; break; }
delay(80);
}
}