Search Code

ESP32-S3 RGB LED矩陣項目3 - 手機文字

ESP32-S3 RGB LED矩陣項目3 - 手機文字

項目3 – 用手機控制矩陣文字(HTTP文字)

喺呢個項目入面,ESP32-S3 RGB LED矩陣會主持一個小型網頁,等你可以直接喺手機或者電腦度改滾動文字、顏色、方向同速度。你唔需要另外裝App – 淨係用瀏覽器就得。咁樣就令到呢個模組變成一個可以即時更新嘅微型Wi-Fi文字顯示器。

呢個系列嘅全部六個項目都會喺一條YouTube影片入面解釋同示範。同一條影片亦都嵌入咗喺呢一頁,所以你可以清楚睇到網頁介面係點樣,同埋文字點樣即時更新喺矩陣上面。呢個項目嘅完整原始碼會自動載入喺文章下面,你可以喺程式碼部分列出嘅聯盟商店購買ESP32-S3 RGB LED矩陣模組。

喺呢篇文章入面,我哋會集中講網絡邏輯點樣運作(家用Wi-Fi對比存取點模式),同埋你可以喺程式碼入面改邊啲設定嚟自訂行為。

ESP32-S3 RGB LED矩陣模組概覽

硬件同呢個系列其他項目一樣:一塊ESP32-S3微控制器板,背面內置一個8×8 RGB LED矩陣同一個QMI8658C動作感應器。USB-C接口用嚟供電同編程,邊緣嘅引腳仍然可以用嚟做其他IO。

  • ESP32-S3 – 具備Wi-Fi同藍牙功能嘅微控制器。
  • 8×8 RGB矩陣 – 64粒可定址RGB LED,用嚟顯示文字同圖形。
  • QMI8658C加速度計 – 用喺傾斜同遊戲項目入面。
  • USB接口 – 為板供電同從Arduino IDE上傳程式。
  • 外露引腳 – 有需要時可以連接額外感應器或者執行器。
  • Boot/Reset按鈕 – 用嚟上傳韌體同重新啟動。

對於項目3嚟講,最重要嘅功能係ESP32嘅Wi-Fi,佢令到塊板可以充當一個微型網頁伺服器,用嚟提供文字控制頁面。

影片入面涵蓋嘅項目(時間戳)

呢個系列嘅一條影片涵蓋晒全部六個項目。方便快速參考:

  • 00:00 – 簡介
  • 02:01 – 安裝ESP32板支援
  • 03:32 – 安裝程式庫
  • 05:32 – 項目1:移動點
  • 11:11 – 項目2:文字滾動
  • 12:59項目3:HTTP文字(呢個項目)
  • 16:41 – 項目4:傾斜點
  • 18:55 – 項目5:向上箭嘴
  • 20:02 – 項目6:靶心遊戲

建議你睇呢篇文章嘅時候,同時睇返影片入面嘅HTTP文字部分。影片會展示ESP32點樣生成網頁,同埋改文字、顏色同速度點樣即時反映喺LED矩陣上面。

喺Arduino IDE安裝ESP32板支援

如果你已經完成咗項目1或者2,板嘅設定已經做好,可以跳過呢個部分。否則,跟住以下步驟喺Arduino IDE度做:

  1. 打開File > Preferences,將ESP32板嘅URL加入「Additional Boards Manager URLs」。
  2. Tools > Board > Boards Manager…,搜尋ESP32,然後安裝官方ESP32套件。
  3. Tools > Board度選擇正確嘅ESP32-S3 RGB矩陣板。
  4. 用USB連接模組,然後喺Tools > Port度選擇正確嘅序列埠。

如果冇正確嘅ESP32板支援同正確嘅埠,網頁伺服器程式就冇辦法上傳。

安裝NeoMatrix同所需程式庫

呢個項目用嘅程式庫同之前文字滾動項目一樣:

  • Adafruit NeoMatrix
  • Adafruit NeoPixel
  • Adafruit GFX Library

透過程式庫管理員安裝:

  1. 打開Sketch > Include Library > Manage Libraries…
  2. 搜尋Adafruit NeoMatrix,然後撳Install
  3. 接受相依程式庫嘅安裝(Adafruit GFXAdafruit NeoPixel)。

安裝完成之後,你應該會喺File > Examples度見到NeoMatrix同NeoPixel嘅範例程式。

項目3入面嘅兩種Wi-Fi模式

呢個項目最重要嘅概念係ESP32可以喺兩種唔同模式下運作:

  1. Station(STA)模式 – ESP32連接你現有嘅家用Wi-Fi網絡。
  2. Access Point(AP)模式 – 如果家用Wi-Fi唔可用,ESP32會建立自己嘅Wi-Fi網絡。

兩種模式都用同一個網頁介面:一個由ESP32自己提供嘅HTML頁面,你可以喺上面改文字、顏色、滾動方向同速度。

模式1 – 連接家用Wi-Fi(Station模式)

喺Station模式入面,ESP32會加入你嘅家居或者辦公室Wi-Fi網絡。呢個係喺路由器可用時嘅首選模式,因為:

  • 你嘅手機同電腦已經連接咗同一個Wi-Fi網絡。
  • 你可以將瀏覽器指向ESP32嘅IP地址,然後喺嗰個網絡上任何裝置控制文字。

喺程式嘅設定部分,你需要提供你嘅Wi-Fi SSID同密碼:


// 家居 Wi-Fi 憑證(Station 模式)
const char* WIFI_SSID = "YourHomeWiFi";
const char* WIFI_PASS = "YourHomePassword";

喺開發板啟動之後,佢會嘗試連接 WIFI_SSID。如果成功,程式會將分配嘅 IP 地址打印到 Serial Monitor,例如:


