本文最后由 森林生灵 于 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 moduleWi-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 moduleMQTT通信: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://www.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://www.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


引脚定义
Serial
FlashMode
Relay
Power
DHT11
WakeUp
Extension
本文标题:用Z-Blog构建简易NodeMCU环境监测平台
版权声明:本文使用「署名 4.0 国际」创作共享协议,转载或使用请遵守署名协议。
上一篇:NodeMCU入门