STM32 | MQTT+esp8266(第十四天)

点击上方"蓝字"关注我们

01、MQTT

>>>

知 识 点:

1.esp8266 wifi的AT指令的使用

2.mqtt的连接、发布、订阅等

3.添加了cJSON

*说 明:

1.通过阿里云物联网平台能够获取开发板的D1、D2、D3灯和温湿度值

2.通过阿里云物联网平台能够控制开发板的D1、D2、D3灯的亮灭

3.D4灯用于表示当前连接服务器的状态。亮-已连接;灭-已断开

4.按键1用于重连阿里云物联网平台

5.按键2用于断开阿里云物联网平台

增加了cJSON对阿里云物联网平台的数据解析

ESP8266引脚.jpg

MQTT记录.txt

{

"ProductKey": "gqeaE1iKozV",

"DeviceName": "TESTDEVICE01",

"DeviceSecret": "df5a9a5c4a0b6c22e3acdf5e091a7ea7"

}

Broker Address:gqeaE1iKozV.iot-as-mqtt.cn-shanghai.aliyuncs.com

Broker Port :1883

Client ID :00001|securemode=3,signmethod=hmacsha1|

User Name:TESTDEVICE01&gqeaE1iKozV

password:C8F156AB9166B4C1000419F693A2095E3E23E3A2

属性上报:/sys/gqeaE1iKozV/${deviceName}/thing/event/property/post

属性设置:/sys/gqeaE1iKozV/${deviceName}/thing/service/property/set

//根据自己的设备名,填入属性信息即可

属性上报:/sys/gqeaE1iKozV/TESTDEVICE01/thing/event/property/post

属性设置:/sys/gqeaE1iKozV/TESTDEVICE01/thing/service/property/set

{

"method":"thing.service.property.set",

"id":"00001",

"params":{

"CurrentTemperature":20.0,

"CurrentHumidity":60.0,

"switch_led_r":1,

"switch_led_g":1,

"switch_led_b":0,

},

"version":"1.0.0"

}

阿里网络地址.txt

https://iot.console.aliyun.com/product

02、led.h

#ifndef __LED_H__​#define __LED_H__​​#define CONNECT_MQTT_LED(x) PEout(14)=(x)?0:1​​extern void led_init(void);​​#endif

03、led.c

#include "stm32f4xx.h"#include "sys.h"​​void led_init(void){  GPIO_InitTypeDef     GPIO_InitStructure;​  //打开端口E的硬件时钟,就是对端口E供电  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE,ENABLE);    //打开端口F的硬件时钟,就是对端口F供电  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF,ENABLE);​    //配置GPIOF的第9 10根  GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_9|GPIO_Pin_10;  GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_OUT;  GPIO_InitStructure.GPIO_Speed = GPIO_High_Speed;  GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;  GPIO_InitStructure.GPIO_PuPd  = GPIO_PuPd_NOPULL;    //初始化  GPIO_Init(GPIOF,&GPIO_InitStructure);    //配置GPIOE的第13 14根  GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_13|GPIO_Pin_14;  GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_OUT;  GPIO_InitStructure.GPIO_Speed = GPIO_High_Speed;  GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;  GPIO_InitStructure.GPIO_PuPd  = GPIO_PuPd_NOPULL;​  //初始化  GPIO_Init(GPIOE,&GPIO_InitStructure);      PFout(9)=PFout(10)=1;  PEout(13)=PEout(14)=1;  }​

04、key.h

#ifndef __KEY_H__#define __KEY_H__​extern void key_init(void);​extern uint32_t key_sta_get(void);​​#endif​

05、key.c

#include "stm32f4xx.h"#include "sys.h"​​void key_init(void){  GPIO_InitTypeDef     GPIO_InitStructure;    //打开端口A的硬件时钟,就是对端口A供电  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE);  ​  //打开端口E的硬件时钟,就是对端口E供电  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE,ENABLE);    //配置GPIOA的第0根引脚  GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_0;  GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_IN;  GPIO_InitStructure.GPIO_Speed = GPIO_High_Speed;  GPIO_InitStructure.GPIO_PuPd  = GPIO_PuPd_NOPULL;  //初始化  GPIO_Init(GPIOA,&GPIO_InitStructure);    //配置GPIOE的第2 3 4根引脚  GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4;  GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_IN;  GPIO_InitStructure.GPIO_Speed = GPIO_High_Speed;  GPIO_InitStructure.GPIO_PuPd  = GPIO_PuPd_NOPULL;​  //初始化  GPIO_Init(GPIOE,&GPIO_InitStructure);  }​uint32_t key_sta_get(void){  uint32_t key_sta=0;    if(PAin(0) == 0)    key_sta|=1<<0;    if(PEin(2) == 0)    key_sta|=1<<1;    if(PEin(3) == 0)    key_sta|=1<<2;    if(PEin(4) == 0)    key_sta|=1<<3;    return key_sta;}​​

06、esp8266.h

#ifndef __ESP8266_H__#define __ESP8266_H__#include "stm32f4xx.h"​#define EN_DEBUG_ESP8266  0//添加WIFI热点宏定义,此处根据自己的wifi作调整//可以使用手机的热点#define WIFI_SSID       "Phonewifi"#define WIFI_PASSWORD    "12345678"​//#define WIFI_SSID       "AASD"//#define WIFI_PASSWORD    "12345678"​​extern uint8_t  g_esp8266_tx_buf[512];extern volatile uint8_t  g_esp8266_rx_buf[512];extern volatile uint32_t g_esp8266_rx_cnt;extern volatile uint32_t g_esp8266_rx_end;​extern volatile uint32_t g_esp8266_transparent_transmission_sta;​extern void   esp8266_init(void);extern int32_t  esp8266_self_test(void);extern int32_t   esp8266_exit_transparent_transmission (void);extern int32_t   esp8266_entry_transparent_transmission(void);extern int32_t   esp8266_connect_ap(char* ssid,char* pswd);extern int32_t   esp8266_connect_server(char* mode,char* ip,uint16_t port);extern int32_t   esp8266_disconnect_server(void);extern void   esp8266_send_bytes(uint8_t *buf,uint32_t len);extern void   esp8266_send_str(char *buf);extern void   esp8266_send_at(char *str);extern int32_t  esp8266_enable_multiple_id(uint32_t b);extern int32_t   esp8266_create_server(uint16_t port);extern int32_t   esp8266_close_server(uint16_t port);extern int32_t   esp8266_enable_echo(uint32_t b);extern int32_t   esp8266_reset(void);#endif

