延續上一篇貼文,本單元的程式修改自Socket.io-v1.x-Library程式庫的“Hello_time”範例,Arduino和Node.js的socket.io建立連線之後,將每隔5秒發送一個事件訊息詢問Node.js目前的時間,Node.js將在收到訊息之後回覆一個事件訊息給Arduino。
實驗材料:
- Arduino Uno板 × 1
- 採用W5100晶片的乙太網路擴展板 × 1
- 執行Node.js的電腦或控制板(如:樹梅派) × 1
撰寫Arduino的Socket即時通訊程式
Socket.io-v1.x-Library程式庫支援三種網路晶片,每個晶片都需要引用不同的程式庫,因此,SocketIOClient.h檔案透過#ifdef(代表“if defined”,「若有定義」)和#endif(代表“end if”,「結束if區塊」)前置處理指令,來決定要引用哪些程式庫:
所以,Arduino程式必須依照網路晶片類型,在開頭使用#define指令定義W5100, ESP8266或者ENC28J60。本文採用W5100晶片的乙太網路擴展板,程式開頭需要先定義W5100(後面不需要設定任何值),再引用SocketIOClient.h程式庫:
接著宣告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訊息:
使用F()函式將字串存入Flash記憶體
上面程式裡的序列輸出字串敘述,採用F()函式包裝字串。F()函式能把字串存入Flash(程式記憶體)區域,底下兩個敘述都會在「序列埠監控視窗」輸出“Hello!”:
差別在於左邊的寫法,字串會佔用主記憶體,右邊的寫法不會。
在loop()函式中監聽是否有新的Socket訊息
底下的程式將每隔5秒,透過socket送出“Time please?”(請問時間)訊息給Node.js伺服器程式,並且在每一次loop()迴圈中,執行client.monitor()看看是否有新的訊息:
如果有新的訊息,即可從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標準日期格式的簡短字串:
筆者將此Node程式命名成arduinoSocket.js存入ch5的socket資料夾,先執行此Node程式,再執行Arduino的socket程式,Node伺服器端的命令列(終端機)視窗將出現如下的訊息:
Arduino的「序列埠監控視窗」將呈現如下的訊息:
趙老師您好:
書中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建立即時連線,程式該如何撰寫呢?
hi alex:
Socket.io-v1.x-Library程式庫是前端,後端你可以用ESPSocket程式庫,我看了一下它的範例程式,有包含網頁檔案,也具備上傳檔案到快閃記憶體的功能,提供你參考,謝謝!
thanks,
jeffrey
趙老師你好:我不知道我哪裡做錯了,一直無法編譯過,以下是錯誤的訊息,是否能幫助我一下呢?
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
編譯時發生錯誤
本文的範例程式適用於W5100晶片的乙太網路卡,請問你有依照上文說明,修改SocketIOClient.h檔嗎?
thanks,
jeffrey
Hello CUBIE!
謝謝您精彩的補充教材!請問採用GPRS實現socket通訊合適嗎?Socket.io-v1.x-Library只支援三種晶片,如果想用Arduino+SIM900模組實現socket請問有推薦的library嗎?
拍謝,我沒用過GPRS模組。不過,GSM的通訊協定與Arduino程式庫都和乙太網路不同,即便是普通的Web程式也無法直接相互移植。
thanks,
jeffrey
老師您好 前篇的更改.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.8.1版編譯Arduino Uno程式沒問題。
thanks,
jeffrey
老師您好:
文中未提到Ethernet模組連接電腦的方式,
請問老師是利用什麼樣的環境連結的?node js伺服器的IP是利用固定IP來接收的?
欸…就是家裡的區域網路,在Windows電腦上用固定IP連接。
thanks,
jeffrey
通常用來當做伺服器端的設備, 都會設定成固定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, 就要看你的需求而定了~~~
非常感謝Kevin的補充~
thanks,
jeffrey
趙老師您好
最近在作個實驗是想用EXCEL讀取馬達所轉的圈數
並且由EXCEL傳送命令給馬達啟動,運轉至所指定圈數
ESP8266與NODE.JS以socket.io照您書上的範例+已經完成彼此間的溝通
問題卡在EXCEL能以甚麼方式與ESP8266或NODE.JS溝通?
(PS EXCEL的VBA我還可以
目前試過PLX-DAQ(須有線且一次只能一個COM不符合我的需求
我的需求將來至少須一次用到5個ESP8266作末端,且須由EXCEL作運算SERVER端)
苦惱中,想請問趙老師您有甚麼建議方向或思路嗎?
感謝
Excel和VBA我完全外行。你的需求好像是要用Excel當前端,剛剛查了一下,Excel是可以透過VBA擷取Web資料,如微軟的Different Ways of Using Web Queries in Microsoft Office Excel 2003文件所示。如此看來,用HTTP通訊就能解決Excel和Node.js的溝通問題了。
thanks,
jeffrey
趙老師您好,感謝您的回覆
以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及目前您目前兩本大作之外
其他幾乎全外行
不知趙老師能否指點一下方向?或我該去認識哪方面的知識來補足?
感謝
我想到的方法有兩個,其一是採用雲端Excel方案。像微軟和Google都有推出雲端(在瀏覽器中運作)的試算表,我不知道它們的功能和「桌面」版的差別,如果它們的功能符合你的需求,應該也會提供JavaScript API,如此一來,就能運用WebSocket和其他HTML5的機能了。
第二個想法是,假設有客戶提出類似的專案需求,我會用互動網頁來解決,因為我對Excel完全不熟,加上Excel的運算式、圖表和表格編輯等功能,JavaScript都做得到,像HandsonTable就是用JavaScript實作的試算表程式庫。
或說回來,如果你的出發點,主要是因為熟悉Excel…那就是另一回事了。
thanks,
jeffrey
趙老師您好
感謝您的回答
您提的兩個方法我會去仔細研究研究
PS 我的出發點確實是因為”相對熟悉”EXCEL,想要的東西能作出來夠用就好”
但這次讀了您的兩本書,也google這方面的東西,才驚覺資訊的東西發展了非常多
過去是夠用就好,現在要夠用要讀很多東西了
您的兩本書讓我獲益良多,謝謝您
PS 能否對 P3-41 Postman 這個軟體在您的網站上作些常用的實戰介紹 ?
下載回來安裝後發現,英文介面加上網路協議不熟稔
時在有點丈二金剛摸不著頭腦
好的,我改天補充說明。
thanks,
jeffrey
Google試算表(類似Excel的功能)和Google Docs(類似Word的功能)是採用Https的協定,你可以透過Google App Script來進行數據的傳輸。基於安全性理由,內容會被轉址到script.googleusercontent.com。所以如果ESP8266直接採用POST和GET的request會有問題。我之前是使用一個”HTTPSRedirect”的Library,你可以上Github搜尋”HTTPSRedirect”,內建的範例就是以Google雲端的服務做介紹。
非常感謝Kevin的補充說明!
thanks,
jeffrey
想請問,若是使用學校網路這類擁有固定IP的網路,是否可以用W5100來達成連上arduino ethernet 伺服器的功能呢??謝謝您!!
可以,書本第15和16章的範例程式同時包固定(靜態)和動態IP的寫法。
thanks,
jeffrey
老師好,請問要如何設定connect time out的時間呢? 因為連線是否成功的等待時間太長了。謝謝您。
也許可以用比較時間差,在預期時間內沒有連線,就回覆無法連線之類的:
thanks,
jeffrey
老師好,這樣還是會卡住耶,因為我程式是寫有偵測到訊號才連線傳資料。
既然這樣的話,不是應該要等到資料準備完畢之後,再連線送出嗎?
thanks,
jeffrey
赵老师您好,请问arduino yun的wifi模块能否用这个库文件呢?
我手边没有Yun,再麻烦你自行测试看看。
thanks,
jeffrey
老師您好:
我目前遇到在node.js視窗一直無法正確收到您文中提到的訊息,目前看到都是來自Arduino:undefined的訊息。
應該是程式庫的bug,我改用socket.io-client程式庫,在ESP8266控制板測試沒問題。預計明天更新一篇文章說明。
更新:請參閱「建立Arduino的Socket即時通訊程式(三)」
thanks,
jeffrey
老師您好:
我使用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版本
編譯錯誤訊息指出,找不到UIPEthernet.h這個檔案,請下載這個程式庫重新編譯試試:
https://github.com/UIPEthernet/UIPEthernet