建立Arduino的Socket即時通訊程式(二)

延續上一篇貼文,本單元的程式修改自Socket.io-v1.x-Library程式庫的“Hello_time”範例,Arduino和Node.js的socket.io建立連線之後,將每隔5秒發送一個事件訊息詢問Node.js目前的時間,Node.js將在收到訊息之後回覆一個事件訊息給Arduino。

Arduino與Node.js的socket訊息交換

實驗材料:

  • Arduino Uno板 × 1
  • 採用W5100晶片的乙太網路擴展板 × 1
  • 執行Node.js的電腦或控制板(如:樹梅派) × 1

Arduino Uno + W5100乙太網路卡 + Raspberry Pi

撰寫Arduino的Socket即時通訊程式

Socket.io-v1.x-Library程式庫支援三種網路晶片,每個晶片都需要引用不同的程式庫,因此,SocketIOClient.h檔案透過#ifdef(代表“if defined”,「若有定義」)和#endif(代表“end if”,「結束if區塊」)前置處理指令,來決定要引用哪些程式庫:

#ifdef和$endif處理指令

所以,Arduino程式必須依照網路晶片類型,在開頭使用#define指令定義W5100, ESP8266或者ENC28J60。本文採用W5100晶片的乙太網路擴展板,程式開頭需要先定義W5100(後面不需要設定任何值),再引用SocketIOClient.h程式庫:

使用#define指令定義W5100

接著宣告SocketIO的程式物件,以及一些變數:

SocketIOClient client;  // 定義SocketIO物件

// 定義乙太網路卡的MAC(實體)位址
byte mac[] = { 0xAA, 0x00, 0xBE, 0xEF, 0xFE, 0xEE };
// 定義連線的伺服器主機IP位址和埠號
char hostname[] = "192.168.1.19";
int port = 5438;

unsigned long previousMillis = 0;  // 暫存前次執行時間 
long interval = 5000;    // 預設間隔5秒

// 取用在SocketIOClient.cpp檔案中定義的一些變數,
// extern代表“external”(外在),這些變數的用途請參閱下文說明。
extern String RID;
extern String Rname;
extern String Rcontent;

使用extern關鍵字宣告的變數,代表該變數存在於外部程式檔,在程式編譯過程中,編譯器會自行找尋定義該變數的程式檔。

client物件的connect()方法用於連結遠端伺服器,在底下的setup()函式中,程式將在連結成功之後,發送一則socket訊息:

setup()函式

使用F()函式將字串存入Flash記憶體

上面程式裡的序列輸出字串敘述,採用F()函式包裝字串。F()函式能把字串存入Flash(程式記憶體)區域,底下兩個敘述都會在「序列埠監控視窗」輸出“Hello!”:

用F()函式包裝字串

差別在於左邊的寫法,字串會佔用主記憶體,右邊的寫法不會。

在loop()函式中監聽是否有新的Socket訊息

底下的程式將每隔5秒,透過socket送出“Time please?”(請問時間)訊息給Node.js伺服器程式,並且在每一次loop()迴圈中,執行client.monitor()看看是否有新的訊息:

loop()函式

如果有新的訊息,即可從RID, Rname和Rcontent這三個外在(extern)變數,取得socket的事件名稱、訊息名稱和訊息內容

完整的Arduino程式碼如下:

#define W5100
#include <SocketIOClient.h>

SocketIOClient client;

byte mac[] = { 0xAA, 0x00, 0xBE, 0xEF, 0xFE, 0xEE };
/*
// 若採用固定IP(靜態位址),請自行設定IP位址和閘道位址:
IPAddress ip(192,168,1,25);
IPAddress subnet(255,255,2255,0);
IPAddress gateway(192,168,1,1);
*/

char hostname[] = "192.168.1.19";
int port = 5438;

extern String RID;
extern String Rname;
extern String Rcontent;