07、esp8266.c

#include "stm32f4xx.h"#include "sys.h"#include "delay.h"#include "usart.h"#include <string.h>#include <stdlib.h>#include <stdio.h>​uint8_t  g_esp8266_tx_buf[512];volatile uint8_t  g_esp8266_rx_buf[512];volatile uint32_t g_esp8266_rx_cnt=0;volatile uint32_t g_esp8266_rx_end=0;​volatile uint32_t g_esp8266_transparent_transmission_sta=0;​​​​​void esp8266_init(void){  usart3_init(115200);}​​void esp8266_send_at(char *str){  //清空接收缓冲区  memset((void *)g_esp8266_rx_buf,0, sizeof g_esp8266_rx_buf);    //清空接收计数值  g_esp8266_rx_cnt = 0;      //串口3发送数据  usart3_send_str(str);}​void esp8266_send_bytes(uint8_t *buf,uint32_t len){  usart3_send_bytes(buf,len);​}​void esp8266_send_str(char *buf){  usart3_send_str(buf);​}​/* 查找接收数据包中的字符串 */int32_t esp8266_find_str_in_rx_packet(char *str,uint32_t timeout){  char *dest = str;  char *src  = (char *)&g_esp8266_rx_buf;    //等待串口接收完毕或超时退出  while((strstr(src,dest)==NULL) && timeout)  {        delay_ms(1);    timeout--;  }​  if(timeout)     return 0;                           return -1; }​​/* 自检程序 */int32_t  esp8266_self_test(void){  esp8266_send_at("AT\r\n");    return esp8266_find_str_in_rx_packet("OK",1000);}​/** * 功能:连接热点 * 参数: *         ssid:热点名 *         pwd:热点密码 * 返回值: *         连接结果,非0连接成功,0连接失败 * 说明: *         失败的原因有以下几种(UART通信和ESP8266正常情况下) *         1. WIFI名和密码不正确 *         2. 路由器连接设备太多,未能给ESP8266分配IP */int32_t esp8266_connect_ap(char* ssid,char* pswd){#if 0  //不建议使用以下sprintf,占用过多的栈  char buf[128]={0};    sprintf(buf,"AT+CWJAP_CUR=\"%s\",\"%s\"\r\n",ssid,pswd);​#endif    //设置为STATION模式    esp8266_send_at("AT+CWMODE_CUR=1\r\n");     if(esp8266_find_str_in_rx_packet("OK",1000))    return -1;​​  //连接目标AP  //sprintf(buf,"AT+CWJAP_CUR=\"%s\",\"%s\"\r\n",ssid,pswd);  esp8266_send_at("AT+CWJAP_CUR=");   esp8266_send_at("\"");esp8266_send_at(ssid);esp8266_send_at("\"");    esp8266_send_at(",");    esp8266_send_at("\"");esp8266_send_at(pswd);esp8266_send_at("\"");    esp8266_send_at("\r\n");  if(esp8266_find_str_in_rx_packet("OK",5000))    if(esp8266_find_str_in_rx_packet("CONNECT",5000))      return -2;​  return 0;}​​​/* 退出透传模式 */int32_t esp8266_exit_transparent_transmission (void){​  esp8266_send_at ("+++");  ​  //退出透传模式,发送下一条AT指令要间隔1秒  delay_s(1); ​  //记录当前esp8266工作在非透传模式  g_esp8266_transparent_transmission_sta = 0;​  return 0;}​/* 进入透传模式 */int32_t  esp8266_entry_transparent_transmission(void){  //进入透传模式  esp8266_send_at("AT+CIPMODE=1\r\n");    if(esp8266_find_str_in_rx_packet("OK",5000))    return -1;    delay_s(2);  //开启发送状态  esp8266_send_at("AT+CIPSEND\r\n");  if(esp8266_find_str_in_rx_packet("OK",5000))    return -2;​  //记录当前esp8266工作在透传模式  g_esp8266_transparent_transmission_sta = 1;  return 0;}​​/** * 功能:使用指定协议(TCP/UDP)连接到服务器 * 参数: *         mode:协议类型 "TCP","UDP" *         ip:目标服务器IP *         port:目标是服务器端口号 * 返回值: *         连接结果,非0连接成功,0连接失败 * 说明: *         失败的原因有以下几种(UART通信和ESP8266正常情况下) *         1. 远程服务器IP和端口号有误 *         2. 未连接AP *         3. 服务器端禁止添加(一般不会发生) */int32_t esp8266_connect_server(char* mode,char* ip,uint16_t port){​#if 0    //使用MQTT传递的ip地址过长,不建议使用以下方法,否则导致栈溢出  //AT+CIPSTART="TCP","a10tC4OAAPc.iot-as-mqtt.cn-shanghai.aliyuncs.com",1883,该字符串占用内存过多了    char buf[128]={0};    //连接服务器  sprintf((char*)buf,"AT+CIPSTART=\"%s\",\"%s\",%d\r\n",mode,ip,port);    esp8266_send_at(buf);#else    char buf[16]={0};  esp8266_send_at("AT+CIPSTART=");  esp8266_send_at("\"");  esp8266_send_at(mode);  esp8266_send_at("\"");  esp8266_send_at(",");  esp8266_send_at("\"");  esp8266_send_at(ip);  esp8266_send_at("\"");    esp8266_send_at(",");  sprintf(buf,"%d",port);  esp8266_send_at(buf);    esp8266_send_at("\r\n");  #endif    if(esp8266_find_str_in_rx_packet("CONNECT",5000))    if(esp8266_find_str_in_rx_packet("OK",5000))      return -1;  return 0;}​/* 断开服务器 */int32_t esp8266_disconnect_server(void){  esp8266_send_at("AT+CIPCLOSE\r\n");      if(esp8266_find_str_in_rx_packet("CLOSED",5000))    if(esp8266_find_str_in_rx_packet("OK",5000))      return -1;    return 0;  }​​/* 使能多链接 */int32_t esp8266_enable_multiple_id(uint32_t b){​  char buf[32]={0};    sprintf(buf,"AT+CIPMUX=%d\r\n", b);  esp8266_send_at(buf);    if(esp8266_find_str_in_rx_packet("OK",5000))    return -1;    return 0;}​/* 创建服务器 */int32_t esp8266_create_server(uint16_t port){  char buf[32]={0};    sprintf(buf,"AT+CIPSERVER=1,%d\r\n", port);  esp8266_send_at(buf);    if(esp8266_find_str_in_rx_packet("OK",5000))    return -1;    return 0;}​/* 关闭服务器 */int32_t esp8266_close_server(uint16_t port){  char buf[32]={0};    sprintf(buf,"AT+CIPSERVER=0,%d\r\n", port);  esp8266_send_at(buf);    if(esp8266_find_str_in_rx_packet("OK",5000))    return -1;    return 0;}​/* 回显打开或关闭 */int32_t esp8266_enable_echo(uint32_t b){  if(b)    esp8266_send_at("ATE1\r\n");   else    esp8266_send_at("ATE0\r\n");     if(esp8266_find_str_in_rx_packet("OK",5000))    return -1;​  return 0;}​/* 复位 */int32_t esp8266_reset(void){  esp8266_send_at("AT+RST\r\n");    if(esp8266_find_str_in_rx_packet("OK",10000))    return -1;​  return 0;}​

