Search Code

ESP32-S3 RGB LED Matrix Project 3 - Text from mobile phone

ESP32-S3 RGB LED Matrix Project 3 - Text from mobile phone

Project 3 – Control Matrix Text from Your Phone (HTTP Text)

In this project the ESP32-S3 RGB LED Matrix hosts a small web page so you can change the scrolling text, color, direction, and speed directly from your phone or computer. You don’t need a separate app – just a web browser. This makes the module a tiny Wi-Fi text display you can update in real time.

All six projects in this series are explained and demonstrated in one YouTube video. The same video is embedded on this page, so you can see exactly how the web interface looks and how text updates instantly on the matrix. The full source code for this project is automatically loaded below the article, and you can purchase the ESP32-S3 RGB LED Matrix module from affiliate stores listed under the code section.

In this article we focus on how the network logic works (home Wi-Fi vs access point) and which settings you can change in the code to customize the behavior.

ESP32-S3 RGB LED Matrix Module Overview

The hardware is the same as for all the other projects in this series: an ESP32-S3 microcontroller board with a built-in 8×8 RGB LED matrix and a QMI8658C motion sensor on the back. The USB-C port is used for power and programming, and pins around the edges are still available for other IO.:contentReference[oaicite:0]{index=0}

  • ESP32-S3 – Wi-Fi and Bluetooth capable microcontroller.
  • 8×8 RGB matrix – 64 addressable RGB LEDs for text and graphics.
  • QMI8658C accelerometer – used in the tilt and game projects.
  • USB port – powers the board and uploads sketches from Arduino IDE.
  • Exposed pins – allow additional sensors or actuators if needed.
  • Boot/Reset buttons – for firmware upload and restarting.

For Project 3, the most important feature is the ESP32’s Wi-Fi, which lets the board act as a tiny web server for the text control page.:contentReference[oaicite:1]{index=1}

Projects Covered in the Video (Timestamps)

The one video for this series covers all six projects. For quick reference:

  • 00:00 – Introduction
  • 02:01 – Installing ESP32 boards
  • 03:32 – Installing libraries
  • 05:32 – Project 1: Moving Dot
  • 11:11 – Project 2: Text Scroll
  • 12:59Project 3: HTTP Text (this project)
  • 16:41 – Project 4: Tilt Dot
  • 18:55 – Project 5: Arrow Up
  • 20:02 – Project 6: Target Game

You are encouraged to watch the HTTP Text section in the video while working with this article. The video shows how the web page is generated by the ESP32 and how changing text, color, and speed is reflected instantly on the LED matrix.:contentReference[oaicite:2]{index=2}

Installing ESP32 Boards in Arduino IDE

If you have already completed Projects 1 or 2, the board setup is done and you can skip this section. Otherwise, follow these steps in the Arduino IDE:

  1. Open File > Preferences and add the ESP32 boards URL to “Additional Boards Manager URLs”.
  2. Go to Tools > Board > Boards Manager…, search for ESP32, and install the official ESP32 package.
  3. Select the correct ESP32-S3 RGB Matrix board from Tools > Board.
  4. Connect the module via USB and choose the correct serial port under Tools > Port.

Without the proper ESP32 board support and correct port, the web-server sketch will not upload.

Installing NeoMatrix and Required Libraries

This project uses the same libraries as the previous text scroll project:

  • Adafruit NeoMatrix
  • Adafruit NeoPixel
  • Adafruit GFX Library

Install via Library Manager:

  1. Open Sketch > Include Library > Manage Libraries….
  2. Search for Adafruit NeoMatrix and click Install.
  3. Accept the installation of dependencies (Adafruit GFX and Adafruit NeoPixel).

Once installed, you should see the NeoMatrix and NeoPixel example sketches under File > Examples.

Two Wi-Fi Modes in Project 3

The most important concept in this project is that the ESP32 can work in two different modes:

  1. Station (STA) mode – the ESP32 connects to your existing home Wi-Fi network.
  2. Access Point (AP) mode – the ESP32 creates its own Wi-Fi network if home Wi-Fi is not available.

Both modes use the same web interface: an HTML page served from the ESP32 itself, where you can change the text, color, scroll direction, and speed.:contentReference[oaicite:3]{index=3}

Mode 1 – Connect to Home Wi-Fi (Station Mode)

