This ESP8266-based project runs a simple web server with 1 page to control an 8×8 LED monochrome matrix. It is powered by USB, and portable, as it uses the WiFiManager library to connect to any available wifi network. It allows you to control the LED matrix from a browser on a phone, tablet, laptop.

IMG_20180508_135301 - Edited

The web single page looks as below and allows to:

  • create an 8×8 binary image by toggling the pixel on/off
  • save/load 1 image, stored in permanent EEPROM
  • input text to be scrolled on the matrix, with a slider for speed
  • show a binary-coded decimal clock, using NTP
  • a slider for the overall brightness (not visible on below screenshot)

Screenshot_20180508-140706

The hardware is fairly simple:

  • an ESP8266 in NodeMCU v1.0 package, programmed with the Arduino IDE
  • 8×8 LED matrix with MAX7219 driver
  • an LED to show the wifi connection status
  • a USB power cord

IMG_20180508_140423 - Edited

The code is a bit more work, combining several elements of other people’s work:

Here’s a little video that shows the startup. It displays the image previously saved in EEPROM, then attempts to connect to the last knows wifi network. When not found, the yellow LED remains on, and it launches an access point to setup the connection (WiFiManager).

Once it connects to the wifi network, it shows a welcome message, and the IP address of the web server. Then briefly the saved image, and then the BCD clock, showing the time 13:54

Below picture shows time 14:38 (ignore the first 2 rows and columns, and the last 2 columns: the dots there indicate the rows and columns that show the actual data)

  • row 3 shows 0001 = 1
  • row 4 shows 0100 = 4
  • row 6 shows 0011 = 3
  • row 7 shows 1000 = 8

IMG_20180508_143858 - Edited

Here is the code:

/*
  BuffaloLabs 8x8 LED matrix
  March 2018
  using LedControl library
  fonts.h has font characters and digits
  wifi manager to config wifi
  web server to create 8x8 image
  showing network time in BCD
  dht11 sensor
   
*/
#include <ESP8266WiFi.h>
//////////////////// WIFI MANAGER ////////////////////////////
#include <DNSServer.h> //needed for wifimanager library
#include <ESP8266WebServer.h> //needed for wifimanager library
#include <WiFiManager.h>         //https://github.com/tzapu/WiFiManager
///////////////////  WEB SERVER  /////////////////////////////
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
ESP8266WebServer server(80);
#include "webpage.h"
///////////////////////////////////  NTP  ////////////////////
#include <TimeLib.h>
#include <WiFiUdp.h>
unsigned int localPort = 8888;      // local port to listen for UDP packets
static const char ntpServerName[] = "us.pool.ntp.org";
const int timeZone = 8;     // Asia HK
const int NTP_PACKET_SIZE = 48; // NTP time stamp is in the first 48 bytes of the message
byte packetBuffer[ NTP_PACKET_SIZE]; //buffer to hold incoming and outgoing packets
WiFiUDP Udp;  // A UDP instance to let us send and receive packets over UDP
int prevHour; // when the clock was displayed
int prevMin;
boolean showClock = false;   // display clock or not
boolean showSaved = true;   // display saved matrix or not
unsigned long timestamp_clock = 0;
///////////////////// DHT11 /////////////////////////////
#include <DHT.h>                    // power from USB 5V seems to give more accurate results
#define DHTTYPE DHT11
#define DHTPIN  0   // = D3 on NodeMCU
DHT dht(DHTPIN, DHTTYPE, 11); // 11 works fine for ESP8266
int DHTtemp;    // sensor returns no decimals, better use int
int DHThum;
unsigned long timestamp_dht = 0;

////////////////////  MATRIX  ////////////////////////////////
#include "fonts.h"
#include <LedControl.h>
int DIN = 14; // D5
int CS =  5;  // D1
int CLK = 4;  // D2
LedControl lc = LedControl(DIN, CLK, CS, 0);
String last_matrix = "0000000000000000000000000000000000000000000000000000000000000000";  // to remember matrix
#include <EEPROM.h>
int eeprom_addr = 0;

///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////

