#include <Arduino.h> #include "EPD.h" #include "EPD_GUI.h" #include "Ap_29demo.h" #include <WiFi.h> #include <WebServer.h> #include <ESPmDNS.h> #include <WiFiClient.h> #include "FS.h" #include "SPIFFS.h" #include <Preferences.h>
// 이미지 버퍼 uint8_t ImageBW[15000];
// 이미지 크기 정의 #define txt_size 3808 #define pre_size 4576 #define full_size 15000 #define MAX_IMAGE_SIZE 15000
// WiFi 설정 const char* WIFI_SSID = "공유기 SSID"; const char* WIFI_PASSWORD = "공유기 비밀번호"; const char* DEVICE_NAME = "esp32-display";
// 대체 AP 모드 설정 const char* AP_SSID = "ESP32_Setup"; const char* AP_PASS = "12345678";
// 웹서버 인스턴스 WebServer server(80); Preferences preferences;
// 연결 상태 bool wifiConnected = false; unsigned long lastReconnectAttempt = 0; const unsigned long reconnectInterval = 5000;
// 파일 저장용 변수 File fsUploadFile; unsigned char price_formerly[pre_size]; unsigned char txt_formerly[txt_size]; unsigned char full_formerly[full_size];
String filename; int flag_txt = 0; int flag_pre = 0; int flag_full = 0;
// 함수 선언 void clear_all(); void loadImageToMemory(String fname, size_t size); void updateDisplay(); void loadSavedImages(); void displayStartupMessage(); bool connectToWiFi(const char* ssid, const char* password, int timeout = 10000); void startAPMode();
// 디스플레이 전체 지우기 void clear_all() { EPD_Clear(); Paint_NewImage(ImageBW, EPD_W, EPD_H, 0, WHITE); EPD_Full(WHITE); EPD_Display_Part(0, 0, EPD_W, EPD_H, ImageBW); }
// 이미지를 메모리에 로드 void loadImageToMemory(String fname, size_t size) { File file = SPIFFS.open(fname, FILE_READ); if (!file) { Serial.println("Failed to open file for reading"); return; } if (fname == "/txt.bin" && size <= txt_size) { file.read(txt_formerly, txt_size); flag_txt = 1; flag_full = 0; Serial.println("Text image loaded to memory"); } else if (fname == "/pre.bin" && size <= pre_size) { file.read(price_formerly, pre_size); flag_pre = 1; flag_full = 0; Serial.println("Price image loaded to memory"); } else if (fname == "/full.bin" && size <= full_size) { file.read(full_formerly, full_size); flag_full = 1; flag_txt = 0; flag_pre = 0; Serial.println("Full screen image loaded to memory"); } file.close(); }
// 디스플레이 업데이트 void updateDisplay() { clear_all(); // 전체 화면 모드 if (flag_full == 1) { Serial.println("Displaying full screen image (400x300)"); EPD_ShowPicture(0, 0, EPD_W, EPD_H, full_formerly, WHITE); } // 부분 이미지 모드 else { // 배경 표시 EPD_ShowPicture(0, 0, EPD_W, 40, background_top, WHITE); // 텍스트 이미지 표시 if (flag_txt == 1) { EPD_ShowPicture(20, 60, 272, 112, txt_formerly, WHITE); } // 가격 이미지 표시 if (flag_pre == 1) { EPD_ShowPicture(20, 190, 352, 104, price_formerly, WHITE); } } // 화면 업데이트 EPD_Display_Part(0, 0, EPD_W, EPD_H, ImageBW); EPD_Sleep(); Serial.println("Display updated successfully"); }
// HTML 페이지 String getHTMLHead() { return R"( <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>ESP32 Display Control</title> <style> body { font-family: 'Segoe UI', Arial, sans-serif; margin: 0; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; } .container { max-width: 800px; margin: 0 auto; background: white; border-radius: 20px; padding: 30px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); } h1 { color: #333; text-align: center; margin-bottom: 10px; } .status { background: #f0f0f0; padding: 15px; border-radius: 10px; margin: 20px 0; } .status-item { display: flex; justify-content: space-between; margin: 5px 0; } .online { color: green; font-weight: bold; } .offline { color: red; font-weight: bold; } .upload-form { border: 3px dashed #ccc; padding: 30px; border-radius: 10px; margin: 20px 0; text-align: center; transition: all 0.3s; } .upload-form:hover { border-color: #667eea; background: #f8f9ff; } input[type="file"] { margin: 15px 0; padding: 10px; } select { margin: 10px 0; padding: 8px 15px; border-radius: 5px; border: 1px solid #ccc; font-size: 16px; } .btn { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 12px 30px; border: none; border-radius: 25px; cursor: pointer; font-size: 16px; font-weight: bold; transition: transform 0.2s; } .btn:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0,0,0,0.3); } .wifi-config { background: #fff3cd; border: 1px solid #ffc107; padding: 15px; border-radius: 10px; margin: 20px 0; } input[type="text"], input[type="password"] { width: 100%; padding: 10px; margin: 5px 0; border: 1px solid #ccc; border-radius: 5px; box-sizing: border-box; } </style> </head> <body> <div class="container"> )"; }
String getHTMLFoot() { return R"( </div> </body> </html> )"; }
// 메인 페이지 void handleRoot() { String html = getHTMLHead(); html += R"( <h1>🖥️ ESP32 E-ink Display</h1> <div class="status"> <h3>시스템 상태</h3> <div class="status-item"> <span>연결 상태:</span> )"; if (WiFi.status() == WL_CONNECTED) { html += "<span class='online'>온라인</span>"; html += R"( </div> <div class="status-item"> <span>IP 주소:</span> <span>)" + WiFi.localIP().toString() + R"(</span> </div> <div class="status-item"> <span>WiFi SSID:</span> <span>)" + WiFi.SSID() + R"(</span> </div> )"; } else { html += "<span class='offline'>오프라인 (AP 모드)</span>"; html += R"( </div> <div class="status-item"> <span>AP SSID:</span> <span>)" + String(AP_SSID) + R"(</span> </div> )"; } html += R"( <div class="status-item"> <span>현재 모드:</span> <span>)" + String(flag_full ? "전체 화면" : "부분 이미지") + R"(</span> </div> </div> <div class="upload-form"> <h3>📤 이미지 업로드</h3> <form method="post" action="/upload" enctype="multipart/form-data"> <label for="type">이미지 타입:</label><br> <select name="type" id="type"> <option value="full">전체 화면 (400x300) - 권장</option> <option value="txt">텍스트 영역 (272x112)</option> <option value="pre">가격 영역 (352x104)</option> </select><br><br> <input type="file" name="file" id="file" accept=".bin,.raw,image/*" required><br><br> <input class="btn" type="submit" value="업로드"> </form> </div> <div class="wifi-config"> <h3>⚙️ WiFi 설정</h3> <form method="post" action="/wifi"> <input type="text" name="ssid" placeholder="WiFi SSID" required><br> <input type="password" name="password" placeholder="WiFi Password" required><br><br> <input class="btn" type="submit" value="WiFi 설정 저장"> </form> </div> )"; html += getHTMLFoot(); server.send(200, "text/html", html); }
// WiFi 설정 저장 void handleWiFiConfig() { String ssid = server.arg("ssid"); String password = server.arg("password"); if (ssid.length() > 0) { preferences.begin("wifi", false); preferences.putString("ssid", ssid); preferences.putString("password", password); preferences.end(); String html = getHTMLHead(); html += R"( <h1>✅ WiFi 설정 저장됨</h1> <p>새로운 WiFi 설정이 저장되었습니다.</p> <p>장치가 재시작됩니다...</p> <script>setTimeout(function(){window.location.href='/';}, 3000);</script> )"; html += getHTMLFoot(); server.send(200, "text/html", html); delay(1000); ESP.restart(); } else { server.send(400, "text/plain", "Invalid parameters"); } }
// API 상태 확인 void handleStatus() { String response = "{\n"; response += " \"status\": \"" + String(wifiConnected ? "connected" : "ap_mode") + "\",\n"; response += " \"full_loaded\": " + String(flag_full == 1 ? "true" : "false") + ",\n"; response += " \"txt_loaded\": " + String(flag_txt == 1 ? "true" : "false") + ",\n"; response += " \"pre_loaded\": " + String(flag_pre == 1 ? "true" : "false") + ",\n"; if (wifiConnected) { response += " \"ip\": \"" + WiFi.localIP().toString() + "\",\n"; response += " \"ssid\": \"" + WiFi.SSID() + "\"\n"; } else { response += " \"ip\": \"" + WiFi.softAPIP().toString() + "\"\n"; } response += "}"; server.send(200, "application/json", response); }
// 파일 업로드 핸들러 void handleFileUpload() { HTTPUpload& upload = server.upload(); if (upload.status == UPLOAD_FILE_START) { String type = server.arg("type"); Serial.printf("Upload Start: %s, Type: %s\n", upload.filename.c_str(), type.c_str()); if (type == "txt") { filename = "/txt.bin"; } else if (type == "pre") { filename = "/pre.bin"; } else if (type == "full") { filename = "/full.bin"; } else { filename = "/full.bin"; } fsUploadFile = SPIFFS.open(filename, FILE_WRITE); } else if (upload.status == UPLOAD_FILE_WRITE) { if (fsUploadFile) { fsUploadFile.write(upload.buf, upload.currentSize); } } else if (upload.status == UPLOAD_FILE_END) { if (fsUploadFile) { fsUploadFile.close(); Serial.printf("Upload Complete: %s, Size: %d\n", filename.c_str(), upload.totalSize); // 메모리에 로드 loadImageToMemory(filename, upload.totalSize); // 화면 업데이트 Serial.println("Updating display..."); updateDisplay(); Serial.println("Display update completed!"); String html = getHTMLHead(); html += R"( <h1>✅ 업로드 성공!</h1> <p>이미지가 성공적으로 업로드되고 화면에 표시되었습니다.</p> <p>파일: )" + filename + R"(</p> <p>크기: )" + String(upload.totalSize) + R"( bytes</p> <br> <a href="/" class="btn">돌아가기</a> )"; html += getHTMLFoot(); server.send(200, "text/html", html); } } }
// WiFi 연결 시도 bool connectToWiFi(const char* ssid, const char* password, int timeout) { Serial.print("Connecting to WiFi: "); Serial.println(ssid); WiFi.begin(ssid, password); unsigned long startTime = millis(); while (WiFi.status() != WL_CONNECTED && millis() - startTime < timeout) { delay(500); Serial.print("."); } if (WiFi.status() == WL_CONNECTED) { Serial.println("\nWiFi Connected!"); Serial.print("IP Address: "); Serial.println(WiFi.localIP()); return true; } else { Serial.println("\nWiFi Connection Failed!"); return false; } }
// AP 모드 시작 void startAPMode() { Serial.println("Starting AP Mode..."); WiFi.mode(WIFI_AP); WiFi.softAP(AP_SSID, AP_PASS); IPAddress IP = WiFi.softAPIP(); Serial.print("AP IP address: "); Serial.println(IP); }
// 저장된 이미지 로드 void loadSavedImages() { // 전체 화면 우선 확인 if (SPIFFS.exists("/full.bin")) { File file = SPIFFS.open("/full.bin", FILE_READ); if (file) { size_t bytesRead = file.read(full_formerly, full_size); file.close(); if (bytesRead > 0) { flag_full = 1; flag_txt = 0; flag_pre = 0; Serial.println("✓ Loaded saved full screen image"); return; } } } // 부분 이미지 로드 if (SPIFFS.exists("/txt.bin")) { File file = SPIFFS.open("/txt.bin", FILE_READ); if (file) { size_t bytesRead = file.read(txt_formerly, txt_size); file.close(); if (bytesRead > 0) { flag_txt = 1; Serial.println("✓ Loaded saved text image"); } } } if (SPIFFS.exists("/pre.bin")) { File file = SPIFFS.open("/pre.bin", FILE_READ); if (file) { size_t bytesRead = file.read(price_formerly, pre_size); file.close(); if (bytesRead > 0) { flag_pre = 1; Serial.println("✓ Loaded saved price image"); } } } }
// 시작 화면 표시 void displayStartupMessage() { if (flag_txt || flag_pre || flag_full) { return; } clear_all(); Serial.println("Display ready!"); }
void setup() { Serial.begin(115200); Serial.println("\n\n================================="); Serial.println("ESP32 E-ink Display Server v3.0"); Serial.println("=================================\n"); // SPIFFS 초기화 if (!SPIFFS.begin(true)) { Serial.println("SPIFFS Mount Failed"); return; } Serial.println("✓ SPIFFS Started"); // Preferences 초기화 preferences.begin("wifi", false); String saved_ssid = preferences.getString("ssid", ""); String saved_password = preferences.getString("password", ""); preferences.end(); // WiFi 연결 시도 WiFi.mode(WIFI_STA); if (saved_ssid.length() > 0) { Serial.println("Trying saved WiFi credentials..."); wifiConnected = connectToWiFi(saved_ssid.c_str(), saved_password.c_str(), 10000); } if (!wifiConnected && strlen(WIFI_SSID) > 0) { Serial.println("Trying hardcoded WiFi credentials..."); wifiConnected = connectToWiFi(WIFI_SSID, WIFI_PASSWORD, 10000); } if (!wifiConnected) { startAPMode(); } // mDNS 시작 if (wifiConnected) { if (MDNS.begin(DEVICE_NAME)) { Serial.printf("✓ mDNS started: %s.local\n", DEVICE_NAME); MDNS.addService("http", "tcp", 80); } } // 웹서버 라우트 설정 server.on("/", HTTP_GET, handleRoot); server.on("/status", HTTP_GET, handleStatus); server.on("/wifi", HTTP_POST, handleWiFiConfig); // Multipart 업로드 (Python 스크립트용) server.on("/upload", HTTP_POST, [](){ // 업로드 완료 후 처리는 handleFileUpload에서 }, handleFileUpload); // 404 핸들러 server.onNotFound([](){ server.send(404, "text/plain", "Not found"); }); server.begin(); Serial.println("✓ HTTP server started"); // 접속 정보 출력 Serial.println("\n================================="); if (wifiConnected) { Serial.println("Python 사용:"); Serial.printf("python esp32_auto_uploader.py %s \"텍스트\"\n", WiFi.localIP().toString().c_str()); } else { Serial.println("AP 모드:"); Serial.printf("WiFi: %s (비밀번호: %s)\n", AP_SSID, AP_PASS); Serial.println("http://192.168.4.1"); } Serial.println("=================================\n"); // 디스플레이 초기화 pinMode(7, OUTPUT); digitalWrite(7, HIGH); EPD_GPIOInit(); EPD_Clear(); Paint_NewImage(ImageBW, EPD_W, EPD_H, 0, WHITE); EPD_Full(WHITE); EPD_Display_Part(0, 0, EPD_W, EPD_H, ImageBW); EPD_Init_Fast(Fast_Seconds_1_5s); // 저장된 이미지 로드 loadSavedImages(); updateDisplay(); displayStartupMessage(); }
void loop() { server.handleClient(); // WiFi 재연결 로직 if (!wifiConnected && WiFi.getMode() == WIFI_STA) { unsigned long now = millis(); if (now - lastReconnectAttempt > reconnectInterval) { lastReconnectAttempt = now; preferences.begin("wifi", true); String saved_ssid = preferences.getString("ssid", ""); String saved_password = preferences.getString("password", ""); preferences.end(); if (saved_ssid.length() > 0) { wifiConnected = connectToWiFi(saved_ssid.c_str(), saved_password.c_str(), 5000); if (wifiConnected) { MDNS.begin(DEVICE_NAME); MDNS.addService("http", "tcp", 80); } } } } delay(2); } |