ESP8266開發之旅 應用篇② OLED顯示天氣屏

1.前言

    這一篇,博主將教你們怎麼去實現一個簡易版本的天氣助手。
    先來一個博主已經實現功能的圖片,以下:html

image

1.1 知識儲備

    本篇須要用到如下知識點:git

  • 運用到ArduinoJson V5庫,github傳送門,請讀者自行下載該庫放到Arduino安裝目錄(這裏直接使用,博主後面計劃會詳細講解該庫,敬請期待);
  • 運用到TCP Client,請參考 ESP8266開發之旅 網絡篇⑦ TCP Server & TCP Client
  • 運用到STA模式,請參考 ESP8266開發之旅 網絡篇④ Station——ESP8266WiFiSTA庫的使用
  • 運用到一鍵配網功能,請參考 ESP8266開發之旅 網絡篇⑧ SmartConfig——一鍵配網
  • 運用U8G2 OLED庫,請參考 玩轉u8g2 OLED庫,一篇就夠

2.實驗內容

    往心知天氣平臺請求當地城市天氣狀況,並在OLED上顯示天氣圖標、溫度值以及城市名稱;github

  • 關於心知天氣,請參考 wiki
  • 由於代碼中須要判斷天氣編碼,讀者也須要注意一下經常使用編碼,請參考 wiki

3.實驗準備

  • NodeMcu開發板;
  • Android 手機 + 博主一鍵配網App;
  • SSD1306 128X64 OLED顯示屏;
  • 若干線;

引腳鏈接:json

  • SSD1306直接鏈接到NodeMcu的IIC端口,請參考 ESP8266開發之旅 基礎篇③ ESP8266與Arduino的開發說明

4.實驗步驟

    把如下代碼燒錄進NodeMcu:api

/**
* 日期:2019/05/25
* 功能:OLED顯示天氣屏
* 做者:單片機菜鳥
**/
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ArduinoJson.h>
#include <U8g2lib.h>

#ifdef U8X8_HAVE_HW_SPI
#include <SPI.h>
#endif
#ifdef U8X8_HAVE_HW_I2C
#include <Wire.h>
#endif

#define LED D4
#define DEBUG //是否開啓debug功能

#ifdef DEBUG
#define DebugPrintln(message)    Serial.println(message)
#else
#define DebugPrintln(message)
#endif

#ifdef DEBUG
#define DebugPrint(message)    Serial.print(message)
#else
#define DebugPrint(message)
#endif

#define WEATHER_CODE_DAY_SUN "0" //晴(國內城市白天晴)
#define WEATHER_CODE_NIGHT_SUN "1" //晴(國內城市夜晚晴)
#define WEATHER_CODE_DAY_SUN1 "2" //晴(國外城市白天晴)
#define WEATHER_CODE_NIGHT_SUN2 "3" //晴(國外城市夜晚晴)
#define WEATHER_CODE_CLOUDY "4" //多雲
#define WEATHER_CODE_DAY_PARTLY_CLOUDY "5" //白天晴間多雲
#define WEATHER_CODE_NIGHT_PARTLY_CLOUDY "6" //夜晚晴間多雲
#define WEATHER_CODE_DAY_MOSTLY_CLOUDY "7" //白天大部多雲
#define WEATHER_CODE_NIGHT_MOSTLY_CLOUDY "8" //夜晚大部多雲
#define WEATHER_CODE_OVERCAST "9" //陰
#define WEATHER_CODE_SHOWER "10" //陣雨
#define WEATHER_CODE_THUNDERSHOWER "11" //雷陣雨
#define WEATHER_CODE_THUNDERSHOWER_WITH_HAIL "12" //雷陣雨伴有冰雹
#define WEATHER_CODE_LIGHT_RAIN "13" //小雨
#define WEATHER_CODE_MODERATE_RAIN "14" //中雨
#define WEATHER_CODE_HEAVY_RAIN "15" //大雨
#define WEATHER_CODE_STORM "16" //暴雨
#define WEATHER_CODE_HEAVY_STORM "17" //大暴雨
#define WEATHER_CODE_SEVERE_STORM "18" //特大暴雨
#define WEATHER_CODE_ICE_RAIN "19" //凍雨
#define WEATHER_CODE_SLEET "20" //雨夾雪
#define WEATHER_CODE_SNOW_FLURRY "21" //陣雪
#define WEATHER_CODE_LIGHT_SNOW "22" //小雪
#define WEATHER_CODE_MODERATE_SNOW "23" //中雪
#define WEATHER_CODE_HEAVY_SNOW "24" //大雪
#define WEATHER_CODE_SNOW_STORM "25" //暴雪

