Beaglebone Black– 智能家居控制系統 LAS - 網頁服務器 Node.js 、Web Service、頁面 和 TCP 請求轉 UDP 發送


上一篇,純粹玩 ESP8266,寫入了 init.lua 能收發 UDP。這次拿 BBB 開刀,用 BBB host 一個 web server ,用於與用戶交互,數據來自 ESP8266 的 UDP 交互結果。本來,ESP8266 能直接用 TCP,但我希望廣播 UDP 來做自動發現,那服務端和設備端統一全部用 UDP 交互吧,服務端再通過 HTTP 與客戶端交互。

以下過程,與 Linux 上面搭 web 沒有區別。我選擇用 node.js,沒有什么特殊原因,只是因為它本來就跟着 BBB debian distro 一起裝好了的。為求快捷,也搭着 Express 一起用。我要用最高速度完成這個東西來,試試而已,Node + Express 很快能搞定。

安裝

首先,BBB 上面要有 node,確認一下在不在:

node –v

當然在。然后當前看看端口

netstate –tlpn

image

這里看到,80,是 systemd 用掉,就是 bone101 那一頁介紹頁面,3000 也是,Cloud9 IDE 的。兩者都可以關掉,關掉對應的服務即可(bonescript.socket 和 bonescript.service)。8080 端口,是 apache2 。那我用 4001 吧。也是沒有原因的。好,繼續。

在某某文件夾里面創建一個子文件夾 /root/lasapp,然后 npm init,按需輸入一些參數,它會幫我生成 package 檔,然后 npm install express –-save,其后等它安裝就好了。具體方法請參看這里:http://www.expressjs.com.cn/starter/installing.html

熱身, Hello World 一下,app.js:

var express = require('express');
var app = express();
app.get('/', function (req, res) {
  res.send('Hello World!');
});
var server = app.listen(4001, function () {
  var host = server.address().address;  
  var port = server.address().port;
});

然后 node app.js。用電腦打開瀏覽器輸入對應地址 http://192.168.7.2:4001/ 就會看到 Hello World,十行代碼不到,夠快了吧。

我准備做的,整個過程,是由一個網頁上的點擊,觸發服務器發送 UDP 廣播,然后接上一篇的 ESP8266 UDP 接收。然后ESP8266,或者多個不同的 ESP8266,響應后把它們回傳報上來的身份標識,服務負責處理回傳保存到數據庫,頁面定時刷新從數據庫取值。一個人項目,藍圖在心中。簡單寫一下的話,是分開前端,后台兩個,后台分開靜態頁、UDP、和 web service 三個部分。隨便從哪里開始,那就從前端那里,頁面吧。

前端頁面

IDE 我用 webstorm,在 windows 寫好 cp 過去 BBB 上,隨便拿個 Bootstrap 模板改就是:

image

模板來自一個什么二十分鍾搭好 bootstrap 的博文,其實不需要二十分鍾,Copy & Paste 然后改文字而已。任何模板都能做,甚至是直接手敲 HTML 也不會有問題。關鍵是中間的部分,將會用 JS 從 web service 獲得 JSON (設備列表)把它填上。中間還有一個綠色按鈕“重新搜索設備”,要有 web service 響應處理設備搜索(就是 UDP 廣播)。

頁面樣式大概弄好了就拷過去 BBB 先。把檔案打包成 lasapp.tar (在 windows 用 7z),然后 pscp (putty 自帶的遠程 copy、cp 工具)去 BBB 上。

pscp lasapp.tar root@192.168.7.2:/root/lasapp.tar

然后到 BBB 上 在 lasapp文件夾內,創建 public 文件夾:

mkdir public

在 public 文件夾內解壓:

tar –xf ~/lasapp.tar

最后修改剛才hello world 那個例子的 app.js 加入靜態文件把 public 文件夾放出來,和根目錄 GET 時候傳送 index.html:

var express = require('express');
var app = express();
app.use(express.static('public')); // 配置靜態文件路徑
app.get('/', function (req, res) {
  res.sendFile('index.html'); // 之前是 send(‘Hello World!’)
});
var server = app.listen(4001, function () {
  var host = '192.168.7.2';
  var port = server.address().port;
});

然后運行測試一下,沒問題就下一步,web service。

后台服務

BBB 空間有限,UDP、網頁服務器、Web Service 三者都能在 node 實現的話,那就不裝其他,就用 node 。快速做一遍三個分別是怎樣在 node 實現。

