検索コード

ESP32-S3 RGB LEDマトリックスプロジェクト3 - 携帯電話からのテキスト

ESP32-S3 RGB LEDマトリックスプロジェクト3 - 携帯電話からのテキスト

プロジェクト3 - あなたの電話からの制御マトリックステキスト (HTTPテキスト)

このプロジェクトでは、ESP32-S3 RGB LEDマトリックスが小さなウェブページをホストしているため、スマートフォンやコンピュータから直接スクロールテキスト、色、方向、速度を変更できます。別のアプリは必要ありません - ウェブブラウザーだけで十分です。これにより、このモジュールはリアルタイムで更新できる小型のWi-Fiテキストディスプレイになります。

このシリーズのすべての6つのプロジェクトは、1本のYouTube動画で説明され、デモが行われています。同じ動画がこのページに埋め込まれているので、ウェブインターフェースがどのように見えるか、またテキストがマトリックス上でどのように瞬時に更新されるかを正確に見ることができます。このプロジェクトの完全なソースコードは記事の下に自動的に読み込まれ、コードセクションにリストされている提携ストアからESP32-S3 RGB LEDマトリックスモジュールを購入することができます。

この記事では、ネットワークロジックがどのように機能するか(ホームWi-Fiとアクセスポイント)や、動作をカスタマイズするためにコード内で変更できる設定について焦点を当てます。

ESP32-S3 RGB LEDマトリックスモジュールの概要

このハードウェアは、このシリーズの他のすべてのプロジェクトと同じです:8×8 RGB LEDマトリックスを内蔵したESP32-S3マイクロコントローラーボードと、背面にQMI8658Cモーションセンサーが搭載されています。USB-Cポートは電源供給とプログラム用に使用され、周囲のピンは他のIO用にまだ利用可能です。:contentReference[oaicite:0]{index=0}

  • ESP32-S3- Wi-FiおよびBluetooth対応のマイクロコントローラ。
  • 8×8 RGBマトリックス- テキストとグラフィック用の64個のアドレス指定可能なRGB LED。
  • QMI8658C 加速度計- ティルトおよびゲームプロジェクトで使用される。
  • USBポート- ボードに電力を供給し、Arduino IDEからスケッチをアップロードします。
  • 露出ピン必要に応じて追加のセンサーやアクチュエーターを許可する。
  • ブート/リセットボタン- ファームウェアのアップロードと再起動のため。

プロジェクト3において、最も重要な機能はESP32のWi-Fiで、これによりボードはテキストコントロールページの小さなウェブサーバーとして機能します。:contentReference[oaicite:1]{index=1}

動画で紹介されたプロジェクト(タイムスタンプ)

このシリーズの1つの動画で、すべての6つのプロジェクトがカバーされています。参照用に:

  • 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マトリックスに即座に反映される様子が示されています。:contentReference[oaicite:2]{index=2}

Arduino IDEでESP32ボードをインストールする

プロジェクト1または2をすでに完了している場合は、ボードのセットアップは完了しているため、このセクションをスキップできます。それ以外の場合は、Arduino IDEで次の手順に従ってください。

  1. オープンFile > Preferences"追加のボードマネージャのURL"にESP32ボードのURLを追加します。
  2. 行くTools > Board > Boards Manager…, 検索するESP32、公式のESP32パッケージをインストールします。
  3. 正しいESP32-S3 RGBマトリックスボードを選択してください。Tools > Board.
  4. モジュールをUSB経由で接続し、正しいシリアルポートを選択します。Tools > Port.

適切なESP32ボードサポートと正しいポートがないと、ウェブサーバースケッチはアップロードされません。

NeoMatrixおよび必要なライブラリのインストール

このプロジェクトは、前回のテキストスクロールプロジェクトと同じライブラリを使用しています:

  • Adafruit NeoMatrix
  • Adafruit NeoPixel
  • Adafruit GFX Library

