软硬件开源项目-智能农业监控系统:MQTT阿里云平台监测+内置Web网页控制+代码解析
智能农业监控系统:开源项目推荐
【下载地址】智能农业监控系统源码 本仓库提供了一套完整的智能农业监控系统源码,基于 W55MH32 以太网单片机实现三大核心功能:通过 ADC 采集土壤湿度与光照数据,基于阈值自动控制水泵灌溉;经 MQTT 协议对接阿里云,实现数据远程查看与设备控制;提供本地 Web 接口,支持实时监测与阈值调整。 项目仓库地址:https://gitee.com/shenzhen-weishi_3_0/W55MH32
完整显示视频bilibili|点击跳转
1 项目构思与核心目标
老家亲戚种大棚,灌溉总让人头疼:每天来回查湿度,凭经验浇水,忙时作物缺水蔫苗,雨天积水烂根,既费人力又浪费水,环境调节总跟不上作物需求。我便琢磨着做套简易系统解决这些问题。
之前用过 WIZnet 的 W5500 芯片做以太网项目,对他们的芯片挺熟悉。听说新出了带 MCU 的 W55MH32 以太网芯片,就申请了开发板试试。拿到板子后,先连接传感器在自家盆栽做测试,确认硬件兼容和数据稳定后,正式搭建智能农业监管系统。这板子自带硬件 TCP/IP 引擎,外设接口丰富,刚好满足传感器连接和数据传输需求,官方还有阿里云连接例程,省了不少事。有之前的经验打底,用它连阿里云、搭局域网监控网页,心里挺有底。我计划搭传感器与水泵联动模型:传感器采集土壤湿度、光照等数据,传云端后自动控制水泵启停。说干就干,画接线图、连传感器和继电器,优化 MQTT 逻辑接阿里云,还做了简易网页方便远程查看操作。调试后,数据采集和水泵自动启停功能都稳定实现了。这个模型还在完善中,后续会优化硬件集成和代码逻辑,让运行更稳定耐用。现在整理开发细节记录下来,之后会开源代码和 PCB 文件,希望给想在田间用物联网技术的朋友做个参考,提供思路,让技术真正为农活添力。
核心目标:
硬件层面:实现传感器数据采集与执行器(水泵)控制的稳定联动,确保环境数据实时性与设备响应可靠性。
软件层面:完成 MQTT 协议对接阿里云,实现设备与云端的双向数据传输,同时支持本地网页控制和环境数据查看。
应用层面:默认土壤湿度低于 30% 自动启动灌溉,高于 50% 自动停止,支持远程动态调整阈值,兼顾自动化与灵活性。
后续计划进一步将W55MH32以太网单片机芯片与光照传感器、土壤湿度传感器深度集成,同时匹配适配田间环境的防护外壳——既通过硬件整合提升系统稳定性,又借助外壳抵御大棚内的温湿度波动、粉尘等干扰,让设备在实际农业场景中更耐用、易部署。此外,还会公开完整的硬件原理图和PCB设计文件,方便有需要的朋友参考复用,降低技术落地的门槛,让这套方案能更便捷地应用到田间地头。
1.1 方案图示

2 硬件选型与搭建
2.1 核心组件清单
主控:W55MH32L-EVB(216MHz主频、自带硬件TCP/IP引擎)。
传感器:土壤湿度传感器(模拟输出)、光照传感器(模拟输出)。
执行器:5V继电器模块、小型水泵。
辅助设备:外部 5V 电源、网线、路由器、杜邦线若干。
2.2 电路连接技巧
开发板引脚定义复杂,我采用"功能分组"法简化连接:
模拟量输入组:PA0接土壤湿度传感器,PA3接光照传感器(利用单片机ADC功能)。
数字输出组:PB10 接继电器IN引脚(控制信号)。
电源组:开发板5V输出给继电器供电,传感器独立接3.3V(避免干扰),继电器COM端接外接电源正极。
水泵:正极接继电器常开端,负极接外接电源负极。
特别注意继电器的"低电平有效"特性——初始化时需将PB10置高,通过拉低电平触发动作,这一点在后续软件设计中需重点匹配。
3 开发环境搭建
3.1 软件工具链
编译环境:Keil uVision5,版本大于V5.3(需安装W55MH32 系列芯片包)。
调试工具:WIZ UartTool串口助手,其他串口助手也可。
浏览器:用于打开网页查看。
云平台:阿里云物联网平台(需完成实名认证)。
源码:gitee仓库项目仓库地址|点击跳转
4 连接阿里云物联网平台
4.1 MQTT连接阿里云收发数据流程
4.1.1 准备阶段
注册与实名认证:用户需要在阿里云平台注册账号,并完成实名认证。
创建产品和添加物模型:登录阿里云物联网平台,创建产品并在产品下添加以下物模型功能。

创建设备:在刚刚创建的产品下创建一个设备。

4.1.2 记录参数
连接参数:在刚刚创建的设备详情页中找到MQTT连接参数。

订阅主题:/sys/k1zh33h3hte/${deviceName}/thing/service/property/set(属性设置主题)
发布主题:/sys/k1zh33h3hte/${deviceName}/thing/event/property/post(上报消息主题)
注意:上面两个主题中的${deviceName}需要替换成设备名。

