2018年8月11日 星期六

當 ESP8266 遇上中華電信 IoT 智慧聯網大平台 { 入門 - 02 } - 設備感測數據讀取與 (JSON) 解析

網頁最後修改時間:2018/08/11

延續上一篇 RESTful API "讀取設備所有感測器最新一筆感測資料" 協定  HTTP GET Request 和 Response 格式測試所得到的結果,改用 MCU 來完成。
/*-/--*-*/*/*/*/***//-*-*-**-*/*-*-/*/*/*-*-/-////--/**/**--**/--///--//**----**//--**//**----***//*-**//*
若接下來的網頁內容有所疑惑或是不懂的話,建議依序閱讀 [1][2][3] 這幾篇網頁。
/*-/--*-*/*/*/*/***//-*-*-**-*/*-*-/*/*/*-*-/-////--/**/**--**/--///--//**----**//--**//**----***//*-**//*


*********************************************************************************
此網頁所用的材料可自行準備,或選用新版本的升級套件
更多 ESP8266 相關商品,請至分類賣場
*********************************************************************************

測試接線圖:

測試的線路如下面所示,使用Arduino UNO + ESP8266 (Wi-Fi) 二合一開發板加上 USB 轉 TTL 模組或是線都可以,怎麼使用與上傳程式請看網頁[2]裡面的說明 。

若使用 Arduino 開發板外接 ESP8266,則 ESP8266 連接線路在部落格網頁裡面有,這裡不再另外贅述。
Arduino UNO + ESP8266 (WiFi) 開發板測試接線圖
程式碼與測試影片:

上一篇,成功的操作 AT 指令控制 ESP8266 建立與中華電信 IoT 智慧聯網大平台 (下面簡稱為 CHT IoT SP ) 的透傳通訊,調用其兩種不同的 RESTful API 協定讀取了設備所有感測器和單一感測器最後一筆的數據。藉由所得到的 Request 發送和 Response 接收的格式,下面選用 "讀取最新一筆感測資料" 協定來撰寫 Arduino (UNO) 的測試程式碼。

下面分為三個部分,每一個部分都有測試影片:
  1. 以取回 SHT31_Temperature 感測器最後一筆數據為例,展示如何發送 HTTP GET Request 格式與接收 HTTP GET Response 格式資料;只有正確接收到完整 HTTP GET Response 的資料,才能區分出 Response 格式中 headers 和 body 這兩個部分以進行下一步 JSON 字串解析。
  2. 以讀取 SHT31_Temperature 感測器最後一筆數據為例,展示如何解析 HTTP GET Response 格式裡屬於 body 中的 JSON 字串,並解析出其中的各個欄位值。
  3. 示範重複讀取並解析由 CHT IoT SP 取回的設備與單一感測器的 HTTP GET Response 的 JSON 字串。
解析 JSON 字串是重點,所以程式碼只說明第二部分。

/*--*//**---/*///**---*-*////***--*/*///***----*///--*/*///**--*/*//**--**/*//
* 讀取 SHT31_Temperature 最後一筆感測資料 - 原始輸出入資料:

下面的影片很簡單的展示了如何發送 HTTP GET Request headers 字串,讀取在 CHT IoT SPSHT31_Temperature 感測器的資料。

讀取回來的資料符合該協定 HTTP GET Response 的格式;其中,格式中 body 部分才是返回的感測器資訊部分,這是 JSON 格式的字串,主要就是解析這一部分。

完整的輸出入資料如下圖所示。

紅色框是輸出到 CHT IoT SP 的 HTTP GET Request 格式字串;藍色框與粉色框是一次接收到的 HTTP GET Response 格式字串,其中粉色框才是所要求的 SHT31_Temperature 的資料,只不過這一長串文字是 JSON 格式的字串,需要再進一步的解析!
讀取 SHT31_Temperature 最後一筆感測資料 - 原始輸出入資料
看到了整個讀取最新一筆感測資料的原始輸出入資料之後,下一小節就實際動手用程式碼來做解析的動作。

/*--*//**---/*///**---*-*////***--*/*///***----*///--*/*///**--*/*//**--**/*//
 讀取 SHT31_Temperature 最後一筆感測資料 - JSON 字串解析

結合上面的結果並加入 JSON 字串解析的功能,實際測試的結果如下影片所示。

HTTP GET Request 字串成功發送之後,CHT IoT SP 會回傳 HTTP GET Response 格式的字串。但不是全部回傳的部分都需要處理,只有最後的那一長串 JSON 字串 (屬於 body ) 才要!

