太空人WiFi天气电子时钟:ESP8266 OLED 0.96英寸 8针显示

一、实现效果

 

WeChat_20221109203218

二、开发说明

        几个月前就实现了效果,一直没有整理发布博客。开发工具:visual studio code  平台:platformio。visual studio code 安装以及platformio插件 配置可百度,就是使用platformio插件项目开始下载慢的问题,这个需要在早上(网络不好需更换wifi)下载,这样项目基本都新建成功,我一开始白天新建项目下载esp8266相关的文件都一直卡着不动,后来都是一大早新建项目都成功了,由于屏幕较小,布局改了几次。使用的库:

TFT_eSPI、TJpg_Decoder、ArduinoJson、TimeLib(下载的别人写好的)以及esp8266wifi连接相关。

三、实现过程

(1)TFT_eSPI配置

        引脚请自行配置tft_espi库中的 User_Setup.h文件。在User_Setup.h文件中使用st7735驱动

以及高度、宽度、RGB等配置

 (2)屏幕引脚插线

具体接线对应如下:

TFT屏幕                    nodemcu

GND                             GND

VCC                              3V3

SCL                                 D5

SDA                                 D7

RES                                 D4

DC                                   D3

CS                                    D8

BLK                                  可以不接(控制屏幕背光)

   (3)利用python将太空人gif转为多个图片以及数据文件

 最终使用space.h文件引入适合的帧数据,不能都引入,都引入就大了。

from PIL import Image
import sys
import os
from io import BytesIO
import binascii
import traceback


curdir = "./"
os.chdir(curdir)

def processImage(in_file, saveImg=True):
    try:
        im = Image.open(in_file)
    except IOError:
        print("Cant load", in_file)
        sys.exit(1)

    # 截取文件名
    filename = in_file.split('.')[0]

    i = 0
    mypalette = im.getpalette()

    arr_name_all = ''  # 存取数组
    arr_size_all = ''  # 存储数组容量

    try:
        with open(filename + '.h', 'w', encoding='utf-8') as f:  # 写入文件
            f.write('#include <pgmspace.h> \n\n')
            while 1:
                print('.', end="")
                im.putpalette(mypalette)
                new_im = Image.new("RGB", im.size)
                new_im.paste(im)

                # 缩放图像,
                width = new_im.size[0]  # 获取原始图像宽度
                height = new_im.size[1]  # 获取原始图像高度
                new_height = 82  # 等比例缩放后的图像高度,根据实际需要调整
                # print(width, " ", height)
                if height > new_height:
                    ratio = round(new_height / height, 3)  # 缩放系数
                    new_im = new_im.resize((int(width * ratio), int(height * ratio)), Image.ANTIALIAS)

                # 获取图像字节流,转16进制格式
                img_byte = BytesIO()  # 获取字节流
                new_im.save(img_byte, format='jpeg')
                # print(img_byte.getvalue())
                
                # 16进制字符串
                img_hex = binascii.hexlify(img_byte.getvalue()).decode('utf-8')  
                
                arr_name = filename + '_' + str(i)
                arr_size = 0  # 记录数组长度
                arr_name_all += arr_name + ','

                # 将ac --> 0xac
                f.write('const uint8_t ' + arr_name + '[] PROGMEM = { \n')  # 写前
                for index, x in zip(range(len(img_hex)), range(0, len(img_hex), 2)):
                    temp_hex = '0x' + img_hex[x:x + 2] + ', '
                    # 30个数据换行
                    if (index + 1) % 30 == 0:
                        temp_hex += '\n'

                    f.write(temp_hex)  # 写入文件
                    arr_size += 1
                f.write('\n};\n\n')  # 写结尾
                i += 1
                arr_size_all += str(arr_size) + ','

                # 保存一帧帧图像
                if saveImg:
                    if not os.path.exists('./out_img'):
                        os.mkdir('./out_img')
                    if not os.path.exists('./out_img/' + filename):
                        os.mkdir('./out_img/' + filename)
                    new_im.save('./out_img/' + filename + '/' + str(i) + '.jpg')

                try:
                    im.seek(im.tell() + 1)
                except EOFError:
                    # 动图读取结束
                    f.write('const uint8_t *' + filename + '[' + str(i) + '] PROGMEM { ' + arr_name_all + '};\n')
                    f.write('const uint32_t ' + filename + '_size[' + str(i) + '] PROGMEM { ' + arr_size_all + '};')
                    print("成功保存文件为:" + filename + '.h')
                    break

    except EOFError as e:
        print(e.args)
        print(traceback.format_exc())
        pass  # end of sequence


