Electronic billboard

There is an electronic billboard that does not play a video, but changes the picture irregularly to achieve the effect of advertising.

How do you make something like this?

Idea: It consists of two parts. One part is the display, which is used to display pictures, and the other part is the small program, which is used to set the pictures displayed on the screen.

Because the images are sent, a Web server is implemented on the ESP32 to receive the posted images.

Therefore, the overall STRUCTURE of TTGO is as follows:

Therefore, just write the code in the “your code” position in the diagram to display the POST image using the TFT driver.

Started making

Hardware: TTGO development version

It consists of an ESP32 +135*240 TFT screen driven by the ST7789.

TTGO was chosen because it integrates the ESP32 and TFT screens into one board, eliminating the need to solder or wire the ESP32 to the TFT screen yourself.

TFT screen PINOUT is as follows:

function stitching
MOSI 19
SCLK 18
CS 5
DC 16
RST 23
BL 4

Development tools and libraries

This example uses arduino and the applet IDE.

TTGO requires libraries:

1, esp32

In the arduino preferences set dl.espressif.com/dl/package_… Then search for esp32 in the Development version option of Arduino and install it.

2, JPEGDecoder

Search for the JPEGDecoder in the Arduino library options and install it

3, TFT_eSPI

Search for TFT_eSPI in the Arduino’s library options and install it

Based on existing code

Before you start writing code, look for existing solutions and code. Luckily, I found one: github.com/nori-dev-ak…

However, this code does not work with TTGO. The Arduino_ST7789 library of the original project cannot drive the TTGO screen because of the different hardware.

So, on top of this item, make modifications. I replaced the original Arduino_ST7789 with the TFT_eSPI library. Make TTGO available.

In addition, for the convenience of operation, I changed the wifi connection mode to APmode (hot spot mode), so that after the phone connects to the wifi hotspot, the small program can operate through 192.168.4.1.

The arduino code is as follows:

#include <WiFi.h>
#include <JPEGDecoder.h>
#include <SPI.h>
#include <TFT_eSPI.h>

TFT_eSPI tft = TFT_eSPI();

const char AP_DEMO_HTTP_200_IMAGE[] = "HTTP/1.1 200 OK\r\nPragma: public\r\nCache-Control: max-age=1\r\nExpires: Thu, 26 Dec 2016 23:59:59 GMT\r\nContent-Type: image/";

typedef enum
{
  UPL_AP_STAT_MAIN = 1,           // GET /
  UPL_AP_STAT_LED_HIGH,           // GET /H
  UPL_AP_STAT_LED_LOW,            // GET /L
  UPL_AP_STAT_GET_IMAGE,          // GET /logo.bmp
  UPL_AP_STAT_GET_FAVICON,        // GET /favicon.ico
  UPL_AP_STAT_POST_UPLOAD,        // POST /upload
  UPL_AP_STAT_POST_START_BOUNDRY, // POST /upload boundry
  UPL_AP_STAT_POST_GET_BOUNDRY,   // POST /upload boundry
  UPL_AP_STAT_POST_START_IMAGE,   // POST /upload image
  UPL_AP_STAT_POST_GET_IMAGE,     // POST /upload image
} UPL_AP_STAT_t;


const char* ssid = "TTGO";
const char* password = "codetyphon";
IPAddress local_ip(192, 168, 4, 1);
IPAddress gateway(192, 168, 4, 1);
IPAddress subnet(255, 255, 255, 0);

#define LED_PIN 4

WiFiServer server(80);

#define MAX_IMAGE_SIZE 65535
#define MAX_BUF_SIZE 1024
//#define IMAGE_DEBUG

int value = 0;
char boundbuf[MAX_BUF_SIZE];
int boundpos = 0;
char imagetypebuf[MAX_BUF_SIZE];
int imagetypepos = 0;
char imagebuf[MAX_IMAGE_SIZE];
int imagepos = 0;
String IPaddress;
void setup()
{
  tft.begin();
  tft.setRotation(1);
  tft.fillScreen(TFT_BLACK);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.drawCentreString("WiFi Connecting.", 120, 65, 4);
  bool ret;
  Serial.begin(115200);
  // We start by connecting to a WiFi network
  Serial.println();
  Serial.println();
  Serial.println("starting...");
  WiFi.softAP(ssid, password);
  delay(100);
  IPaddress = WiFi.softAPIP().toString();
  tft.fillScreen(TFT_BLACK);
  tft.drawCentreString(IPaddress, 120, 65, 4);
  tft.setRotation(0);
  server.begin();
}

