文章程式碼顯示

2018年1月30日 星期二

NodeMCU 教學 - 06:使用 WeMos D1 mini NodeMCU 網路爬蟲 抓取 JSON 天氣資訊

本篇功能為實現 D1 mini 抓取中央氣象局資料

本來想用 D1 mini 配合 DHT 11 來完成氣溫讀取的部分,但想想後覺得這樣的功能我在

《進階※應用篇》寫程式Arduino教學 - 04:使用 BLYNK 監控 Arduino 並使用手機遙控冷氣家電

已經做過了,重覆做一樣的東西好像沒什麼新鮮感,雖然是用不同微控器來做

之前在學 Python 的時候有接觸到網路爬蟲,簡而言之就是從網路上去抓取資料並且將資料進行解析再分析的過程,於是我就想到那我來研究看看

用微控器來做網路爬蟲如何?

但想想後又覺得這樣的做法有點不切實際,畢竟網路爬蟲就是要大量的從網路上面抓資料、解析、分析 ... 等等(也跟「大數據(big data)」沾上邊),對於運算效能很吃重,顯然這部分用電腦來做是比較合適的。

所以對於微控器來說,我們就不注重在「大量抓取」資料以及其分析的過程,而是抓取合適的資料並且將資料進行即時顯示

想來想去最實際的例子是從微控器連網抓取天氣資料,並且透過 LCD 進行顯示(類似這樣)。

再進一步想,能不能將數值顯示在 LCD 上不是重點(Arduino 與 LCD的使用顯然已經被當做初學 Arduino 都會學到的應用了 )

所以重點在於「微控器連網 - > 抓取合適資料 -> 解析資料 」 三項流程

最後面的顯示資料我決定使用前兩天才搞好的 D1 mini 與 BLYNK 來進行顯示(此部分在下一章進行,本章著重於上述的三項流程)

順帶一提,仔細觀察 NodeMCU 教學 - 03:WeMos D1 mini (NodeMCU) 與 Line 的連結 我們在觸發 IFTTT 上的 "事件後" 發現 IFTTT 是會回傳一些資訊回來的,如下圖


紅框中就是從 IFTTT 回傳,微控器收到的資料

我採用的天氣資料來源是中央氣象局,若要從氣象局透過像是這種利用網址(URL)來取得天氣資料該怎麼做呢?

我們按照之前做過的方式,想辦法先取得網址(URL),在電腦上用瀏覽器進行驗證

第一步我們要先加入氣象局的會員,接著登入會員取得授權碼




目前開放的資料清單


網址(URL)的部份我們用以下

https://opendata.cwb.gov.tw/api/v1/rest/datastore/ O-A0003-001?Authorization=你的授權碼



得到的數據是 JSON 格式的

JSON 是個以純文字為基底去儲存和傳送簡單結構資料,你可以透過特定的格式去儲存任何資料(字串,數字,陣列,物件),也可以透過物件或陣列來傳送較複雜的資料。一旦建立了您的 JSON 資料,就可以非常簡單的跟其他程式溝通或交換資料,因為 JSON 就只是純文字個格式。(引用自此)

密密麻麻。此處顯示了各個觀測站目前的參數,但資料量過多,我們進一步加入 locationName 進行篩選



得到的資料如上圖所示,明顯的少了很多,但這樣的顯示方式可讀性有點差。


我們進入 json.parser 將得到的資料貼上去,這網站可以幫我們進行 JSON 格式的排版

在 CWB_Data_Dictionary_V1.1.pdf 裡面有提到在授權碼之後可以輸入哪些參數來篩選數據(例如剛剛的 locationName)

https://opendata.cwb.gov.tw/api/v1/rest/datastore/O-A0003-001?Authorization=你的授權碼&locationName=新竹



得到的數據簡碼說明如上圖


我們著重在氣溫,上圖中我們可以看到得到的數據只剩下 TEMP 了。

所以現在的問題變成

「如何使用微控器達到同樣得到這些資料的效果?」

#include "ESP8266WiFi.h"

void get_Weather(); // Function 原型

// ===== Wifi setup =====
const char *ssid     = "你的WIFI帳號";
const char *password = "你的WIFI密碼";

// =====  setup =====
const char *host = "opendata.cwb.gov.tw";
const char *dataid = "O-A0003-001";
const char *locationName = "新竹";
const char *privateKey = "你的識別碼";
const char *elementName = "TEMP";

// ===== Hardware setup =====
const int buttonPin = D8;     // the number of the pushbutton pin
const int ledPin = D4;        // the number of the LED pin