08、esp8266_mqtt.h

#ifndef __ES8266_MQTT_H#define __ES8266_MQTT_H​#include "stm32f4xx.h"​​​//此处是阿里云服务器的公共实例登陆配置-------------------------------------注意修改为自己的云服务设备信息!!!!​#define MQTT_BROKERADDRESS     "gbfzwEQBsLm.iot-as-mqtt.cn-shanghai.aliyuncs.com"#define MQTT_CLIENTID       "00001|securemode=3,signmethod=hmacsha1|"#define MQTT_USARNAME       "TESTDEVICE01&gbfzwEQBsLm"#define MQTT_PASSWD       "4CBC23E827DF6833CAAAE8CC471C3AACF0B6DBED"#define  MQTT_PUBLISH_TOPIC     "/sys/gbfzwEQBsLm/TESTDEVICE01/thing/event/property/post"#define MQTT_SUBSCRIBE_TOPIC   "/sys/gbfzwEQBsLm/TESTDEVICE01/thing/service/property/set"​​//此处是阿里云服务器的企业实例登陆配置-------------------------------------注意修改为自己的云服务设备信息!!!!//#define MQTT_BROKERADDRESS     "iot-060a065f.mqtt.iothub.aliyuncs.com"//#define MQTT_CLIENTID       "0001|securemode=3,signmethod=hmacsha1|"//#define MQTT_USARNAME       "smartdevice&g850YXdgU5r"//#define MQTT_PASSWD       "A8F93BD31F6085B1AB2AE3CC311E38971B15885D"//#define  MQTT_PUBLISH_TOPIC     "/sys/g850YXdgU5r/smartdevice/thing/event/property/post"//#define MQTT_SUBSCRIBE_TOPIC   "/sys/g850YXdgU5r/smartdevice/thing/service/property/set"​​#define BYTE0(dwTemp)       (*( char *)(&dwTemp))#define BYTE1(dwTemp)       (*((char *)(&dwTemp) + 1))#define BYTE2(dwTemp)       (*((char *)(&dwTemp) + 2))#define BYTE3(dwTemp)       (*((char *)(&dwTemp) + 3))  ​//MQTT连接服务器extern int32_t mqtt_connect(char *client_id,char *user_name,char *password);​//MQTT消息订阅extern int32_t mqtt_subscribe_topic(char *topic,uint8_t qos,uint8_t whether);​//MQTT消息发布extern uint32_t mqtt_publish_data(char *topic, char *message, uint8_t qos);​//MQTT发送心跳包extern int32_t mqtt_send_heart(void);​extern int32_t esp8266_mqtt_init(void);​extern void mqtt_disconnect(void);​extern void mqtt_report_devices_status(void);​#endif

09、esp8266_mqtt.c

