raspberrypi 官網 raspberrypi 官網

互動 IoT 系統應用 - 上課教材

互動 IoT 系統應用 > 課程內容 > 第 11 章 - nodejs 與 websocket 網頁界面控制

第 11 章 - nodejs 與 websocket 網頁界面控制

上次更新日期 2022/12/02

我們前面學習了 python 來進行樹莓派的控制,事實上,還有其他方式可以直接控制樹莓派的 GPIO 等 IoT 的元件! 這邊我們就來測試一下使用 Node.js,使用類似 javascript 的程式碼,進行 LED 等元件的控制吧!

學習目標:

  1. 使用 nodejs filesystem 讀取外部網頁檔
  2. 使用 nodejs websocket 與樹莓派互動

11.1: 讀寫外部檔案的方式

我們在前一章一開始的 hellow world 展示的程式中,使用的是透過 http 模組來建立 web server 的方法。 在該程式當中,寫網頁得要一個一個使用 res.write("content") 才行!如果我有暨有的網頁檔可以顯示的話, 那能不能使用外部檔案來顯示呢?答案當然是可以的!主要透過的是 fs 這個模組的幫忙!

  • 讀取一個外部的網頁檔

我們先來編寫一個外部網頁檔,檔名為 myname.html 好了!檔案的內容長的有點像這樣:

 $ cd ~/nodejs
 $ vim myname.html
<html>
<head>
        <meta charset='utf-8' />
        <title>nodejs check</title>
</head>
<body>
<ul>
        <li>name: 鳥哥 蔡</li>
        <li>ID: 4090cxxx</li>
</ul>
</body>
</html>

這是一般網頁檔案的內容喔!包括編碼與展示。讀取一個外部的檔案,需要使用外部模組,這個讀取模組的使用方式如下:

var fs = require('fs');
var web1 = '';
fs.readFile( filename, 'utf8', function (err, data) {
     web1 = data.toString();
});

網路上面有很多的範本,大部分的範本都將網頁處理的工作放置到上述的 fs.readFile 內部,不過,鳥哥個人比較喜歡先將資料讀出, 之後可以放置到任何地方去應用!這樣處理起來比較愉快。另外,上面的『 data 』是屬於物件的一種,我們只需要轉成文字, 因此這裡就使用 data.toString() 來轉出。同時,由於 data 在函式內部,如果想要讓資料保留,就得要事先宣告全域變數, 這樣在未來使用上面,比較不會出現找不到內容的問題。

現在,讓我們建立名為 readfile-1.js 的檔案,準備來處理外部檔案讀取的任務:

 $ cd ~/nodejs
 $ vim readfs-1.js
// 先匯入相關的模組
var http = require('http');
var fs   = require('fs');

// 讀取外部檔案資料之用的變數與讀取方式
var web1 = '';
fs.readFile('myname.html', 'utf8', function (err, data) {
        web1 = data.toString();
});

// 定義好主機名稱與埠口,要讓外部瀏覽,就得要改成 0.0.0.0 才行!
var hostname = '0.0.0.0';
var port = 8080;

// 建立 req 與 res 兩個變數,其中 res 主要是與網頁輸出的內容有關喔!
// var server = http.createServer( function(req, res) { ... } );
var server = http.createServer( function (req, res) {
        res.statusCode = 200;
        res.setHeader('Content-Type', 'text/html');
        res.write(web1);
        res.end();
});

// 開始監聽網路服務!
server.listen( port, hostname );

 $ node readfile-1.js

之後如果開啟另外一個終端機,直接輸入『 curl http://localhost:8080 』或者是在外部的系統,打開瀏覽器, 在網址列輸入你的主機位址:『 http://your.ip.addr:8080 』,應該就能看到正確的網頁內文了!

  • 使用 appendFile 『新增』或『累加』資料到檔案去

有個很簡單的方法,直接打開檔案,檔案不存在就建立新檔案,檔案存在,就將資料累加到該檔案去! 加入的方法很簡單,語法有點像這樣:

fs.appendFile( filename, data_content, 'utf8', function(err) { ... });
# filename: 檔名,例如 'myfile.txt'
# data_content: 實際內容,可以用變數取代,也能直接輸入字串
# 'utf8': 編碼的意思
# function...: 有問題或者其他功能時,就進行的任務

實際寫一隻程式,有點像這樣:

 $ vim writefs-1.js
// 先匯入相關的模組
var fs   = require('fs');

// 設定好要寫入的資料
var data = "This is the first time to use nodejs filesystem module.\n";
data = data + "  Write to raspberry pi filesystem.\n";

// 讀取外部檔案資料之用的變數與讀取方式
// 寫入檔名為 mywrite.txt 當中
fs.appendFile('mywrite.txt', data, 'utf8', function (err) {
        if (err) throw err;
});

 $ node writefs-1.js

 $ cat mywrite.txt

執行完畢後,就可以出現一個 mywrite.txt 的檔案。只是,當你重複執行 node writefs-1.js 後,mywrite.txt 的內容會越來越大! 畢竟不是新建檔案,則是累加資料的緣故。如果想要 mywrite.txt 每次都是新的內容,那就得要改用 writeFile 了!

  • 使用 writeFile 每次都建立新檔案

writeFile 每次都可以新建檔案~如果檔名存在,就會刪除舊檔,然後建立一個新的!如果檔名不存在,就直接丟一個新的檔案出來! 使用上也很簡單!跟 appendFile 幾乎一模一樣喔!

例題 11.1.1: 透過 writeFile 取代上一隻腳本,檔名改為 writefs-2.js。寫入的檔案就稱為 mywrite-2.txt。

先了解怎麼建立檔案,未來如果有從 client 傳資料過來的情況時,我們就可以將該資料儲存起來了! 而且跟 apache 不一樣的是,因為是我們自己啟動的 webserver,所以,權限就不會是問題! nodejs 可以直接儲存我們需要的資料到該檔案中喔!

11.2: 使用 socket.io 跟樹莓派互動

終究是想要透過網路/網頁,讓我們可以跟 IoT 的元件互動,而網頁可以透過所謂的 websocket (網頁插槽檔) 的方式來實現即時互動的功能。

  • 簡單說明的 websocket 是什麼?

你可以想像某一個網頁本身就是 server/client 連動的情況,你在瀏覽器上面所進行的任何動作,Server 會立刻知道的意思! 一般來說,我們得要『送出瀏覽器要求』的動作,或者是透過類似 ajax 的部份非同步互動功能,server 才會知道用戶端現在做些什麼。 但是 websocket 的意思,則是同一個網頁, server/client 同時都知道裡面的變化!最常見到的,當然就是目前網頁版本的留天室! 你在瀏覽器丟什麼資料, server 立刻可以知道!大致的意義就是這樣。

上面這個 websocket 的功能 (對,就是個簡單定義的功能),可以使用 nodejs 的 socket.io 模組來實現! 不過,由於 websocket 需要 server/client 同時運作,所以 server/client 兩邊都需要有 socket.io 才行!不過別緊張, 用戶端的瀏覽器連線到我們的 web server 時,我們不是會傳送網頁過去嘛?此時,就可以告知用戶端直接使用 server 提供的 socket.io 的 javascript 版本,這樣兩邊就立刻同步了!所以,用戶端同樣什麼事都不用做!網頁可以直接下載使用 socket.io 的 script 的!

  • 1. 先建立簡單首頁檔

我們先透過前一個小節的功能,當使用者連線到我們的 nodejs 網站時,網站會提供名為 webindex.html 檔案內容給用戶! 這個 webindex.html 的內容一開始設計簡單一點,內容會有點像這樣:

 $ cd ~/nodejs
 $ vim webindex.html
<html>
<head>
        <meta charset='utf-8' />
        <title>control your LED</title>
</head>
<body>
        <h1>控制你的 LED 燈開還是關</h1>
        <form id='myform'>
                <input type='checkbox' id='myled' />LED 開
        </form>
</body>
</html>
  • 2. 建立回應網頁內容的 webserver.js

接下來,讓我們來處理一下建立 nodejs 網站的腳本。我們在前一章的 hello world 以及前一小節的讀取外部檔案, 大概知道如何建立網站內容了!現在,將 readfs-1.js 另存新檔成為 webserver.js,但是得要加入 webindex.html 的內容!

 $ cp readfs-1.js webserver.js
 $ vim webserver.js
....
fs.readFile('webindex.html', 'utf8', function (err, data) {
        if (err) {
                web1 = "404 cannot find content";
        } else {
                web1 = data.toString();
        }
});
....

 $ node webserver.js

接下來你在網頁上面打開網址列,輸入 http://your.pi.ip:8080 之後,就可以看到類似底下的網頁內容!

websocket-01
  • 3. 開始安裝 socket.io 模組

我們要使用的 websocket 功能,透過 socket.io 模組來處理,所以直接安裝到本專案目錄下:

 $ npm install socket.io

 $ ll -d node_modules/*socket*
drwxr-xr-x 3 rasppi rasppi 4096 12月  2 10:04 node_modules/@socket.io
drwxr-xr-x 4 rasppi rasppi 4096 12月  2 10:04 node_modules/socket.io
drwxr-xr-x 3 rasppi rasppi 4096 12月  2 10:04 node_modules/socket.io-adapter

 $ find . -name socket.io.js
./node_modules/socket.io/client-dist/socket.io.js

上面出現的檔案,就是預計要提供給用戶端的 socket.io 的 javascript 腳本了!

  • 4. 修改 webindex.html,讓用戶端瀏覽器開始支援 socket.io

接下來,簡單的修改一下 webindex.html 的內容。由於 nodejs 需要 javascript 的程式設計技能, 所以暫時看不太懂也沒有關係!大概知道程式碼塞在哪裡就可以!

 $ vim webindex.html
<head>
....
    <script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
    <script>
        var socket = io();
        window.addEventListener("load", function() {
            var ledbox = document.getElementById('myled');
            ledbox.addEventListener("change", function() {
                socket.emit("myled", Number(this.checked)); //送回 0 或 1
            });
        });

        socket.on("myled", function(data) {
            document.getElementById('myled').checked = data;
            socket.emit("myled", data);
        });

    </script>
</head>
....

觀察一下上面的資訊,裡面的意思是,當 myled 這個網頁元件,也就是複選的選單項目上,若有任何的變更 (change) 時, 就請 socket.io 傳送一個名為 myled 的變數到 server 上,而變數的內容就是 0 與 1 !當沒有勾選,就會是 0, 勾選了,就會傳回 1 給 server 知道這樣。

  • 5. 開始修改 webserver.js

webserver.js 的修改比較複雜,不過,重點還是在接收來自用戶勾選的 0 或 1 的數值!修改的地方大概有:

 $ vim webserver.js
// 先匯入相關的模組
var http = require('http');
var fs   = require('fs');
var sio  = require('socket.io');
....
var io = sio(server);

// 開始監聽網路服務!
server.listen( port, hostname );

io.sockets.on('connection', function(socket) {
        var ledval = 0;
        socket.on('myled', function(data) {
                ledval = data;
                console.log(ledval);
        });
});

大概這樣!要注意,你得要在 http 建立服務之後,並且在 server.listen 之前,才加入 var io = sio(server) 這段, 否則, nodejs 會無法偵測到用戶端傳來的訊息喔!接下來就來測試吧!

  • 6. 測試互動

互動很簡單,在樹莓派上面直接執行『 node webserver.js 』在用戶端使用瀏覽器,一直點選、取消對話框, 你的 server 就會出現如下的示意畫面:

websocket-01

你就可以發現,server 會一直發現到用戶端有傳來 1 或 0 的數據。接下來,就讓我們應用這個數據來處理 LED 燈吧!

11.3: 當週實做

實作:透過 GPIO 的 LED 燈訊號,透過網頁來控制 LED 的發亮與否

首先,在硬體部份,我們沿用上一章的介紹, 透過 J8:7, J8:9 的處理,讓 LED 正極接到 J8:7 (GPIO4) 以及 LED 負極接到 J8:9 (接地)。然後,思考一下怎麼接上 webserver.js 的連動,你可以這樣考慮一下:

  • 匯入 gpio 的 onoff 模組
  • 新增 LED 變數,使用 gpio 模組的輸出功能
  • 在 socket.on 的函數內,加入 LED.writeSync(???),放入正確的變數資料即可。
  • 當重新執行 webserver.js 時,透過網頁的控制,你就可以發現 LED 會變亮、關閉了!

透過 socket.io 模組的支援,可以實現透過網頁直接控制樹莓派了!

  • 參考資料
  1. W3C schools 關於 Node.js 與樹莓派的教材:
    https://www.w3schools.com/nodejs/nodejs_raspberrypi.asp
  2. Node.js 官方網站:
    https://nodejs.org/en/
  3. Node.js 官方網站對於 fs.readFile 的說明:
    https://nodejs.org/api/fs.html#fsreadfilepath-options-callback
  4. Node.js 與 Socket.IO 建立網頁應用程式 App
    https://blog.gtwang.org/programming/socket-io-node-js-realtime-app/
  5. Socket.IO chat Get Started
    https://socket.io/get-started/chat/

...