本文分享自华为云社区《物联网应用开发实践案例-智慧农业【玩转华为云】》,作者: DS小龙哥。

  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运行效果。
v2-035fba3ce7e6a8933017cb1468f7db6d_720w.jpg
v2-e1e01d1155dc85001224580b08ed6de0_720w.jpg
当前文章主要完成3个任务的实践:
(1)云端产品的创建、设备的创建
(2)设备上云,完成服务器登录、数据上传
(3)手机APP、电脑上位机软件的开发,可以通过云端API接口与设备、服务器之前通讯

  1.3 设备实物图
目前联网的设备采用的ESP8266(手上没有现成的NBIOT模块,暂时使用ESP8266代替,核心原理是一样的),正常项目里会使用NBIOT模块联网作为数据传输源。
小熊开发板的设备相关实物图如下:
v2-14424332644c334c894d900f17ad55bc_720w.jpg
v2-f5836b677b34f6ba84da99bc4e1d9f32_720w.jpg
v2-e4a96f202c2a63a62583d9c6e551ccd1_720w.jpg
v2-6b73ba29550e3a94a0bc1a04645107e3_720w.jpg

  2. 创建IOT服务器端产品
需要先创建产品、在产品下再创建设备。产品是一个大框架,产品下的设备可以有很多,在应用层,可以通过华为云平台提供的API创建设备,删除设备,查询设备属性,在做产品时,软件端可以做一个设备注册的引导界面,完成产品下的设备注册,再将数据传递给设备端,这个过程叫“配网“,具体逻辑需要配合设备端完成。最终完成自动化设备创建,注册,上线等操作。
下面先介绍如何手动创建产品,创建设备,了解创建产品创建设备的过程中需要填充什么参数,理解之后,再使用API时才更加理解参数含义。

  2.1 创建产品
直接打开物联网产品页面: https://www.huaweicloud.com/product/iothub.html
v2-c627d551ee132f0183672977789d900c_720w.jpg
打开产品页面,选择右上角创建产品。
v2-85bda15bf82e772e2d6eb69afd4ed502_720w.jpg
根据自己情况填写信息。就是填写自己产品的一些参数信息。
v2-339dd83095f4c9720fe595eb008ac5c3_720w.jpg
创建成功后打开产品详情页面,拉到最下面,点击创建自定义模型文件。
v2-f8a33bda258a4532f6ce38e3fba5ce5d_720w.jpg
这里创建模型文件主要就是为了MQTT客户端能够正确的上传传感器数据上来,每个传感器设置一个属性,这个属性就是表示了传感器的数据值类型。
比如: 先添加一个电机,这个电机就是浇水电机,能上报开关状态,云端也能下发命令控制电机,所以需要添加属性和下发的命令。
v2-b4e17004f558bc1c1e2e91cf843e8ec0_720w.jpg
添加属性:
v2-5aaeea138b1e966e79946698157ef987_720w.jpg
添加命令: 因为电机需要云端远程控制。
v2-5f8d70a02450060762e292918ea7a6b3_720w.jpg
接下来就创建温度、湿度、光照度传感器的属性,这些传感器只是向云端上传数据,不需要下发指令控制,选择只读就可以了,电机要先实现远程浇灌控制,属性就选择读写。
v2-b133ba9b968d37704b6ca0f1e764515c_720w.jpg
v2-66dc0f8cce83d0498c5f48d7865d09f0_720w.jpg
v2-74904ba2fff4d27a64b5bf635e1f26eb_720w.jpg
创建完毕效果,一共有4个属性,电机、温度、湿度、光强度:
v2-94d3fa76bc1fe4be8486532d94e5fb22_720w.jpg

  2.2 创建设备
选择设备页面,注册设备。
v2-2017f4a3072aa08ae0d8625b43456703_720w.jpg
创建后保持设备密匙等信息,接下来登录服务器时,生成MQTT账号密匙需要用到这些参数。
v2-f0bcfc15ded06e122e11ee35408b0cfc_720w.jpg
当前创建的设备信息如下:
  