void setup() {

  Serial.begin(115200);
  Serial.println();
  Serial.println("LED MATRIX web server");
  EEPROM.begin(512);

  lc.shutdown(0, false);      //The MAX72XX is in power-saving mode on startup
  //  lc.setScanLimit(0, 8);    // necessary??
  lc.setIntensity(0, 3);     // Set the brightness to maximum value  0-15
  lc.clearDisplay(0);         // and clear the display

  displaySaved();       // display last saved matrix

  pinMode(LED_BUILTIN, OUTPUT);       // blink onboard LED
  for (int i = 0; i < 10; i++) {
    digitalWrite(LED_BUILTIN, LOW);
    delay(100);
    digitalWrite(LED_BUILTIN, HIGH);
    delay(100);
  }

  /////////////// CONFIGURE WIFI ////////////////////////////
  Serial.println("configure wifi manager...");
  WiFiManager wifiManager;
  //  wifiManager.resetSettings();  // known networks are saved so need to reset for testing
  wifiManager.autoConnect("matrix");
  digitalWrite(LED_BUILTIN, LOW);   // inverted pin - LOW is on
  Serial.println("connected to wifi!");

  //////////////  START NTP  //////////////////////////////
  Serial.print("Starting UDP");
  Udp.begin(localPort);
  Serial.print(" on local port: ");
  Serial.println(Udp.localPort());
  setSyncProvider(getNtpTime);
  setSyncInterval(300);

  ////////////// CONFIGURE SERVER ///////////////////////////
  server.on("/", handleRoot);
  server.begin();
  Serial.print("HTTP server started on ");
  Serial.println(WiFi.localIP());

  scrollText("BuffaloLabs IP:" + WiFi.localIP().toString(), 50);

  displaySaved();       // display last saved matrix
  dht.readHumidity();  // first reading seems wrong
  delay(3000);        // for DHT

}

///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////

void loop() {

  server.handleClient();

  if (millis() - timestamp_dht > 10000) {            // update reading every 10s
    int h = dht.readHumidity();
    int t = dht.readTemperature();
    if (h>0 &&  h<=100) DHThum = h;
    if (t>-20 && t<100) DHTtemp = t;
    Serial.println("Temp: " + String(DHTtemp) + " Hum: " + String(DHThum));    
    timestamp_dht = millis();
  }

  if (timeStatus() != timeNotSet && (showClock || showSaved)) {    // if time is set and we're not in draw mode
    if (hour() != prevHour || minute() != prevMin) { //update the display only if time has changed
      prevHour = hour();
      prevMin = minute();
      String HMM = String(prevHour);
      HMM += ":";
      if (prevMin < 10) HMM += "0";
      HMM += prevMin;
      scrollText(HMM + " T:" + String(DHTtemp) + "`C H:" + String(DHThum) + "%", 50);      
      if (showClock) displayClockBCD();
      if (showSaved) displaySaved();
    }
  }
  if (timeStatus() == timeNotSet) {
    setSyncProvider(getNtpTime);
  }

}

///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////

void printByte(const byte character [])
{
  int i = 0;
  for (i = 0; i < 8; i++)
  {
    lc.setRow(0, i, character[i]);
  }
}

/////////////////////////////////////////////////////////////////////////

void printInt(int character [])
{
  int i = 0;
  for (i = 0; i < 8; i++)
  {
    lc.setRow(0, i, character[i]);
  }
}

/////////////////////////////////////////////////////////////////////////

void printFont(const byte character[] )
{
  int i = 0;
  for (i = 0; i < 8; i++)
  {
    lc.setRow(0, i, flipByte(character [i]));
  }
}

/////////////////////////////////////////////////////////////////////////

byte flipByte(byte c) {
  char r = 0;
  for (byte i = 0; i < 8; i++) {
    r <<= 1;
    r |= c & 1;
    c >>= 1;
  }
  return r;
}

/////////////////////////////////////////////////////////////////////////

