commit 2681e792c4c3f96b7dcc75b4aa967aa876114673 Author: byun Date: Fri Mar 27 21:45:58 2026 +0000 Upload files to "/" diff --git a/AquaLED_Controller.ino b/AquaLED_Controller.ino new file mode 100644 index 0000000..2ae1356 --- /dev/null +++ b/AquaLED_Controller.ino @@ -0,0 +1,451 @@ +/* + * ============================================================ + * 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); +} \ No newline at end of file