In Station mode, the ESP32 joins your home or office Wi-Fi network. This is the preferred mode whenever your router is available because:

  • Your phone and computer are already connected to the same Wi-Fi network.
  • You can point your browser to the ESP32’s IP address and control the text from any device on that network.

In the settings section of the sketch, you provide your Wi-Fi SSID and password:


// Home Wi-Fi credentials (Station mode)
const char* WIFI_SSID = "YourHomeWiFi";
const char* WIFI_PASS = "YourHomePassword";

After the board boots, it tries to connect to WIFI_SSID. If successful, the code prints the assigned IP address to the Serial Monitor, for example:


Connected to WiFi
IP address: 192.168.1.16

To control the text:

  1. Make sure your phone or PC is connected to the same Wi-Fi network (for example, YourHomeWiFi).
  2. Open a browser and enter the printed IP address, such as http://192.168.1.16/. :contentReference[oaicite:4]{index=4}
  3. The control page will appear, allowing you to type text, choose color, select direction, and adjust scroll speed.

Mode 2 – Standalone Access Point (AP Mode)

If the ESP32 cannot connect to your home Wi-Fi (wrong password, network not available, or you are using the module outside), the sketch automatically falls back to Access Point mode. In AP mode, the ESP32 itself becomes a Wi-Fi hotspot with its own network name and password.

In this project, the AP settings are fixed as:


// Access Point (AP) credentials (fallback mode)
const char* AP_SSID = "ESP32";
const char* AP_PASS = "password";

When Station mode fails, the module switches to AP mode and starts broadcasting a Wi-Fi network called ESP32. To control the matrix:

  1. On your phone or computer, open the Wi-Fi settings and connect to the network ESP32.
  2. Enter the password password (as defined in the code).
  3. Once connected, open a browser and go to http://192.168.4.1/ (the default IP for ESP32 AP mode).
  4. The same control page appears, allowing you to change text, color, speed, and direction.

This fallback behavior makes the project useful anywhere: at home, in the lab, or in a demo environment where no router is available.

Project 3 – Main Settings in the Code

The full HTTP Text sketch is loaded below this article by the website. Here we only document the most important configuration options you are likely to edit.

Wi-Fi and Access Point Settings

At the top of the sketch you will find the Wi-Fi configuration section. Only change the Station (home Wi-Fi) credentials; the AP settings are normally kept as defaults:


// ---------- Wi-Fi SETTINGS ----------

// Home Wi-Fi (Station mode)
const char* WIFI_SSID = "YourHomeWiFi";      // put your router SSID here
const char* WIFI_PASS = "YourHomePassword";  // put your router password here

// Fallback Access Point (AP mode)
const char* AP_SSID = "ESP32";               // fixed AP name
const char* AP_PASS = "password";            // fixed AP password

Behavior:

  • If WIFI_SSID and WIFI_PASS are correct and the network is available → ESP32 connects as a normal Wi-Fi device (Station mode).
  • If connection fails after a timeout → ESP32 starts its own AP using AP_SSID and AP_PASS.

Matrix Pin, Size, and Brightness

These settings are the same as the previous projects:


// Matrix configuration
const int MATRIX_PIN    = 14;   // RGB matrix data pin
const int MATRIX_WIDTH  = 8;
const int MATRIX_HEIGHT = 8;

// Overall display brightness (0–255)
uint8_t matrixBrightness = 40;  // adjust for your environment

Keep MATRIX_PIN at 14 for this board. You can increase matrixBrightness if you need more light, but lower values are more comfortable for close-up viewing.

Default Text and Scroll Settings

When the board starts, it displays an initial message until you open the web page and type a new one. You can change the default text in the configuration:


// Default message shown at startup
String currentText = "Robojax";   // overwrite from web UI later

The rest of the scroll behavior is controlled by a set of variables that are updated by the web interface:


// Scroll delay in milliseconds (lower = faster)
int scrollDelayMs = 50;

// Scroll direction: 0=left, 1=right, 2=up, 3=down
int scrollDirection = 0;   // default: scroll left

The web page sends new values based on the slider and button selections. From the Arduino side, you only need to know that:

  • Decreasing scrollDelayMs makes the text move faster.
  • Increasing scrollDelayMs makes it move slower.
  • Changing scrollDirection switches between left, right, up, or down scroll modes.