void printUploadForm(WiFiClient client)
{
  Serial.println("printUploadForm");
  // HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK)
  // and a content-type so the client knows what's coming, then a blank line:
  client.println("HTTP/1.1 200 OK");
  client.println("Content-type:text/html");
  client.println();
  client.println("<html>");
  client.println("<body>");
  client.println();
  client.println("<form action=\"upload\" method=\"post\" enctype=\"multipart/form-data\">");
  client.println("Select image to upload:");
  client.println("<input type=\"file\" name=\"fileToUpload\" id=\"fileToUpload\">");
  client.println("<input type=\"submit\" value=\"Upload Image\" name=\"submit\">");
  client.println("</form>");
  client.println();
  client.println("</body>");
  client.println("</html>");

  client.println();
}

void printImage(WiFiClient client)
{
  Serial.println("printImage");
  // HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK)
  // and a content-type so the client knows what's coming, then a blank line:
  client.print(AP_DEMO_HTTP_200_IMAGE);
  client.print(imagetypebuf);
  client.print("\r\n\r\n");
#ifdef IMAGE_DEBUG
  Serial.print(AP_DEMO_HTTP_200_PNG);
#endif
  for (int i = 0; i < imagepos; i++)
  {
    client.write(imagebuf[i]);
#ifdef IMAGE_DEBUG
    Serial.write(imagebuf[i]);
#endif
  }
  drawArrayJpeg((uint8_t *)imagebuf, imagepos, 0, 0);
}

void printThanks(WiFiClient client)
{
  Serial.println("printThanks");
  // HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK)
  // and a content-type so the client knows what's coming, then a blank line:
  client.println("HTTP/1.1 200 OK");
  client.println("Content-type:text/html");
  client.println();
  client.println("<html>");
  client.println("<body>");
  client.println();
  client.println("Thank You");
  client.println("<a id=\"logo\" href=\"/\"><img src=\"logo.bmp\" alt=\"logo\" border=\"0\"></a>");
  client.println();
  client.println("</body>");
  client.println("</html>");
  // the content of the HTTP response follows the header:
  //client.print("Click <a href=\"/H\">here</a> turn the LED on pin 5 on<br>");
  //client.print("Click <a href=\"/L\">here</a> turn the LED on pin 5 off<br>");

  // The HTTP response ends with another blank line:
  client.println();
}