unsigned long previousMillis = 0; 
long interval = 5000; 
void setup() {
	Serial.begin(9600);
   
    Ethernet.begin(mac);
   /*
   // 若採用固定IP,請寫成:
	Ethernet.begin(mac, ip, gateway, subnet);
  */

	if (!client.connect(hostname, port)) {
		Serial.println(F("Not connected."));
   }
   
	if (client.connected()) {
		client.send("connection", "msg", "Oh~Yeah!!");
	} else {
		Serial.println(F("Connection Error"));
		while(1);
	}
}

void loop() {
	unsigned long currentMillis = millis();
	if(currentMillis - previousMillis > interval) {
	  previousMillis = currentMillis; 
	  client.send("atime", "msg", "Time please?");
	}

    // 監聽是否有新的訊息
	if (client.monitor()) {
		Serial.println(RID);
		if (RID == "atime" && Rname == "time") {
			Serial.print("Time is ");
			Serial.println(Rcontent);
		}
	}
}

Node.js伺服器端的socket.io程式

本單元的Node.js伺服器端程式改自書本第5章的chat.js程式(位於ch5的socket資料夾),因為該專案路徑已經包含socket.io套件。完整的Node.js程式碼如下:

var io = require("socket.io");
var express = require("express");
var app = express();
app.use(express.static('www'));

var server = app.listen(5438, function(req, res) {
  console.log("網站伺服器在5438埠口開工了!");
});

var sio = io(server);

sio.on('connection', function(socket){
  console.log("Connected");

  // 接收'connection'事件訊息
  socket.on('connection', function (data) {
  console.log('來自Arduino的訊息:' + data.msg);
  });

  // 接收'atime'事件訊息
  socket.on('atime', function (data) {
  console.log('來自Arduino的訊息:' + data.msg);
  // 發送時間資料給前端
    socket.emit('atime', { 'time': new Date().toJSON() });
  });
});

需要補充說明的是new Date().toJSON()這個敘述,toJSON()方法會把日期物件資料轉換成ISO標準日期格式的簡短字串:

new Date().toJSON()

筆者將此Node程式命名成arduinoSocket.js存入ch5的socket資料夾,先執行此Node程式,再執行Arduino的socket程式,Node伺服器端的命令列(終端機)視窗將出現如下的訊息:

Node伺服器端的命令列(終端機)視窗

Arduino的「序列埠監控視窗」將呈現如下的訊息:

序列埠監控視窗

延伸閱讀

Posts created 467

