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

来源:http://www.cnblogs.com/leptonation/archive/2016/02/04/5181985.html
-Advertisement-
Play Games

上一篇,純粹玩 ESP8266,寫入了 init.lua 能收發 UDP。這次拿 BBB 開刀,用 BBB host 一個 web server ,用於與用戶交互,數據來自 ESP8266 的 UDP 交互結果。本來,ESP8266 能直接用 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 [email protected]:/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

 

要調用它,就需要在 express 那邊開介面。/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 高低電平控制電源開關。整個項目在下一篇就完成了。

重要參考

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

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

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

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/

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1:伺服器端/** * Created by LiuFei on 2016/1/22. */public class HttpService extends Service{ @Override public IBinder onBind(Intent intent) { return null;
  • 第一版: ListView一屏顯示多少對象其內部就創建多少View對象。滑動時退出的緩存對象留給滑進去時調用getView傳的convertView。因為如果每次都findViewById查找創建視圖對象,浪費性能和記憶體。所以我們都利用佈局創建View給convertView。佈局內部的view對象
  • 排名函數三兄弟,一看名字就知道,都是為了排名而生!但是各自有各自的特色!以下一個例子說明問題!(以下慄子沒有使用Partition By 的關鍵字,整個結果集進行排序) RANK 每個值一個排名,同樣的值排同樣的位置,如第一名有2個,下一個值就要排第三,如此類推,表現如下麵的 RandNr 列 DE
  • 在當今這個多種不同資料庫混用,各種不同語言不同框架融合的年代(一切為了降低成本並高效的提供服務),知識點多如牛毛。雖然大部分SQL腳本可以使用標準SQL來寫,但在實際中,效率就是一切,因而每種不同廠商的SQL新特性有時還是會用到,這部分內容更是讓人抓瞎,常常會由於一些很簡單的問題花很久來搜索準確答案
  • TableSample 平時用得少,基本上就是用於表裡面抽樣數據來看的。 用法如下 SELECT * FROM tbname TABLESAMPLE SYSTEM (N PERCENT/M Rows) REPEATABLE() TABLESAMPLE SYSTEM 這個表示使用System的方法進行
  • cat 1file-1:1234 cat 2file-2:1245 上面是2個文件的內容。 diff -uN 1 2 >patch.log cat patch.logpatch.log:--- 1 2016-02-05 12:15:04.214850218 +0800+++ 2 2016-02-05
  • 安裝命令centos下 yum install vsftpd 出現“Complete!”時意味著安裝完成。Linux中,系統對於大小寫嚴格區分,比如abc和ABC是完全不相同的字元,要特別註意。配置Vsftpd虛擬用戶使用vsftpd伺服器之前,要對伺服器進行配置,主要包括如下幾個步驟:(1)生成虛
  • 首先從u-boot官網下載最新版的u-boot,這裡我下的是u-boot-2015.10。下載完成後解壓,閱讀README,在Building the Software:中得知編譯方法:如果使用交叉編譯的話要執行以下命令: CROSS_COMPILE=arm-linux- export CROSS_
一周排行
    -Advertisement-
    Play Games
  • .Net8.0 Blazor Hybird 桌面端 (WPF/Winform) 實測可以完整運行在 win7sp1/win10/win11. 如果用其他工具打包,還可以運行在mac/linux下, 傳送門BlazorHybrid 發佈為無依賴包方式 安裝 WebView2Runtime 1.57 M ...
  • 目錄前言PostgreSql安裝測試額外Nuget安裝Person.cs模擬運行Navicate連postgresql解決方案Garnet為什麼要選擇Garnet而不是RedisRedis不再開源Windows版的Redis是由微軟維護的Windows Redis版本老舊,後續可能不再更新Garne ...
  • C#TMS系統代碼-聯表報表學習 領導被裁了之後很快就有人上任了,幾乎是無縫銜接,很難讓我不想到這早就決定好了。我的職責沒有任何變化。感受下來這個系統封裝程度很高,我只要會調用方法就行。這個系統交付之後不會有太多問題,更多應該是做小需求,有大的開發任務應該也是第二期的事,嗯?怎麼感覺我變成運維了?而 ...
  • 我在隨筆《EAV模型(實體-屬性-值)的設計和低代碼的處理方案(1)》中介紹了一些基本的EAV模型設計知識和基於Winform場景下低代碼(或者說無代碼)的一些實現思路,在本篇隨筆中,我們來分析一下這種針對通用業務,且只需定義就能構建業務模塊存儲和界面的解決方案,其中的數據查詢處理的操作。 ...
  • 對某個遠程伺服器啟用和設置NTP服務(Windows系統) 打開註冊表 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\NtpServer 將 Enabled 的值設置為 1,這將啟用NTP伺服器功 ...
  • title: Django信號與擴展:深入理解與實踐 date: 2024/5/15 22:40:52 updated: 2024/5/15 22:40:52 categories: 後端開發 tags: Django 信號 松耦合 觀察者 擴展 安全 性能 第一部分:Django信號基礎 Djan ...
  • 使用xadmin2遇到的問題&解決 環境配置: 使用的模塊版本: 關聯的包 Django 3.2.15 mysqlclient 2.2.4 xadmin 2.0.1 django-crispy-forms >= 1.6.0 django-import-export >= 0.5.1 django-r ...
  • 今天我打算整點兒不一樣的內容,通過之前學習的TransformerMap和LazyMap鏈,想搞點不一樣的,所以我關註了另外一條鏈DefaultedMap鏈,主要調用鏈為: 調用鏈詳細描述: ObjectInputStream.readObject() DefaultedMap.readObject ...
  • 後端應用級開發者該如何擁抱 AI GC?就是在這樣的一個大的浪潮下,我們的傳統的應用級開發者。我們該如何選擇職業或者是如何去快速轉型,跟上這樣的一個行業的一個浪潮? 0 AI金字塔模型 越往上它的整個難度就是職業機會也好,或者說是整個的這個運作也好,它的難度會越大,然後越往下機會就會越多,所以這是一 ...
  • @Autowired是Spring框架提供的註解,@Resource是Java EE 5規範提供的註解。 @Autowired預設按照類型自動裝配,而@Resource預設按照名稱自動裝配。 @Autowired支持@Qualifier註解來指定裝配哪一個具有相同類型的bean,而@Resourc... ...