if __name__ == '__main__':
    processImage("space.gif", True)
    # im=Image.open("foo0.bmp")
    # print ("img info:",im.format,im.size)

 (4)使用processing 软件制作字体

        使用processing打开Create_font.pde文件(https://processing.org/ 下载processing软件,并且安装)。只需修改几个地方就可以,如下所示:

 

        每个汉字对应的unicode码值可以通过在线转换工具获取,然后将转换后的/u替换为0x即可。完成修改后,点击运行,弹出对话框显示自定义库中的所有字符,同时在FontFiles文件夹中生成一个.vlw格式的文件,存放我们制作出来的字库文件。通过https://tomeko.net/online_tools/file_to_hex.php?lang=zh,将vlw文件转换成Arduin使用的字库文件xxxFont.h

 将生成的16进制数据按照下列各式存放在自定义的.h格式文件中

#include <pgmspace.h>
const uint8_t  font_10[] PROGMEM = {
0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x1D, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x52, 0xA0, 0x00, 0x00, 0x00, 0x1F,
0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x02,
...
0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6C, 0xB9, 0x00, 0x00, 0x00, 0x26,
0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x07,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x88, 0x96, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x29,
};

    (5)  完整代码

#include <TFT_eSPI.h>
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>
#include <ESP8266HTTPClient.h>
#include <TJpg_Decoder.h>
#include <WiFiUdp.h>
#include <TimeLib.h>
#include <ArduinoJson.h>
// #include <NTPClient.h>

#include "img/space.h"    // 太空人
#include "font/date_18.h" // 时间
#include "font/time_20.h" // 日期

TFT_eSPI tft = TFT_eSPI(); // 引脚请自行配置tft_espi库中的 User_Setup.h文件
TFT_eSprite clk = TFT_eSprite(&tft);
WiFiClient espClient;
ESP8266WiFiMulti wifis;
WiFiUDP Udp;

// NTP Servers:
// static const char ntpServerName[] = "cn.ntp.org.cn";
static const char ntpServerName[] = "ntp1.aliyun.com";
const int timeZone = 8;        // 时区
unsigned int localPort = 1337; // local port to listen for UDP packets

unsigned long weatherTime = 0;
struct WeatherData
{
  char city[16];    //城市名称
  char weather[32]; //天气介绍(多云...)
  char temp[16];    //温度
  char udate[32];   //更新时间
};

WeatherData weatherData;
void sendNTPpacket(IPAddress &address);
time_t getNtpTime();
void getCityWeater();

/**********************************************
 *  加载进度条
 *
 ***********************************************/
int loadNum = 1;   // 进度条长度 初始1
int maxLoad = 147; // 进度条最大长度
void loading(int num)
{
  clk.setColorDepth(8);

  clk.createSprite(160, 80); // 创建布局大小 宽x高 0.96寸ttf 80x160 tft.int() 后设置方向     
  tft.setRotation(1);
  clk.fillSprite(TFT_BLACK); // 布局背景颜色

  clk.drawRoundRect(5, 40, 150, 16, 6, TFT_WHITE);       // 画进度条外边 (x方向5, y方向40, 长度150(左右边距5,总长度160), 高度16 , 圆角6 , 白色)
  clk.fillRoundRect(7, 42, loadNum, 12, 5, TFT_WHITE);   // 画进度条填充 (x方向7, y方向42, 长度loadNum, 高度12 , 圆角5 , 白色)设置的坐标位置和长度在外边内
  clk.setTextColor(TFT_GREEN, TFT_BLACK);                // 设置字体颜色背景
  clk.drawCentreString("Connecting to WiFi", 80, 20, 1); // 设置字体居中显示
  clk.pushSprite(0, 0);                                  // 布局坐标
  clk.deleteSprite();
  if (loadNum < maxLoad) // 值小于最大值 +1
    loadNum += 1;
  delay(1);
  if (loadNum < num) // 值小于设定值 继续执行
    loading(num);
}
// wifi连接
void wifiConnect()
{
  Serial.print("Connecting");
  // while (WiFi.status() != WL_CONNECTED)
  while (wifis.run() != WL_CONNECTED)
  {
    digitalWrite(D0, LOW);
    delay(250);
    Serial.print(".");
    digitalWrite(D0, HIGH);
    delay(250);
  }
  Serial.println();
  Serial.print("Connected, IP address: ");
  Serial.println(WiFi.localIP());
}

/**************************
 *  太空人动画
 * x    x轴,默认0
 * y    y轴,默认0
 * dt   延时,默认60ms
 *
 * ***********************/
void spaceAnimation(int x = 0, int y = 30, int dt = 60)
{
  // TJpgDec.setJpgScale(2);
  TJpgDec.drawJpg(x, y, space_0, sizeof(space_0));
  delay(dt);
  TJpgDec.drawJpg(x, y, space_4, sizeof(space_4));
  delay(dt);
  TJpgDec.drawJpg(x, y, space_8, sizeof(space_8));
  delay(dt);
  TJpgDec.drawJpg(x, y, space_12, sizeof(space_12));
  delay(dt);
  TJpgDec.drawJpg(x, y, space_16, sizeof(space_16));
  delay(dt);
  TJpgDec.drawJpg(x, y, space_20, sizeof(space_20));
  delay(dt);
  TJpgDec.drawJpg(x, y, space_24, sizeof(space_24));
  delay(dt);
  TJpgDec.drawJpg(x, y, space_28, sizeof(space_28));
  delay(dt);
  TJpgDec.drawJpg(x, y, space_32, sizeof(space_32));
  delay(dt);
  TJpgDec.drawJpg(x, y, space_36, sizeof(space_36));
  delay(dt);
  TJpgDec.drawJpg(x, y, space_40, sizeof(space_40));
  delay(dt);
  TJpgDec.drawJpg(x, y, space_44, sizeof(space_44));
  delay(dt);
  TJpgDec.drawJpg(x, y, space_47, sizeof(space_47));
  delay(dt);
}
bool tft_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t *bitmap)
{
  if (y >= tft.height())
    return 0;
  tft.pushImage(x, y, w, h, bitmap);
  // Return 1 to decode next block
  return 1;
}
void digitalClockDisplay()
{

  clk.setColorDepth(8);

  /***中间时间区***/
  byte xpos = 20;
  byte ypos = 4;
  //时分
  clk.createSprite(90, 32);
  clk.fillSprite(TFT_WHITE);
  clk.loadFont(TIME_32);
  clk.setTextColor(TFT_BLACK, TFT_WHITE);
  int hh = hour();
  if (hh < 10)
    xpos += clk.drawString("0", xpos, ypos);
  xpos += clk.drawNumber(hh, xpos, ypos);
  xpos += clk.drawString(":", xpos, ypos);
  int mm = minute();
  if (mm < 10)
    xpos += clk.drawString("0", xpos, ypos);
  clk.drawNumber(mm, xpos, ypos); //绘制时和分
  clk.unloadFont();
  clk.pushSprite(31, 25);
  clk.deleteSprite();

  //秒
  clk.createSprite(40, 20);
  clk.fillSprite(TFT_WHITE);
  clk.loadFont(TIME_20);
  clk.setTextColor(TFT_BLACK, TFT_WHITE);
  int seconds = second();
  String secondStr = (seconds < 10 ? "0" : "") + String(seconds);
  clk.drawString(secondStr, 5, 0);
  clk.unloadFont();
  clk.pushSprite(120, 36);
  clk.deleteSprite();
  /***中间时间区***/

  /***顶部***/
  clk.loadFont(DATE_18);
  String weeks[7] = {"日", "一", "二", "三", "四", "五", "六"};
  String week = " 周" + weeks[weekday() - 1];

  //年月日 星期
  clk.createSprite(160, 22);
  clk.fillSprite(TFT_WHITE);
  clk.setTextColor(TFT_BLACK, TFT_WHITE);
  String str = String(year()) + "年" + String(month()) + "月" + String(day()) + "日" + week;
  clk.drawString(str, 2, 4);
  clk.unloadFont();
  clk.pushSprite(0, 0);
  clk.deleteSprite();

  /***顶部***/
}
void setup()
{
  Serial.begin(9600);
  tft.init();
  tft.setRotation(1); // 屏幕旋转方向0-3  镜像 4-7
  tft.fillScreen(TFT_BLACK);
  pinMode(D0, OUTPUT);
  digitalWrite(D0, HIGH);
  // wifi 配置 可添加多个
  wifis.addAP("wifi名称", "密码");
  wifis.addAP("wifi名称", "密码");
  loading(60); // 进度条加载到60
  if (wifis.run() == WL_CONNECTED)
  {
    Serial.println("connected wifi");
    loading(100); //进度条加载完成
    Udp.begin(localPort);
    setSyncProvider(getNtpTime);
    setSyncInterval(300);
    loading(maxLoad); //进度条加载完成
    tft.fillScreen(TFT_WHITE);
    TJpgDec.setJpgScale(1);
    TJpgDec.setSwapBytes(true);
    TJpgDec.setCallback(tft_output);
    tft.drawLine(30, 22, 30, 80, TFT_BLACK);
    tft.drawFastHLine(0, 22, 160, TFT_BLACK);
    tft.drawFastHLine(30, 57, 130, TFT_BLACK);
    weatherTime = millis();
    getCityWeater();
  }
  else
  {
  }
}
void loop()
{
  digitalClockDisplay();
  if (millis() - weatherTime > 300000)
  { // 5分钟更新一次天气
    weatherTime = millis();
    getCityWeater();
  }
  spaceAnimation();
  // scale.power_up();
}

/*-------- NTP code ----------*/

const int NTP_PACKET_SIZE = 48;     // NTP time is in the first 48 bytes of message
byte packetBuffer[NTP_PACKET_SIZE]; // buffer to hold incoming & outgoing packets

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 < 1500)
  {
    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();
}
bool parseUserData(String content, struct WeatherData *weatherData)
{
  //    -- 根据我们需要解析的数据来计算JSON缓冲区最佳大小
  //   如果你使用StaticJsonBuffer时才需要
  //    const size_t BUFFER_SIZE = 1024;
  //   在堆栈上分配一个临时内存池
  //    StaticJsonBuffer<BUFFER_SIZE> jsonBuffer;
  //    -- 如果堆栈的内存池太大,使用 DynamicJsonBuffer jsonBuffer 代替
  DynamicJsonDocument doc(1024);
  auto error = deserializeJson(doc, content);
  if (error)
  {
    Serial.print(F("deserializeJson() failed with code "));
    Serial.println(error.c_str());
    return false;
  }
  JsonObject obj = doc.as<JsonObject>();
  //复制我们感兴趣的字符串
  strcpy(weatherData->city, obj["results"][0]["location"]["name"]);
  strcpy(weatherData->weather, obj["results"][0]["now"]["text"]);
  strcpy(weatherData->temp, obj["results"][0]["now"]["temperature"]);
  strcpy(weatherData->udate, obj["results"][0]["last_update"]);
  //  -- 这不是强制复制,你可以使用指针,因为他们是指向“内容”缓冲区内,所以你需要确保
  //   当你读取字符串时它仍在内存中
  return true;
}
// 获取城市天气
void getCityWeater()
{
  //创建 HTTPClient 对象
  HTTPClient httpClient;
  // https 请求报400
  httpClient.begin(espClient, "api.seniverse.com", 80, "/v3/weather/now.json?key=Sy_3POubsgptOeUau&location=wenzhou&language=zh-Hans&unit=c", false);
  //启动连接并发送HTTP请求
  int httpCode = httpClient.GET();
  Serial.print("request weather data:");

  //如果服务器响应OK则显示
  if (httpCode == HTTP_CODE_OK)
  {
    Serial.println("request weather data success");
    String response = httpClient.getString();
    if (parseUserData(response, &weatherData))
    { //解析响应内容
      clk.createSprite(125, 22);
      clk.loadFont(WEATHER_16);
      clk.fillSprite(TFT_WHITE);
      clk.setTextColor(TFT_BLACK, TFT_WHITE);
      byte xpos = 3, ypos = 2;
      xpos += clk.drawString(weatherData.city, xpos, ypos);
      xpos += 4;
      xpos += clk.drawString(weatherData.weather, xpos, ypos);
      xpos += 5;
      clk.setTextColor(TFT_RED, TFT_WHITE);
      String temp = String(weatherData.temp) + "℃";
      xpos += clk.drawString(temp, xpos, ypos);
      clk.pushSprite(31, 62);
      clk.deleteSprite();
    }
  }
  else
  {
    Serial.print("request weather data fail:");
    Serial.println(httpCode);
  }
  httpClient.end(); //关闭ESP8266与服务器连接
}

物联沃分享整理
物联沃-IOTWORD物联网 » 太空人WiFi天气电子时钟:ESP8266 OLED 0.96英寸 8针显示

发表评论