太空人Wifi天气电子时钟(ESP8266+oled 8针0.96寸)

一、实现效果

太空人Wifi天气电子时钟(ESP8266+oled 8针0.96寸)_第1张图片

太空人Wifi天气电子时钟(ESP8266+oled 8针0.96寸)_第2张图片

 

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等配置

太空人Wifi天气电子时钟(ESP8266+oled 8针0.96寸)_第3张图片

太空人Wifi天气电子时钟(ESP8266+oled 8针0.96寸)_第4张图片

 (2)屏幕引脚插线

太空人Wifi天气电子时钟(ESP8266+oled 8针0.96寸)_第5张图片

具体接线对应如下:

TFT屏幕                    nodemcu

GND                             GND

VCC                              3V3

SCL                                 D5

SDA                                 D7

RES                                 D4

DC                                   D3

CS                                    D8

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

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

太空人Wifi天气电子时钟(ESP8266+oled 8针0.96寸)_第6张图片

太空人Wifi天气电子时钟(ESP8266+oled 8针0.96寸)_第7张图片

太空人Wifi天气电子时钟(ESP8266+oled 8针0.96寸)_第8张图片

 最终使用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  \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软件,并且安装)。只需修改几个地方就可以,如下所示:

太空人Wifi天气电子时钟(ESP8266+oled 8针0.96寸)_第9张图片

 

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

太空人Wifi天气电子时钟(ESP8266+oled 8针0.96寸)_第10张图片

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

#include 
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 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
// #include 

#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 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();
  //复制我们感兴趣的字符串
  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与服务器连接
}

你可能感兴趣的:(嵌入式开发,单片机,嵌入式硬件,esp8266,太空人时钟,8针oled0.96寸)