4.1.3 连接、订阅和发布消息
接着我们可以使用上面记录的连接参数进行连接,当连接成功后,订阅上面的订阅主题。并通过发布主题上报物模型数据。
在阿里云平台,如果产品创建阶段选择的数据格式为Alink JSON格式时,接收和发送数据格式都会遵守下面这个格式:
{
"method": "thing.event.property.post",
"id": "2241348",
"params": {
"prop_float": 1.25,
"prop_int16": 4658,
"prop_bool": 1
},
"version": "1.0"
}
5 主要程序解析
5.1 main.c分析
1.系统初始化与硬件配置
完成基础硬件初始化(时钟、延时、串口、定时器等),配置ADC(模数转换)用于传感器数据采集,初始化继电器控制模块,并设置WIZnet以太网芯片的网络参数(MAC、IP、网关等)。
2.网络通信功能 实现双重网络通信能力:
MQTT协议:通过MQTT客户端(do_mqtt()和mqtt_post_properties())实现传感器数据的远程发布。
获取网页:通过loopback_tcps()提供TCP服务器功能,进行HTTP请求和响应处理。
3.传感器数据处理与控制
周期性(5秒间隔)通过process_sensors_and_control()读取传感器数据(湿度、光照强度)。
基于湿度阈值(高低阈值)实现继电器自动控制逻辑。
将实时数据(湿度、光照、继电器状态)通过MQTT发布到阿里平台同时刷新网页环境数据。
#include "bsp_adc.h" #include "bsp_rcc.h" #include "bsp_tim.h" #include "bsp_uart.h" #include "delay.h" #include "do_mqtt.h" #include "do_sensor.h" #include "loopback.h" #include "sv.h" #include "wiz_interface.h" #include "wizchip_conf.h" #include < stdio.h > #include < stdlib.h > #include < string.h > /* 全局实时传感器数据(在do_mqtt.c中定义) */ extern float g_humidity_value; // 当前湿度读数 extern float g_light_intensity; // 当前光照强度读数 extern uint8_t g_solenoid_valve_state; // 电磁阀状态(1:开启, 0:关闭) /* 湿度阈值(与阿里云IoT模型对齐,范围0~100) */ int g_humidity_low_threshold = 30; // 浇水的下限阈值 int g_humidity_high_threshold = 50; // 停止浇水的上限阈值 /* 函数原型 */ extern void mqtt_post_properties(void); /* 套接字和缓冲区配置常量 */ #define SOCKET_TCP_ID 0 #define SOCKET_MQTT_ID 1 #define ETHERNET_BUF_MAX_SIZE (1024 * 2) // 以太网缓冲区最大大小 #define SENSOR_READ_INTERVAL 5000 // 传感器采样间隔(毫秒) uint16_t g_tcp_listen_port = 8080; // TCP服务器监听端口 wiz_NetInfo g_default_network_info = { // 默认网络配置 .mac = {0x00, 0x08, 0xdc, 0x12, 0x22, 0x12}, .ip = {192, 168, 1, 30}, .gw = {192, 168, 1, 1}, .sn = {255, 255, 255, 0}, .dns = {8, 8, 8, 8}, .dhcp = NETINFO_DHCP}; uint8_t g_ethernet_data_buf[ETHERNET_BUF_MAX_SIZE] = { 0}; // 以太网数据缓冲区 static uint8_t s_mqtt_send_buf[ETHERNET_BUF_MAX_SIZE] = { 0}; // MQTT发送缓冲区(静态) static uint8_t s_mqtt_recv_buf[ETHERNET_BUF_MAX_SIZE] = { 0}; // MQTT接收缓冲区(静态) /* 声明systick_count,名称与bsp_tim.o中的引用匹配 */ volatile uint32_t systick_count = 0; // 系统滴答计数器(毫秒)- bsp_tim使用的名称 /** * @brief 主程序循环 * @details 处理MQTT通信、TCP回环和传感器处理 */ int main(void) { rcc_clk_config(); // RCC时钟配置 delay_init(); // 延时初始化 console_usart_init(115200); // 控制台串口初始化,波特率115200 tim3_init(); // TIM3定时器初始化 printf("%s 传感器监控系统rn", _WIZCHIP_ID_); adc_dma_init(); // ADC DMA初始化 sv_init(); // 电磁阀初始化 wiz_toe_init(); // WIZ芯片TOE初始化 wiz_phy_link_check(); // WIZ物理层连接检查 network_init(g_ethernet_data_buf, &g_default_network_info); // 网络初始化 mqtt_init(SOCKET_MQTT_ID, s_mqtt_send_buf, s_mqtt_recv_buf); // MQTT初始化 printf("系统初始化完成。rn"); while (1) { do_mqtt(); // 处理MQTT通信 // 处理TCP回环 loopback_tcps(SOCKET_TCP_ID, g_ethernet_data_buf, g_tcp_listen_port); // 按间隔读取传感器并发布数据 if (systick_count >= SENSOR_READ_INTERVAL) { systick_count = 0; process_sensors_and_control(); // 处理传感器数据并控制设备 mqtt_post_properties(); // 通过MQTT发布传感器数据 } } }
5.2 ADC采集
1.关键配置:
初始化ADC为连续扫描模式,同时启用DMA传输。
配置GPIO为模拟输入模式,对应传感器连接的引脚。
设置DMA以循环方式将 ADC 数据传输到缓冲区,无需CPU干预。
2.数据处理:
采样数据交替存储在s_adc_dma_buffer缓冲区中。
提供adc_get_average_value()函数,可获取指定通道的平均采样值,减少噪声影响。
#include "bsp_adc.h"
#include "w55mh32_adc.h"
#include "w55mh32_dma.h"
#include "w55mh32_gpio.h"
#include "w55mh32_rcc.h"
/* ADC DMA数据静态缓冲区(存储交替的通道读数) */
static uint16_t s_adc_dma_buffer[ADC_BUFFER_SIZE];
/**
* @brief 初始化带DMA功能的ADC
* @details 配置ADC通道(湿度和光照)、GPIO和DMA以实现连续采样
*/
void adc_dma_init(void) {
ADC_InitTypeDef adc_init_struct; // ADC初始化结构体
GPIO_InitTypeDef gpio_init_struct; // GPIO初始化结构体
DMA_InitTypeDef dma_init_struct; // DMA初始化结构体
// 使能ADC、GPIO和DMA的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
// 配置GPIO引脚为模拟输入(PA0: 湿度, PA3: 光照)
gpio_init_struct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_3;
gpio_init_struct.GPIO_Mode = GPIO_Mode_AIN; // 模拟输入模式
GPIO_Init(GPIOA, &gpio_init_struct);
// 配置ADC为连续扫描模式
ADC_StructInit(&adc_init_struct); // 初始化ADC结构体为默认值
adc_init_struct.ADC_Mode = ADC_Mode_Independent; // 独立模式
adc_init_struct.ADC_ScanConvMode = ENABLE; // 使能扫描模式
adc_init_struct.ADC_ContinuousConvMode = ENABLE; // 使能连续转换模式
adc_init_struct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 无外部触发
adc_init_struct.ADC_DataAlign = ADC_DataAlign_Right; // 数据右对齐
adc_init_struct.ADC_NbrOfChannel = 2; // 2个通道(湿度+光照)
ADC_Init(ADC1, &adc_init_struct);
// 配置ADC通道和采样时间
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1,
ADC_SampleTime_55Cycles5); // 湿度通道(PA0)
ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 2,
ADC_SampleTime_55Cycles5); // 光照通道(PA3)
// 配置DMA用于ADC数据传输
DMA_DeInit(DMA1_Channel1); // 重置DMA通道1配置
dma_init_struct.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; // 外设基地址(ADC数据寄存器)
dma_init_struct.DMA_MemoryBaseAddr = (uint32_t)s_adc_dma_buffer; // 内存基地址(DMA缓冲区)
dma_init_struct.DMA_DIR = DMA_DIR_PeripheralSRC; // 传输方向:外设到内存
dma_init_struct.DMA_BufferSize = ADC_BUFFER_SIZE; // 缓冲区大小
dma_init_struct.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 禁止外设地址递增
dma_init_struct.DMA_MemoryInc = DMA_MemoryInc_Enable; // 使能内存地址递增
dma_init_struct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // 外设数据大小:半字(16位)
dma_init_struct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; // 内存数据大小:半字(16位)
dma_init_struct.DMA_Mode = DMA_Mode_Circular; // 循环模式
dma_init_struct.DMA_Priority = DMA_Priority_High; // 高优先级
dma_init_struct.DMA_M2M = DMA_M2M_Disable; // 禁止内存到内存传输
DMA_Init(DMA1_Channel1, &dma_init_struct);
// 使能DMA和ADC
DMA_Cmd(DMA1_Channel1, ENABLE); // 使能DMA通道1
ADC_DMACmd(ADC1, ENABLE); // 使能ADC的DMA请求
// 校准并启动ADC
ADC_Cmd(ADC1, ENABLE); // 使能ADC1
ADC_ResetCalibration(ADC1); // 重置校准寄存器
while (ADC_GetResetCalibrationStatus(ADC1)) // 等待重置校准完成
;
ADC_StartCalibration(ADC1); // 开始校准
while (ADC_GetCalibrationStatus(ADC1)) // 等待校准完成
;
ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 软件触发ADC转换
}
/**
* @brief 获取特定通道的ADC平均值
* @param channel_index ADC通道索引(0: 湿度, 1: 光照)
* @param sample_count 用于平均的样本数量
* @return 平均ADC值(12位)
*/
uint16_t adc_get_average_value(uint8_t channel_index, uint8_t sample_count) {
uint32_t sum = 0; // 样本总和
uint8_t valid_samples = 0; // 有效样本数
// 从DMA缓冲区读取交替的样本(通道0在偶数索引,通道1在奇数索引)
for (uint8_t i = 0; i < sample_count && i < ADC_BUFFER_SIZE / 2; i++) {
sum += s_adc_dma_buffer[i * 2 + channel_index]; // 累加对应通道的样本
valid_samples++; // 计数有效样本
}
return (valid_samples > 0) ? (sum / valid_samples) : 0; // 返回平均值(避免除零)
}
5.3 传感器数据读取
1.湿度传感器读取(humidity_read):
从ADC获取湿度通道的平均原始值。
将ADC值转换为电压(基于3.3V参考电压和12位ADC)。
通过传感器特定公式将电压转换为湿度值(0-100%)。
对结果进行范围限制,确保在有效区间内。
2.光照强度读取(light_read_intensity):
从 ADC 获取光照通道的平均原始值。
处理零值情况避免除零错误。
通过传感器公式将ADC值转换为光照强度(自定义单位)。
3.传感器处理与控制(process_sensors_and_control):
读取湿度和光照值并更新全局变量。
根据湿度阈值控制继电器:低于低阈值打开(浇水),高于高阈值关闭(停止浇水)。
打印当前状态信息(湿度、光照、阀门状态及阈值)。
#include "bsp_adc.h"
#include "do_sensor.h"
#include "sv.h"
#include < stdio.h >
extern float g_humidity_value;
extern float g_light_intensity;
extern uint8_t g_solenoid_valve_state;
extern int g_humidity_low_threshold;
extern int g_humidity_high_threshold;
/**
* @brief 从传感器读取湿度值
* @details 将ADC原始值转换为相对湿度(0~100%)
* @return 湿度值(0.0~100.0)
*/
float humidity_read(void) {
// 从湿度通道(索引0)读取ADC平均值
uint16_t adc_raw_value = adc_get_average_value(0, ADC_BUFFER_SIZE / 2);
// 将ADC值转换为电压(参考电压3.3V,12位ADC)
float voltage = adc_raw_value * 3.3f / 4096.0f;
// 将电压转换为湿度(传感器特定公式)
float humidity = (3.3f - voltage) * 30.3f;
// 将值限制在有效范围内(0~100%)
if (humidity < 0.0f) {
humidity = 0.0f;
}
if (humidity > 100.0f) {
humidity = 100.0f;
}
return humidity;
}
/**
* @brief 从传感器读取光照强度
* @details 将ADC原始值转换为光照强度(任意单位)
* @return 光照强度值
*/
float light_read_intensity(void) {
// 从光照通道(索引1)读取ADC平均值
uint16_t adc_raw_value = adc_get_average_value(1, ADC_BUFFER_SIZE / 2);
// 避免除零错误
if (adc_raw_value == 0) {
adc_raw_value = 1;
}
// 将ADC值转换为光照强度(传感器特定公式)
return 500000.0f / (float)adc_raw_value;
}
/**
* @brief 读取传感器并控制电磁阀
* @details 读取湿度和光照传感器,基于阈值控制阀门
*/
void process_sensors_and_control(void) {
g_humidity_value = humidity_read();
g_light_intensity = light_read_intensity();
// 基于湿度阈值控制电磁阀
if (g_humidity_value < g_humidity_low_threshold) {
g_solenoid_valve_state = 1;
sv_open(); // 打开阀门(开始浇水)
} else if (g_humidity_value > g_humidity_high_threshold) {
g_solenoid_valve_state = 0;
sv_close(); // 关闭阀门(停止浇水)
}
// 打印当前状态
printf("Humidity:%.1f Light:%.0f Valve:%s Low:%d High:%drn",
g_humidity_value, g_light_intensity,
g_solenoid_valve_state ? "ON" : "OFF", g_humidity_low_threshold,
g_humidity_high_threshold);
}
5.4 继电器控制浇灌
1.硬件定义与初始化:
定义继电器控制引脚为 GPIOB的Pin10。
sv_init() 函数负责初始化GPIO:
使能GPIOB时钟。
配置引脚为开漏输出模式(GPIO_Mode_Out_OD),速度50MHz。
初始状态设置为高电平(关闭继电器,利用开漏模式的内部上拉)。
2.核心控制函数:
sv_close():通过设置引脚为高电平关闭继电器,停止浇水,并打印状态信息。
sv_open():通过清除引脚为低电平打开继电器,开始浇水,并打印状态信息。
#include "bsp_uart.h"
#include "sv.h"
#include "w55mh32_rcc.h"
#include < stdio.h >
/* 电磁阀控制引脚 */
#define SOLENOID_VALVE_PIN GPIO_Pin_10
#define SOLENOID_VALVE_PORT GPIOB
/**
* @brief 初始化电磁阀(继电器)控制
* @details 配置GPIO引脚为开漏模式用于继电器控制
*/
void sv_init(void) {
GPIO_InitTypeDef gpio_init_struct;
// 使能GPIOB时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
// 配置引脚为开漏输出(继电器控制)
gpio_init_struct.GPIO_Mode = GPIO_Mode_Out_OD;
gpio_init_struct.GPIO_Speed = GPIO_Speed_50MHz;
gpio_init_struct.GPIO_Pin = SOLENOID_VALVE_PIN;
GPIO_Init(SOLENOID_VALVE_PORT, &gpio_init_struct);
// 初始状态:阀门关闭(引脚高电平,开漏模式配合内部上拉)
GPIO_SetBits(SOLENOID_VALVE_PORT, SOLENOID_VALVE_PIN);
}
/**
* @brief 关闭继电器(停止浇水)
* @details 设置继电器控制引脚为高电平,使电磁阀失活
*/
void sv_close(void) {
GPIO_SetBits(SOLENOID_VALVE_PORT, SOLENOID_VALVE_PIN);
printf("Solenoid valve closed (high humidity).rn");
}
/**
* @brief 打开继电器(开始浇水)
* @details 清除继电器控制引脚为低电平,使电磁阀激活
*/
void sv_open(void) {
GPIO_ResetBits(SOLENOID_VALVE_PORT, SOLENOID_VALVE_PIN);
printf("Solenoid valve opened (low humidity).rn");
}
5.5 连接阿里云
1.功能定位:通过 MQTT 协议实现设备与阿里云 IoT 平台的双向通信,完成传感器数据上报与云端控制指令接收。
2.核心配置:包含阿里云MQTT 服务器地址、端口、认证信息(客户端ID、用户名、密码)及收发主题,采用QoS0等级通信。
3.关键流程:
初始化:解析域名、建立网络连接、配置MQTT客户端参数。
通信机制:通过状态机管理连接、订阅、消息收发及错误重连。
数据交互:将传感器数据(湿度、光照、阀门状态)打包为JSON上报;解析云端上下发的JSON指令,控制阀门门开关及更新湿度阈值。
4.与系统集成:关联传感器数据全局变量,调用继电器控制函数,实现本地设备状态与云端的同步。
需要注意:mqttconn s_mqtt_connection_params函数中参数要修改为4.1.2中创建设备的MQTT参数和订阅发布主题。
#include "MQTTClient.h"
#include "cJSON.h"
#include "delay.h"
#include "do_dns.h"
#include "do_mqtt.h"
#include "mqtt_interface.h"
#include "sv.h"
#include "wiz_interface.h"
#include "wizchip_conf.h"
#include < string.h >
#define MQTT_ETHERNET_MAX_SIZE (1024 * 2) // MQTT缓冲区最大大小
/* MQTT控制句柄 */
MQTTClient g_mqtt_client = {0};
Network g_mqtt_network = {0};
int g_mqtt_conn_status;
static uint8_t s_mqtt_run_status = CONN; // MQTT状态机状态
/* MQTT连接参数(替换为你的设备凭证) */
static mqttconn s_mqtt_connection_params = {
.mqttHostUrl = "iot-06z009vm5y6jfwj.mqtt.iothub.aliyuncs.com",
.server_ip =
{
0,
},
.port = 1883,
.clientid = "k1zh33h3hte.IGAT|securemode=2,signmethod=hmacsha256,timestamp="
"1752635274022|",
.username = "IGAT&k1zh33h3hte",
.passwd =
"f04b7d14d10a981e0eb6248da38b52060ff443c3f4b825d01594dfaa7e5720c1",
.pubtopic = "/sys/k1zh33h3hte/IGAT/thing/event/property/post",
.subtopic = "/sys/k1zh33h3hte/IGAT/thing/service/property/set",
.pubQoS = QOS0,
};
/* MQTT接收缓冲区(静态) */
static char s_mqtt_received_msg[512] = {0};
static uint8_t s_mqtt_receive_flag = 0; // 新MQTT消息标志
/* MQTT消息/配置结构 */
MQTTMessage g_mqtt_pub_msg = {.qos = QOS0, .retained = 0, .dup = 0, .id = 0};
MQTTPacket_willOptions g_mqtt_will = MQTTPacket_willOptions_initializer;
MQTTPacket_connectData g_mqtt_conn_data = MQTTPacket_connectData_initializer;
/* 全局传感器数据(与main.c共享) */
float g_humidity_value = 0.0f; // 当前湿度
float g_light_intensity = 0.0f; // 当前光照强度
uint8_t g_solenoid_valve_state = 0; // 电磁阀状态(1:开启, 0:关闭)
/* 湿度阈值(与main.c共享) */
extern int g_humidity_low_threshold;
extern int g_humidity_high_threshold;
/**
* @brief 初始化MQTT客户端
* @param sn 套接字号
* @param send_buf MQTT发送数据缓冲区
* @param recv_buf MQTT接收数据缓冲区
*/
void mqtt_init(uint8_t sn, uint8_t *send_buf, uint8_t *recv_buf) {
wiz_NetInfo network_info = {0};
wizchip_getnetinfo(&network_info);
// 将MQTT服务器域名解析为IP地址
if (do_dns(send_buf, (uint8_t *)s_mqtt_connection_params.mqttHostUrl,
s_mqtt_connection_params.server_ip)) {
while (1)
; // DNS解析失败时停机
}
// 初始化网络和MQTT客户端
NewNetwork(&g_mqtt_network, sn);
ConnectNetwork(&g_mqtt_network, s_mqtt_connection_params.server_ip,
s_mqtt_connection_params.port);
MQTTClientInit(&g_mqtt_client, &g_mqtt_network, 1000, send_buf,
MQTT_ETHERNET_MAX_SIZE, recv_buf, MQTT_ETHERNET_MAX_SIZE);
// 配置MQTT连接参数
g_mqtt_conn_data.willFlag = 0;
g_mqtt_conn_data.MQTTVersion = 4;
g_mqtt_conn_data.clientID.cstring = s_mqtt_connection_params.clientid;
g_mqtt_conn_data.username.cstring = s_mqtt_connection_params.username;
g_mqtt_conn_data.password.cstring = s_mqtt_connection_params.passwd;
g_mqtt_conn_data.keepAliveInterval = 30;
g_mqtt_conn_data.cleansession = 1;
}
/**
* @brief 解码MQTT JSON消息(设置阈值/阀门状态)
* @param msg JSON消息负载
*/
void mqtt_json_decode(char *msg) {
cJSON *root_json = cJSON_Parse(msg);
if (!root_json) {
printf("MQTT JSON parse failedrn");
return;
}
// 从JSON中提取"params"对象
cJSON *params_json = cJSON_GetObjectItem(root_json, "params");
if (!params_json) {
cJSON_Delete(root_json);
return;
}
// 如果"Elect"字段存在,更新电磁阀状态
cJSON *valve_json = cJSON_GetObjectItem(params_json, "Elect");
if (valve_json) {
g_solenoid_valve_state = valve_json->valueint;
if (g_solenoid_valve_state) {
sv_open();
} else {
sv_close();
}
}
// 如果"Low"/"High"字段存在,更新湿度阈值
cJSON *low_threshold_json = cJSON_GetObjectItem(params_json, "Low");
cJSON *high_threshold_json = cJSON_GetObjectItem(params_json, "High");
if (low_threshold_json) {
g_humidity_low_threshold = low_threshold_json->valueint;
}
if (high_threshold_json) {
g_humidity_high_threshold = high_threshold_json->valueint;
}
// 确保阈值范围有效(下限 <= 上限)
if (g_humidity_low_threshold > g_humidity_high_threshold) {
g_humidity_low_threshold = g_humidity_high_threshold - 1;
}
cJSON_Delete(root_json); // 释放JSON对象
}
/**
* @brief 接收MQTT消息的回调函数
* @param md 消息数据(主题和负载)
*/
void mqtt_message_arrived(MessageData *md) {
char topic_name[64] = {0};
char msg_payload[512] = {0};
// 提取主题和负载
sprintf(topic_name, "%.*s", (int)md->topicName->lenstring.len,
md->topicName->lenstring.data);
sprintf(msg_payload, "%.*s", (int)md->message->payloadlen,
(char *)md->message->payload);
printf("MQTT recv: %s, %srnrn", topic_name, msg_payload);
// 存储消息并设置标志
s_mqtt_receive_flag = 1;
memset(s_mqtt_received_msg, 0, sizeof(s_mqtt_received_msg));
memcpy(s_mqtt_received_msg, msg_payload, strlen(msg_payload));
}
/**
* @brief 通过MQTT发布传感器数据
* @details 以JSON格式发送湿度、光照、阀门状态和阈值
*/
void mqtt_post_properties(void) {
char json_payload[256] = {0};
// 将传感器数据格式化为JSON负载
int payload_len =
snprintf(json_payload, sizeof(json_payload),
"{"id":"123","version":"1.0","params":{"
""Elect":%d,"
""Humidity":%.1f,"
""light":%.0f,"
""Low":%d,"
""High":%d"
"},"method":"thing.event.property.post"}",
g_solenoid_valve_state, g_humidity_value, g_light_intensity,
g_humidity_low_threshold, g_humidity_high_threshold);
// 发布消息
g_mqtt_pub_msg.payload = json_payload;
g_mqtt_pub_msg.payloadlen = payload_len;
MQTTPublish(&g_mqtt_client, s_mqtt_connection_params.pubtopic,
&g_mqtt_pub_msg);
printf("MQTT published: %s, %srnrn", s_mqtt_connection_params.pubtopic,
json_payload);
}
/**
* @brief MQTT状态机(处理连接、订阅和消息)
*/
void do_mqtt(void) {
uint8_t ret;
switch (s_mqtt_run_status) {
case CONN: // 连接到MQTT服务器
ret = MQTTConnect(&g_mqtt_client, &g_mqtt_conn_data);
printf("Connecting to MQTT server: %d.%d.%d.%d:%drn",
s_mqtt_connection_params.server_ip[0],
s_mqtt_connection_params.server_ip[1],
s_mqtt_connection_params.server_ip[2],
s_mqtt_connection_params.server_ip[3],
s_mqtt_connection_params.port);
printf("Connection %srnrn", ret == SUCCESSS ? "success" : "failed");
s_mqtt_run_status = (ret == SUCCESSS) ? SUB : ERR;
break;
case SUB: // 订阅主题
ret = MQTTSubscribe(&g_mqtt_client, s_mqtt_connection_params.subtopic,
s_mqtt_connection_params.pubQoS, mqtt_message_arrived);
printf("Subscribing to %srn", s_mqtt_connection_params.subtopic);
printf("Subscription %srnrn", ret == SUCCESSS ? "success" : "failed");
s_mqtt_run_status = (ret == SUCCESSS) ? KEEPALIVE : ERR;
break;
case KEEPALIVE: // 维持连接并检查消息
if (MQTTYield(&g_mqtt_client, 30) != SUCCESSS) {
s_mqtt_run_status = ERR;
break;
}
// 继续处理接收的消息
case RECV: // 处理接收的消息
if (s_mqtt_receive_flag) {
s_mqtt_receive_flag = 0;
mqtt_json_decode(s_mqtt_received_msg); // 解码并处理消息
}
break;
case ERR: // 处理错误(重试)
printf("MQTT error! Reconnecting...rn");
delay_ms(1000);
s_mqtt_run_status = CONN; // 重试连接
break;
default:
break;
}
}
5.6 网页控制
1.功能定位:提供Web服务接口,支持通过HTTP协议获取设备状态和控制参数。
2.核心功能:
解析HTTP 请求(支持 GET、POST方法)。
提供多个API端点:
根路径/:返回网页内容。
/api/sensor:以JSON格式返回传感器数据(湿度、光照、阀门状态、阈值)。
/api/threshold:接收POST请求更新湿度阈值。
3.数据交互:
读取全局变量获取传感器状态和阈值。
通过HTTP响应返回JSON格式数据。
解析POST请求中的JSON数据更新系统参数。
4.通信管理:
基于TCP状态机管理连接生命周期(建立、数据传输、关闭)。
处理不同HTTP状态码(200、204、400、404)。
#include "cJSON.h"
#include "loopback.h"
#include "socket.h"
#include "web_page.h"
#include "wizchip_conf.h"
#include < stdio.h >
#include < stdlib.h >
#include < string.h >
/* 使用web_page.h中定义的HTTP_RESPONSE_404 */
#define DATA_BUF_SIZE 1024
#define STR(x) #x // 用于行号的字符串化宏
/* 全局传感器数据(与main.c共享) */
extern float g_humidity_value;
extern float g_light_intensity;
extern uint8_t g_solenoid_valve_state;
extern int g_humidity_low_threshold;
extern int g_humidity_high_threshold;
/**
* @brief HTTP请求行结构(方法、URI、版本)
*/
typedef struct {
char method[16]; // 例如:"GET"
char uri[256]; // 例如:"/api/sensor"
char version[16]; // 例如:"HTTP/1.1"
} HttpReqLine;
/**
* @brief 从原始数据解析HTTP请求行
* @param request 原始HTTP请求数据
* @param req_line 存储解析结果的输出结构
* @return 成功返回0,失败返回-1
*/
static int http_parse_request_line(const char *request, HttpReqLine *req_line) {
char buffer[1024];
strncpy(buffer, request, sizeof(buffer));
buffer[sizeof(buffer) - 1] = ''; // 确保字符串以空字符结尾
// 查找第一行的结束符("rn"或"n")
char *line_end = strstr(buffer, "rn");
if (!line_end) {
line_end = strstr(buffer, "n"); // 兼容Unix换行符
}
if (!line_end) {
return -1; // 格式无效
}
*line_end = ''; // 截断到第一行
// 使用空格分割为方法、URI和版本
char *method = strtok(buffer, " ");
char *uri = strtok(NULL, " ");
char *version = strtok(NULL, " ");
if (!method || !uri || !version) {
return -1; // 请求行不完整
}
// 将解析的值复制到结构中
strncpy(req_line->method, method, sizeof(req_line->method) - 1);
strncpy(req_line->uri, uri, sizeof(req_line->uri) - 1);
strncpy(req_line->version, version, sizeof(req_line->version) - 1);
return 0;
}
/**
* @brief 发送带有CORS头的HTTP响应
* @param sn 套接字号
* @param status HTTP状态码(例如:"200 OK")
* @param content_type MIME类型(例如:"application/json")
* @param body 响应体内容
*/
static void send_http_response(uint8_t sn, const char *status,
const char *content_type, const char *body) {
char response[512];
int body_len = strlen(body);
// 构建带有CORS头的响应
sprintf(response,
"HTTP/1.1 %srn"
"Content-Type: %srn"
"Access-Control-Allow-Origin: *rn"
"Access-Control-Allow-Methods: GET, POST, OPTIONSrn"
"Access-Control-Allow-Headers: Content-Typern"
"Connection: closern"
"Content-Length: %drn"
"rn"
"%s",
status, content_type, body_len, body);
// 发送完整响应
send(sn, (uint8_t *)response, strlen(response));
}
/**
* @brief 带HTTP支持的TCP服务器回环测试
* @param sn 套接字号
* @param network_buf 网络数据缓冲区
* @param port 监听端口
* @return 成功返回1,失败返回错误码
*/
int32_t loopback_tcps(uint8_t sn, uint8_t *network_buf, uint16_t port) {
int32_t ret;
uint16_t recv_size = 0;
uint16_t send_size = 0;
switch (getSn_SR(sn)) {
case SOCK_ESTABLISHED: // 连接已建立
if (getSn_IR(sn) & Sn_IR_CON) {
setSn_IR(sn, Sn_IR_CON); // 清除连接中断标志
}
// 检查接收数据
if ((recv_size = getSn_RX_RSR(sn)) > 0) {
if (recv_size > DATA_BUF_SIZE) {
recv_size = DATA_BUF_SIZE; // 限制为缓冲区大小
}
// 读取接收的数据
ret = recv(sn, network_buf, recv_size);
if (ret <= 0) {
return ret; // 处理错误
}
recv_size = (uint16_t)ret;
network_buf[recv_size] = ''; // 添加空终止符
// 解析HTTP请求行
HttpReqLine req_line;
if (http_parse_request_line((char *)network_buf, &req_line) == 0) {
printf("HTTP Method: %s, URI: %sn", req_line.method, req_line.uri);
// 处理OPTIONS请求(CORS预检)
if (strcmp(req_line.method, "OPTIONS") == 0) {
send_http_response(sn, "204 No Content", "text/plain", "");
disconnect(sn);
close(sn);
}
// 处理GET /(提供网页)
else if (strcmp(req_line.method, "GET") == 0 &&
strcmp(req_line.uri, "/") == 0) {
uint16_t content_len = strlen(index_page);
send_size = 0;
while (strlen(index_page) != send_size) {
ret = send(sn, (uint8_t *)index_page + send_size,
strlen(index_page) - send_size);
if (ret < 0) {
close(sn);
return ret;
}
send_size += ret;
}
disconnect(sn);
close(sn);
}
// 处理GET /api/sensor(返回传感器数据)
else if (strcmp(req_line.method, "GET") == 0 &&
strcmp(req_line.uri, "/api/sensor") == 0) {
char sensor_json[128];
sprintf(sensor_json,
"{"humi":%.1f,"light":%.0f,"sv":"%s","low":%d,"
""high":%d}",
g_humidity_value, g_light_intensity,
g_solenoid_valve_state ? "ON" : "OFF",
g_humidity_low_threshold, g_humidity_high_threshold);
send_http_response(sn, "200 OK", "application/json", sensor_json);
disconnect(sn);
close(sn);
}
// 处理POST /api/threshold(更新阈值)
else if (strcmp(req_line.method, "POST") == 0 &&
strcmp(req_line.uri, "/api/threshold") == 0) {
// 查找请求体(健壮解析)
char *body_start = NULL;
char *header_end = strstr((char *)network_buf, "rnrn");
if (header_end) {
body_start = header_end + 4; // 跳过"rnrn"
} else {
// 兼容Unix换行符
header_end = strstr((char *)network_buf, "nn");
if (header_end) {
body_start = header_end + 2; // 跳过"nn"
}
}
// 验证请求体存在
if (!body_start || body_start >= (char *)network_buf + recv_size) {
send_http_response(
sn, "400 Bad Request", "application/json",
"{"success":false, "error":"Missing request body"}");
disconnect(sn);
close(sn);
return 1;
}
// 解析JSON请求体
cJSON *root_json = cJSON_Parse(body_start);
if (!root_json) {
send_http_response(
sn, "400 Bad Request", "application/json",
"{"success":false, "error":"Invalid JSON format"}");
disconnect(sn);
close(sn);
return 1;
}
// 提取并验证阈值
cJSON *low_json = cJSON_GetObjectItem(root_json, "low");
cJSON *high_json = cJSON_GetObjectItem(root_json, "high");
if (cJSON_IsNumber(low_json) && cJSON_IsNumber(high_json)) {
int new_low = low_json->valueint;
int new_high = high_json->valueint;
// 验证范围
if (new_low < 0 || new_high > 100 || new_low >= new_high) {
send_http_response(sn, "400 Bad Request", "application/json",
"{"success":false, "error":"Invalid "
"range (0-100, low < high)"}");
} else {
// 更新阈值
g_humidity_low_threshold = new_low;
g_humidity_high_threshold = new_high;
printf("Thresholds updated: Low=%d, High=%dn", new_low,
new_high);
send_http_response(sn, "200 OK", "application/json",
"{"success":true}");
}
} else {
send_http_response(sn, "400 Bad Request", "application/json",
"{"success":false, "error":"Missing 'low' "
"or 'high' parameters"}");
}
cJSON_Delete(root_json); // 释放JSON对象
disconnect(sn);
close(sn);
}
// 处理未知请求
else {
send_http_response(sn, "404 Not Found", "text/html",
"< html >
Page Not Found< /body >< /html >"); disconnect(sn); close(sn); } } else { printf("Failed to parse HTTP requestn"); send_http_response( sn, "400 Bad Request", "application/json", "{"success":false, "error":"Invalid request format"}"); disconnect(sn); close(sn); } } break; case SOCK_CLOSE_WAIT: // 关闭等待状态 if ((ret = disconnect(sn)) != SOCK_OK) { return ret; } break; case SOCK_INIT: // 套接字已初始化,开始监听 if ((ret = listen(sn)) != SOCK_OK) { return ret; } break; case SOCK_CLOSED: // 套接字已关闭,重新初始化 if ((ret = socket(sn, Sn_MR_TCP, port, 0x00)) != sn) { return ret; } break; default: break; } return 1; }
6 功能验证
程序烧录完毕,硬件连接完成如下图所示:

硬件连接完毕,上电通过串口助手打印如下信息:

6.1 同步阿里云
通过串口每5S采集一次数据发送到云平台,当我们把光照传感器逐渐靠近光源,光照值越来越大。此时土壤湿度传感器未插入土壤,在空气中检测湿度小于30%RH,继电器打开,开始浇灌。


当我们把土壤湿度传感器插入盆栽中,可以看到当湿度小于30%RH时自动开始灌溉,当湿度大于50%RH时停止灌溉。


当湿度小于30%RH时继电器打开,开始浇灌,当湿度大于50%RH时继电器关闭停止灌溉。

服务器下发指令控制继电器开关,进而实现对浇灌启停的控制。

6.2 网页控制
通过网页平台不仅能实时监测农田环境数据,更可远程灵活设定调控阈值,让田间管理实现精准化、智能化调控。

7总结
该系统以W55MH32L-EVB为核心,通过MQTT连阿里云,采集土壤湿度、光照数据,实现水泵智能控制。支持自动与远程调控,5秒采集一次,同时支持网页修改湿度阈值,功能稳定,为智能灌溉提供有效方案。感谢大家的耐心阅读!如果您在搭建过程中遇到硬件接线、阿里云配置或代码调试问题,欢迎在评论区留言交流~ 觉得有帮助的话也请点赞收藏,您的支持是我分享的动力!
审核编辑 黄宇
-
单片机
+关注
关注
6074文章
45341浏览量
663700 -
阿里云
+关注
关注
3文章
1028浏览量
45506 -
MQTT协议
+关注
关注
0文章
103浏览量
6425 -
智能农业
+关注
关注
0文章
134浏览量
8792
发布评论请先 登录
基于阿里云MQTT物联网平台视频监控(下)

智能农业监控系统:MQTT阿里云平台监测+内置Web网页控制+代码解析
评论