#include "stm32f4xx.h"#include <string.h>#include <stdlib.h>#include <stdio.h>#include "sys.h"#include "delay.h"#include "usart.h"#include "dht11.h"#include "esp8266.h"#include "esp8266_mqtt.h"​const uint8_t g_packet_heart_reply[2] = {0xc0,0x00};​​char  g_mqtt_msg[526];​uint32_t g_mqtt_tx_len;​//MQTT发送数据void mqtt_send_bytes(uint8_t *buf,uint32_t len){    esp8266_send_bytes(buf,len);}​//发送心跳包int32_t mqtt_send_heart(void){    uint8_t buf[2]={0xC0,0x00};    uint32_t cnt=2;    uint32_t wait=0;    #if 0    mqtt_send_bytes(buf,2);  return 0;#else    while(cnt--)    {      mqtt_send_bytes(buf,2);    memset((void *)g_esp8266_rx_buf,0,sizeof(g_esp8266_rx_buf));    g_esp8266_rx_cnt=0;          wait=3000;//等待3s时间        while(wait--)    {      delay_ms(1);​      //检查心跳响应固定报头      if((g_esp8266_rx_buf[0]==0xD0) && (g_esp8266_rx_buf[1]==0x00))       {        printf("心跳响应确认成功,服务器在线\r\n");        return 0;      }    }  }  printf("心跳响应确认失败,服务器离线\r\n");  return -1;#endif  ​}​//MQTT无条件断开void mqtt_disconnect(void){  uint8_t buf[2]={0xe0,0x00};      mqtt_send_bytes(buf,2);    esp8266_disconnect_server();}​​//MQTT连接服务器的打包函数int32_t mqtt_connect(char *client_id,char *user_name,char *password){    uint32_t client_id_len = strlen(client_id);    uint32_t user_name_len = strlen(user_name);    uint32_t password_len = strlen(password);    uint32_t data_len;    uint32_t cnt=2;    uint32_t wait=0;    g_mqtt_tx_len=0;      //可变报头+Payload  每个字段包含两个字节的长度标识    data_len = 10 + (client_id_len+2) + (user_name_len+2) + (password_len+2);​    //固定报头    //控制报文类型    g_esp8266_tx_buf[g_mqtt_tx_len++] = 0x10;    //MQTT Message Type CONNECT    //剩余长度(不包括固定头部)    do    {        uint8_t encodedByte = data_len % 128;        data_len = data_len / 128;        // if there are more data to encode, set the top bit of this byte        if ( data_len > 0 )            encodedByte = encodedByte | 128;        g_esp8266_tx_buf[g_mqtt_tx_len++] = encodedByte;    } while ( data_len > 0 );​    //可变报头    //协议名    g_esp8266_tx_buf[g_mqtt_tx_len++] = 0;          // Protocol Name Length MSB    g_esp8266_tx_buf[g_mqtt_tx_len++] = 4;          // Protocol Name Length LSB    g_esp8266_tx_buf[g_mqtt_tx_len++] = 'M';        // ASCII Code for M    g_esp8266_tx_buf[g_mqtt_tx_len++] = 'Q';        // ASCII Code for Q    g_esp8266_tx_buf[g_mqtt_tx_len++] = 'T';        // ASCII Code for T    g_esp8266_tx_buf[g_mqtt_tx_len++] = 'T';        // ASCII Code for T    //协议级别    g_esp8266_tx_buf[g_mqtt_tx_len++] = 4;          // MQTT Protocol version = 4    //连接标志    g_esp8266_tx_buf[g_mqtt_tx_len++] = 0xc2;        // conn flags    g_esp8266_tx_buf[g_mqtt_tx_len++] = 0;          // Keep-alive Time Length MSB    g_esp8266_tx_buf[g_mqtt_tx_len++] = 60;          // Keep-alive Time Length LSB  60S心跳包​    g_esp8266_tx_buf[g_mqtt_tx_len++] = BYTE1(client_id_len);// Client ID length MSB    g_esp8266_tx_buf[g_mqtt_tx_len++] = BYTE0(client_id_len);// Client ID length LSB    memcpy(&g_esp8266_tx_buf[g_mqtt_tx_len],client_id,client_id_len);    g_mqtt_tx_len += client_id_len;​    if(user_name_len > 0)    {        g_esp8266_tx_buf[g_mqtt_tx_len++] = BYTE1(user_name_len);    //user_name length MSB        g_esp8266_tx_buf[g_mqtt_tx_len++] = BYTE0(user_name_len);      //user_name length LSB        memcpy(&g_esp8266_tx_buf[g_mqtt_tx_len],user_name,user_name_len);        g_mqtt_tx_len += user_name_len;    }​    if(password_len > 0)    {        g_esp8266_tx_buf[g_mqtt_tx_len++] = BYTE1(password_len);    //password length MSB        g_esp8266_tx_buf[g_mqtt_tx_len++] = BYTE0(password_len);      //password length LSB        memcpy(&g_esp8266_tx_buf[g_mqtt_tx_len],password,password_len);        g_mqtt_tx_len += password_len;    }​      while(cnt--)    {        memset((void *)g_esp8266_rx_buf,0,sizeof(g_esp8266_rx_buf));    g_esp8266_rx_cnt=0;            mqtt_send_bytes(g_esp8266_tx_buf,g_mqtt_tx_len);            wait=3000;//等待3s时间            while(wait--)        {      delay_ms(1);​      //检查连接确认固定报头            if((g_esp8266_rx_buf[0]==0x20) && (g_esp8266_rx_buf[1]==0x02))             {        if(g_esp8266_rx_buf[3] == 0x00)        {          printf("连接已被服务器端接受,连接确认成功\r\n");          return 0;//连接成功        }        else        {          switch(g_esp8266_rx_buf[3])          {            case 1:printf("连接已拒绝,不支持的协议版本\r\n");            break;            case 2:printf("连接已拒绝,不合格的客户端标识符\r\n");            break;                case 3:printf("连接已拒绝,服务端不可用\r\n");            break;                case 4:printf("连接已拒绝,无效的用户或密码\r\n");            break;              case 5:printf("连接已拒绝,未授权\r\n");            break;            default:printf("未知响应\r\n");            break;          }          return 0;        }             }          }    }      return -1;}​//MQTT订阅/取消订阅数据打包函数//topic       主题//qos         消息等级//whether     订阅/取消订阅请求包int32_t mqtt_subscribe_topic(char *topic,uint8_t qos,uint8_t whether){          uint32_t cnt=2;    uint32_t wait=0;      uint32_t topiclen = strlen(topic);​    uint32_t data_len = 2 + (topiclen+2) + (whether?1:0);//可变报头的长度(2字节)加上有效载荷的长度    g_mqtt_tx_len=0;      //固定报头    //控制报文类型    if(whether)     g_esp8266_tx_buf[g_mqtt_tx_len++] = 0x82; //消息类型和标志订阅    else      g_esp8266_tx_buf[g_mqtt_tx_len++] = 0xA2; //取消订阅​    //剩余长度    do    {        uint8_t encodedByte = data_len % 128;        data_len = data_len / 128;        // if there are more data to encode, set the top bit of this byte        if ( data_len > 0 )            encodedByte = encodedByte | 128;        g_esp8266_tx_buf[g_mqtt_tx_len++] = encodedByte;    } while ( data_len > 0 );​    //可变报头    g_esp8266_tx_buf[g_mqtt_tx_len++] = 0;        //消息标识符 MSB    g_esp8266_tx_buf[g_mqtt_tx_len++] = 0x01;           //消息标识符 LSB      //有效载荷    g_esp8266_tx_buf[g_mqtt_tx_len++] = BYTE1(topiclen);//主题长度 MSB    g_esp8266_tx_buf[g_mqtt_tx_len++] = BYTE0(topiclen);//主题长度 LSB    memcpy(&g_esp8266_tx_buf[g_mqtt_tx_len],topic,topiclen);      g_mqtt_tx_len += topiclen;​    if(whether)    {        g_esp8266_tx_buf[g_mqtt_tx_len++] = qos;//QoS级别    }​​    while(cnt--)    {    g_esp8266_rx_cnt=0;        memset((void *)g_esp8266_rx_buf,0,sizeof(g_esp8266_rx_buf));        mqtt_send_bytes(g_esp8266_tx_buf,g_mqtt_tx_len);            wait=3000;//等待3s时间        while(wait--)        {      delay_ms(1);            //检查订阅确认报头            if(g_esp8266_rx_buf[0]==0x90)            {        printf("订阅主题确认成功\r\n");                //获取剩余长度        if(g_esp8266_rx_buf[1]==3)        {          printf("Success - Maximum QoS 0 is %02X\r\n",g_esp8266_rx_buf[2]);          printf("Success - Maximum QoS 2 is %02X\r\n",g_esp8266_rx_buf[3]);              printf("Failure is %02X\r\n",g_esp8266_rx_buf[4]);          }        //获取剩余长度        if(g_esp8266_rx_buf[1]==2)        {          printf("Success - Maximum QoS 0 is %02X\r\n",g_esp8266_rx_buf[2]);          printf("Success - Maximum QoS 2 is %02X\r\n",g_esp8266_rx_buf[3]);              }                        //获取剩余长度        if(g_esp8266_rx_buf[1]==1)        {          printf("Success - Maximum QoS 0 is %02X\r\n",g_esp8266_rx_buf[2]);            }                        return 0;//订阅成功            }                    }    }      if(cnt)     return 0;  //订阅成功      return -1;}​//MQTT发布数据打包函数//topic   主题//message 消息//qos     消息等级uint32_t mqtt_publish_data(char *topic, char *message, uint8_t qos){static   uint16_t id=0;      uint32_t topicLength = strlen(topic);    uint32_t messageLength = strlen(message);​    uint32_t data_len;  uint8_t encodedByte;​    g_mqtt_tx_len=0;    //有效载荷的长度这样计算:用固定报头中的剩余长度字段的值减去可变报头的长度    //QOS为0时没有标识符    //数据长度             主题名   报文标识符   有效载荷    if(qos)  data_len = (2+topicLength) + 2 + messageLength;    else  data_len = (2+topicLength) + messageLength;​    //固定报头    //控制报文类型    g_esp8266_tx_buf[g_mqtt_tx_len++] = 0x30;    // MQTT Message Type PUBLISH​    //剩余长度    do    {        encodedByte = data_len % 128;        data_len = data_len / 128;        // if there are more data to encode, set the top bit of this byte        if ( data_len > 0 )            encodedByte = encodedByte | 128;        g_esp8266_tx_buf[g_mqtt_tx_len++] = encodedByte;    } while ( data_len > 0 );​    g_esp8266_tx_buf[g_mqtt_tx_len++] = BYTE1(topicLength);//主题长度MSB    g_esp8266_tx_buf[g_mqtt_tx_len++] = BYTE0(topicLength);//主题长度LSB      memcpy(&g_esp8266_tx_buf[g_mqtt_tx_len],topic,topicLength);//拷贝主题      g_mqtt_tx_len += topicLength;​    //报文标识符    if(qos)    {        g_esp8266_tx_buf[g_mqtt_tx_len++] = BYTE1(id);        g_esp8266_tx_buf[g_mqtt_tx_len++] = BYTE0(id);        id++;    }      memcpy(&g_esp8266_tx_buf[g_mqtt_tx_len],message,messageLength);      g_mqtt_tx_len += messageLength;  ​  mqtt_send_bytes(g_esp8266_tx_buf,g_mqtt_tx_len);      //咱们的Qos等级设置的是00,因此阿里云物联网平台是没有返回响应信息的    return g_mqtt_tx_len;}​​​//设备状态上报void mqtt_report_devices_status(void){    uint8_t led_1_sta = GPIO_ReadOutputDataBit(GPIOF,GPIO_Pin_9) ? 0:1;    uint8_t led_2_sta = GPIO_ReadOutputDataBit(GPIOF,GPIO_Pin_10) ? 0:1;    uint8_t led_3_sta = GPIO_ReadOutputDataBit(GPIOE,GPIO_Pin_13) ? 0:1;​​    //把开发板相关的状态变量利用sprintf函数存放到一个数组里,再把该数组利用MQTT协议打包成消息报文    //sprintf(str,"a=%d",a);    //需要更改"temperature"和"CurrentHumidity"为对应的平台设备信息;    sprintf(g_mqtt_msg,            "{\"method\":\"thing.service.property.set\",\"id\":\"0001\",\"params\":{\    \"CurrentTemperature\":%.1f,\    \"CurrentHumidity\":%.1f,\    \"switch_led_r\":%d,\    \"switch_led_g\":%d,\    \"switch_led_b\":%d,\  },\"version\":\"1.0.0\"}",            g_temp,            g_humi,            led_1_sta,            led_2_sta,            led_3_sta);​    //上报信息到平台服务器    mqtt_publish_data(MQTT_PUBLISH_TOPIC,g_mqtt_msg,0);}​int32_t esp8266_mqtt_init(void){  int32_t rt;    //esp8266初始化  esp8266_init();​//  printf("esp8266_init");​  //退出透传模式,才能输入AT指令  rt=esp8266_exit_transparent_transmission();  if(rt)  {    printf("esp8266_exit_transparent_transmission fail\r\n");    return -1;  }    printf("esp8266_exit_transparent_transmission success\r\n");  delay_s(2);    //复位模块  rt=esp8266_reset();  if(rt)  {    printf("esp8266_reset fail\r\n");    return -2;  }  printf("esp8266_reset success\r\n");  delay_s(2);      //关闭回显  rt=esp8266_enable_echo(0);  if(rt)  {    printf("esp8266_enable_echo(0) fail\r\n");    return -3;  }    printf("esp8266_enable_echo(0)success\r\n");  delay_s(2);        //连接热点  rt = esp8266_connect_ap(WIFI_SSID,WIFI_PASSWORD);  if(rt)  {    printf("esp8266_connect_ap fail\r\n");    return -4;  }    printf("esp8266_connect_ap success\r\n");  delay_s(2);    rt =esp8266_connect_server("TCP",MQTT_BROKERADDRESS,1883);  if(rt)  {    printf("esp8266_connect_server fail\r\n");    return -5;  }    printf("esp8266_connect_server success\r\n");  delay_s(2);    //进入透传模式  rt =esp8266_entry_transparent_transmission();  if(rt)  {    printf("esp8266_entry_transparent_transmission fail\r\n");    return -6;  }    printf("esp8266_entry_transparent_transmission success\r\n");  delay_s(2);    if(mqtt_connect(MQTT_CLIENTID, MQTT_USARNAME, MQTT_PASSWD))  {    printf("mqtt_connect fail\r\n");    return -7;      }  printf("mqtt_connect success\r\n");  delay_s(2);        if(mqtt_subscribe_topic(MQTT_SUBSCRIBE_TOPIC,0,1))  {    printf("mqtt_subscribe_topic fail\r\n");    return -8;  }      printf("mqtt_subscribe_topic success\r\n");    return 0;}​

