2018年8月18日 星期六

當 ESP8266 遇上中華電信 IoT 智慧聯網大平台 { 入門 - 03 } - 使用 Arduino + ESP8266 上傳 SHT31 溫溼度數據

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

經過前面幾篇網頁與中華電信 IoT 智慧聯網大平台 ( 網頁中簡稱 CHT IoT SP ) 接觸之後,知道了怎麼使用 Postman 和 ESP8266 AT 指令,確認 RESTful API 協定與 CHT IoT SP 互動的 Request 和 Response 格式的正確性,也能撰寫程式讀取與解析 CHT IoT SP 回傳的資料。

現在還缺的,就是知道怎麼上傳感測數據 ?

/*-/--*-*/*/*/*/***//-*-*-**-*/*-*-/*/*/*-*-/-////--/**/**--**/--///--//**----**//--**//**----***//*-**//*
接下來的內容若是有看不懂或是疑惑的地方,建議先行依序閱讀 [1][2][3], [4] 這幾篇網頁。
/*-/--*-*/*/*/*/***//-*-*-**-*/*-*-/*/*/*-*-/-////--/**/**--**/--///--//**----**//--**//**----***//*-**//*


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

建立一個新專案:

方便起見,這裡再重新建立一個新的 CHT IoT SP 專案;只有一個設備,設備包含兩個感測器,分別用來記錄與顯示 SHT31 的溫溼度值 ( 不懂專案建立,請參閱前面幾篇的說明 )。

專案建立後,可得類似下面的設備管理頁面;剛建立的感測器沒有數值,不需要先手動設定,下一節再直接用 Postman 指定即可

專案、設備和感測器建置完成之後的設備管理頁面
Postman 測試:

上傳數據的 RESTful API 協定很簡單,都是使用 HTTP POST 且採用相同的 URI

https://iot.cht.com.tw/iot/v1/device/${device_id}/rawdata

RESTful API HTTP POST 資料儲存協定列表
不過仔細看了上面資料之後就會發現到,只有協定描述不同而已!其實換湯不換藥,變化只在 Request 的 body 部分;依據其儲存目的,可以為單一感測器單一資料的儲存、單一感測器多筆資料儲存、多筆感測器單一筆資料儲存或多筆感測器多筆資料儲存...等,都只是 JSON 字串組合的變化而已!

body 中的幾個 key 欄位:time save 兩個欄位可省略不寫,使用系統時間設定與永久儲存作為預設值;其他欄位值則一定要寫不可省略。body 所使用字元數目,將是 headers  Content-Length 的欄位值。

headers 在 API 文件網頁上的格式範例可以直接用,但是實際在撰寫程式時只會留下真正需要的,其他的都會在不影響數據儲存功能的狀況下省略掉。

由於在此範例下只有兩個感測器,所以能夠一次上傳溫度與濕度兩個數據最好!但為了測試,我會先上傳 SHT31 的溫度值,然後再接著一次同時上傳溫度與濕度值,分別演示協定 "依平台接收時間儲存資料" 和 "儲存感測資料" ( 敏感的資料已經先遮掉;感測器數據只是演示用,故只暫時儲存 ( "save":false ),會被之後的資料取代掉 )。

不管是使用上面哪一個協定儲存感測器數據,Request 的 headers 使用的部分都是相同 (當然 ${PROJECT_KEY}body 的字串長度是不同的,要因資料而異做更改),不過現在不著墨在此上面,請將重點花在 body 裡面的 JSON 字串要怎麼寫!

下面需要填的部分就是粉色底線上面的 ${device_id}${PROJECT_KEY};填上這兩個值之後,就可以切換到 Body 的視窗

Postman - Headers 設定視窗
下面是以儲存 SHT31 溫度值作為例子。

JSON 字串格式,可以直接由 API 文件網頁中的 "依平台接收時間儲存資料" 協定裡的範例複製過來再做修改。

完成修改之後按下 "SEND" 按鈕,若格式正確則會馬上收到回傳 (由於回傳的 Response 格式裡只有 headers 的部分有東西 (只需要注意 Status 欄位), body 沒有內容)。

"依平台接收時間儲存資料" 操作範例與結果
 接下來是以一次同時儲存 SHT31 溫度與濕度值作為例子。

JSON 字串格式,可以直接由 API 文件網頁中的 "儲存感測資料" 協定裡的範例複製過來再做修改。

