/* * ============================================================ * AquaLED Controller - ESP32-WROOM-32 (30pin) * GPIO13 -> PWM LED (수초 어항용) * ESP32 Arduino Core 3.x 전용 * 보드: ESP32 Dev Module * ============================================================ */ #include #include #include #include #define LED_PIN 13 #define PWM_RESOLUTION 8 const char* AP_SSID = "AquaLED_AP"; const char* AP_PASS = "aqua1234"; const char* NTP_SERVER = "pool.ntp.org"; const long GMT_OFFSET = 9L * 3600L; const int DST_OFFSET = 0; Preferences prefs; WebServer server(80); struct Config { int duty; int freq; int onH, onM; int offH, offM; bool scheduleOn; bool manualState; String staSSID; String staPASS; }; Config cfg; bool ntpSynced = false; bool staConnected = false; bool needReconnect = false; void loadConfig() { prefs.begin("aled", true); cfg.duty = prefs.getInt ("duty", 200); cfg.freq = prefs.getInt ("freq", 1000); cfg.onH = prefs.getInt ("onH", 8); cfg.onM = prefs.getInt ("onM", 0); cfg.offH = prefs.getInt ("offH", 22); cfg.offM = prefs.getInt ("offM", 0); cfg.scheduleOn = prefs.getBool ("sched", true); cfg.manualState = prefs.getBool ("mstate", false); cfg.staSSID = prefs.getString("ssid", ""); cfg.staPASS = prefs.getString("pass", ""); prefs.end(); } void saveConfig() { prefs.begin("aled", false); prefs.putInt ("duty", cfg.duty); prefs.putInt ("freq", cfg.freq); prefs.putInt ("onH", cfg.onH); prefs.putInt ("onM", cfg.onM); prefs.putInt ("offH", cfg.offH); prefs.putInt ("offM", cfg.offM); prefs.putBool ("sched", cfg.scheduleOn); prefs.putBool ("mstate", cfg.manualState); prefs.putString("ssid", cfg.staSSID); prefs.putString("pass", cfg.staPASS); prefs.end(); } // Core 3.x: 핀 번호로 PWM 제어 void applyPWM(int duty) { ledcWrite(LED_PIN, constrain(duty, 0, 255)); } void reconfigPWM(int freq, int duty) { ledcDetach(LED_PIN); ledcAttach(LED_PIN, freq, PWM_RESOLUTION); ledcWrite(LED_PIN, constrain(duty, 0, 255)); } void startAP() { WiFi.softAP(AP_SSID, AP_PASS); Serial.printf("[AP] SSID: %s IP: %s\n", AP_SSID, WiFi.softAPIP().toString().c_str()); } bool connectSTA(const String& ssid, const String& pass, int timeoutMs = 10000) { if (ssid.length() == 0) return false; Serial.printf("[STA] 연결 시도: %s\n", ssid.c_str()); WiFi.begin(ssid.c_str(), pass.c_str()); unsigned long t = millis(); while (WiFi.status() != WL_CONNECTED && millis() - t < (unsigned long)timeoutMs) { delay(300); } if (WiFi.status() == WL_CONNECTED) { Serial.printf("[STA] 연결됨 IP: %s\n", WiFi.localIP().toString().c_str()); configTime(GMT_OFFSET, DST_OFFSET, NTP_SERVER); return true; } Serial.println("[STA] 연결 실패"); return false; } void tryNTP() { if (ntpSynced) return; struct tm t; if (getLocalTime(&t, 2000)) { ntpSynced = true; Serial.printf("[NTP] 동기화: %04d-%02d-%02d %02d:%02d:%02d\n", t.tm_year+1900, t.tm_mon+1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec); } } void checkSchedule() { if (!cfg.scheduleOn || !ntpSynced) return; struct tm t; if (!getLocalTime(&t, 500)) return; int now = t.tm_hour * 60 + t.tm_min; int onT = cfg.onH * 60 + cfg.onM; int offT = cfg.offH * 60 + cfg.offM; bool shouldOn; if (onT < offT) { shouldOn = (now >= onT && now < offT); } else { shouldOn = (now >= onT || now < offT); } static bool prevState = false; if (shouldOn != prevState) { prevState = shouldOn; applyPWM(shouldOn ? cfg.duty : 0); Serial.printf("[SCHED] LED %s\n", shouldOn ? "ON" : "OFF"); } } String getTimeStr() { if (!ntpSynced) return "NTP 미동기화"; struct tm t; if (!getLocalTime(&t, 500)) return "시간 오류"; char buf[32]; strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &t); return String(buf); } String buildPage() { char onBuf[6], offBuf[6]; snprintf(onBuf, 6, "%02d:%02d", cfg.onH, cfg.onM); snprintf(offBuf, 6, "%02d:%02d", cfg.offH, cfg.offM); int pct = cfg.duty * 100 / 255; String staIP = staConnected ? WiFi.localIP().toString() : "-"; String timeStr = getTimeStr(); String ntpBadge = ntpSynced ? "동기화됨" : "미동기화"; String staBadge = staConnected ? "연결됨" : "미연결"; String schedSel = cfg.scheduleOn ? "checked" : ""; String manualSel = !cfg.scheduleOn ? "checked" : ""; String manStyle = cfg.scheduleOn ? "none" : "block"; String schedStyle = cfg.scheduleOn ? "block" : "none"; String ledState = ""; if (!cfg.scheduleOn) { ledState = cfg.manualState ? "ON" : "OFF"; } String html = "" "" "" "AquaLED Controller" "" "
" "