10、usart.h

#ifndef __USART_H__#define __USART_H__​extern volatile uint8_t  g_usart1_rx_buf[512];extern volatile uint32_t g_usart1_rx_cnt;extern volatile uint32_t g_usart1_rx_end;​extern void usart1_init(uint32_t baud);extern void usart3_init(uint32_t baud);​extern void usart3_send_str(char *str);extern void usart3_send_bytes(uint8_t *buf,uint32_t len);​​​​#endif

11、usart.c

#include "stm32f4xx.h"#include "sys.h"#include "usart.h"#include "esp8266.h"#include <stdio.h>#include <string.h>#include <stdlib.h>​static USART_InitTypeDef       USART_InitStructure;static GPIO_InitTypeDef     GPIO_InitStructure;static NVIC_InitTypeDef       NVIC_InitStructure;​​volatile uint8_t  g_usart1_rx_buf[512];volatile uint32_t g_usart1_rx_cnt=0;volatile uint32_t g_usart1_rx_end=0;​#pragma import(__use_no_semihosting_swi)​struct __FILE { int handle; /* Add whatever you need here */ };FILE __stdout;FILE __stdin;​int fputc(int ch, FILE *f) {  USART_SendData(USART1,ch);      //等待数据发送成功  while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);  USART_ClearFlag(USART1,USART_FLAG_TXE);​  return ch;}​void _sys_exit(int return_code) {​}​void _ttywrch(int ch) {   ch = ch; } void usart1_init(uint32_t baud){  //使能端口A硬件时钟  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE);    //使能串口1硬件时钟  RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);      //配置PA9、PA10为复用功能引脚  GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_9|GPIO_Pin_10;  GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF;  GPIO_InitStructure.GPIO_Speed = GPIO_High_Speed;  GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;  GPIO_InitStructure.GPIO_PuPd  = GPIO_PuPd_NOPULL;    GPIO_Init(GPIOA,&GPIO_InitStructure);    //将PA9、PA10连接到USART1的硬件  GPIO_PinAFConfig(GPIOA, GPIO_PinSource9,  GPIO_AF_USART1);  GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1);      //配置USART1的相关参数:波特率、数据位、校验位  USART_InitStructure.USART_BaudRate = baud;//波特率  USART_InitStructure.USART_WordLength = USART_WordLength_8b;//8位数据位  USART_InitStructure.USART_StopBits = USART_StopBits_1;//1位停止位  USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验  USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件流控制  USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;//允许串口发送和接收数据  USART_Init(USART1, &USART_InitStructure);      //使能串口接收到数据触发中断  USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;  NVIC_Init(&NVIC_InitStructure);    //使能串口1工作  USART_Cmd(USART1,ENABLE);}​void usart3_init(uint32_t baud){  //使能端口B硬件时钟  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB,ENABLE);    //使能串口3硬件时钟  RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3,ENABLE);    //配置PB10、PB11为复用功能引脚  GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_10|GPIO_Pin_11;  GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF;  GPIO_InitStructure.GPIO_Speed = GPIO_High_Speed;  GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;  GPIO_InitStructure.GPIO_PuPd  = GPIO_PuPd_NOPULL;    GPIO_Init(GPIOB,&GPIO_InitStructure);    //将PB10、PB11连接到USART3的硬件  GPIO_PinAFConfig(GPIOB, GPIO_PinSource10, GPIO_AF_USART3);  GPIO_PinAFConfig(GPIOB, GPIO_PinSource11, GPIO_AF_USART3);      //配置USART1的相关参数:波特率、数据位、校验位  USART_InitStructure.USART_BaudRate = baud;//波特率  USART_InitStructure.USART_WordLength = USART_WordLength_8b;//8位数据位  USART_InitStructure.USART_StopBits = USART_StopBits_1;//1位停止位  USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验  USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件流控制  USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;//允许串口发送和接收数据  USART_Init(USART3, &USART_InitStructure);      //使能串口接收到数据触发中断  USART_ITConfig(USART3, USART_IT_RXNE, ENABLE);    NVIC_InitStructure.NVIC_IRQChannel = USART3_IRQn;  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;  NVIC_Init(&NVIC_InitStructure);    //使能串口3工作  USART_Cmd(USART3,ENABLE);}​void usart3_send_str(char *str){  char *p = str;    while(*p!='\0')  {    USART_SendData(USART3,*p);        p++;      //等待数据发送成功    while(USART_GetFlagStatus(USART3,USART_FLAG_TXE)==RESET);    USART_ClearFlag(USART3,USART_FLAG_TXE);  }}​​void usart3_send_bytes(uint8_t *buf,uint32_t len){  uint8_t *p = buf;    while(len--)  {    USART_SendData(USART3,*p);        p++;        //等待数据发送成功    while(USART_GetFlagStatus(USART3,USART_FLAG_TXE)==RESET);    USART_ClearFlag(USART3,USART_FLAG_TXE);  }}​​​​void USART1_IRQHandler(void){  uint8_t d=0;    //检测是否接收到数据  if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)  {    d=USART_ReceiveData(USART1);        g_usart1_rx_buf[g_usart1_rx_cnt++]=d;        if(g_usart1_rx_cnt >= sizeof g_usart1_rx_buf)    {      g_usart1_rx_end=1;    }        #if EN_DEBUG_ESP8266        //将接收到的数据发给串口3    USART_SendData(USART3,d);    while(USART_GetFlagStatus(USART3,USART_FLAG_TXE)==RESET);#endif        //清空标志位,可以响应新的中断请求    USART_ClearITPendingBit(USART1, USART_IT_RXNE);  }}​​​void USART3_IRQHandler(void){  uint8_t d=0;    //检测是否接收到数据  if (USART_GetITStatus(USART3, USART_IT_RXNE) == SET)  {    d=USART_ReceiveData(USART3);            g_esp8266_rx_buf[g_esp8266_rx_cnt++]=d;        if(g_esp8266_rx_cnt >= sizeof g_esp8266_rx_buf)    {      g_esp8266_rx_end=1;    }​#if EN_DEBUG_ESP8266        //将接收到的数据返发给PC    USART_SendData(USART1,d);    //while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);#endif        //清空标志位,可以响应新的中断请求    USART_ClearITPendingBit(USART3, USART_IT_RXNE);  }}​

