Zobrazení textu na e-Paperu odeslaného z ESP32

27.12.2023 Arduino #esp32 #epaper #eink

I když displeje e-Paper nemusí být ideální pro každou aplikaci, jsou skvělou volbou, pokud potřebujete displej s nízkou spotřebou energie, který je viditelný za denního světla. Zde je uveden základní návod, jak odesílat text z ESP32, resp. z webového rozhraní umístěného na serveru ESP32 na e-Paper.


Displeje Electronic Paper, neboli e-Paper, jsou trochu jiná technologie. Jsou velmi podobné běžnému papíru, jsou čitelné v normálních a jasně osvětlených situacích, ale ne při slabém nebo tmavém osvětlení. Dobře fungují na jasném slunci a poskytují velmi široký pozorovací úhel.

Kromě viditelnosti je třeba u displejů e-Paper zvážit i další faktory.

  • Spotřebovávají velmi málo elektřiny.
  • Nemají příliš rychlé obnovovací frekvence.
  • Mají omezené barevné možnosti.
  • Jsou dražší než většina ostatních technologií.

I přes tyto nevýhody existují oblasti, kde jsou displeje e-Paper opravdu vhodné. Velmi běžnou aplikací jsou elektronické čtečky, jako je Amazon Kindle. Jejich nízká spotřeba energie a vzhled připomínající papír z nich činí ideální náhradu standardního papíru v knihách.

e-Paper také najde uplatnění ve velkých zobrazovacích panelech, jako jsou ty, které se používají na letištích nebo autobusových terminálech. Mohou být také dobrou volbou pro přenosné nástroje a „inteligentní“ cenovky.

Jak e-Paper funguje

Displeje e-Paper byly původně vyvinuty na počátku 70. let minulého století Nickem Sheridonem ve výzkumném středisku XEROX Palo Alto Research Center (PARC). Původní produkt se jmenoval „Gyricon“ a sestával z polyethylenových kuliček o průměru asi 100 mikrometrů.

Displeje e-Paper pracují na principu elektroforézy, pohybu elektricky nabitých molekul v elektrickém poli. Když je aplikován náboj, koule se pohybují směrem k elektrickému poli nebo od něj v závislosti na polaritě.

Tyto displeje využívají miliony e-Ink kapslí, každá z těchto kapslí obsahuje obě koule, které jsou buď kladně nebo záporně nabité. Dvě různé koule mají různé barvy, takže jejich uspořádání v kapsli e-Ink závisí na vnějších elektrických nábojích.

Změnou polarity elektrického pole kolem kapslí mohou být jejich vnitřní koule přitahovány nebo odpuzovány, aby se zobrazil buď černý nebo bílý povrch. Vícebarevné e-Ink displeje používají jiné barvy než černou.

Na displeji e-Paper jsou tyto kapsle e-Ink zavěšeny v kapalném polymeru mezi dvěma mřížkami elektrod, přičemž každá elektroda má šířku jednotlivého pixelu. Vrchní vrstva je průhledná.

Když jsou na elektrody aplikovány elektrické náboje, přičemž horní a spodní elektroda mají opačnou polaritu, kapsle e-Ink se zarovnají podle elektrického náboje. To má za následek změnu zobrazovacích pixelů, což vytváří vzor, ​​který vidíte shora.

Jako další bonus je nabití na kapslích zachováno po neomezenou dobu, přepólování musí nastat pouze pro změnu stávajícího displeje. Výsledkem je, že vzor je zachován po velmi dlouhou dobu, doslova roky nebo dokonce desetiletí.

Pokroky v technologii e-Paper umožnily plnobarevné displeje a také zlepšenou dobu přepínání. Tyto pokročilé displeje jsou mimo dosah většiny experimentátorů, alespoň prozatím. Ale v budoucnu byste mohli vidět, že e-Paper bude používán pro video displeje, takové, které vyžadují velmi malý proud a které mají velmi široký pozorovací úhel a které jsou použitelné za denního světla.