完成修改之後按下 "SEND" 按鈕,若格式正確則會馬上收到回傳 (由於回傳的 Response 格式裡只有 headers 的部分有東西 (只需要注意 Status 欄位), body 沒有內容)

"儲存感測資料" 操作範例與結果
上面的例子中,save 欄位都是 false,可讓這些值暫時儲存做為顯示,但不會永久儲存,而且當使用曲線圖時,這些值也不會出現在上面!

再一次提醒,Postman 所做的測試,只有 Body 頁面中的 JSON 字串能直接使用在程式,Headers 頁面裡 (甚至是 "Code" 按下出現的視窗內容) 都不能直接用。

ESP8266 AT 指令測試:

上面做完 Postman 的測試之後,熟悉 HTTP 格式的用戶,應該就可以直接跳到撰寫程式的步驟;想了解或是不熟的用戶,就繼續往下看!

這裡要做的,就是使用 AT 指令連線到 CHT IoT SP ,並直接發送 headers 加上 body 這兩個部分的 Request 字串出去。所以只要這裡發送的 Request 字串是正確的,那麼程式就不容易出錯!

HTTP POST Request =  Request headers + Request body(JSON 格式字串)

一旦 Request 字串成功發送之後,隨即而來就是接收 Response 回應的部分,而 Response 接收的部分有兩個選擇:
  • 不理會
    不理會的前提是 Request 發送是正確的,能確保感測資料無誤的儲存到 CHT IoT SP 上去,否則數據儲存不成功,可能無法得知!
  • 正常接收
    只需要接收與處理 headers 的部分,確認 Status 欄位回傳 200 無誤,即是正常處理完成儲存數據的動作
HTTP POST Response =  Response headers + Response body("")

因為數據儲存協定回傳的 Response body 部分是空白的,所以接收的只有 headers 的部分;這部分在撰寫程式時,處不處理看個人選擇。

下圖,建立與 CHT IoT SP 的 TCP 透傳通訊後 (1),把要發送的 Request 字串組合到下方輸入欄中按下 "Send" (2),在成功發送 Request 格式字串之後,就會得到 CHT IoT SP 的 Response 回應字串 (3)

ESP8266 AT 指令測試結果
比較 API 文件與上面範例應該不難發現,輸入欄中 Request 的 headers 欄位與 API 文件中的不同!但這不是故意混淆寫的,而是在這裡是盡量縮減字元數,只留下必要的欄位部分 (有時間的話可以自己加加減減這些欄位試試,看看輸出的結果與實際的動作是不是都是相同的?)

還有一點,上面漏掉說明了!就是 headersContent-Length 欄位值:這個欄位在 Postman 的 "Code" 按下的視窗中不會有,但是在上面與 API 文件中有,這代表 body 內容 (JSON 字串) 的字元數目,可能每次都不一樣,但是一定要有,否則會出現錯誤! ( 對照 3 的輸出內容,可知道其 body 是沒有資料的 )

到此,數據儲存的 Request 和 Response 的格式已都說明清楚,現在可以進入實際程式寫作的部分了!

參考電路圖:

參考電路圖如下圖所示。

其中,中間部分也可以使用 Arduino 開發板外接 ESP8266 無線模組來取代,其餘的接線不變。

參考電路圖
程式碼說明:

程式一開始會連線至 CHT IoT SP 並與其建立 TCP 透傳通訊,接著讀取 SHT31 溫溼度有效值,建立 Request 的 body 內容 (JSON 字串) 和 headers 然後發送等待 Response;Response 處不處理都可以,不影響結果!

提供的程式碼對於 Response 是採不處理的方式,但範例測試 ( 定時上傳數據;最下面有測試結果 ) 是有做處理以作為判斷數據傳成不成功的依據。需要加入 Response 處理的用戶可參考上一篇的程式碼自己加進去。

/*-/--*-*/*/*/*/***//-*-*-**-*/*-*-/*/*/*-*-/-////--/**/**--**/--///--//**----**//--**//**----***//*-**//*
環境設置:
  • 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: I2C 函式庫初始化
  • line 10: SHT31 初始化
  • line 13  ~ 14: 退出 ESP8266 透傳模式並確認 ESP8266 是否連接正確;若發生錯誤,LED 每秒閃爍1 次!
  • line 15: 關閉 TCP 連線*
  • line 16: 斷開與無線路由器 (AP) 的連接*
  • line 19 ~ 20: 建立與 CHT IoT SP 的 TCP Client 透傳通訊;若連線的 AT 指令出現錯誤,LED 每秒閃爍 2 次!
  • line 21 ~ 24: 主要執行發送 HTTP GET Request 格式字串;若出現錯誤,關閉與 AP 的連線,LED 每秒閃爍 4 次!
  • line 26: LED 常亮,表示完成 SHT31 溫溼度值上傳儲存的動作