void handleRoot() {
  server.send(200, "text/html", webpage_html);

  String data = server.arg(0);

  if (server.argName(0) == "matrix") {
    displayMatrix(data);
    last_matrix = data;
    Serial.println("received data and updated the matrix");
    showClock = false;
    showSaved = false;
  }

  if (server.argName(0) == "intensity") {
    lc.setIntensity(0, data.toInt());     // Set the brightness to maximum value  0-15
    Serial.print("updated intensity to ");
    Serial.println(data);
  }

  if (server.argName(0) == "text") {
    Serial.println("scrolling text: " + data);
    int d = map(server.arg(1).toInt(), 1, 10, 200, 20);
    scrollText(data, d);
    if (!showClock) {
      displayMatrix(last_matrix);
    } else {
      displayClockBCD();
    }
  }

  if (server.argName(0) == "save") {
    Serial.println("saving matrix");
    for (int u = 0; u < 8; u++) { // for every row, construct 1 byte
      byte row = 0;
      for (int i = 0; i < 8; i++) {
        bitWrite(row, 7 - i, last_matrix.charAt(8 * u + i) - 48);
      }
      //      Serial.println(row);
      EEPROM.write(eeprom_addr + u, row);
    }
    EEPROM.commit();
  }

  if (server.argName(0) == "load") {
    Serial.println("loading matrix");
    displaySaved();
    showClock = false;
    showSaved = true;
  }

  if (server.argName(0) == "clock") {
    Serial.println("showing clock");
    showClock = true;
    showSaved = false;
    displayClockBCD();
  }

}

/////////////////////////////////////////////////////////////////////////

void displayMatrix(String t) {
  for (int i = 0; i < 64; i++) {
    lc.setLed(0, i / 8, i % 8, t.charAt(i) - 48);
  }
}

/////////////////////////////////////////////////////////////////////////

void displaySaved() {
  last_matrix = "";
  byte a[8];
  for (int u = 0; u < 8; u++) {
    a[u] = EEPROM.read(eeprom_addr + u);
    for (int i = 0; i < 8; i++) {
      last_matrix += bitRead(a[u], 7 - i);
    }
  }
  printByte(a);
}

/////////////////////////////////////////////////////////////////////////

void scrollText(String t, int s) {   // text and speed
  int l = t.length() + 1;  // store length
  t = " " + t + " ";             // add space before and after
  for (int i = 0; i <= l * 8; i++) { // index of moving window over 2 chars
    int this_frame[8] = {0, 0, 0, 0, 0, 0, 0, 0};  // define as int holding 2 chars (2x byte)
    for (int u = 0; u < 8; u++) { // for each row
      int left = (int) flipByte(font[t.charAt(i / 8)] [u]);  // left char
      int right = (int) flipByte(font[t.charAt(i / 8 + 1)] [u]);  // right char
      this_frame[u] = (left << 8) | right;  // combine chars to 16 bits
      this_frame[u] = this_frame[u] << i % 8; // move window to very left
      this_frame[u] = this_frame[u] >> 8;    // move result to very right
      this_frame[u] = this_frame[u] & 0x00FF;  // mask result to byte on right
    }
    printInt(this_frame);
    delay(s);
  }
}


/////////////////////////////////////////////////////////////////////////

time_t getNtpTime()
{
  IPAddress ntpServerIP; // NTP server's ip address

  while (Udp.parsePacket() > 0) ; // discard any previously received packets
  Serial.println("Transmit NTP Request");
  // get a random server from the pool
  WiFi.hostByName(ntpServerName, ntpServerIP);
  Serial.print(ntpServerName);
  Serial.print(": ");
  Serial.println(ntpServerIP);
  sendNTPpacket(ntpServerIP);
  uint32_t beginWait = millis();
  while (millis() - beginWait < 2500) {
    int size = Udp.parsePacket();
    if (size >= NTP_PACKET_SIZE) {
      Serial.println("Receive NTP Response");
      Udp.read(packetBuffer, NTP_PACKET_SIZE);  // read packet into the buffer
      unsigned long secsSince1900;
      // convert four bytes starting at location 40 to a long integer
      secsSince1900 =  (unsigned long)packetBuffer[40] << 24;
      secsSince1900 |= (unsigned long)packetBuffer[41] << 16;
      secsSince1900 |= (unsigned long)packetBuffer[42] << 8;
      secsSince1900 |= (unsigned long)packetBuffer[43];
      return secsSince1900 - 2208988800UL + timeZone * SECS_PER_HOUR;
    }
  }
  Serial.println("No NTP Response :-(");
  return 0; // return 0 if unable to get the time
}

/////////////////////////////////////////////////////////////////////////