Pro experiment byl využit Good Display GDEY075T7 7.5" 800x480 ePaper displej Grayscale a jako MCU LaskaKit ESPink ESP32 e-Paper, který je určen k přímému zapojení displeje pomocí vestavěného konektoru. Takže se nemusí vše složitě propojovat a můžeme se soustředit na programový kód.

Programový kód

Kód je rozdělen do třech souborů:

  • main.cpp - jádro programu
  • spffis.h - soubor pro práci s interním uložištěm MCU. Do formátu JSON se ukládá naposledy odeslaný text z webové aplikace umístěné na serveru ESP32.
  • htmlui.h - html kód webového rozhraní aplikace.

Před programováním se musí ještě vytvořit konfigurační soubor pro ukládání textu do ESP32 s využitím uložiště SPFFIS.

main.cpp

#include "FS.h"
#include "SD.h"
#include "SPI.h"

#include "Arduino.h"
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <ESP32AnalogRead.h>
#include <math.h>

#include <SPIFFS.h>
#include <ArduinoJson.h>

#include <PubSubClient.h>

#include <Wire.h>
#include <htmlui.h>
#include <spffis.h>

#define FORMAT_SPIFFS_IF_FAILED true

#define ENABLE_GxEPD2_GFX 0

#include <GxEPD2_BW.h>
#include <GxEPD2_3C.h>
#include <GxEPD2_7C.h>
#include <Fonts/FreeMonoBold9pt7b.h>
#include "bitmaps/Bitmaps1304x984.h" // 12.48" b/w

#define SLEEP_SEC 15         // Measurement interval (seconds)

const char* ssid = "ePaper";
const char* password = "12345678";

AsyncWebServer server(80);

GxEPD2_BW<GxEPD2_750_T7, GxEPD2_750_T7::HEIGHT> display(GxEPD2_750_T7(/*CS=5*/ SS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // GDEY075T7 800x480, GDEW075T7, Waveshare 7.5"

void helloWorld(String data)
{
  display.setRotation(3);
  display.setFont(&FreeMonoBold9pt7b);
  if (display.epd2.WIDTH < 104) display.setFont(0);
  display.setTextColor(GxEPD_BLACK);
  int16_t tbx, tby; uint16_t tbw, tbh;
  display.getTextBounds(data, 0, 0, &tbx, &tby, &tbw, &tbh);
  // Centrování obsahu
  uint16_t x = ((display.width() - tbw) / 2) - tbx;
  uint16_t y = ((display.height() - tbh) / 2) - tby;
  display.setFullWindow();
  display.firstPage();
  do
  {
    display.fillScreen(GxEPD_WHITE);
    display.setCursor(x, y);
    display.print(data);
  }
  while (display.nextPage());

}

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

  // Server
  WiFi.softAP(ssid, password);

  IPAddress IP = WiFi.softAPIP();
  Serial.print("AP IP address: ");
  Serial.println(IP);

  if(!SPIFFS.begin(FORMAT_SPIFFS_IF_FAILED)){
    Serial.println("SPIFFS Mount Failed");
    return;
  }
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/html", index_html);
  });
 
  // Získání posledního textu z uložiště MCU
  server.on("/getdata", HTTP_GET, [] (AsyncWebServerRequest *request) {
    request->send(200, "text/plain", valConfig().c_str());
  });

  // Nastavení textu z webového rozhraní
  server.on("/setdata", HTTP_GET, [] (AsyncWebServerRequest *request) {
    int params = request->params();
    for(int i=0;i<params;i++){
      AsyncWebParameter* p = request->getParam(i);
      if (p->name() == "data" ) {
        data = p->value();
        helloWorld(data);
      }
    }
 
    saveConfig(fileConfig);
    request->send(200, "text/plain", "OK");
  });

  Serial.println();
  Serial.println("setup");
  delay(100);

  server.begin();
  Serial.println("HTTP server started");

  data = readConfig(fileConfig);

  // Zapnutí displeje
  pinMode(2, OUTPUT);
  digitalWrite(2, HIGH);   // zapnutí LED on
  Serial.println("Display power ON");
  delay(1000);   
  
  display.init(); // inicializace

  display.fillScreen(GxEPD_WHITE);
  delay(1000);
  // První aktualizace displeje
  helloWorld(data);
  delay(1000);
  
  display.powerOff();

  Serial.println("Setup done");

}


