系列文章目录
毕设拯救计划(一)基于FreeRTOS的智能家居(STM32+Onenet云)
毕设拯救计划(二)基于QT的智能家居(泰山派+Onenet云)
前言
这期算是补档,笔者之前出过STM32做的智能家具,当时利用的是EMQX,但是现在好像不是很好用,然后现在更新成ONENET云,并增加了Free RTOS。这部分算是第二节的下位机部分,大家可以自行扩展。
一、设计思路
这部分的效果展示网上的公开资料已经很多了,具体的功能代码之前也已经给出了来了课设拯救计划之基于MQTT云的智能家居,这里主要是说一些具体的规划和实现细节,至于什么外设怎么使用将不再赘述。
1.1 Free RTOS的控制思路
具体的框架思路如下图所示,在各部分模块初始化之后,创建所有需要的任务,将不需要的外设提前挂起以免占用资源。这里要主要内存的大小和优先的设置。这里我的优先级为:语音控制/网络控制>屏幕刷新>RTC>温湿度>监视器/灯光/电机>测距,遵循的原则是控制部分优先级最高,然后按照使用频率设计优先级。
这里的大部分移植都比较简单,只需要把链接中的相关部分打包成任务就可以了,比较麻烦的会单独说一下。
1.2 监视器思路
这里是将监视器拆为三部分:测距任务、监视拍照任务及图像调度任务,这样的好处是仿真任务的处理时间过长导致占用时间过长。
// 图像捕获任务
void Monitor_task(void *pvParameters) {
float length = 0;
while(1)
{
camera_refresh();
if(Monitor_Status == 1 && sd_ok){
length = Get_Length();
if(length < 50){
LED0=0; //点亮DS0,提示正在拍照
camera_new_pathname(pname);//得到文件名
if(bmp_encode(pname,(lcddev.width-240)/2,(lcddev.height-320)/2,240,320,0)){//拍照有误
Show_Str(40,130,240,12,"read file error",12,0);
printf("read file error\n\r");
}
else{
Show_Str(40,130,240,12,"photo saved",12,0);
printf("photo saved\n\r");
Show_Str(40+42,150,240,12,pname,12,0);
message++;
}
LED1=1;//关闭DS1
LCD_Clear(BLACK);
}
}
else vTaskDelay(pdMS_TO_TICKS(100));
}
}
// 图片调度任务
void PhotoManangerTask(void *pvParameters) {
u8 res;
DIR picdir; // 图片目录
FILINFO picfileinfo; // 文件信息
u8 *fn; // 长文件名
u16 totpicnum; // 图片文件总数
u16 curindex; // 图片当前索引
u8 key; // 键值
u8 pause = 0; // 暂停标记
u8 t;
u16 temp;
u16 *picindextbl; // 图片索引表
// 打开图片文件夹
while(f_opendir(&picdir, "0:/PHOTO")) {
vTaskDelay(pdMS_TO_TICKS(100));
Show_Str(30, 170, 240, 16, "PHOTO文件夹错误!", 16, 0);
vTaskDelay(pdMS_TO_TICKS(100));
LCD_Fill(30, 170, 240, 186, WHITE); // 清除显示
}
// 获取图片总数
totpicnum = pic_get_tnum("0:/PHOTO");
// 为长文件名、路径名、索引表分配内存
picfileinfo.lfsize = _MAX_LFN * 2 + 1; // 长文件名最大长度
picfileinfo.lfname = mymalloc(SRAMIN, picfileinfo.lfsize); // 为长文件缓存区分配内存
pname = mymalloc(SRAMIN, picfileinfo.lfsize); // 为带路径的文件名分配内存
picindextbl = mymalloc(SRAMIN, 2 * totpicnum); // 申请2 * totpicnum个字节的内存, 用于存放图片索引
// 记录图片索引
res = f_opendir(&picdir, "0:/PHOTO");
if (res == FR_OK) {
curindex = 0; // 当前索引为0
while (1) { // 全部查询一遍
temp = picdir.index; // 记录当前index
res = f_readdir(&picdir, &picfileinfo); // 读取目录下的一个文件
if (res != FR_OK || picfileinfo.fname[0] == 0) break; // 错误了/到末尾了,退出
fn = (u8*)(*picfileinfo.lfname ? picfileinfo.lfname : picfileinfo.fname);
res = f_typetell(fn);
if ((res & 0XF0) == 0X50) { // 判断是否为图片文件
picindextbl[curindex] = temp; // 记录索引
curindex++;
}
}
}
// 显示图片
Show_Str(30, 170, 240, 16, "开始显示...", 16, 0);
vTaskDelay(pdMS_TO_TICKS(1500));
piclib_init(); // 初始化画图
// 开始图片显示循环
curindex = 0; // 从0开始显示
res = f_opendir(&picdir, "0:/PHOTO");
while (res == FR_OK) {
dir_sdi(&picdir, picindextbl[curindex]); // 改变当前目录索引
res = f_readdir(&picdir, &picfileinfo); // 读取目录下的一个文件
if (res != FR_OK || picfileinfo.fname[0] == 0) break; // 错误了/到末尾了,退出
fn = (u8*)(*picfileinfo.lfname ? picfileinfo.lfname : picfileinfo.fname);
strcpy((char*)pname, "0:/PHOTO/"); // 复制路径
strcat((char*)pname, (const char*)fn); // 将文件名接在后面
// 打印路径进行调试
printf("Loading image: %s\n", pname);
LCD_Clear(WHITE);
res = ai_load_picfile(pname, 0, 0, lcddev.width, lcddev.height, 1); // 显示图片
if (res != 0) printf("Error displaying image: %s\n", pname);
Show_Str(2, 2, 240, 16, pname, 16, 1); // 显示图片名字
// 按键逻辑
t = 0;
while (1) {
key = KEY_Scan(0); // 扫描按键
if (t > 250) key = 1; // 模拟按下KEY0
if ((t % 20) == 0) LED0 = !LED0; // LED0闪烁, 提示程序正在运行
if (key == KEY1_PRES) { // 上一张
if (curindex) curindex--;
else curindex = totpicnum - 1;
break;
} else if (key == KEY0_PRES) { // 下一张
curindex++;
if (curindex >= totpicnum) curindex = 0; // 到末尾, 从头开始
break;
} else if (key == WKUP_PRES) { // 暂停
pause = !pause;
LED1 = !pause; // 暂停时LED1亮
}
if (pause == 0) t++;
vTaskDelay(pdMS_TO_TICKS(100));
}
}
// 释放内存
myfree(SRAMIN, picfileinfo.lfname);
myfree(SRAMIN, pname);
myfree(SRAMIN, picindextbl);
}
1.3 时钟刷新任务
这里部分也是由三部分组成:屏幕刷新任务、RTC时钟及闹钟(这里用的是软件定时器的方法,电机部分也是这个原理),大家可以回顾一下相关部分内容。
二、 联网功能
这里简单提一下,笔者主要是拿STM32作为下位机使用,实际上可以直接使用TCP进行双机互通(你的上位机是电脑、单片机、Arm平台都可以,而且工作量会小很多),感兴趣的可以看一下毕设拯救计划(二)基于QT的智能家居(泰山派+Onenet云)。具体的STM32的实现TCP的思路可以参考STM32 esp8266 TCP,这里讲了一下怎么移植,大家可以学习一下,个人感觉这里没必要深究,会用就可以了,而且确实很省事情!!
2.1 新版ONENET的实现思路
其实这部分也是很简单的,大家具体流程可以参考智能家居,我们这里利用的esp8266进行联网功能。主要是分为三部分:联网、订阅、发布,唯一值得注意的是上传的数据包结构,如果结构出错会直接断开连接。这部分的代码我这边是直接给出了,其实算是很固定的用法了,大家可以按照上面的视频自己再写写熟练一下。
这部分是联网功能,主要是针对8266来写的,代码如下:
// esp8266.c
//==========================================================
// 函数名称: ESP8266_Clear
//
// 函数功能: 清空缓存
//
// 入口参数: 无
//
// 返回参数: 无
//
// 说明:
//==========================================================
void ESP8266_Clear(void)
{
memset(esp8266_buf, 0, sizeof(esp8266_buf));
esp8266_cnt = 0;
}
//==========================================================
// 函数名称: ESP8266_WaitRecive
//
// 函数功能: 等待接收完成
//
// 入口参数: 无
//
// 返回参数: REV_OK-接收完成 REV_WAIT-接收超时未完成
//
// 说明: 循环调用检测是否接收完成
//==========================================================
_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; //返回接收未完成标志
}
//==========================================================
// 函数名称: ESP8266_SendCmd
//
// 函数功能: 发送命令
//
// 入口参数: cmd:命令
// res:需要检查的返回指令
//
// 返回参数: 0-成功 1-失败
//
// 说明:
//==========================================================
_Bool ESP8266_SendCmd(char *cmd, char *res)
{
unsigned char timeOut = 255;
Usart_SendString(USART3, (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;
}
//==========================================================
// 函数名称: ESP8266_SendData
//
// 函数功能: 发送数据
//
// 入口参数: data:数据
// len:长度
//
// 返回参数: 无
//
// 说明:
//==========================================================
void ESP8266_SendData(unsigned char *data, unsigned short len)
{
char cmdBuf[32];
ESP8266_Clear(); //清空接收缓存
sprintf(cmdBuf, "AT+CIPSEND=%d\r\n", len); //发送命令
if(!ESP8266_SendCmd(cmdBuf, ">")) //收到‘>’时可以发送数据
{
Usart_SendString(USART3, data, len); //发送设备连接请求数据
}
}
//==========================================================
// 函数名称: ESP8266_GetIPD
//
// 函数功能: 获取平台返回的数据
//
// 入口参数: 等待的时间(乘以10ms)
//
// 返回参数: 平台返回的原始数据
//
// 说明: 不同网络设备返回的格式不同,需要去调试
// 如ESP8266的返回格式为 "+IPD,x:yyy" x代表数据长度,yyy是数据内容
//==========================================================
unsigned char *ESP8266_GetIPD(unsigned short timeOut)
{
char *ptrIPD = NULL;
do
{
if(ESP8266_WaitRecive() == REV_OK) //如果接收完成
{
ptrIPD = strstr((char *)esp8266_buf, "IPD,"); //搜索“IPD”头
if(ptrIPD == NULL) //如果没找到,可能是IPD头的延迟,还是需要等待一会,但不会超过设定的时间
{
//UsartPrintf(USART_DEBUG, "\"IPD\" not found\r\n");
}
else
{
ptrIPD = strchr(ptrIPD, ':'); //找到':'
if(ptrIPD != NULL)
{
ptrIPD++;
return (unsigned char *)(ptrIPD);
}
else
return NULL;
}
}
delay_ms(5); //延时等待
} while(timeOut--);
return NULL; //超时还未找到,返回空指针
}
//==========================================================
// 函数名称: ESP8266_Init
//
// 函数功能: 初始化ESP8266
//
// 入口参数: 无
//
// 返回参数: 无
//
// 说明:
//==========================================================
void ESP8266_Init(void)
{
ESP8266_Clear();
while(ESP8266_SendCmd("+++", ""));
while(ESP8266_SendCmd("AT+RESTORE\r\n", "OK"));
while(ESP8266_SendCmd("AT\r\n", "OK"));
ESP8266_SendCmd("AT+RST\r\n", "");
delay_ms(500);
ESP8266_SendCmd("AT+CIPCLOSE\r\n", "");
delay_ms(500);
while(ESP8266_SendCmd("AT+CWMODE=1\r\n", "OK"));
while(ESP8266_SendCmd("AT+CIPMUX=0\r\n", "OK"));
while(ESP8266_SendCmd(ESP8266_WIFI_INFO, "WIFI GOT IP"));
}
接下来就是对Onenet云操作的一些函数了:
// onenet.c
//==========================================================
// 函数名称: OneNet_DevLink
//
// 函数功能: 与onenet创建连接
//
// 入口参数: 无
//
// 返回参数: 1-成功 0-失败
//
// 说明: 与onenet平台建立连接
//==========================================================
_Bool OneNet_DevLink(void)
{
MQTT_PACKET_STRUCTURE mqttPacket = {NULL, 0, 0, 0}; //协议包
unsigned char *dataPtr;
char authorization_buf[160];
_Bool status = 1;
OneNET_Authorization("2018-10-31", PROID, 1956499200, ACCESS_KEY, DEVICE_NAME,
authorization_buf, sizeof(authorization_buf), 0);
UsartPrintf(USART_DEBUG, "OneNET_DevLink\r\n"
"NAME: %s, PROID: %s, KEY:%s\r\n"
, DEVICE_NAME, PROID, authorization_buf);
if(MQTT_PacketConnect(PROID, authorization_buf, DEVICE_NAME, 256, 1, MQTT_QOS_LEVEL0, NULL, NULL, 0, &mqttPacket) == 0)
{
ESP8266_SendData(mqttPacket._data, mqttPacket._len); //上传平台
dataPtr = ESP8266_GetIPD(250); //等待平台响应
if(dataPtr != NULL)
{
if(MQTT_UnPacketRecv(dataPtr) == MQTT_PKT_CONNACK)
{
switch(MQTT_UnPacketConnectAck(dataPtr))
{
case 0:UsartPrintf(USART_DEBUG, "Tips: 连接成功\r\n");status = 0;break;
case 1:UsartPrintf(USART_DEBUG, "WARN: 连接失败:协议错误\r\n");break;
case 2:UsartPrintf(USART_DEBUG, "WARN: 连接失败:非法的clientid\r\n");break;
case 3:UsartPrintf(USART_DEBUG, "WARN: 连接失败:服务器失败\r\n");break;
case 4:UsartPrintf(USART_DEBUG, "WARN: 连接失败:用户名或密码错误\r\n");break;
case 5:UsartPrintf(USART_DEBUG, "WARN: 连接失败:非法链接(比如token非法)\r\n");break;
default:UsartPrintf(USART_DEBUG, "ERR: 连接失败:未知错误\r\n");break;
}
}
}
MQTT_DeleteBuffer(&mqttPacket); //删包
}
else
UsartPrintf(USART_DEBUG, "WARN: MQTT_PacketConnect Failed\r\n");
return status;
}
extern u8 temp,humi,message;
unsigned char OneNet_FillBuf(char *buf)
{
char text[256];
memset(text, 0, sizeof(text));
strcpy(buf, "{\"id\":\"1\",\"params\":{");
memset(text, 0, sizeof(text));
sprintf(text, "\"tem\":{\"value\":%d},", temp);
strcat(buf, text);
memset(text, 0, sizeof(text));
sprintf(text, "\"hmi\":{\"value\":%d},", humi);
strcat(buf, text);
memset(text, 0, sizeof(text));
sprintf(text, "\"message\":{\"value\":%d},", message);
strcat(buf, text);
memset(text, 0, sizeof(text));
sprintf(text, "\"monitor\":{\"value\":%s},", Monitor_info.Monitor_Status ? "true" : "false");
strcat(buf, text);
// printf("Buf after monitor: %s\n", buf); // 打印拼接后的buf
memset(text, 0, sizeof(text));
sprintf(text, "\"led\":{\"value\":%s}", led_info.Led_Status ? "true" : "false");
strcat(buf, text);
// printf("Buf after led: %s\n", buf); // 打印拼接后的buf
strcat(buf, "}}");
return strlen(buf);
}
//==========================================================
// 函数名称: OneNet_SendData
//
// 函数功能: 上传数据到平台
//
// 入口参数: type:发送数据的格式
//
// 返回参数: 无
//
// 说明:
//==========================================================
void OneNet_SendData(void)
{
MQTT_PACKET_STRUCTURE mqttPacket = {NULL, 0, 0, 0}; //协议包
char buf[256];
short body_len = 0, i = 0;
// UsartPrintf(USART_DEBUG, "Tips: OneNet_SendData-MQTT\r\n");
memset(buf, 0, sizeof(buf));
body_len = OneNet_FillBuf(buf); //获取当前需要发送的数据流的总长度
if(body_len)
{
if(MQTT_PacketSaveData(PROID, DEVICE_NAME, body_len, NULL, &mqttPacket) == 0) //封包
{
for(; i < body_len; i++)
mqttPacket._data[mqttPacket._len++] = buf[i];
ESP8266_SendData(mqttPacket._data, mqttPacket._len); //上传数据到平台
// UsartPrintf(USART_DEBUG, "Send %d Bytes\r\n", mqttPacket._len);
MQTT_DeleteBuffer(&mqttPacket); //删包
}
else
UsartPrintf(USART_DEBUG, "WARN: EDP_NewBuffer Failed\r\n");
}
}
//==========================================================
// 函数名称: OneNET_Publish
//
// 函数功能: 发布消息
//
// 入口参数: topic:发布的主题
// msg:消息内容
//
// 返回参数: 无
//
// 说明:
//==========================================================
void OneNET_Publish(const char *topic, const char *msg)
{
MQTT_PACKET_STRUCTURE mqtt_packet = {NULL, 0, 0, 0}; //协议包
UsartPrintf(USART_DEBUG, "Publish Topic: %s, Msg: %s\r\n", topic, msg);
if(MQTT_PacketPublish(MQTT_PUBLISH_ID, topic, msg, strlen(msg), MQTT_QOS_LEVEL0, 0, 1, &mqtt_packet) == 0)
{
ESP8266_SendData(mqtt_packet._data, mqtt_packet._len); //向平台发送订阅请求
MQTT_DeleteBuffer(&mqtt_packet); //删包
}
}
//==========================================================
// 函数名称: OneNET_Subscribe
//
// 函数功能: 订阅
//
// 入口参数: 无
//
// 返回参数: 无
//
// 说明:
//==========================================================
void OneNET_Subscribe(void)
{
MQTT_PACKET_STRUCTURE mqtt_packet = {NULL, 0, 0, 0}; //协议包
char topic_buf[56];
const char *topic = topic_buf;
snprintf(topic_buf, sizeof(topic_buf), "$sys/%s/%s/thing/property/set", PROID, DEVICE_NAME);
UsartPrintf(USART_DEBUG, "Subscribe Topic: %s\r\n", topic_buf);
if(MQTT_PacketSubscribe(MQTT_SUBSCRIBE_ID, MQTT_QOS_LEVEL0, &topic, 1, &mqtt_packet) == 0)
{
ESP8266_SendData(mqtt_packet._data, mqtt_packet._len); //向平台发送订阅请求
MQTT_DeleteBuffer(&mqtt_packet); //删包
}
}
//==========================================================
// 函数名称: OneNet_RevPro
//
// 函数功能: 平台返回数据检测
//
// 入口参数: dataPtr:平台返回的数据
//
// 返回参数: 无
//
// 说明:
//==========================================================
void OneNet_RevPro(unsigned char *cmd)
{
char *req_payload = NULL;
char *cmdid_topic = NULL;
unsigned short topic_len = 0;
unsigned short req_len = 0;
unsigned char qos = 0;
static unsigned short pkt_id = 0;
unsigned char type = 0;
short result = 0;
cJSON *raw_json, *params_json, *led_json, *monitor_json;
type = MQTT_UnPacketRecv(cmd);
switch(type)
{
case MQTT_PKT_PUBLISH: //接收的Publish消息
result = MQTT_UnPacketPublish(cmd, &cmdid_topic, &topic_len, &req_payload, &req_len, &qos, &pkt_id);
if(result == 0)
{
UsartPrintf(USART_DEBUG, "topic: %s, topic_len: %d, payload: %s, payload_len: %d\r\n",
cmdid_topic, topic_len, req_payload, req_len);
raw_json = cJSON_Parse(req_payload);
params_json = cJSON_GetObjectItem(raw_json,"params");
led_json = cJSON_GetObjectItem(params_json,"led");
if(led_json != NULL)
{
if(led_json->type == cJSON_True) control = 1;
else if(led_json->type == cJSON_False) control = 2;
}
monitor_json = cJSON_GetObjectItem(params_json,"monitor");
if(monitor_json !=NULL)
{
if(monitor_json->type == cJSON_True) control = 4;
else if(monitor_json->type == cJSON_False) control = 3;
}
cJSON_Delete(raw_json);
}
case MQTT_PKT_PUBACK: //发送Publish消息,平台回复的Ack
if(MQTT_UnPacketPublishAck(cmd) == 0)
UsartPrintf(USART_DEBUG, "Tips: MQTT Publish Send OK\r\n");
break;
case MQTT_PKT_SUBACK: //发送Subscribe消息的Ack
if(MQTT_UnPacketSubscribe(cmd) == 0)
UsartPrintf(USART_DEBUG, "Tips: MQTT Subscribe OK\r\n");
else
UsartPrintf(USART_DEBUG, "Tips: MQTT Subscribe Err\r\n");
break;
default:
result = -1;
break;
}
ESP8266_Clear(); //清空缓存
if(result == -1)
return;
if(type == MQTT_PKT_CMD || type == MQTT_PKT_PUBLISH)
{
MQTT_FreeBuffer(cmdid_topic);
MQTT_FreeBuffer(req_payload);
}
}
2.2 获取当地时间及天气预报
这部分内容就更简单了,主要是调用了一下相关的API,获取了包括时间戳、天气情况等进行预测。考虑这部分不会经常使用,我们这里是在软件初始化之后就立即获取,得到所需的数据后先存储下来,定时对RTC进行时间校准。而对于天气情况等信息则是由语音下达命令之后,再打印出来,以免重复调度占用资源。接下来是我所用的几个API接口:
//心知天气API
#define Xinzhi_TCP "AT+CIPSTART=\"TCP\",\"api.seniverse.com\",80\r\n"
//拼多多API
#define Time_TCP "AT+CIPSTART=\"TCP\",\"qapi.pinduoduo.com\",80\r\n"
//获取当天天气
#define Now_GET "GET https://api.seniverse.com/v3/weather/now.json?key=[你申请的密钥]=shenyang&language=en&unit=c\r\n"
//获取天气预报
#define Forcast_GET "GET https://api.seniverse.com/v3/weather/daily.json?key=[你申请的密钥]=shenyang&language=en&unit=c&start=0&days=3\r\n"
//获取拼多多时间戳
#define Time_GET "GET https://api.pinduoduo.com/api/server/_stm\r\n"
之后就可以得到相对于的数据包,大家按照需求进行解包即可。
免责声明:本次项目的部分代码也是参考了很多优秀作者开源的项目,再次感谢,如有侵权可联系笔者。