2

3月月更

 2 years ago
source link: https://xie.infoq.cn/article/9d21b21516553fe36cfe7d166
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
1

物联网应用开发实践案例 - 智慧农业

作者:DS小龙哥
  • 2022 年 3 月 18 日
  • 本文字数:14943 字

    阅读完需:约 49 分钟

1. 设计需求、硬件环境介绍

1.1 项目背景

近几年,物联网、智能家居、AI 人工智能技术发送非常迅速。在物联网技术的支撑下,如今农业逐渐走向现代化,自动化、现在智能化的农业生产成为了主流。告别“刀耕火种”的传统农业后,现代农业也正在向智慧型转变,当前智慧农业模式已经深入到农业生产的各个环节,灌溉、施肥、植保等细分领域都将与物联网、信息技术等先进科技相结合,效率、效果也将得到大大提高。

要知道,所谓的“智慧农业”就是充分应用现代信息技术成果,集成应用计算机技术与网络技术、物联网技术、无线通信技术以及专家智慧与知识等,实现农业可视化远程诊断、远程控制、灾变预警等智能管理。那么融入物联网的智慧农业的有以下几个优点:

1、低成本化

众所周知,目前想要购买一套全面的智慧农业设备的成本都较高,这是普通农户难以承受的,因此,想要实现全面智慧农业,那么低成本的智慧农业设备将成为智慧农业趋势之一。

2、操作简单化 智慧农业的根本是服务于农业、服务于农户,所以想要做到让农户更快地与智慧农业接轨就必须要把系统做得易操作、易学。要知道,当前我国农民普遍文化程度较低,只有将操作简单化才能够让每个农民都能熟练操作。

智慧农业也是一个大范围,比如: 智慧鱼塘、智慧大棚、智慧园林、城市绿化、智能果园等等都属于智慧农业的范围。

有了智能设备的加持:可以实现自动浇水灌溉、实时检测土壤养分、水分、环境温度、自动补光等一系列联动操作。

本篇文章就利用华为云 IOT 物联网平台实践搭建一个智慧农业智慧大脑,设备平台采用小熊开发板,搭载的 CPU 是意法半导体的 STM32L431 芯片,这是意法半导体推出的低功耗芯片;配合外部的一些专业传感器,能够获取空气中的温湿度数据,光照度数据等,根据种植区的空气温湿度数据,判断是否进行灌溉。

1.2 实现功能

本项目是利用意法半导体的 STM32L431+ESP8266 WIFI ,配合华为云物联网平台服务器,组建一个智慧农业控制系统,结合外部传感器采集的数据,并利用这些数据判断是否进行灌溉,补光等信息提示。

考虑到以学习、实践为目的,当前项目采用了 ESP8266 无线 WIFI 网卡作为联网设备,ESP8266 价钱便宜,支持串口编程,有标准的一套 AT 资料,资料多,作为学习而言,非常适合。可以通过对 ESP8266 的编程实验,了解 TCP、MQTT 网络编程相关知识点。

当前项目主要分为六个功能模块,分别是:基础系统模块、温度采集模块、湿度采集模块、光照采集模块、无线传感器网络模块、OLED 显示屏模块。

(1)基础系统模块:进行各个数据的接收与转发,控制扫水作业是否进行,浇水作业是采用板载的电机模拟

(2)温度采集模块:采集监测区域的温度数据,传输到微控制器

(3)湿度采集模块:采集监测区域的湿度数据,传输到微控制器

(4)光照采集模块:采集监测区域的光照数据,传输到微控制器

(5)无线传感器网络模块:数据上传至云平台,数据下发交互等

(6)LCD 显示屏模块:实时显示所监测到的各项数据

小熊开发板的扩展板上自带了光敏传感器、温湿度传感器、直流电机模块,可以很方便的实现上面的这些功能需求。

本项目设备的源代码里,连接华为云的 MQTT 协议是按照 MQTT 的官方中文手册编写的,不依赖任何外部 SDK,不依赖 ESP8266 设备,只要能联网的设备都可以连接华为云 IOT,非常适合移植到其他单片机平台;不管是采用 51,STM32F1 系列,都可以直接参考代码移植。

华为云物联网平台提供了 API 接口,可以通过 API 开发配套的上位机,方便实现数据查看,手动灌溉等操作。