void loop()
{
}

spffis.h

String data="";
String fileConfig = "/config.json";
using namespace std;
vector<String> v;

String readFile(fs::FS &fs, String filename){
  Serial.println("readFile -> Reading file: " + filename);

  File file = fs.open(filename);
  if(!file || file.isDirectory()){
    Serial.println("readFile -> failed to open file for reading");
    return "";
  }

  String fileText = "";
  while(file.available()){
    fileText = file.readString();
  }

  file.close();
  return fileText;
}

String readConfig(String file_name) {
  String file_content = readFile(SPIFFS, file_name);

  int config_file_size = file_content.length();
  Serial.println("Config file size: " + String(config_file_size));

  if(config_file_size > 1024) {
    Serial.println("Config file too large");
  }
  StaticJsonDocument<1024> doc;
  auto error = deserializeJson(doc, file_content);
  if ( error ) { 
    Serial.println("Error interpreting config file");
  }

  String _data = doc["data"];

  data = _data;

  Serial.print("Read config:");   
  Serial.println(data);
  return data;
}

void writeFile(fs::FS &fs, String filename, String message){
  Serial.println("writeFile -> Writing file: " + filename);

  File file = fs.open(filename, FILE_WRITE);
  if(!file){
    Serial.println("writeFile -> failed to open file for writing");
    return;
  }
  if(file.print(message)){
    Serial.println("writeFile -> file written");
  } else {
    Serial.println("writeFile -> write failed");
  }
  file.close();
}

bool saveConfig(String file_name) {
  StaticJsonDocument<1024> doc;

  // write variables to JSON file
  doc["data"] = data;
  
  // write config file
  String tmp = "";
  serializeJson(doc, tmp);
  writeFile(SPIFFS, file_name, tmp);
  
  return true;
}

String valConfig(){
  data = readConfig(fileConfig);
  
  String result = String(data);
  Serial.print("Data send to config UI: ");
  Serial.println(result);
  return result;
}

htmlui.h