// send an NTP request to the time server at the given address
void sendNTPpacket(IPAddress &address)
{
  // set all bytes in the buffer to 0
  memset(packetBuffer, 0, NTP_PACKET_SIZE);
  // Initialize values needed to form NTP request
  // (see URL above for details on the packets)
  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  packetBuffer[1] = 0;     // Stratum, or type of clock
  packetBuffer[2] = 6;     // Polling Interval
  packetBuffer[3] = 0xEC;  // Peer Clock Precision
  // 8 bytes of zero for Root Delay & Root Dispersion
  packetBuffer[12] = 49;
  packetBuffer[13] = 0x4E;
  packetBuffer[14] = 49;
  packetBuffer[15] = 52;
  // all NTP fields have been given values, now
  // you can send a packet requesting a timestamp:
  Udp.beginPacket(address, 123); //NTP requests are to port 123
  Udp.write(packetBuffer, NTP_PACKET_SIZE);
  Udp.endPacket();
}

//////////////////////////////////////////////////////////////////////////////////

void displayClockBCD()
{
  Serial.print(hour());
  printDigits(minute());
  printDigits(second());
  Serial.print(" ");
  Serial.print(day());
  Serial.print(".");
  Serial.print(month());
  Serial.print(".");
  Serial.print(year());
  Serial.println();

  byte a[8];

  a[0] = B10100000;
  a[1] = B11100000 | (hour() / 10);
  a[2] = B10100000 | (hour() % 10);
  a[3] = B00000000;
  a[4] = B00000000;
  a[5] = B11100000 | (minute() / 10);
  a[6] = B10100000 | (minute() % 10);
  a[7] = B00000000;

  printByte(a);

  /*
    matrix.fillScreen(GREEN);

    matrix.drawPixel(4, 1, RED * bitRead(hour() / 10, 1));
    matrix.drawPixel(5, 1, RED * bitRead(hour() / 10, 0));

    matrix.drawPixel(2, 2, RED * bitRead(hour() % 10, 3));
    matrix.drawPixel(3, 2, RED * bitRead(hour() % 10, 2));
    matrix.drawPixel(4, 2, RED * bitRead(hour() % 10, 1));
    matrix.drawPixel(5, 2, RED * bitRead(hour() % 10, 0));

    matrix.drawPixel(3, 5, RED * bitRead(minute() / 10, 2));
    matrix.drawPixel(4, 5, RED * bitRead(minute() / 10, 1));
    matrix.drawPixel(5, 5, RED * bitRead(minute() / 10, 0));

    matrix.drawPixel(2, 6, RED * bitRead(minute() % 10, 3));
    matrix.drawPixel(3, 6, RED * bitRead(minute() % 10, 2));
    matrix.drawPixel(4, 6, RED * bitRead(minute() % 10, 1));
    matrix.drawPixel(5, 6, RED * bitRead(minute() % 10, 0));
    matrix.show();
  */
}

//////////////////////////////////////////////////////////////////////////////////

void printDigits(int digits)
{
  Serial.print(":");
  if (digits < 10)
    Serial.print('0');
  Serial.print(digits);
}

and the fonts.h