🐠 AQUALED 어항 LED 컨트롤러

" "
" "🕐 " + timeStr + " " + ntpBadge + "" "📶 STA " + staIP + " " + staBadge + "" "
" "
" // PWM 카드 "
💡
" "

⚡ PWM 제어

" "
" "" "" "
" "0 ← 어둡게 / 밝게 → 255" "" + String(cfg.duty) + "" " (" + String(pct) + "%)" "
" "" "" "" "
" // 스케줄 카드 "
" "

📅 동작 설정

" "
" "" "
" "" "" "" "" "
" "
" "
" "
" "
" "
" "
" "
" "

" "※ 꺼지는 시각이 켜지는 시각보다 이를 경우 자정을 넘기는 스케줄로 처리됩니다

" "
" "
" "

현재 상태: " + ledState + "

" "
" "" "" "
" "" "
" // WiFi 카드 "
📡
" "

📶 외부 WiFi 연결 (STA)

" "
" "" "" "" "" "

" "※ 저장 후 STA 재연결합니다. AP(192.168.4.1)는 계속 유지됩니다

" "" "
" // 시스템 정보 카드 "
" "

ℹ️ 시스템 정보

" "
AP SSID" + String(AP_SSID) + "
" "
AP PW" + String(AP_PASS) + "
" "
AP IP192.168.4.1
" "
STA IP" + staIP + "
" "
GPIO13
" "
Duty / Freq" "" + String(cfg.duty) + " / " + String(cfg.freq) + " Hz
" "
" "
" // /wrap "" ""; return html; } void setupRoutes() { server.on("/", HTTP_GET, []() { server.send(200, "text/html; charset=utf-8", buildPage()); }); server.on("/set_pwm", HTTP_GET, []() { if (server.hasArg("duty")) cfg.duty = constrain(server.arg("duty").toInt(), 0, 255); if (server.hasArg("freq")) cfg.freq = constrain(server.arg("freq").toInt(), 1, 40000); reconfigPWM(cfg.freq, 0); if (!cfg.scheduleOn && cfg.manualState) applyPWM(cfg.duty); saveConfig(); server.sendHeader("Location", "/"); server.send(302, "text/plain", ""); }); server.on("/set_schedule", HTTP_GET, []() { if (server.hasArg("mode")) cfg.scheduleOn = (server.arg("mode") == "sched"); if (server.hasArg("on_time")) { String t = server.arg("on_time"); if (t.length() >= 5) { cfg.onH = t.substring(0,2).toInt(); cfg.onM = t.substring(3,5).toInt(); } } if (server.hasArg("off_time")) { String t = server.arg("off_time"); if (t.length() >= 5) { cfg.offH = t.substring(0,2).toInt(); cfg.offM = t.substring(3,5).toInt(); } } if (!cfg.scheduleOn) applyPWM(cfg.manualState ? cfg.duty : 0); saveConfig(); server.sendHeader("Location", "/"); server.send(302, "text/plain", ""); }); server.on("/manual", HTTP_GET, []() { if (server.hasArg("s")) { cfg.manualState = (server.arg("s") == "1"); applyPWM(cfg.manualState ? cfg.duty : 0); saveConfig(); } server.sendHeader("Location", "/"); server.send(302, "text/plain", ""); }); server.on("/set_wifi", HTTP_GET, []() { if (server.hasArg("ssid")) cfg.staSSID = server.arg("ssid"); if (server.hasArg("pass") && server.arg("pass").length() > 0) cfg.staPASS = server.arg("pass"); saveConfig(); needReconnect = true; server.sendHeader("Location", "/"); server.send(302, "text/plain", ""); }); server.onNotFound([]() { server.send(404, "text/plain", "Not Found"); }); } void setup() { Serial.begin(115200); delay(300); Serial.println("\n====== AquaLED Controller ======"); loadConfig(); // Core 3.x PWM 초기화 (ledcSetup / ledcAttachPin 없음) ledcAttach(LED_PIN, cfg.freq, PWM_RESOLUTION); applyPWM(0); Serial.printf("[PWM] GPIO%d %dHz 8bit\n", LED_PIN, cfg.freq); WiFi.mode(WIFI_AP_STA); startAP(); staConnected = connectSTA(cfg.staSSID, cfg.staPASS); if (staConnected) tryNTP(); setupRoutes(); server.begin(); Serial.println("[WEB] 서버 시작 -> http://192.168.4.1"); } unsigned long lastScheduleCheck = 0; unsigned long lastNtpRetry = 0; void loop() { server.handleClient(); if (needReconnect) { needReconnect = false; WiFi.disconnect(); ntpSynced = false; staConnected = false; delay(200); staConnected = connectSTA(cfg.staSSID, cfg.staPASS, 10000); if (staConnected) tryNTP(); } unsigned long now = millis(); if (now - lastScheduleCheck >= 30000UL) { lastScheduleCheck = now; staConnected = (WiFi.status() == WL_CONNECTED); checkSchedule(); } if (!ntpSynced && staConnected && (now - lastNtpRetry >= 60000UL)) { lastNtpRetry = now; tryNTP(); } delay(5); }