const char index_html[] PROGMEM = R"rawliteral(
 <!DOCTYPE HTML><html>
    <head>
      <title>ESP Web Server</title>
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <style>

            html {
                box-sizing: border-box;
                -ms-overflow-style: scrollbar;
            }

                *,
                *::before,
                *::after {
                box-sizing: inherit;
            }
            .container{
                width: 100%;
                padding-right: 15px;
                padding-left: 15px;
                margin-right: auto;
                margin-left: auto;
            }

            .row {
                display: -ms-flexbox;
                display: flex;
                -ms-flex-wrap: wrap;
                flex-wrap: wrap;
                margin-right: -15px;
                margin-left: -15px;
            }

            .no-gutters {
                margin-right: 0;
                margin-left: 0;
            }

            .no-gutters > .col,
            .no-gutters > [class*="col-"] {
                padding-right: 0;
                padding-left: 0;
            }

            .col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col,
            .col-auto {
                position: relative;
                width: 100%;
                padding-right: 15px;
                padding-left: 15px;
            }

            .col {
                -ms-flex-preferred-size: 0;
                flex-basis: 0;
                -ms-flex-positive: 1;
                flex-grow: 1;
                max-width: 100%;
            }
            .col-auto {-ms-flex: 0 0 auto; flex: 0 0 auto;width: auto;max-width: 100%;}
            .col-1 {-ms-flex: 0 0 8.333333%;flex: 0 0 8.333333%; max-width: 8.333333%;}
            .col-2 {-ms-flex: 0 0 16.666667%;flex: 0 0 16.666667%;max-width: 16.666667%;}
            .col-3 {-ms-flex: 0 0 25%;flex: 0 0 25%; max-width: 25%;}
            .col-4 { -ms-flex: 0 0 33.333333%;flex: 0 0 33.333333%; max-width: 33.333333%;}
            .col-5 {-ms-flex: 0 0 41.666667%;flex: 0 0 41.666667%;max-width: 41.666667%; }
            .col-6 {-ms-flex: 0 0 50%;flex: 0 0 50%; max-width: 50%;}
            .col-7 {-ms-flex: 0 0 58.333333%;flex: 0 0 58.333333%;max-width: 58.333333%;}
            .col-8 {-ms-flex: 0 0 66.666667%;flex: 0 0 66.666667%;max-width: 66.666667%;}
            .col-9 { -ms-flex: 0 0 75%;flex: 0 0 75%; max-width: 75%; }
            .col-10 {-ms-flex: 0 0 83.333333%;flex: 0 0 83.333333%;max-width: 83.333333%;}
            .col-11 {-ms-flex: 0 0 91.666667%;flex: 0 0 91.666667%;max-width: 91.666667%;}
            .col-12 {-ms-flex: 0 0 100%;flex: 0 0 100%; max-width: 100%; }

            .m-0 {margin: 0 !important;}
            .mt-0,.my-0 {margin-top: 0 !important; }
            .mr-0,.mx-0 {margin-right: 0 !important;}
            .mb-0,.my-0 {margin-bottom: 0 !important;}
            .ml-0,.mx-0 {margin-left: 0 !important;}
            .m-1 { margin: 0.25rem !important;}
            .mt-1, .my-1 { margin-top: 0.25rem !important; }
            .mr-1,.mx-1 { margin-right: 0.25rem !important;}
            .mb-1, .my-1 {margin-bottom: 0.25rem !important; }
            .ml-1,.mx-1 {margin-left: 0.25rem !important; }
            .m-2 {margin: 0.5rem !important;}
            .mt-2,.my-2 {margin-top: 0.5rem !important;}
            .mr-2,.mx-2 { margin-right: 0.5rem !important; }
            .mb-2,.my-2 {margin-bottom: 0.5rem !important;}
            .ml-2,.mx-2 {margin-left: 0.5rem !important;}
            .m-3 { margin: 1rem !important;}
            .mt-3,.my-3 {margin-top: 1rem !important;}
            .mr-3,.mx-3 {margin-right: 1rem !important;}
            .mb-3,.my-3 {margin-bottom: 1rem !important;}
            .ml-3, .mx-3 {margin-left: 1rem !important; }
            .m-4 { margin: 1.5rem !important; }
            .mt-4, .my-4 {margin-top: 1.5rem !important;}
            .mr-4,.mx-4 { margin-right: 1.5rem !important;}
            .mb-4, .my-4 {margin-bottom: 1.5rem !important; }
            .ml-4,.mx-4 {margin-left: 1.5rem !important;}
            .m-5 {margin: 3rem !important; }
            .mt-5,.my-5 {margin-top: 3rem !important;}
            .mr-5,.mx-5 {margin-right: 3rem !important;}
            .mb-5, .my-5 { margin-bottom: 3rem !important;}
            .ml-5,.mx-5 { margin-left: 3rem !important; }
            .p-0 { padding: 0 !important;}
            .pt-0, .py-0 {padding-top: 0 !important;}
            .pr-0,.px-0 {padding-right: 0 !important;}
            .pb-0,.py-0 {padding-bottom: 0 !important;}
            .pl-0,.px-0 {padding-left: 0 !important;}
            .p-1 { padding: 0.25rem !important;}
            .pt-1, .py-1 {padding-top: 0.25rem !important;}
            .pr-1, .px-1 {padding-right: 0.25rem !important;}
            .pb-1,.py-1 { padding-bottom: 0.25rem !important;}
            .pl-1,.px-1 {padding-left: 0.25rem !important;}
            .p-2 {padding: 0.5rem !important;}
            .pt-2,.py-2 {padding-top: 0.5rem !important;}
            .pr-2, .px-2 {padding-right: 0.5rem !important;}
            .pb-2, .py-2 {padding-bottom: 0.5rem !important;}
            .pl-2,.px-2 {padding-left: 0.5rem !important;}
            .p-3 {padding: 1rem !important;}
            .pt-3, .py-3 {padding-top: 1rem !important;}
            .pr-3,.px-3 {padding-right: 1rem !important;}
            .pb-3, .py-3 {padding-bottom: 1rem !important; }
            .pl-3,.px-3 { padding-left: 1rem !important;}
            .p-4 {padding: 1.5rem !important; }
            .pt-4, .py-4 {padding-top: 1.5rem !important;}
            .pr-4,.px-4 {padding-right: 1.5rem !important;}
            .pb-4,.py-4 {padding-bottom: 1.5rem !important;}
            .pl-4,.px-4 {padding-left: 1.5rem !important; }
            .p-5 { padding: 3rem !important; }
            .pt-5, .py-5 {padding-top: 3rem !important;}
            .pr-5,.px-5 {padding-right: 3rem !important;}
            .pb-5,.py-5 {padding-bottom: 3rem !important;}
            .pl-5,.px-5 {padding-left: 3rem !important;}
            body{
                font-family: Arial, Helvetica, sans-serif;
            }
            .btn {
                display: inline-block;
                font-weight: 400;
                color: #212529;
                text-align: center;
                vertical-align: middle;
                -webkit-user-select: none;
                -moz-user-select: none;
                -ms-user-select: none;
                user-select: none;
                background-color: transparent;
                border: 1px solid transparent;
                padding: 0.375rem 0.75rem;
                font-size: 1rem;
                line-height: 1.5;
                border-radius: 0.25rem;
                transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
            }
            .btn:hover {
                color: #212529;
                text-decoration: none;
            }
            .btn-primary {color: #fff;background-color: #007bff; border-color: #007bff;}
            .btn-primary:hover {color: #fff;background-color: #0069d9;border-color: #0062cc;}
            .btn-success {color: #fff;background-color: #28a745;border-color: #28a745;}
            .btn-success:hover { color: #fff;background-color: #218838;border-color: #1e7e34;}
            .btn-danger {color: #fff;background-color: #d9534f;border-color: #d43f3a;}
            .btn-danger:hover {color: #fff;background-color: #c9302c;border-color: #ac2925;}

            input,button,select,optgroup,textarea {  margin: 0;font-family: inherit;font-size: inherit;line-height: inherit;}
            textarea {overflow: auto;resize: vertical;}
            textarea.form-control {height: auto;}

            .toggle {position: relative; display: inline-block; width: 48px; height:26px; float:right; margin-top:20px;} 
            .toggle input {display: none;}
            .shade {position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; }
            .shade:before {position: absolute; content: ""; height: 18px; width: 18px; left:4px; bottom: 4px; background-color: #fff; -webkit-transition: .4s; transition: .4s;}
            input:checked+.shade {background-color: #2196F3}
            input:checked+.shade:before {-webkit-transform: translateX(22px); -ms-transform: translateX(22px); transform: translateX(22px)}
      </style>
    </head>
    <body onload="iniPage()">
        <div class="container">
          <div id="content">
              <div class="row">
                <div class="col-12">
                  <h2>Setting Text on the ePaper</h2>
                </div>
              </div>
              <div class="row mt-4">
                <label class="col-4 ">Text:</label>
                <input type="text" id="data" name="data">
              </div>
              
              <div class="row mt-4">
                <div class="col-12">
                  <input type="button" name="senddata" id="senddata" value="Send" onclick="sendData();" class="btn btn-success">
                </div>
              </div>
              
          </div>
        </div>
    <script>

    function sendData() {
      var data = document.getElementById("data").value;
    
      var xhr = new XMLHttpRequest();
      xhr.open("GET", "/setdata?data="+data, true); 
      xhr.send();
    }

    function iniPage(){
      var xhttp = new XMLHttpRequest();
      xhttp.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
          var stateArr = this.responseText.split("|");
          document.getElementById("data").value = stateArr[0];
        }
      };
      xhttp.open("GET", "/getdata", true);
      xhttp.send();     
    }
    
    </script>
    </body>
    </html>
)rawliteral";

Ke stažení - archiv