// ===== Variables will change =====
int buttonState;             // the current reading from the input pin
int lastButtonState = LOW;   // the previous reading from the input pin
long lastDebounceTime = 0;  // the last time the output pin was toggled
long debounceDelay = 50;    // the debounce time; increase if the output flickers

void setup() 
{
  pinMode(buttonPin, INPUT);
  pinMode(ledPin, OUTPUT);
  Serial.begin(115200);
  delay(10);

  // ===== We start by connecting to a WiFi network =====
  Serial.println();
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);

  // ===== Wait for the connection, flashing the LED while we wait =====
  int led = HIGH;  
  while (WiFi.status() != WL_CONNECTED) {
    delay(200);
    digitalWrite(ledPin, led);
    led = !led;
    Serial.print(".");
  }
  digitalWrite(ledPin, LOW);

  // ===== Connect successful =====
  Serial.println("");
  Serial.println("WiFi connected");  
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}

void loop() 
{
  int reading = digitalRead(buttonPin);
  if (reading != lastButtonState) {
    lastDebounceTime = millis();
  }
  if ((millis() - lastDebounceTime) > debounceDelay) 
  {
    if (reading != buttonState) 
    {
      Serial.print("Button now ");
      Serial.println(HIGH == reading ? "HIGH" : "LOW");
      buttonState = reading;
      if (buttonState == LOW) {
        // ===== ================================
        get_Weather();
      }
    }
  }
  lastButtonState = reading;
}

void get_Weather()
{
  digitalWrite(ledPin, HIGH);
  Serial.print("Connecting to ");
  Serial.println(host);
  WiFiClient client;
  const int httpPort = 80;
  if (!client.connect(host, httpPort)) {
    Serial.println("Connection failed");
    return;
  }
  
  // Create a URI for the request
  String url = "/api/v1/rest/datastore/";
  url += dataid;
  url += "?Authorization=";
  url += privateKey;
  url += "&locationName=";
  url += locationName;
  url += "&elementName=";
  url += elementName;
  
  Serial.print("Requesting URL: ");
  Serial.println(url);
  
  // This will send the request to the server
  client.print(String("GET ") + url + " HTTP/1.1\r\n" +
               "Host: " + host + "\r\n" + 
               "Connection: close\r\n\r\n");

  // Read all the lines of the reply from server and print them to Serial,
  // the connection will close when the server has sent all the data.
  while(client.connected())
  {
    if(client.available())
    {
      String line = client.readStringUntil('\r');
      Serial.print(line);
    } else {
      // No data yet, wait a bit
      delay(50);
    };
  }
  
  Serial.println();
  Serial.println("closing connection");
  client.stop();
  digitalWrite(ledPin, LOW);
}



反藍處就是 D1 mini 收到的資料


抓下來的資料看起來是沒什麼大問題的

接下來的問題就是我們要如何把這樣的資料進行解碼,提取出我們需要的部分。

這裡我們用到 ArduinoJson 函式庫



#include "ESP8266WiFi.h"
#include "ArduinoJson.h"

void get_Weather(); // Function 原型
bool showWeather(char *json);

// ===== Wifi setup =====
// ===== Wifi setup =====
const char *ssid     = "你的WIFI帳號";
const char *password = "你的WIFI密碼";

// =====  setup =====
const char *host = "opendata.cwb.gov.tw";
const char *dataid = "O-A0003-001";
const char *locationName = "新竹";
const char *privateKey = "你的識別碼";
const char *elementName = "TEMP";//可用逗號隔開來取得多筆資料,詳細請見 CWB_Opendata_API.pdf

// ===== Hardware setup =====
const int buttonPin = D8;     // the number of the pushbutton pin
const int ledPin = D4;        // the number of the LED pin

// ===== Variables will change =====
int buttonState;             // the current reading from the input pin
int lastButtonState = LOW;   // the previous reading from the input pin
long lastDebounceTime = 0;  // the last time the output pin was toggled
long debounceDelay = 50;    // the debounce time; increase if the output flickers

static char respBuf[4096];

void setup() 
{
  pinMode(buttonPin, INPUT);
  pinMode(ledPin, OUTPUT);
  Serial.begin(115200);
  delay(10);

  // ===== We start by connecting to a WiFi network =====
  Serial.println();
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);

  // ===== Wait for the connection, flashing the LED while we wait =====
  int led = HIGH;  
  while (WiFi.status() != WL_CONNECTED) {
    delay(200);
    digitalWrite(ledPin, led);
    led = !led;
    Serial.print(".");
  }
  digitalWrite(ledPin, LOW);

  // ===== Connect successful =====
  Serial.println("");
  Serial.println("WiFi connected");  
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}