const byte font[128][8] = {  // http://arduino-er.blogspot.hk/2014/08/port-ascii-font-to-arduino-88-led-matrix.html
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0000 (nul)
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0001
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0002
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0003
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0004
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0005
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0006
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0007
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0008
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0009
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+000A
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+000B
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+000C
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+000D
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+000E
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+000F
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0010
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0011
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0012
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0013
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0014
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0015
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0016
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0017
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0018
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0019
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+001A
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+001B
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+001C
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+001D
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+001E
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+001F
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0020 (space)
  { 0x18, 0x3C, 0x3C, 0x18, 0x18, 0x00, 0x18, 0x00},   // U+0021 (!)
  { 0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0022 (")
  { 0x36, 0x36, 0x7F, 0x36, 0x7F, 0x36, 0x36, 0x00},   // U+0023 (#)
  { 0x0C, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x0C, 0x00},   // U+0024 ($)
  { 0x00, 0x63, 0x33, 0x18, 0x0C, 0x66, 0x63, 0x00},   // U+0025 (%)
  { 0x1C, 0x36, 0x1C, 0x6E, 0x3B, 0x33, 0x6E, 0x00},   // U+0026 (&)
  { 0x06, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+0027 (')
  { 0x18, 0x0C, 0x06, 0x06, 0x06, 0x0C, 0x18, 0x00},   // U+0028 (()
  { 0x06, 0x0C, 0x18, 0x18, 0x18, 0x0C, 0x06, 0x00},   // U+0029 ())
  { 0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00},   // U+002A (*)
  { 0x00, 0x0C, 0x0C, 0x3F, 0x0C, 0x0C, 0x00, 0x00},   // U+002B (+)
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x06},   // U+002C (,)
  { 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00},   // U+002D (-)
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00},   // U+002E (.)
  { 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x00},   // U+002F (/)
  { 0x3E, 0x63, 0x73, 0x7B, 0x6F, 0x67, 0x3E, 0x00},   // U+0030 (0)
  { 0x0C, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x3F, 0x00},   // U+0031 (1)
  { 0x1E, 0x33, 0x30, 0x1C, 0x06, 0x33, 0x3F, 0x00},   // U+0032 (2)
  { 0x1E, 0x33, 0x30, 0x1C, 0x30, 0x33, 0x1E, 0x00},   // U+0033 (3)
  { 0x38, 0x3C, 0x36, 0x33, 0x7F, 0x30, 0x78, 0x00},   // U+0034 (4)
  { 0x3F, 0x03, 0x1F, 0x30, 0x30, 0x33, 0x1E, 0x00},   // U+0035 (5)
  { 0x1C, 0x06, 0x03, 0x1F, 0x33, 0x33, 0x1E, 0x00},   // U+0036 (6)
  { 0x3F, 0x33, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x00},   // U+0037 (7)
  { 0x1E, 0x33, 0x33, 0x1E, 0x33, 0x33, 0x1E, 0x00},   // U+0038 (8)
  { 0x1E, 0x33, 0x33, 0x3E, 0x30, 0x18, 0x0E, 0x00},   // U+0039 (9)
  { 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00},   // U+003A (:)
  { 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x06},   // U+003B (//)
  { 0x18, 0x0C, 0x06, 0x03, 0x06, 0x0C, 0x18, 0x00},   // U+003C (<)
  { 0x00, 0x00, 0x3F, 0x00, 0x00, 0x3F, 0x00, 0x00},   // U+003D (=)
  { 0x06, 0x0C, 0x18, 0x30, 0x18, 0x0C, 0x06, 0x00},   // U+003E (>)
  { 0x1E, 0x33, 0x30, 0x18, 0x0C, 0x00, 0x0C, 0x00},   // U+003F (?)
  { 0x3E, 0x63, 0x7B, 0x7B, 0x7B, 0x03, 0x1E, 0x00},   // U+0040 (@)
  { 0x0C, 0x1E, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x00},   // U+0041 (A)
  { 0x3F, 0x66, 0x66, 0x3E, 0x66, 0x66, 0x3F, 0x00},   // U+0042 (B)
  { 0x3C, 0x66, 0x03, 0x03, 0x03, 0x66, 0x3C, 0x00},   // U+0043 (C)
  { 0x1F, 0x36, 0x66, 0x66, 0x66, 0x36, 0x1F, 0x00},   // U+0044 (D)
  { 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x46, 0x7F, 0x00},   // U+0045 (E)
  { 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x06, 0x0F, 0x00},   // U+0046 (F)
  { 0x3C, 0x66, 0x03, 0x03, 0x73, 0x66, 0x7C, 0x00},   // U+0047 (G)
  { 0x33, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x33, 0x00},   // U+0048 (H)
  { 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00},   // U+0049 (I)
  { 0x78, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E, 0x00},   // U+004A (J)
  { 0x67, 0x66, 0x36, 0x1E, 0x36, 0x66, 0x67, 0x00},   // U+004B (K)
  { 0x0F, 0x06, 0x06, 0x06, 0x46, 0x66, 0x7F, 0x00},   // U+004C (L)
  { 0x63, 0x77, 0x7F, 0x7F, 0x6B, 0x63, 0x63, 0x00},   // U+004D (M)
  { 0x63, 0x67, 0x6F, 0x7B, 0x73, 0x63, 0x63, 0x00},   // U+004E (N)
  { 0x1C, 0x36, 0x63, 0x63, 0x63, 0x36, 0x1C, 0x00},   // U+004F (O)
  { 0x3F, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x0F, 0x00},   // U+0050 (P)
  { 0x1E, 0x33, 0x33, 0x33, 0x3B, 0x1E, 0x38, 0x00},   // U+0051 (Q)
  { 0x3F, 0x66, 0x66, 0x3E, 0x36, 0x66, 0x67, 0x00},   // U+0052 (R)
  { 0x1E, 0x33, 0x07, 0x0E, 0x38, 0x33, 0x1E, 0x00},   // U+0053 (S)
  { 0x3F, 0x2D, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00},   // U+0054 (T)
  { 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0x00},   // U+0055 (U)
  { 0x33, 0x33, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00},   // U+0056 (V)
  { 0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00},   // U+0057 (W)
  { 0x63, 0x63, 0x36, 0x1C, 0x1C, 0x36, 0x63, 0x00},   // U+0058 (X)
  { 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x0C, 0x1E, 0x00},   // U+0059 (Y)
  { 0x7F, 0x63, 0x31, 0x18, 0x4C, 0x66, 0x7F, 0x00},   // U+005A (Z)
  { 0x1E, 0x06, 0x06, 0x06, 0x06, 0x06, 0x1E, 0x00},   // U+005B ([)
  { 0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x40, 0x00},   // U+005C (\)
  { 0x1E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1E, 0x00},   // U+005D (])
  { 0x08, 0x1C, 0x36, 0x63, 0x00, 0x00, 0x00, 0x00},   // U+005E (^)
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF},   // U+005F (_)
  { B00011000, B00100100, B00100100, B00011000, 0x00, 0x00, 0x00, 0x00},   // U+0060 (`)   // edited to degrees symbol
  { 0x00, 0x00, 0x1E, 0x30, 0x3E, 0x33, 0x6E, 0x00},   // U+0061 (a)
  { 0x07, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x3B, 0x00},   // U+0062 (b)
  { 0x00, 0x00, 0x1E, 0x33, 0x03, 0x33, 0x1E, 0x00},   // U+0063 (c)
  { 0x38, 0x30, 0x30, 0x3e, 0x33, 0x33, 0x6E, 0x00},   // U+0064 (d)
  { 0x00, 0x00, 0x1E, 0x33, 0x3f, 0x03, 0x1E, 0x00},   // U+0065 (e)
  { 0x1C, 0x36, 0x06, 0x0f, 0x06, 0x06, 0x0F, 0x00},   // U+0066 (f)
  { 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x1F},   // U+0067 (g)
  { 0x07, 0x06, 0x36, 0x6E, 0x66, 0x66, 0x67, 0x00},   // U+0068 (h)
  { 0x0C, 0x00, 0x0E, 0x0C, 0x0C, 0x0C, 0x1E, 0x00},   // U+0069 (i)
  { 0x30, 0x00, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E},   // U+006A (j)
  { 0x07, 0x06, 0x66, 0x36, 0x1E, 0x36, 0x67, 0x00},   // U+006B (k)
  { 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00},   // U+006C (l)
  { 0x00, 0x00, 0x33, 0x7F, 0x7F, 0x6B, 0x63, 0x00},   // U+006D (m)
  { 0x00, 0x00, 0x1F, 0x33, 0x33, 0x33, 0x33, 0x00},   // U+006E (n)
  { 0x00, 0x00, 0x1E, 0x33, 0x33, 0x33, 0x1E, 0x00},   // U+006F (o)
  { 0x00, 0x00, 0x3B, 0x66, 0x66, 0x3E, 0x06, 0x0F},   // U+0070 (p)
  { 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x78},   // U+0071 (q)
  { 0x00, 0x00, 0x3B, 0x6E, 0x66, 0x06, 0x0F, 0x00},   // U+0072 (r)
  { 0x00, 0x00, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x00},   // U+0073 (s)
  { 0x08, 0x0C, 0x3E, 0x0C, 0x0C, 0x2C, 0x18, 0x00},   // U+0074 (t)
  { 0x00, 0x00, 0x33, 0x33, 0x33, 0x33, 0x6E, 0x00},   // U+0075 (u)
  { 0x00, 0x00, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00},   // U+0076 (v)
  { 0x00, 0x00, 0x63, 0x6B, 0x7F, 0x7F, 0x36, 0x00},   // U+0077 (w)
  { 0x00, 0x00, 0x63, 0x36, 0x1C, 0x36, 0x63, 0x00},   // U+0078 (x)
  { 0x00, 0x00, 0x33, 0x33, 0x33, 0x3E, 0x30, 0x1F},   // U+0079 (y)
  { 0x00, 0x00, 0x3F, 0x19, 0x0C, 0x26, 0x3F, 0x00},   // U+007A (z)
  { 0x38, 0x0C, 0x0C, 0x07, 0x0C, 0x0C, 0x38, 0x00},   // U+007B ({)
  { 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00},   // U+007C (|)
  { 0x07, 0x0C, 0x0C, 0x38, 0x0C, 0x0C, 0x07, 0x00},   // U+007D (})
  { 0x6E, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},   // U+007E (~)
  { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}    // U+007F
};

const byte digits[10][8] = {    // from http://xantorohara.github.io/led-matrix-editor/#
  {
    B00111000,
    B01000100,
    B01000100,
    B01000100,
    B01000100,
    B01000100,
    B00111000,
    B00000000
  }, {
    B00001000,
    B00011000,
    B00001000,
    B00001000,
    B00001000,
    B00001000,
    B00011100,
    B00000000
  }, {
    B00111000,
    B01000100,
    B00000100,
    B00001000,
    B00010000,
    B00100000,
    B01111100,
    B00000000
  }, {
    B00111000,
    B01000100,
    B00000100,
    B00011000,
    B00000100,
    B01000100,
    B00111000,
    B00000000
  }, {
    B00000100,
    B00001100,
    B00010100,
    B00100100,
    B01111100,
    B00000100,
    B00000100,
    B00000000
  }, {
    B01111100,
    B01000000,
    B01111000,
    B00000100,
    B00000100,
    B01000100,
    B00111000,
    B00000000
  }, {
    B00111000,
    B01000100,
    B01000000,
    B01111000,
    B01000100,
    B01000100,
    B00111000,
    B00000000
  }, {
    B01111100,
    B00000100,
    B00000100,
    B00001000,
    B00010000,
    B00100000,
    B00100000,
    B00000000
  }, {
    B00111000,
    B01000100,
    B01000100,
    B00111000,
    B01000100,
    B01000100,
    B00111000,
    B00000000
  }, {
    B00111000,
    B01000100,
    B01000100,
    B00111100,
    B00000100,
    B01000100,
    B00111000,
    B00000000
  }
};

and the webpage.h

/*
   inspired by www.pial.net 8*8 dot matrix font generator
*/


const char webpage_html[] PROGMEM = R"=====(

<!DOCTYPE html>
<html lang="en-US">
<style>
  body {
    font-family: arial;
    font-size: 20px;
    background-color:#eeeeee;
  }
  div {
    background-color:#cccccc; 
    padding: 5px; 
    border: 5px solid #eeeeee;
  }
  input {
    font-size: 20px;
  }
  .slider {
    -webkit-appearance: none;
    width: 300px;
    height: 25px;
    background: #bbbbbb;
    outline: none;
    opacity: 0.7;
    -webkit-transition: .2s;
    transition: opacity .2s;
   }
   .slider:hover {
    opacity: 1;
   }
  .slider::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 25px;
    height: 25px;
    background: #888888;
    cursor: pointer;
  }
  .slider::-moz-range-thumb {
    width: 25px;
    height: 25px;
    background: #888888;
    cursor: pointer;
  }