32 thoughts on “建立Arduino的Socket即時通訊程式(二)

  1. 趙老師您好:

    書中5-30頁,Node.js是作網頁伺服器,瀏覽器為用戶端,兩者用socket.io建立即時連線。

    而在這個範例中,老師用Node.js是作網頁伺服器,Arduino UNO+W5100及ESP8266當作用戶端,兩者用Socket.io-v1.x-Library程式庫及socket.io建立即時連線。

    如果Arduino UNO+W5100及ESP8266當作網頁伺服器,瀏覽器為用戶端,兩者用Socket.io-v1.x-Library程式庫及socket.io建立即時連線,程式該如何撰寫呢?

    1. hi alex:

      Socket.io-v1.x-Library程式庫是前端,後端你可以用ESPSocket程式庫,我看了一下它的範例程式,有包含網頁檔案,也具備上傳檔案到快閃記憶體的功能,提供你參考,謝謝!

      thanks,
      jeffrey

  2. 趙老師你好:我不知道我哪裡做錯了,一直無法編譯過,以下是錯誤的訊息,是否能幫助我一下呢?

    In file included from /Users/eagle/Desktop/test_socket/test_socket.ino:2:0:
    /Users/eagle/Documents/Arduino/libraries/Socket.io-v1.x-Library-master/SocketIOClient.h:40:42: fatal error: ESP8266WiFi.h: No such file or directory
    #include //For ESP8266
    ^
    compilation terminated.
    exit status 1
    編譯時發生錯誤

    1. 本文的範例程式適用於W5100晶片的乙太網路卡,請問你有依照上文說明,修改SocketIOClient.h檔嗎?

      thanks,
      jeffrey

  3. Hello CUBIE!
    謝謝您精彩的補充教材!請問採用GPRS實現socket通訊合適嗎?Socket.io-v1.x-Library只支援三種晶片,如果想用Arduino+SIM900模組實現socket請問有推薦的library嗎?

    1. 拍謝,我沒用過GPRS模組。不過,GSM的通訊協定與Arduino程式庫都和乙太網路不同,即便是普通的Web程式也無法直接相互移植。

      thanks,
      jeffrey

  4. 老師您好 前篇的更改.h已經更改過了,但我編譯上面的ARDUINO程式,一直編譯失敗,會出現下面問題
    In file included from sketch_mar30a.ino:4:0:
    C:\Users\User1\Documents\Arduino\libraries\SocketIOClient/SocketIOClient.h:30:22: fatal error: Ethernet.h: No such file or directory
    #include
    ^
    compilation terminated.

    1. 請在你的程式碼開頭加入底下的敘述測試看看,我在1.8.1版編譯Arduino Uno程式沒問題。

      #define W5100
      #include "SocketIOClient.h"
      #include "Ethernet.h"
      #include "SPI.h"
      

      thanks,
      jeffrey

  5. 老師您好:
    文中未提到Ethernet模組連接電腦的方式,
    請問老師是利用什麼樣的環境連結的?node js伺服器的IP是利用固定IP來接收的?

    1. 通常用來當做伺服器端的設備, 都會設定成固定IP. 因為當你需要它的時候, 如果它在固定的位置, 你很容易的就能在同一個地方找到它.
      固定IP還可以分成二種類型, 一種是虛擬IP(常見的為:192.168.X.X, 用於區域網路), 另一種為實體IP(例如:61.219.146.X, 用於外部網路).
      區域網路的概念你可以把它想像成一棟大樓, 對大樓管理員而言, 假設你跟管理員說東西要交給B棟 9F(這個相當於虛擬IP)的潘先生, 他可以很輕易的知道東西要交到本大樓第二棟9F的住戶手裡; 但如果是跟外面的路人說要把東西交給B棟 9F的潘先生, 路人會一頭霧水, 是哪一棟大樓的B棟 9F?
      如果要讓路人也能知道正確的位置, 你必需要跟他講一個實際的地址(例如:XX市XX路378號9F, 這個就是實體IP), 這樣才能找到實際的位置.
      至於node.js伺服器要使用虛擬IP還是實體IP, 就要看你的需求而定了~~~

  6. 趙老師您好
    最近在作個實驗是想用EXCEL讀取馬達所轉的圈數
    並且由EXCEL傳送命令給馬達啟動,運轉至所指定圈數
    ESP8266與NODE.JS以socket.io照您書上的範例+已經完成彼此間的溝通
    問題卡在EXCEL能以甚麼方式與ESP8266或NODE.JS溝通?
    (PS EXCEL的VBA我還可以
    目前試過PLX-DAQ(須有線且一次只能一個COM不符合我的需求
    我的需求將來至少須一次用到5個ESP8266作末端,且須由EXCEL作運算SERVER端)
    苦惱中,想請問趙老師您有甚麼建議方向或思路嗎?
    感謝

  7. 趙老師您好,感謝您的回覆
    以HTTP通訊方式之前有實作過”可行” (EXCEL當client ESP8266當SERVER)
    但礙於
    1.HTTP是單向通訊(P5-27頁)無法”即時”回報SERVER當前的狀態
    (像是收到資料沒有?目前馬達已經轉到第幾圈?工作完成否?)給client端
    2.若以固定時間取查詢SERVER目前工作狀態
    會變成無法即時且EXCEL會在此查詢期間卡住,不符所需
    所以轉SOCKET.IO方式

    以SOCKET.IO作為EXCEL VBA與EPS8266之間的溝通方式
    (google不太到這樣的資料,或這是我google的面向完全不對@@)
    我這外行,除了EXCEL跟VBA及目前您目前兩本大作之外
    其他幾乎全外行

    不知趙老師能否指點一下方向?或我該去認識哪方面的知識來補足?
    感謝

    1. 我想到的方法有兩個,其一是採用雲端Excel方案。像微軟和Google都有推出雲端(在瀏覽器中運作)的試算表,我不知道它們的功能和「桌面」版的差別,如果它們的功能符合你的需求,應該也會提供JavaScript API,如此一來,就能運用WebSocket和其他HTML5的機能了。

      第二個想法是,假設有客戶提出類似的專案需求,我會用互動網頁來解決,因為我對Excel完全不熟,加上Excel的運算式、圖表和表格編輯等功能,JavaScript都做得到,像HandsonTable就是用JavaScript實作的試算表程式庫。

      或說回來,如果你的出發點,主要是因為熟悉Excel…那就是另一回事了。

      thanks,
      jeffrey

  8. 趙老師您好
    感謝您的回答
    您提的兩個方法我會去仔細研究研究
    PS 我的出發點確實是因為”相對熟悉”EXCEL,想要的東西能作出來夠用就好”
    但這次讀了您的兩本書,也google這方面的東西,才驚覺資訊的東西發展了非常多
    過去是夠用就好,現在要夠用要讀很多東西了

    您的兩本書讓我獲益良多,謝謝您

    PS 能否對 P3-41 Postman 這個軟體在您的網站上作些常用的實戰介紹 ?
    下載回來安裝後發現,英文介面加上網路協議不熟稔
    時在有點丈二金剛摸不著頭腦

  9. Google試算表(類似Excel的功能)和Google Docs(類似Word的功能)是採用Https的協定,你可以透過Google App Script來進行數據的傳輸。基於安全性理由,內容會被轉址到script.googleusercontent.com。所以如果ESP8266直接採用POST和GET的request會有問題。我之前是使用一個”HTTPSRedirect”的Library,你可以上Github搜尋”HTTPSRedirect”,內建的範例就是以Google雲端的服務做介紹。

  10. 想請問,若是使用學校網路這類擁有固定IP的網路,是否可以用W5100來達成連上arduino ethernet 伺服器的功能呢??謝謝您!!

    1. 可以,書本第15和16章的範例程式同時包固定(靜態)和動態IP的寫法。

      thanks,
      jeffrey

  11. 老師好,請問要如何設定connect time out的時間呢? 因為連線是否成功的等待時間太長了。謝謝您。

    1. 也許可以用比較時間差,在預期時間內沒有連線,就回覆無法連線之類的:

      byte connected = false;
      unsigned long previousMillis = 3000;  // 3秒
      :
      :
      unsigned long currentMillis = millis();
      if (currentMillis - previousMillis >= interval && connected == false) {
        // 等待時間到…    
      }
      

      thanks,
      jeffrey

  12. 老師好,這樣還是會卡住耶,因為我程式是寫有偵測到訊號才連線傳資料。

    1. 既然這樣的話,不是應該要等到資料準備完畢之後,再連線送出嗎?

      thanks,
      jeffrey

  13. 老師您好:
    我目前遇到在node.js視窗一直無法正確收到您文中提到的訊息,目前看到都是來自Arduino:undefined的訊息。

  14. 老師您好:
    我使用ENC28J60,依照前一個教學修改libraries後
    執行上面的編譯產生
    C:\Users\user\Documents\Arduino\libraries\SocketIOClient/SocketIOClient.h:31:10: fatal error: UIPEthernet.h: No such file or directory
    #include
    compilation terminated.
    exit status 1
    開發板 Arduino Mega or Mega 2560 編譯錯誤。

    我用的是Arduino 1.8.14版本

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

Related Posts

Begin typing your search term above and press enter to search. Press ESC to cancel.

Back To Top