測試 Web Service 與發出 UDP

寫個 api.js 先,創建一個 api 文件夾然后在里面 vim api.js :

exports.udpService = function(port,bc_addr){ 
        var dgram = require('dgram'); 
        var port = port; 
        var bc_addr = bc_addr; 
        var queryTxt = '{"cmd":"0"}'; 
        var queryMsg = new Buffer(queryTxt); 
        var client = dgram.createSocket('udp4'); 
        client.bind(port, function(){ 
                this.setBroadcast(true); 
                this.setMulticastTTL(128); 
        }); 
        return { 
                query : function(req,res){ 
                        client.send(queryMsg,0,queryMsg.length,port,bc_addr); 
                        res.sendStatus(200); 
                } 
        }; 
}; 

node 可以發 datagram(UDP),API 請參看這里:
https://nodejs.org/docs/latest-v0.10.x/api/dgram.html

代碼 api.js 的 query 方法是接受到請求時候,對廣播地址(bc_addr)的特定端口(port)以 UDP 包方式發出一個字符 {“cmd”:”0”}。注意 setBroadcast 和 setMulticastTTL 兩個方法都必須在 bind 綁定完成后才能操作,所以我放了它在 callback 內。

完成需要告訴客戶端,搞定了沒問題,STATUS 200 OK。

 

關於廣播

IPv4 中,掩碼 subnet mask,是指定子網的方式。一個 192.168.7.0  作為 network prefix 指定了掩碼 255.255.255.252,等於 2^8 – 252 = 4個地址,這四個 192.168.7.0 至 192.168.7.3 之中,第一個 192.168.7.0是 network prefix,最后一個 192.168.7.3 是 broadcast address 廣播地址,只有余下的 192.168.7.1 和 192.168.7.2 兩個地址可以用作 host 主機。博文中 BBB 插着 USB 不插網線默認就是這個網段,BBB 用 USB 共享網絡時本身 IP 用 192.168.7.2,電腦這時候應該設置為 192.168.7.1,因為不改 BBB 地址網段的情況下,你別無選擇,余下只有一個主機地址可用。用這個子網,要發廣播,這子網的廣播地址是 192.168.7.3 了。而 255.255.255.255 就是公網以外,全物理網段廣播,不區分割開了多少個子網。

Multicast TLL 這 Multicast 這個字是來自 IPv6,IPv6地址分三類,Unicast、Anycast、Multicast。Unicast 是給單獨一個主機接收,Anycast 是給最近的一個主機接收,Multicast 是給網段所有主機接收,Multicast 意義上就是 IPv4 的 Broadcast。TTL 全寫是 Time To Live,意義是封包的存活時間,實際上實現的時候,它是每到達一個節點就會減一,直到 0 時候它就會不再被傳送。所以它並不是一個實際時間值(多少毫秒等等)。直接插 USB 連然后對一個只有兩個主機地址的網段廣播而且設置 TTL 128 其實是沒有任何意義,這里面沒有 128 個節點。看不慣就把上面代碼那句刪掉吧。能設置的范圍是 1-255,默認值是 OS 指定,我沒有查看 BBB 的 Debian 默認值是多少,據說是 1。

有興趣研究可以參考:

https://en.wikipedia.org/wiki/IP_address

https://en.wikipedia.org/wiki/Subnetwork

http://tools.ietf.org/html/rfc4291#section-2

書的話只需要一本,TCP/IP Illustrated Vol 1 The Protocols,Richard Stevens,ISBN: 9780321336316

 

要調用它,就需要在app.js那邊開GET接口。/query 接到 GET 請求就調用這個 api.js 里面的 query 方法。現在修改 app.js:

var express = require('express'); 
var app = express(); 
var api = require('./api/api'); // 引用才能使用 api.js 
var svc = new api.udpService(4000,'192.168.7.3'); // 利用 api 創建 udpService 實例

app.use(express.static('public'));

app.get('/',function(req,res){ 
        res.sendFile('index.html'); 
});

app.get('/query', svc.query); // 調用 query 方法

var server = app.listen(4001, function(){ 
        var host = server.address().address; 
        var port = server.address().port; 
        console.log('app listening at http://%s:%s',host,port); 
});

