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]
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 | |
身份级别 | level | 0-待确认,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 | |
创建人 | creater | ref:User |
绑定用户 | users | Array ref:User |
创建时间 | date | |
密码 | password | |
运行状态 | runState | Object |
{
// 文档名 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
}
数据定义
数据定义
字段 | |
---|---|
参数id | id |
参数名 | name |
单位 | unit |
参数最大值 | max |
参数最小值 | min |
预警最大值 | wmax |
预警最小值 | wmin |
参数描述 | desc |
手动 | manual |
默认数据定义
字段 | ||
---|---|---|
设置人 | whoSet | ref:User |
数据定义 | define | Array |
日期 | date | |
使用计数 | usedCount |
{
// 文档名 dfdefine
// 设置人
whoSet: {
type: mongoose.Schema.Types.ObjectId,
ref: 'user'
},
// 数据定义
define: Array,
// 日期
date: Date,
// 使用计数
usedCount: {
type: Number,
default: 0,
min: 0
}
}
设备数据定义
字段 | ||
---|---|---|
设备 | device | ref:Device |
数据定义 | define | |
参考定义 | refDefine | ref:Dfdefine |
设置人 | whoSet | ref:User |
日期 | date | |
过期 | expired | Boolen |
{
// 文档名 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 | |
设备 | device | ref:Device |
数据 | data | Object |
日期 | date | |
记录 | issue | ref:Issue |
预警 | warning | Boolen |
使用的数据定义 | usedDefine | ref: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 | ||
data | define表 {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
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提供authenticate
、autenticatePunlish
和authenticateSubscribe
方法对连接请求进行验证,均可以被覆盖。
//连接认证,验证上传的用户名和密码与服务器保存的信息(如数据库)一致
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服务)。
- 为服务器端和客户端准备公钥、私钥
// 生成服务器端私钥
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
- 生成 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]: 后面生成客户端和服务器端证书的时候也需要填写,不要写成一样的
- 生成服务器端证书和客户端证书
// 服务器端需要向 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 uno
和Obloq
模块转到arduino R3 Uno
、IO扩展板
和WiFi Bee
模块,相关文档和代码十分有限,网上大部分教程都无法使用,有各种错误,这里记录了最终成功的过程。
材料
所用设备
软件及库
Arduino IDE 1.8.7
- 串口调试软件CoolTerm
FTDI USB Drivers
驱动,国内,海外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.jsonpubsubclient
,官网,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