ライブラリマネージャーからインストール:

  1. 開くSketch > Include Library > Manage Libraries….
  2. 検索するAdafruit NeoMatrixそしてクリックしてくださいインストール.
  3. 依存関係のインストールを受け入れてください。Adafruit GFXAdafruit NeoPixel).

インストールが完了すると、NeoMatrixとNeoPixelのサンプルスケッチが表示されるはずです。File > Examples.

プロジェクト3の2つのWi-Fiモード

このプロジェクトで最も重要な概念は、ESP32が働くことができるということです。二つの異なるモード:

  1. ステーション (STA) モードESP32は、既存の家庭用Wi-Fiネットワークに接続します。
  2. アクセスポイント(AP)モードESP32は、自宅のWi-Fiが利用できない場合、自分自身のWi-Fiネットワークを作成します。

両方のモードは同じウェブインターフェースを使用します:ESP32自体から提供されるHTMLページで、テキスト、色、スクロール方向、速度を変更することができます。:contentReference[oaicite:3]{index=3}

モード 1 - 自宅の Wi-Fi に接続 (ステーションモード)

ステーションモードでは、ESP32が自宅またはオフィスのWi-Fiネットワークに接続します。これは、ルーターが利用可能な場合に推奨されるモードです。

  • あなたの電話とコンピュータはすでに同じWi-Fiネットワークに接続されています。
  • ブラウザをESP32のIPアドレスに向けることで、そのネットワーク上の任意のデバイスからテキストを制御することができます。

スケッチの設定セクションで、Wi-Fi SSIDとパスワードを提供します:


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

ボードが起動した後、接続を試みますWIFI_SSID. 成功した場合、コードは割り当てられたIPアドレスをシリアルモニターに出力します。例えば:


Connected to WiFi
IP address: 192.168.1.16

テキストを制御するために:

  1. スマートフォンまたはPCが同じWi-Fiネットワークに接続されていることを確認してください(例えば、YourHomeWiFi).
  2. ブラウザを開き、印刷されたIPアドレスを入力します。http://192.168.1.16/. :contentReference[oaicite:4]{index=4}
  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";

ステーションモードが失敗すると、モジュールはAPモードに切り替わり、「と呼ばれるWi-Fiネットワークの放送を開始します。ESP32マトリックスを制御するには:

  1. スマートフォンやコンピュータでWi-Fi設定を開き、ネットワークに接続してください。ESP32.
  2. パスワードを入力してくださいパスワード(コードで定義されているように)
  3. 接続が完了したら、ブラウザを開いて行きますhttp://192.168.4.1/(ESP32 APモードのデフォルトIP)。
  4. 同じコントロールページが表示され、テキスト、色、速度、方向を変更することができます。

このフォールバック機能により、プロジェクトは自宅、ラボ、またはルーターが利用できないデモ環境など、どこでも便利に使用できます。

プロジェクト3 - コードのメイン設定

この記事の下に、ウェブサイトによって完全なHTTPテキストスケッチが読み込まれています。ここでは、あなたが編集する可能性が高い最も重要な設定オプションのみを記録します。

Wi-Fiおよびアクセスポイント設定

スケッチの上部にはWi-Fi設定セクションがあります。ステーション(自宅のWi-Fi)認証情報のみを変更してください。AP設定は通常デフォルトのままです。


// ---------- 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

行動:

  • もしWIFI_SSIDそしてWIFI_PASS正しいことが確認され、ネットワークが利用可能です → ESP32は通常のWi-Fiデバイス(ステーションモード)として接続します。
  • タイムアウト後に接続が失敗した場合 → ESP32は独自のAPを使用して起動しますAP_SSIDAP_PASS.

マトリックスピン、サイズ、明るさ

これらの設定は前のプロジェクトと同じです。


// 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

保持するMATRIX_PINat14このボードのために。あなたは増やすことができますmatrixBrightnessもっと明るさが必要な場合でも、近くでの視聴には低い値の方が快適です。

デフォルトテキストとスクロール設定

ボードが起動すると、ウェブページを開いて新しいメッセージを入力するまで初期メッセージが表示されます。デフォルトのテキストは設定で変更できます。


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

スクロール動作の残りは、ウェブインターフェースによって更新される一群の変数によって制御されます。


// 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

ウェブページはスライダーやボタンの選択に基づいて新しい値を送信します。Arduino側では、次のことだけを知っていれば十分です:

  • 減少しているscrollDelayMsテキストをより速く動かします。
  • 増加するscrollDelayMsそれを遅く動かします。
  • 変更中scrollDirection左、右、上、または下へスクロールモードの切り替え。

テキストカラー(ウェブページから制御)

テキストの色は、3つの0-255の値(赤、緑、青)によって制御されます。新しい色をウェブページで選択するたびに更新されます。


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

ブラウザで色を選択し、「適用」をクリックすると、ESP32がRGB値を解析し、これらの3つの変数を更新します。テキストはすぐにマトリックス上で色が変わります。ビデオでは、赤、緑、青の例を含む複数の色の変化を示しています。:contentReference[oaicite:5]{index=5}

要約

プロジェクト3は、ESP32-S3 RGB LEDマトリックスを完全にワイヤレスなテキストディスプレイに変え、ウェブブラウザを持つ任意のデバイスで制御できるようにします。このスケッチは堅牢性を考慮して設計されています。

  • 最初に、設定したSSIDとパスワードを使用して、自宅のWi-Fiに接続しようとします。
  • それが失敗した場合、自動的に名前を持つアクセスポイントになります。ESP32とパスワードpassword.
  • どちらのモードでも、ブラウザで正しいIPアドレスを開くと、テキスト、色、方向、速度のための同じコントロールページが表示されます。

