STM32+微信小程序+Onenet三件套:从零搭建物联网监控系统(附完整代码)
STM32+微信小程序+Onenet:构建你的首个物联网监控系统全栈指南
最近几年,身边做硬件开发的朋友,聊天的主题总绕不开“物联网”。从家里的智能插座到工厂里的设备监控,似乎一切都能连上网。但很多刚入门的朋友,尤其是学生和嵌入式爱好者,常常卡在“如何把单片机采集的数据,安全、稳定地展示在手机端”这个环节。硬件调通了,云平台注册了,小程序界面也画好了,可数据就是传不过去,或者指令下不来,调试过程堪比“玄学”。
如果你也遇到过类似困扰,这篇文章或许能给你一条清晰的路径。我们不谈空洞的理论,直接从手头的STM32开发板、ESP8266模块、Onenet云平台和微信开发者工具出发,一步步搭建一个能上传环境数据(比如温湿度)、并能从小程序远程下发控制指令的完整监控系统。我会把过程中最容易“踩坑”的地方、参数配置的细节,以及我调试时用到的“土办法”都分享出来,目标是让你看完就能动手做出来。
1. 系统架构与核心组件选型
在动手写代码和接线之前,我们得先搞清楚整个系统是怎么跑起来的。一个典型的物联网监控系统,可以抽象为三层:设备层、云平台层和应用层。我们的项目正好对应这三个部分。
- 设备层:以STM32微控制器为核心,它负责采集物理世界的数据(通过传感器),并执行具体的控制动作(如点亮LED、驱动继电器)。STM32本身通常不具备直接连接互联网的能力,因此需要搭配一个网络模块,我们选择最常见且性价比极高的ESP8266 WiFi模块。
- 云平台层:这是连接设备和应用的“中间人”和“数据中转站”。我们选用中国移动Onenet平台。它的角色是:接收STM32上传的数据并存储;接收来自小程序的控制指令,并转发给STM32。它解决了设备与应用之间直接通信的诸多难题,如IP地址动态变化、网络协议差异、数据持久化存储等。
- 应用层:面向最终用户的交互界面。我们使用微信小程序,因为它无需安装、跨平台、开发门槛相对较低。小程序通过调用Onenet提供的API,实现数据的可视化展示和指令的下发。
这三者之间的数据流向是一个闭环:
- 数据上传流:传感器 -> STM32 -> (串口) -> ESP8266 -> (WiFi/HTTP/MQTT) -> Onenet云端。
- 指令下发流:微信小程序 -> (HTTP) -> Onenet云端 -> (推送到ESP8266) -> (串口) -> STM32 -> 执行机构。
选择STM32F103C8T6(即常说的“蓝桥杯”或“最小系统板”)和ESP-01S模块作为硬件核心,主要是基于其极高的普及率和丰富的社区资源,几乎你遇到的所有问题,网上都能找到讨论。
2. 硬件连接与STM32基础工程搭建
硬件是系统的骨架,连接错误会导致后续所有软件调试功亏一篑。我们先确保物理链路正确无误。
2.1 STM32与ESP8266的接线
ESP8266模块(以ESP-01S为例)通常使用3.3V供电,并通过串口(UART)与STM32通信。切记,ESP8266的IO口电平是3.3V,切勿接到STM32的5V引脚上,否则极易烧毁模块。
下面是一个可靠的接线对照表:
| STM32F103C8T6引脚 | ESP-01S引脚 | 功能说明 | 注意事项 |
|---|---|---|---|
| 3.3V | VCC | 电源正极 | 确保电源容量足够,可单独使用一个3.3V LDO供电 |
| GND | GND | 电源地 | 共地至关重要 |
| PA9 (TX) | RX | STM32发送,ESP8266接收 | 交叉连接 |
| PA10 (RX) | TX | STM32接收,ESP8266发送 | 交叉连接 |
| 3.3V (或任一GPIO) | CH_PD (或 EN) | 使能引脚,高电平有效 | 接3.3V常启 |
| GND (或任一GPIO) | GPIO0 | 工作模式选择 | 下载固件时接GND,正常运行时悬空或接3.3V |
提示:为简化,我们让ESP8266一上电就进入正常工作模式(GPIO0悬空)。如果后续需要更新AT固件,才需要将GPIO0接GND后重启。
接线完成后,建议先使用USB转TTL模块,配合串口调试助手(如XCOM、SSCOM)单独测试ESP8266模块是否能正常响应AT指令,确保模块本身是好的。
2.2 创建STM32工程与驱动准备
在Keil MDK或STM32CubeIDE中新建一个工程。我们需要的核心驱动包括:
- USART驱动:用于与ESP8266通信。配置一个USART(如USART1),波特率设置为115200(与ESP8266默认AT指令波特率一致)。
- 定时器驱动:用于实现非阻塞式的延时和网络心跳包发送。推荐使用一个基本定时器(如TIM2)产生1ms中断。
- GPIO驱动:用于控制LED等指示设备状态,或连接传感器(如DHT11温湿度传感器需要一根数据线)。
一个健壮的工程应该将代码模块化。我建议的文件夹结构如下:
Project/
├── Core/ (MCU核心启动文件等)
├── Drivers/ (HAL库或标准外设库)
├── User/
│ ├── main.c
│ ├── esp8266.c/.h // ESP8266通信与控制模块
│ ├── onenet.c/.h // Onenet协议封装模块
│ ├── sensor.c/.h // 传感器数据采集模块
│ ├── timer.c/.h // 软件定时器管理
│ └── protocol.c/.h // 自定义数据解析协议
└── ...
在main.c中,我们的主循环逻辑会非常清晰:
int main(void) {
// 硬件初始化
HAL_Init();
SystemClock_Config();
USART1_Init(115200); // 与ESP8266通信的串口
TIM2_Init(); // 1ms定时器
LED_GPIO_Init();
Sensor_Init();
// 模块初始化
ESP8266_Init();
Onenet_Init();
while (1) {
// 1. 处理ESP8266接收到的数据
ESP8266_DataProcess();
// 2. 处理Onenet相关逻辑(如心跳、指令解析)
Onenet_Process();
// 3. 定时采集并上传传感器数据
if (timer_flag.data_upload == 1) {
timer_flag.data_upload = 0;
float temp = Read_Temperature();
float humi = Read_Humidity();
Onenet_UploadData("temperature", temp);
Onenet_UploadData("humidity", humi);
}
// 4. 其他应用任务...
LED_Blink(); // 用LED闪烁指示系统运行状态
}
}
3. ESP8266联网与Onenet设备接入
这是从设备到云端的关键一步,也是问题高发区。我们将它分解为几个明确的阶段。
3.1 ESP8266的AT指令控制策略
STM32通过串口向ESP8266发送AT指令来控制其行为。必须实现一个稳定、容错的AT指令交互机制。核心函数ESP8266_SendCmd需要包含等待响应和超时判断。
/**
* @brief 向ESP8266发送指令并等待指定响应
* @param cmd: 发送的AT指令
* @param ack: 期望收到的应答关键字
* @param timeout: 超时时间(毫秒)
* @retval 0:成功, -1:失败(超时或应答错误)
*/
int8_t ESP8266_SendCmd(char *cmd, char *ack, uint16_t timeout) {
char recv_buffer[256];
uint16_t time_cnt = 0;
uart_rx_count = 0; // 清空接收缓冲区
// 发送指令
printf("Send: %s\r\n", cmd); // 调试信息
USART_SendString(USART1, cmd);
// 等待响应
while(time_cnt < timeout) {
HAL_Delay(10);
time_cnt += 10;
if(uart_rx_count > 0) { // 有数据收到
recv_buffer[uart_rx_count] = '\0';
if(strstr(recv_buffer, ack) != NULL) {
printf("Ack: %s\r\n", recv_buffer); // 调试信息
return 0; // 找到期望应答
}
// 如果收到"ERROR",也认为是明确失败
if(strstr(recv_buffer, "ERROR") != NULL) {
printf("Error: %s\r\n", recv_buffer);
return -1;
}
}
}
printf("Timeout waiting for: %s\r\n", ack);
return -1; // 超时
}
联网流程可以封装为一个函数,按顺序执行:
- 测试AT启动:发送
AT\r\n,期待回复OK。 - 设置工作模式:发送
AT+CWMODE=1\r\n(设置为Station模式,即客户端模式),期待OK。 - 连接WiFi:发送
AT+CWJAP="你的WiFi名","你的WiFi密码"\r\n,这个过程可能需要几秒到十几秒,等待WIFI CONNECTED和WIFI GOT IP,最后是OK。 - 查询IP地址(可选):发送
AT+CIFSR\r\n,确认已获取到IP。
注意:WiFi名称和密码最好定义为宏或存储在外部Flash中,方便修改。如果密码包含特殊字符,可能需要做转义处理。
3.2 在Onenet创建产品与设备
登录Onenet控制台,进行以下操作:
- 创建产品:进入“产品开发”->“创建产品”。选择“设备接入方式”为“公开协议”,协议选择“HTTP”或“MQTT”。对于初学者,HTTP协议更简单直观,适合本教程。产品名称自定,如“环境监控仪”。
- 添加设备:在产品详情页,点击“添加设备”。填写设备名称(如“Lab_Sensor_01”)和鉴权信息(auth_info,这是一个重要的密钥,请妥善保存)。成功后会得到设备的ID。
- 记录关键信息:你需要记录下三个核心参数,并填入STM32代码中:
PRODUCT_ID(产品ID)DEVICE_ID(设备ID)AUTH_INFO(设备鉴权信息)
在STM32代码中,通常会在onenet.c文件的开头定义这些常量:
// onenet.c
#define ONENET_PRODUCT_ID "123456" // 替换为你的产品ID
#define ONENET_DEVICE_ID "dev_001" // 替换为你的设备ID
#define ONENET_AUTH_INFO "auth_key" // 替换为你的鉴权信息
#define ONENET_API_KEY "api_key" // 在“API-KEY”页面生成,用于小程序调用API
3.3 实现HTTP数据流上传
Onenet的HTTP API非常清晰。上传一个数据点(例如温度)本质上就是向一个特定的URL发送一个HTTP POST请求。
构建HTTP POST请求包是重点。我们需要在STM32端用字符串拼接出完整的HTTP报文。以下是一个示例函数,用于上传一个浮点数类型的数据流:
/**
* @brief 向Onenet上传一个数据点
* @param stream_name: 数据流名称,如"temperature"
* @param value: 数据值
* @retval 0:成功, -1:失败
*/
int8_t Onenet_UploadDataPoint(char *stream_name, float value) {
char http_packet[512];
char data_body[128];
// 1. 构建JSON格式的数据体
sprintf(data_body, "{\"datastreams\":[{\"id\":\"%s\",\"datapoints\":[{\"value\":%.2f}]}]}", stream_name, value);
// 2. 计算数据体长度
int body_len = strlen(data_body);
// 3. 构建完整的HTTP请求报文
sprintf(http_packet,
"POST /devices/%s/datapoints?type=3 HTTP/1.1\r\n"
"api-key: %s\r\n"
"Host: api.heclouds.com\r\n"
"Content-Length: %d\r\n"
"Connection: close\r\n"
"\r\n" // 空行分隔头部和主体
"%s", // 数据主体
ONENET_DEVICE_ID,
ONENET_API_KEY, // 注意:这里是API-KEY,用于鉴权
body_len,
data_body
);
// 4. 通过ESP8266发送这个HTTP请求包
// 先建立TCP连接到 api.heclouds.com:80
char tcp_cmd[128];
sprintf(tcp_cmd, "AT+CIPSTART=\"TCP\",\"api.heclouds.com\",80\r\n");
if(ESP8266_SendCmd(tcp_cmd, "CONNECT", 5000) != 0) {
return -1; // TCP连接失败
}
// 发送数据长度
sprintf(tcp_cmd, "AT+CIPSEND=%d\r\n", strlen(http_packet));
if(ESP8266_SendCmd(tcp_cmd, ">", 2000) != 0) {
return -1;
}
// 发送HTTP数据包
if(ESP8266_SendCmd(http_packet, "CLOSED", 10000) != 0) { // 发送后等待服务器关闭连接
// 这里可以更精细地解析返回的HTTP响应,判断是否上传成功(响应码200)
// 简单处理,只要没出错,认为成功
}
return 0;
}
上传成功后,你可以在Onenet控制台的“设备管理”->“数据流展示”中看到刚刚上传的数据点,并以图表形式展示。
4. 微信小程序开发与数据交互
小程序作为用户界面,主要做两件事:从Onenet获取最新数据显示,以及向Onenet发送指令。
4.1 小程序项目初始化与配置
- 注册小程序:在微信公众平台注册小程序账号,获得唯一的
AppID。 - 安装开发者工具:下载并安装微信开发者工具,使用
AppID创建一个新的小程序项目。 - 配置服务器域名:在小程序管理后台的“开发”->“开发设置”->“服务器域名”中,将Onenet的API域名加入
request合法域名列表:https://api.heclouds.com。这一步必须做,否则无法发起网络请求。
小程序的代码结构主要关注以下几个文件:
app.json: 全局配置,包括页面路径、窗口样式等。project.config.json: 项目配置,包含你的AppID。pages/index/index.js: 首页的逻辑层。pages/index/index.wxml: 首页的视图层(结构)。pages/index/index.wxss: 首页的样式层。
4.2 实现数据获取与展示
在小程序的首页(index.js)中,我们会在onLoad生命周期函数中加载数据。使用微信小程序的wx.request API向Onenet发起HTTP GET请求,获取设备的最新数据。
// index.js
Page({
data: {
temperature: '--',
humidity: '--',
lastUpdate: '未更新'
},
onLoad: function(options) {
this.fetchDeviceData();
// 可以设置定时器,每10秒自动刷新一次
setInterval(() => {
this.fetchDeviceData();
}, 10000);
},
fetchDeviceData: function() {
const that = this;
// 替换为你的设备ID和API-KEY
const deviceId = 'your_device_id';
const apiKey = 'your_api_key';
const url = `https://api.heclouds.com/devices/${deviceId}/datastreams`;
wx.request({
url: url,
header: {
'api-key': apiKey // 关键:在Header中传递API-KEY
},
success(res) {
if (res.statusCode === 200 && res.data.errno === 0) {
const data = res.data.data;
let temp = '--';
let humi = '--';
// 遍历数据流,找到温度和湿度
data.forEach(stream => {
if (stream.id === 'temperature' && stream.datapoints.length > 0) {
temp = stream.datapoints[0].value.toFixed(1);
}
if (stream.id === 'humidity' && stream.datapoints.length > 0) {
humi = stream.datapoints[0].value.toFixed(1);
}
});
const now = new Date().toLocaleTimeString();
that.setData({
temperature: temp,
humidity: humi,
lastUpdate: `更新于 ${now}`
});
} else {
wx.showToast({
title: '数据获取失败',
icon: 'none'
});
}
},
fail(err) {
console.error('请求失败:', err);
wx.showToast({
title: '网络错误',
icon: 'none'
});
}
});
}
})
对应的index.wxml可以这样简单布局:
<!-- index.wxml -->
<view class="container">
<view class="card">
<text class="label">当前温度</text>
<text class="value">{{temperature}} °C</text>
</view>
<view class="card">
<text class="label">当前湿度</text>
<text class="value">{{humidity}} %</text>
</view>
<text class="update-info">{{lastUpdate}}</text>
<view class="control-panel">
<text class="panel-title">设备控制</text>
<button type="primary" bindtap="sendCommand" data-cmd="led_on">打开LED</button>
<button type="warn" bindtap="sendCommand" data-cmd="led_off">关闭LED</button>
</view>
</view>
4.3 实现指令下发功能
指令下发是小程序向Onenet发送一个命令,Onenet会暂存这个命令。STM32需要定期向Onenet“拉取”命令,或者Onenet通过更复杂的MQTT/EDP协议“推送”命令。对于HTTP协议,我们采用命令拉取的方式。
首先,在小程序中实现发送命令的函数:
// index.js
Page({
// ... 其他数据和方法 ...
sendCommand: function(e) {
const command = e.currentTarget.dataset.cmd; // 获取按钮上定义的指令,如"led_on"
const deviceId = 'your_device_id';
const apiKey = 'your_api_key';
const url = `https://api.heclouds.com/cmds?device_id=${deviceId}`;
wx.request({
url: url,
method: 'POST',
header: {
'api-key': apiKey,
'Content-Type': 'application/json'
},
data: command, // 发送的指令内容,可以是简单字符串或JSON
success(res) {
if (res.statusCode === 200) {
wx.showToast({
title: '指令发送成功',
icon: 'success'
});
console.log('命令ID:', res.data.data.cmd_uuid);
} else {
wx.showToast({
title: '发送失败',
icon: 'none'
});
}
},
fail(err) {
console.error('指令发送失败:', err);
}
});
}
})
然后,在STM32端,我们需要定时(例如每5秒)向Onenet查询是否有待执行的命令。这需要调用另一个Onenet API:GET /cmds。
在onenet.c中增加一个函数:
/**
* @brief 从Onenet拉取命令
* @retval 命令内容字符串指针,若无命令返回NULL
*/
char* Onenet_FetchCommand(void) {
// 构建HTTP GET请求,查询命令
char http_packet[512];
sprintf(http_packet,
"GET /cmds?device_id=%s&timeout=30 HTTP/1.1\r\n" // timeout参数表示命令在服务器保留时间
"api-key: %s\r\n"
"Host: api.heclouds.com\r\n"
"Connection: close\r\n"
"\r\n",
ONENET_DEVICE_ID,
ONENET_API_KEY
);
// ... 通过ESP8266发送GET请求的代码,与上传数据类似 ...
// 解析HTTP响应,提取命令体
// 响应格式大致为:HTTP/1.1 200 OK ... \r\n\r\n{命令内容}
// 需要找到双换行\r\n\r\n后的位置,即为命令正文
char *response = esp8266_rx_buffer; // 假设接收到的数据存在这个缓冲区
char *body_start = strstr(response, "\r\n\r\n");
if(body_start != NULL) {
body_start += 4; // 跳过"\r\n\r\n"
if(strlen(body_start) > 0) {
// 这里可以进一步解析,比如判断是否是JSON
return body_start; // 返回命令字符串
}
}
return NULL;
}
在main.c的主循环或定时任务中调用这个函数,并对返回的命令进行解析和执行:
// 在主循环或一个定时任务中
if(timer_flag.cmd_fetch == 1) {
timer_flag.cmd_fetch = 0;
char *cmd = Onenet_FetchCommand();
if(cmd != NULL) {
printf("收到命令: %s\r\n", cmd);
// 解析并执行命令
if(strstr(cmd, "led_on") != NULL) {
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
printf("LED已打开\r\n");
} else if(strstr(cmd, "led_off") != NULL) {
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
printf("LED已关闭\r\n");
}
// 命令执行后,可以发送一个ACK或删除命令(通过另一个API)
}
}
5. 系统联调与常见问题排查
当所有部分都编码完成后,真正的挑战才开始。联调是一个系统性工程,建议遵循“分模块调试,逐步集成”的原则。
调试顺序建议:
- ESP8266基础AT指令测试:使用USB转TTL连接电脑,用串口调试助手手动发送
AT、AT+CWMODE等指令,确保模块响应正常。 - STM32与ESP8266串口通信测试:编写STM32代码,仅实现向串口发送
AT\r\n并接收打印回显。确保硬件连接和串口驱动无误。 - ESP8266联网测试:在STM32上实现完整的WiFi连接流程,并通过
AT+CIFSR查询IP确认联网成功。 - Onenet HTTP API手动测试:使用电脑上的网络调试工具(如Postman或curl),模拟STM32发送HTTP POST请求上传数据点,以及GET请求拉取命令。这一步能帮你验证API-KEY、设备ID是否正确,以及数据格式是否符合要求。
- STM32上传数据到Onenet:将第4步验证成功的HTTP请求包,用STM32代码实现并发送。在Onenet控制台查看数据是否成功出现。
- 小程序获取数据测试:在小程序开发者工具中,临时写死一个设备ID和API-KEY,运行
fetchDeviceData函数,在控制台查看网络请求是否成功,数据解析是否正确。 - 小程序下发命令测试:同样,先在小程序端测试命令发送功能,观察Onenet控制台“设备管理”->“命令历史”中是否有记录。
- STM32拉取命令测试:最后实现STM32的拉取命令功能,并解析执行。
常见问题与排查清单:
-
ESP8266无响应或响应乱码
- 检查接线:TX/RX是否交叉连接?电源电压是否为稳定的3.3V?电流是否足够(建议500mA以上)?
- 检查波特率:确保STM32串口初始化波特率与ESP8266一致(通常是115200)。
- 检查AT指令格式:每条指令末尾需要加
\r\n(回车换行)。
-
ESP8266连接WiFi失败
- 确认WiFi名称和密码正确,且网络可用。
- 检查WiFi名称是否包含中文或特殊字符,尝试改为纯英文。
- 指令
AT+CWJAP后等待时间加长(如30秒),有些网络连接较慢。
-
Onenet上传数据返回错误
- 检查API-KEY和设备ID:这是最容易出错的地方,确保代码中的值与控制台完全一致。
- 检查HTTP报文格式:特别是
Content-Length是否计算正确,头部和主体之间是否有空行(\r\n\r\n)。 - 使用调试工具:将STM32准备发送的HTTP报文通过串口打印出来,复制到Postman中发送,对比错误信息。
-
小程序无法请求Onenet
- 确认域名已配置:在小程序后台务必添加
https://api.heclouds.com到request合法域名。 - 检查网络权限:确保小程序运行在真机调试或已通过体验版审核,部分网络API在未验证时受限。
- 查看开发者工具控制台:网络请求的详细错误信息会在控制台显示。
- 确认域名已配置:在小程序后台务必添加
-
指令下发后STM32收不到
- 检查拉取命令的时机和频率:STM32是否定期执行了
Onenet_FetchCommand函数? - 检查命令拉取API的响应:打印出STM32收到的完整HTTP响应,看是否包含了命令内容。
- 注意命令的时效性:在
GET /cmds请求中设置的timeout参数,如果STM32在超时后才拉取,命令可能已被服务器清除。
- 检查拉取命令的时机和频率:STM32是否定期执行了
调试是一个需要耐心和细致观察的过程。最有效的方法就是打印日志。在STM32端,将关键步骤的状态、发送的数据、接收的响应都通过串口打印出来;在小程序端,利用console.log输出网络请求和响应。通过对比预期和实际结果,能快速定位问题所在。
整个系统跑通的那一刻,看着手机小程序上实时显示着STM32采集的温度,点击按钮就能远程控制一个LED的亮灭,这种全栈打通的成就感,是单纯调通一个模块无法比拟的。它让你对物联网系统的“端-云-端”架构有了最直观和深刻的理解。这个项目虽然基础,但构成了绝大多数物联网应用的骨架,你可以在此基础上,增加更多的传感器、更复杂的控制逻辑、更美观的小程序界面,甚至探索MQTT协议以降低功耗和延迟,走向更专业的物联网开发领域。
更多推荐



所有评论(0)