一、环境介绍
单片机采用:STM32F103ZET6
上网方式:采用ESP8266,也可以使用其他设备代替,只要支持TCP协议即可。比如:GSM模块、有线网卡等。
与物联网云平台的通信协议: 标准MQTT协议3.1.1(TCP)
开发软件:keil5
物联网平台: 腾讯IOT物联网物联网平台。腾讯的物联网平台比起其他厂家的物联网平台更加有优势,腾讯物联网平台可以将数据推到微信小程序上,用户可以直接使用小程序绑定设备,完成与设备之间交互,现在用户基本都会使用微信,所以使用起来非常方便。
本项目完整代码下载地址: https://download.csdn.net/download/xiaolong1126626497/18973282
二、智慧农业介绍
智慧农业就是将物联网技术运用到传统农业中去,运用传感器和软件通过移动平台或者电脑平台对农业生产进行控制,使传统农业更具有“智慧”。除 了精准感知、控制与决策管理外,从广泛意义上讲,智慧农业还包括农业电子商务、食品溯源防伪、农业休闲旅游、农业信息服务等方面的内容。
所谓“智慧农业”就是充分应用现代信息技术成果,集成应用计算机与网络技术、物联网技术、音视频技术、3S技术、无线通信技术及专家智慧与知识,实现农业可视化远程诊断、远程控制、灾变预警等智能管理。
智慧农业是农业生产的高级阶段,是集新兴的互联网、移动互联网、云计算和物联网技术为一体,依托部署在农业生产现场的各种传感节点(环境温湿度、土壤水分、二氧化碳、图像等)和无线通信网络实现农业生产环境的智能感知、智能预警、智能决策、智能分析、专家在线指导,为农业生产提供精准化种植、可视化管理、智能化决策。
“智慧农业”是云计算、传感网、3S等多种信息技术在农业中综合、全面的应用,实现更完备的信息化基础支撑、更透彻的农业信息感知、更集中的数据资源、更广泛的互联互通、更深入的智能控制、更贴心的公众服务。“智慧农业”与现代生物技术、种植技术等科学技术融合于一体,对建设世界水平农业具有重要意义。
本项目采用STM32F103ZET6 + ESP8266 设计一个智慧农业管理系统, 能够获取空气中的温湿度数据,光照度数据等,根据种植区的空气温湿度数据,判断是否进行灌溉;可以通过ESP8266 + MQTT 协议将采集的温湿度、光照度上传至腾讯云物联网平台,并推送到微信小程序上实时查看;可以在小程序上直接控制电机抽水灌溉。
硬件详情介绍:
主控MCU: STM32F103ZET6
环境光传感器: BH1750
温湿度传感器: SHT30
本地OLED显示屏: 中景园电子0.96寸 SPI接口 OLED显示屏
电机: 微型直流电机
三、创建腾讯云物联网平台设备并配置微信小程序
如果之前从来没有使用过腾讯云物联网平台,创建的详细步骤请看这里: https://blog.csdn.net/xiaolong1126626497/article/details/116902653
下面就是登录腾讯云物联网平台,创建一个智慧农业的设备的关键步骤,有些细节步骤没写,细节请看上面链接这篇文章。
官网地址: https://console.cloud.tencent.com/iotexplorer
四、生成腾讯物联网平台的设备登录信息
使用MQTT协议登录需要一些参数信息,需要使用官网提供的方式生成。
Python示例代码:
#!/usr/bin/python # -*- coding: UTF-8 -*- import base64 import hashlib import hmac import random import string import time import sys # 生成指定长度的随机字符串 def RandomConnid(length): return ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(length)) # 生成接入物联网通信平台需要的各参数 def IotHmac(productID, devicename, devicePsk): # 1. 生成 connid 为一个随机字符串,方便后台定位问题 connid = RandomConnid(5) # 2. 生成过期时间,表示签名的过期时间,从纪元1970年1月1日 00:00:00 UTC 时间至今秒数的 UTF8 字符串 expiry = int(time.time()) + 30*24*60 * 60 # 3. 生成 MQTT 的 clientid 部分, 格式为 ${productid}${devicename} clientid = "{}{}".format(productID, devicename) # 4. 生成 MQTT 的 username 部分, 格式为 ${clientid};${sdkappid};${connid};${expiry} username = "{};12010126;{};{}".format(clientid, connid, expiry) # 5. 对 username 进行签名,生成token secret_key = devicePsk.encode('utf-8') # convert to bytes data_to_sign = username.encode('utf-8') # convert to bytes secret_key = base64.b64decode(secret_key) # this is still bytes token = hmac.new(secret_key, data_to_sign, digestmod=hashlib.sha256).hexdigest() # 6. 根据物联网通信平台规则生成 password 字段 password = "{};{}".format(token, "hmacsha256") return { "clientid" : clientid, "username" : username, "password" : password } if __name__ == '__main__': # 参数分别填入: 产品ID,设备名称,设备密匙 print(IotHmac("6142CX41XE", "SmartAgriculture","20Y/aAcmj+y6SDDh+ANR9g=="))
输出的登录参数,用于MQTT协议填参数:
{'clientid': '6142CX41XESmartAgriculture', 'username': '6142CX41XESmartAgriculture;12010126;HUA2G;1624271589', 'password': 'a8aadebe9721f70e6f9e14fe56ff1d2b5cac9625fa1f96af2f0e0098597fe78b;hmacsha256'}
五、使用MQTT软件测试
MQTT客户端软件下载地址: https://download.csdn.net/download/xiaolong1126626497/18784012
该软件采用QT开发,源码地址: https://blog.csdn.net/xiaolong1126626497/article/details/116779490
设备主题发布与定义的格式:
设备消息数据上传格式:
{"method":"report","clientToken":"123","params":{"light":78.4,"temperature":21.4,"humidity":60.8,"motor":1}}
登录成功之后,就可以看到设备在线:
打开微信小程序可以查看到设备上传的数据:
六、编写STM32设备端代码
6.1 main.c代码
#include "stm32f10x.h" #include "led.h" #include "delay.h" #include "key.h" #include "usart.h" #include <string.h> #include "timer.h" #include "esp8266.h" #include "mqtt.h" #include "oled.h" #include "fontdata.h" #include "bh1750.h" #include "iic.h" #include "sht3x.h" /* 硬件连接方式: ESP8266串口WIFI模块与STM32的串口3相连接。 PB10--RXD 模块接收脚 PB11--TXD 模块发送脚 GND---GND 地 VCC---VCC 电源(3.3V~5.0V) OLED接线: D0----SCK-----PB14 D1----MOSI----PB13 RES—复位(低电平有效)—PB12 DC---数据和命令控制管脚—PB1 CS---片选引脚-----PA7 微型直流电机: PB8 紫光灯: PB9 LED硬件连接: PB5 PE5 KEY硬件连接:PE3 PE4 */ #define ESP8266_WIFI_AP_SSID "CMCC-Cqvn" //将要连接的路由器名称 --不要出现中文、空格等特殊字符 #define ESP8266_AP_PASSWORD "99pu58cb" //将要连接的路由器密码 //腾讯物联网服务器的设备信息 #define MQTT_ClientID "6142CX41XESmartAgriculture" #define MQTT_UserName "6142CX41XESmartAgriculture;12010126;HUA2G;1624271589" #define MQTT_PassWord "a8aadebe9721f70e6f9e14fe56ff1d2b5cac9625fa1f96af2f0e0098597fe78b;hmacsha256" //订阅与发布的主题 #define SET_TOPIC "$thing/down/property/6142CX41XE/SmartAgriculture" //订阅 #define POST_TOPIC "$thing/up/property/6142CX41XE/SmartAgriculture" //发布 char mqtt_message[200];//上报数据缓存区 char OLED_ShowBuff[100]; u8 ESP8266_Stat=0; /* 函数功能: 温湿度\光强度显示 */ void ShowTemperatureAndHumidity(float temp,float humi,float light) { sprintf(OLED_ShowBuff,"T: %.2f",temp); OLED_ShowString(40,16*0,16,OLED_ShowBuff); sprintf(OLED_ShowBuff,"H: %.2f%%",humi); OLED_ShowString(40,16*1,16,OLED_ShowBuff); sprintf(OLED_ShowBuff,"L: %.2f%%",light); OLED_ShowString(40,16*2,16,OLED_ShowBuff); } /* 函数功能: ESP8266显示页面 */ void ESP8266_ShowPageTable(void) { if(ESP8266_Stat)OLED_ShowString(0,16*0,16,"WIFI STAT:ERROR"); else OLED_ShowString(0,16*0,16,"WIFI STAT:OK"); //显示字符串 sprintf((char*)OLED_ShowBuff,"%s",ESP8266_WIFI_AP_SSID); OLED_ShowString(0,16*1,16,OLED_ShowBuff); sprintf((char*)OLED_ShowBuff,"%s",ESP8266_AP_PASSWORD); OLED_ShowString(0,16*2,16,OLED_ShowBuff); } int main() { u32 time_cnt=0; u32 i; u8 key; u8 page=0; float temp=0; float humi=0; float light=0; u8 motor_state=0; float Humidity; float Temperature; delay_ms(1000); delay_ms(1000); LED_Init(); KEY_Init(); IIC_Init(); //OLED初始化 OLED_Init(0xc8,0xa1); //OLED显示屏初始化--正常显示; //清屏 OLED_Clear(0); USART1_Init(115200); TIMER1_Init(72,20000); //超时时间20ms USART3_Init(115200);//串口-WIFI TIMER3_Init(72,20000); //超时时间20ms Init_SHT30(); USART1_Printf("正在初始化WIFI请稍等.\n"); if(ESP8266_Init()) { ESP8266_Stat=1; USART1_Printf("ESP8266硬件检测错误.\n"); } else { //非加密端口 USART1_Printf("WIFI:%d\n",ESP8266_STA_TCP_Client_Mode(ESP8266_WIFI_AP_SSID,ESP8266_AP_PASSWORD,"106.55.124.154",1883,1)); } //2. MQTT协议初始化 MQTT_Init(); //3. 连接腾讯云IOT服务器 while(MQTT_Connect(MQTT_ClientID,MQTT_UserName,MQTT_PassWord)) { USART1_Printf("服务器连接失败,正在重试...\n"); delay_ms(500); } USART1_Printf("服务器连接成功.\n"); //3. 订阅主题 if(MQTT_SubscribeTopic(SET_TOPIC,0,1)) { USART1_Printf("主题订阅失败.\n"); } else { USART1_Printf("主题订阅成功.\n"); } while(1) { //按键可以测试开锁和关锁 key=KEY_Scan(0); if(key==1) { //清屏 OLED_Clear(0); //翻页 if(page>=1) { page=0; } else { page++; } LED1=!LED1; //LEd状态灯 } else if(key==2) { LED1=!LED1; //LEd状态灯 time_cnt=0; //电机状态改变 MOTOR_DEV=!MOTOR_DEV; //电机状态 motor_state=MOTOR_DEV; //补光灯 LIGHT_DEV=!LIGHT_DEV; } //微信小程序开锁方式: 接收WIFI返回的数据 if(USART3_RX_FLAG) { USART3_RX_BUFFER[USART3_RX_CNT]='\0'; //向串口打印微信小程序返回的数据 for(i=0;i<USART3_RX_CNT;i++) { USART1_Printf("%c",USART3_RX_BUFFER[i]); } //如果是下发了属性,判断是开锁还是关锁 if(USART3_RX_CNT>5) { //使用字符串查找函数 if(strstr((char*)&USART3_RX_BUFFER[5],"\"motor\":1")) { LED1=0; //亮灯 MOTOR_DEV=1; //开电机 motor_state=1; } else if(strstr((char*)&USART3_RX_BUFFER[5],"\"motor\":0")) { LED1=1; //灭灯 MOTOR_DEV=0; //关电机 motor_state=0; } } USART3_RX_CNT=0; USART3_RX_FLAG=0; } //定时与保持与微信小程序的同步--1秒一次 delay_ms(10); time_cnt++; if(time_cnt==50) { time_cnt=0; //状态灯 --表示程序还活着 LED2=!LED2; //读取光强度 light=Read_BH1750_Data(); //读取温湿度 SHT3x_ReadData(&Humidity,&Temperature); humi=Humidity; temp=Temperature; //上传数据 sprintf(mqtt_message,"{\"method\":\"report\",\"clientToken\":\"123\",\"params\":{\"temperature\":%f,\"humidity\":%f,\"motor\":%d,\"light\":%f}}", temp,humi,motor_state,light); MQTT_PublishData(POST_TOPIC,mqtt_message,0); //根据湿度自动灌溉 if(humi<50.0) //小于50自动灌溉 { motor_state=1; //电机状态更新 MOTOR_DEV=1; //开电机 } } //OLED显示屏 if(page==0) { ShowTemperatureAndHumidity(temp,humi,light); } else if(page==1) { ESP8266_ShowPageTable(); } } }
6.2 mqtt.c 代码
#include "mqtt.h" u8 *mqtt_rxbuf; u8 *mqtt_txbuf; u16 mqtt_rxlen; u16 mqtt_txlen; u8 _mqtt_txbuf[256];//发送数据缓存区 u8 _mqtt_rxbuf[256];//接收数据缓存区 typedef enum { //名字 值 报文流动方向 描述 M_RESERVED1 =0 , // 禁止 保留 M_CONNECT , // 客户端到服务端 客户端请求连接服务端 M_CONNACK , // 服务端到客户端 连接报文确认 M_PUBLISH , // 两个方向都允许 发布消息 M_PUBACK , // 两个方向都允许 QoS 1消息发布收到确认 M_PUBREC , // 两个方向都允许 发布收到(保证交付第一步) M_PUBREL , // 两个方向都允许 发布释放(保证交付第二步) M_PUBCOMP , // 两个方向都允许 QoS 2消息发布完成(保证交互第三步) M_SUBSCRIBE , // 客户端到服务端 客户端订阅请求 M_SUBACK , // 服务端到客户端 订阅请求报文确认 M_UNSUBSCRIBE , // 客户端到服务端 客户端取消订阅请求 M_UNSUBACK , // 服务端到客户端 取消订阅报文确认 M_PINGREQ , // 客户端到服务端 心跳请求 M_PINGRESP , // 服务端到客户端 心跳响应 M_DISCONNECT , // 客户端到服务端 客户端断开连接 M_RESERVED2 , // 禁止 保留 }_typdef_mqtt_message; //连接成功服务器回应 20 02 00 00 //客户端主动断开连接 e0 00 const u8 parket_connetAck[] = {0x20,0x02,0x00,0x00}; const u8 parket_disconnet[] = {0xe0,0x00}; const u8 parket_heart[] = {0xc0,0x00}; const u8 parket_heart_reply[] = {0xc0,0x00}; const u8 parket_subAck[] = {0x90,0x03}; void MQTT_Init(void) { //缓冲区赋值 mqtt_rxbuf = _mqtt_rxbuf; mqtt_rxlen = sizeof(_mqtt_rxbuf); mqtt_txbuf = _mqtt_txbuf; mqtt_txlen = sizeof(_mqtt_txbuf); memset(mqtt_rxbuf,0,mqtt_rxlen); memset(mqtt_txbuf,0,mqtt_txlen); //无条件先主动断开 MQTT_Disconnect(); delay_ms(100); MQTT_Disconnect(); delay_ms(100); } /* 函数功能: 登录服务器 函数返回值: 0表示成功 1表示失败 */ u8 MQTT_Connect(char *ClientID,char *Username,char *Password) { u8 i,j; int ClientIDLen = strlen(ClientID); int UsernameLen = strlen(Username); int PasswordLen = strlen(Password); int DataLen; mqtt_txlen=0; //可变报头+Payload 每个字段包含两个字节的长度标识 DataLen = 10 + (ClientIDLen+2) + (UsernameLen+2) + (PasswordLen+2); //固定报头 //控制报文类型 mqtt_txbuf[mqtt_txlen++] = 0x10; //MQTT Message Type CONNECT //剩余长度(不包括固定头部) do { u8 encodedByte = DataLen % 128; DataLen = DataLen / 128; // if there are more data to encode, set the top bit of this byte if ( DataLen > 0 ) encodedByte = encodedByte | 128; mqtt_txbuf[mqtt_txlen++] = encodedByte; }while ( DataLen > 0 ); //可变报头 //协议名 mqtt_txbuf[mqtt_txlen++] = 0; // Protocol Name Length MSB mqtt_txbuf[mqtt_txlen++] = 4; // Protocol Name Length LSB mqtt_txbuf[mqtt_txlen++] = 'M'; // ASCII Code for M mqtt_txbuf[mqtt_txlen++] = 'Q'; // ASCII Code for Q mqtt_txbuf[mqtt_txlen++] = 'T'; // ASCII Code for T mqtt_txbuf[mqtt_txlen++] = 'T'; // ASCII Code for T //协议级别 mqtt_txbuf[mqtt_txlen++] = 4; // MQTT Protocol version = 4 对于 3.1.1 版协议,协议级别字段的值是 4(0x04) //连接标志 mqtt_txbuf[mqtt_txlen++] = 0xc2; // conn flags mqtt_txbuf[mqtt_txlen++] = 0; // Keep-alive Time Length MSB mqtt_txbuf[mqtt_txlen++] = 100; // Keep-alive Time Length LSB 100S心跳包 保活时间 mqtt_txbuf[mqtt_txlen++] = BYTE1(ClientIDLen);// Client ID length MSB mqtt_txbuf[mqtt_txlen++] = BYTE0(ClientIDLen);// Client ID length LSB memcpy(&mqtt_txbuf[mqtt_txlen],ClientID,ClientIDLen); mqtt_txlen += ClientIDLen; if(UsernameLen > 0) { mqtt_txbuf[mqtt_txlen++] = BYTE1(UsernameLen); //username length MSB mqtt_txbuf[mqtt_txlen++] = BYTE0(UsernameLen); //username length LSB memcpy(&mqtt_txbuf[mqtt_txlen],Username,UsernameLen); mqtt_txlen += UsernameLen; } if(PasswordLen > 0) { mqtt_txbuf[mqtt_txlen++] = BYTE1(PasswordLen); //password length MSB mqtt_txbuf[mqtt_txlen++] = BYTE0(PasswordLen); //password length LSB memcpy(&mqtt_txbuf[mqtt_txlen],Password,PasswordLen); mqtt_txlen += PasswordLen; } memset(mqtt_rxbuf,0,mqtt_rxlen); MQTT_SendBuf(mqtt_txbuf,mqtt_txlen); for(j=0;j<10;j++) { delay_ms(50); if(USART3_RX_FLAG) { memcpy((char *)mqtt_rxbuf,USART3_RX_BUFFER,USART3_RX_CNT); //memcpy for(i=0;i<USART3_RX_CNT;i++)USART1_Printf("%#x ",USART3_RX_BUFFER[i]); USART3_RX_FLAG=0; USART3_RX_CNT=0; } //CONNECT if(mqtt_rxbuf[0]==parket_connetAck[0] && mqtt_rxbuf[1]==parket_connetAck[1]) //连接成功 { return 0;//连接成功 } } return 1; } /* 函数功能: MQTT订阅/取消订阅数据打包函数 函数参数: topic 主题 qos 消息等级 0:最多分发一次 1: 至少分发一次 2: 仅分发一次 whether 订阅/取消订阅请求包 (1表示订阅,0表示取消订阅) 返回值: 0表示成功 1表示失败 */ u8 MQTT_SubscribeTopic(char *topic,u8 qos,u8 whether) { u8 i,j; mqtt_txlen=0; int topiclen = strlen(topic); int DataLen = 2 + (topiclen+2) + (whether?1:0);//可变报头的长度(2字节)加上有效载荷的长度 //固定报头 //控制报文类型 if(whether)mqtt_txbuf[mqtt_txlen++] = 0x82; //消息类型和标志订阅 else mqtt_txbuf[mqtt_txlen++] = 0xA2; //取消订阅 //剩余长度 do { u8 encodedByte = DataLen % 128; DataLen = DataLen / 128; // if there are more data to encode, set the top bit of this byte if ( DataLen > 0 ) encodedByte = encodedByte | 128; mqtt_txbuf[mqtt_txlen++] = encodedByte; }while ( DataLen > 0 ); //可变报头 mqtt_txbuf[mqtt_txlen++] = 0; //消息标识符 MSB mqtt_txbuf[mqtt_txlen++] = 0x0A; //消息标识符 LSB //有效载荷 mqtt_txbuf[mqtt_txlen++] = BYTE1(topiclen);//主题长度 MSB mqtt_txbuf[mqtt_txlen++] = BYTE0(topiclen);//主题长度 LSB memcpy(&mqtt_txbuf[mqtt_txlen],topic,topiclen); mqtt_txlen += topiclen; if(whether) { mqtt_txbuf[mqtt_txlen++] = qos;//QoS级别 } for(i=0;i<10;i++) { memset(mqtt_rxbuf,0,mqtt_rxlen); MQTT_SendBuf(mqtt_txbuf,mqtt_txlen); for(j=0;j<10;j++) { delay_ms(50); if(USART3_RX_FLAG) { memcpy((char *)mqtt_rxbuf,(char*)USART3_RX_BUFFER,USART3_RX_CNT); USART3_RX_FLAG=0; USART3_RX_CNT=0; } if(mqtt_rxbuf[0]==parket_subAck[0] && mqtt_rxbuf[1]==parket_subAck[1]) //订阅成功 { return 0;//订阅成功 } } } return 1; //失败 } //MQTT发布数据打包函数 //topic 主题 //message 消息 //qos 消息等级 u8 MQTT_PublishData(char *topic, char *message, u8 qos) { int topicLength = strlen(topic); int messageLength = strlen(message); static u16 id=0; int DataLen; mqtt_txlen=0; //有效载荷的长度这样计算:用固定报头中的剩余长度字段的值减去可变报头的长度 //QOS为0时没有标识符 //数据长度 主题名 报文标识符 有效载荷 if(qos) DataLen = (2+topicLength) + 2 + messageLength; else DataLen = (2+topicLength) + messageLength; //固定报头 //控制报文类型 mqtt_txbuf[mqtt_txlen++] = 0x30; // MQTT Message Type PUBLISH //剩余长度 do { u8 encodedByte = DataLen % 128; DataLen = DataLen / 128; // if there are more data to encode, set the top bit of this byte if ( DataLen > 0 ) encodedByte = encodedByte | 128; mqtt_txbuf[mqtt_txlen++] = encodedByte; }while ( DataLen > 0 ); mqtt_txbuf[mqtt_txlen++] = BYTE1(topicLength);//主题长度MSB mqtt_txbuf[mqtt_txlen++] = BYTE0(topicLength);//主题长度LSB memcpy(&mqtt_txbuf[mqtt_txlen],topic,topicLength);//拷贝主题 mqtt_txlen += topicLength; //报文标识符 if(qos) { mqtt_txbuf[mqtt_txlen++] = BYTE1(id); mqtt_txbuf[mqtt_txlen++] = BYTE0(id); id++; } memcpy(&mqtt_txbuf[mqtt_txlen],message,messageLength); mqtt_txlen += messageLength; MQTT_SendBuf(mqtt_txbuf,mqtt_txlen); return mqtt_txlen; } void MQTT_SentHeart(void) { MQTT_SendBuf((u8 *)parket_heart,sizeof(parket_heart)); } void MQTT_Disconnect(void) { MQTT_SendBuf((u8 *)parket_disconnet,sizeof(parket_disconnet)); } void MQTT_SendBuf(u8 *buf,u16 len) { USARTx_DataSend(USART3,buf,len); }