Text Color (Controlled from the Web Page)

The color of the text is controlled by three 0–255 values (red, green, blue). They are updated whenever you choose a new color on the web page:


// Current text color (R, G, B)
uint8_t textRed   = 255;
uint8_t textGreen = 255;
uint8_t textBlue  = 255;

When you pick a color in the browser and click Apply, the ESP32 parses the RGB values and updates these three variables; the text immediately changes color on the matrix. In the video, this behavior is demonstrated with multiple color changes, including red, green and blue examples.:contentReference[oaicite:5]{index=5}

Summary

Project 3 turns your ESP32-S3 RGB LED Matrix into a fully wireless text display that you can control using any device with a web browser. The sketch is designed to be robust:

  • It first tries to connect to your home Wi-Fi using the SSID and password you configured.
  • If that fails, it automatically becomes an access point with the name ESP32 and password password.
  • In both modes, opening the correct IP address in a browser displays the same control page for text, color, direction, and speed.

The complete HTTP Text code is available below this article (loaded automatically on the website). For a full step-by-step walk-through and a live demonstration of how the text updates in real time, make sure to watch the Project 3 section of the video. If you would like to build the project yourself, you can also purchase the ESP32-S3 RGB LED Matrix module using the affiliate links listed beneath the code.

Images

ESP32 S3 Matrix
ESP32 S3 Matrix
ESP32 S3 Matrix  pin out
ESP32 S3 Matrix pin out
ESP32-S3_RGB_8x8_matrix-3
ESP32-S3_RGB_8x8_matrix-3
ESP32 S3 Matrix displaying rainbow heart
ESP32 S3 Matrix displaying rainbow heart
ESP32-S3_RGB_8x8_matrix1
ESP32-S3_RGB_8x8_matrix1
ESP32-S3_RGB_8x8_matrix-2
ESP32-S3_RGB_8x8_matrix-2
ESP32-S3 RGB Matrix- Mobile Phone Text
ESP32-S3 RGB Matrix- Mobile Phone Text
801-ESP32-S3 RGB LED Matrix Project 3 - Text from mobile phone
Language: C++
/*
  Project 4: HTTP Text Scroll – ESP32-S3 RGB LED Matrix (Waveshare)

  - Connects to your home WiFi (station mode, with AP fallback).
  - Serves a web page where you can set:
      * Text
      * Color
      * Display ON/OFF
      * Scroll direction (Left / Right / Up / Down)
      * Scroll delay (speed)
  - Supports 8×8 RGB LED matrix using Adafruit_NeoMatrix.

  ▶️ Video Tutorial:
  https://youtu.be/JKLuYrRcLMI

  📄 Resources & Code Page:
  https://robojax.com/your-resources-page-here

  HTTP_Text_Scroll
*/

#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>

#include <Adafruit_GFX.h>
#include <Adafruit_NeoMatrix.h>
#include <Adafruit_NeoPixel.h>

// ======================= WIFI SETTINGS =========================
// Home WiFi (change these to your router credentials)
const char* WIFI_SSID = "Biseem";
const char* WIFI_PASS = "wan9&Jang~";

// Fallback Access Point
const char* AP_SSID = "ESP32";
const char* AP_PASS = "password";

// ======================= MATRIX SETTINGS =======================

#define MATRIX_PIN    14
#define MATRIX_WIDTH  8
#define MATRIX_HEIGHT 8

// 0, 1, 2, or 3 – adjust if text orientation is wrong with USB up
#define MATRIX_ROTATION 0
#define BRIGHTNESS 15

Adafruit_NeoMatrix matrix = Adafruit_NeoMatrix(
  MATRIX_WIDTH, MATRIX_HEIGHT, MATRIX_PIN,
  NEO_MATRIX_TOP + NEO_MATRIX_LEFT +
  NEO_MATRIX_ROWS + NEO_MATRIX_PROGRESSIVE,
  NEO_RGB + NEO_KHZ800
);

// ======================= SCROLL STATE ==========================

enum ScrollDir {
  DIR_LEFT = 0,
  DIR_RIGHT,
  DIR_UP,
  DIR_DOWN
};

String   scrollText  = "Robojax";
uint16_t textColor   = 0xFFFF;  // default white
bool     displayOn   = true;
ScrollDir currentDir = DIR_LEFT;

