电子广告牌
有一种电子广告牌,它不播放视频,而是不定期切换画面,以达到广告效果。
如何制作一个类似的东西?
思路:它由2部分组成。一部分是显示屏,用于显示图片,另一部分是小程序,用来设置屏幕显示的图片。
因为图片是发送过去的,因此,在esp32上面,要实现一个web server来接收POST过来的图片。
所以,整体TTGO的架构如下:
因此,只需要在图中“your code”位置编写代码,把POST的图片用TFT驱动给显示出来就可以了。
开始制作
硬件:TTGO开发版
它由esp32+135*240大小的tft屏组成,tft屏的驱动是st7789。
之所以选择TTGO,是因为它把esp32和tft屏整合到一个板子里,这样省去了自己焊接或用杜邦线连接esp32与tft屏幕的麻烦。
TFT屏幕PINOUT如下:
| 功能 | 针脚 |
|---|---|
| MOSI | 19 |
| SCLK | 18 |
| CS | 5 |
| DC | 16 |
| RST | 23 |
| BL | 4 |
开发工具及库
本例子使用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…