類似的解析過程雷同於部落格另一篇網頁 "使用 Arduino IDE 開發 ESP8266 物聯網應用 - 取回 ThingSpeak 特定 Channel 和 Field 最後一筆資料" 。但因為年代久遠,當中所使用的 ArduinoJSON 函式庫經過多年來的精進,已經進入到 V6 beta 版階段 (相信不久就能 release),補正很多缺點與加入許多功能,好用許多!

但有一個重點需要注意:在 ArduinoJSON V6 未真正 Release 之前,下面的程式只能使用 V5.#.# 的版本編譯!



/*-/--*-*/*/*/*/***//-*-*-**-*/*-*-/*/*/*-*-/-////--/**/**--**/--///--//**----**//--**//**----***//*-**//*
環境設置:
  • Software
    • Arduino IDE v1.8.5
  • Library
    • ArduinoJSON v5.13.2
  • Hardware (下面兩種型式皆可)
    • Arduino UNO + ESP8266 (WiFi) 二合一開發板**
    • Arduino 開發板外接 ESP8266 無線模組**
** ESP8266 AT 韌體版本 AT: 1.2.0.0, SDK: v1.5.4.1
/*-/--*-*/*/*/*/***//-*-*-**-*/*-*-/*/*/*-*-/-////--/**/**--**/--///--//**----**//--**//**----***//*-**//*

下面的程式碼與影片中使用相同,當中移除一些冗長的註解並採分段說明,使用之前請自行整理。

先看一下主程式的部分;由於 loop() 裡面不需要程式碼,所以只說明 setup() 的部分。

setup
  • line  9 ~ 10: 退出 ESP8266 透傳模式並確認 ESP8266 是否連接正確;若發生錯誤,LED 每秒閃爍1 次!
  • line 11: 關閉 TCP 連線*
  • line 12: 斷開與無線路由器 (AP) 的連接*
  • line 16 ~ 17: 建立與 CHT IoT SP 的 TCP Client 透傳通訊;若連線的 AT 指令出現錯誤,LED 每秒閃爍 2 次!
  • line 18: 主要執行 HTTP GET Request與讀取 HTTP GET Response 和解析 JSON 字串的函式;這部分留待下面該函式的部分再做說明。
  • line 19: LED 常亮,表示正常完成連線、讀取和解析 SHT31_Temperature 感測資料
  • line 23: 程式會停在這裡不再往下執行
*  重新關閉 TCP 連線並斷開與無線路由器的連接,可以讓之前的連線狀態完全與 AP 脫離,加快重新連線的速度與穩定性。
CHTIoTTCPGetReply_JSONParse_Demo00.ino, (01/05), setup()+loop()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void setup() {
  Serial.begin( 115200 );
  swSerialOutput.begin( 115200 );
  delay( 3000 );

  pinMode( _builtin_led, OUTPUT );

  swSerialOutput.print( F("SoftwareSerial Ready!\r\n") );
  if( !exitPassthroughMode() )
    ledErrorIndicator( 1 );
  closeTCPConnection();
  disconnectFromAP();

  swSerialOutput.print( F("\r\n>>>>>>>> START <<<<<<<<\r\n") );
  swSerialOutput.print( F("-------------------\r\n SHT31_Temperature \r\n-------------------\r\n") );
  if( !station_createTCPPassthrough() )
    ledErrorIndicator( 2 );
  retrieveDataFromCHTIoT();
  digitalWrite( _builtin_led, HIGH );
  swSerialOutput.print( F(">>>>>>>> END <<<<<<<<\r\n") );
  
  disconnectFromAP();
  stopRunning();
}

程式用到的標頭檔與變數宣告和定義。
CHTIoTTCPGetReply_JSONParse_Demo00.ino, (02/05)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <SoftwareSerial.h>
#include <ArduinoJson.h>

//--** Software Serial **//
#define _rxpin  2
#define _txpin  3
SoftwareSerial swSerialOutput( _rxpin, _txpin );  // RX, TX
// 如果要輸出一些除錯訊息的話
#define DEBUG
#ifdef DEBUG
  #define dbgOutput( str )   (swSerialOutput.print( str ))
#else
  #define dbgOutput( str )
#endif

// 常亮表示 AT 指令執行正常,閃爍就是出現錯誤!
#define _builtin_led    13

static String rcv ="";

checkOK 此處的 checkOK() 函式與 "如何使用 MCU 建立與其他 ESP8266 的 UDP 透傳通訊" 裡用的不同,穩定性高且相對速度快!尤其是對於那些返回行數多的 AT 指令,非常有效且好用,分享給大家!

其他下面幾個一般輔助函式的程式碼,就不再多加說明!
CHTIoTTCPGetReply_JSONParse_Demo00.ino, (03/05), functions()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
bool checkOK( ) {
  uint8_t i = 0;
  while( !Serial.find( "OK" ) ) {
    if( ++i >= 6 ) return false;
  }
  return true;
}