*  重新關閉 TCP 連線並斷開與無線路由器的連接,可以讓之前的連線狀態完全與 AP 脫離,加快重新連線的速度與穩定性。
SHT31TCPPost_Demo02.ino, (01/06), 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
25
26
27
28
29
void setup() {
  Serial.begin( 115200 );
  swSerialOutput.begin( 115200 );
  delay( 3000 );

  pinMode( _builtin_led, OUTPUT );

  swSerialOutput.print( F("SoftwareSerial Ready!\r\n") );  
  Wire.begin();
  softResetSHT31();
  swSerialOutput.print( F("SHT31 Ready!\r\n") );

  if( !exitPassthroughMode() )
    ledErrorIndicator( 1 );
  closeTCPConnection();
  disconnectFromAP();

  swSerialOutput.print( F("\r\n>>>>>>>> START <<<<<<<<\r\n") );
  if( !station_createTCPPassthrough() )
    ledErrorIndicator( 2 );
  if( !postSHT31ToCHTIoT() ) {
    disconnectFromAP();
    ledErrorIndicator( 4 );
  }
  swSerialOutput.print( F("\r\n>>>>>>>> END <<<<<<<<\r\n") );
  digitalWrite( _builtin_led, HIGH );
}

void loop() {}

程式用到的標頭檔與變數宣告和定義
  • line 22 ~ 25: 儲存 SHT31 溫度與濕度值的結構
SHT31TCPPost_Demo02.ino, (02/06)
 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
#include <SoftwareSerial.h>
#include <Wire.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

//--** SHT31 預設的 IIC 地址 **--//
#define SHT31_IIC_ADDRESS   0x44

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

struct sht31_t{
  float temperature;
  float humidity;
} sht31 = { 0.0, 0.0 };

其他下面幾個一般輔助函式的程式碼,請參考前幾篇網頁中的說明。
SHT31TCPPost_Demo02.ino, (03/06), 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
46
47
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 stopRunning() {
  while(1) delay( 1000 );
}

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 );
  }
}

請參考前幾篇網頁中的說明。
SHT31TCPPost_Demo02.ino, (04/06), 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;
}

下面兩個函式負責 SHT31 的軟體重啟以及讀取溫溼度值的工作;成功讀取的溫溼度值會儲存到 sht31 結構成員中。相關程式碼的說明已列在其中,不再贅述!
SHT31TCPPost_Demo02.ino, (05/06), SHT31()
 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
void softResetSHT31() {

  // 確認此 I2C 地址是否存在 SHT31
  Wire.beginTransmission( SHT31_IIC_ADDRESS );
  if( Wire.endTransmission() != 0 ) {
      swSerialOutput.print( F("SHT31-D not found!\r\n") );
      ledErrorIndicator(3);
  }

  // soft reset 0x30A2
  Wire.beginTransmission( SHT31_IIC_ADDRESS );
  Wire.write( 0x30 );
  Wire.write( 0xA2 );
  Wire.endTransmission();
  delay(10);
}

bool readTempHumSHT31() {

  Wire.beginTransmission( SHT31_IIC_ADDRESS );
  // 16-bit command byte
  // 0x2C06: high repeatability measurement with clock stretching enabled
  Wire.write( 0x2C );
  Wire.write( 0x06 );
  Wire.endTransmission();
  delay(300);

  // for clock stretching enabled
  Wire.beginTransmission( SHT31_IIC_ADDRESS );
  Wire.endTransmission();

  // 取得 6-byte 的資料
  // temp msb, temp lsb, temp crc, humi msb, humi lsb, humi crc
  Wire.requestFrom( SHT31_IIC_ADDRESS, 6 );
  if( Wire.available() == 6 ) {
    uint8_t rawdata[6] = {0};
    rawdata[0] = Wire.read(); rawdata[1] = Wire.read(); rawdata[2] = Wire.read();
    rawdata[3] = Wire.read(); rawdata[4] = Wire.read(); rawdata[5] = Wire.read();
    int temp = (rawdata[0] << 8) + rawdata[1];
    sht31.temperature = -45.0 + (175.0 * temp / 65535.0);
    //float fTemp = (cTemp * 1.8) + 32.0;
    sht31.humidity = (100.0 * ((rawdata[3] * 256.0) + rawdata[4])) / 65535.0;
    return true;
  }
  return false;
}