この記事の下に完全なHTTPテキストコードが表示されています(ウェブサイト上で自動的に読み込まれます)。テキストがリアルタイムでどのように更新されるかの詳細なステップバイステップの説明とライブデモを確認したい場合は、動画のプロジェクト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++
/*
 * プロジェクト4:HTTPテキストスクロール - ESP32-S3 RGB LEDマトリックス(Waveshare)
 * 
 * - 自宅のWiFiに接続(ステーションモード、APフォールバック付き)。
 * - 次の設定が可能なウェブページを提供:
 * テキスト
 * 色
 * ディスプレイのON/OFF
 * スクロール方向(左 / 右 / 上 / 下)
 * スクロール遅延(速度)
 * - Adafruit_NeoMatrixを使用した8×8 RGB LEDマトリックスに対応。
 * 
 * ▶️ ビデオチュートリアル:
 * https://youtu.be/JKLuYrRcLMI
 * 
 * 📄 リソース&コードページ:
 * 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設定 =========================
 // ホームWiFi(これらをあなたのルーターの認証情報に変更してください)
const char* WIFI_SSID = "Biseem";
const char* WIFI_PASS = "wan9&Jang~";

 // フォールバックアクセスポイント
const char* AP_SSID = "ESP32";
const char* AP_PASS = "password";

 // ======================= マトリックス設定 =======================

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

 // 0、1、2、または3 - テキストの向きが間違っている場合はUSBを上にして調整してください
#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
);

 // ======================= スクロール状態 =========================

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

String   scrollText  = "Robojax";
uint16_t textColor   = 0xFFFF; // デフォルトの白
bool     displayOn   = true;
ScrollDir currentDir = DIR_LEFT;

 // 1ステップあたりこのms(低いほど速い)
unsigned long scrollInterval = 80;
unsigned long lastScrollTime = 0;

 // 水平方向のスクロール位置
int scrollX = MATRIX_WIDTH;
int scrollY = 0; // 上段

 // 縦の文字ごとのスクロールの状態
int vertCharIndex = 0; // 文字列のどの文字
int vertY         = MATRIX_HEIGHT; // そのキャラクターの垂直位置

 // ======================= ウェブサーバー ============================

WebServer server(80);

 // ======================= HTML ページ ============================

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';
      });
  }

 // デフォルトをアクティブに設定
  setDir('left');
</script>
</body>
</html>
)rawliteral";

 // ======================= ヘルパー ===============================

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) {
 // "#RRGGBB" または "RRGGBB" を期待してください。
  String c = hex;
  if (c.startsWith("#")) c.remove(0, 1);
  if (c.length() != 6) {
 // デフォルトの白
    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; // デフォルトフォント ~6 px チャーごと
  int textHeight = 8; // 5x7のフォントは8に収まります。

  switch (currentDir) {
    case DIR_LEFT:
 // 右端のすぐ外側から始めてください。
      scrollX = MATRIX_WIDTH;
      scrollY = 0;
      break;

    case DIR_RIGHT:
 // 左端のすぐ外側から始めてください。
      scrollX = -textWidth;
      scrollY = 0;
      break;

    case DIR_UP:
 // 行ごとの垂直スクロール、マトリックスの下にある最初の文字から始まります
      vertCharIndex = 0;
      vertY = MATRIX_HEIGHT; // 8 → 下から入る
      break;

    case DIR_DOWN:
 // 行ごとの縦スクロール、行列の上に最初の文字を表示から始める
      vertCharIndex = 0;
      vertY = -textHeight; // -8 → 上から入る
      break;
  }
}

 // ======================= HTTP ハンドラー =========================

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();

 // シンプルな安全クランプ
    if (val < 10)   val = 10; // とても速い
    if (val > 1000) val = 1000; // 1秒マックス

    scrollInterval = (unsigned long)val;
 // スクロール間隔が設定されました;
 // Serial.print(scrollInterval);
 // Serial.println(" ミリ秒");
  }

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

 // ======================= スクロールロジック ==========================

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);

 // -------- 水平スクロール (左 / 右) --------
  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 { // 右に進む
      scrollX++;
      if (scrollX > MATRIX_WIDTH) {
        scrollX = -textWidth;
      }
    }
    return;
  }

 // -------- 縦スクロール(上 / 下) - 各文字ごと --------

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

 // 現在のキャラクター
  char c = scrollText[vertCharIndex];

  int charWidth  = 6;
  int baseX = (MATRIX_WIDTH - charWidth) / 2; // 水平方向に中央揃えする

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

  if (currentDir == DIR_UP) {
 // キャラクターを上に移動させる
    vertY--;
    if (vertY < -textHeight) {
 // このキャラクターは完全に通過しましたので、次に進んでください。
      vertY = MATRIX_HEIGHT;
      vertCharIndex++;
      if (vertCharIndex >= scrollText.length()) {
        vertCharIndex = 0; // 最初の文字に戻る
      }
    }
  } else if (currentDir == DIR_DOWN) {
 // キャラクターを下に移動させる
    vertY++;
    if (vertY > MATRIX_HEIGHT) {
 // このキャラクターは完全に通過しましたので、次に進んでください。
      vertY = -textHeight;
      vertCharIndex++;
      if (vertCharIndex >= scrollText.length()) {
        vertCharIndex = 0; // ループバック
      }
    }
  }
}

 // ======================= WIFI 初期化 =============================

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; // 10秒

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

 // ======================= セットアップ & ループ ==========================

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

 // マトリックス初期化
  matrix.begin();
  matrix.setRotation(MATRIX_ROTATION);
  matrix.setBrightness(BRIGHTNESS);
  matrix.fillScreen(0);
  matrix.show();

 // WiFi
  connectWiFi();

 // ウェブサーバールート
  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();
  }
}

必要かもしれないもの

リソースと参考文献

ファイル📁

フリッツィングファイル