bool exitPassthroughMode() {
  Serial.println( "AT" );
  if( !checkOK() ) {
    Serial.print( "+++" );
    delay( 1000 );
    Serial.println( "AT" );
    if( !checkOK() ) return false;
  }
  return true;
}

void closeTCPConnection() {
  Serial.println( "AT+CIPCLOSE" );
  checkOK();
}

void disconnectFromAP() {
  Serial.println( "AT+CWQAP" );
  checkOK();
}

void ledErrorIndicator( uint8_t times ) {
  static uint16_t offms[] = {900, 400, 250, 150, 100};
  while(1) {
    for( int i = 0; i < times; i++ ) {
      digitalWrite( _builtin_led, HIGH );
      delay( 100 );
      digitalWrite( _builtin_led, LOW );
      delay( offms[times - 1] );
    }
    delay( 1000 );
  }
}

void stopRunning() {
  while(1) delay( 1000 );
}

相信看過前面幾篇網頁的用戶對下面的程式碼不會太陌生,差在改用 TCP 連線而已,所以不再加以贅述!

這裡提醒一點,dbgOutput() 可以用來偵測此處的 AT 指令是否輸出正確。測試沒問題之後就可將程式開頭的 //#define DEBUG 註解掉,加快執行的速度!
CHTIoTTCPGetReply_JSONParse_Demo00.ino, (04/05), station_createTCPPassthrough()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
bool station_createTCPPassthrough() { // station
  swSerialOutput.print( F("Connecting to CHT IoT SP...") );

  dbgOutput( "\r\n" );
  // 建立 station
  Serial.println( "AT+CWMODE_CUR=1" );    // station mode
  dbgOutput( F("AT+CWMODE_CUR=1\r\n") );
  if( !checkOK() ) return false;
  // 連線到 ROUTER
  Serial.println( "AT+CWJAP_CUR=\"Proteus-WiFi\",\"asdfghjkl\"" );
  dbgOutput( "AT+CWJAP_CUR=\"Proteus-WiFi\",\"asdfghjkl\"\r\n" );
  if( !checkOK() ) return false;
  // 指定 station 的 IP、Gateway 和 Netmask
  Serial.println( "AT+CIPSTA_CUR=\"192.168.11.11\",\"192.168.11.1\",\"255.255.255.0\"" );
  dbgOutput( "AT+CIPSTA_CUR=\"192.168.11.11\",\"192.168.11.1\",\"255.255.255.0\"\r\n" );
  if( !checkOK() ) return false;
  // Establish TCP Transmission
  // <type>,<remote IP>,<remote port>
  Serial.println( "AT+CIPSTART=\"TCP\",\"iot.cht.com.tw\",80");
  dbgOutput( "AT+CIPSTART=\"TCP\",\"iot.cht.com.tw\",80\r\n");
  if( !checkOK() ) return false;
  // 設定傳輸模式為透傳模式
  Serial.println( "AT+CIPMODE=1" );
  dbgOutput( "AT+CIPMODE=1\r\n" );
  if( !checkOK() ) return false;
  // 開始傳送資料
  Serial.println( "AT+CIPSEND" );
  dbgOutput( "AT+CIPSEND\r\n" );
  if( !Serial.find( ">" ) ) return false;

  swSerialOutput.print( "done.\r\n\r\n" );
  return true;
}

整個程式最主要的部分,負責 HTTP GET Request 的發送、HTTP GET Response 的接收和 JSON 字串的解析。

retrieveDataFromCHTIoT
  • line  5: 發送 HTTP GET Request 字串給 ESP8266 並傳送至 CHT IoT SP 
  • line  7 ~ 56: 持續等待 CHT IoT SP HTTP GET Response 的回傳,並且處理 JSON 字串的解析
    • line 11 ~ 24: 處理 Request headers 的部分
      line 12 ~ 17 確認回傳的資料是否是請求成功的回傳,若不是則輸出提示並停止往下執行;line 20 ~ 24 確認 Request headers 部分是否傳送結束,若不是輸出提示並停止往下執行。
    • line 26 ~ 51: 接收並處理 Response body 部分,並解析出 JSON 字串裡的各欄位值
      line 30 ~ 31 配置 ArduinoJSON 需要的陣列和緩衝的大小;line 34 ~ 38 解析 JSON 字串並確認是否解析成功;line 41 ~ 49 輸出 JSON 字串解析之後的各欄位值。
