智能家居入门7
- 前言
- 一、ONENET云平台创建产品与设备
- 二、STM32端连接服务器前的准备
- 三、STM32端实现
- 四、微信小程序端连接服务器前的准备
- 五、微信小程序端实现
- 六、最终测试
前言
本篇文章介绍最新ONENET云平台的MQTT协议接入方法,在STM32上实现数据上云与服务器下发数据解析,以及微信小程序接入服务器。对于智能家居而言,最重要的就是通信,通信是否稳定是否响应快,这是最重要的,外设的连接与控制这些都是很简单的,网上的一搜直接就有现成的代码,所以本篇博客不会介绍关于外设的使用与控制,主要介绍最新ONENET的接入与消息解析,这个框架搭好以后,其它外设连接那都不是事儿。
硬件准备:
STM32F103C8T6
ESP8266-01S
OLED液晶显示屏或串口调试模块(USB转TTL)
DHT11(可有可无,自己虚构上传数据也可以)
烧录器
一、ONENET云平台创建产品与设备
这部分直接以视频的形式给出:
需要注意的点:
①数据协议:
这里选择的是OneJson,当然也可以选择数据流,但是它们对应的发送数据和接收数据的topic是不一样的,所以如果想省事直接使用本文代码,那就选择OneJson。
②新版三元组:
按照视频创建完毕后,进入设备管理的设备详情中,可以看到后续会用到的三个参数:
二、STM32端连接服务器前的准备
①token:
首先计算token,在连接时会用到:
上图中的clienid和username就是新版三元组中包含的其中两个参数:设备名称、产品ID,password就是使用官方软件计算生成的token。
接下来视频演示如何计算token:
重点提取如下:
纠错:上图中的时间过小,在后面多加随便一个数字既可:2810295937232。
②STM32端OneJson数据协议对应的发布、订阅topic:
在创建产品时我们选择的数据协议是OneJson,文档中有明确给出OneJson数据协议(物模型)的发布和订阅topic,如果数据协议选择数据流的小伙伴,在文档的这个界面往后翻翻就可以看到对应的。
在代码中,首先连接服务器,成功之后就可以订阅主题或者发布数据到主题中。
我们不止要知道发布订阅主题,对于上传数据而言还要知道上传的数据格式;对于订阅服务器下发的数据而言,还需要知道数据格式,以便于解析,下面第三点来介绍这些。
③STM32端OneJson数据协议对应的数据格式::
1、设备属性上报OneJSON数据格式:
图中很容易看出来数据格式中有些可以不用填,本文最终上传的数据格式如下:
{
"id": "123",
"params": {
"temp": {
"value": 25.00,
},
"humi": {
"value": 70.00,
}
}
}
2、设备属性设置OneJSON数据格式(服务器下发数据):
本文代码中订阅话题之后,若是服务器下发数据,STM32端就会收到,打印出接收到的数据如下:
+MQTTSUBRECV:0,"$sys/bs2u21MIHC/dht11/thing/property/set",54,{"id":"208","version":"1.0","params":{"fan_ctl":true}}
代码中会对此消息进行解析,就可以将控制命令给解出来进行动作了。
④esp8266-01s烧录新的mqtt固件:
下载固件与烧录软件:通过百度网盘分享的文件:new_onenet_mqtt固件烧录.rar
链接:https://pan.baidu.com/s/1aAEqlCVyUB-9ZoaFU0qf2w?pwd=1yjb 提取码:1yjb
具体烧录过程参考:https://blog.csdn.net/jackcsdnfghdtrjy/article/details/104770612
三、STM32端实现
上传数据至服务器这部分,是参考的b站视频(彼岸有光我们有船),数据接收与解析为原创,干货来咯。
相较于之前的智能家居stm32端代码,本文的NET相关代码只剩下面两个即可:
esp8266.cpp:
//单片机头文件
#include "stm32f10x.h"
//网络设备驱动
#include "esp8266.h"
//硬件驱动
#include "delay.h"
#include "usart.h"
//C库
#include <string.h>
#include <stdio.h>
#include "cJSON.h"
#include <stdlib.h>
#define WIFI_SSID "wzqzq" // WIFI名
#define WIFI_PSWD "12345678" // WIFI密码
#define ESP8266_WIFI_INFO "AT+CWJAP=\"" WIFI_SSID "\",\"" WIFI_PSWD "\"\r\n"
#define ESP8266_ONENET_INFO "AT+MQTTCONN=0,\"mqtts.heclouds.com\",1883,1\r\n"
#define ESP8266_USERCFG_INFO "AT+MQTTUSERCFG=0,1,\"dht11\",\"bs2u21MIHC\",\"version=2018-10-31&res=produmd5&sign=dkKx5uuWp0sMqet7BJGa2w%3D%3D\",0,0,\"\"\r\n"
const char* pubtopic="$sys/bs2u21MIHC/dht11/thing/property/post";
const char* subtopic="$sys/bs2u21MIHC/dht11/thing/property/set";
unsigned char esp8266_buf[ESP_RX_MAX];
unsigned short esp8266_cnt = 0, esp8266_cntPre = 0;
extern u8 ESP8266_INIT_OK;
char bool_value[12];
char *new_json;
// 函数功能: 清空缓存
void ESP8266_Clear(void)
{
memset(esp8266_buf, 0, sizeof(esp8266_buf));
esp8266_cnt = 0;
}
// 函数功能: 等待接收完成
_Bool ESP8266_WaitRecive(void)
{
if(esp8266_cnt == 0) //如果接收计数为0 则说明没有处于接收数据中,所以直接跳出,结束函数
return REV_WAIT;
if(esp8266_cnt == esp8266_cntPre) //如果上一次的值和这次相同,则说明接收完毕
{
esp8266_cnt = 0; //清0接收计数
return REV_OK; //返回接收完成标志
}
esp8266_cntPre = esp8266_cnt; //置为相同
return REV_WAIT; //返回接收未完成标志
}
// 函数功能: 发送命令
_Bool ESP8266_SendCmd(char *cmd, char *res)
{
unsigned char timeOut = 200;
Usart_SendString(USART2, (unsigned char *)cmd, strlen((const char *)cmd));
while(timeOut--)
{
if(ESP8266_WaitRecive() == REV_OK) //如果收到数据
{
if(strstr((const char *)esp8266_buf, res) != NULL) //如果检索到关键词
{
ESP8266_Clear(); //清空缓存
return 0;
}
}
delay_ms(10);
}
return 1;
}
// 函数功能: 发送数据
void ESP8266_SendData(double temp,double humi,double adcx)
{
char cmdBuf[512];
ESP8266_Clear(); //清空接收缓存
//先发送要发送数据的指令做准备
sprintf(cmdBuf, "AT+MQTTPUB=0,\"%s\",\"{\\\"id\\\":\\\"123\\\"\\,\\\"params\\\":{\\\"temp\\\":{\\\"value\\\":%.2f\\}\\,\\\"humi\\\":{\\\"value\\\":%.2f\\}\\,\\\"ch4\\\":{\\\"value\\\":%.2f\\}}}\",0,0\r\n",pubtopic,temp,humi,adcx); //发送命令
while(ESP8266_SendCmd(cmdBuf, "OK"))
delay_ms(500);
memset(cmdBuf,0,sizeof(cmdBuf));
delay_ms(100);
}
//订阅话题
void ESP8266_sub()
{
char cmdBuf[512];
ESP8266_Clear(); //清空接收缓存
sprintf(cmdBuf, "AT+MQTTSUB=0,\"%s\",0\r\n", subtopic); //发送命令
while(ESP8266_SendCmd(cmdBuf, "OK"))
delay_ms(500);
memset(cmdBuf,0,sizeof(cmdBuf));
delay_ms(100);
}
//键值对提取
char * extract_json(const char *src) {
// 找到 "params" 字段的开始位置
const char *start = strstr(src, "params");
DEBUG_LOG("JSON: %s\n",src);
if (start) {
// 跳过 "params" 字符串的长度
start += strlen("params");
// 找到 '{' 字符的位置,表示 params 对象的开始
while (*start && *start != '{') {
start++;
}
if (*start == '{') {
// 从 '{' 开始复制,直到 '}' 或字符串结束
const char *end = strchr((char *)start, '}');
if (end) {
// 计算需要复制的长度
size_t len = (size_t)(end - start);
// 创建一个新的缓冲区并复制目标字符串
char *new_json = (char *)malloc(len + 2); // +1 为了空字符,+1 为了避免潜在的 '{'
if (new_json) {
strncpy(new_json, start, len);
new_json[len] = '}'; // 添加 '}' 以确保 JSON 结束
new_json[len + 1] = '\0'; // 确保以空字符结尾
DEBUG_LOG("Extracted JSON: %s\n",new_json);
return new_json;
}
}
}
}
}
//解析
void parse_onenet_command(const char *json_str) {
cJSON *json, *led_ctl_item, *fan_ctl_item;
char* temp = extract_json(json_str);
json = cJSON_Parse(temp);
if (json == NULL) {
// 处理 JSON 解析错误
const char *error_ptr = cJSON_GetErrorPtr();
printf("Error parsing JSON: %s\n", error_ptr);
return;
}
// 直接获取 "led_ctl" 的值
led_ctl_item = cJSON_GetObjectItem(json, "led_ctl");
if (led_ctl_item != NULL)
{
if(led_ctl_item -> valueint)
{
DEBUG_LOG("led open\n");
}
else
{
DEBUG_LOG("led close\n");
}
}
fan_ctl_item = cJSON_GetObjectItem(json, "fan_ctl");
if (fan_ctl_item != NULL)
{
if(fan_ctl_item -> valueint)
{
DEBUG_LOG("fan open\n");
}
else
{
DEBUG_LOG("fan close\n");
}
}
// 清理并释放内存
free(new_json);
free(temp);
cJSON_Delete(json);
}
//主函数或者定时器中循环调用,如果esp8266接收到数据,就解析
unsigned char *ESP8266_GetIPD(unsigned short timeOut)
{
char *ptrIPD = NULL;
do
{
if(ESP8266_WaitRecive() == REV_OK) //如果接收完成
{
parse_onenet_command((const char *)esp8266_buf);
}
delay_ms(5);
timeOut--; //延时等待
} while(timeOut > 0);
return NULL; //超时还未找到,返回空指针
}
// 函数功能: 初始化ESP8266
void ESP8266_Init(void)
{
ESP8266_Clear();
DEBUG_LOG("0. AT - 测试MCU-8266通讯");
while(ESP8266_SendCmd("AT\r\n", "OK"))
delay_ms(500);
DEBUG_LOG("1. AT+RST - 软复位8266");
ESP8266_SendCmd("AT+RST\r\n", "");
delay_ms(500);
ESP8266_SendCmd("AT+CIPCLOSE\r\n", "");
delay_ms(500);
DEBUG_LOG("2. AT+CWMODE=1,1 - 设置8266工作模式为STA");
while(ESP8266_SendCmd("AT+CWMODE=1\r\n", "OK"))
delay_ms(500);
DEBUG_LOG("3. AT+CWDHCP=1,1 - 使能STA模式下DHCP");
while(ESP8266_SendCmd("AT+CWDHCP=1,1\r\n", "OK"))
delay_ms(500);
DEBUG_LOG("4. AT+CWJAP - 连接WIFI -> [ SSID: %s ] -> [ Password: %s ] ",WIFI_SSID, WIFI_PSWD);
while(ESP8266_SendCmd(ESP8266_WIFI_INFO, "GOT IP"))
delay_ms(500);
DEBUG_LOG("6. AT+MQTTUSERCFG=0,1 - 设置user信息:ID,devicename,token");
while(ESP8266_SendCmd(ESP8266_USERCFG_INFO, "OK"))
delay_ms(500);
DEBUG_LOG("7. AT+MQTTCONN=0 - 连接服务器:ip,端口号");
while(ESP8266_SendCmd(ESP8266_ONENET_INFO, "OK"))
delay_ms(500);
DEBUG_LOG("服务器已连接");
}
// 函数功能: 串口2收发中断
void USART2_IRQHandler(void)
{
if(USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) //接收中断
{
if(esp8266_cnt >= sizeof(esp8266_buf)) esp8266_cnt = 0; //防止串口被刷爆
esp8266_buf[esp8266_cnt++] = USART2->DR;
USART_ClearFlag(USART2, USART_FLAG_RXNE);
}
}
集成到自己的代码时需要修改wifi、产品ID、设备名称、token、发布订阅话题、发送数据中的标识符要与自己在云平台创建的一致。重要代码的注释也写的很清楚了,结合上面讲的那些话题与数据格式,很容易就可以看懂。
这部分代码解决之后就可以使用云平台进行测试了,测试过程如下视频:
四、微信小程序端连接服务器前的准备
跟第二节一样,也需要准备token,前面已经弄好了,直接复制即可。
①onenet 云端API介绍:
通过阅读onenet物联网开放平台的文档,发现OneNET API提供产品、设备、服务等云端API,使用标准HTTP方法实现资源CURD操作,其中URL中的中文参数使用UTF-8编码。具体的大家可以去看文档,写的很清楚,所以微信小程序使用这种方式接入服务器。
从文档中的接口列表可以看到物模型使用的接口:
②具体的使用方式如下:
1、设备属性最新数据查询(微信小程序从服务器获取数据):
文档往下翻可以看到使用示例,微信小程序中参考这种格式即可:
2、设备属性期望设置(微信小程序发布数据到图中所示的接口地址,此时服务器对应的就会将此数据发布到:$sys/{pid}/{device-name}/thing/property/set这个话题中,这个话题也就是stm32端订阅的话题,这样32端就会接收到微信小程序下发的指令):
文档往下翻可以看到使用示例,微信小程序中参考这种格式即可:
有了上面这两个示例,就可以在微信小程序中进行代码的编写了!
五、微信小程序端实现
index.js
Page({
data: {
temp:0.0,
humi:0.0,
gas_ch4:0.0,
fumes:0.0,
leds:false,
fen:false,
water:false
},
/* 获取OneNET云平台设备数据 */
getinfo() {
/* 发起 HTTPS 网络请求 获取设备参数 */
wx.request({
/* 网址、产品ID、设备名 */
url: "https://iot-api.heclouds.com/thingmodel/query-device-property?product_id=bs2u21MIHC&device_name=dht11",
header: {
/* 用户鉴权信息 */
"authorization": "version=2018-10-31&res=products%2Fbs2u21MIHC%2Fdevices%2Fdht11&et=2810295937232&method=md5&sign"
},
method: "GET", /* HTTP 请求方法:获取 */
success: res => {
console.log("获取成功", res)
this.setData({
humi: res.data.data[3].value,
temp: res.data.data[5].value,
gas_ch4: res.data.data[0].value,
})
}
});
},
//下发指令:led
onledsChange(event){
const that = this
console.log(event.detail.value);
const sw = event.detail.value
that.setData({leds:sw})
if(sw){
wx.request({
url: 'https://iot-api.heclouds.com/thingmodel/set-device-property',
method: 'POST',
header: {
/* 用户鉴权信息 */
"authorization": "version=2018-10-31&res=products%2Fbs2u21MIHC%2Fdevices%2Fdht11&et=2810295937232&method=md5&sign"
},
data: {
"product_id": "bs2u21MIHC",
"device_name": "dht11",
"params": {
"led_ctl": true /* 控制板端LED */
}
},
success(res){
console.log("成功",res.data)
},
fail(res){
console.log("失败",res)
}
})
}else{
wx.request({
url: 'https://iot-api.heclouds.com/thingmodel/set-device-desired-property',
method: 'POST',
header: {
/* 用户鉴权信息 */
"authorization": "version=2018-10-31&res=products%2Fbs2u21MIHC%2Fdevices%2Fdht11&et=2810295937232&method=md5&sign"
},
data: {
"product_id": "bs2u21MIHC",
"device_name": "dht11",
"params": {
"led_ctl": false /* 控制板端LED */
}
},
success(res){
console.log("成功",res.data)
},
fail(res){
console.log("失败",res)
}
})
}
},
//下发指令:风扇
onfenChange(event){
const that = this
console.log(event.detail.value);
const sw = event.detail.value
that.setData({fen:sw})
if(sw){
wx.request({
url: 'https://iot-api.heclouds.com/thingmodel/set-device-desired-property',
method: 'POST',
header: {
/* 用户鉴权信息 */
"authorization": "version=2018-10-31&res=products%2Fbs2u21MIHC%2Fdevices%2Fdht11&et=2810295937232&method=md5&sign"
},
data: {
"product_id": "bs2u21MIHC",
"device_name": "dht11",
"params": {
"fan_ctl": true /* 控制板端空调 */
}
},
success(res){
console.log("成功",res.data)
},
fail(res){
console.log("失败",res)
}
})
}else{
wx.request({
url: 'https://iot-api.heclouds.com/thingmodel/set-device-desired-property',
method: 'POST',
header: {
/* 用户鉴权信息 */
"authorization": "version=2018-10-31&res=products%2Fbs2u21MIHC%2Fdevices%2Fdht11&et=2810295937232&method=md5&sign"
},
data: {
"product_id": "bs2u21MIHC",
"device_name": "dht11",
"params": {
"fan_ctl": false /* 控制板端空调 */
}
},
success(res){
console.log("成功",res.data)
},
fail(res){
console.log("失败",res)
}
})
}
},
sliderChanging:function(e){
console.log(e.detail.value)
wx.request({
url: 'https://iot-api.heclouds.com/thingmodel/set-device-desired-property',
method: 'POST',
header: {
/* 用户鉴权信息 */
"authorization": "version=2018-10-31&res=products%2Fbs2u21MIHC%2Fdevices%2Fdht11&et=2810295937232&method=md5&sign"
},
data: {
"product_id": "bs2u21MIHC",
"device_name": "dht11",
"params": {
"curtain": e.detail.value /* 控制板端窗帘,这个例子中stm32端没写 */
}
},
success(res){
console.log("成功",res.data)
},
fail(res){
console.log("失败",res)
}
})
},
onLoad() {
var that = this
setInterval(function(){
that.getinfo()
},5000)
}
})
上面的代码也不是完整的,有了思路和格式就是非常大的帮助,大家可以参照这样的发布与请求数据的格式将代码集成到自己的项目中,进行自己的界面设计,注意替换产品ID、设备名称、鉴权信息(token)等。
六、最终测试
微信小程序代码也调试好之后,就可以进行联调了,开发板上电连接上服务器中,微信小程序观察能否接收到数据,并测试能否下发指令控制开发板,具体如下视频: