zoukankan      html  css  js  c++  java
  • Beaglebone Black– 智能家居控制系统 LAS

    上一篇,纯粹玩 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)

  • 相关阅读:
    C语言 realloc为什么要有返回值,realloc返回值具体解释/(解决随意长度字符串输入问题)。
    opencv中的vs框架中的Blob Tracking Tests的中文注释。
    Java实现 蓝桥杯VIP 算法提高 棋盘多项式
    Java实现 蓝桥杯VIP 算法提高 棋盘多项式
    Java实现 蓝桥杯VIP 算法提高 棋盘多项式
    Java实现 蓝桥杯VIP 算法提高 棋盘多项式
    Java实现 蓝桥杯VIP 算法提高 分苹果
    Java实现 蓝桥杯VIP 算法提高 分苹果
    Java实现 蓝桥杯VIP 算法提高 分苹果
    Java实现 蓝桥杯VIP 算法提高 分苹果
  • 原文地址:https://www.cnblogs.com/leptonation/p/5181985.html
Copyright © 2011-2022 走看看