Components and supplies
Lithium Ion Battery - 400mAh
micro switch
400x300, 4.2inch E-Ink display module
Tools and machines
Soldering kit
Apps and platforms
Arduino IDE
Project description
Code
Code
cpp
...
1/* ESP32 Info Display using an EPD 4.2" Display 2 #################################################################################################################################### 3 This software, the ideas and concepts is Copyright (c) Mirko Pavleski 2025. All rights to this software are reserved. 4 5 Any redistribution or reproduction of any part or all of the contents in any form is prohibited other than the following: 6 1. You may print or download to a local hard disk extracts for your personal and non-commercial use only. 7 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. 8 3. You may not, except with my express written permission, distribute or commercially exploit the content. 9 4. You may not transmit it or store it in any other website or other form of electronic retrieval system for commercial purposes. 10 11 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 12 OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 13 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 14 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15*/ 16 17#include <WiFi.h> 18#include <WebServer.h> 19#include <ArduinoJson.h> 20#include "GxEPD2_BW.h" 21 22// Network credentials 23const char* ssid = "********"; 24const char* password = "********"; 25 26// Pin definitions 27#define PWR 7 28#define BUSY 48 29#define RES 47 30#define DC 46 31#define CS 45 32 33// Web server on port 80 34WebServer server(80); 35 36// E-paper display initialization 37#include <Fonts/FreeMonoBold24pt7b.h> 38#include <Fonts/FreeMonoBold18pt7b.h> 39#include <Fonts/FreeMonoBold12pt7b.h> 40GxEPD2_BW<GxEPD2_420_GYE042A87, GxEPD2_420_GYE042A87::HEIGHT> epd(GxEPD2_420_GYE042A87(CS, DC, RES, BUSY)); 41 42// Display settings structure 43struct Row { 44 String text; 45 int fontSize; 46 Row() : text(""), fontSize(18) {} 47}; 48 49struct DisplaySettings { 50 static const int MAX_ROWS = 8; 51 Row rows[MAX_ROWS]; 52 bool border; 53 bool invertColors; // New field for color inversion 54 DisplaySettings() : border(true), invertColors(false) {} 55}; 56 57DisplaySettings currentSettings; 58 59const char index_html[] PROGMEM = R"rawliteral( 60<!DOCTYPE HTML> 61<html> 62<head> 63 <title>Info Display Control</title> 64 <meta name="viewport" content="width=device-width, initial-scale=1"> 65 <style> 66 body { font-family: Arial; margin: 20px; background-color: #f0f0f0; } 67 .content { max-width: 800px; margin: auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); } 68 .form-group { margin-bottom: 15px; } 69 label { display: block; margin-bottom: 5px; font-weight: bold; } 70 input[type=text] { 71 width: calc(100% - 180px); 72 padding: 8px; 73 margin-bottom: 0; 74 border: 1px solid #ddd; 75 border-radius: 4px; 76 } 77 select { 78 width: 100px; 79 padding: 8px; 80 border: 1px solid #ddd; 81 border-radius: 4px; 82 margin: 0 5px; 83 } 84 button { background-color: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; } 85 button:hover { background-color: #45a049; } 86 87 .preview-container { 88 width: 400px; 89 height: 300px; 90 margin: 20px auto; 91 position: relative; 92 } 93 .preview { 94 width: 400px; 95 height: 300px; 96 position: relative; 97 border: 1px solid #000; 98 background: white; 99 overflow: hidden; 100 transition: all 0.3s ease; 101 } 102 .preview.inverted { 103 background: black; 104 color: white; 105 } 106 .preview-row { 107 position: absolute; 108 left: 10px; 109 right: 10px; 110 white-space: nowrap; 111 overflow: hidden; 112 font-family: monospace; 113 } 114 .error { color: red; display: none; margin-top: 5px; } 115 .row-container { margin-bottom: 10px; } 116 .row-group { 117 display: grid; 118 grid-template-columns: auto 110px 60px; 119 gap: 5px; 120 margin-bottom: 5px; 121 align-items: center; 122 } 123 .del-btn { 124 background-color: #ff4444; 125 padding: 8px; 126 font-size: 12px; 127 height: 35px; 128 margin: 0; 129 width: 100%; 130 } 131 .color-options { 132 display: flex; 133 gap: 10px; 134 margin-bottom: 15px; 135 } 136 .color-btn { 137 padding: 10px 20px; 138 border: 2px solid #ddd; 139 border-radius: 4px; 140 cursor: pointer; 141 transition: all 0.3s ease; 142 } 143 .color-btn.normal { 144 background: white; 145 color: black; 146 } 147 .color-btn.inverted { 148 background: black; 149 color: white; 150 } 151 .color-btn.selected { 152 border-color: #4CAF50; 153 } 154 </style> 155 <script> 156 const rowHeights = { 157 12: 32, 158 18: 40, 159 24: 48 160 }; 161 162 function calculateRowPositions() { 163 let positions = []; 164 let currentY = 10; 165 166 for (let i = 0; i < 8; i++) { 167 const fontSize = parseInt(document.getElementById(`fontSize${i}`).value); 168 const height = rowHeights[fontSize]; 169 positions.push({ 170 y: currentY, 171 height: height 172 }); 173 currentY += height; 174 } 175 return positions; 176 } 177 178 function updatePreview() { 179 const preview = document.querySelector('.preview'); 180 preview.innerHTML = ''; 181 182 const positions = calculateRowPositions(); 183 184 for (let i = 0; i < 8; i++) { 185 const text = document.getElementById(`row${i}`).value; 186 const fontSize = document.getElementById(`fontSize${i}`).value; 187 if (text) { 188 const rowDiv = document.createElement('div'); 189 rowDiv.className = 'preview-row'; 190 191 // Create a span with double character width 192 const textSpan = document.createElement('span'); 193 194 // Use a non-breaking space for visual representation 195 textSpan.innerHTML = text.replace(/ /g, ' '); 196 textSpan.style.letterSpacing = `${fontSize * 0.65}px`; // Adjust letter spacing 197 198 rowDiv.appendChild(textSpan); 199 rowDiv.style.top = `${positions[i].y}px`; 200 rowDiv.style.fontSize = `${fontSize}px`; 201 preview.appendChild(rowDiv); 202 } 203 } 204} 205 206 207 function toggleColors(inverted) { 208 const preview = document.querySelector('.preview'); 209 preview.className = inverted ? 'preview inverted' : 'preview'; 210 211 // Update button styles 212 document.querySelector('.color-btn.normal').className = 'color-btn normal' + (!inverted ? ' selected' : ''); 213 document.querySelector('.color-btn.inverted').className = 'color-btn inverted' + (inverted ? ' selected' : ''); 214 } 215 216 function clearRow(index) { 217 document.getElementById(`row${index}`).value = ''; 218 updatePreview(); 219 } 220 221 function submitForm() { 222 const rows = []; 223 for (let i = 0; i < 8; i++) { 224 rows.push({ 225 text: document.getElementById(`row${i}`).value, 226 fontSize: parseInt(document.getElementById(`fontSize${i}`).value) 227 }); 228 } 229 230 const formData = { 231 rows: rows, 232 border: document.getElementById('border').checked, 233 invertColors: document.querySelector('.preview').classList.contains('inverted') 234 }; 235 236 fetch('/update', { 237 method: 'POST', 238 headers: { 239 'Content-Type': 'application/json', 240 }, 241 body: JSON.stringify(formData) 242 }).then(response => { 243 if (response.ok) { 244 alert('Display updated successfully!'); 245 } 246 }); 247 return false; 248 } 249 </script> 250</head> 251<body> 252 <div class="content"> 253 <h1>Info Display Control</h1> 254 <form onsubmit="return submitForm()"> 255 <div class="form-group"> 256 <label>Display Colors:</label> 257 <div class="color-options"> 258 <div class="color-btn normal selected" onclick="toggleColors(false)">Black on White</div> 259 <div class="color-btn inverted" onclick="toggleColors(true)">White on Black</div> 260 </div> 261 </div> 262 263 <div class="form-group"> 264 <label>Display Rows:</label> 265 <div class="row-container"> 266 %row_inputs% 267 </div> 268 </div> 269 270 <div class="form-group"> 271 <label> 272 <input type="checkbox" id="border" name="border" checked> 273 Show Border 274 </label> 275 </div> 276 277 <div class="preview-container"> 278 <div class="preview"></div> 279 </div> 280 <button type="submit">Update Display</button> 281 </form> 282 </div> 283 <script> 284 updatePreview(); 285 </script> 286</body> 287</html> 288)rawliteral"; 289 290void epdPower(int state) { 291 pinMode(PWR, OUTPUT); 292 digitalWrite(PWR, state); 293} 294 295void epdInit() { 296 epd.init(115200, true, 50, false); 297 epd.setRotation(0); 298 epd.setTextColor(currentSettings.invertColors ? GxEPD_WHITE : GxEPD_BLACK); 299 epd.setFullWindow(); 300} 301 302void setFontSize(int size) { 303 switch (size) { 304 case 12: 305 epd.setFont(&FreeMonoBold12pt7b); 306 break; 307 case 18: 308 epd.setFont(&FreeMonoBold18pt7b); 309 break; 310 case 24: 311 epd.setFont(&FreeMonoBold24pt7b); 312 break; 313 } 314} 315 316int getRowHeight(int fontSize) { 317 switch (fontSize) { 318 case 12: return 32; 319 case 18: return 40; 320 case 24: return 48; 321 default: return 40; 322 } 323} 324 325void updateDisplay() { 326 epdPower(HIGH); 327 epdInit(); 328 329 // Set background color based on inversion setting 330 epd.fillScreen(currentSettings.invertColors ? GxEPD_BLACK : GxEPD_WHITE); 331 332 if (currentSettings.border) { 333 epd.drawRect(0, 0, 400, 300, currentSettings.invertColors ? GxEPD_WHITE : GxEPD_BLACK); 334 } 335 336 int yPos = getRowHeight(24); 337 int margin = 10; 338 339 for (int i = 0; i < currentSettings.MAX_ROWS; i++) { 340 if (currentSettings.rows[i].text.length() > 0) { 341 setFontSize(currentSettings.rows[i].fontSize); 342 epd.setCursor(margin, yPos); 343 epd.print(currentSettings.rows[i].text); 344 yPos += getRowHeight(currentSettings.rows[i].fontSize); 345 } 346 } 347 348 epd.display(); 349 epd.hibernate(); 350 epdPower(LOW); 351} 352 353// Rest of the code remains the same until handleUpdate() 354 355 356 357String getRowInputsHTML() { 358 String html = ""; 359 for (int i = 0; i < currentSettings.MAX_ROWS; i++) { 360 html += "<div class='row-group'>"; 361 362 // Text input 363 html += "<input type='text' id='row" + String(i) + "' maxlength='30' "; 364 html += "placeholder='Row " + String(i + 1) + "' "; 365 html += "value='" + currentSettings.rows[i].text + "' "; 366 html += "onkeyup='updatePreview()'>"; 367 368 // Font size selector 369 html += "<select id='fontSize" + String(i) + "' onchange='updatePreview()'>"; 370 html += "<option value='12'" + String(currentSettings.rows[i].fontSize == 12 ? " selected" : "") + ">Small (12pt)</option>"; 371 html += "<option value='18'" + String(currentSettings.rows[i].fontSize == 18 ? " selected" : "") + ">Medium (18pt)</option>"; 372 html += "<option value='24'" + String(currentSettings.rows[i].fontSize == 24 ? " selected" : "") + ">Large (24pt)</option>"; 373 html += "</select>"; 374 375 // DEL button 376 html += "<button type='button' class='del-btn' onclick='clearRow(" + String(i) + ")'>DEL</button>"; 377 html += "</div>"; 378 } 379 return html; 380} 381 382String getHTML() { 383 String html = String(index_html); 384 html.replace("%row_inputs%", getRowInputsHTML()); 385 return html; 386} 387 388void handleRoot() { 389 server.send(200, "text/html", getHTML()); 390} 391 392void handleUpdate() { 393 if (server.hasArg("plain")) { 394 StaticJsonDocument<1024> doc; 395 DeserializationError error = deserializeJson(doc, server.arg("plain")); 396 397 if (!error) { 398 JsonArray rows = doc["rows"]; 399 int i = 0; 400 for (JsonVariant row : rows) { 401 currentSettings.rows[i].text = row["text"].as<String>(); 402 currentSettings.rows[i].fontSize = row["fontSize"].as<int>(); 403 i++; 404 } 405 406 currentSettings.border = doc["border"].as<bool>(); 407 currentSettings.invertColors = doc["invertColors"].as<bool>(); 408 409 updateDisplay(); 410 server.send(200, "text/plain", "OK"); 411 } else { 412 server.send(400, "text/plain", "Invalid JSON"); 413 } 414 } 415} 416 417void setup() { 418 Serial.begin(115200); 419 420 421 epdPower(HIGH); 422 epdInit(); 423 epd.fillScreen(GxEPD_WHITE); 424 epd.drawRect(0, 0, 400, 300, GxEPD_BLACK); 425 epd.setFont(&FreeMonoBold24pt7b); 426 epd.setCursor(90, 70); 427 epd.print("Wireless"); 428 epd.setCursor(35, 150); 429 epd.print("Info-Display"); 430 epd.setFont(&FreeMonoBold18pt7b); 431 epd.setCursor(100, 230); 432 epd.print("by mircemk"); 433 epd.display(); 434 delay(2000); 435 epd.fillScreen(GxEPD_WHITE); 436 epd.hibernate(); 437 epdPower(LOW); 438 439 440 // Connect to Wi-Fi 441 WiFi.begin(ssid, password); 442 while (WiFi.status() != WL_CONNECTED) { 443 delay(1000); 444 Serial.println("Connecting to WiFi..."); 445 } 446 Serial.println("Connected to WiFi"); 447 Serial.print("IP Address: "); 448 Serial.println(WiFi.localIP()); 449 450 // Setup web server routes 451 server.on("/", HTTP_GET, handleRoot); 452 server.on("/update", HTTP_POST, handleUpdate); 453 server.begin(); 454 455 // Initial display update with default settings 456 currentSettings.invertColors = false; // Ensure default color scheme 457 updateDisplay(); 458} 459 460void loop() { 461 server.handleClient(); 462 delay(10); // Small delay to prevent watchdog issues 463}
Documentation
Schematic
...
Schemaric.jpg
Comments
Only logged in users can leave comments