// 1 step every this many ms (lower = faster)
unsigned long scrollInterval = 80;
unsigned long lastScrollTime = 0;

// Horizontal scroll position
int scrollX = MATRIX_WIDTH;
int scrollY = 0;              // top row

// State for vertical per-letter scroll
int vertCharIndex = 0;        // which character of the string
int vertY         = MATRIX_HEIGHT;    // vertical position of that character

// ======================= WEB SERVER ============================

WebServer server(80);

// ======================= HTML PAGE ============================

const char MAIN_page[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ESP32 Text Scroll</title>
<style>
  body {
    font-family: Arial, sans-serif;
    background: #111;
    color: #eee;
    text-align: center;
    margin: 0;
    padding: 10px;
  }
  .container {
    max-width: 360px;
    width: 100%%;
    margin: 0 auto;
    background: #222;
    padding: 12px;
    border-radius: 10px;
    box-sizing: border-box;
  }
  input[type="text"] {
    width: 100%%;
    padding: 8px;
    box-sizing: border-box;
    border-radius: 5px;
    border: 1px solid #444;
    background: #111;
    color: #eee;
    margin-bottom: 10px;
  }
  .row {
    margin: 10px 0;
  }
  .label {
    display: block;
    margin-bottom: 5px;
    text-align: left;
  }
  .toggle {
    display: inline-flex;
    align-items: center;
    gap: 8px;
  }
  .arrow-grid {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    grid-template-rows: repeat(3, 50px);
    gap: 8px;
    justify-content: center;
    margin-top: 10px;
  }
  .arrow-btn {
    background: #333;
    border: 1px solid #555;
    color: #eee;
    font-size: 18px;
    border-radius: 10px;
    cursor: pointer;
    width: 100%%;
    height: 100%%;
  }
  .arrow-btn.active {
    background: #0a84ff;
  }
  .arrow-btn:disabled {
    opacity: 0.5;
    cursor: default;
  }
  button#applyBtn {
    margin-top: 15px;
    padding: 10px 20px;
    border-radius: 8px;
    border: none;
    background: #0a84ff;
    color: #fff;
    font-size: 16px;
    cursor: pointer;
  }
  button#applyBtn:active {
    transform: scale(0.97);
  }
</style>
</head>
<body>
  <div class="container">
    <h2>Robojax ESP32-S3 Matrix Text Scroll</h2>

    <div class="row">
      <span class="label">Text to scroll:</span>
      <input type="text" id="text" value="Hello" />
    </div>

    <div class="row">
      <span class="label">Color:</span>
      <input type="color" id="color" value="#ffffff" />
    </div>

    <div class="row">
      <span class="label">Scroll Delay (fast → slow):</span>
      <input
        type="range"
        id="speed"
        min="20"
        max="300"
        value="80"
        oninput="document.getElementById('speedVal').innerText = this.value + ' ms';"
      />
      <div id="speedVal" style="text-align:right;font-size:12px;margin-top:4px;">
        80 ms
      </div>
    </div>

    <div class="row">
      <label class="toggle">
        <input type="checkbox" id="power" checked />
        <span>Display ON</span>
      </label>
    </div>

    <div class="row">
      <span class="label">Scroll Direction:</span>
      <div class="arrow-grid">
        <div></div>
        <button class="arrow-btn" id="btnUp" onclick="setDir('up')">&#9650;</button>
        <div></div>

        <button class="arrow-btn" id="btnLeft" onclick="setDir('left')">&#9664;</button>
        <div></div>
        <button class="arrow-btn" id="btnRight" onclick="setDir('right')">&#9654;</button>

        <div></div>
        <button class="arrow-btn" id="btnDown" onclick="setDir('down')">&#9660;</button>
        <div></div>
      </div>
    </div>

    <button id="applyBtn" onclick="sendUpdate()">Apply</button>
    <p id="status"></p>
  </div>

