ELECROW CrowPanel ESP32 4.2” E-paper Wi-Fi Info-Dispaly Project

 The device presented in this project uses an Elecrow E-Paper display module with a built-in ESP32S microcontroller and additional components enclosed into a suitable housing, and requires almost no hardware intervention, so we only need to install the sketch and get a fully functional final product.

  An e-paper display (also known as an electronic paper display or E Ink display) is a type of screen that mimics the appearance of regular ink on paper. It's typically used in devices like e-readers, smartwatches, and digital signage.
 

 E-paper displays are known for being energy-efficient because they only consume power when the image or text changes, and they can be easily read in bright sunlight, unlike traditional backlit screens. This time I will describe and do a brief test specifically on this ELECROW CrowPanel ESP32 4.2” E-paper HMI Display, Driven By SPI Interface. 
 

  First, let me introduce you to the basic features of this device. In fact, this is an e-paper display with built-in ESP32-S3 chip as the main control ensures powerful performance and fast and stable data transmission through the SPI interface. The display has a high resolution of 400x300 pixels and supports two colors: white and black. If the back plate is removed, it will be seen that the device is equipped with multiple interface and button designs, including TF card slot, BAT interface, UART0 interface, 20 pin GPIO interface, back button, home button and rotary switch switch, which is convenient for users to develop and operate.

 The white acrylic shell is not only beautiful but also protects the screen. Immediately after switching on, a demo program appears on the display, which shows the features of the device. Then the home screen appears, in which we can select the three options offered using the side buttons. 

The first is to display text, then an example image, and a lamp control software scenario. If at any time we turn off the power, the image that is currently displayed remains on the screen. This is actually the main feature of this type of display. It only consumes power when refreshing.
      First, let me briefly explain the procedure for installing some code using the Arduino IDE, which is actually the simplest way. Demo codes for this display can be found on the Elecrow website , as well as on GitHub . As an example, I will now install the code for a Wi-Fi weather station that draws data from the Openweathermaps website. For this purpose, we first need to have ESP32 support installed on the Arduino IDE, which is described in several of my previous videos. Then we go to Tools and select ESP32S3 Dev Module. Next, in the menu on the board, we need to make a few settings.

Now we can start the installation. We open the folder with the sketch and activate the .ino file. This will open the entire code structure, we just need to enter the WiFi network data and the openvedarmap API key and the country code. Then we select the appropriate COM port and press upload. In a short time, the upload will be completed and the weather station will appear on the screen. You can also find more examples on Elekrow's YouTube channel, but they all contain full support contained within the code itself, and does not use external libraries. 
  GxEPD2 is the most famous library for E-ink displays but unfortunately it does not have full support for this type of displays. On the makerguides page is described a little tweak with which we can get the GxEPD2 library library working on the CrowPanel 4.2-inch E-Paper and all credits go to them. 
  Now that I already had an advanced library at my disposal, I came up with the idea of ​​creating a practical and very useful project. It is an information display that can be used in countless places and cases and has a universal purpose. 

 

The display itself visually resembles a whiteboard intended for writing various temporary messages and informations. So the basic requirement for good functionality of such a device is to find an easy way to enter and delete messages. Considering that the display module contains an ESP32 microcontroller which, by the way, has WiFi and Bluetooth support, I managed to create an Arduino code with a WebServer in the local WiFi network, creating an easily manageable web interface through which the Info-Display is controlled. So we can write or delete messages via any computer or smartphone in the local network. 

As for installing the code, the procedure is standard, and the only requirement is to have the GxEPD2 library installed, which is provided. Once the code installation is complete, we need to turn on the serial monitor and reset the device, which will give us its local IP address on the serial monitor. We will need this address later.

 

  Let's see how this device works in real conditions. When turned on, a text with the device name appears on the screen, after which a blank white screen appears in the style of a whiteboard. At this point, the board is connected to the Wi-Fi network and ready to print text on it. As I mentioned earlier, we enter the text via the web interface, which we access by entering the given IP address from the serial monitor, in any web browser. We enter the received address into the browser and a clear interface appears on the screen through which we enter text on the display. 

The text that needs to be displayed is entered in 8 fields. In each row individually, we can choose one of three offered font sizes, and next to it is a button for deleting the content in the row. Below is a field in which we can visually see how the text written on the board will look. At the bottom is a green Update button through which we transmit the content to the board via the Wi-Fi network. On the two buttons at the top of the screen, we can choose white text on a black background, or black text on a white background. 

With combinations of characters, letters and numbers, we can also form a frame, various figures and similar content. In the following, I will present just a few examples of how to fill in the board.

  And finally a short conclusion. Electronic paper displays can clearly display images/text under lighting or natural light, without backlight, with lower power consumption and longer life due to partial refresh. The visibility angle is almost 180 degrees. The device presented in this project uses an Elecrow E-Paper display module with a built-in ESP32S microcontroller and additional components enclosed into a suitable housing, and requires almost no hardware intervention, so we only need to install the sketch and get a fully functional final product. 

 


 

CODE
/* ESP32 Info Display using an EPD 4.2" Display
  ####################################################################################################################################
  This software, the ideas and concepts is Copyright (c) Mirko Pavleski 2025. All rights to this software are reserved.

  Any redistribution or reproduction of any part or all of the contents in any form is prohibited other than the following:
  1. You may print or download to a local hard disk extracts for your personal and non-commercial use only.
  2. You may copy the content to individual third parties for their personal use, but only if you acknowledge the author Mirko Pavleski as the source of the material.
  3. You may not, except with my express written permission, distribute or commercially exploit the content.
  4. You may not transmit it or store it in any other website or other form of electronic retrieval system for commercial purposes.

  THE SOFTWARE IS PROVIDED "AS IS" FOR PRIVATE USE ONLY, IT IS NOT FOR COMMERCIAL USE IN WHOLE OR PART OR CONCEPT. FOR PERSONAL USE IT IS SUPPLIED WITHOUT WARRANTY
  OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
  IN NO EVENT SHALL THE AUTHOR OR COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

#include <WiFi.h>
#include <WebServer.h>
#include <ArduinoJson.h>
#include "GxEPD2_BW.h"

// Network credentials
const char* ssid = "********";
const char* password = "********";

// Pin definitions
#define PWR 7
#define BUSY 48
#define RES 47
#define DC 46
#define CS 45

// Web server on port 80
WebServer server(80);

// E-paper display initialization
#include <Fonts/FreeMonoBold24pt7b.h>
#include <Fonts/FreeMonoBold18pt7b.h>
#include <Fonts/FreeMonoBold12pt7b.h>
GxEPD2_BW<GxEPD2_420_GYE042A87, GxEPD2_420_GYE042A87::HEIGHT> epd(GxEPD2_420_GYE042A87(CS, DC, RES, BUSY));

// Display settings structure
struct Row {
  String text;
  int fontSize;
  Row() : text(""), fontSize(18) {}
};

struct DisplaySettings {
  static const int MAX_ROWS = 8;
  Row rows[MAX_ROWS];
  bool border;
  bool invertColors;  // New field for color inversion
  DisplaySettings() : border(true), invertColors(false) {}
};

DisplaySettings currentSettings;

const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML>
<html>
<head>
  <title>Info Display Control</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    body { font-family: Arial; margin: 20px; background-color: #f0f0f0; }
    .content { max-width: 800px; margin: auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
    .form-group { margin-bottom: 15px; }
    label { display: block; margin-bottom: 5px; font-weight: bold; }
    input[type=text] { 
      width: calc(100% - 180px);
      padding: 8px;
      margin-bottom: 0;
      border: 1px solid #ddd;
      border-radius: 4px;
    }
    select { 
      width: 100px;
      padding: 8px;
      border: 1px solid #ddd;
      border-radius: 4px;
      margin: 0 5px;
    }
    button { background-color: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; }
    button:hover { background-color: #45a049; }
    
    .preview-container {
      width: 400px;
      height: 300px;
      margin: 20px auto;
      position: relative;
    }
    .preview {
      width: 400px;
      height: 300px;
      position: relative;
      border: 1px solid #000;
      background: white;
      overflow: hidden;
      transition: all 0.3s ease;
    }
    .preview.inverted {
      background: black;
      color: white;
    }
    .preview-row {
      position: absolute;
      left: 10px;
      right: 10px;
      white-space: nowrap;
      overflow: hidden;
      font-family: monospace;
    }
    .error { color: red; display: none; margin-top: 5px; }
    .row-container { margin-bottom: 10px; }
    .row-group {
      display: grid;
      grid-template-columns: auto 110px 60px;
      gap: 5px;
      margin-bottom: 5px;
      align-items: center;
    }
    .del-btn {
      background-color: #ff4444;
      padding: 8px;
      font-size: 12px;
      height: 35px;
      margin: 0;
      width: 100%;
    }
    .color-options {
      display: flex;
      gap: 10px;
      margin-bottom: 15px;
    }
    .color-btn {
      padding: 10px 20px;
      border: 2px solid #ddd;
      border-radius: 4px;
      cursor: pointer;
      transition: all 0.3s ease;
    }
    .color-btn.normal {
      background: white;
      color: black;
    }
    .color-btn.inverted {
      background: black;
      color: white;
    }
    .color-btn.selected {
      border-color: #4CAF50;
    }
  </style>
  <script>
    const rowHeights = {
      12: 32,
      18: 40,
      24: 48
    };

    function calculateRowPositions() {
      let positions = [];
      let currentY = 10;
      
      for (let i = 0; i < 8; i++) {
        const fontSize = parseInt(document.getElementById(`fontSize${i}`).value);
        const height = rowHeights[fontSize];
        positions.push({
          y: currentY,
          height: height
        });
        currentY += height;
      }
      return positions;
    }

 function updatePreview() {
  const preview = document.querySelector('.preview');
  preview.innerHTML = '';
  
  const positions = calculateRowPositions();
  
  for (let i = 0; i < 8; i++) {
    const text = document.getElementById(`row${i}`).value;
    const fontSize = document.getElementById(`fontSize${i}`).value;
    if (text) {
      const rowDiv = document.createElement('div');
      rowDiv.className = 'preview-row';
      
      // Create a span with double character width
      const textSpan = document.createElement('span');
      
      // Use a non-breaking space for visual representation
      textSpan.innerHTML = text.replace(/ /g, '&nbsp;');
      textSpan.style.letterSpacing = `${fontSize * 0.65}px`; // Adjust letter spacing
      
      rowDiv.appendChild(textSpan);
      rowDiv.style.top = `${positions[i].y}px`;
      rowDiv.style.fontSize = `${fontSize}px`;
      preview.appendChild(rowDiv);
    }
  }
}

    
    function toggleColors(inverted) {
      const preview = document.querySelector('.preview');
      preview.className = inverted ? 'preview inverted' : 'preview';
      
      // Update button styles
      document.querySelector('.color-btn.normal').className = 'color-btn normal' + (!inverted ? ' selected' : '');
      document.querySelector('.color-btn.inverted').className = 'color-btn inverted' + (inverted ? ' selected' : '');
    }
    
    function clearRow(index) {
      document.getElementById(`row${index}`).value = '';
      updatePreview();
    }
    
    function submitForm() {
      const rows = [];
      for (let i = 0; i < 8; i++) {
        rows.push({
          text: document.getElementById(`row${i}`).value,
          fontSize: parseInt(document.getElementById(`fontSize${i}`).value)
        });
      }
      
      const formData = {
        rows: rows,
        border: document.getElementById('border').checked,
        invertColors: document.querySelector('.preview').classList.contains('inverted')
      };
      
      fetch('/update', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(formData)
      }).then(response => {
        if (response.ok) {
          alert('Display updated successfully!');
        }
      });
      return false;
    }
  </script>
</head>
<body>
  <div class="content">
    <h1>Info Display Control</h1>
    <form onsubmit="return submitForm()">
      <div class="form-group">
        <label>Display Colors:</label>
        <div class="color-options">
          <div class="color-btn normal selected" onclick="toggleColors(false)">Black on White</div>
          <div class="color-btn inverted" onclick="toggleColors(true)">White on Black</div>
        </div>
      </div>
      
      <div class="form-group">
        <label>Display Rows:</label>
        <div class="row-container">
          %row_inputs%
        </div>
      </div>
      
      <div class="form-group">
        <label>
          <input type="checkbox" id="border" name="border" checked>
          Show Border
        </label>
      </div>
      
      <div class="preview-container">
        <div class="preview"></div>
      </div>
      <button type="submit">Update Display</button>
    </form>
  </div>
  <script>
    updatePreview();
  </script>
</body>
</html>
)rawliteral";

void epdPower(int state) {
  pinMode(PWR, OUTPUT);
  digitalWrite(PWR, state);
}

void epdInit() {
  epd.init(115200, true, 50, false);
  epd.setRotation(0);
  epd.setTextColor(currentSettings.invertColors ? GxEPD_WHITE : GxEPD_BLACK);
  epd.setFullWindow();
}

void setFontSize(int size) {
  switch (size) {
    case 12:
      epd.setFont(&FreeMonoBold12pt7b);
      break;
    case 18:
      epd.setFont(&FreeMonoBold18pt7b);
      break;
    case 24:
      epd.setFont(&FreeMonoBold24pt7b);
      break;
  }
}

int getRowHeight(int fontSize) {
  switch (fontSize) {
    case 12: return 32;
    case 18: return 40;
    case 24: return 48;
    default: return 40;
  }
}

void updateDisplay() {
  epdPower(HIGH);
  epdInit();
  
  // Set background color based on inversion setting
  epd.fillScreen(currentSettings.invertColors ? GxEPD_BLACK : GxEPD_WHITE);
  
  if (currentSettings.border) {
    epd.drawRect(0, 0, 400, 300, currentSettings.invertColors ? GxEPD_WHITE : GxEPD_BLACK);
  }
  
  int yPos = getRowHeight(24);
  int margin = 10;
  
  for (int i = 0; i < currentSettings.MAX_ROWS; i++) {
    if (currentSettings.rows[i].text.length() > 0) {
      setFontSize(currentSettings.rows[i].fontSize);
      epd.setCursor(margin, yPos);
      epd.print(currentSettings.rows[i].text);
      yPos += getRowHeight(currentSettings.rows[i].fontSize);
    }
  }
  
  epd.display();
  epd.hibernate();
  epdPower(LOW);
}

// Rest of the code remains the same until handleUpdate()



String getRowInputsHTML() {
  String html = "";
  for (int i = 0; i < currentSettings.MAX_ROWS; i++) {
    html += "<div class='row-group'>";
    
    // Text input
    html += "<input type='text' id='row" + String(i) + "' maxlength='30' ";
    html += "placeholder='Row " + String(i + 1) + "' ";
    html += "value='" + currentSettings.rows[i].text + "' ";
    html += "onkeyup='updatePreview()'>";
    
    // Font size selector
    html += "<select id='fontSize" + String(i) + "' onchange='updatePreview()'>";
    html += "<option value='12'" + String(currentSettings.rows[i].fontSize == 12 ? " selected" : "") + ">Small (12pt)</option>";
    html += "<option value='18'" + String(currentSettings.rows[i].fontSize == 18 ? " selected" : "") + ">Medium (18pt)</option>";
    html += "<option value='24'" + String(currentSettings.rows[i].fontSize == 24 ? " selected" : "") + ">Large (24pt)</option>";
    html += "</select>";
    
    // DEL button
    html += "<button type='button' class='del-btn' onclick='clearRow(" + String(i) + ")'>DEL</button>";
    html += "</div>";
  }
  return html;
}

String getHTML() {
  String html = String(index_html);
  html.replace("%row_inputs%", getRowInputsHTML());
  return html;
}

void handleRoot() {
  server.send(200, "text/html", getHTML());
}

void handleUpdate() {
  if (server.hasArg("plain")) {
    StaticJsonDocument<1024> doc;
    DeserializationError error = deserializeJson(doc, server.arg("plain"));
    
    if (!error) {
      JsonArray rows = doc["rows"];
      int i = 0;
      for (JsonVariant row : rows) {
        currentSettings.rows[i].text = row["text"].as<String>();
        currentSettings.rows[i].fontSize = row["fontSize"].as<int>();
        i++;
      }
      
      currentSettings.border = doc["border"].as<bool>();
      currentSettings.invertColors = doc["invertColors"].as<bool>();
      
      updateDisplay();
      server.send(200, "text/plain", "OK");
    } else {
      server.send(400, "text/plain", "Invalid JSON");
    }
  }
}

void setup() {
  Serial.begin(115200);


  epdPower(HIGH);
  epdInit();
  epd.fillScreen(GxEPD_WHITE);
  epd.drawRect(0, 0, 400, 300, GxEPD_BLACK);
  epd.setFont(&FreeMonoBold24pt7b);
  epd.setCursor(90, 70);
  epd.print("Wireless");
  epd.setCursor(35, 150);
  epd.print("Info-Display");
  epd.setFont(&FreeMonoBold18pt7b);
  epd.setCursor(100, 230);
  epd.print("by mircemk"); 
  epd.display();
  delay(2000);
  epd.fillScreen(GxEPD_WHITE);
  epd.hibernate(); 
  epdPower(LOW);
  
  
  // Connect to Wi-Fi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi...");
  }
  Serial.println("Connected to WiFi");
  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());
  
  // Setup web server routes
  server.on("/", HTTP_GET, handleRoot);
  server.on("/update", HTTP_POST, handleUpdate);
  server.begin();
  
  // Initial display update with default settings
  currentSettings.invertColors = false;  // Ensure default color scheme
  updateDisplay();
}

void loop() {
  server.handleClient();
  delay(10);  // Small delay to prevent watchdog issues
}
License
All Rights
Reserved
licensBg
0