</style>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>BuffaloLabs 8x8 LED matrix</title>
</head>

<body>
<div style="background-color:#888888">BuffaloLabs 8x8 LED matrix</div>

<div>
<table style="cursor: pointer; width: 240px; height: 240px;" border="0" cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td id="1" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="2" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="3" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="4" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="5" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="6" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="7" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="8" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
</tr>
<tr>
<td id="9" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="10" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="11" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="12" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="13" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="14" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="15" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="16" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
</tr>
<tr>
<td id="17" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="18" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="19" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="20" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="21" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="22" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="23" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="24" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
</tr>
<tr>
<td id="25" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="26" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="27" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="28" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="29" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="30" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="31" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="32" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
</tr>
<tr>
<td id="33" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="34" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="35" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="36" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="37" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="38" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="39" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="40" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
</tr>
<tr>
<td id="41" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="42" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="43" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="44" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="45" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="46" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="47" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="48" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
</tr>
<tr>
<td id="49" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="50" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="51" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="52" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="53" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="54" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="55" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="56" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
</tr>
<tr>
<td id="57" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="58" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="59" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="60" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="61" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="62" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="63" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
<td id="64" style="height: 10px; width: 10px; background-color: #e2e2e0; border: 1px solid #cccccc;" onmousedown="switchValue(this)">&nbsp;0</td>
</tr>
</tbody>
</table>

<input onclick="resetmatrix()" type="button" value="Reset" /> 
<input onclick="savematrix()" type="button" value="Save" /> 
</div>

<div>
Input text:<br>
<input type="text" id="textbox">
<input onclick="sendText()" type="button" value="Send" />
<br>
Scrolling speed:<br>
<input type="range" min="1" max="10" value="7" class="slider" id="speed">
</div>

<div>
<input onclick="showclock()" type="button" value="Show BCD Clock" />
<input onclick="loadmatrix()" type="button" value="Show Saved Matrix" />
</div>

<div>
Brightness:<br>
<input onchange="sendIntensity()" type="range" min="1" max="15" value="3" class="slider" id="intensity">
</div>

<script type="text/javascript"><!--
    function switchValue(obj)
    {
      if(obj.innerHTML == "&nbsp;0")
      {
        obj.style.backgroundColor = "#FF0000";
        obj.innerHTML = "&nbsp;1";
      } else {
        obj.style.backgroundColor = "#E2E2E0";
        obj.innerHTML = "&nbsp;0";
      }
      sendBinary();
    }
    function resetmatrix()
    {
      var tdObj;
      for(var i=1;i<65;i++)
      {
        tdObj = document.getElementById(i);
        tdObj.style.backgroundColor = "#E2E2E0";
        tdObj.innerHTML = "&nbsp;0";
      }
      sendBinary();
    }
    function savematrix()
    {
      var params = "save=";
      var req = new XMLHttpRequest();
      req.open('POST', '?' + params, true);
      req.send();
    }
    function loadmatrix()
    {
      var params = "load=";
      var req = new XMLHttpRequest();
      req.open('POST', '?' + params, true);
      req.send();
    }
    function sendBinary()
    {
      var tdObj;
      var params = "matrix=";
      for(var i=1;i<65;i++)
      {
        tdObj = document.getElementById(i);
        if(tdObj.innerHTML == "&nbsp;0")
        {
          params += "0";
        } else {
          params += "1";               
        }
      }
      var req = new XMLHttpRequest();
      req.open('POST', '?' + params, true);
      req.send();
    }
    function sendIntensity()
    {
      var slider = document.getElementById("intensity");
      var params = "intensity=";
      params += slider.value;
      var req = new XMLHttpRequest();
      req.open('POST', '?' + params, true);
      req.send();
    }
    function sendText()
    {
      var t = document.getElementById("textbox");
      var s = document.getElementById("speed");
      var params = "text=";
      params += t.value;
      params += "&speed=";
      params += s.value;
      var req = new XMLHttpRequest();
      req.open('POST', '?' + params, true);
      req.send();
    }
    function showclock()
    {
      var params = "clock=";
      var req = new XMLHttpRequest();
      req.open('POST', '?' + params, true);
      req.send();
    }
</script>
</body></html>
)=====" ;
8×8 LED mono matrix with ESP8266 web server

3 thoughts on “8×8 LED mono matrix with ESP8266 web server

  • 24/11/2021 at 23:17
    Permalink

    Nice project, would like to see the code!

    Reply
    • 13/01/2022 at 09:42
      Permalink

      hi, i’ve added the code, sorry for the delay, hope it’s useful to you.

      Reply

Leave a Reply to Nadila Poppyanti Cancel reply

Your email address will not be published. Required fields are marked *