提供的 API 除了可以查询设备属性信息之外,还可以创建产品、设备、对开发上位机来讲非常方便,可以开发出从底层设备到云端服务器、再到应用 APP 软件,完成 3 层数据交互。

下面是开发的上位机 APP 运行效果。

当前文章主要完成 3 个任务的实践:

(1)云端产品的创建、设备的创建

(2)设备上云,完成服务器登录、数据上传

(3)手机 APP、电脑上位机软件的开发,可以通过云端 API 接口与设备、服务器之前通讯

1.3 设备实物图

目前联网的设备采用的 ESP8266(手上没有现成的 NBIOT 模块,暂时使用 ESP8266 代替,核心原理是一样的),正常项目里会使用 NBIOT 模块联网作为数据传输源。

小熊开发板的设备相关实物图如下:

2. 创建 IOT 服务器端产品

需要先创建产品、在产品下再创建设备。产品是一个大框架,产品下的设备可以有很多,在应用层,可以通过华为云平台提供的 API 创建设备,删除设备,查询设备属性,在做产品时,软件端可以做一个设备注册的引导界面,完成产品下的设备注册,再将数据传递给设备端,这个过程叫“配网“,具体逻辑需要配合设备端完成。最终完成自动化设备创建,注册,上线等操作。

下面先介绍如何手动创建产品,创建设备,了解创建产品创建设备的过程中需要填充什么参数,理解之后,再使用 API 时才更加理解参数含义。

2.1 创建产品

直接打开物联网产品页面: https://www.huaweicloud.com/product/iothub.html

打开产品页面,选择右上角创建产品。

根据自己情况填写信息。就是填写自己产品的一些参数信息。

创建成功后打开产品详情页面,拉到最下面,点击创建自定义模型文件。

这里创建模型文件主要就是为了 MQTT 客户端能够正确的上传传感器数据上来,每个传感器设置一个属性,这个属性就是表示了传感器的数据值类型。

比如: 先添加一个电机,这个电机就是浇水电机,能上报开关状态,云端也能下发命令控制电机,所以需要添加属性和下发的命令。

添加属性:

添加命令: 因为电机需要云端远程控制。

接下来就创建温度、湿度、光照度传感器的属性,这些传感器只是向云端上传数据,不需要下发指令控制,选择只读就可以了,电机要先实现远程浇灌控制,属性就选择读写。

创建完毕效果,一共有 4 个属性,电机、温度、湿度、光强度:

2.2 创建设备

选择设备页面,注册设备。

创建后保持设备密匙等信息,接下来登录服务器时,生成 MQTT 账号密匙需要用到这些参数。

当前创建的设备信息如下:

 {     "device_id": "61cd1d97078a93029b84e7b6_1126626497",     "secret": "1126626497" }

2.3 生成 MQTT 登录账号信息

官微提供的在线小工具: https://iot-tool.obs-website.cn-north-4.myhuaweicloud.com/

按照提示填入数据,生成,非常方便。

当前生成的信息如下:

 ClientId 61cd1d97078a93029b84e7b6_1126626497_0_0_2021123003 Username 61cd1d97078a93029b84e7b6_1126626497 Password b219f3a0099fa0284a2671a5c699b67a7cf6d5f7355d9ee8190011f3b64f71b5

3. 使用 MQTT 客户端模拟测试

为了验证服务器配置是否 OK,先使用 MQTT 客户端软件进行连接测试。

3.1 华为云 IOT 服务器地址与端口

 端口: 1883 域名: a161a58a78.iot-mqtts.cn-north-4.myhuaweicloud.com IP地址: 121.36.42.100

3.2 订阅主题

在产品页面,可以看到主题管理页面,能看到当前设备可以订阅的主题有哪些。

一般订阅下发的数据:

 格式: $oc/devices/{device_id}/sys/messages/down //订阅主题: 平台下发消息给设备 $oc/devices/61cd1d97078a93029b84e7b6_1126626497/sys/messages/down

3.3 上报主题数据

官方文档介绍: https://support.huaweicloud.com/devg-iothub/iot_01_2127.html

服务 ID,属性 ID 在产品页面查看,2.1 小节创建产品里就讲了这个属性的作用。

每次可以单个属性上报,也可以一起上报。

 格式: $oc/devices/{device_id}/sys/properties/report //设备上报主题请求 $oc/devices/61cd1d97078a93029b84e7b6_1126626497/sys/properties/report //上报的数据格式如下 //电机开状态反馈 {"services": [{"service_id": "motor","properties":{"motor":1}}]} //电机关状态反馈 {"services": [{"service_id": "motor","properties":{"motor":0}}]} //温度上报 {"services": [{"service_id": "motor","properties":{"SHT30_H":14}}]} //湿度上报 {"services": [{"service_id": "motor","properties":{"SHT30_L":70}}]} //光照强度上报 {"services": [{"service_id": "motor","properties":{"BH1750":80}}]} //也可以一起上报 {"services": [{"service_id": "motor","properties":{"motor":1}},{"service_id": "motor","properties":{"SHT30_H":15}},{"service_id": "motor","properties":{"SHT30_L":70}},{"service_id": "motor","properties":{"BH1750":80}}]}

3.4 登录服务器

按照软件提示,填入相关数据即可。

如需要也需要使用和我一样的同款软件,打开百度搜索MQTT客户端_v2.4(协议3.1.1).exe 即可找到下载地址。

发送数据后查看云端,已经登录成功,数据已经上传成功。

3.5 下发命令

电机设备支持读写,支持下发命令,在设备页面测试。

点击确定之后,参看 MQTT 客户端软件,已经收到了下发的数据。

 len:174,Data:l$oc/devices/61cd1d97078a93029b84e7b6_1126626497/sys/commands/request_id=390ce15d-6e69-4021-b83a-5e953eea874c{"paras":{"motor":1},"service_id":"motor","command_name":"motor"}

4. 设备端上华为云 IOT

4.1 安装 keil 软件

MCU 采用的 STM32 芯片,设备端代码编写开发就采用的 keil5。

keil5 安装包下载地址: http://www.myir-tech.com/download.asp

安装 keil 时,软件要放在英文目录下,电脑的用户名必须是英文,否则会出现一些奇怪问题。

安装过程中,根据提示下一步下一步点击即可。

4.2 编写代码

工程代码:

工程代码较多,这里就贴出 main.c 全部代码:

 #include "main.h" #include "stm32l4xx_hal.h" #include "i2c.h" #include "usart.h" #include "gpio.h" #include "E53_IA1.h" #include "lcd.h" #include "spi.h" #include "mqtt.h" #include "esp8266.h" /* USER CODE BEGIN Includes */ #include "stdio.h" /* USER CODE END Includes */ void SystemClock_Config(void); #define ESP8266_WIFI_AP_SSID  "CMCC-Cqvn"   //将要连接的路由器名称 --不要出现中文、空格等特殊字符 #define ESP8266_AP_PASSWORD "99pu58cb"     //将要连接的路由器密码 //华为云IOT物联网服务器的设备信息 #define MQTT_ClientID "61cd1d97078a93029b84e7b6_1126626497_0_0_2021123003" #define MQTT_UserName "61cd1d97078a93029b84e7b6_1126626497" #define MQTT_PassWord "b219f3a0099fa0284a2671a5c699b67a7cf6d5f7355d9ee8190011f3b64f71b5" //订阅与发布的主题 #define SET_TOPIC  "$oc/devices/61cd1d97078a93029b84e7b6_1126626497/sys/messages/down"  //订阅 #define POST_TOPIC "$oc/devices/61cd1d97078a93029b84e7b6_1126626497/sys/properties/report"  //发布 //保存温湿度、光照强度 E53_IA1_Data_TypeDef E53_IA1_Data; //显示文本 char lcd_text_str[50]; UART_HandleTypeDef at_usart; //低功耗串口初始化 int32_t at_usart_init(void) {     at_usart.Instance = LPUART1;     at_usart.Init.BaudRate = 115200;     at_usart.Init.WordLength = UART_WORDLENGTH_8B;     at_usart.Init.StopBits = UART_STOPBITS_1;     at_usart.Init.Parity = UART_PARITY_NONE;     at_usart.Init.HwFlowCtl = UART_HWCONTROL_NONE;     at_usart.Init.Mode = UART_MODE_RX | UART_MODE_TX;     if(HAL_UART_Init(&at_usart) != HAL_OK)     {         _Error_Handler(__FILE__, __LINE__);     }    // __HAL_UART_CLEAR_FLAG(usart, UART_FLAG_TC);     __HAL_UART_ENABLE_IT(&at_usart, UART_IT_IDLE);     __HAL_UART_ENABLE_IT(&at_usart, UART_IT_RXNE);     HAL_NVIC_EnableIRQ(LPUART1_IRQn);                   //使能USART1中断通道     HAL_NVIC_SetPriority(LPUART1_IRQn, 3, 3);               //抢占优先级3,子优先级3     return 0; } unsigned char ESP8266_RecvBuf[MAX_RECV_CNT]; unsigned int ESP8266_Recv_cnt=0; unsigned int ESP8266_Recv_flag=0; void LPUART1_IRQHandler() {     //接收到数据     if(__HAL_UART_GET_FLAG(&at_usart, UART_FLAG_RXNE) != RESET)     {         if(ESP8266_Recv_cnt<MAX_RECV_CNT-1)         {             ESP8266_RecvBuf[ESP8266_Recv_cnt++] = (uint8_t)(at_usart.Instance->RDR & 0x00FF);         }          else         {              ESP8266_Recv_flag=1;         }     }       else if (__HAL_UART_GET_FLAG(&at_usart, UART_FLAG_IDLE) != RESET)     {         __HAL_UART_CLEAR_IDLEFLAG(&at_usart);          ESP8266_Recv_flag=1;     } } void AT_SendData(unsigned char *p,unsigned int len) {     int i=0;     for(i=0;i<len;i++)     {         while((LPUART1->ISR & 0X40) == 0); //循环发送,直到发送完毕         LPUART1->TDR = p[i];     } } char mqtt_message[200]; int main(void) {     int i=0;     int cnt=0;     int motor_state=0;     HAL_Init();     SystemClock_Config();     MX_GPIO_Init();     MX_I2C1_Init();     MX_SPI2_Init();     MX_USART1_UART_Init();     at_usart_init();     //初始化硬件     Init_E53_IA1();     LCD_Init();                      LCD_Clear(BLACK);//清屏为黑色     LCD_ShowString(0, 00, 240, 32, 32, "Init ESP8266");//显示字符串,字体大小32*32     if(ESP8266_Init())    {       printf("ESP8266硬件检测错误.\n");       LCD_Clear(BLACK);//清屏为黑色       LCD_ShowString(0, 00, 240, 32, 32, "ESP8266 ERROR");//显示字符串,字体大小32*32    }    else    {        LCD_Clear(BLACK);//清屏为黑色        LCD_ShowString(0, 00, 240, 32, 32, "ESP8266 OK");//显示字符串,字体大小32*32        printf("准备连接到指定的服务器.\n");       //非加密端口       printf("WIFI:%d\r\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))     {         printf("服务器连接失败,正在重试...\n");         HAL_Delay(500);     }     printf("服务器连接成功.\n");     //3. 订阅主题     if(MQTT_SubscribeTopic(SET_TOPIC,0,1))     {         printf("主题订阅失败.\n");     }     else     {         printf("主题订阅成功.\n");     }               while (1)       {             if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin)==GPIO_PIN_RESET)//查询按键KEY1低电平             {                 HAL_Delay(10);//消抖                 if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin)==GPIO_PIN_RESET)//查询按键KEY1低电平                 {                     HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_SET);//亮                     //补光灯亮                     HAL_GPIO_WritePin(IA1_Light_GPIO_Port, IA1_Light_Pin, GPIO_PIN_SET);                     //电机转                     HAL_GPIO_WritePin(IA1_Motor_GPIO_Port, IA1_Motor_Pin, GPIO_PIN_SET);                     motor_state=1;                 }             }             if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin)==GPIO_PIN_RESET)//查询按键KEY2低电平             {                 HAL_Delay(10);//消抖                 if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin)==GPIO_PIN_RESET)//查询按键KEY2低电平                 {                     HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_RESET);//灭                      //补光灯灭                     HAL_GPIO_WritePin(IA1_Light_GPIO_Port, IA1_Light_Pin, GPIO_PIN_RESET);                      //电机停                     HAL_GPIO_WritePin(IA1_Motor_GPIO_Port, IA1_Motor_Pin, GPIO_PIN_RESET);                     motor_state=0;                 }             }          cnt++;          HAL_Delay(10);             if(cnt>=100)          {             cnt=0;             E53_IA1_Read_Data();             printf("光照强度:%d %%\r\n", (int)E53_IA1_Data.Lux);             printf("湿度:%d %%\r\n",(int)E53_IA1_Data.Humidity);             printf("温度:%d ℃\r\n", (int)E53_IA1_Data.Temperature);             sprintf(lcd_text_str,"L: %d %%",(int)E53_IA1_Data.Lux);             LCD_ShowString(40, 50+10+32*1, 240, 32, 32,lcd_text_str);             sprintf(lcd_text_str,"H: %d %%",(int)E53_IA1_Data.Humidity);             LCD_ShowString(40, 50+10+32*2, 240, 32, 32,lcd_text_str);             sprintf(lcd_text_str,"T: %d C",(int)E53_IA1_Data.Temperature);             LCD_ShowString(40, 50+10+32*3, 240, 32, 32,lcd_text_str);             //切换引脚的状态             HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);                //上传数据             sprintf(mqtt_message,"{\"services\": [{\"service_id\": \"motor\",\"properties\":{\"motor\":%d}},"             "{\"service_id\": \"motor\",\"properties\":{\"SHT30_H\":%d}},{\"service_id\": \"motor\",\"properties\":"             "{\"SHT30_L\":%d}},{\"service_id\": \"motor\",\"properties\":{\"BH1750\":%d}}]}",             motor_state,(int)E53_IA1_Data.Humidity,(int)E53_IA1_Data.Temperature,(int)E53_IA1_Data.Lux);             MQTT_PublishData(POST_TOPIC,mqtt_message,0);             //根据湿度自动灌溉             if((int)E53_IA1_Data.Humidity<50)  //小于50自动灌溉             {                  printf("自动灌溉....\n");                  motor_state=1; //电机状态更新                  //电机转                  HAL_GPIO_WritePin(IA1_Motor_GPIO_Port, IA1_Motor_Pin, GPIO_PIN_SET);             }            }           //接收到数据           if(ESP8266_Recv_flag)           {                //如果是下发了属性,判断是开锁还是关锁                 if(ESP8266_Recv_cnt>5)                 {                     ESP8266_RecvBuf[ESP8266_Recv_cnt]='\0';                     //使用字符串查找函数                     if(strstr((char*)&ESP8266_RecvBuf[5],"\"machine\":1"))                     {                          motor_state=1; //电机状态更新                          //电机转                          HAL_GPIO_WritePin(IA1_Motor_GPIO_Port, IA1_Motor_Pin, GPIO_PIN_SET);                            printf("开启电机...\n");                     }                     else if(strstr((char*)&ESP8266_RecvBuf[5],"\"machine\":0"))                     {                         //电机停                         HAL_GPIO_WritePin(IA1_Motor_GPIO_Port, IA1_Motor_Pin, GPIO_PIN_RESET);                         motor_state=0;                         printf("关闭电机...\n");                     }                     for(i=0;i<ESP8266_Recv_cnt;i++)printf("%c",ESP8266_RecvBuf[i]);                     ESP8266_Recv_cnt=0;                     }                 ESP8266_Recv_flag=0;           }       } } void SystemClock_Config(void) {   RCC_OscInitTypeDef RCC_OscInitStruct;   RCC_ClkInitTypeDef RCC_ClkInitStruct;   RCC_PeriphCLKInitTypeDef PeriphClkInit;     /**Initializes the CPU, AHB and APB busses clocks      */   RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_MSI;   RCC_OscInitStruct.HSIState = RCC_HSI_ON;   RCC_OscInitStruct.HSICalibrationValue = 16;   RCC_OscInitStruct.MSIState = RCC_MSI_ON;   RCC_OscInitStruct.MSICalibrationValue = 0;   RCC_OscInitStruct.MSIClockRange = RCC_MSIRANGE_6;   RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;   RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_MSI;   RCC_OscInitStruct.PLL.PLLM = 1;   RCC_OscInitStruct.PLL.PLLN = 40;   RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7;   RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2;   RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2;   if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)   {     _Error_Handler(__FILE__, __LINE__);   }     /**Initializes the CPU, AHB and APB busses clocks      */   RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK                               |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;   RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;   RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;   RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;   RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;   if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK)   {     _Error_Handler(__FILE__, __LINE__);   }   PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1|RCC_PERIPHCLK_I2C1;   PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2;   PeriphClkInit.I2c1ClockSelection = RCC_I2C1CLKSOURCE_HSI;   if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)   {     _Error_Handler(__FILE__, __LINE__);   }     /**Configure the main internal regulator output voltage      */   if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK)   {     _Error_Handler(__FILE__, __LINE__);   }     /**Configure the Systick interrupt time      */   HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);     /**Configure the Systick      */   HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);   /* SysTick_IRQn interrupt configuration */   HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0); } /* USER CODE BEGIN 4 */ /* USER CODE END 4 */ /**   * @brief  This function is executed in case of error occurrence.   * @param  file: The file name as string.   * @param  line: The line in file as a number.   * @retval None   */ void _Error_Handler(char *file, int line) {   /* USER CODE BEGIN Error_Handler_Debug */   /* User can add his own implementation to report the HAL error return state */   while(1)   {   }   /* USER CODE END Error_Handler_Debug */ } #ifdef  USE_FULL_ASSERT /**   * @brief  Reports the name of the source file and the source line number   *         where the assert_param error has occurred.   * @param  file: pointer to the source file name   * @param  line: assert_param error line source number   * @retval None   */ void assert_failed(uint8_t* file, uint32_t line) {    /* USER CODE BEGIN 6 */   /* User can add his own implementation to report the file name and line number,      tex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */   /* USER CODE END 6 */ } #endif /* USE_FULL_ASSERT */