{
  •      "device_id": "61cd1d97078a93029b84e7b6_1126626497",
  •      "secret": "1126626497"
  • }
  • 复制代码

      2.3 生成MQTT登录账号信息
    按照提示填入数据,生成,非常方便。
    v2-837031cd88b2c0c6fd5d79ea4c428201_720w.jpg
    当前生成的信息如下:
      
    ClientId 61cd1d97078a93029b84e7b6_1126626497_0_0_2021123003
  • Username 61cd1d97078a93029b84e7b6_1126626497
  • Password b219f3a0099fa0284a2671a5c699b67a7cf6d5f7355d9ee8190011f3b64f71b5
  • 复制代码

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

      3.1 华为云IOT服务器地址与端口
    v2-2bf8a304049cb9b6eb31fd67654de88b_720w.jpg
      
    端口: 1883
  • 域名: a161a58a78.iot-mqtts.cn-north-4.myhuaweicloud.com
  • IP地址: 121.36.42.100
  • 复制代码

      3.2 订阅主题
    在产品页面,可以看到主题管理页面,能看到当前设备可以订阅的主题有哪些。
    v2-d5d2044834693a63bb2084f8f6fc7e23_720w.jpg
    一般订阅下发的数据:
      
    格式: $oc/devices/{device_id}/sys/messages/down
  • //订阅主题: 平台下发消息给设备
  • $oc/devices/61cd1d97078a93029b84e7b6_1126626497/sys/messages/down
  • 复制代码

      3.3 上报主题数据
    v2-15819c442d7e93eed22ad755ea53d66b_720w.jpg
    服务ID,属性ID在产品页面查看,2.1小节创建产品里就讲了这个属性的作用。
    v2-ac01024ea85b8d91f8cb1bad88f70c5d_720w.jpg
    每次可以单个属性上报,也可以一起上报。
    格式: $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 即可找到下载地址。
    v2-5735ece93c1af5a1baafd17e6c54495d_720w.jpg
    发送数据后查看云端,已经登录成功,数据已经上传成功。
    v2-3fe003e46264e53e96f7a0c348b10b56_720w.jpg

      3.5 下发命令
    电机设备支持读写,支持下发命令,在设备页面测试。
    v2-8efb238a58ef6153176ae6a14d675faa_720w.jpg
    v2-f2ddcd2d4da70d5e4051159b7a178747_720w.jpg
    点击确定之后,参看MQTT客户端软件,已经收到了下发的数据。
    v2-5015c1db4410c74842e91eca68b12302_720w.jpg
      
    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时,软件要放在英文目录下,电脑的用户名必须是英文,否则会出现一些奇怪问题。
    安装过程中,根据提示下一步下一步点击即可。
    v2-f6115f4fc6bd5faaa0b5311a2437c777_720w.jpg

      4.2 编写代码
    工程代码:

    v2-f3a6cfa5da3644db0bc8ee5d3aec1861_720w.jpg
    v2-c89b60d3327eadff96a31165a59da520_720w.jpg
    v2-3562df0ffaab2c161fbdad257355187f_720w.jpg
    工程代码较多,这里就贴出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参数。
    v2-3046016643eb3302b747907381228218_720w.jpg

      5.1 创建IAM账户
    创建一个IAM账户,方便接下来使用API接口访问华为云服务时,生成token登录密匙。
    v2-7584c4cd6b1407ea99702aac8766709e_720w.jpg
    v2-cfbfc4c6e89683978701135f667ab07c_720w.jpg
    v2-57af068a3506cc639371d76042f60e87_720w.jpg
    v2-d0fde720d27bcc490443d2ff5fb60ef5_720w.jpg
    v2-8cf220946cdf9a0858a912e80df774a4_720w.jpg
    账户创建好之后,代码里就可以编写一个获取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 查询设备列表
    官方提供了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 v2-3223f583ee87cf11c688d7423abb493a_720w.jpg
    返回的结果:
      
    {
  • "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数组保存了设备信息,每一个设备就是一个独立的对象,上面的数据里返回了两个设备的信息,说明产品的目录下创建了两个设备。
    v2-f42beda28048afd3297662d9efbca4b0_720w.jpg

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

      
    //查询所有设备
  • 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)应用端查询设备属性的请求
    v2-df6f0f4d5d2a331797b3ade6ba42a406_720w.jpg
    (2)在线调试地址
    (3)设备端响应的数据格式
    v2-3bb7ed0a150fa0c2d627cc3e82ee79ec_720w.jpg
    (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
    v2-b09b27acfd78c3e6229bde10d8d2be25_720w.jpg
    QT5.12.6安装包下载地址: https://download.qt.io/archive/qt/5.12/5.12.6/

      6. 总结
    整个项目的实现主要分为两个大部分:1. 设备上云 2. 应用侧的软件开发
    (1)设备上云: 目前通过STM32、ESP8266已经完了华为云物联网云平台的连接,ESP8266上云的过程主要是MQTT协议的理解,目前采用的ESP8266没有内置MQTT协议相关的AT指令,需要自己实现MQTT协议,这个过程稍微麻烦一点,需要安装官网的MQTT协议手册拼接结构完成协议构造。对于设备端而言,只要是通信采用标准的MQTT协议,不管连接哪一个物联网云平台,过程是没有多大区别的。
    (2)应用层软件开发: 应用侧软件开发主要是方便远程管理设备,目前华为云物联网平台没有提供在线web设计功能、没有提供公版的APP;所以,在设备上云之后,想要方便的对设备的属性进行查看,管理,都需要自己开发上位机才行。