项目功能
利用嵌入式设备组成蔬菜大棚监测系统,实现实时温湿度监测,光照强度监测,在温湿度过高的程度时控制风扇的开关和排风速率,根据光照数据实时调整补光LED的亮度,同时在安卓端实时查看设备情况,查看历史传感器数据,展示温湿度变化图表,同时支持手动接管风扇和LED的开关。
整体设计
硬件端
硬件连接
使用博创的i.MX6和配套的Cortex-A底板、自行购买的高亮度LED模块、自行购买的ESP-01模块以及配套的5V供电串口转接板和USB烧录器、博创的温湿度传感器、博创的光照强度传感器、博创的直流电机桥模块。
接线如下,其中博创的温湿度传感器连接 底板的P8接口,博创的光照强度传感器的J1接口连接到 底板的P1接口,J2接口连接到 底板的P5接口,博创的直流电机桥模块连接 底板的P6接口,高亮度LED模块可以随便在底板P系列接口处找到空余的5V和GND接入,同时将Signal引脚接入 底板J5的20Pin,ESP-01转接板可以随便在底板P系列接口处找到空余的5V和GND接入即可。
烧录系统
进入虚拟机内核,修改设备树文档,启用PWM3引脚(SD1_DAT0),并注释掉引脚复用的冲突部分,用于高亮度LED使用。注意,此处不能启用PWM4引脚(SD1_CMD),因为温湿度传感器默认配置下依赖此引脚作为GPIO。详情请见番外篇——直流电机桥源码分析&LED驱动例程开发
修改后重新编译内核和设备树,并烧录系统,完成后初始化网络,挂载虚拟机NFS共享目录。
软件端
代码整合
由于技术有限,选择使用额外的ESP-01作为中转,而不是直接让开发板操作数据库,使用UDP协议和开发板双向通信,ESP-01需要从数据库获取控制数据,并发送给开发板,同时从开发板接收传感器数据,发送至服务器。
整合时需要特别注意变量名称可能重复,例如int fd经常用作open操作,在整合时需要对不同的传感器分别命名。
整合后需要对每个传感器进行功能测试,互相兼容即可。通过测试后可以开发上层应用,控制每一轮循环内检测传感器数值,同时接收UDP提供的数据库控制数据,两者结合对电机、LED硬件进行相应的控制。
最终开发后代码如下,驱动文件请自行加载,代码仅供参考,请以实际情况为准。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <math.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<netdb.h>
#include<sys/socket.h>
#include<sys/wait.h>
#include<arpa/inet.h>
#include<errno.h>
#define DCM_IOCTRL_SETPWM (0x10)
#define DCM_IOCTRL_STATUS (0x20)
#define DCM_IOCTRL_STOP (0x30)
#define DCM_IOCTRL_START (0x40)
#define I2C_DEV "/dev/i2c-0"
#define I2C_SLAVE 0x0703
#define I2C_TENBIT 0x0704
#define CHIP_ADDR 0x23
#define HBLED_IOCTRL_SETPWM (0x10)
#define HBLED_IOCTRL_OFF (0x20)
#define HBLED_IOCTRL_ON (0x30)
//local listening port
#define getPort 12300
//remote listening port
#define sendPort 10000
//remote ip
#define sendIP "192.168.43.86"
//cycle count
#define delayTimes 15
//limit
#define TempTrigger 23
#define HumiTrigger 60
#define IlluTrigger 400
char *DCM_DEV= "/dev/DCMotor";
int cmd_group1[] = { DCM_IOCTRL_SETPWM, DCM_IOCTRL_START, DCM_IOCTRL_STATUS, DCM_IOCTRL_STOP };
char* HBLED_DEV = "/dev/HBLED";
int cmd_group2[] = { HBLED_IOCTRL_SETPWM, HBLED_IOCTRL_ON, HBLED_IOCTRL_OFF };
//R&W BH1750
static int read_BH1750(int fd, void *buff, int count)
{
int bh1750_res;
bh1750_res = read(fd,buff,count);
return bh1750_res;
}
static int write_BH1750(int fd, unsigned char addr, size_t count)
{
int bh1750_res;
char sendbuffer[count+1];
sendbuffer[0] = addr;
bh1750_res = write(fd,sendbuffer,count);
return bh1750_res;
}
int main(void)
{
//UDP_init_para
int sockListen;
int manualValue;
int mrpmValue;
int lswitch;
char msg[128] = "";
int recvbytes;
char recvbuf[128];
char tempManul[10];
char tempRpm[10];
char tempLignt[10];
double temperature;
double humidity;
int rpm;
double light;
int count = 0;
//DCMotor pre-config
int dcm_fd;
int dcm_arg = 0;
//SHT11 pre-config
int sht_fd, sht_ret;
//0:temp_value; 1:humi_value
int sht_data[2];
float temp_value = 0.0;
float humi_value = 0.0;
//BH1750 pre-config
int bh1750_fd,bh1750_res;
float flux;
unsigned char bh1750_buf[2];
//HBLED pre-config
int hbled_fd;
int hbled_arg = 0;
//open DCMotor
dcm_fd = open(DCM_DEV, O_WRONLY);
if(dcm_fd < 0){
printf("open /dev/DCMotor error!\n");
return -1;
}
//open SHT11
sht_fd = open("/dev/SHT11",0);
if(sht_fd < 0)
{
printf("open /dev/SHT11 error!\n");
return -1;
}
//open BH1750
bh1750_fd = open(I2C_DEV, O_RDWR);
if(bh1750_fd < 0){
printf("open /dev/BH1750 error!\n");
return -1;
}
//config BH1750
if(-1 == ioctl(bh1750_fd,I2C_TENBIT,0)){
printf("ioctl error on line %d\n",__LINE__);
return -1;
}
if(-1 == ioctl(bh1750_fd,I2C_SLAVE,CHIP_ADDR)){
printf("ioctl error on line %d\n",__LINE__);
return -1;
}
bh1750_res = write_BH1750(bh1750_fd, 0x01, 1);
if(bh1750_res == -1){
printf("write error on line %d\n", __LINE__);
}
bh1750_res = write_BH1750(bh1750_fd, 0x10, 1);
if(bh1750_res == -1){
printf("write error on line %d\n", __LINE__);
}
//open HBLED
hbled_fd = open(HBLED_DEV, O_WRONLY);
if (hbled_fd < 0) {
printf("open /dev/HBLED error!\n");
return -1;
}
while(1)
{
if ((count+1) % (delayTimes+1) != 0) {
if((sockListen = socket(AF_INET, SOCK_DGRAM, 0)) == -1){
printf("socket fail\n");
return -1;
}
int set = 1;
setsockopt(sockListen, SOL_SOCKET, SO_REUSEADDR, &set, sizeof(int));
struct sockaddr_in recvAddr;
memset(&recvAddr, 0, sizeof(struct sockaddr_in));
recvAddr.sin_family = AF_INET;
recvAddr.sin_port = htons(getPort);
recvAddr.sin_addr.s_addr = INADDR_ANY;
// bind is critial before listening
if(bind(sockListen, (struct sockaddr *)&recvAddr, sizeof(struct sockaddr)) == -1){
printf("bind fail\n");
return -1;
}
char* temp;
int i = 0;
int addrLen = sizeof(struct sockaddr_in);
if((recvbytes = recvfrom(sockListen, recvbuf, 128, 0,
(struct sockaddr *)&recvAddr, &addrLen)) != -1 && strlen(recvbuf) != 0){
recvbuf[recvbytes] = '\0';
temp = recvbuf;
memset(tempManul, 0, strlen(tempManul));
memset(tempRpm, 0, strlen(tempRpm));
memset(tempLignt, 0, strlen(tempLignt));
while (*temp >= '0' && *temp <= '9')
{
tempManul[i] = *temp;
i++;
temp++;
}
temp++;
i = 0;
while (*temp >= '0' && *temp <= '9')
{
tempRpm[i] = *temp;
i++;
temp++;
}
temp++;
i = 0;
while (*temp >= '0' && *temp <= '9')
{
tempLignt[i] = *temp;
i++;
temp++;
}
manualValue = atoi(tempManul);
mrpmValue = atoi(tempRpm);
lswitch = atoi(tempLignt);
// --------此处设置是否手动,转速值,光照值从数据库中获得---------
sht_ret=read(sht_fd,sht_data,sizeof(sht_data));
if(sht_ret<0)
{
printf("SHT11 read err!\n");
continue;
}
temp_value = (float)sht_data[0]/1000;
humi_value = (float)sht_data[1]/1000;
printf("temp:%.2f humi:%4.2f%\n",temp_value,humi_value);
memset(bh1750_buf,0,sizeof(bh1750_buf));
read_BH1750(bh1750_fd,bh1750_buf,2);
flux = (float)(bh1750_buf[0] << 8 | bh1750_buf[1])/1.2;
printf("BH1750: %6.2f lux\n", flux);
fflush(stdout);
dcm_arg = 0;
if(manualValue){
if(mrpmValue){
dcm_arg = mrpmValue;
ioctl(dcm_fd, DCM_IOCTRL_START, dcm_arg);
ioctl(dcm_fd, DCM_IOCTRL_SETPWM, dcm_arg);
}else{
ioctl(dcm_fd, DCM_IOCTRL_STOP, dcm_arg);
}
}else{
if(temp_value >= TempTrigger){
dcm_arg = (int)(temp_value - TempTrigger) / 2 + 1;
if(dcm_arg > 9){
dcm_arg = 9;
}
if(dcm_arg < 1){
dcm_arg = 1;
}
}
if(humi_value >= HumiTrigger){
dcm_arg = (int)(humi_value - HumiTrigger) / 5 + 1 + dcm_arg;
if(dcm_arg > 9){
dcm_arg = 9;
}
if(dcm_arg < 1){
dcm_arg = 1;
}
}
if(dcm_arg){
ioctl(dcm_fd, DCM_IOCTRL_START, dcm_arg);
ioctl(dcm_fd, DCM_IOCTRL_SETPWM, dcm_arg);
}
else{
ioctl(dcm_fd, DCM_IOCTRL_STOP, dcm_arg);
}
}
if(lswitch){
ioctl(hbled_fd, HBLED_IOCTRL_SETPWM, hbled_arg);
ioctl(hbled_fd, HBLED_IOCTRL_ON, hbled_arg);
if(flux <= IlluTrigger){
hbled_arg++;
if(hbled_arg > 100){
hbled_arg = 100;
}
}else{
hbled_arg--;
if(hbled_arg < 0){
hbled_arg = 0;
}
}
ioctl(hbled_fd, HBLED_IOCTRL_SETPWM, hbled_arg);
}else{
ioctl(hbled_fd, HBLED_IOCTRL_OFF, hbled_arg);
}
// ---------------------------------------------------------------
printf("receive a broadCast messgse:%d %d %d\n", manualValue, mrpmValue, lswitch);
memset(recvbuf, 0, strlen(recvbuf));
count++;
}else{
printf("recvfrom fail\n");
}
}
//close(sockListen);
//------------------------------------------------------------------------
else {
// get all four data from sensor
// ----------------------------------------
temperature = (float)((int)(temp_value * 100) / 100);
humidity = (float)((int)(humi_value * 100) / 100);
rpm = dcm_arg;
light = flux;
// ----------------------------------------
sprintf(msg, "%f %f %d %f", temperature, humidity, rpm, light);
int brdcFd;
if((brdcFd = socket(PF_INET, SOCK_DGRAM, 0)) == -1){
printf("socket fail\n");
return -1;
}
int optval = 1;//这个值一定要设置,否则可能导致sendto()失败
setsockopt(brdcFd, SOL_SOCKET, SO_BROADCAST | SO_REUSEADDR, &optval, sizeof(int));
struct sockaddr_in theirAddr;
memset(&theirAddr, 0, sizeof(struct sockaddr_in));
theirAddr.sin_family = AF_INET;
theirAddr.sin_addr.s_addr = inet_addr(sendIP);
theirAddr.sin_port = htons(sendPort);
int sendBytes;
if((sendBytes = sendto(brdcFd, msg, strlen(msg), 0,
(struct sockaddr *)&theirAddr, sizeof(struct sockaddr))) == -1){
printf("sendto fail, errno=%d\n", errno);
return -1;
}
printf("msg=%s, msgLen=%d\n", msg, (int)strlen(msg));
count = 0;
}
}
ioctl(dcm_fd, DCM_IOCTRL_STOP, dcm_arg);
ioctl(hbled_fd, HBLED_IOCTRL_OFF, hbled_arg);
close(dcm_fd);
close(sht_fd);
close(bh1750_fd);
close(hbled_fd);
return 0;
}
开发板UDP通信流程
UDP通信在未接收到信息时会阻塞线程,利用此特点,可以设置计数器,每接收15个数据再向ESP8266发送一次。
最终可以将配网、加载驱动、执行程序写在同一个脚本中,然后修改rc.local,使其开机自启。
其他
ESP8266模块
使用Arduino IDE对ESP-01进行开发,实现配网、SQL语句操作数据库、UDP协议和开发板通信。
首先从数据库接收最新的控制数据发送到开发板,发送频率控制在一秒一次,然后判断UDP接收,解析后有内容则按规则拆解变量,再发送至数据库。
若有需要,请自行深入学习如何在Arduino上实现ESP8266开发,此处仅提供部分代码以供参考。使用Arduino IDE进行编译烧录,服务器ip、UDP发送和监听端口、数据库账号密码、wifi账号密码需要根据需要进行修改。
#include <ESP8266WiFi.h>
#include <WiFiUDP.h>
#include <MySQL_Connection.h>
#include <MySQL_Cursor.h>
#include<stdlib.h>
IPAddress server_addr(***,***,***,***);
IPAddress local_IP(192, 168, 43, 86);
IPAddress gateway(192, 168, 43, 1);
IPAddress subnet(255, 255, 255, 0);
char user[30] = "MYSQL_ACCOUNT";
char password[30] = "MYSQL_PASSWD";
char ssid[30] = "SSID";
char pass[30] = "PASSWD";
unsigned int rmUDPPort = 12300;
unsigned int lcUDPPort = 10000;
char ReceiveBuffer[255] = "";
//buffer to hold incoming packet
char SendBuffer[255] = "";
// a string to send back
int manualValue;
int mrpmValue;
int lswitchValue;
WiFiClient client;
WiFiUDP Udp;
char SQL[500];
void Send(char sendbuff[])
{
Udp.beginPacket("192.168.43.3", rmUDPPort);
Udp.write(sendbuff);
Udp.endPacket();
}
void SQL_GEN_I(double temperature, double humidity, int rpm, double light){
sprintf(SQL, "INSERT INTO SCDP.Data (temperature, humidity, rpm, light) VALUES ('%f','%f','%d','%f')", temperature, humidity, rpm, light);
}
void SQL_GEN_S(){
sprintf(SQL, "SELECT manual, mrpm, lswitch FROM SCDP.Control order by id desc limit 1");
}
void SQL_SELECT(){
MySQL_Connection conn(&client);
MySQL_Cursor* cur;
Serial.print("Connecting to SQL... ");
if (conn.connect(server_addr, 3306, user, password))
Serial.println("OK.");
else
Serial.println("FAILED.");
cur = new MySQL_Cursor(&conn);
if (conn.connected())
cur->execute(SQL);
row_values *row = NULL;
column_names *cols = cur->get_columns();
do {
row = cur->get_next_row();
if (row != NULL) {
manualValue = atoi(row->values[0]);
mrpmValue = atoi(row->values[1]);
lswitchValue = atoi(row->values[2]);
} while (row != NULL);
cur->close();
conn.close();
delete cur;
}
void SQL_INSERT(){
MySQL_Connection conn(&client);
MySQL_Cursor* cur;
Serial.print("Connecting to SQL... ");
if (conn.connect(server_addr, 3306, user, password))
Serial.println("OK.");
else
Serial.println("FAILED.");
cur = new MySQL_Cursor(&conn);
if (conn.connected()){
cur->execute(SQL);
}
cur->close();
conn.close();
delete cur;
}
void setup() {
// put your setup code here, to run once:
Serial.begin(115200);
Serial.printf("\nConnecting to %s", ssid);
//设置静态IP
WiFi.config(local_IP, gateway, subnet);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, pass);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nConnected to network");
Serial.print("My IP address is: ");
Serial.println(WiFi.localIP());
// 开始UDP端口侦听
Udp.begin(lcUDPPort);
}
void loop() {
// put your main code here, to run repeatedly:
delay(1000);
ReceiveBuffer[0] = '\0';
SendBuffer[0] = '\0';
int packetSize = Udp.parsePacket();
//解析包不为空
if (packetSize)
{
String tempString = Udp.readString();
for(int i = 0; i < tempString.length(); i++){
ReceiveBuffer[i] = tempString[i];
}
int countSpace = 0;
int i = 0;
char tempTemperature[255] = "";
char tempHumidity[255] = "";
char tempRPM[255] = "";
char tempLight[255] = "";
char tempChar[255] = "";
for(i = 0; i < strlen(ReceiveBuffer); i++){
switch(countSpace){
case 0:
if(ReceiveBuffer[i] != ' '){
if(ReceiveBuffer[i] == '.' || ReceiveBuffer[i] <= '9' || ReceiveBuffer[i] >= '0'){
tempChar[0] = ReceiveBuffer[i];
strcat(tempTemperature, tempChar);
}
}
else{
countSpace++;
tempChar[0] = '\0';
strcat(tempTemperature, tempChar);
}
break;
case 1:
if(ReceiveBuffer[i] != ' '){
if(ReceiveBuffer[i] == '.' || ReceiveBuffer[i] <= '9' || ReceiveBuffer[i] >= '0'){
tempChar[0] = ReceiveBuffer[i];
strcat(tempHumidity, tempChar);
}
}
else{
countSpace++;
tempChar[0] = '\0';
strcat(tempHumidity, tempChar);
}
break;
case 2:
if(ReceiveBuffer[i] != ' '){
if(ReceiveBuffer[i] <= '9' || ReceiveBuffer[i] >= '0'){
tempChar[0] = ReceiveBuffer[i];
strcat(tempRPM, tempChar);
}
}
else{
countSpace++;
tempChar[0] = '\0';
strcat(tempRPM, tempChar);
}
break;
case 3:
if(ReceiveBuffer[i] != ' '){
if(ReceiveBuffer[i] == '.' || ReceiveBuffer[i] <= '9' || ReceiveBuffer[i] >= '0'){
tempChar[0] = ReceiveBuffer[i];
strcat(tempLight, tempChar);
}
}
else{
countSpace++;
tempChar[0] = '\0';
strcat(tempLight, tempChar);
}
break;
}
}
double temperature = atof(tempTemperature);
double humidity = atof(tempHumidity);
int rpm = atoi(tempRPM);
double light = atof(tempLight);
SQL_GEN_I(temperature, humidity, rpm, light);
SQL_INSERT();
}
SQL_GEN_S();
SQL_SELECT();
sprintf(SendBuffer, "%d %d %d", manualValue, mrpmValue, lswitchValue);
Send(SendBuffer);
}
安卓端开发
使用Android Studio进行开发,通过定时从数据库获取数据实现实时查看设备情况以及查看历史传感器数据,展示温湿度变化图表。手动接管风扇和LED的开关有变化时,向数据库上传操作数据。
安卓端代码不予提供。
服务器配置
使用华为云CentOS7云耀云服务器,借助宝塔面板完成LNMP环境配置,数据库结构如下。