SKN Craft
Published © MIT

PIXELBOARD – IoT Smart Notice Display with XIAO ESP32S3

PIXELBOARD is a Wi-Fi–enabled smart scrolling LED notice board that lets you send messages directly from your phone or laptop to a dot matri

BeginnerFull instructions provided4 hours166
PIXELBOARD – IoT Smart Notice Display with XIAO ESP32S3

Things used in this project

Hardware components

Seeed Studio XIAO ESP32S3 Sense
Seeed Studio XIAO ESP32S3 Sense
×1

Software apps and online services

Arduino IDE
Arduino IDE

Story

Read more

Schematics

schematic

Code

code

C/C++
// Smart Notice Board with XIAO ESP32S3 & Dot Matrix LED Display
#include <WiFi.h>
#include <WiFiManager.h>
#include <MD_Parola.h>
#include <MD_MAX72xx.h>
#include <SPI.h>

// -------------------- Debug --------------------
#define DEBUG 0
#if DEBUG
  #define PRINT(s, x)  { Serial.print(F(s)); Serial.print(x); }
  #define PRINTS(x)    Serial.print(F(x))
  #define PRINTX(x)    Serial.println(x, HEX)
#else
  #define PRINT(s, x)
  #define PRINTS(x)
  #define PRINTX(x)
#endif

// -------------------- LED Matrix Config --------------------
// Use software SPI so we can choose any pins on XIAO ESP32S3
#define HARDWARE_TYPE MD_MAX72XX::FC16_HW
#define MAX_DEVICES   4

// XIAO ESP32S3 pin mapping (adjust if you wired differently)
#define DATA_PIN 10   // D10 -> DIN
#define CLK_PIN   8   // D8  -> CLK
#define CS_PIN    9   // D9  -> CS (LOAD)

// SOFTWARE SPI (data, clk, cs, numDevices)
MD_Parola P = MD_Parola(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, MAX_DEVICES);

// -------------------- WiFi / Web --------------------
WiFiServer server(80);

// Scrolling parameters
uint8_t frameDelay = 25;                 // default speed
textEffect_t scrollEffect = PA_SCROLL_LEFT;

// Global message buffers shared by WiFi and scrolling
#define BUF_SIZE 512
char curMessage[BUF_SIZE];
char newMessage[BUF_SIZE];
bool newMessageAvailable = false;

// Proper HTTP header (CRLF line endings)
const char WebResponse[] =
  "HTTP/1.1 200 OK\r\n"
  "Content-Type: text/html\r\n"
  "Connection: close\r\n"
  "\r\n";

const char WebPage[] =
"<!DOCTYPE html>\n"
"<html>\n"
"<head>\n"
"<meta name='viewport' content='width=device-width, initial-scale=1'>\n"
"<title>Smart Notice Board</title>\n"
"<script>\n"
"let strLine='';\n"
"function SendData(){\n"
"  const nocache='/?nocache=' + Math.random()*1000000;\n"
"  const f=document.getElementById('data_form');\n"
"  strLine='&MSG=' + encodeURIComponent(f.Message.value);\n"
"  strLine += '/&SD=' + f.ScrollType.value;\n"
"  strLine += '/&I=' + f.Invert.value;\n"
"  strLine += '/&SP=' + f.Speed.value;\n"
"  const request=new XMLHttpRequest();\n"
"  request.open('GET', strLine + nocache, false);\n"
"  request.send(null);\n"
"}\n"
"</script>\n"
"<style>\n"
"  body{font-family:Arial,sans-serif;margin:10px;padding:10px;text-align:center}\n"
"  .container{max-width:600px;margin:0 auto}\n"
"  p{font-size:30px;margin-top:10px}\n"
"  form{margin-top:10px;display:inline-block;text-align:left;width:60%}\n"
"  input[type='text']{padding:10px;font-size:16px;width:100%;margin-bottom:20px;box-sizing:border-box}\n"
"  label{font-size:16px;margin-bottom:10px;display:block}\n"
"  input[type='radio']{margin-right:10px}\n"
"  input[type='submit']{padding:10px 20px;font-size:18px;background:#3F51B5;color:#fff;border:0;margin-top:5px;cursor:pointer}\n"
"  @media(max-width:600px){form{width:100%}}\n"
"</style>\n"
"</head>\n"
"<body>\n"
"<div class='container'>\n"
"  <p><b>Smart Notice Board with XIAO ESP32S3</b></p>\n"
"  <h3><b><a href='https://iotprojectsideas.com'>https://iotprojectsideas.com</a></b></h3>\n"
"  <form id='data_form' name='frmText' onsubmit='SendData();return false;'>\n"
"    <label>Message:<br><input type='text' name='Message' maxlength='255'></label>\n"
"    <br>\n"
"    <label>Invert:</label>\n"
"    <input type='radio' name='Invert' value='0' checked> Normal\n"
"    <input type='radio' name='Invert' value='1'> Inverse\n"
"    <br><br>\n"
"    <label>Scroll Type:</label>\n"
"    <input type='radio' name='ScrollType' value='L' checked> Left\n"
"    <input type='radio' name='ScrollType' value='R'> Right\n"
"    <br><br>\n"
"    <label>Speed:</label><br>\n"
"    <div style='display:flex;justify-content:space-between'>\n"
"      <span>Fast</span><span>Slow</span>\n"
"    </div>\n"
"    <input type='range' name='Speed' min='10' max='200' value='25'>\n"
"    <br><br>\n"
"    <input type='submit' value='Send Data'>\n"
"  </form>\n"
"</div>\n"
"</body>\n"
"</html>\n";