整個程式最主要的部分,負責呼叫取得 SHT31 的溫溼度、合成 headers 和 body 兩部分欄位資料形成 HTTP POST Request 格式字串發送給 CHT IoT SP,結束時離開透傳並關閉 TCP 連線。

postSHT31ToCHTIoT
  • line  2: 讀取 SHT31 溫溼度值;成功返回 true,失敗返回 false
  • line  4 ~ 8: 建立 Request body 所需要的內容 ( JSON 字串)
  • line 10 ~ 14: 建立 Request headers 所需要的欄位資料 (字串);裡面缺少兩個資料,請依實際專案中所提供的值填進去
  • line 16 ~ 17: 輸出 Request headersbody 的實際內容到 SoftwareSerial
  • line 18 ~ 19: 輸出 Request headers 和 body 的實際內容到 ESP8266 轉發到 CHT IoT SP
  • line 21 ~ 24: 若需要檢查 Response,程式碼可加在此處
SHT31TCPPost_Demo02.ino, (06/06), postSHT31ToCHTIoT()
 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
bool postSHT31ToCHTIoT() {
  if( readTempHumSHT31() ) {
    swSerialOutput.println( F("Request ->:") );
    String BODY = "[{\"id\":\"SHT31_Temperature\",\"save\":true,\"value\":[\"";
    BODY += String( sht31.temperature );   // Temperature value of the SHT31
    BODY += "\"]},{\"id\":\"SHT31_Humidity\",\"save\":true,\"value\":[\"";
    BODY += String( sht31.humidity );      // Humidity value the the SHT31
    BODY += "\"]}]";
    
    String HEADERS = "POST /iot/v1/device/##########/rawdata HTTP/1.1\r\nHost:iot.cht.com.tw\r\nContent-Type: application/json\r\nContent-Length:";
    //                        填入設備編號  ^^^^^^^^^^
    HEADERS += String( BODY.length() );
    HEADERS += "\r\nCK:******************\r\n";
    //                 ^^^^^^^^^^^^^^^^^^  填入 ${PROJECT_KEY} 或 ${DEVICE_KEY}

    swSerialOutput.println( HEADERS );
    swSerialOutput.print( BODY);
    Serial.println( HEADERS );
    Serial.print( BODY );

    //--** Check HTTP Response **--//
    // 
    // code here
    // 
    exitPassthroughMode();  // +++
    closeTCPConnection();   // AT+CIPCLOSE
    return true;
  } // if
  return false;
}

完成編譯之後上傳程式執行,可得到類似下面的輸出與結果 (點擊可放大看原圖)

程式碼測試結果
網頁所提供的程式碼只有單次上傳的功能,省略了 Response 確認與定時上傳數據的功能。

不過使用上面所提供的函式,很容易就能實現定時上傳數據的功能,在此不再贅述!

一旦加入了 Response 確認與定時上傳數據的功能之後,可得到類似下面的輸出結果

定時數據儲存與 Response 確認
將這些數據配合使用 CHT IoT SP  "應用服務 / 儀表板" 的功能頁面


CHT IoT SP / 應用服務 / 儀表板
可在其中加入很多小工具在儀表板區域中,例如 "數據折線圖""即時數據"

CHT IoT SP / 應用服務 / 儀表板 / 新增小工具
讓數據資料以類似下面的方式作呈現與即時顯示

CHT IoT SP / 應用服務 / 儀表板
結論:

經過幾篇網頁撰寫過程中與中華電信 IoT 智慧聯網大平台實際的相處下,發現它的功能還不少!像是能源、交通、安防和建築/家庭領域服務,和 AI、大數據和區塊鍊的智慧服務,值得花一些時間去發掘!

看一下硬體也已經支援了 NB-IoT ( 公司行號比較好申請 SIM 卡,個人不知道什麼時可以申請 ? ) ... 等,所以一個平台就包含了幾乎現在物聯網會用到技術服務,只要它不要虎頭蛇尾,持續不斷的改善與更新,就會是未來一個物聯網學習、練功和實務應用的好選擇!


<< 部落格相關文章 >>

沒有留言:

張貼留言

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

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

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