微信小程序+Aquaponics+MQTT+Node搭建鱼菜共生数据监测物联网应用实践

All On Github:


Aquaponics-WeChat-Mini-Program


This is an old archive, the latest version https://git.weixin.qq.com/PorYoung/Aquaponics.git

Aquaponics-WeChat-Mini-Program

Aquaponics data watching platform based on wechat small-program

[Relavant projects]

Smartaq.cn Blogs


Aquaponics-MQTT-Server


Aquaponics-MQTT-Server

A HTTP & MQTT Server based on NodeJS, express and mosca.

开发日志

开发记录

redis配置

const redis = require("redis")
const redisClient = redis.createClient({
    host: '127.0.0.1',
    port: 6379,
    db: '1'
})
const asyncRedisClient = require('async-redis').createClient({
    host: '127.0.0.1',
    port: 6379,
    db: '1'
})
const session = require('express-session')
let RedisStore = require('connect-redis')(session)
global.redisClient = redisClient
// session配置
app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: 'PorYoung',
  cookie: {
    maxAge: 60 * 1000 * 30
  },
  resave: false,
  saveUninitialized: true,
}))

redis操作完成后释放连接

用户管理模块

数据库设计
用户表
字段
用户ID_id唯一
用户名nickName微信用户名
头像avatarUrl微信头像
密码password
身份级别level0-待确认,1-超级管理员,2-管理员,3-用户
用户管理权限userManageEnable布尔
添加设备权限addDeviceEnable布尔,默认true
删除设备权限deleteDeviceEnable布尔,默认false
其他功能权限待定待定
const mongoose = app.mongoose
const Schema = mongoose.Schema

const UserSchema = new Schema({
    // 文档名 user
    //wechat openid
    openid: {
        type: String,
        required: true,
        unique: true
    },
    // 用户名
    nickName: String,
    // 用户头像URL
    avatarUrl: String,
    // 用户级别
    level: {
        type: Number,
        default: 0
    },
    // 用户管理权限
    userManageEnable: {
        type: Boolean,
        default: false
    },
    // 添加设备权限
    addDeviceEnable: {
        type: Boolean,
        default: true
    },
    // 删除设备权限
    deleteDeviceEnable: {
        type: Boolean,
        default: false
    }
})
设备表
字段
设备名name
设备标签tag
设备描述description
设备头像avatarUrl
创建人createrref:User
绑定用户usersArray ref:User
创建时间date
密码password
运行状态runStateObject
{
    // 文档名 device
    //设备密码
    password: String,
    // 设备名
    name: String,
    // 设备标签
    tag: String,
    // 设备描述
    description: String,
    // 创建人
    creater: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'user'
    },
    // 绑定用户列表
    users: [{
        type: mongoose.Schema.Types.ObjectId,
        ref: 'user'
    }],
    // 设备头像
    avatarUrl: String,
    // 创建时间
    date: Date,
    // 运行状态
    runState: Object,
    // 设备图片
    images: Array
}
数据定义

数据定义

字段
参数idid
参数名name
单位unit
参数最大值max
参数最小值min
预警最大值wmax
预警最小值wmin
参数描述desc
手动manual

默认数据定义

字段
设置人whoSetref:User
数据定义defineArray
日期date
使用计数usedCount
{
    // 文档名 dfdefine
    // 设置人
    whoSet: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'user'
    },
    // 数据定义
    define: Array,
    // 日期
    date: Date,
    // 使用计数
    usedCount: {
        type: Number,
        default: 0,
        min: 0
    }
}

设备数据定义

字段
设备deviceref:Device
数据定义define
参考定义refDefineref:Dfdefine
设置人whoSetref:User
日期date
过期expiredBoolen
{
    // 文档名 define
    // 设备
    device: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'device'
    },
    // 
    define: Array,
    refDefine: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'dfdefine'
    },
    date: Date,
    whoSet: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'user'
    },
    expired: {
        type: Boolean,
        default: false
    }
}
数据表
字段
ID_id
设备deviceref:Device
数据dataObject
日期date
记录issueref:Issue
预警warningBoolen
使用的数据定义usedDefineref:Define

数据对象

字段
参数参数id{val:参数值,stat:参数状态(0:正常,-1:预警,-2:越界,-3:无数据)}
{
    // 文档名: data
    // 日期
    date: Date,
    // 所属设备
    device: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'device'
    },
    // 数据对象
    data: Object,
    // 预警信息
    warning: {
        type: Boolean,
        default: false
    },
    // 记录
    issue: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'issue'
    }
}
功能实现
登陆功能
errMsg含义
2首次登陆
1登陆成功
-1登陆成功但等级为0
-2无法获取openid
-3获取用户code失败
绑定用户api返回值errMsg含义
1成功
-1用户不存在
-2非法代码
用户管理功能