void loop()
{
  int cnt;
  bool newconn = false;
  int stat;
  WiFiClient client = server.available(); // listen for incoming clients

  if (client)
  { // if you get a client,
    stat = 0;
    boundpos = 0;
    Serial.println("new client"); // print a message out the serial port
    String currentLine = "";      // make a String to hold incoming data from the client
    while (client.connected())
    { // loop while the client's connected
      cnt = client.available();
      if (cnt)
      { // if there's bytes to read from the client,
#ifdef IMAGE_DEBUG
        if (newconn == false)
        {
          Serial.println(cnt);
          newconn = true;
        }
#endif
        char c = client.read(); // read a byte, then
#ifndef IMAGE_DEBUG
        if (stat != UPL_AP_STAT_POST_GET_IMAGE)
        {
#endif
          Serial.write(c); // print it out the serial monitor
#ifndef IMAGE_DEBUG
        }
#endif

        if (stat == UPL_AP_STAT_POST_GET_IMAGE)
        {
          if (imagepos < MAX_IMAGE_SIZE)
          {
            imagebuf[imagepos] = c;
            imagepos++;
          }
        }
        if (c == '\n')
        { // if the byte is a newline character
#ifdef IMAGE_DEBUG
          Serial.print("stat is equal=");
          Serial.println(stat);
#endif
          if (stat == UPL_AP_STAT_POST_START_BOUNDRY)
          {
            boundbuf[boundpos] = '\0';
            boundpos++;
#ifdef IMAGE_DEBUG
            Serial.println("&&&&&&&&&&&&&&&&&");
            Serial.println(boundbuf);
            Serial.println("&&&&&&&&&&&&&&&&&");
#endif
            stat = UPL_AP_STAT_POST_UPLOAD;
            Serial.println("stats=UPL_AP_STAT_POST_UPLOAD");
          }
          if (stat == UPL_AP_STAT_POST_START_IMAGE && currentLine.length() == 0)
          {
            imagetypebuf[imagetypepos] = '\0';
            imagetypepos++;
#ifdef IMAGE_DEBUG
            Serial.println("&&&&&&&&&&&&&&&&&");
            Serial.println(imagetypebuf);
            Serial.println("&&&&&&&&&&&&&&&&&");
#endif
            imagepos = 0;
            stat = UPL_AP_STAT_POST_GET_IMAGE;
            Serial.println("stats=UPL_AP_STAT_POST_GET_IMAGE");
          }
          // if you got a newline, then clear currentLine:
          currentLine = "";
          newconn = false;
        }
        else if (c != '\r')
        { // if you got anything else but a carriage return character,
          currentLine += c; // add it to the end of the currentLine
          if (stat == UPL_AP_STAT_POST_START_BOUNDRY)
          {
            if (boundpos < MAX_BUF_SIZE)
            {
              boundbuf[boundpos] = c;
              boundpos++;
            }
          }
          if (stat == UPL_AP_STAT_POST_START_IMAGE)
          {
            if (imagetypepos < MAX_BUF_SIZE)
            {
              imagetypebuf[imagetypepos] = c;
              imagetypepos++;
            }
          }
        }

        // Check to see if the client request was "GET / "
        if (currentLine.endsWith("GET / "))
        {
          stat = UPL_AP_STAT_MAIN;
          Serial.println("stats=UPL_AP_STAT_MAIN");
        }
        if (currentLine.endsWith("GET /logo.bmp "))
        {
          stat = UPL_AP_STAT_GET_IMAGE;
          Serial.println("stats=UPL_AP_STAT_GET_IMAGE");
        }
        if (currentLine.endsWith("POST /upload "))
        {
          stat = UPL_AP_STAT_POST_UPLOAD;
          Serial.println("stats=UPL_AP_STAT_POST_UPLOAD");
        }
        if (stat == UPL_AP_STAT_POST_UPLOAD && currentLine.endsWith("Content-Type: multipart/form-data; boundary="))
        {
          stat = UPL_AP_STAT_POST_START_BOUNDRY;
          Serial.println("stats=UPL_AP_STAT_POST_START_BOUNDRY");
        }
        if (stat == UPL_AP_STAT_POST_UPLOAD && currentLine.endsWith("Content-Type: image/"))
        {
          stat = UPL_AP_STAT_POST_START_IMAGE;
          Serial.println("stats=UPL_AP_STAT_POST_START_IMAGE");
        }
        if (stat == UPL_AP_STAT_POST_UPLOAD && boundpos > 0 && currentLine.endsWith(boundbuf))
        {
          Serial.println("found boundry");
        }
        if (stat == UPL_AP_STAT_POST_GET_IMAGE && boundpos > 0 && currentLine.endsWith(boundbuf))
        {
          Serial.println("found image boundry");
          Serial.println(imagepos);
          stat = UPL_AP_STAT_POST_UPLOAD;
          imagepos = imagepos - boundpos - 3;
#ifdef IMAGE_DEBUG
          Serial.println(imagepos);
          for (int i = 0; i < imagepos; i++)
          {
            Serial.write(imagebuf[i]);
          }
#endif
          Serial.println("stats=UPL_AP_STAT_POST_UPLOAD");
        }
      }
      else
      {
        if (stat == UPL_AP_STAT_MAIN)
        {
          printUploadForm(client);
          break;
        }
        if (stat == UPL_AP_STAT_POST_UPLOAD)
        {
          printThanks(client);
          break;
        }
        if (stat == UPL_AP_STAT_GET_IMAGE)
        {
          printImage(client);
          break;
        }

        Serial.println("stat unknown");
        delay(1000);
        break;
      }
    }
    // close the connection:
    client.stop();
    Serial.println("client disonnected");
  }

  delay(100);
}

/*====================================================================================
  This sketch contains support functions to render the Jpeg images.
  Created by Bodmer 15th Jan 2017
  ==================================================================================*/

// Return the minimum of two values a and b
#define minimum(a, b) (((a) < (b)) ? (a) : (b))

//====================================================================================
//   This function opens the Filing System Jpeg image file and primes the decoder
//====================================================================================
void drawArrayJpeg(uint8_t *buff_array, uint32_t buf_size, int xpos, int ypos)
{

  Serial.println("=====================================");
  Serial.println("Drawing Array ");
  Serial.println("=====================================");

  boolean decoded = JpegDec.decodeArray(buff_array, buf_size);
  if (decoded)
  {
    // print information about the image to the serial port
    jpegInfo();

    // render the image onto the screen at given coordinates
    renderJPEG(xpos, ypos);
  }
  else
  {
    Serial.println("Jpeg file format not supported!");
  }
}

//====================================================================================
//   Decode and paint onto the TFT screen
//====================================================================================
void renderJPEG(int xpos, int ypos) {
  // retrieve infomration about the image
  uint16_t *pImg;
  uint16_t mcu_w = JpegDec.MCUWidth;
  uint16_t mcu_h = JpegDec.MCUHeight;
  uint32_t max_x = JpegDec.width;
  uint32_t max_y = JpegDec.height;

  // Jpeg images are draw as a set of image block (tiles) called Minimum Coding Units (MCUs)
  // Typically these MCUs are 16x16 pixel blocks
  // Determine the width and height of the right and bottom edge image blocks
  uint32_t min_w = minimum(mcu_w, max_x % mcu_w);
  uint32_t min_h = minimum(mcu_h, max_y % mcu_h);

  // save the current image block size
  uint32_t win_w = mcu_w;
  uint32_t win_h = mcu_h;

  // record the current time so we can measure how long it takes to draw an image
  uint32_t drawTime = millis();

  // save the coordinate of the right and bottom edges to assist image cropping
  // to the screen size
  max_x += xpos;
  max_y += ypos;

  // read each MCU block until there are no more
  while (JpegDec.readSwappedBytes()) {

    // save a pointer to the image block
    pImg = JpegDec.pImage ;

    // calculate where the image block should be drawn on the screen
    int mcu_x = JpegDec.MCUx * mcu_w + xpos;  // Calculate coordinates of top left corner of current MCU
    int mcu_y = JpegDec.MCUy * mcu_h + ypos;

    // check if the image block size needs to be changed for the right edge
    if (mcu_x + mcu_w <= max_x) win_w = mcu_w;
    else win_w = min_w;

    // check if the image block size needs to be changed for the bottom edge
    if (mcu_y + mcu_h <= max_y) win_h = mcu_h;
    else win_h = min_h;

    // copy pixels into a contiguous block
    if (win_w != mcu_w)
    {
      uint16_t *cImg;
      int p = 0;
      cImg = pImg + win_w;
      for (int h = 1; h < win_h; h++)
      {
        p += mcu_w;
        for (int w = 0; w < win_w; w++)
        {
          *cImg = *(pImg + w + p);
          cImg++;
        }
      }
    }

    // draw image MCU block only if it will fit on the screen
    if (( mcu_x + win_w ) <= tft.width() && ( mcu_y + win_h ) <= tft.height())
    {
      tft.pushRect(mcu_x, mcu_y, win_w, win_h, pImg);
    }
    else if ( (mcu_y + win_h) >= tft.height()) JpegDec.abort(); // Image has run off bottom of screen so abort decoding
  }

  // calculate how long it took to draw the image
  drawTime = millis() - drawTime;

  // print the results to the serial port
  Serial.print(F(  "Total render time was    : ")); Serial.print(drawTime); Serial.println(F(" ms"));
  Serial.println(F(""));
}

//====================================================================================
//   Send time taken to Serial port
//====================================================================================
void jpegInfo()
{
  Serial.println(F("==============="));
  Serial.println(F("JPEG image info"));
  Serial.println(F("==============="));
  Serial.print(F("Width      :"));
  Serial.println(JpegDec.width);
  Serial.print(F("Height     :"));
  Serial.println(JpegDec.height);
  Serial.print(F("Components :"));
  Serial.println(JpegDec.comps);
  Serial.print(F("MCU / row  :"));
  Serial.println(JpegDec.MCUSPerRow);
  Serial.print(F("MCU / col  :"));
  Serial.println(JpegDec.MCUSPerCol);
  Serial.print(F("Scan type  :"));
  Serial.println(JpegDec.scanType);
  Serial.print(F("MCU width  :"));
  Serial.println(JpegDec.MCUWidth);
  Serial.print(F("MCU height :"));
  Serial.println(JpegDec.MCUHeight);
  Serial.println(F("==============="));
}