CHTIoTTCPGetReply_JSONParse_Demo00.ino, (05/05), retrieveDataFromCHTIoT()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
void retrieveDataFromCHTIoT() {
  swSerialOutput.println( F("Request ->:") );
  // RESTful API "讀取最新一筆感測資料" 協定 HTTP GET Request 格式
  swSerialOutput.println( F("GET /iot/v1/device/6945661182/sensor/SHT31_Temperature/rawdata HTTP/1.1\r\nHost: iot.cht.com.tw\r\nCK: PK64AFUMECPJ8M2FZ0\r\n") );
  Serial.println(  "GET /iot/v1/device/6945661182/sensor/SHT31_Temperature/rawdata HTTP/1.1\r\nHost: iot.cht.com.tw\r\nCK: PK64AFUMECPJ8M2FZ0\r\n" );

  while(1) {
    if( Serial.available() ) {
      char status[32] = {0};

      // check HTTP status
      Serial.readBytesUntil( '\r', status, sizeof(status) );
      if( strcmp( status, "HTTP/1.1 200 " ) != 0)  {
        dbgOutput(F("Unexpected response: "));
        dbgOutput(status);  dbgOutput( "\r\n" );
        stopRunning();
      }

      // Skip HTTP headers
      char endOfHeaders[] = "\r\n\r\n";
      if (!Serial.find(endOfHeaders)) {
        dbgOutput(F("Invalid response")); dbgOutput( "\r\n" );
        stopRunning();
      }

      //--** 開始取得從 Serial 回傳的 JSON 字串,並直接解析 **--//      
      //{"id":"SHT31_Temperature","deviceId":"6945661182","time":"2018-08-01T08:20:33.361Z","value":["33.6"]}

      // Allocate JsonBuffer
      const size_t capacity = JSON_ARRAY_SIZE(1) + JSON_OBJECT_SIZE(4) + 100;
      DynamicJsonBuffer jsonBuffer(capacity);

      // Parse JSON object
      JsonObject& root = jsonBuffer.parseObject(Serial);
      if (!root.success()) {
        dbgOutput(F("Parsing failed!"));  dbgOutput( "\r\n" );
        stopRunning();
      }

      // Extract values
      swSerialOutput.println( F("JSON Parser <- Response:") );
      swSerialOutput.print( F("id=") );
      swSerialOutput.println(root["id"].as<char*>());
      swSerialOutput.print( F("deviceId=") );
      swSerialOutput.println(root["deviceId"].as<char*>());
      swSerialOutput.print( F("time=") );
      swSerialOutput.println(root["time"].as<char*>());
      swSerialOutput.print( F("value=") );
      swSerialOutput.println(root["value"][0].as<float>());
      break;
    } // if
  } // while

  exitPassthroughMode();  // +++
  closeTCPConnection();   // AT+CIPCLOSE
} // retrieveDataFromCHTIoT()

完成編譯之後上傳程式執行,可得到跟影片相同的輸出過程與結果。

截取其輸出畫面的結果,粉色框的部分就是上一段 retrieveDataFromCHTIoT() 執行 JSON 字串解析之後的結果
CHTIoTTCPGetReply_JSONParse_Demo00.ino 執行輸出畫面
到此就完成讀取 SHT31_Temperature 最後一筆感測資料與解析回傳的感測資料各個欄位值的演示與說明。

/*--*//**---/*///**---*-*////***--*/*///***----*///--*/*///**--*/*//**--**/*//
* 讀取並解析出設備與單一感測器的欄位數據:

若看懂上一小節的程式碼,那麼接下來下面這個影片中所展示的程式執行過程與結果,應該也不會太難實現!

程式將在連上 CHT IoT 之後,一次性連續完成兩種協定三個讀取的動作;先 "讀取 ( 二合一開發板 ) 設備所有感測器最新一筆感測資料",再來 "讀取 ( SHT31_Temperature ) 最新一筆感測資料",最後 "讀取 ( SHT31_Humidity ) 最新一筆感測資料" 。


結論:

雖然到現在的為止,所談論的部分只侷限在兩個 CHT IoT SP 的 RESTful API 協定中,但是起頭的方法都差不多,都可以使用前篇談及的基本的方法,先行驗證 Request 輸出以及取回 Response 的輸入。有了輸出入的東西之後,就能以本篇的程式碼為基礎,撰寫出屬於自己的解析程式。

知道了怎麼解決讀取數據的問題之後,接下來就來實際玩玩上傳與不一樣的 RESTful API 協定吧!


<< 部落格相關文章 >>

沒有留言:

張貼留言

留言屬名為"Unknown"或"不明"的用戶,大多這樣的留言都會直接被刪除掉,不會得到任何回覆!

發問問題,請描述清楚你(妳)的問題,別人回答前不會想去 "猜" 問題是什麼?

不知道怎麼發問,請看 [公告] 部落格提問須知 - 如何問問題 !