AP Mode 설정

This commit is contained in:
choibk 2025-12-19 23:27:35 +09:00
commit 05822ba583
10 changed files with 662 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.pio/
.vscode/
include/README
test/README

24
data/index.html Normal file
View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body style="background-color: #1C304A;">
<h2>Main</h2>
<div id="wifi"></div>
<a href="/wifi">WiFi Setup</a>
<script>
async function loadStatus() {
const res = await fetch("/api/status");
const st = await res.json();
let html = "WiFi: " + st.status;
if (st.status === "connected") {
html += `<br>${st.ssid} (${st.ip})`;
}
document.getElementById("wifi").innerHTML = html;
}
setInterval(loadStatus, 3000);
loadStatus();
</script>
</body>
</html>

181
data/wifi.html Normal file
View File

@ -0,0 +1,181 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>WiFi Setup</title>
<script>
/*
===============================
WiFi Scan
================================
*/
async function scanWifi() {
const scanDiv = document.getElementById("scan");
scanDiv.innerHTML = "Scanning...";
try {
const res = await fetch("/api/scan");
const list = await res.json();
if (!list.length) {
scanDiv.innerHTML = "No networks found.";
return;
}
// RSSI 기준 정렬 (신호 강한 순)
list.sort((a, b) => b.rssi - a.rssi);
let html = "<ul>";
list.forEach(n => {
html += `<li>
<a href="#" onclick="selectSSID('${n.ssid}')">
${n.ssid} (${n.rssi} dBm)
</a>
</li>`;
});
html += "</ul>";
scanDiv.innerHTML = html;
} catch (e) {
scanDiv.innerHTML = "Scan failed.";
}
}
/* 선택한 SSID 입력창에 반영 */
function selectSSID(ssid) {
document.getElementById("ssid").value = ssid;
}
/*
===============================
Connect / Disconnect
================================
*/
async function connectWifi() {
const ssid = document.getElementById("ssid").value;
const pass = document.getElementById("pass").value;
const res = await fetch("/api/connect", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ssid, pass })
});
const txt = await res.text();
document.getElementById("result").innerText = txt;
}
async function disconnectWifi() {
await fetch("/api/disconnect", { method: "POST" });
}
/*
===============================
Status Polling
================================
*/
async function pollStatus() {
const res = await fetch("/api/status");
const st = await res.json();
let txt = "Status: " + st.status;
if (st.status === "connected") {
txt += `<br>SSID: ${st.ssid}<br>IP: ${st.ip}<br>RSSI: ${st.rssi}`;
}
document.getElementById("result").innerHTML = txt;
}
setInterval(pollStatus, 2000);
/*
===============================
Saved Networks
================================
*/
async function loadSaved() {
const res = await fetch("/api/saved");
const list = await res.json();
if (!list.length) {
document.getElementById("saved").innerHTML =
"저장된 AP 정보가 없습니다.";
return;
}
let html = "<ul>";
list.forEach((s, i) => {
html += `<li>
${s}
<button onclick="connectSaved(${i})">Connect</button>
<button onclick="deleteSaved(${i})">Delete</button>
</li>`;
});
html += "</ul>";
document.getElementById("saved").innerHTML = html;
}
async function connectSaved(i) {
await fetch("/api/connect_saved", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ index: i })
});
}
async function deleteSaved(i) {
if (!confirm("이 AP 정보를 삭제하시겠습니까?")) return;
await fetch("/api/delete_saved", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ index: i })
});
loadSaved(); // 목록 갱신
}
/* 페이지 로드 시 저장된 AP 불러오기 */
loadSaved();
</script>
</head>
<body style="background-color:#1C304A; color:white; font-family: Arial;">
<h2>WiFi Setup</h2>
<!-- 입력 영역 -->
<label>SSID</label><br>
<input id="ssid" style="width:200px;"><br><br>
<label>Password</label><br>
<input id="pass" type="password" style="width:200px;"><br><br>
<!-- 버튼 정렬 -->
<button onclick="connectWifi()">Connect</button>
<button onclick="scanWifi()">Scan</button>
<hr>
<!-- Scan 결과 -->
<div id="scan"></div>
<hr>
<!-- 상태 표시 -->
<div id="result"></div>
<hr>
<!-- 저장된 네트워크 -->
<h3>Saved Networks</h3>
<div id="saved"></div>
<br>
<button onclick="disconnectWifi()">Disconnect</button>
&nbsp;&nbsp;
<a href="/" style="color:#7faaff;">Back</a>
</body>
</html>

46
lib/README Normal file
View File

@ -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 <Foo.h>
#include <Bar.h>
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

16
platformio.ini Normal file
View File

@ -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

35
src/main.cpp Normal file
View File

@ -0,0 +1,35 @@
#include <Arduino.h>
#include <ESP8266WebServer.h>
#include <LittleFS.h>
#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 관리
}

10
src/mainpage/mainpage.cpp Normal file
View File

@ -0,0 +1,10 @@
#include "mainpage.h"
#include <LittleFS.h>
void mainPageSetup(ESP8266WebServer& server) {
server.on("/", [&server]() {
File f = LittleFS.open("/index.html", "r");
server.streamFile(f, "text/html");
f.close();
});
}

8
src/mainpage/mainpage.h Normal file
View File

@ -0,0 +1,8 @@
#ifndef MAINPAGE_MODULE_H
#define MAINPAGE_MODULE_H
#include <ESP8266WebServer.h>
void mainPageSetup(ESP8266WebServer& server);
#endif

322
src/wifi/wifi.cpp Normal file
View File

@ -0,0 +1,322 @@
#include "wifi.h"
#include <ESP8266WiFi.h>
#include <LittleFS.h>
#include <EEPROM.h>
#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;
}
}

16
src/wifi/wifi.h Normal file
View File

@ -0,0 +1,16 @@
#pragma once
#include <ESP8266WebServer.h>
void wifiSetup(ESP8266WebServer &server);
void wifiLoop();
// FSM 상태
enum WifiState
{
WIFI_IDLE,
WIFI_CONNECTING,
WIFI_CONNECTED,
WIFI_LOST
};
extern WifiState wifiState;