#define SUN_DAY 0
#define SUN_NIGHT 1
#define SUN_CLOUD 2
#define CLOUD 3
#define RAIN 4
#define THUNDER 5

//聲明方法
bool autoConfig();
void smartConfig();
bool sendRequest(const char* host, const char* cityid, const char* apiKey);
bool skipResponseHeaders();
void readReponseContent(char* content, size_t maxSize);
void stopConnect();
void clrEsp8266ResponseBuffer(void);
bool parseUserData(char* content, struct UserData* userData);
void drawWeatherSymbol(u8g2_uint_t x, u8g2_uint_t y, uint8_t symbol);
void drawWeather(uint8_t symbol, int degree);
  
const unsigned long BAUD_RATE = 115200;// serial connection speed
const unsigned long HTTP_TIMEOUT = 5000;               // max respone time from server
const size_t MAX_CONTENT_SIZE = 500;                   // max size of the HTTP response
const char* host = "api.seniverse.com";
const char* APIKEY = "wcmquevztdy1jpca";        //API KEY
const char* city = "guangzhou";
const char* language = "zh-Hans";//zh-Hans 簡體中文  會顯示亂碼

int flag = HIGH;//默認當前滅燈
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);

WiFiClient client;
char response[MAX_CONTENT_SIZE];
char endOfHeaders[] = "\r\n\r\n";

long lastTime = 0;
// 請求服務間隔
long Delay = 20000;

// 咱們要今後網頁中提取的數據的類型
struct UserData {
  char city[16];//城市名稱
  char weather_code[4];//天氣現象code(多雲...)
  char temp[5];//溫度
};

/**
* @Desc 初始化操做
*/
void setup() {
  Serial.begin(BAUD_RATE);
  pinMode(LED,OUTPUT);
  digitalWrite(LED, HIGH);

  WiFi.disconnect();
  if(!autoConfig()){
    smartConfig();
    DebugPrint("Connecting to WiFi");//寫幾句提示,哈哈
    while (WiFi.status() != WL_CONNECTED) {
    //這個函數是wifi鏈接狀態,返回wifi連接狀態
       delay(500);
       DebugPrint(".");
    }
  }
  
  delay(1000);
  digitalWrite(LED, LOW);
  DebugPrintln("IP address: ");
  DebugPrintln(WiFi.localIP());//WiFi.localIP()返回8266得到的ip地址
  lastTime = millis();
  u8g2.begin();  
  u8g2.enableUTF8Print();
  //使能軟件看門狗的觸發間隔
  ESP.wdtEnable(5000);
}
  
/**
* @Desc  主函數
*/
void loop() {
  while (!client.connected()){
     if (!client.connect(host, 80)){
         flag = !flag;
         digitalWrite(LED, flag);
         delay(500);
         //喂狗
         ESP.wdtFeed();
     }
  }

  if(millis()-lastTime>=Delay){
   //每間隔20s左右調用一次
     lastTime = millis();
     if (sendRequest(host, city, APIKEY) && skipResponseHeaders()) {
       clrEsp8266ResponseBuffer();
       readReponseContent(response, sizeof(response));
       UserData userData;
       if (parseUserData(response, &userData)) {
          showWeather(&userData);
       }
     }
  }
  
   //喂狗
   ESP.wdtFeed();
}

/**
* 自動鏈接20s 超過以後自動進入SmartConfig模式
*/
bool autoConfig(){
  WiFi.mode(WIFI_AP_STA);     //設置esp8266 工做模式
  WiFi.begin();
  delay(2000);//剛啓動模塊的話 延時穩定一下
  DebugPrintln("AutoConfiging ......");
  for(int index=0;index<10;index++){
    int wstatus = WiFi.status();
    if (wstatus == WL_CONNECTED){
      DebugPrintln("AutoConfig Success");
      DebugPrint("SSID:");
      DebugPrintln(WiFi.SSID().c_str());
      DebugPrint("PSW:");
      DebugPrintln(WiFi.psk().c_str());
      return true;
    }else{
      DebugPrint(".");
      delay(500);
      flag = !flag;
      digitalWrite(LED, flag);
    } 
  }
  DebugPrintln("AutoConfig Faild!");
  return false;
}