void loop() 
{
  int reading = digitalRead(buttonPin);
  if (reading != lastButtonState) {
    lastDebounceTime = millis();
  }
  if ((millis() - lastDebounceTime) > debounceDelay) 
  {
    if (reading != buttonState) 
    {
      Serial.print("Button now ");
      Serial.println(HIGH == reading ? "HIGH" : "LOW");
      buttonState = reading;
      if (buttonState == LOW) {
        // ===== ================================
        get_Weather();
      }
    }
  }
  lastButtonState = reading;
}

void get_Weather()
{
  digitalWrite(ledPin, HIGH);
  Serial.print("Connecting to ");
  Serial.println(host);
  WiFiClient client;
  const int httpPort = 80;
  if (!client.connect(host, httpPort)) {
    Serial.println("Connection failed");
    return;
  }
  
  // Create a URI for the request
  String url = "/api/v1/rest/datastore/";
  url += dataid;
  url += "?Authorization=";
  url += privateKey;
  url += "&locationName=";
  url += locationName;
  url += "&elementName=";
  url += elementName;
  
  Serial.print("Requesting URL: ");
  Serial.println(url);
  
  // This will send the request to the server
  client.print(String("GET ") + url + " HTTP/1.1\r\n" +
               "Host: " + host + "\r\n" + 
               "Connection: close\r\n\r\n");
  client.flush();

  // Collect http response headers and content from Weather Underground
  // HTTP headers are discarded.
  // The content is formatted in JSON and is left in respBuf.
  int respLen = 0;
  bool skip_headers = true;
  while (client.connected() || client.available()) {
    if (skip_headers) {
      String aLine = client.readStringUntil('\n');
      //Serial.println(aLine);
      // Blank line denotes end of headers
      if (aLine.length() <= 1) {
        skip_headers = false;
      }
    }
    else {
      int bytesIn;
      bytesIn = client.read((uint8_t *)&respBuf[respLen], sizeof(respBuf) - respLen);
      Serial.print(F("bytesIn ")); Serial.println(bytesIn);
      if (bytesIn > 0) {
        respLen += bytesIn;
        if (respLen > sizeof(respBuf)) respLen = sizeof(respBuf);
      }
      else if (bytesIn < 0) {
        Serial.print(F("read error "));
        Serial.println(bytesIn);
      }
    }
    delay(1);
  }
  
  Serial.println();
  Serial.println("closing connection");
  Serial.println();
  client.stop();

  if (respLen >= sizeof(respBuf)) {
    Serial.print(F("respBuf overflow "));
    Serial.println(respLen);
    return;
  }
  
  // Terminate the C string
  respBuf[respLen++] = '\0';
  Serial.print(F("respLen "));
  Serial.println(respLen);
  Serial.println("");
  //Serial.println(respBuf);

  if(showWeather(respBuf)) Serial.println("SUCCESS !!");
  digitalWrite(ledPin, LOW);  
}
/* =====================*/
// ===== showWeather ====
/* =====================*/
bool showWeather(char *json){
  
  StaticJsonBuffer<3> jsonBuffer;

  // Skip characters until first '{' found
  // Ignore chunked length, if present
  char *jsonstart = strchr(json, '{');
  Serial.println("jsonstart");
  Serial.println(jsonstart);
  Serial.println("");
  if (jsonstart == NULL) {
    Serial.println(F("JSON data missing"));
    return false;
  }
  json = jsonstart;

  // Parse JSON
  JsonObject& root = jsonBuffer.parseObject(json);
  if (!root.success()) {
    Serial.println(F("jsonBuffer.parseObject() failed"));
    return false;
  }

  // Extract weather info from parsed JSON
  int stationId = root["records"]["location"][0]["stationId"];
  Serial.print("stationId = ");
  Serial.println(stationId);
  float temp = root["records"]["location"][0]["weatherElement"][0]["elementValue"];
  Serial.print("Temp. = ");
  Serial.println(temp);  

  return true;
}

上述程式碼得到的結果如下



可以用 D1 mini 進行資料解析後,剩下的就是把資料傳到手機上,詳見下文

NodeMCU 教學 - 07:使用 WeMos D1 mini NodeMCU 網路爬蟲 JSON 天氣資料 與手機的連結


補充:

後來發現這個網站對 Json 解析更好用一些,把屬於 struct 或 array 做很清楚的區隔
JSON Editor Online


參考連結
Weather_API - 03_取得指定鄉鎮資訊
取得臺灣即時氣象資訊
ESP8266 get weather from Weather Underground
Arduino Json 库使用教程
json在arduino上的应用
Decoding and Encoding JSON with Arduino or ESP8266
Part 1. How to Use the Wunderground API

↓↓↓ 連結到部落格方針與索引 ↓↓↓

Blog 使用方針與索引