用Z-Blog构建简易NodeMCU环境监测平台

本文最后由 森林生灵 于 2017/06/03 16:14:36 编辑

文章目录 (?) [+]

        曾用过 Yeelink 做树莓派的环境温度采集监测,唯一不足之处是 Yeelink 平台的用户数据是公开的,缺乏私密性。刚好,最近有一个课程设计的题目也与此有关,于是便想趁此机会做一个和 Yeelink 功能差不多的私有平台。Google 搜索得知 Yeelink 平台使用了 RESTful 架构,简单了解 REST 后,感觉似乎对于一个小型信息监测平台用此有些小题大做了,想起之前学习的 MQTT 协议和开发过的几个Z-BlogPHP 的插件,索性使用了 MQTT + Z-BlogPHP 来做这个平台。

    环境温湿度曲线图

    NodeMCU节点

        数据采集节点使用的是 NodeMCU,在 NodeMCU 上可以像 Arduino 一样操作硬件 IO,用 Node.js 类似语法写网络应用,而且具有GPIO、PWM、I2C、1-Wire、ADC等功能,开发起来比较方便。NodeMCU 节点使用 DHT11 定时读取环境温湿度,并通过 MQTT 将数据发布给订阅端的服务器。

        配置文件:config.lua

    -- config.lua
    
    local module = {}
    
    module.HOST = "127.0.0.1"  -- MQTT Server Host
    module.PORT = 1883         -- MQTT Server Port
    module.USER = "mosquitto"  -- MQTT User Name
    module.PWD = ""            -- MQTT User Password
    module.ID = node.chipid()
    module.ENDPOINT = "/nodemcu/"
    module.DELAY = 600000      -- 10 minutes
    module.DHTPIN = 5          -- DHT11 Pin
    -- module.PMPIN = 6
    
    return module

        传感器:sensor.lua

    -- sensor.lua
    
    local module = {}
    
    function sensor_data()
        status, temp, humi, temp_dec, humi_dec = dht.read(config.DHTPIN)
        if status == dht.OK then
            --print("DHT Temperature:" .. temp .. "; Humidity:" .. humi)
            pm = 0 -- TODO:
            v = temp + humi + pm
            ok, json = pcall(cjson.encode, {tdata=temp, hdata=humi, pdata=pm, verify=v})
            if ok then
                --print(json)
                return json
            else
                print("JSON Encode Failed")
            end
        elseif status == dht.ERROR_CHECKSUM then
            print("DHT Checksum Error")
        elseif status == dht.ERROR_TIMEOUT then
            print("DHT Read Timeout")
        end
    
        return 0
    end
    
    
    return module

        Wi-Fi连接:network.lua

    -- network.lua
    local module = {}
    
    local function wifi_setup()
        print('Configuring Wifi ...')
        wifi.setmode(wifi.STATIONAP)
        wifi.ap.config({ssid = 'DataEndpoint_' .. config.ID, auth = wifi.OPEN})
        -- print('AP MAC:' .. wifi.ap.getmac())
    
        enduser_setup.manual(true)
        enduser_setup.start(
            function()
                enduser_setup.stop()
                wifi.setmode(wifi.STATION)
                print('Connected to wifi as:' .. wifi.sta.getip())
                -- print('STA MAC:' .. wifi.sta.getmac())
                mosquitto.start()
            end,
            function(err, str)
                print('Enduser Setup Error #' .. err .. ':' .. str)
            end
        )
    end
    
    local function wifi_wait_ip()
        print('Connecting Wifi ...')
        wifi.setmode(wifi.STATION)
        tmr.alarm(6, 8000, tmr.ALARM_SINGLE,
            function()
                if wifi.sta.getip() == nil then
                    print('IP unavailable, Waiting for Enduser Mode...')
                    wifi_setup()
                else
                    print('Connected to wifi as:' .. wifi.sta.getip())
                    print('STA MAC:' .. wifi.sta.getmac())
                    mosquitto.start()
                end
            end
        )
    end
    
    function module.start()
        wifi_wait_ip()
    end
    
    return module

        MQTT通信:mosquitto.lua

    -- mosquitto.lua
    
    local module = {}
    
    m = nil
    
    -- TODO:
    local function register_device()
        m:subscribe(config.ENDPOINT .. config.ID .. "/control", 1,
            function(client)
                print("Subscribe Success")
            end
        )
    end
    
    local function push_data()
        jsondata = sensor_data()
        m:publish(config.ENDPOINT .. config.ID .. "/sensor", jsondata, 1, 0,
            function(client)
                print("Publish Success")
                sensor.DATA = nil
            end
        )
    end
    
    local function mqtt_start()
        m = mqtt.Client(config.ID, 120, config.USER, config.PWD)
    
        -- Registers a callback function for an event
        m:on("connect",
            function(client)
                print("MQTT Connected")
            end
        )
    
        m:on("message",
            function(client, topic, data)
                if data ~= nil then
                    print(topic .. ":" .. data)
                    -- TODO:
                end
            end
        )
    
        -- Connect to broker
        m:connect(config.HOST, config.PORT, 0, 1,
            function(client)
                register_device()
                tmr.stop(5)
                tmr.alarm(5, config.DELAY, tmr.ALARM_AUTO, push_data)
            end,
            function(client, reason)
                print("MQTT Connect Failed:" .. reason)
            end 
        )
    end
    
    function module.start()
        mqtt_start()
    end
    
    return module

        启动脚本:init.lua

    -- init.lua
    
    config = require('config')
    sensor = require('sensor')
    network = require('network')
    mosquitto = require('mosquitto')
    
    network.start()

    Z-BlogPHP插件

        注册插件

    <?php
        // include.php
    
        require dirname(__FILE__) . DIRECTORY_SEPARATOR .'function/dbconfig.php';
        RegisterPlugin('ENVChart', 'ActivePlugin_ENVChart');
        
        function ActivePlugin_ENVChart() {
            Add_Filter_Plugin('Filter_Plugin_Zbp_CheckRights', 'ENVChart_CheckRights');
            Add_Filter_Plugin('Filter_Plugin_ViewPost_Template', 'ENVChart_Request');
            Add_Filter_Plugin('Filter_Plugin_Index_Begin', 'ENVChart_Request');
            Add_Filter_Plugin('Filter_Plugin_Admin_TopMenu', 'ENVChart_Admin_TopMenu');
        }
    
        function ENVChart_DatabaseTable($table, $option) {
            global $zbp;
    
            if (1 == $option) {
                if (!$zbp->db->ExistTable($zbp->table[$table])) {
                    $s = $zbp->db->sql->CreateTable($zbp->table[$table], $zbp->datainfo[$table]);
                    $zbp->db->QueryMulit($s);
                    return true;
                }
            }
            else if (0 == $option) {
                if ($zbp->db->ExistTable($GLOBALS['table'][$table])) {
                    $s = $zbp->db->sql->DelTable($GLOBALS['table'][$table]);
                    $zbp->db->QueryMulit($s);
                    return true;
                }
            }
            return false;
        }
    
        function ENVChart_CheckRights() {
            global $zbp;
        
            $arr = array(
                array('action' => 'ViewChart', 'level' => 1, 'lang' => '查看图表')
            );
            foreach ($arr as $key => $value) {
                $zbp->actions[$arr[$key]['action']] = $arr[$key]['level'];
                $zbp->lang['actions'][$arr[$key]['action']] = $arr[$key]['lang'];
            }
        }
        
        function ENVChart_Request() {
            global $zbp;
        
            if (isset($_GET['chart'])) {
                chart_page();
                die();
            }
        }
        
        function ENVChart_Admin_TopMenu(&$topmenus) {
            global $zbp;
            $topmenus[] = MakeTopMenu('ViewChart', '查看图表', $zbp->host .'?chart', '', 'topmenu6');
        }
        
        function chart_page() {
            global $zbp;
        
            $article = new Post;
            $article->Title = '环境温湿度曲线图';
            
            $article->IsLock = true;
            $article->Type = ZC_POST_TYPE_PAGE;
        
            $article->Content .= '<style type="text/css">text.highcharts-credits {display: none;}rect.highcharts-background {fill-opacity: 0.9;}</style>';
            if (!$zbp->CheckRights('ViewChart')) {
                $article->Content .= '<div class="textbox_red">没有权限!请 <a href="'. $zbp->host .'?signin">登录</a> 后查看。</div>';
            }
            else {
                if (!$zbp->CheckPlugin('ENVChart')) {$zbp->ShowError(48);die();}
                $article->Content .= '<script src="'. $zbp->host .'zb_users/plugin/ENVChart/code/highstock.js"></script><script src="'. $zbp->host .'zb_users/plugin/ENVChart/code/modules/exporting.js"></script><div id="container" style="height: 400px; min-width: 310px;text-align: center;"></div><script src="'. $zbp->host .'zb_users/plugin/ENVChart/code/script.js"></script>';
            }
        
            $zbp->template->SetTags('title', $article->Title);
            $zbp->template->SetTags('article', $article);
            $zbp->template->SetTemplate($article->Template);
            $zbp->template->SetTags('comments', array());
            $zbp->template->Display();
        }
        
        function InstallPlugin_ENVChart() {
            global $zbp;
    
            if (ENVChart_DatabaseTable('datachart', 1)) {
                $zbp->SetHint('good', '创建datachart数据表成功!');
            }
            else {
                $zbp->SetHint('bad', '创建datachart数据表失败!可能是数据表已存在。');
            }
        }
    
        function UninstallPlugin_ENVChart() {}

        数据表结构

    <?php
        // dbconfig.php
    
        $table['datachart'] = '%pre%datachart';
        $datainfo['datachart'] = array(
            'ID' => array('ID', 'integer', '', 0),
            'Time' => array('Time', 'integer', '', 0),
            'Hdata' => array('Hdata', 'float', '', 0),
            'Tdata' => array('Tdata', 'float', '', 0),
            'Pdata' => array('Pdata', 'float', '', 0)
        );

        订阅 MQTT 主题

        该脚本需要在命令行中执行。phpMQTT 使用的是 https://github.com/bluerhinos/phpMQTT 。Mysqli 使用的是https://git.oschina.net/lanseyujie/codes/1m9oqs4bhwlfka6v7cdiu62 。

    <?php
        if (!isset($_SERVER['SHELL'])) {
            header("HTTP/1.0 404 Not Found");
            die('Please run this script with bash');
        }
    
        require dirname(__FILE__) . DIRECTORY_SEPARATOR .'function/Mysqli.php';
        require dirname(__FILE__) . DIRECTORY_SEPARATOR .'function/phpMQTT.php';
    
        $mqtthost = '127.0.0.1';
        $mqttport = 1883;
        $mqttclientid = 'MQTTTEST';
        $mqttusername = '';
        $mqttuserpwd = '';
        $mqttsubtopic = '/nodemcu/10732666/sensor';
    
        $dbhost = '127.0.0.1';
        $dbport = 3306;
        $dbuser = 'root';
        $dbpwd = '';
        $dbname = '';  // 需要跟 Z-Blog 使用同一数据库
        
        $mqtt = new phpMQTT($mqtthost, $mqttport, $mqttclientid);
        //$mqtt->debug = true;
        if (!$mqtt->connect(true, NULL, $mqttusername, $mqttuserpwd)) {
            exit(1);
        }
        
        $topics[$mqttsubtopic] = array('qos'=>0, 'function'=>'procmsg');
        $mqtt->subscribe($topics, 0);
        while ($mqtt->proc()) {}
        $mqtt->close();
    
        function procmsg($topic, $msg) {
            echo date('r') ."\n";
            echo 'SubTopic:'. $topic ."\n";
            echo 'MSG:'. $msg ."\n";
    
            $array = json_decode($msg, true);
            if (!is_null($array) && array_key_exists('verify', $array)) {
                $sum = $array['hdata'] + $array['tdata'] + $array['pdata'];
    
                if ($sum == $array['verify']) {
                    //var_dump($array);
                    $db = new Mysqli;
                    //$db->debug();
                    $db->Connect($dbhost, $dbport, 'utf8', $dbuser, $dbpwd, $dbname);
        
                    $time = time();
                    $hdata = $array['hdata'];
                    $tdata = $array['tdata'];
                    $pdata = $array['pdata'];
        
                    $db->Insert("INSERT INTO `zbp_datachart` (Time,Hdata,Tdata,Pdata) VALUES ('$time','$hdata','$tdata','$pdata')");
        
                    $db->Close();
                }
    	    else {
                    echo 'Verify Value is '. $array['verify'];
                    echo 'But Sum is '. $sum .'!Data Verify Fail';
                }
            }
        }

        JSON 数据输出

    <?php
        // SELECT `Time`,`Hdata` FROM `zbp_datachart` ORDER BY `ID` ASC
        // SELECT `Time`,`Tdata` FROM `zbp_datachart` ORDER BY `ID` ASC
        // SELECT `Time`,`Pdata` FROM `zbp_datachart` ORDER BY `ID` ASC
    
        header("Access-Control-Allow-Origin: https://lanseyujie.com");
        
        require('../../../zb_system/function/c_system_base.php');
    
        $zbp->Load();
        if (!$zbp->CheckRights('ViewChart')) {
            $zbp->ShowError(6);
            die();
        }
        if (!$zbp->CheckPlugin('ENVChart')) {
            $zbp->ShowError(48);
            die();
        }
    
        $action = GetVars('name', 'GET');
    
        if ('humi' == $action || 'temp' == $action || 'pm' == $action) {
            if ('humi' == $action) {
                $column = 'Hdata';
            }
            else if ('temp' == $action) {
                $column = 'Tdata';
            }
            else if ('pm' == $action) {
                $column = 'Pdata';
            }
            $sql = $zbp->db->sql->Select($zbp->table['datachart'], array('Time', $column), null, array('ID' => 'ASC'), null, null);
            $array = $zbp->db->Query($sql);
    
            //echo $array[0]['Time'];
            //echo $array[1]['Hdata'];
            //echo '<pre>';
            //var_dump($array);
    
            $array_humi = array();
            foreach ($array as $key => $value) {
                $array_humi[] = array(intval($array[$key]['Time'].'000'), floatval($array[$key][$column]));
            }
    
            //var_dump($array_humi);
            echo json_encode($array_humi);
        }
    
        //echo '[[1495043031000,19.3],[1495102348000,20.7],[1495110003000,19.31],[1495115556000,19.32],[1495115842000,15.32],[1495115993000,16.03],[1495116342000,20.32]]';

        HighStock 配置

        HighStock 下载地址:https://www.highcharts.com/products/highstock

    // script.js
    
    Highcharts.setOptions({
    	global: {
    		useUTC: false
    	}
    });
    
    var seriesOptions = [],
    	seriesCounter = 0,
    	names = ['humi', 'temp'];
    
    /**
     * Create the chart when all data is loaded
     * @returns {undefined}
     */
    
    function createChart() {
    
    	Highcharts.stockChart('container', {
    
    		rangeSelector: {
    			selected: 4
    		},
    
    		yAxis: {
    			labels: {
    				formatter: function() {
    					return (this.value > 0 ? ' + ' : '') + this.value + '%';
    				}
    			},
    			plotLines: [{
    				value: 0,
    				width: 2,
    				color: 'silver'
    			}]
    		},
    
    		plotOptions: {
    			series: {
    				compare: 'percent',
    				showInNavigator: true
    			}
    		},
    
    		tooltip: {
    			pointFormat: '<span style="color:{series.color}">{series.name}</span>: <b>{point.y}</b> ({point.change}%)<br/>',
    			valueDecimals: 2,
    			split: true
    		},
    
    		series: seriesOptions
    	});
    }
    
    $.each(names, function(i, name) {
    
    	$.getJSON('https://lanseyujie.com/zb_users/plugin/DataChart/json.php?name=' + name.toLowerCase(), function(data) {
    
    		seriesOptions[i] = {
    			name: name,
    			data: data
    		};
    
    		// As we're loading the data asynchronously, we don't know what order it will arrive. So
    		// we keep a counter and create the chart when all the data is loaded.
    		seriesCounter += 1;
    
    		if (seriesCounter === names.length) {
    			createChart();
    		}
    	});
    });

    硬件电路

        原理图

    节点电路原理图

        PCB

    印刷电路板设计图

    印刷电路板实物图

    引脚定义

    1. Serial

    2. FlashMode

    3. Relay

    4. Power

    5. DHT11

    6. WakeUp

    7. Extension

    本文标题:用Z-Blog构建简易NodeMCU环境监测平台
    本文链接:https://lanseyujie.com/post/nodemcu-environment-monitoring-platform-with-zblog.html
    版权声明:本文使用「署名 4.0 国际」创作共享协议,转载或使用请遵守署名协议。
    点赞 0 分享 0