CrowPanel ESP32 전자잉크 스크린 제어해보기

webnautes
By -
0

 CrowPanel ESP32 4.2inch E-paper HMI Display 개발보드를 사용하여 전자 잉크 스크린 제어해봅니다.



2025. 11. 1  최초작성




전자 잉크 스크린을 여러 곳에서 팔고 있는데 전 ESP32 기반 전자잉크 스크린 4.2인치를 엘레파츠에서 주문했습니다.

https://www.eleparts.co.kr/goods/view?no=16874152 



전자잉크 스크린과 C타입 USB 선이 포함되어 있습니다.

USB 선을 사용하여 아두이노 IDE에서 컴파일 한 펌웨어를 업로드할 수 있습니다.





다음 링크에서 Arduino IDE를 다운로드해서 사용했습니다. 최신 버전 아두이노 IDE에선 UI가 이전 버전과 많이 달라졌습니다.

https://www.arduino.cc/en/software/





1. 메뉴에서 Tools > Board > Boards Manager를 선택합니다.




2. esp32를 검색하여 esp32 항목을 설치합니다. 





3. 아두이노 IDE 메뉴에서 Tools > Board > esp32 > ESP32S3 Dev Module를 선택합니다.

Tools > Flash Size > 8MB (64Mb)를 선택합니다.

Tools > Partition Scheme > Huge APP (3MB No OTA/1MB SPIFPS)를 선택합니다.

Tools > PSRAM > OPI PSRAM을 선택합니다. 



4. C타입 USB선을 사용하여 전자잉크 스크린을 피시에 연결합니다.




5. 메뉴에서 Tools > Port 항목에 바로 포트가 잡히지 않다가 좀 기다려보고 다시 시도해보니 잡힌 COM4를 잡았습니다.

아두이노 IDE 아래쪽에 다음처럼 보입니다.




6. 다음 링크에 있는 코드를 다운로드합니다.

https://www.elecrow.com/download/product/CrowPanel/E-paper/4.2-DIE07300S/Arduino/Demos.zip 




7. 압축을 풀어준 후 4.2_WIFI_refresh 폴더에 있는 4.2_WIFI_refresh.ino 파일을 아두이노 IDE에서 열어줘야합니다.

아두이노 IDE의 메뉴에서 File > Open을 선택 후  4.2_WIFI_refresh.ino 파일을 열어줍니다.



8. 다음 코드로 대체하고 업로드합니다.


다음 부분에 공유기 접속 정보를 입력해줘야 합니다.


const char* WIFI_SSID = "공유기 SSID";      

const char* WIFI_PASSWORD = "공유기 비밀번호";


#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);
}





9. 메뉴에서 Tools > Serial Monitor를 선택하고 115200 baud로 변경한 후,  전자잉크 스크린을 피시에 다시 연결하면 시리얼 모니터에 전자잉크 스크린의 아이피 주소가 보입니다.




10. 다음 파이썬 코드에서 앞에서 확인한 아이피를 사용하여 실행합니다.

파이썬 코드를 실행하려면 requests, numpy, Pillow 패키지를 설치해야 합니다. 


실행 결과입니다.


(esp32) C:\Users\freem\esp32>python esp32_image_uploader.py 192.168.45.74 안녕하세요~~

==================================================

ESP32 E-ink Display

==================================================

IP: 192.168.45.74

텍스트: 안녕하세요~~

--------------------------------------------------


✓ 연결 성공


이미지 생성 중...

ESP32로 전송 중...


✓ 전송 완료!



전자잉크 스크린의 상태입니다. 파이썬에서 전송한 문자열이 보입니다.





#!/usr/bin/env python3
"""
ESP32 E-ink Display 자동 업로더
사용자 입력 없이 자동으로 한글 텍스트를 이미지로 생성하여 전송
"""

import requests
import numpy as np
from PIL import Image, ImageDraw, ImageFont
import io
import os
import sys
import urllib.request