// -------------------- Helpers --------------------
const char* err2Str(wl_status_t code) {
  switch (code) {
    case WL_IDLE_STATUS:    return "IDLE";
    case WL_NO_SSID_AVAIL:  return "NO_SSID_AVAIL";
    case WL_CONNECTED:      return "CONNECTED";
    case WL_CONNECT_FAILED: return "CONNECT_FAILED";
    case WL_DISCONNECTED:   return "DISCONNECTED";
    default: return "??";
  }
}

uint8_t htoi(char c) {
  c = toupper(c);
  if (c >= '0' && c <= '9') return (c - '0');
  if (c >= 'A' && c <= 'F') return (c - 'A' + 0xA);
  return 0;
}

// Parse GET data:
//  /&MSG=   (URL-encoded)
//  /&SD=    L or R
//  /&I=     0 or 1
//  /&SP=    10..200
void getData(char *szMesg, uint16_t len) {
  char *pStart, *pEnd;

  // Message
  pStart = strstr(szMesg, "/&MSG=");
  if (pStart != NULL) {
    char *psz = newMessage;
    pStart += 6;
    pEnd = strstr(pStart, "/&");
    if (pEnd != NULL) {
      while (pStart != pEnd) {
        if ((*pStart == '%') && isxdigit(*(pStart + 1)) && isxdigit(*(pStart + 2))) {
          char c = 0;
          pStart++;
          c += (htoi(*pStart++) << 4);
          c += htoi(*pStart++);
          *psz++ = c;
        } else if (*pStart == '+') {
          *psz++ = ' ';
          pStart++;
        } else {
          *psz++ = *pStart++;
        }
      }
      *psz = '\0';
      newMessageAvailable = (strlen(newMessage) != 0);
      PRINT("\nNew Msg: ", newMessage);
    }
  }

  // Scroll direction
  pStart = strstr(szMesg, "/&SD=");
  if (pStart != NULL) {
    pStart += 5;
    PRINT("\nScroll direction: ", *pStart);
    scrollEffect = (*pStart == 'R' ? PA_SCROLL_RIGHT : PA_SCROLL_LEFT);
    P.setTextEffect(scrollEffect, scrollEffect);
    P.displayReset();
  }

  // Invert
  pStart = strstr(szMesg, "/&I=");
  if (pStart != NULL) {
    pStart += 4;
    PRINT("\nInvert mode: ", *pStart);
    P.setInvert(*pStart == '1');
  }

  // Speed
  pStart = strstr(szMesg, "/&SP=");
  if (pStart != NULL) {
    pStart += 5;
    int16_t speed = atoi(pStart);
    if (speed < 10) speed = 10;
    if (speed > 200) speed = 200;
    PRINT("\nSpeed: ", speed);
    P.setSpeed(speed);
    frameDelay = speed;
  }
}

void handleWiFi(void) {
  static enum { S_IDLE, S_WAIT_CONN, S_READ, S_EXTRACT, S_RESPONSE, S_DISCONN } state = S_IDLE;
  static char szBuf[1024];
  static uint16_t idxBuf = 0;
  static WiFiClient client;
  static uint32_t timeStart;

  switch (state) {
    case S_IDLE:
      idxBuf = 0;
      state = S_WAIT_CONN;
      break;

    case S_WAIT_CONN: {
      client = server.available();
      if (!client) break;
      if (!client.connected()) break;
      timeStart = millis();
      state = S_READ;
    } break;

    case S_READ:
      while (client.available()) {
        char c = client.read();
        if (c == '\r' || c == '\n') {
          szBuf[idxBuf] = '\0';
          while (client.available()) client.read(); // flush remaining
          PRINT("\nRecv: ", szBuf);
          state = S_EXTRACT;
          break;
        } else if (idxBuf < sizeof(szBuf) - 1) {
          szBuf[idxBuf++] = c;
        }
      }
      if (millis() - timeStart > 1000) {
        state = S_DISCONN;
      }
      break;

    case S_EXTRACT:
      getData(szBuf, BUF_SIZE);
      state = S_RESPONSE;
      break;

    case S_RESPONSE:
      client.print(WebResponse);
      client.print(WebPage);
      state = S_DISCONN;
      break;

    case S_DISCONN:
      client.stop();
      state = S_IDLE;
      break;

    default:
      state = S_IDLE;
  }
}

// -------------------- Setup / Loop --------------------
void setup() {
  Serial.begin(115200);
  delay(200);

  // Matrix init
  P.begin();
  P.setIntensity(0);
  P.displayClear();
  P.displaySuspend(false);
  curMessage[0] = '\0';
  newMessage[0] = '\0';
  P.displayScroll(curMessage, PA_LEFT, scrollEffect, frameDelay);

  // WiFi portal (AP shows as "SmartNoticeBoard")
  WiFiManager wifiManager;
  wifiManager.autoConnect("SmartNoticeBoard");

  // Start server
  server.begin();
  PRINTS("\nServer started");

  // Show IP address as first message
  sprintf(curMessage, "%03d:%03d:%03d:%03d",
          WiFi.localIP()[0], WiFi.localIP()[1],
          WiFi.localIP()[2], WiFi.localIP()[3]);
  PRINT("\nAssigned IP ", curMessage);
}

void loop() {
  handleWiFi();

  if (P.displayAnimate()) {
    if (newMessageAvailable) {
      strcpy(curMessage, newMessage);
      newMessageAvailable = false;
    }
    // Apply current scroll effect and speed each cycle
    P.setTextEffect(scrollEffect, scrollEffect);
    P.setSpeed(frameDelay);
    P.displayReset();
  }
}

Credits

SKN Craft
2 projects • 1 follower
Teacher

Comments