ESP32之阿里云IoT物模型通信(MQTT-TLS连接通信),基于VSCode环境下的ESP-IDF开发(附源码)

背景知识:

本实验意在实现 ESP32 和阿里云物联网平台之间的通信,需要了解通信的建立需要选择通信协议和数据格式。通信协议我们选择MQTT协议,而数据格式有两种:ICA标准数据格式(Alink JSON)和透传/自定义,两者二选一,这里我们选择的是Alink JSON方式。

实验现象:设备按键按下则点亮/熄灭LED,并将亮灯的设备属性消息上报给阿里云物联网平台;同时阿里云物联网平台的按钮按下也会下发一条设置设备属性的消息给设备,设备收到消息则点亮/熄灭LED。

目录

背景知识:

1.创建产品和设备

[1.1 创建产品和设备](#1.1 创建产品和设备)

[1.2 为自定义产品添加一个标准灯功能](#1.2 为自定义产品添加一个标准灯功能)

2.准备工作

[2.1 获取基础工程](#2.1 获取基础工程)

[2.2 基本知识概述](#2.2 基本知识概述)

[2.2.1 属性主题和数据格式](#2.2.1 属性主题和数据格式)

(1)设备上报属性

[①请求主题(设备 -> 阿里云):](#①请求主题(设备 -> 阿里云):)

②响应主题(阿里云->设备):

(2)设置设备属性

[①请求主题(阿里云 -> 设备):](#①请求主题(阿里云 -> 设备):)

[② 响应主题(设备 -> 阿里云):](#② 响应主题(设备 -> 阿里云):)

3.编写、修改代码

[3.1 准备工作](#3.1 准备工作)

[3.1.1 确定Topic](#3.1.1 确定Topic)

[3.1.2 创建物模型结构体类型](#3.1.2 创建物模型结构体类型)

[3.2 编写上行函数(设备->阿里云)](#3.2 编写上行函数(设备->阿里云))

[3.2.1 设备上报属性请求函数](#3.2.1 设备上报属性请求函数)

(1)创建物模型描述结构体

(2)设置属性值

(3)将生成cJSON的对象转换为字符串

(4)发布属性消息

(5)释放物模型描述结构体

[3.2.2 设置设备属性响应函数](#3.2.2 设置设备属性响应函数)

[3.3 编写下行函数(阿里云->设备)](#3.3 编写下行函数(阿里云->设备))

[3.3.1 订阅主题](#3.3.1 订阅主题)

[3.3.2 解析收到的cJSON格式数据](#3.3.2 解析收到的cJSON格式数据)

[3.3.3 cJSON完整代码](#3.3.3 cJSON完整代码)

[3.4 调用设备属性上报函数](#3.4 调用设备属性上报函数)

[3.4.1 移植灯键驱动程序](#3.4.1 移植灯键驱动程序)

[3.4.2 调用设备属性上报函数](#3.4.2 调用设备属性上报函数)

[3.4.3 灯键驱动完整代码](#3.4.3 灯键驱动完整代码)

4.验证功能

[4.1 验证设备属性上报(上行)](#4.1 验证设备属性上报(上行))

[4.2 验证设置设备属性(下行)](#4.2 验证设置设备属性(下行))

5.注意事项

6.参考文档

7.源码下载


1.创建产品和设备

1.1 创建产品和设备

参考ESP32接入阿里云物联网平台(MQTT-TLS连接通信),基于VSCode环境下的ESP-IDF开发(附源码)-CSDN博客

1.2 为自定义产品添加一个标准灯功能

此时自定义产品sdp1中已经有标准灯功能:

2.准备工作

2.1 获取基础工程

https://download.csdn.net/download/Freddy_Ssc/90622560下载基础工程

2.2 基本知识概述

本节知识均来自对阿里云物联网平台Alink协议自主开发的设备属性、事件、服务的总结。

2.2.1 属性主题和数据格式

(1)设备上报属性
①请求主题(设备 -> 阿里云):

/sys/{productKey}/{deviceName}/thing/event/property/post

数据格式示例:

{

"id": "123",//每条消息的唯一标识符,用于标识是哪条消息的回复

"version": "1.0", //协议版本,当前只能取值1.0

"params": {

"LightSwitch": 0 //开关状态 0:关 1:开

},

"method": "thing.event.property.post" //属性上报

}

②响应主题(阿里云->设备):

/sys/{productKey}/{deviceName}/thing/event/property/post_reply

数据格式示例:

  • 成功返回示例:

{

"code": 200,

"data": {},

"id": "123",

"message": "success",

"method": "thing.event.property.post",

"version": "1.0"

}

  • 失败返回示例:

{

"code": 6813,

"data": {},

"id": "123",

"message": "The format of result is error!",

"method": "thing.event.property.post",

"version": "1.0"

}

(2)设置设备属性
①请求主题(阿里云 -> 设备):

sys/{productKey}/{deviceName}/thing/service/property/set

数据格式示例:

{

"id": "123",

"version": "1.0",

"params": {

"LightSwitch":1

},

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

}

② 响应主题(设备 -> 阿里云):

/sys/{productKey}/{deviceName}/thing/service/property/set_reply

数据格式示例:

  • 成功返回示例:

{

"code": 200,

"data": {},

"id": "123",

"message": "success",

"version": "1.0"

}

  • 失败返回示例:

{

"code": 9201,

"data": {},

"id": "123",

"message": "device offLine",

"version": "1.0"

}

3.编写、修改代码

3.1 准备工作

先创建aliot_dm.c和aliot_dm.h两个文件,用于生成cJSON格式的物模型函数。

3.1.1 确定Topic

分别将设备上报属性的请求Topic、响应Topic、设置设备属性的请求Topic、响应Topic分别宏定义在mqtt_aliot.h中。

复制代码
//MQTT Topic
#define ALIOT_MQTT_TOPIC_POST       "/sys/"ALIOT_PRODUCTKEY"/"ALIOT_DEVICENAME"/thing/event/property/post"          //上报属性
#define ALIOT_MQTT_TOPIC_POST_REPLY "/sys/"ALIOT_PRODUCTKEY"/"ALIOT_DEVICENAME"/thing/event/property/post_reply"    //下发回复属性
#define ALIOT_MQTT_TOPIC_SET        "/sys/"ALIOT_PRODUCTKEY"/"ALIOT_DEVICENAME"/thing/event/property/set"           //下发设置属性
#define ALIOT_MQTT_TOPIC_SET_REPLY  "/sys/"ALIOT_PRODUCTKEY"/"ALIOT_DEVICENAME"/thing/event/property/set_reply"     //上报回复设置属性

3.1.2 创建物模型结构体类型

复制代码
typedef struct 
{
    cJSON* dm_js; //以cJSON格式存放
    char* dm_js_str; //以字符串格式存放
}ALIOT_DM_DES; //物模型描述结构体

3.2 编写上行函数(设备->阿里云)

3.2.1 设备上报属性请求函数

函数框架如下:

(1)创建物模型描述结构体

先创建物模型描述结构体,创建cJSON对象,然后添加消息唯一标志符对象和固定对象:

(2)设置属性值

即向cJSON中添加变化的属性对象:

(3)将生成cJSON的对象转换为字符串
(4)发布属性消息

调用 esp_mqtt_client_publish 函数,发布属性消息:

复制代码
esp_mqtt_client_publish(mqtt_handle, ALIOT_MQTT_TOPIC_POST, dm->dm_js_str, strlen(dm->dm_js_str), 1, 0);
(5)释放物模型描述结构体

3.2.2 设置设备属性响应函数

函数框架如下:

具体实现略,详情请参考3.2.1。

3.3 编写下行函数(阿里云->设备)

3.3.1 订阅主题

如果需要接收阿里云物联网平台下发的信息,则需要先订阅响应主题。需要接收哪个主题的数据就订阅哪个主题,因此我们需要订阅设备上报属性响应主题和设置设备属性请求主题。

在MQTT事件回调函数中当设备连接MQTT服务器后,订阅主题:

3.3.2 解析收到的cJSON格式数据

在MQTT事件回调函数中当收到MQTT数据事件后,解析cJSON格式数据,如果是设置设备属性请求,则点亮/熄灭LED灯,并回复一个设置设备属性响应:

3.3.3 cJSON完整代码

aliot_dm.h完整代码:

复制代码
#ifndef _ALIOT_DM_H_
#define _ALIOT_DM_H_
#include "cJSON.h"

typedef enum
{
    ALIOT_DM_POST,
    ALIOT_DM_SET_ACK,
}ALIOT_DM_TYPE;

typedef struct 
{
    cJSON* dm_js; //以cJSON格式存放
    char* dm_js_str; //以字符串格式存放
}ALIOT_DM_DES; //物模型描述结构体


//创建物模型描述结构体
ALIOT_DM_DES *aliot_malloc_des(ALIOT_DM_TYPE type);

//往物模型结构体里面添加属性值
void aliot_set_dm_int(ALIOT_DM_DES *dm, const char *name, int value);

//属性回复
void aliot_set_dm_property_ack(ALIOT_DM_DES *dm, int code, const char *message);

//生成cJSON字符串,保存在dm_js_str
void aliot_dm_serialize(ALIOT_DM_DES *dm);

//释放物模型描述结构体
void aliot_dm_free(ALIOT_DM_DES *dm);

/* 上报一个整形数据 */
void aliot_post_property_int(const char* name, int value);

//上报属性回复
void aliot_property_ack(int code, const char *message);

#endif

aliot_dm.c完整代码:

复制代码
#include "aliot_dm.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include "mqtt_aliot.h"
#include "esp_log.h"

#define TAG     "aliot_dm"

static int s_aliot_id = 0;
/** 
 * 创建物模型描述结构体
 * 主题和数据格式
    设备上报属性请求Topic:/sys/${productKey}/${deviceName}/thing/event/property/post
    {
        "id": "123",            //变化
        "version": "1.0",       //固定
        "params": {             //固定
            "LightSwitch": 0    //变化:0关闭,1打开
        },
        "method": "thing.event.property.post" //固定
    }
    设置设备属性响应Topic:/sys/${productKey}/${deviceName}/thing/service/property/set_reply
    {
        "code": 200, //变化:成功200,失败9201
        "data": {},  //固定
        "id": "123", //变化
        "message": "success", //变化:成功success,失败device offLine
        "version": "1.0" //固定
    }
**/
ALIOT_DM_DES *aliot_malloc_des(ALIOT_DM_TYPE type)
{
    ALIOT_DM_DES* dm = (ALIOT_DM_DES *)malloc(sizeof(ALIOT_DM_DES));
    if (dm)
    {
        memset(dm, 0, sizeof(ALIOT_DM_DES));
        dm->dm_js = cJSON_CreateObject();//创建jSON对象
        char id[10];
        snprintf(id, sizeof(id), "%d", s_aliot_id++);//数字转换成字符串
        cJSON_AddStringToObject(dm->dm_js, "id", id);//添加键值对id
        
        switch(type)
        {
            case ALIOT_DM_POST:
                cJSON_AddStringToObject(dm->dm_js, "version", "1.0");//添加键值对version
                cJSON_AddStringToObject(dm->dm_js, "method", "thing.event.property.post");//添加键值对method
                cJSON_AddObjectToObject(dm->dm_js, "params");//添加对象params
                break;
            case ALIOT_DM_SET_ACK:
                cJSON_AddStringToObject(dm->dm_js, "version", "1.0");//添加键值对version
                cJSON_AddObjectToObject(dm->dm_js, "data");//添加对象
                break;
        }
        return dm;
    }
    return NULL;
}

//设置物模型结构体里面的属性值
void aliot_set_dm_int(ALIOT_DM_DES *dm, const char *name, int value)
{
    if (dm)
    {
        cJSON* params_js = cJSON_GetObjectItem(dm->dm_js, "params");//获取对象params
        if (params_js)
        {
            cJSON_AddNumberToObject(params_js, name, value);//向params对象中添加属性值
        }
    }
}

//属性回复
void aliot_set_dm_property_ack(ALIOT_DM_DES *dm, int code, const char *message)
{
    if (dm)
    {
        cJSON_AddNumberToObject(dm->dm_js, "code", code);//向对象中添加属性值
        cJSON_AddStringToObject(dm->dm_js, "message", message);//向对象中添加属性值
    }
}

//生成cJSON字符串,保存在dm_js_str
void aliot_dm_serialize(ALIOT_DM_DES *dm)
{
    if (dm)
    {
        if (dm->dm_js_str)//判断此字符串是否有效,有则清空
        {
            cJSON_free(dm->dm_js_str);//释放
            dm->dm_js_str = NULL;//清零
        }
        dm->dm_js_str = cJSON_PrintUnformatted(dm->dm_js);//把cJSON对象转换成字符串
    }
}

//释放物模型描述结构体
void aliot_dm_free(ALIOT_DM_DES *dm)
{
    if (dm)
    {
        if (dm->dm_js_str)//判断此字符串是否有效,有则清空
        {
            cJSON_free(dm->dm_js_str);//释放
            dm->dm_js_str = NULL;//清零
        }
        if (dm->dm_js)
        {
            cJSON_Delete(dm->dm_js);//删除cJSON对象
            dm->dm_js = NULL;//清零
        }
        free(dm);//释放物模型描述结构体
    }
}

//设备上报属性请求(上报一个整形数据 )
void aliot_post_property_int(const char *name, int value)
{
    //创建物模型描述结构体
    ALIOT_DM_DES *dm = aliot_malloc_des(ALIOT_DM_POST);

    //设置物模型结构体里面的属性值
    aliot_set_dm_int(dm, name, value);

    //将生成cJSON的对象转换为字符串
    aliot_dm_serialize(dm);

    //发布属性消息
    ESP_LOGI(TAG, "publish payload:%s", dm->dm_js_str);
    esp_mqtt_client_publish(mqtt_handle, ALIOT_MQTT_TOPIC_POST, dm->dm_js_str, strlen(dm->dm_js_str), 1, 0);

    //释放物模型描述结构体
    aliot_dm_free(dm);
}

//设置设备属性响应
void aliot_property_ack(int code, const char *message)
{
    //创建物模型描述结构体
    ALIOT_DM_DES *dm = aliot_malloc_des(ALIOT_DM_SET_ACK);

    //设置物模型结构体里面的属性值
    aliot_set_dm_property_ack(dm, code, message);

    //生成cJSON字符串,保存在dm_js_str
    aliot_dm_serialize(dm);

    //发布属性消息
    ESP_LOGI(TAG, "publish payload:%s", dm->dm_js_str);
    esp_mqtt_client_publish(mqtt_handle, ALIOT_MQTT_TOPIC_SET_REPLY, dm->dm_js_str, strlen(dm->dm_js_str), 1, 0);

    //释放物模型描述结构体
    aliot_dm_free(dm);
}

3.4 调用设备属性上报函数

3.4.1 移植灯键驱动程序

移植一份LED和按键驱动代码,这里用我自己写的一份灯键驱动程序,直接复制粘贴过来,改一下led和button的IO口就行,移植过程不做展示。

3.4.2 调用设备属性上报函数

在按键处理函数中,当按键按下后,点亮/熄灭LED灯,同时如果已经连接了MQTT,则调用设备属性上报请求函数:

3.4.3 灯键驱动完整代码

button.h完整代码:

复制代码
#ifndef _BUTTON_H_
#define _BUTTON_H_

#ifdef __cplusplus
extern "C" {
#endif

#include "driver/gpio.h"

/* Private defines -----------------------------------------------------------*/
/* LED端口定义 */
#define PIN_LED0            GPIO_NUM_2

/* BUTTON端口定义 */
#define PIN_BUTTON0         GPIO_NUM_14
#define READ_BUTTON0        gpio_get_level(PIN_BUTTON0)     /* 读取BUTTON0引脚 */


typedef enum
{
    EVENT_KEY_0_NOTHING = 0,
    
    EVENT_KEY_0_PRESS,
    EVENT_KEY_0_CLICK,
    EVENT_KEY_0_LONG_PRESS,
    EVENT_KEY_0_LONG_PRESS_RELEASE,
    
    EVENT_KEY_MAX,
}enButtonEventType;

void led_init(void);
void button_init(void (*button_event_callback)(enButtonEventType eve));
void button_scan(void);
void button_event_handle(enButtonEventType eve);
void button_event_distribution(void);
void button_task(void *pvParameters);

/* USER CODE END Prototypes */

#ifdef __cplusplus
}
#endif

#endif

button.h完整代码:

复制代码
#include "button.h"
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "esp_log.h"
#include "aliot_dm.h"
#include "mqtt_aliot.h"

#define TAG "button"

#define GPIO_PinState   bool

void (*button_event_callback)(enButtonEventType eve);

void led_init(void)
{
    gpio_config_t io_conf = {};//zero-initialize the config structure.
    
    io_conf.intr_type = GPIO_INTR_DISABLE;//disable interrupt
    io_conf.mode = GPIO_MODE_OUTPUT;//set as output mode
    io_conf.pin_bit_mask = BIT64(PIN_LED0);//bit mask of the pins that you want to set,e.g.GPIO18
    io_conf.pull_down_en = 0;//disable pull-down mode
    io_conf.pull_up_en = 0;//disable pull-up mode
    gpio_config(&io_conf);//configure GPIO with the given settings

    gpio_set_level(PIN_LED0, 0);
}

void button_init(void (*button_event_call_back)(enButtonEventType eve))
{
    gpio_config_t io_conf = {};//zero-initialize the config structure.
    
    io_conf.intr_type = GPIO_INTR_DISABLE;//disable interrupt
    io_conf.mode = GPIO_MODE_INPUT;
    io_conf.pin_bit_mask = BIT64(PIN_BUTTON0);
    io_conf.pull_down_en = 0;
    io_conf.pull_up_en = 1;
    gpio_config(&io_conf);

    button_event_callback = button_event_call_back;
}

/** 
*   按键值读取函数
*/
GPIO_PinState read_button_0_state(void)
{
    return READ_BUTTON0;
}

typedef enum
{
    BUTTON_IDLE = 0,
    BUTTON_PRESS,
    BUTTON_CLICK,
    BUTTON_LONG_PRESS,
    BUTTON_LONG_PRESS_RESEASE,
}enButtonStatusType;

typedef enum
{
    BUTTON_0 = 0,
    BUTTON_NUM
}enButtonType;

typedef struct
{
    GPIO_PinState (*read_button_state)(void); //读取键值函数
    GPIO_PinState active_level; //有效电平
    enButtonStatusType status; //按键状态
    uint16_t counter; //按键按下检测计数器
    uint8_t pressDone; //标志位,其中 bit0 和 bit1 分别用于处理 BUTTON_PRESS 和 BUTTON_LONG_PRESS 事件
}strButtonType;
strButtonType strButton[BUTTON_NUM] = 
{
    {read_button_0_state, 0, BUTTON_IDLE, 0, 0},
};

enButtonEventType strButtonEve[BUTTON_NUM][4] = 
{
    {EVENT_KEY_0_PRESS, EVENT_KEY_0_CLICK, EVENT_KEY_0_LONG_PRESS, EVENT_KEY_0_LONG_PRESS_RELEASE},
};

#define BUTTON_COUNT_PRESS         5       //单位:10ms
#define BUTTON_COUNT_LONG_PRESS    100     //单位:10ms

/** 
*   按键扫描函数  
*   提示:建议每 10ms 调用一次,否则需要修改 TIM_COUNT_CLICK 和 TIM_COUNT_LONG_PRESS 的值
*/
void button_scan(void)
{
    for (uint8_t i = 0; i < BUTTON_NUM; i++)
    {
        if (strButton[i].read_button_state() == strButton[i].active_level) //button pressed
        {
            strButton[i].counter++;
            
            if (strButton[i].counter == BUTTON_COUNT_LONG_PRESS)
            {
                strButton[i].status = BUTTON_LONG_PRESS;
            }
            else if (strButton[i].counter == BUTTON_COUNT_PRESS)
            {
                if (strButton[i].status == BUTTON_IDLE)
                {
                    strButton[i].status = BUTTON_PRESS;
                }
            }
        }
        else //button released
        {
            strButton[i].counter = 0;
            if (strButton[i].status == BUTTON_PRESS)
            {
                strButton[i].status = BUTTON_CLICK;
            }
            else if (strButton[i].status == BUTTON_LONG_PRESS)
            {
                strButton[i].status = BUTTON_LONG_PRESS_RESEASE;
            }
        }
    }
}

/** 
*   按键事件派发函数  
*   提示:此函数为过度函数,不建议修改
*/
void button_event_distribution(void)
{
    for (uint8_t i = 0; i < BUTTON_NUM; i++)
    {
        switch (strButton[i].status)
        {
            case BUTTON_PRESS:
            {
                if ((strButton[i].pressDone & 0x01) == 0)
                {
                    button_event_callback(strButtonEve[i][strButton[i].status - 1]);
                    
                    strButton[i].pressDone |= 0x01;
                }
            }break;
            case BUTTON_CLICK:
            {
                button_event_callback(strButtonEve[i][strButton[i].status - 1]);
                
                strButton[i].pressDone &= ~(0x01 << 0);
                strButton[i].status = BUTTON_IDLE;
            }break;
            case BUTTON_LONG_PRESS:
            {
                if ((strButton[i].pressDone & 0x02 )== 0)
                {
                    button_event_callback(strButtonEve[i][strButton[i].status - 1]);
                    
                    strButton[i].pressDone |= 0x02;
                }
            }break;
            case BUTTON_LONG_PRESS_RESEASE:
            {
                button_event_callback(strButtonEve[i][strButton[i].status - 1]);
                
                strButton[i].pressDone = 0;
                strButton[i].status = BUTTON_IDLE;
            }break;
            default: break;
        }
    }
}

/** 
*   按键事件处理函数  
*   提示:在此函数中增加需要的功能
*/
void button_event_handle(enButtonEventType eve)
{
    static int led0_level = 0;
    switch (eve)
    {
        case EVENT_KEY_0_CLICK:
        {
            ESP_LOGI(TAG, "Button0 clickd.");
            if (led0_level)
                led0_level = 0;
            else 
                led0_level = 1;

            gpio_set_level(PIN_LED0, led0_level); //点亮/熄灭LED
            if (is_mqtt_connected()) //如果MQTT已经连接了,则发送一个设备上报属性请求
            {
                aliot_post_property_int("LightSwitch", led0_level);
            }
        }break;
        
        default:break;
    }
}

void button_task(void *pvParameters)
{
    button_init(button_event_handle);//初始化按键、并注册按键事件的回调任务
    while(1)
    {
        button_scan();//按键扫描
        button_event_distribution();//按键事件派发
        vTaskDelay(10 / portTICK_PERIOD_MS);

        // // 检查栈剩余空间
        // uint32_t free_stack_size = uxTaskGetStackHighWaterMark(NULL);
        // ESP_LOGI(TAG, "Free stack size: %ld bytes", free_stack_size);
    }
}

4.验证功能

4.1 验证设备属性上报(上行)

按下按键前,LightSwitch显示无状态:

按下按键后,LightSwitch显示开启状态,说明数据上报成功:

再来看看上报的数据,发现数据和我们发送的一致:

最后再看看ESP32的打印日志:

4.2 验证设置设备属性(下行)

测试前,我们先再按一次按键,让LightSwitch变成关闭状态:

接着,按下面操作设置开关状态:

测试后,LightSwitch显示打开状态,同时ESP32设备的LED也被点亮了:

5.注意事项

暂无。

6.参考文档

物模型属性、事件、服务的Alink JSON数据格式和Topic_物联网平台(IoT)-阿里云帮助中心

【【2024最新版 ESP32教程(基于ESP-IDF)】ESP32入门级开发课程 更新中 中文字幕】

7.源码下载

https://download.csdn.net/download/Freddy_Ssc/90628093?spm=1001.2014.3001.5503

相关推荐
阿运河6 小时前
如何配置 VScode 断点调试Linux 工程代码
linux·ide·vscode
BXCQ_xuan8 小时前
阿里云CDN的源站配置:权重的详解
阿里云·云计算
k↑11 小时前
物联网之使用Vertx实现MQTT-Server最佳实践【响应式】
物联网·mqtt·微服务·响应式
通信大模型13 小时前
基于注意力机制的无人机轨迹优化方法:面向无线能量传输的物联网系统
人工智能·深度学习·物联网·无人机·信息与通信
LXL_2414 小时前
如何安装不同版本的ESP-IDF,并配置Vscode插件,以及在Vscode中切换版本
ide·vscode·编辑器
kaiyuanheshang17 小时前
关于VScode的调试
ide·vscode·编辑器·debug·调试
Austindatabases17 小时前
给阿里云MongoDB 的感谢信 !!成本降低80%
数据库·mongodb·阿里云·云计算
三天不学习21 小时前
Visual Studio Code 前端项目开发规范合集【推荐插件】
前端·ide·vscode
是大糊涂不聪明1 天前
VSCode远程无法选择虚拟环境问题
ide·vscode·编辑器
qq_427649061 天前
VScode密钥(公钥,私钥)实现免密登录【很细,很全,附带一些没免密登录成功的一些解决方法】
ide·vscode·ssh·密钥·免密登录·公钥·私钥