Connected to WiFi
IP address: 192.168.1.16

要控制文字:

  1. 確保你嘅手機或電腦連接咗同一個 Wi-Fi 網絡(例如 YourHomeWiFi)。
  2. 打開瀏覽器並輸入打印出嚟嘅 IP 地址,例如 http://192.168.1.16/
  3. 控制頁面會出現,容許你輸入文字、揀顏色、選擇方向,同埋調整滾動速度。

模式 2 – 獨立存取點(AP 模式)

如果 ESP32 連接唔到你嘅家居 Wi-Fi(密碼錯誤、網絡唔可用,或者你喺戶外使用模組),程式會自動切換到存取點模式。喺 AP 模式,ESP32 本身會變成一個 Wi-Fi 熱點,帶有自己嘅網絡名稱同密碼。

喺呢個項目入面,AP 設定係固定嘅:


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

當 Station 模式失敗,模組會切換到 AP 模式,並開始廣播一個叫 ESP32 嘅 Wi-Fi 網絡。要控制矩陣:

  1. 喺你嘅手機或電腦上,打開 Wi-Fi 設定並連接到 ESP32 網絡。
  2. 輸入密碼 password(同程式入面定義嘅一樣)。
  3. 連接之後,打開瀏覽器並前往 http://192.168.4.1/(ESP32 AP 模式嘅默認 IP)。
  4. 相同嘅控制頁面會出現,容許你更改文字、顏色、速度同方向。

呢種後備行為令到項目喺任何地方都有用:喺屋企、實驗室,或者冇路由器嘅演示環境。

項目 3 – 程式入面嘅主要設定

完整嘅 HTTP Text sketch 由網站喺呢篇文章下面載入。呢度我哋只記錄你最可能編輯嘅重要配置選項。

Wi-Fi 同存取點設定

喺 sketch 嘅頂部,你會搵到 Wi-Fi 配置部分。只更改 Station(家居 Wi-Fi)憑證;AP 設定通常保持默認值:


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

// Home Wi-Fi (Station mode)
const char* WIFI_SSID = "YourHomeWiFi";      // 喺度輸入你嘅路由器 SSID
const char* WIFI_PASS = "YourHomePassword";  // 喺度輸入你嘅路由器密碼

// Fallback Access Point (AP mode)
const char* AP_SSID = "ESP32";               // 固定 AP 名稱
const char* AP_PASS = "password";            // 固定 AP 密碼

行為:

  • 如果 WIFI_SSIDWIFI_PASS 正確,而且網絡可用 → ESP32 會作為普通 Wi-Fi 裝置連接(Station 模式)。
  • 如果連接喺超時後失敗 → ESP32 會使用 AP_SSIDAP_PASS 啟動自己嘅 AP。

矩陣引腳、尺寸同亮度

呢啲設定同之前嘅項目一樣:


// Matrix configuration
const int MATRIX_PIN    = 14;   // RGB 矩陣數據引腳
const int MATRIX_WIDTH  = 8;
const int MATRIX_HEIGHT = 8;

// Overall display brightness (0–255)
uint8_t matrixBrightness = 40;  // 根據你嘅環境調整

MATRIX_PIN 保持喺 14 以配合呢塊板。如果你需要更多光,可以增加 matrixBrightness,但較低嘅值喺近距離觀看時會更舒適。

默認文字同滾動設定

當開發板啟動時,佢會顯示一個初始訊息,直到你打開網頁並輸入新嘅文字。你可以更改配置入面嘅默認文字:


// Default message shown at startup
String currentText = "Robojax";   // 之後可以從網頁 UI 覆寫

其餘嘅滾動行為由一組由網頁界面更新嘅變數控制:


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

// Scroll direction: 0=left, 1=right, 2=up, 3=down
int scrollDirection = 0;   // 默認:向左滾動

網頁會根據滑桿同按鈕選擇發送新值。從 Arduino 方面,你只需要知道:

  • 減少 scrollDelayMs 會令文字移動得更快。
  • 增加 scrollDelayMs 會令佢移動得更慢。
  • 更改 scrollDirection 會喺左、右、上或下滾動模式之間切換。

文字顏色(從網頁控制)

文字顏色由三個 0–255 嘅值(紅、綠、藍)控制。每當你喺網頁上揀新顏色時,佢哋就會更新:


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

當你喺瀏覽器揀顏色並撳 Apply,ESP32 會解析 RGB 值並更新呢三個變數;文字會即刻喺矩陣上改變顏色。喺影片入面,呢種行為用多種顏色變化嚟示範,包括紅色、綠色同藍色嘅例子。

總結

項目 3 將你嘅 ESP32-S3 RGB LED 矩陣變成一個完全無線嘅文字顯示器,你可以用任何有網頁瀏覽器嘅裝置嚟控制。呢個 sketch 設計得好穩健:

  • 佢首先會嘗試用你設定嘅SSID同密碼連接你屋企嘅Wi-Fi。
  • 如果咁樣失敗,佢會自動變成一個名稱係ESP32、密碼係password嘅存取點。
  • 喺兩種模式之下,喺瀏覽器打開正確嘅IP地址都會顯示同一個控制頁面,用嚟控制文字、顏色、方向同速度。

完整嘅HTTP文字代碼喺呢篇文章下面有得睇(會喺網站上自動載入)。如果想睇逐步詳細講解同文字即時更新嘅現場示範,記得睇影片入面Project 3嘅部分。如果你想自己整呢個項目,都可以用下面代碼列出嘅聯盟連結購買ESP32-S3 RGB LED矩陣模組。

圖片

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
語言: 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();
  }
}

你可能需要嘅嘢

資源與參考資料

文件📁

Fritzing 文件