5. 上位机软件开发

上位机与设备之间通信,需要通过服务器完成,服务器提供了对应的 API 接口,所以对于上位机而言通信主要是对 HTTP 请求进行处理,返回的数据进行解析等操作。

当前的软件采用是采用 QT 设计的,实现了产品注册、设备注册、获取在线设备,获取设备属性,远程指令发送等主要功能。

访问华为云的接口都需要填一个X-Auth-Token参数,这个参数获取需要 IAM 账号,下面第一步就先介绍,如何创建 IAM 账号,如何获取X-Auth-Token参数。

5.1 创建 IAM 账户

创建一个 IAM 账户,方便接下来使用 API 接口访问华为云服务时,生成 token 登录密匙。

地址: https://console.huaweicloud.com/iam/?region=cn-north-4#/iam/users

账户创建好之后,代码里就可以编写一个获取 Token 的函数。

 /* 功能: 获取token */ void Widget::GetToken() {     //表示获取token     function_select=3;     QString requestUrl;     QNetworkRequest request;     //设置请求地址     QUrl url;     //获取token请求地址     requestUrl = QString("https://iam.%1.myhuaweicloud.com/v3/auth/tokens")                  .arg(SERVER_ID);     //自己创建的TCP服务器,测试用     //requestUrl="http://10.0.0.6:8080";     //设置数据提交格式     request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/json;charset=UTF-8"));     //构造请求     url.setUrl(requestUrl);     request.setUrl(url);     QString text =QString("{\"auth\":{\"identity\":{\"methods\":[\"password\"],\"password\":"     "{\"user\":{\"domain\": {"     "\"name\":\"%1\"},\"name\": \"%2\",\"password\": \"%3\"}}},"     "\"scope\":{\"project\":{\"name\":\"%4\"}}}}")             .arg(MAIN_USER)             .arg(IAM_USER)             .arg(IAM_PASSWORD)             .arg(SERVER_ID);     //发送请求     manager->post(request, text.toUtf8()); }

5.2 查询设备列表

帮助文档地址: https://support.huaweicloud.com/api-iothub/iot_06_v5_0048.html

官方提供了 API 接口,可以直接获取产品下面的所有设备详细信息返回。

关于请求参数,返回结果的字段含义,在帮助文档里有详细介绍。

URL格式: /v5/iot/{project_id}/devices示例:  https://iotda.cn-north-4.myhuaweicloud.com/v5/iot/0f2d61e43600f4e22f74c003616710bc/devices?product_id=6210e8acde9933029be8facf&is_cascade_query=false&limit=10&marker=ffffffffffffffffffffffff&offset=0

接口的在线调试地址:https://apiexplorer.developer.huaweicloud.com/apiexplorer/debug?product=IoTDA&api=ListDevices

返回的结果:

{ "devices": [  {   "app_id": "1af45e3938bb4482bc0be0a5cb3089e3",   "app_name": "DefaultApp_620esbs1",   "device_id": "6210e8acde9933029be8facf_dev2",   "node_id": "dev2",   "gateway_id": "6210e8acde9933029be8facf_dev2",   "device_name": "dev2",   "node_type": "GATEWAY",   "description": null,   "fw_version": null,   "sw_version": null,   "device_sdk_version": null,   "product_id": "6210e8acde9933029be8facf",   "product_name": "DHT11",   "status": "INACTIVE",   "tags": []  },  {   "app_id": "1af45e3938bb4482bc0be0a5cb3089e3",   "app_name": "DefaultApp_620esbs1",   "device_id": "6210e8acde9933029be8facf_dev1",   "node_id": "dev1",   "gateway_id": "6210e8acde9933029be8facf_dev1",   "device_name": "dev1",   "node_type": "GATEWAY",   "description": null,   "fw_version": null,   "sw_version": null,   "device_sdk_version": null,   "product_id": "6210e8acde9933029be8facf",   "product_name": "DHT11",   "status": "OFFLINE",   "tags": []  } ], "page": {  "count": 2,  "marker": "6210efa980c60c11be19ead1" }}

上面的返回结果里通过 JSON 数组保存了设备信息,每一个设备就是一个独立的对象,上面的数据里返回了两个设备的信息,说明产品的目录下创建了两个设备。

应用层编写代码完成设备列表获取:

//查询所有设备void Widget::Get_AllDevice(){    //查询设备列表    function_select=1;    QString requestUrl;    QNetworkRequest request;    //设置请求地址    QUrl url;    //设备列表请求地址    requestUrl = QString("https://iotda.%1.myhuaweicloud.com/v5/iot/%2/devices?product_id=%3&is_cascade_query=false&limit=10&marker=ffffffffffffffffffffffff&offset=0")                 .arg(SERVER_ID)            .arg(PROJECT_ID)            .arg(Product_id);    //设置数据提交格式    request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/json"));    //设置token    request.setRawHeader("X-Auth-Token",Token);    //构造请求    url.setUrl(requestUrl);    request.setUrl(url);    //发送请求    manager->get(request);}

服务器返回的结果解析:

//查询设备列表    if(function_select==1)    {        //清空原来的设备列表        ui->comboBox->clear();        device_id_lis.clear();        //解析数据        QJsonParseError json_error;        QJsonDocument document = QJsonDocument::fromJson(replyData, &json_error);        if(json_error.error == QJsonParseError::NoError)        {            QJsonObject obj = document.object();            //判断是否是对象,然后开始解析数据            if(document.isObject())            {                QJsonObject obj = document.object();                if(obj.contains("devices"))                {                    QJsonArray array=obj.take("devices").toArray();                    for(int i=0;i<array.size();i++)                    {                        QJsonObject obj1=array.at(i).toObject();                        //得到设备ID                        if(obj1.contains("device_id"))                        {                            QString device_id=obj1.take("device_id").toString();                            device_id_lis.append(device_id);                            ui->comboBox->addItem(device_id);                            qDebug()<<"device_id:"<<device_id;                        }                    }                }            }         }        return;    }

5.3 查询设备属性

(1)应用端查询设备属性的请求

帮助文档地址: https://support.huaweicloud.com/api-iothub/iot_06_v5_0034.html

(2)在线调试地址

接口的在线调试地址: https://apiexplorer.developer.huaweicloud.com/apiexplorer/debug?product=IoTDA&api=ListProperties

(3)设备端响应的数据格式

帮助文档: https://support.huaweicloud.com/api-iothub/iot_06_v5_3011.html

(4)使用总结

上位机 APP 向设备端请求查询设备属性时,设备端会收到如下的消息:

$oc/devices/6210e8acde9933029be8facf_dev1/sys/properties/get/request_id=5f359b5c-542f-460e-9f51-85e82150ff4a{"service_id":"DHT11"}

设备端需要解析这个字符串,得到里面的request_id=5f359b5c-542f-460e-9f51-85e82150ff4a 值。在向服务器回应时,要带上这个请求 ID。

设备端响应的主题格式: $oc/devices/{device_id}/sys/properties/get/response/request_id={request_id}示    例:$oc/devices/6210e8acde9933029be8facf_dev1/sys/properties/get/response/request_id=6c36c85e-68e1-4d01-a2a3-b89f09bd0427设备端响应的数据格式:{"services": [{"service_id": "gps","properties":{"DHT11_t":12,"DHT11_h":33}}]}应用端上位机收到设备端的响应数据:"{\"response\":{\"services\":[{\"service_id\":\"temp\",\"properties\":{\"DHT11_t\":13,\"DHT11_h\":33.345}}]}}"

(5)应用端获取设备属性

   //查询设备属性void Widget::Get_device_properties(){    //表示获取token    function_select=0;    QString requestUrl;    QNetworkRequest request;    //设置请求地址    QUrl url;    if(device_id_lis.size()<=0)    {        //显示错误代码        QMessageBox::information(this,"提示","未选择设备,请先获取设备列表\n选择设备后重试.",QMessageBox::Ok,QMessageBox::Ok);        return;    }    //获取token请求地址    requestUrl = QString("https://iotda.%1.myhuaweicloud.com/v5/iot/%2/devices/%3/properties?service_id=%4")                 .arg(SERVER_ID)            .arg(PROJECT_ID)            .arg(device_id_lis.at(ui->comboBox->currentIndex()))            .arg(service_id);    //自己创建的TCP服务器,测试用    //requestUrl="http://10.0.0.6:8080";    //设置数据提交格式    request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/json"));    //设置token    request.setRawHeader("X-Auth-Token",Token);    //构造请求    url.setUrl(requestUrl);    request.setUrl(url);    //发送请求    manager->get(request);}

(6)应用端解析数据

    //查询设备属性    if(function_select==0)    {        //解析数据        QJsonParseError json_error;        QJsonDocument document = QJsonDocument::fromJson(replyData, &json_error);        if(json_error.error == QJsonParseError::NoError)        {            //判断是否是对象,然后开始解析数据            if(document.isObject())            {                QJsonObject obj = document.object();                if(obj.contains("response"))                {                    QJsonObject obj1=obj.take("response").toObject();                    if(obj1.contains("services"))                    {                         QJsonArray array=obj1.take("services").toArray();                    }                }            }         }        return;    }

5.4 上位机开发环境搭建

上位机软件是采用 QT 开发的,Qt 是一个 1991 年由 QtCompany 开发的跨平台 C++图形用户界面应用程序开发框架。它既可以开发 GUI 程序,也可用于开发非 GUI 程序,比如控制台工具和服务器。QT 在发布 Qt 4.6 的同时,作为 Qt 开发跨平台 IDE 的 Qt Creator 也发布了更新版本。Qt Creator 是一个用于 Qt 开发的轻量级跨平台集成开发环境。Qt Creator 可带来两大关键益处:提供首个专为支持跨平台开发而设计的集成开发环境 (IDE),并确保首次接触 Qt 框架的开发人员能迅速上手和操作。即使不开发 Qt 应用程序,Qt Creator 也是一个简单易用且功能强大的 IDE。

目前 QT 在嵌入式领域、桌面端都用的非常多,开发桌面,嵌入式的上位机还是非常方便。 嵌入式领域包括: 车机主机、嵌入式 Linux 设备等。

QT 官网: https://resources.qt.io/cn

QT5.12.6 安装包下载地址: https://download.qt.io/archive/qt/5.12/5.12.6/

QT 学习专栏: https://blog.csdn.net/xiaolong1126626497/category_11400392.html

QT 环境搭建文章:https://xiaolong.blog.csdn.net/article/details/120654599

整个项目的实现主要分为两个大部分:1. 设备上云 2. 应用侧的软件开发

(1)设备上云: 目前通过 STM32、ESP8266 已经完了华为云物联网云平台的连接,ESP8266 上云的过程主要是 MQTT 协议的理解,目前采用的 ESP8266 没有内置 MQTT 协议相关的 AT 指令,需要自己实现 MQTT 协议,这个过程稍微麻烦一点,需要安装官网的 MQTT 协议手册拼接结构完成协议构造。对于设备端而言,只要是通信采用标准的 MQTT 协议,不管连接哪一个物联网云平台,过程是没有多大区别的。

(2)应用层软件开发: 应用侧软件开发主要是方便远程管理设备,目前华为云物联网平台没有提供在线 web 设计功能、没有提供公版的 APP;所以,在设备上云之后,想要方便的对设备的属性进行查看,管理,都需要自己开发上位机才行。

划线
评论
复制
发布于: 2022 年 03 月 18 日阅读数: 1002

版权声明: 本文为 InfoQ 作者【DS小龙哥】的原创文章。

原文链接:【https://xie.infoq.cn/article/9d21b21516553fe36cfe7d166】。文章转载请联系作者。

用户头像

DS小龙哥

关注

之所以觉得累,是因为说的比做的多。 2022.01.06 加入

熟悉C/C++、51单片机、STM32、Linux应用开发、Linux驱动开发、音视频开发、QT开发. 目前已经完成的项目涉及音视频、物联网、智能家居、工业控制领域


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK