用小程序实现TTGO电子屏的画面切换

1,738 阅读4分钟

电子广告牌

有一种电子广告牌,它不播放视频,而是不定期切换画面,以达到广告效果。

如何制作一个类似的东西?

思路:它由2部分组成。一部分是显示屏,用于显示图片,另一部分是小程序,用来设置屏幕显示的图片。

因为图片是发送过去的,因此,在esp32上面,要实现一个web server来接收POST过来的图片。

所以,整体TTGO的架构如下:

因此,只需要在图中“your code”位置编写代码,把POST的图片用TFT驱动给显示出来就可以了。

开始制作

硬件:TTGO开发版

它由esp32+135*240大小的tft屏组成,tft屏的驱动是st7789。

之所以选择TTGO,是因为它把esp32和tft屏整合到一个板子里,这样省去了自己焊接或用杜邦线连接esp32与tft屏幕的麻烦。

TFT屏幕PINOUT如下:

功能针脚
MOSI19
SCLK18
CS5
DC16
RST23
BL4

开发工具及库

本例子使用arduino和小程序IDE。

TTGO需要的库:

1、esp32

arduino首选项里设置dl.espressif.com/dl/package_… ,然后去arduino的开发版选项里搜索esp32,安装即可。

2、JPEGDecoder

在arduino的库选项里搜索JPEGDecoder并安装

3、TFT_eSPI

在arduino的库选项里搜索TFT_eSPI并安装

基于已有的代码

在开始编写代码之前,寻找现有的方案及代码。很幸运,找到一个:github.com/nori-dev-ak…

但是,该代码并不能用于TTGO。因为硬件不同,导致原项目的Arduino_ST7789库无法驱动TTGO屏幕。

于是,在此项目之上,进行修改。我用TFT_eSPI库替代了原有的Arduino_ST7789。使得ttgo可以使用。

并且,为了方便操作,我把wifi的连接方式更改为APmode(热点模式),这样,手机连接wifi热点后,小程序可以通过192.168.4.1来操作。

arduino代码如下:

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

测试

经过分析原始代码,发现设置图片有2个步骤,可采用类似postman的AdvancedRestClient进行测试。

1、post图片到http://192.168.4.1/upload

Content-Type要设置为:

multipart/form-data; boundary=

2、get请求令其生效

get http://192.168.4.1/logo.bmp

小程序:

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: '传输失败',
          icon: 'none',
          duration: 2000
        })
      }
      return res.statusCode
    } catch (error) {
      wx.hideLoading()
      wx.showToast({
        title: error.errMsg,
        icon: 'none',
        duration: 2000
      })
      return 500
    }
  },
  async setimg() {
    wx.showLoading({
      title: "设置中。。。"
    })
    const _self = this
    try {
      const res = await app.wx.request({
        url: `http://${_self.data.host}/logo.bmp`
      })
      wx.hideLoading()
      console.log('set img:', res)
      if (res.statusCode == 200) {
        wx.showToast({
          title: '设置成功',
          icon: 'success',
          duration: 2000
        })
      } else {
        wx.showToast({
          title: '设置失败',
          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 () {},
})

最终效果:

所有程序,位于:github.com/nasaiot/ttg…