commit 05822ba5837941520d1d06f3aa493f54bd753105 Author: choibk Date: Fri Dec 19 23:27:35 2025 +0900 AP Mode 설정 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ca33e27 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.pio/ +.vscode/ +include/README +test/README diff --git a/data/index.html b/data/index.html new file mode 100644 index 0000000..a6c7777 --- /dev/null +++ b/data/index.html @@ -0,0 +1,24 @@ + + + + +

Main

+
+WiFi Setup + + + + + diff --git a/data/wifi.html b/data/wifi.html new file mode 100644 index 0000000..8baa9e5 --- /dev/null +++ b/data/wifi.html @@ -0,0 +1,181 @@ + + + + + + WiFi Setup + + + + + + +

WiFi Setup

+ + +
+

+ +
+

+ + + + + +
+ + +
+ +
+ + +
+ +
+ + +

Saved Networks

+
+ +
+ +    + Back + + + + \ No newline at end of file diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..9379397 --- /dev/null +++ b/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into the executable file. + +The source code of each library should be placed in a separate directory +("lib/your_library_name/[Code]"). + +For example, see the structure of the following example libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +Example contents of `src/main.c` using Foo and Bar: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +The PlatformIO Library Dependency Finder will find automatically dependent +libraries by scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..c9b9d8a --- /dev/null +++ b/platformio.ini @@ -0,0 +1,16 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:d1] +platform = espressif8266 +board = d1 +framework = arduino +monitor_speed = 115200 +board_build.filesystem = littlefs \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..5787302 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,35 @@ +#include +#include +#include +#include "mainpage/mainpage.h" +#include "wifi/wifi.h" + +ESP8266WebServer server(80); // 🔹 전역 서버 객체 + +void setup() +{ + Serial.begin(115200); + delay(1000); + Serial.println("Booting system..."); + + if (!LittleFS.begin()) + { + Serial.println("LittleFS mount failed!"); + } + else + { + Serial.println("LittleFS mounted."); + } + + mainPageSetup(server); // 🔹 Main 페이지 등록 + wifiSetup(server); // 🔹 WiFi 모듈 초기화 + + server.begin(); + Serial.println("Web server started"); +} + +void loop() +{ + server.handleClient(); + wifiLoop(); // FSM 기반 WiFi 관리 +} diff --git a/src/mainpage/mainpage.cpp b/src/mainpage/mainpage.cpp new file mode 100644 index 0000000..7b98eec --- /dev/null +++ b/src/mainpage/mainpage.cpp @@ -0,0 +1,10 @@ +#include "mainpage.h" +#include + +void mainPageSetup(ESP8266WebServer& server) { + server.on("/", [&server]() { + File f = LittleFS.open("/index.html", "r"); + server.streamFile(f, "text/html"); + f.close(); + }); +} diff --git a/src/mainpage/mainpage.h b/src/mainpage/mainpage.h new file mode 100644 index 0000000..8e07395 --- /dev/null +++ b/src/mainpage/mainpage.h @@ -0,0 +1,8 @@ +#ifndef MAINPAGE_MODULE_H +#define MAINPAGE_MODULE_H + +#include + +void mainPageSetup(ESP8266WebServer& server); + +#endif diff --git a/src/wifi/wifi.cpp b/src/wifi/wifi.cpp new file mode 100644 index 0000000..f2103d3 --- /dev/null +++ b/src/wifi/wifi.cpp @@ -0,0 +1,322 @@ +#include "wifi.h" +#include +#include +#include + +#define EEPROM_SIZE 1024 +#define MAX_WIFI 10 +#define SSID_LEN 32 +#define PASS_LEN 64 + +unsigned long connectStart = 0; +bool connecting = false; + +struct WifiCred +{ + char ssid[SSID_LEN]; + char pass[PASS_LEN]; +}; + +WifiState wifiState = WIFI_IDLE; + +WiFiEventHandler onDisconnectHandler; + +unsigned long lastReconnectAttempt = 0; +const unsigned long RECONNECT_INTERVAL = 10000; // 10초 + +WifiCred wifiList[MAX_WIFI]; +int wifiCount = 0; + +// --- EEPROM functions --- +void loadWifiFromEEPROM() +{ + EEPROM.begin(EEPROM_SIZE); + EEPROM.get(0, wifiCount); + if (wifiCount < 0 || wifiCount > MAX_WIFI) + wifiCount = 0; + EEPROM.get(sizeof(int), wifiList); + Serial.printf("Loaded %d WiFi creds\n", wifiCount); +} + +void saveWifiToEEPROM() +{ + EEPROM.put(0, wifiCount); + EEPROM.put(sizeof(int), wifiList); + EEPROM.commit(); + Serial.println("WiFi creds saved to EEPROM"); +} + +void addWifiCred(const String &ssid, const String &pass) +{ + int found = -1; + + for (int i = 0; i < wifiCount; i++) + { + if (ssid == wifiList[i].ssid) + { + found = i; + break; + } + } + + WifiCred cred; + strncpy(cred.ssid, ssid.c_str(), SSID_LEN); + strncpy(cred.pass, pass.c_str(), PASS_LEN); + + if (found == -1) + { + // 신규 AP → 공간 없으면 마지막 제거 + if (wifiCount < MAX_WIFI) + { + wifiCount++; + } + // 뒤에서부터 한 칸씩 밀기 + for (int i = wifiCount - 1; i > 0; i--) + { + wifiList[i] = wifiList[i - 1]; + } + wifiList[0] = cred; + } + else + { + // 기존 AP → 0번으로 이동 + for (int i = found; i > 0; i--) + { + wifiList[i] = wifiList[i - 1]; + } + wifiList[0] = cred; + } + + saveWifiToEEPROM(); +} + +// --- WiFi Setup --- +void wifiSetup(ESP8266WebServer &server) +{ + + loadWifiFromEEPROM(); + + setupWifiEvents(); + + WiFi.mode(WIFI_AP); + WiFi.softAP("TEST_AP", "12345678"); + + Serial.print("AP IP: "); + Serial.println(WiFi.softAPIP()); + + // ✅ ⑤ 자동 재연결 + if (wifiCount > 0) + { + Serial.print("Auto connect to: "); + Serial.println(wifiList[0].ssid); + WiFi.mode(WIFI_AP_STA); + WiFi.begin(wifiList[0].ssid, wifiList[0].pass); + connecting = true; + connectStart = millis(); + wifiState = WIFI_CONNECTING; + } + + // --- WiFi Page --- + server.on("/wifi", [&server]() + { + Serial.println("GET /wifi"); + File f = LittleFS.open("/wifi.html", "r"); + if (!f) { + server.send(500, "text/plain", "wifi.html not found"); + return; + } + server.streamFile(f, "text/html"); + f.close(); }); + + // --- Scan API --- + server.on("/api/scan", HTTP_GET, [&server]() + { + Serial.println("API: scan"); + + int n = WiFi.scanNetworks(); + String json = "["; + bool first = true; + + for (int i = 0; i < n; i++) { + String ssid = WiFi.SSID(i); + if (ssid.length() == 0) continue; + if (!first) json += ","; + first = false; + json += "{\"ssid\":\"" + ssid + "\",\"rssi\":" + String(WiFi.RSSI(i)) + "}"; + } + json += "]"; + server.send(200, "application/json", json); }); + + // --- Connect API --- + server.on("/api/connect", HTTP_POST, [&server]() + { + Serial.println("API: connect"); + + String body = server.arg("plain"); + + int s1 = body.indexOf("\"ssid\":\"") + 8; + int s2 = body.indexOf("\"", s1); + int p1 = body.indexOf("\"pass\":\"") + 8; + int p2 = body.indexOf("\"", p1); + + String ssid = body.substring(s1, s2); + String pass = body.substring(p1, p2); + + Serial.printf("Connecting to SSID: %s\n", ssid.c_str()); + + addWifiCred(ssid, pass); + + WiFi.mode(WIFI_AP_STA); + WiFi.begin(ssid.c_str(), pass.c_str()); + + connecting = true; + connectStart = millis(); + wifiState = WIFI_CONNECTING; + + server.send(200, "text/plain", "Connecting..."); }); + + // --- Status API --- + server.on("/api/status", HTTP_GET, [&server]() + { + String json = "{"; + wl_status_t st = WiFi.status(); + + json += "\"status\":\""; + if (st == WL_CONNECTED) json += "connected"; + else if (connecting) json += "connecting"; + else json += "disconnected"; + json += "\""; + + if (st == WL_CONNECTED) { + json += ",\"ssid\":\"" + WiFi.SSID() + "\""; + json += ",\"ip\":\"" + WiFi.localIP().toString() + "\""; + json += ",\"rssi\":" + String(WiFi.RSSI()); + } + json += "}"; + + server.send(200, "application/json", json); }); + + // --- Saved AP List API --- + server.on("/api/saved", HTTP_GET, [&server]() + { + String json = "["; + for (int i = 0; i < wifiCount; i++) { + if (i) json += ","; + json += "\""; + json += wifiList[i].ssid; + json += "\""; + } + json += "]"; + server.send(200, "application/json", json); }); + + /// --- Connect to saved AP by index --- + server.on("/api/connect_saved", HTTP_POST, [&server]() + { + Serial.println("API: connect_saved"); + + String body = server.arg("plain"); // ✅ body 정의 + + int p = body.indexOf(":") + 1; + int e = body.indexOf("}", p); + int idx = body.substring(p, e).toInt(); + + if (idx >= 0 && idx < wifiCount) { + Serial.printf("Connecting to saved: %s\n", wifiList[idx].ssid); + + WiFi.mode(WIFI_AP_STA); + WiFi.begin(wifiList[idx].ssid, wifiList[idx].pass); + + connecting = true; + connectStart = millis(); + + server.send(200, "text/plain", "Connecting to saved AP..."); + } else { + server.send(400, "text/plain", "Invalid index"); + } }); + + // --- Disconnect API --- + server.on("/api/disconnect", HTTP_POST, [&server]() + { + Serial.println("API: disconnect"); + + WiFi.disconnect(true); + WiFi.mode(WIFI_AP); // ✅ 여기 중요 + + connecting = false; + server.send(200, "text/plain", "Disconnected"); }); + // --- Delete saved AP by index --- + server.on("/api/delete_saved", HTTP_POST, [&server]() + { + Serial.println("API: delete_saved"); + + String body = server.arg("plain"); + + int p = body.indexOf(":") + 1; + int e = body.indexOf("}", p); + int idx = body.substring(p, e).toInt(); + + if (idx >= 0 && idx < wifiCount) { + Serial.printf("Delete saved AP: %s\n", wifiList[idx].ssid); + + // 뒤의 항목들을 앞으로 당김 + for (int i = idx; i < wifiCount - 1; i++) { + wifiList[i] = wifiList[i + 1]; + } + wifiCount--; + saveWifiToEEPROM(); + + server.send(200, "text/plain", "Deleted"); + } else { + server.send(400, "text/plain", "Invalid index"); + } }); +} +void setupWifiEvents() { + onDisconnectHandler = WiFi.onStationModeDisconnected( + [](const WiFiEventStationModeDisconnected& event) { + Serial.println("WiFi event: disconnected"); + wifiState = WIFI_LOST; + lastReconnectAttempt = 0; + } + ); +} +void wifiLoop() { + wl_status_t st = WiFi.status(); + + // CONNECTING → CONNECTED + if (wifiState == WIFI_CONNECTING && st == WL_CONNECTED) { + wifiState = WIFI_CONNECTED; + connecting = false; + Serial.println("WiFi FSM: CONNECTED"); + return; + } + + // CONNECTED 상태에서 끊김 감지 (이벤트가 놓쳤을 경우 보조) + if (wifiState == WIFI_CONNECTED && st != WL_CONNECTED) { + wifiState = WIFI_LOST; + Serial.println("WiFi FSM: LOST"); + } + + // LOST 상태 → 제한적 재연결 + if (wifiState == WIFI_LOST && wifiCount > 0) { + if (millis() - lastReconnectAttempt > RECONNECT_INTERVAL) { + lastReconnectAttempt = millis(); + Serial.println("WiFi FSM: Reconnect attempt"); + + WiFi.mode(WIFI_AP_STA); + WiFi.begin(wifiList[0].ssid, wifiList[0].pass); + + connecting = true; + connectStart = millis(); + wifiState = WIFI_CONNECTING; + } + } + + // CONNECTING 타임아웃 → LOST + if (wifiState == WIFI_CONNECTING && + millis() - connectStart > 15000) { + Serial.println("WiFi FSM: connect timeout"); + WiFi.disconnect(); + wifiState = WIFI_LOST; + connecting = false; + } +} diff --git a/src/wifi/wifi.h b/src/wifi/wifi.h new file mode 100644 index 0000000..2a2d57b --- /dev/null +++ b/src/wifi/wifi.h @@ -0,0 +1,16 @@ +#pragma once +#include + +void wifiSetup(ESP8266WebServer &server); +void wifiLoop(); + +// FSM 상태 +enum WifiState +{ + WIFI_IDLE, + WIFI_CONNECTING, + WIFI_CONNECTED, + WIFI_LOST +}; + +extern WifiState wifiState;