/**
* 開啓SmartConfig功能
*/
void smartConfig()
{
  WiFi.mode(WIFI_STA);
  delay(1000);
  DebugPrintln("Wait for Smartconfig");
  // 等待配網
  WiFi.beginSmartConfig();
  while (1){
    DebugPrint(".");
    delay(200);
    flag = !flag;
    digitalWrite(LED, flag);
    
    if (WiFi.smartConfigDone()){
      //smartconfig配置完畢
      DebugPrintln("SmartConfig Success");
      DebugPrint("SSID:");
      DebugPrintln(WiFi.SSID().c_str());
      DebugPrint("PSW:");
      DebugPrintln(WiFi.psk().c_str());
      WiFi.mode(WIFI_AP_STA);     //設置esp8266 工做模式
      WiFi.setAutoConnect(true);  // 設置自動鏈接
      break;
    }
  }
}

/**
* @發送請求指令
*/
bool sendRequest(const char* host, const char* cityid, const char* apiKey) {
  // We now create a URI for the request
  //心知天氣
  String GetUrl = "/v3/weather/now.json?key=";
  GetUrl += apiKey;
  GetUrl += "&location=";
  GetUrl += city;
  GetUrl += "&language=";
  GetUrl += language;
  // This will send the request to the server
  client.print(String("GET ") + GetUrl + " HTTP/1.1\r\n" +
               "Host: " + host + "\r\n" +
               "Connection: close\r\n\r\n");
  DebugPrintln("create a request:");
  DebugPrintln(String("GET ") + GetUrl + " HTTP/1.1\r\n" +
               "Host: " + host + "\r\n" +
               "Connection: close\r\n");
  delay(1000);
  return true;
}
 
/**
* @Desc 跳過 HTTP 頭,使咱們在響應正文的開頭
*/
bool skipResponseHeaders() {
  // HTTP headers end with an empty line
  bool ok = client.find(endOfHeaders);
  if (!ok) {
    DebugPrintln("No response or invalid response!");
  }
  return ok;
}
 
/**
* @Desc 從HTTP服務器響應中讀取正文
*/
void readReponseContent(char* content, size_t maxSize) {
  size_t length = client.readBytes(content, maxSize);
  delay(100);
  DebugPrintln("Get the data from Internet!");
  content[length] = 0;
  DebugPrintln(content);
  DebugPrintln("Read data Over!");
  client.flush();//這句代碼須要加上  否則會發現每隔一次client.find會失敗
}
  
// 關閉與HTTP服務器鏈接
void stopConnect() {
  client.stop();
}
 
void clrEsp8266ResponseBuffer(void){
    memset(response, 0, MAX_CONTENT_SIZE);      //清空
}

bool parseUserData(char* content, struct UserData* userData) {
//    -- 根據咱們須要解析的數據來計算JSON緩衝區最佳大小
//   若是你使用StaticJsonBuffer時才須要
//    const size_t BUFFER_SIZE = 1024;
//   在堆棧上分配一個臨時內存池
//    StaticJsonBuffer<BUFFER_SIZE> jsonBuffer;
//    -- 若是堆棧的內存池太大,使用 DynamicJsonBuffer jsonBuffer 代替
  DynamicJsonBuffer jsonBuffer;
  
  JsonObject& root = jsonBuffer.parseObject(content);
  
  if (!root.success()) {
    Serial.println("JSON parsing failed!");
    return false;
  }
   
  //複製咱們感興趣的字符串
  strcpy(userData->city, root["results"][0]["location"]["name"]);
  strcpy(userData->weather_code, root["results"][0]["now"]["code"]);
  strcpy(userData->temp, root["results"][0]["now"]["temperature"]);
  //  -- 這不是強制複製,你可使用指針,由於他們是指向「內容」緩衝區內,因此你須要確保
  //   當你讀取字符串時它仍在內存中
  return true;
}

/**
 * 根據天氣接口返回的數據判斷顯示
 */