class AutoESP32Uploader:
    def __init__(self, esp_ip, port=80):
        """
        ESP32 디스플레이 업로더 초기화
       
        Args:
            esp_ip: ESP32의 IP 주소
            port: 웹서버 포트 (기본: 80)
        """
        self.esp_ip = esp_ip
        self.port = port
        self.base_url = f"http://{esp_ip}:{port}"
       
        # 이미지 크기 정의
        self.txt_size = (272, 112)
        self.pre_size = (352, 104)
        self.full_size = (400, 300# 전체 화면
       
        # 폰트 파일 경로
        self.font_path = self.get_korean_font()
   
    def get_korean_font(self):
        """한글 폰트를 찾거나 다운로드"""
        # 현재 디렉토리에 폰트 저장
        font_dir = os.path.dirname(os.path.abspath(__file__))
        font_file = os.path.join(font_dir, "NanumGothic.ttf")
       
        # 이미 다운로드된 폰트가 있으면 사용
        if os.path.exists(font_file):
            return font_file
       
        # 시스템 폰트 경로 확인
        system_fonts = [
            "C:/Windows/Fonts/malgun.ttf",     # Windows 한글
            "C:/Windows/Fonts/gulim.ttc",      # Windows 한글
            "/usr/share/fonts/truetype/nanum/NanumGothic.ttf"# Linux 한글
            "/System/Library/Fonts/AppleGothic.ttf"# macOS 한글
        ]
       
        for path in system_fonts:
            if os.path.exists(path):
                return path
       
        # 폰트 다운로드 (자동 시도)
        try:
            font_url = "https://github.com/google/fonts/raw/main/ofl/notosanskr/NotoSansKR%5Bwght%5D.ttf"
            urllib.request.urlretrieve(font_url, font_file)
            return font_file
        except:
            return None
   
    def check_connection(self):
        """ESP32 연결 확인"""
        try:
            response = requests.get(f"{self.base_url}/status", timeout=3)
            return response.status_code == 200
        except:
            return False
   
    def create_text_image(self, text, size=(272, 112), font_size=24):
        """텍스트를 이미지로 변환 (흰색 배경, 검은색 글씨, 중앙 정렬)"""
        # 이미지 생성 (흰색 배경)
        img = Image.new('RGB', size, color='white')
        draw = ImageDraw.Draw(img)
       
        # 폰트 설정
        font = None
        if self.font_path:
            try:
                font = ImageFont.truetype(self.font_path, font_size)
            except:
                pass
       
        if font is None:
            font = ImageFont.load_default()
       
        # 텍스트 중앙 정렬
        bbox = draw.textbbox((0, 0), text, font=font)
        text_width = bbox[2] - bbox[0]
        text_height = bbox[3] - bbox[1]
       
        x = (size[0] - text_width) // 2
        y = (size[1] - text_height) // 2
       
        # 텍스트 그리기 (검은색)
        draw.text((x, y), text, fill='black', font=font)
       
        return img
   
    def image_to_binary(self, img, threshold=128, invert=False):
        """이미지를 E-ink용 바이너리 데이터로 변환
       
        Args:
            img: PIL Image 객체
            threshold: 흑백 변환 임계값 (0-255)
            invert: True이면 색상 반전 (검은색↔흰색)
        """
        # 그레이스케일로 변환
        img = img.convert('L')
       
        # NumPy 배열로 변환
        img_array = np.array(img)
       
        if invert:
            # 색상 반전: 검은색 글씨 → 흰색 비트, 흰색 배경 → 검은색 비트
            binary_array = (img_array <= threshold).astype(np.uint8)
        else:
            # 정상: 흰색=1, 검은색=0
            binary_array = (img_array > threshold).astype(np.uint8)
       
        # 바이트 패킹
        height, width = binary_array.shape
        packed_data = []
       
        for y in range(height):
            for x in range(0, width, 8):
                byte = 0
                for bit in range(8):
                    if x + bit < width:
                        if binary_array[y, x + bit]:
                            byte |= (1 << (7 - bit))
                packed_data.append(byte)
       
        return bytes(packed_data)
   
    def upload_image(self, binary_data, image_type="txt"):
        """바이너리 데이터를 ESP32로 업로드 (urllib 사용)"""
        try:
            # multipart 데이터를 수동으로 생성
            boundary = '----WebKitFormBoundary' + ''.join(
                chr(ord('a') + i % 26) for i in range(16)
            )
           
            # multipart body 생성
            body_parts = []
           
            # file 파트
            body_parts.append(f'--{boundary}\r\n'.encode())
            body_parts.append(f'Content-Disposition: form-data; name="file"; filename="{image_type}.bin"\r\n'.encode())
            body_parts.append(b'Content-Type: application/octet-stream\r\n\r\n')
            body_parts.append(binary_data)
            body_parts.append(b'\r\n')
           
            # type 파트
            body_parts.append(f'--{boundary}\r\n'.encode())
            body_parts.append(b'Content-Disposition: form-data; name="type"\r\n\r\n')
            body_parts.append(image_type.encode())
            body_parts.append(b'\r\n')
           
            # 종료
            body_parts.append(f'--{boundary}--\r\n'.encode())
           
            # 전체 body
            body = b''.join(body_parts)
           
            # urllib.request 사용 (requests 버그 우회)
            req = urllib.request.Request(
                f"{self.base_url}/upload",
                data=body,
                headers={
                    'Content-Type': f'multipart/form-data; boundary={boundary}',
                    'Content-Length': str(len(body))
                }
            )
           
            with urllib.request.urlopen(req, timeout=30) as response:
                return response.status == 200
           
        except Exception as e:
            print(f"✗ 업로드 오류: {e}")
            return False
   
    def send_text(self, text, image_type="full", font_size=48, save_debug=False, invert=False):
        """텍스트를 이미지로 변환하여 전송"""
        # 타입에 따라 크기 결정
        if image_type == "txt":
            size = self.txt_size
        elif image_type == "pre":
            size = self.pre_size
        elif image_type == "full":
            size = self.full_size
        else:
            size = (400, 300)
       
        print(f"이미지 생성 중...")
        img = self.create_text_image(text, size, font_size)
       
        # 디버그용 이미지 저장
        if save_debug:
            debug_path = f"debug_{image_type}.png"
            img.save(debug_path)
            print(f"  디버그: {debug_path}")
       
        print(f"ESP32로 전송 중...")
        binary_data = self.image_to_binary(img, invert=invert)
        success = self.upload_image(binary_data, image_type)
       
        return success


def main():
    """메인 실행 함수 - 자동 모드"""
    print("=" * 50)
    print("ESP32 E-ink Display")
    print("=" * 50)
   
    # 기본 설정
    DEFAULT_IP = "192.168.45.74"  # 여기에 ESP32 IP 주소를 설정하세요
    DEFAULT_TEXT = "안녕하세요!"
    DEFAULT_INVERT = True  # True: 검은배경+흰글씨 (E-ink에 맞게 설정됨)
   
    # 명령줄 인자: IP와 텍스트만 입력
    # 사용법: python esp32_auto_uploader.py 192.168.45.74 "안녕하세요"
    esp_ip = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_IP
    text = sys.argv[2] if len(sys.argv) > 2 else DEFAULT_TEXT
   
    # 고정 설정
    image_type = "full"
    font_size = 48
    invert = DEFAULT_INVERT
   
    print(f"IP: {esp_ip}")
    print(f"텍스트: {text}")
    print("-" * 50)
   
    # 업로더 생성
    uploader = AutoESP32Uploader(esp_ip)
   
    print()
    # 연결 확인
    if not uploader.check_connection():
        print(f"✗ 연결 실패: {esp_ip}")
        print("  ESP32 IP 주소를 확인하세요")
        return 1
   
    print("✓ 연결 성공")
    print()
   
    # 텍스트 전송
    success = uploader.send_text(text, image_type, font_size, invert=invert)
   
    print()
    if success:
        print("✓ 전송 완료!")
        return 0
    else:
        print("✗ 전송 실패")
        return 1


if __name__ == "__main__":
    """
    사용법:
        python esp32_auto_uploader.py
        python esp32_auto_uploader.py 192.168.45.74
        python esp32_auto_uploader.py 192.168.45.74 "환영합니다!"
    """
   
    try:
        exit_code = main()
        sys.exit(exit_code)
    except KeyboardInterrupt:
        print("\n종료됨")
        sys.exit(1)
    except Exception as e:
        print(f"\n✗ 오류: {e}")
        sys.exit(1)





참고


https://github.com/Elecrow-RD/CrowPanel-ESP32-4.2-E-paper-HMI-Display-with-400-300/tree/master 


https://www.elecrow.com/wiki/CrowPanel_ESP32_E-paper_4.2-inch_HMI_Display.html 


댓글 쓰기

0 댓글

댓글 쓰기 (0)