12、main.c

/*****************************************************************名    称:基于stm32f4的mqtt*作    者:陈堪才*创建日期:2021/05/22*知 识 点:  1.esp8266 wifi的AT指令的使用  2.mqtt的连接、发布、订阅等    3.添加了cJSON*说  明:      1.通过阿里云物联网平台能够获取开发板的D1、D2、D3灯和温湿度值  2.通过阿里云物联网平台能够控制开发板的D1、D2、D3灯的亮灭  3.D4灯用于表示当前连接服务器的状态。亮-已连接;灭-已断开  4.按键1用于重连阿里云物联网平台  5.按键2用于断开阿里云物联网平台*修改日期:  2021/05/27,增加了cJSON对阿里云物联网平台的数据解析*****************************************************************/#include "stm32f4xx.h"#include "sys.h"#include "usart.h"#include "esp8266.h"#include "esp8266_mqtt.h"#include "delay.h"#include "led.h"#include "beep.h"#include "dht11.h"#include "key.h"#include "tim.h"#include "cjson.h"#include <stdio.h>#include <string.h>#include <stdlib.h>​​void mqtt_cjson_parse(char *pbuf){  cJSON *json , *json_params, *json_id, *json_led, *json_method;    char *p = pbuf;      //解析数据包  json = cJSON_Parse(p);        if (!json)    {      cJSON_Delete(json);    json=NULL;        return;  }         //根据method键获取值  json_method = cJSON_GetObjectItem(json ,"method");   if(json_method->type == cJSON_String)  {    printf("method:%s\r\n", json_method->valuestring);    }      //根据id键获取值        json_id = cJSON_GetObjectItem(json , "id");   if(json_id->type == cJSON_String)  {    printf("id:%s\r\n", json_id->valuestring);    }      //根据params键获取值  json_params = cJSON_GetObjectItem(json , "params");   if(json_params)  {    //根据switch_led_r键获取值    json_led=cJSON_GetObjectItem(json_params , "switch_led_r");     if(json_led->type == cJSON_Number)    {      PFout(9) = !json_led->valueint;       printf("switch_led_r:%d\r\n", json_led->valueint);      }        //根据switch_led_g键获取值    json_led=cJSON_GetObjectItem(json_params , "switch_led_g");     if(json_led->type == cJSON_Number)    {      PFout(10) = !json_led->valueint;       printf("switch_led_g:%d\r\n", json_led->valueint);      }          //根据switch_led_b键获取值    json_led=cJSON_GetObjectItem(json_params , "switch_led_b");     if(json_led->type == cJSON_Number)    {      PEout(13) = !json_led->valueint;      printf("switch_led_b:%d\r\n", json_led->valueint);      }          }  cJSON_Delete(json);  json=NULL;  }​int main(void){  uint32_t   i=0;  uint32_t   delay_1ms_cnt=0;  uint8_t    buf[5]={20,05,56,8,20};  uint32_t  key_sta=0;  int32_t    rt=0;​  Delay_Init();    //led初始化  led_init();    //beep初始化  beep_init();      //温湿度传感器初始化  dht11_init();    //按键检测  key_init();    //定时器初始化  tim3_init();    //串口1初始化波特率为115200bps  usart1_init(115200);    //串口延迟一会,确保芯片内部完成全部初始化,printf无乱码输出  delay_ms(500);    //打印开机信息  printf("This is esp8266 mqtt with aliyun test by teacher.chen\r\n");    while(esp8266_mqtt_init())  {    printf("esp8266_mqtt_init ...");        delay_s(1);  }    //连接服务器状态指示灯,点亮-连接成功  CONNECT_MQTT_LED(1);​  printf("esp8266 connect aliyun with mqtt success\r\n");      while(1)  {    //检查接收到数据    if(g_esp8266_rx_end && g_esp8266_transparent_transmission_sta)    {#if 1                printf("g_esp8266_rx_buf:%s\n",g_esp8266_rx_buf);            for(i=0;i<g_esp8266_rx_cnt;i++)      {  /*由于mqtt协议发布消息数据包 = 0x30+剩余长度+01+00+Topic主题名+json内容,例如通过阿里云物联网平台发送如下      0x30 0xE2 0x01 0x00 /thing/service/property/set{"method":"thing.service.property.set","id":"408723893","params":{"switch_led_g":1,"version":"1.0.0"}    传给cJSON时必须全为字符串,不能有0x00,否则遇到0x00会导致直接结束cJSON的。因此需要自行查找'{'开头的json内容        */        if(g_esp8266_rx_buf[i] == '{')        {          mqtt_cjson_parse((char *)&g_esp8266_rx_buf[i]);          break;        }      }#else      for(i=0;i<g_esp8266_rx_cnt;i++)      {        //判断的关键字符是否为 1"        //核心数据,即{"switch_led_r":1}中的"1"        if((g_esp8266_rx_buf[i]==0x31) && (g_esp8266_rx_buf[i+1]==0x22))        {            //判断控制变量            if( g_esp8266_rx_buf[i+3]=='1' )              PFout(9)=0;//控制灯亮            else              PFout(9)=1;//控制灯灭        }  ​        //判断的关键字符是否为 2"        //核心数据,即{"switch_led_g":1}中的"1"        if((g_esp8266_rx_buf[i]==0x32) && (g_esp8266_rx_buf[i+1]==0x22))        {                      //判断控制变量            if( g_esp8266_rx_buf[i+3]=='1' )              PFout(10)=0;//控制灯亮            else              PFout(10)=1;//控制灯灭        }​        //判断的关键字符是否为 3"        //核心数据,即{"switch_led_b":1}中的"1"        if(g_esp8266_rx_buf[i]==0x33 && g_esp8266_rx_buf[i+1]==0x22)        {            //判断控制变量            if( g_esp8266_rx_buf[i+3]=='1' )              PEout(13)=0;//控制灯亮            else              PEout(13)=1;//控制灯灭        }              }      #endif      //清空接收缓冲区、接收计数值、接收结束标志位      memset((void *)g_esp8266_rx_buf,0,sizeof g_esp8266_rx_buf);      g_esp8266_rx_cnt=0;      g_esp8266_rx_end=0;    }        delay_1ms_cnt++;    delay_ms(1);        //6秒时间到达    if((delay_1ms_cnt % 6000) ==0)    {        if(0 == dht11_read(buf))      {        g_temp = (float)buf[2]+(float)buf[3]/10;        g_humi = (float)buf[0]+(float)buf[1]/10;      }            //上报设备状态      mqtt_report_devices_status();      }        //60秒时间到达    if((delay_1ms_cnt % 60000) ==0)    {      /*  设备端在保活时间间隔内(保护时间在mqtt_connect设置为60s),至少需要发送一次报文,包括ping请求。        连接保活时间的取值范围为30秒~1200秒。建议取值300秒以上。        从物联网平台发送CONNACK响应CONNECT消息时,开始心跳计时。收到PUBLISH、SUBSCRIBE、PING或 PUBACK消息时,会重置计时器。      */      //发送心跳包,过于频繁发送心跳包,服务器将会持续一段时间不发送响应信息[可选]      rt = mqtt_send_heart();            if(rt == 0)        CONNECT_MQTT_LED(1);      else         CONNECT_MQTT_LED(0);          }            //按键检测    if(key_sta_get())    {      delay_ms(50);            key_sta=key_sta_get();            if(key_sta & 0x01)      {        printf("connect aliyun mqtt\r\n");                //重连阿里云物联网平台        rt = esp8266_mqtt_init();                if(rt == 0)          CONNECT_MQTT_LED(1);        else           CONNECT_MQTT_LED(0);      }​      if(key_sta & 0x02)              {        printf("disconnect aliyun mqtt\r\n");                //断开阿里云物联网平台        mqtt_disconnect();        CONNECT_MQTT_LED(0);      }            }  }​}​​​​