void showWeather(struct UserData* userData){
  if(strcmp(userData->weather_code,WEATHER_CODE_DAY_SUN) == 0 
  || strcmp(userData->weather_code,WEATHER_CODE_DAY_SUN1) == 0){
    drawWeather(SUN_DAY,userData->temp,userData->city);
  }else if(strcmp(userData->weather_code,WEATHER_CODE_NIGHT_SUN) == 0 
  || strcmp(userData->weather_code,WEATHER_CODE_NIGHT_SUN2) == 0 ){
    drawWeather(SUN_NIGHT,userData->temp,userData->city);
  }else if(strcmp(userData->weather_code,WEATHER_CODE_DAY_PARTLY_CLOUDY) == 0 
  || strcmp(userData->weather_code,WEATHER_CODE_NIGHT_PARTLY_CLOUDY) == 0 ){
    drawWeather(SUN_CLOUD,userData->temp,userData->city);
  }else if(strcmp(userData->weather_code,WEATHER_CODE_CLOUDY) == 0 
  || strcmp(userData->weather_code,WEATHER_CODE_DAY_MOSTLY_CLOUDY) == 0 
  || strcmp(userData->weather_code,WEATHER_CODE_NIGHT_MOSTLY_CLOUDY) == 0 
  || strcmp(userData->weather_code,WEATHER_CODE_OVERCAST) == 0){
    drawWeather(CLOUD,userData->temp,userData->city); 
  }else if(strcmp(userData->weather_code,WEATHER_CODE_SHOWER) == 0 
  || strcmp(userData->weather_code,WEATHER_CODE_LIGHT_RAIN) == 0 
  || strcmp(userData->weather_code,WEATHER_CODE_MODERATE_RAIN) == 0 
  || strcmp(userData->weather_code,WEATHER_CODE_HEAVY_RAIN) == 0 
  || strcmp(userData->weather_code,WEATHER_CODE_STORM) == 0
  || strcmp(userData->weather_code,WEATHER_CODE_HEAVY_STORM) == 0 
  || strcmp(userData->weather_code,WEATHER_CODE_SEVERE_STORM) == 0){
    drawWeather(RAIN,userData->temp,userData->city);
  }else if(strcmp(userData->weather_code,WEATHER_CODE_THUNDERSHOWER) == 0 
  || strcmp(userData->weather_code,WEATHER_CODE_THUNDERSHOWER_WITH_HAIL) == 0){
    drawWeather(THUNDER,userData->temp,userData->city);
  }else{
    drawWeather(CLOUD,userData->temp,userData->city); 
  }
}

void drawWeather(uint8_t symbol, char* degree,char* city)
{
  DebugPrintln(city);
  u8g2.clearBuffer();         // clear the internal memory
  //繪製天氣符號
  drawWeatherSymbol(0, 48, symbol);
  //繪製溫度
  u8g2.setFont(u8g2_font_logisoso32_tf);
  u8g2.setCursor(48+3, 42);
  u8g2.print(degree);
  u8g2.print("°C");   // requires enableUTF8Print()
  u8g2.setFont(u8g2_font_unifont_t_chinese3);

  u8g2_uint_t strWidth = u8g2.getUTF8Width(city);
  u8g2_uint_t displayWidth = u8g2.getDisplayWidth();
  
  u8g2.setCursor(displayWidth - strWidth - 5, 60);
  u8g2.print(city);
  u8g2.sendBuffer();        // transfer internal memory to the display
}

/**
 * 繪製天氣符號
 */
void drawWeatherSymbol(u8g2_uint_t x, u8g2_uint_t y, uint8_t symbol)
{
  // fonts used:
  // u8g2_font_open_iconic_embedded_6x_t
  // u8g2_font_open_iconic_weather_6x_t
  // encoding values, see: https://github.com/olikraus/u8g2/wiki/fntgrpiconic
  switch(symbol)
  {
    case SUN_DAY://太陽
      u8g2.setFont(u8g2_font_open_iconic_weather_6x_t);
      u8g2.drawGlyph(x, y, 69); 
      break;
    case SUN_NIGHT://太陽
      u8g2.setFont(u8g2_font_open_iconic_weather_6x_t);
      u8g2.drawGlyph(x, y, 66); 
      break;  
    case SUN_CLOUD://晴間多雲
      u8g2.setFont(u8g2_font_open_iconic_weather_6x_t);
      u8g2.drawGlyph(x, y, 65); 
      break;
    case CLOUD://多雲
      u8g2.setFont(u8g2_font_open_iconic_weather_6x_t);
      u8g2.drawGlyph(x, y, 64); 
      break;
    case RAIN://下雨
      u8g2.setFont(u8g2_font_open_iconic_weather_6x_t);
      u8g2.drawGlyph(x, y, 67); 
      break;
    case THUNDER://打雷
      u8g2.setFont(u8g2_font_open_iconic_embedded_6x_t);
      u8g2.drawGlyph(x, y, 67);
      break;      
  }
}

等待幾秒以後,模塊自動進入配網模式,請使用博主的app進行配置,配置成功後就會每隔一段時間去請求天氣接口,而後處理顯示在OLED。服務器

實驗結果:網絡

image

5.總結

    本篇博文內容簡單,主要是整合了esp8266和oled的應用,爲了優化請求網絡以及顯示時間,讀者可自行加入鬧鐘功能,具體請參考 玩轉 RTC時鐘庫 + DS3231app

相關文章
相關標籤/搜索