//====================================================================================
//   Open a Jpeg file and dump it to the Serial port as a C array
//====================================================================================
void createArray(const char *filename)
{

  fs::File jpgFile; // File handle reference for SPIFFS
  //  File jpgFile;  // File handle reference For SD library

  if (!(jpgFile = SPIFFS.open(filename, "r")))
  {
    Serial.println(F("JPEG file not found"));
    return;
  }

  uint8_t data;
  byte line_len = 0;
  Serial.println("// Generated by a JPEGDecoder library example sketch:");
  Serial.println("// https://github.com/Bodmer/JPEGDecoder");
  Serial.println("");
  Serial.println("#if defined(__AVR__)");
  Serial.println("  #include <avr/pgmspace.h>");
  Serial.println("#endif");
  Serial.println("");
  Serial.print("const uint8_t ");
  while (*filename != '.')
    Serial.print(*filename++);
  Serial.println("[] PROGMEM = {"); // PROGMEM added for AVR processors, it is ignored by Due

  while (jpgFile.available())
  {

    data = jpgFile.read();
    Serial.print("0x");
    if (abs(data) < 16)
      Serial.print("0");
    Serial.print(data, HEX);
    Serial.print(","); // Add value and comma
    line_len++;
    if (line_len >= 32)
    {
      line_len = 0;
      Serial.println();
    }
  }

  Serial.println("};\r\n");
  // jpgFile.seek( 0, SeekEnd);
  jpgFile.close();
}
Copy the code

test

After analyzing the original code, it is found that there are two steps to set up the image, which can be tested using AdvancedRestClient similar to Postman.

1, post pictures to http://192.168.4.1/upload

Content-type to be set to:

multipart/form-data; boundary=
Copy the code

2, get request to make it effective

Get http://192.168.4.1/logo.bmp

Small program:

const app = getApp() Page({ data: { host: '192.168.4.1'}, async getFiles () {const res = await app.wx.choosemessagefile ({type: 'image', }) if (res.errMsg == 'chooseMessageFile:ok') { return res.tempFiles } else { return [] } }, Async upload(path) {const _self = this _self.setdata ({img: path}) wx.showloading ({title: "... ") }) try { const res = await app.wx.uploadFile({ filePath: path, name: 'fileToUpload', url: `http://${_self.data.host}/upload`, header: { 'Content-Type': 'multipart/form-data; boundary=' }, }) console.log(res) wx.hideLoading() if (res.statusCode ! = 200) {wx.showtoast ({title: 'transfer failed ', icon: 'none', duration: 2000 }) } return res.statusCode } catch (error) { wx.hideLoading() wx.showToast({ title: error.errMsg, icon: Duration: 2000}) return 500}}, async setimg() {wx.showloading ({title: "setting..." }) const _self = this try { const res = await app.wx.request({ url: `http://${_self.data.host}/logo.bmp` }) wx.hideLoading() console.log('set img:', If (res.statuscode == 200) {wx.showtoast ({title: 'set successfully ', icon: 'success', duration: } else {wx.showToast({title: 'failed to set ', icon: 'none', duration: 2000 }) } return res.statusCode } catch (error) { wx.hideLoading() wx.showToast({ title: error.errMsg, icon: 'none', duration: 2000 }) return 500 } }, async looping(files) { const _self = this if (files.length > 0) { const file = files[0] console.log(file) files.splice(0, 1) const status = await _self.upload(file.path) if (status == 200) { const code = await _self.setimg() if (code == 200) { setTimeout(() => { _self.looping(files) }, 20); }}}}, async up() { const _self = this const files = await _self.getfiles() console.log(files) if (files.length == 0) { //none } if (files.length == 1) { //one const path = files[0].path const code = await _self.upload(path) if (code == 200) { await _self.setimg() } } if (files.length > 1) { _self.looping(files) } }, input: function (event) { this.setData({ host: event.detail.value }) }, onLoad: function () {}, })Copy the code

Final result:

All programs, located at: github.com/nasaiot/ttg…