总结

>>>

  1. 选择MQTT库:可以使用像Eclipse Paho、Mosquitto等开源MQTT库,针对嵌入式开发的有很多,诸如MQTT-C、mqtt_client等。

  2. 设置开发环境:根据你的STM32开发板,设置好开发环境,比如STM32CubeIDE或Keil等。

  3. 网络连接:确保你的STM32能够通过Wi-Fi、以太网等方式连接到互联网。通常会用到ESP8266、ESP32等Wi-Fi模块或以太网模块(如W5500)。

  4. 添加MQTT库:将选定的MQTT库添加到你的STM32项目中。如果是使用HAL库的项目,记得根据库的要求配置时钟、GPIO等。

  5. 实现MQTT客户端

    • 初始化网络和MQTT库。

    • 连接到MQTT代理(Broker)。

    • 订阅主题,发布消息。

    • 处理接收到的消息。

故我在

点击下方卡片 关注我

↓↓↓

相关推荐
xiaozhiwise16 分钟前
Makefile 之 自动化变量
linux
Yang.991 小时前
基于Windows系统用C++做一个点名工具
c++·windows·sql·visual studio code·sqlite3
熬夜学编程的小王1 小时前
【初阶数据结构篇】双向链表的实现(赋源码)
数据结构·c++·链表·双向链表
zz40_2 小时前
C++自己写类 和 运算符重载函数
c++
六月的翅膀2 小时前
C++:实例访问静态成员函数和类访问静态成员函数有什么区别
开发语言·c++
liujjjiyun2 小时前
小R的随机播放顺序
数据结构·c++·算法
¥ 多多¥2 小时前
c++中mystring运算符重载
开发语言·c++·算法
意疏3 小时前
【Linux 篇】Docker 的容器之海与镜像之岛:于 Linux 系统内探索容器化的奇妙航行
linux·docker
BLEACH-heiqiyihu3 小时前
RedHat7—Linux中kickstart自动安装脚本制作
linux·运维·服务器