<script>
  let currentDir = 'left';

  function setDir(dir) {
    currentDir = dir;
    document.querySelectorAll('.arrow-btn').forEach(b => b.classList.remove('active'));
    if (dir === 'up')    document.getElementById('btnUp').classList.add('active');
    if (dir === 'down')  document.getElementById('btnDown').classList.add('active');
    if (dir === 'left')  document.getElementById('btnLeft').classList.add('active');
    if (dir === 'right') document.getElementById('btnRight').classList.add('active');
    sendUpdate();
  }

  function sendUpdate() {
    const text  = document.getElementById('text').value;
    const color = document.getElementById('color').value;
    const power = document.getElementById('power').checked ? '1' : '0';
    const speed = document.getElementById('speed').value;

    const url = `/update?text=${encodeURIComponent(text)}&color=${encodeURIComponent(color)}&power=${power}&dir=${currentDir}&speed=${speed}`;

    fetch(url)
      .then(r => r.text())
      .then(t => {
        document.getElementById('status').innerText = t;
      })
      .catch(err => {
        document.getElementById('status').innerText = 'Error sending update';
      });
  }

  // Set default active
  setDir('left');
</script>
</body>
</html>
)rawliteral";

// ======================= HELPERS ===============================

bool isHexChar(char c) {
  return (c >= '0' && c <= '9') ||
         (c >= 'a' && c <= 'f') ||
         (c >= 'A' && c <= 'F');
}

String urlDecode(const String &src) {
  String result;
  result.reserve(src.length());
  for (size_t i = 0; i < src.length(); i++) {
    char c = src[i];
    if (c == '+') {
      result += ' ';
    } else if (c == '%' && i + 2 < src.length()) {
      char h1 = src[i + 1];
      char h2 = src[i + 2];
      if (isHexChar(h1) && isHexChar(h2)) {
        char hex[3] = {h1, h2, 0};
        int val = (int)strtol(hex, nullptr, 16);
        result += (char)val;
        i += 2;
      } else {
        result += c;
      }
    } else {
      result += c;
    }
  }
  return result;
}

uint16_t colorFromHex(const String &hex) {
  // Expect "#RRGGBB" or "RRGGBB"
  String c = hex;
  if (c.startsWith("#")) c.remove(0, 1);
  if (c.length() != 6) {
    // default white
    return matrix.Color(255, 255, 255);
  }
  long value = strtol(c.c_str(), NULL, 16);
  uint8_t r = (value >> 16) & 0xFF;
  uint8_t g = (value >> 8) & 0xFF;
  uint8_t b = (value) & 0xFF;
  return matrix.Color(r, g, b);
}

void resetScrollPosition() {
  int textWidth  = scrollText.length() * 6;  // default font ~6 px per char
  int textHeight = 8;                        // 5x7 font fits in 8

  switch (currentDir) {
    case DIR_LEFT:
      // Start from just outside the right edge
      scrollX = MATRIX_WIDTH;
      scrollY = 0;
      break;

    case DIR_RIGHT:
      // Start from just outside the left edge
      scrollX = -textWidth;
      scrollY = 0;
      break;

    case DIR_UP:
      // Per-letter vertical scroll, starting with first character below matrix
      vertCharIndex = 0;
      vertY = MATRIX_HEIGHT;   // 8 → enters from bottom
      break;

    case DIR_DOWN:
      // Per-letter vertical scroll, starting with first character above matrix
      vertCharIndex = 0;
      vertY = -textHeight;     // -8 → enters from top
      break;
  }
}

// ======================= HTTP HANDLERS =========================

void handleRoot() {
  server.send_P(200, "text/html", MAIN_page);
}

void handleUpdate() {
  if (server.hasArg("text")) {
    String txt = urlDecode(server.arg("text"));
    if (txt.length() == 0) {
      scrollText = " ";
    } else {
      scrollText = txt;
    }
  }

  if (server.hasArg("color")) {
    String hex = server.arg("color");
    textColor = colorFromHex(hex);
  }

  if (server.hasArg("power")) {
    String p = server.arg("power");
    displayOn = (p == "1");
  }

  if (server.hasArg("dir")) {
    String d = server.arg("dir");
    if (d == "left")      currentDir = DIR_LEFT;
    else if (d == "right") currentDir = DIR_RIGHT;
    else if (d == "up")    currentDir = DIR_UP;
    else if (d == "down")  currentDir = DIR_DOWN;
  }

  if (server.hasArg("speed")) {
    String s = server.arg("speed");
    int val = s.toInt();

    // simple safety clamp
    if (val < 10)   val = 10;    // very fast
    if (val > 1000) val = 1000;  // 1 second max

    scrollInterval = (unsigned long)val;
    // Serial.print("Scroll interval set to ");
    // Serial.print(scrollInterval);
    // Serial.println(" ms");
  }

  resetScrollPosition();
  server.send(200, "text/plain", "Updated");
}

// ======================= SCROLL LOGIC ==========================

void drawScroll() {
  if (!displayOn) {
    matrix.fillScreen(0);
    matrix.show();
    return;
  }

  int textWidth  = scrollText.length() * 6;
  int textHeight = 8;

  matrix.fillScreen(0);
  matrix.setTextSize(1);
  matrix.setTextWrap(false);
  matrix.setTextColor(textColor);

  // -------- HORIZONTAL SCROLL (LEFT / RIGHT) --------
  if (currentDir == DIR_LEFT || currentDir == DIR_RIGHT) {
    matrix.setCursor(scrollX, 0);
    matrix.print(scrollText);
    matrix.show();

    if (currentDir == DIR_LEFT) {
      scrollX--;
      if (scrollX < -textWidth) {
        scrollX = MATRIX_WIDTH;
      }
    } else { // DIR_RIGHT
      scrollX++;
      if (scrollX > MATRIX_WIDTH) {
        scrollX = -textWidth;
      }
    }
    return;
  }

  // -------- VERTICAL SCROLL (UP / DOWN) – PER LETTER --------

  if (scrollText.length() == 0) {
    matrix.show();
    return;
  }

  // Current character
  char c = scrollText[vertCharIndex];

  int charWidth  = 6;
  int baseX = (MATRIX_WIDTH - charWidth) / 2;  // center horizontally

  matrix.setCursor(baseX, vertY);
  matrix.print(c);
  matrix.show();

  if (currentDir == DIR_UP) {
    // Move character upwards
    vertY--;
    if (vertY < -textHeight) {
      // This character is fully passed, go to next
      vertY = MATRIX_HEIGHT;
      vertCharIndex++;
      if (vertCharIndex >= scrollText.length()) {
        vertCharIndex = 0; // loop back to first character
      }
    }
  } else if (currentDir == DIR_DOWN) {
    // Move character downwards
    vertY++;
    if (vertY > MATRIX_HEIGHT) {
      // This character is fully passed, go to next
      vertY = -textHeight;
      vertCharIndex++;
      if (vertCharIndex >= scrollText.length()) {
        vertCharIndex = 0; // loop back
      }
    }
  }
}

// ======================= WIFI INIT =============================

void startAPFallback() {
  Serial.println("Starting AP fallback...");
  WiFi.mode(WIFI_AP);
  bool apOk = WiFi.softAP(AP_SSID, AP_PASS);
  if (apOk) {
    Serial.print("AP started. SSID: ");
    Serial.print(AP_SSID);
    Serial.print("  IP: ");
    Serial.println(WiFi.softAPIP());
  } else {
    Serial.println("Failed to start AP.");
  }
}

void connectWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);

  Serial.print("Connecting to WiFi ");
  Serial.print(WIFI_SSID);

  unsigned long startAttempt = millis();
  const unsigned long timeout = 10000; // 10s

  while (WiFi.status() != WL_CONNECTED && (millis() - startAttempt) < timeout) {
    Serial.print(".");
    delay(500);
  }
  Serial.println();

  if (WiFi.status() == WL_CONNECTED) {
    Serial.print("Connected. IP address: ");
    Serial.println(WiFi.localIP());
  } else {
    Serial.println("WiFi connect failed, starting AP fallback.");
    startAPFallback();
  }
}

// ======================= SETUP & LOOP ==========================

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

  // Matrix init
  matrix.begin();
  matrix.setRotation(MATRIX_ROTATION);
  matrix.setBrightness(BRIGHTNESS);
  matrix.fillScreen(0);
  matrix.show();

  // WiFi
  connectWiFi();

  // Web server routes
  server.on("/", handleRoot);
  server.on("/update", handleUpdate);
  server.onNotFound([]() {
    server.send(404, "text/plain", "Not found");
  });
  server.begin();
  Serial.println("HTTP server started.");

  resetScrollPosition();
}

void loop() {
  server.handleClient();

  unsigned long now = millis();
  if (now - lastScrollTime >= scrollInterval) {
    lastScrollTime = now;
    drawScroll();
  }
}

Things you might need

Resources & references

Files📁

Fritzing File