下一步,修改 index.html 把圖中綠色按鈕的點擊,用 ajax 請求發到 /query,就完成了。簡單點比如就 <a …… onclick="$.ajax({url:'/query'})"> … 。

image

最后一步,BBB 插上電源和網線,廣播地址改為正確值。web 請求就是這樣和 UDP 廣播連在一起(/query 的 GET 請求收到后,觸發 udpService 的 query 方法)。效果 ok 就來真的了。

整體后台代碼

由於空間所限,數據量小,並發少,數據庫用 Sqlite 我夠了,喜歡其他的請自行修改。首先安裝一下 Sqlite3,去到之前建的 lasapp 目錄,然后:

npm install sqlite3 –-save

新版的 express 已經沒有了內置 body parser,要自己裝再自己加入中間件,這樣安裝:

npm install body-parser

然后可以寫代碼了,看看我的最終版代碼:

/lasapp/app.js

var express = require('express'); 
var app = express(); 
var api = require('./api/api'); 
var bp = require('body-parser');

var svc = new api.udpService(4000,'255.255.255.255');

app.use(express.static('public')); 
app.use(bp.json()); 
app.get('/',function(req,res){ 
        res.sendFile('index.html'); 
});

app.get('/query', svc.query);

app.get('/devices/getAll',api.deviceService().getAll);

app.put('/devices', api.deviceService().save);

var server = app.listen(4001, function(){ 
        var host = server.address().address; 
        var port = server.address().port; 
        console.log('app listening at http://%s:%s',host,port); 
});

與之前代碼區別有幾個地方:

  • 它引用了 body-parser 並且在 app.use 啟用了 json 中間件,目的是對 body 解析 JSON https://www.npmjs.com/package/body-parser
  • udp 廣播地址用了 255.255.255.255 全物理網段廣播
  • 多了兩個接口
    • get /devices/getAll
    • put /devices
  • 兩個接口對應調用了 deviceService 里面的兩個方法

看看 api.js 里面是怎樣的:

/lasapp/api/api.js

exports.dbHelper = function(){
    var sqlite = require('sqlite3').verbose();
    var db = new sqlite.Database('lasdb.db');
    db.serialize(function(){
        db.run("CREATE TABLE if not exists devices(guid TEXT, dType TEXT,displayName TEXT)");
    });
    return {
        saveOrUpdate: function(device,callback){
            db.get("SELECT guid FROM devices WHERE guid=?",device.guid,function(err,row){
                if(err===null && row === undefined) {
                    db.run("INSERT INTO devices VALUES (?,?,?)",device.guid,device.dType,device.displayName);
                } else if (err===null) {
                    db.run("UPDATE devices SET displayName=? WHERE guid=?",device.displayName,device.guid);
                } else {
                    console.log(err);
                }
            });
            var getType={};
            if(callback && getType.toString.call(callback)==='[object Function]'){
                callback(device);
            }
        },
        getAll: function(callback){
            var result;
            db.all("SELECT guid,dType,displayName FROM devices", function(err,rows){
                if(err!==null){
                    console.log(err);
                    return;
                }
                var getType={};
                if(callback && getType.toString.call(callback)==='[object Function]'){
                    callback(rows);
                }
            });
        },
        closeDB: function(){
            db.close();
        }
    };
};

exports.deviceService = function(){
    return {
        getAll: function(req,res){
            var dbHelper = new exports.dbHelper();
            dbHelper.getAll(function(r){
                res.set({'Content-Type':'application/json'});
                res.send(r);
                dbHelper.closeDB();
            });
        },
        save: function(req,res){
            var dbHelper = new exports.dbHelper();
            dbHelper.saveOrUpdate(req.body,function(r){
                res.set({'Content-Type':'application/json'});
                res.send(r);
                dbHelper.closeDB();
            });
        }
    };
};