用户名首字母分类

  • 使用pinyin模块进行分类
  • 分类结果存储在redis中
  • 新增或删减用户更新redis
  • userListCategorize方法:`server/common/init

获取用户列表

  • 返回值定义
    | errMsg | |
    | ------ | -------- |
    | -1 | 权限不足 |

  • 实现getUserListCategorized方法:server/controller/user

权限修改

  • 上传参数
params
_id被修改用户id
letter用户名首字母(供redis使用)
index用户在分类中的索引(供redis使用)
level用户级别
userManageEnable
addDeviceEnable
deleteDeviceEnable
  • 返回值
errMsg
1成功
-1未获得上传参数
-2更新失败

删除用户

设备管理

添加设备

删除设备

获取默认参数设置

  • 返回值
字段
errMsg
datadefine表 {define: 数据定义数组, 其它}有上传_id
date{define: Config.indexDefaultDefine}无上传_id

获取设备参数

  • 返回值
字段
errMsg
data{define: 数据定义数组, whoSet, date}
设备参数设计
小工具
计算模型

MongoDB/Mongoose操作

查询数组内对象字段

db.c.find({array:{$eleMatch:{arrt:val}}})
db.c.find({'array.arrt':val})

Aquaponics-Management-Platform


Aquaponics-Management-Platform

QuickStart

see egg docs for more detail.

Development

$ npm i
$ npm run dev
$ open http://localhost:7001/

Deploy

$ npm start
$ npm stop

npm scripts

  • Use npm run lint to check code style.
  • Use npm test to run unit test.
  • Use npm run autod to auto detect dependencies upgrade, see autod for more detail.

node-mqtt-demo


MQTT + NodeJS + Weixin MiniProgram

PorYoung Blog

MQTT协议介绍

基于NodeJS的MQTT服务器搭建

以下仅为本地开发环境搭建过程记录,操作系统为win10。

NodeJS环境搭建

NodeJS环境搭建较为简单,访问NodeJS官网,根据系统不同选择不同方式安装。

搭建HTPPS和MQTT服务器

NodeJS可以使用mosca模块搭建MQTT服务器。

mosca可以单独使用,也可以和https服务器一起运行。也可以使用http服务器,但在微信小程序中,即使选择不校验域名和HTTPS,也会报错(websocket failed: Error in connection establishment: net::ERR_SSL_PROTOCOL_ERROR)

const mosca = require('mosca');
const https = require('https');
const fs = require('fs');
const MqttServer = new mosca.Server({
  port: 1883
});

/**
 * HTTP方式
 * const httpServer = require('http').createServer((req, res) => {
  res.end('hello world!');
}).listen(3000); */
// HTTPS证书位置
const options = {
  key: fs.readFileSync('server.key'),
  cert: fs.readFileSync('server.crt')
};
const httpsServer = https.createServer(options, (req, res) => {
  // HTTPS服务器
  res.end('hello world!');
}).listen(443);
//通过attachHttpServer(),使得可以从浏览器或其他方式以https端口连接MQTT服务器
//微信小程序连接地址(使用MQTT.js包)为:wxs://localhost
MqttServer.attachHttpServer(httpsServer);

MQTT验证用户身份

mosca提供authenticateautenticatePunlishauthenticateSubscribe方法对连接请求进行验证,均可以被覆盖。

//连接认证,验证上传的用户名和密码与服务器保存的信息(如数据库)一致
const User = {
  username: 'test',
  password: '123456'
};
const authenticate = (client, username, password, callback) => {
  let flag = (username == User.username && password == User.password);
  if (flag) client.user = username;
  callback(null, flag);
};
//发布校验,授权可以发布'/users/user/'或者'/public/'下的主题
const authenticatePublish = (client, topic, payload, callback) => {
  let t = topic.split('/');
  if (t[1] == 'public') {
    callback(null, true);
  } else if (t[1] == 'users') {
    callback(null, client.user == t[2]);
  } else {
    callback(null, false);
  }
}
//订阅校验,授权可以订阅'/users/user/'或者'/public/'下的主题
const authenticateSubscribe = (client, topic, callback) => {
  let t = topic.split('/');
  if (t[1] == 'public') {
    callback(null, true);
  } else if (t[1] == 'users') {
    callback(null, client.user == t[2]);
  } else {
    callback(null, false);
  }
}

MqttServer.on('ready', () => {
  console.log('Mqtt Server is running...');
  MqttServer.authenticate = authenticate;
  MqttServer.authorizePublish = authenticatePublish;
  MqttServer.authorizeSubscribe = authenticateSubscribe;
});

监听事件

mosca可以监听的事件参考官方文档。

//部分事件示例如下
MqttServer.on('clientConnected', (client) => {
  console.log('client connected:', client.id);
});
MqttServer.on('subscribed', (topic, client) => { //订阅
  let qtt = {
    topic: '/public/systemInfo',
    payload: client.user + ' has subscribed topic: ' + topic
  };
  MqttServer.publish(qtt);
});
MqttServer.on('unSubscribed', (topic, client) => { //取消订阅
  console.log('unSubscribed: ', topic);
})
MqttServer.on('clientDisConnected', (client) => {
  console.log('client disconnected', client.id);
});
MqttServer.on('published', (packet, client) => {
  let topic = packet.topic;
  let qtt = {
    topic: 'other',
    payload: 'This is server'
  };
  switch(topic){
    case 'test1':{
      console.log(packet.payload.toString());
      MqttServer.publish(qtt); //服务器自身也会收到消息
      break;
    }
    case 'other':{
      break;
    }
  }
}

NodeJS搭建本地HTTPS服务器

NodeJS本地启动https服务需要密钥和证书,可以使用openssl对自身签证。

Window平台可以选择其他开源平台提供的工具,如http://slproweb.com/products/Win32OpenSSL.html,选择32位或64位Light版(小但能用)。

安装好之后,可能需要手动配置环境变量,在Path中添加安装路径中的bin目录,即openssl.exe所在的目录)。

以管理员模式启动CMD,进入想要保存证书的目录,输入如下命令:(参考node.js在本地启动https服务)。

  1. 为服务器端和客户端准备公钥、私钥
// 生成服务器端私钥
openssl genrsa -out server.key 1024
// 生成服务器端公钥
openssl rsa -in server.key -pubout -out server.pem
// 生成客户端私钥
openssl genrsa -out client.key 1024
// 生成客户端公钥
openssl rsa -in client.key -pubout -out client.pem
  1. 生成 CA 证书
// 生成 CA 私钥
openssl genrsa -out ca.key 1024
// X.509 Certificate Signing Request (CSR) Management.
openssl req -new -key ca.key -out ca.csr
// X.509 Certificate Data Management.
openssl x509 -req -in ca.csr -signkey ca.key -out ca.crt

第2步中的Organization Name (eg, company) [Internet Widgits Pty Ltd]: 后面生成客户端和服务器端证书的时候也需要填写,不要写成一样的

  1. 生成服务器端证书和客户端证书
// 服务器端需要向 CA 机构申请签名证书,在申请签名证书之前依然是创建自己的 CSR 文件  
openssl req -new -key server.key -out server.csr  
// 向自己的CA机构申请证书,签名过程需要CA的证书和私钥参与,最终颁发一个带有CA签名的证书
openssl x509 -req -CA ca.crt -CAkey ca.key -CAcreateserial -in server.csr -out server.crt
// client 端
openssl req -new -key client.key -out client.csr
// client 端到 CA 签名
openssl x509 -req -CA ca.crt -CAkey ca.key -CAcreateserial -in client.csr -out client.crt

完成后,所在文件夹下生成如下文件:

    ├── ca.crt
    ├── ca.csr
    ├── ca.key
    ├── ca.srl
    ├── client.crt
    ├── client.csr
    ├── client.key
    ├── client.pem
    ├── server.crt
    ├── server.csr
    ├── server.key
    └── server.pem

再使用NodeJS的HTTPS模块启动https服务即可,配置方法见Node.js HTTPS.

微信小程序使用MQTT.js连接服务器

MQTT.js文档,下载打包好的MQTT.js可以访问http://unpkg.com.

const mqtt = require('../../utils/mqtt.min.js');
const mqttOptions = {
  username,
  password,
  clientId
};
const mqttHost = 'wxs://localhost';
const mqttClient = mqtt.connect(mqttHost, mqttOptions);

mqttClient.on('connect', function() {
  console.log('connect');
})

mqttClient.on('message', function(topic, message) {
  // message is Buffer
  console.log('收到来自', topic, '的消息', message.toString())
})

mqttClient.on('reconnect', (error) => {
  console.log('正在重连:', error)
})

mqttClient.on('error', (error) => {
  console.log('连接失败:', error)
})

//以下是发布和订阅的方法,其他方法详见文档
mqttClient.publish(topic, message, [options], [callback]);
mqttClient.subscribe(topic/topic array/topic object, [options], [callback]);

Arduino-Mqtt


Arduino R3 Uno + ESP8266 MQTT连接记录

使用DFRobot的arduino扩展板和WiFi Bee模块连接mqtt服务器,过程十分艰辛,从arduino unoObloq模块转到arduino R3 UnoIO扩展板WiFi Bee模块,相关文档和代码十分有限,网上大部分教程都无法使用,有各种错误,这里记录了最终成功的过程。

材料

所用设备

  1. DFRduino UNO R3
  2. IO 传感器扩展板 V7.1
  3. ESP8266 WiFi Bee模块
  4. XBee USB Adapter 适配器(带FTDI烧写功能)
  5. USB线、连接线若干

软件及库

  1. Arduino IDE 1.8.7
  2. 串口调试软件CoolTerm
  3. FTDI USB Drivers驱动,国内海外
  4. firebeetle8266 Arduino开发环境,国内使用http://git.oschina.net/dfrobot/firebeetle-esp8266/raw/master/package_firebeetle8266_index.json,海外使用https://raw.githubusercontent.com/DFRobot/FireBeetle-ESP8266/master/package_firebeetle8266_index.json
  5. pubsubclient官网github主页

开发记录

XBee USB Adapter适配器

arduino自带FTDI驱动,如果无法连接,请检查连接线是否兼容,若没有驱动,可前往https://www.ftdichip.cn/FTDrivers.htm下载

WiFi Bee-ESP8266编程环境配置

WiFi Bee模块官方封装了esp8266.h库,但不满足我们的使用pubsubclient连接MQTT服务器的要求,因此需要烧录程序。

arduino首选项附加开发板管理网址里添加网址http://git.oschina.net/dfrobot/firebeetle-esp8266/raw/master/package_firebeetle8266_index.json,更新索引并下载firebeetle8266开发板。

WiFi Bee拨到UART,插在适配器上连接电脑,选择firebeetle8266开发板,添加测试代码Serial.println("test");,上传代码,若上传失败,断电重试。

查看串口监视器,是否有输出。

遗留问题:传代码速度的选择影响上传速度,但不知道是不是都可以选,测试了9600和115200没有报错,921600报错

烧录连接WiFi和MQTT服务器代码到ESP

尝试了115200和9600两种不同的波特率,貌似只有9600没有问题。

#include <ESP8266WiFi.h>
#include <PubSubClient.h>
char ssid[] = "AP Name";
char password[] = "AP Pass";

//Public MQTT Server
const char* mqtt_server = "mq.tongxinmao.com";
int mqtt_port = 18830;
char publishTopic[] = "/public/TEST/tp";
char subscribeTopic[] = "/public/TEST/ts";

WiFiClient espClient;
PubSubClient client(espClient);

String strRecv = "";
long now = 0;
long lastRecv = 0;
bool newDataComing = false;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  delay(10);

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
  }
  Serial.println("");
  Serial.println("INFO: WiFi connected");
  Serial.print("INFO: IP address: ");
  Serial.println(WiFi.localIP());

  client.setServer(mqtt_server, mqtt_port);
  client.setCallback(callback);
}

void loop() {
  // put your main code here, to run repeatedly:
  if (!client.connected()) {
    reconnect();
  }
  client.loop();
  if (Serial.available() > 0) {
    char str = char(Serial.read());
    strRecv = strRecv + str;
    lastRecv = millis();
    newDataComing = true;
    delay(2);
  } else {
    now = millis();
    if ((now - lastRecv > 100) && (newDataComing == true)) {
      boolean isOK = client.publish(publishTopic, String(strRecv).c_str());
      strRecv = "";
      newDataComing = false;
    }
  }
}

void callback(char* topic, byte* payload, unsigned int length) {
  String msg;
  for (int i = 0; i < length; i++) {
    msg.concat((char)payload[i]);
  }
  Serial.print(msg);
}

void reconnect() {
  // Loop until we're reconnected
  while (!client.connected()) {
    // Create a random client ID
    String clientId = mqttUser;
    // Attempt to connect
    if (client.connect(clientId.c_str(), mqttUser.c_str(), mqttPass.c_str())) {
      client.subscribe(subscribeTopic);
      client.subscribe(publishTopic);
    } else {
      Serial.print("ERROR: failed, rc=");
      Serial.print(client.state());
      Serial.println(" DEBUG: try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}

Arduino控制代码

以水温采集传感器为例,防水温度传感器,此处传感器数据线连3号引脚。

#include "Arduino.h"
#include "SoftwareSerial.h"
#include <OneWire.h>
#include <DallasTemperature.h>
#define ONE_WIRE_BUS 3
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);

#include <SoftwareSerial.h>
SoftwareSerial debugSerial(10, 11); // RX, TX
SoftwareSerial espSerial(0, 1);
int value = 0;
long lastRecv = 0;
long lastTime = 0;
bool newDataComing = false;

float getTemp() {
  sensors.requestTemperatures(); // Send the command to get temperatures
  //  Serial.print("Temperature for Device 1 is: ");
  float temp = sensors.getTempCByIndex(0);
  return temp;
}

void setup() {
  // put your setup code here, to run once:
  delay(5000);        // it will be better to delay 2s to wait esp8266 module OK
  Serial.begin(9600);
  debugSerial.begin(9600);
  espSerial.begin(9600);
  sensors.begin();
}
String strRecv = "";
void loop() {
  // put your main code here, to run repeatedly:
  long now = millis();
  if (espSerial.available() > 0) {
    char str = char(espSerial.read());
    strRecv = strRecv + str;
    lastRecv = millis();
    newDataComing = true;
    delay(2);
  } else if ((now - lastRecv > 100) && (newDataComing == true)) {
    debugSerial.print("[Receive:");
    debugSerial.println(strRecv);
    debugSerial.println("]");
    strRecv = "";
    newDataComing = false;
  }
  if (now - lastTime > 1000) {
    lastTime = now;
    float temp = getTemp();
    String JsonData = "{\"Temp\":\"TEMP_VALUE\"}";
    JsonData.replace("TEMP_VALUE", String(temp));
    debugSerial.println("[Send:");
    debugSerial.println(JsonData);
    debugSerial.println("]");
    Serial.println(JsonData);
  }
}

Debug串口

使用XBee USB Adapter适配器,将接地引脚、TX、RX分别接在IO扩展板的接地、10、11上,并用usb线连接电脑,打开CoolTerm,选择不同的端口,查看调试信息,进行调试。

其他问题

数据上行与下行的若干问题【2019-3-26更新】
pubsubclient mqtt的上行和下行数据大小限制

pubsubclient默认下行数据大小128bytes,超过大小的都会忽略,可以在pubsubclient.h中修改。

pubsubclient默认没有下行数据的限制,最大数据受制于硬件,但使用上例的client.publish()似乎无法直接发送超过100字节的数据,需要使用以下(不止这一种)方式替换:

client.beginPublish(publishTopic, String(strRecv).length(), false);
client.print(String(strRecv).c_str());
client.endPublish();
使用Serial.flush避免串口数据混淆
arduino处理接收数据的方式【2019-3-31更新】

此处假定arduino接收的数据格式为|1|2|3|的以|分割的形式

#define MAX_INSTRUCTION_LENGTH 10
String strArr[MAX_INSTRUCTION_LENGTH];
int len = 0;
int pos = 0;
String str = strRecv.substring(strRecv.indexOf("|") + 1, strRecv.length());
do {
	pos = str.indexOf("|");
	if (pos != -1) {
	  strArr[len] = str.substring(0, pos);
	  len++;
	  str = str.substring(pos + 1, str.length());
	}
} while (pos > 0);

int idx1 = atoi(strArr[0].c_str());
if (idx1 == 1) {
	int idx2 = atoi(strArr[1].c_str());
	if (idx2 == 1 && !stopUploadAllData) {
		// 停止上传数据 |1|1|
	  stopUploadAllData = true;
	  debugSerial.println("Stop");
	} else if (idx2 == 2 && stopUploadAllData) {
		// 启动上传数据 |1|2|
	  stopUploadAllData = false;
	  debugSerial.println("Start");
	} else if (idx2 == 3) {
	  int t = atoi(strArr[2].c_str());
	  if (t >= 1 && t <= 10) {
	  	// 修改采集数据间隔时间 |1|3|time|
	    collectIntervalTime = t * 1000;
	    debugSerial.println("Time Change To " + (String)collectIntervalTime);
	  }
	}
}

博客:Smartaq.cn

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值