exports.udpService = function(port,bc_addr){
    var dgram = require('dgram');
    var port = port;
    var bc_addr = bc_addr;
    var queryTxt = '{"cmd":"0"}';
    var queryMsg = new Buffer(queryTxt);
    var client = dgram.createSocket('udp4');
    client.bind(port, function(){
        this.setBroadcast(true);
        this.setMulticastTTL(128);
    });
    client.on('message', function(msgRec,remote){
        var msg = msgRec.toString();
        if(msg==queryTxt){
            return;
        }
        var cmdObj;
        try {
            cmdObj = JSON.parse(msg);
        } catch(e) {
            console.log('Improper JSON literial received.');
            console.log(msg);
            return;
        }
        if(!cmdObj.cmd){
            console.log('JSON object format error.');
            return;
        }
        console.log('From:'+remote.address+' Port:'+remote.port+' > '+msg);
        if(cmdObj.cmd==2 && cmdObj.dType){
            var dbHelper = new exports.dbHelper();
            dbHelper.saveOrUpdate(cmdObj,function(){
                dbHelper.closeDB();
            });
        } else {
            console.log('cmd code not recognize or dType missing.');
        }
    });
    return {
        query : function(req,res){
            client.send(queryMsg,0,queryMsg.length,port,bc_addr);
            res.sendStatus(200);
        }
    };
};

三大塊,一個 dbHelper 做數據層用來和 Sqlite 數據庫交互,數據層方法除了closeDB 其他全部有 callback可配置,兩個服務分別負責 UDP 處理和 Web Service 的處理。

udpService 除了一些參數驗證之外,就是 on(“message”,….) 監聽 UDP 包到達,到達后調用 dbHelper 保存或更新值,最后 udpService 實例只開放一個方法,query,用來發出廣播 UDP 包。

deviceService 只有 save 和 getAll,兩者對應 dbHelper 里面的方法,查詢完成后 res.send。

不復雜,然后測試一下:

整體集成測試

用node app.js 啟動。

首先用 POSTMAN 對 /query 發 get 請求,另外用工具監聽,看看 UDP 是否正常廣播。

image

然后用工具,發三個 UDP ,分別是 guid:0001, 0002 和 0003,模擬 ESP8266 對 cmd:0 命令的響應。

{"cmd":"2","guid":"0001","dType":"powerPlug"}
{"cmd":"2","guid":"0002","dType":"powerPlug"}
{"cmd":"2","guid":"0003","dType":"powerPlug"}

image

或者再發多一次 guid 0003 看看它有沒有重復插入(當然不會)。

然后 POSTMAN 模擬 /devices/getAll 的 GET 請求,看看返回值是否正常。

image

再試試 PUT,對 /device 發出 PUT 請求,模擬網頁對 displayName,智能設備的顯示名進行更新,PUT 的 body 為:

{"guid":"0003","dType":"powerPlug","displayName":"主卧插座1"}

記得 Header 加上 Content-Type = application/json

image

最后再對 /device/getAll 發出 GET 請求看看是否更新正確:

image

API 初稿就這樣完成。改一下頁面,讓它觸發對應的 web service ,或許加個定時自動刷新頁面,整個項目初稿就搞定了。代碼有太多的改善空間,太混亂半成品不放 GIT 出來了,做好先。

下一篇,智能插座接線,和加上從 UDP 包接收,觸發 GPIO 高低電平控制電源開關。整個項目在下一篇就完成了。

重要參考

Node.js http://nodejs.org/
SQLite http://sqlite.org/
node-sqlite3 API https://github.com/mapbox/node-sqlite3/wiki/API
node-sqlite3 流控制、同步異步關閉等 https://github.com/mapbox/node-sqlite3/wiki/Control-Flow
Expressjs http://expressjs.com/
Expressjs 中間件 http://www.expressjs.com.cn/guide/using-middleware.html
Expressjs 其他有用模塊列表 https://github.com/expressjs
Expressjs Process Manager http://expressjs.com/en/advanced/pm.html
Project Style Template (僅供參考) https://github.com/jshttp/style-guide/tree/master/template
body-parser https://github.com/expressjs/body-parser
https://www.npmjs.com/package/body-parser
IP / Broadcasting / IPv6

https://en.wikipedia.org/wiki/IP_address
https://en.wikipedia.org/wiki/Subnetwork
http://tools.ietf.org/html/rfc4291#section-2

Postman https://www.getpostman.com/
網絡調試助手 http://www.onlinedown.net/soft/47906.htm
Bootstrap 秒速入門(所謂的二十分鍾打造站點) http://www.revillweb.com/tutorials/bootstrap-tutorial/
http://www.w3cplus.com/css/twitter-bootstrap-tutorial.html
我曾經買過 Theme 的網站,有個別設計質量相當高 http://themeforest.net/

我在這群里,歡迎加入交流:
開發板玩家群 578649319開發板玩家群 578649319
硬件創客 (10105555)硬件創客 (10105555)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM