AT 工程:https://github.com/espressif/esp-at
AT 文档:https://docs.espressif.com/projects/esp-at/zh_CN/latest/

一. 简介

虽然 ESP-AT 内部已经集成了很多指令, 比如 Wi-Fi, BT, BLE, IP 等等, 但是同时也支持客户进行二次开发, 定义客户自己的命令.

本文主要介绍客户如何自定义 AT 命令.

1.1 ESP-AT 命令的四种格式

ESP-AT 命令包含 4 种命令格式:

  • Test Command

    • 示例: AT+<x>=?
    • 用途: 查询 Set Command 的各个参数以及参数的范围
  • Query Command
    • 示例: AT+<x>?
    • 用途: 查询命令, 可以返回当前参数的值, 也可以返回其他一些想要得到的信息
  • Set Command
    • 示例: AT+<x>=<…>
    • 用途: 设置命令, 向AT输入一些参数, 执行相应的操作
  • Execute Command
    • 示例: AT+<x>
    • 用途: 执行指令, 该指令不带参数

1.2 如何开始自定义一组 AT 命令

首先, 我们看一下 ESP-AT 命令结构体的定义:

typedef struct {char *at_cmdName;                               /*!< at command name */uint8_t (*at_testCmd)(uint8_t *cmd_name);       /*!< Test Command function pointer */uint8_t (*at_queryCmd)(uint8_t *cmd_name);      /*!< Query Command function pointer */uint8_t (*at_setupCmd)(uint8_t para_num);       /*!< Setup Command function pointer */uint8_t (*at_exeCmd)(uint8_t *cmd_name);        /*!< Execute Command function pointer */
} esp_at_cmd_struct;

这个结构体中包含 5 个元素, 第一个是个字符串指针, 是 AT 命令的名字, AT 命令的名字有一定的格式要求, 都是+开始. 后面跟着四个函数参数指针, 分别对应上面提到的四种命令.

现在我们首先举个简单的例子, 定义一个命令, 用来输出Hello word.

定义命令:

static esp_at_cmd_struct at_example_cmd[] = {{"+EXAMPLE", NULL, NULL, NULL, NULL},
};

这样, 这条命令就定义好了, 命令的名字叫 +EXAMPLE, 实际用起来的时候,用户输入的命令就是这个样子:

  • AT+EXAMPLE=?
  • AT+EXAMPLE?
  • AT+EXAMPLE=<param_1>,<param_2>,<param_3>…
  • AT+EXAMPLE

当然, 仅仅这样, 还是远远不够的, 要想真的用起来, 至少还少 2 个步骤, 首先就是注册这组命令, 其次就是添加命令的具体实现.

注册命令:

注册自定义命令数组需要用 API:

bool esp_at_custom_cmd_array_regist(const esp_at_cmd_struct *custom_at_cmd_array, uint32_t cmd_num);

注册客户自定义命令的代码需要加到 app_main() 里, 建议放在 app_main() 的最后 at_custom_init(); 之前, 参考代码如下:

bool esp_at_example_cmd_regist(void)
{return esp_at_custom_cmd_array_regist(at_example_cmd, sizeof(at_example_cmd) / sizeof(at_example_cmd[0]));
}void app_main()
{...if(esp_at_example_cmd_regist() == false) {printf("regist example cmd fail\r\n");}at_custom_init();
}

添加命令的具体实现:

刚次才我们定义的命令数组里, 四个回调函数都是 NULL, 其实还是什么都做不了的, 我们现在添加个实例函数, 来输出 Hello World.

因为不需要带参数, 我们就用执行命令来实现吧, 示例代码如下:

uint8_t at_exeCmdExample(uint8_t *cmd_name)
{esp_at_port_write_data("Hello World", strlen("Hello World"));return ESP_AT_RESULT_CODE_OK;
}

同时修改命令数组如下:

static esp_at_cmd_struct at_example_cmd[] = {{"+EXAMPLE", NULL, NULL, NULL, at_exeCmdExample},
};

如果想同时打印这条命令的名字, 可以将 cmd_name 也打印出来, 例如:

uint8_t at_exeCmdExample(uint8_t *cmd_name)
{esp_at_port_write_data((uint8_t *)cmd_name, strlen((char *)cmd_name));esp_at_port_write_data((uint8_t *)":Hello World\r\n",strlen(":Hello World"));return ESP_AT_RESULT_CODE_OK;
}

此时在终端打印信息是这样的:

AT+EXAMPLE
+EXAMPLE:Hello WordOK

如何添加多个命令

上面的例子只有一个命令, 如果需要多个命令, 可以依次添加, 例如:

static esp_at_cmd_struct at_example_cmd[] = {{"+EXAMPLE1", NULL, NULL, NULL, NULL},{"+EXAMPLE2", NULL, NULL, NULL, NULL},{"+EXAMPLE3", NULL, NULL, NULL, NULL},{"+EXAMPLE4", NULL, NULL, NULL, NULL},{"+EXAMPLE5", NULL, NULL, NULL, NULL},
};

二. ESP-AT 自定义命令进阶

2.1 命令实现中, 不同返回值的区别

在上面的 Hello Word 示例中, 我们在 at_exeCmdExample() 最后返回了 ESP_AT_RESULT_CODE_OK, 这个返回值的作用就是命令执行完毕之后, 输出字符 "OK".

ESP-AT 中, 返回值不止这一个, 而且每个效果都不同, 我们先看一下一共有哪些返回值

/*** @brief the result code of AT command processing**/
typedef enum {ESP_AT_RESULT_CODE_OK           = 0x00,       /*!< "OK" */ESP_AT_RESULT_CODE_ERROR        = 0x01,       /*!< "ERROR" */ESP_AT_RESULT_CODE_FAIL         = 0x02,       /*!< "ERROR" */ESP_AT_RESULT_CODE_SEND_OK      = 0x03,       /*!< "SEND OK" */ESP_AT_RESULT_CODE_SEND_FAIL    = 0x04,       /*!< "SEND FAIL" */ESP_AT_RESULT_CODE_IGNORE       = 0x05,       /*!< response nothing */ESP_AT_RESULT_CODE_PROCESS_DONE = 0x06,       /*!< response nothing */ESP_AT_RESULT_CODE_MAX
} esp_at_result_code_string_index;

从后面的注释能够看出, 不同的返回值可以输出 "OK", "ERROR", "SEND OK", "SEND FAIL", 或者处理结果的应答都没有.

前面 3 个应该很好理解, 如果顺利执行完毕, 那么就返回 ESP_AT_RESULT_CODE_OK, 最终在串口上面会输出 "OK", 如果返回的是 ESP_AT_RESULT_CODE_ERROR,ESP_AT_RESULT_CODE_FAIL, 那么最终会在串口上面输出 "ERROR"

ESP_AT_RESULT_CODE_SEND_OKESP_AT_RESULT_CODE_SEND_FAIL可以用在这样的一个场景, 比如基于TCP 连接发送一包数据, 这个时候发送失败了, 可以 return ESP_AT_RESULT_CODE_SEND_FAIL;, 如果发送成功了, 可以 return ESP_AT_RESULT_CODE_SEND_OK;

除了通过 return 返回值的方式, 向串口输出指令执行的结果, 我们还可以用另外一种方式来做:

/*** @brief response AT process result,** @param result_code see esp_at_result_code_string_index**/
void esp_at_response_result(uint8_t result_code);

假如您现在向服务器发送了一串数据, 然后您想先打印 "SEND OK", 然后等待服务器回应, 最后再退出函数, 您可以这样做:

uint8_t at_exeCmdExample(uint8_t *cmd_name)
{//send data to ServerSend...// 先打印 SEND OKesp_at_response_result(ESP_AT_RESULT_CODE_OK);// 等待服务器回应, 具体的阻塞实现在下面会介绍wait...//最后返回 OK //如果您在这里不想再输出 OK 了, 可以 return ESP_AT_RESULT_CODE_PROCESS_DONE;return ESP_AT_RESULT_CODE_OK;
}

ESP_AT_RESULT_CODE_IGNOREESP_AT_RESULT_CODE_PROCESS_DONE 的区别就是:

ESP_AT_RESULT_CODE_IGNORE 不输出命令执行结果, 也不会有状态的切换, 仍处于处理当前命令的状态. 这个时候输入下一条命令, 会返回 busy.

ESP_AT_RESULT_CODE_PROCESS_DONE 不输出命令执行结果, 但会将当前的状态切换到空闲状态, 可以处理下一条命令

2.2 如何获取命令的参数

上一节提到的设置命令, 该命令是需要带一些参数的, 参数可能不止一个, 类型也不尽相同, 有的是整形, 有的是字符串, 该怎么在回调函数中处理这些参数呢?我们还是举一个例子, 假如我们现在要去连接一个 TCP Server, 那么它的参数至少有两个, IP 地址和端口号, 我们约定他的命令是这个样子的:

AT+TCP=<IP>,<port>

其中, IP 地址是字符串, port 是数字.

我们可以定义命令数组如下:

static esp_at_cmd_struct at_example_cmd[] = {{"+TCP", NULL, NULL, at_setupCmdTcp, NULL},
};

设置命令的具体实现如下:

uint8_t at_setupCmdTcp(uint8_t para_num)
{int32_t cnt = 0, value = 0;uint8_t* s = NULL;// 首先获取ip地址, 这是一个字符串, 如果失败, 会返回 ERRORif (esp_at_get_para_as_str(cnt++, &s) != ESP_AT_PARA_PARSE_RESULT_OK) {return ESP_AT_RESULT_CODE_ERROR;}// 然后获取端口号, 同理, 如果失败, 也会返回错误if (esp_at_get_para_as_digit(cnt++, &value) != ESP_AT_PARA_PARSE_RESULT_OK) {return ESP_AT_RESULT_CODE_ERROR;}// 最后再检查下参数个数, para_num 是用户输入的这条命令的参数个数, 如果不等于 2, 也可以报错if (para_num != cnt) {return ESP_AT_RESULT_CODE_ERROR;}// 下面就可以加入用户自己的处理代码了// TODO: return ESP_AT_RESULT_CODE_OK;
}

这里需要关注这样的几点:

  • param_num 是用户实际输入的参数个数, 每个参数之间是以,隔开
  • esp_at_para_parse_result_type esp_at_get_para_as_digit(int32_t para_index, int32_t *value);用于获取整形数据
  • esp_at_para_parse_result_type esp_at_get_para_as_str(int32_t para_index, uint8_t **result);用于获取字符串参数

2.3 如何处理可选参数

有的时候, 可能有些参数是可选, 也就是可变参数.

这个时候涉及到两种情况, 一种省略的是第一个或者中间的参数, 另一种是省略最后的部分, 省略第一个参数和省略中间参数的处理方式相同.

2.3.1 省略的是中间的参数

这种情况, 命令的定义一般是这样的:

AT+TESTCMD=<parm_1>[,<param_2>],<param_3>

约定中间的参数 param_2 可以省略, 它的类型是整形, 另外两个参数 param_1 是整形, param_3 是字符串.

示例代码如下:

static esp_at_cmd_struct at_example_cmd[] = {{"+TESTCMD", NULL, NULL, at_setupCmdTestCmd, NULL},
};uint8_t at_setupCmdTestCmd(uint8_t para_num)
{int32_t cnt = 0, value = 0;uint8_t* s = NULL;esp_at_para_parse_result_type parse_result = ESP_AT_PARA_PARSE_RESULT_FAIL;// 首先获取第一个参数 param_1,他的类型是整形if (esp_at_get_para_as_digit(cnt++, &value) != ESP_AT_PARA_PARSE_RESULT_OK) {return ESP_AT_RESULT_CODE_ERROR;}// 这里需要注意, 需要把 value 的值赋值给另外一个变量, 因为下面的代码会把 value 的值重置成另一个参数的值, 或者下面再获取整形参数值的时候, 用另外定义的变量// param_1 = value;// 现在处理第二个可选参数, 尝试下是否能获取到parse_result = esp_at_get_para_as_digit(cnt++, &value);if (parse_result != ESP_AT_PARA_PARSE_RESULT_OMITTED) {// 能走到这里, 说明这个可选参数没有被省略// 进一步判断返回值是不是OK// 需要注意, 例子这里举得是整形参数, 如果是字符串的话// 客户输入 "" 这样的空串也是会返回OK的, 只是字符串指针是 NULLif (parse_result != ESP_AT_PARA_PARSE_RESULT_OK) {return ESP_AT_RESULT_CODE_ERROR;}// param_2 = value;} else {// 走到这里, 说明用户没有输入第二个参数// 是不是用默认值, 还是做别的处理, 取决于客户自己的逻辑}// 现在获取最后一个参数 param_3if (esp_at_get_para_as_str(cnt++, &s) != ESP_AT_PARA_PARSE_RESULT_OK) {return ESP_AT_RESULT_CODE_ERROR;}// param_3 = s;// 最后再检查下参数个数, para_num 是用户输入的这条命令的参数个数, 如果不等于3, 报错if (para_num != cnt) {return ESP_AT_RESULT_CODE_ERROR;}// 下面就可以加入用户自己的处理代码了// TODO: return ESP_AT_RESULT_CODE_OK;
}

2.3.2 省略的是最后的参数

这种情况, 命令的定义一般是这样的:

AT+TESTCMD=<parm_1>,<param_2>[,<param_3>]

约定最后的的参数 param_3 可以省略, 它的类型是整形, 另外两个参数 param_1 是整形, param_2 是字符串.

省略的形式有两种, 例如:

AT+TESTCMD=123,"abc"
AT+TESTCMD=123,"abc",

示例代码如下:

static esp_at_cmd_struct at_example_cmd[] = {{"+TESTCMD", NULL, NULL, at_setupCmdTestCmd, NULL},
};uint8_t at_setupCmdTestCmd(uint8_t para_num)
{int32_t cnt = 0, value = 0;uint8_t* s = NULL;esp_at_para_parse_result_type parse_result = ESP_AT_PARA_PARSE_RESULT_FAIL;// 首先获取第一个参数 param_1, 他的类型是整形if (esp_at_get_para_as_digit(cnt++, &value) != ESP_AT_PARA_PARSE_RESULT_OK) {return ESP_AT_RESULT_CODE_ERROR;}// 这里需要注意, 需要把 value 的值赋值给另外一个变量, 因为下面的代码会把 value 的值重置成另一个参数的值, 或者下面再获取整形参数值的时候, 用另外定义的变量//param_1 = value;// 现在处理第二个参数if (esp_at_get_para_as_str(cnt++, &s) != ESP_AT_PARA_PARSE_RESULT_OK){return ESP_AT_RESULT_CODE_ERROR;}// param_2 = s;if (para_num != cnt) {// 走到这里说明第三个参数可能存在, 尝试获取它, 看看是不是真的输入了parse_result = esp_at_get_para_as_digit(cnt++, &value);if (parse_result != ESP_AT_PARA_PARSE_RESULT_OMITTED) {if (parse_result != ESP_AT_PARA_PARSE_RESULT_OK) {// 第三个参数格式错误return ESP_AT_RESULT_CODE_ERROR;}// 获取到了第三个参数// param_3 = value;} else {// 说明参数还是被省略// 用户自己处理这种情况}}// 最后再检查下参数个数, para_num 是用户输入的这条命令的参数个数, 如果不等于 3, 报错if (para_num != cnt) {return ESP_AT_RESULT_CODE_ERROR;}// 下面就可以加入用户自己的处理代码了// TODO: return ESP_AT_RESULT_CODE_OK;
}

2.4 如何在命令中加入定时器进入阻塞状态

某些应用场景, 需要将命令处理过程阻塞在哪里, 然后等待结果返回, 再将命令退出, 这种情况, 我们一般可以通过信号量来处理.

这次我们用查询指令来举例, 比如想去云端查询某个状态.

示例代码如下:

static esp_at_cmd_struct at_example_cmd[] = {{"+TESTCMD", NULL, at_queryCmdTestCmd, NULL, NULL},
};static xSemaphoreHandle at_operation_sema = NULL;uint8_t at_queryCmdTestCmd(uint8_t *cmd_name)
{......assert(!at_operation_sema);at_operation_sema = xSemaphoreCreateBinary();//用户代码处理, 比如向另外一个 task 发送一个 queue, 让这个 task 做一些耗时比较久的事情, 然后 AT 这里等待结果xSemaphoreTake(at_operation_sema, portMAX_DELAY);vSemaphoreDelete(at_operation_sema);at_operation_sema = NULL;return ESP_AT_RESULT_CODE_OK;
}另一个 task 处理完可以这样处理: void user_task(void)
{...if (at_operation_sema) {xSemaphoreGive(at_operation_sema);}...
}

2.5 如何从 AT 命令 port 中截取数据

一般有两个应用场景, 第一个是截取指定长度的数据, 另一个是数据长度不指定, 类似于透传

这里需要注意这两个 API:

/*** @brief Set AT core as specific status, it will call callback if receiving data.* @param callback**/
void esp_at_port_enter_specific(esp_at_port_specific_callback_t callback);/*** @brief Exit AT core as specific status.* @param NONE**/
void esp_at_port_exit_specific(void);

一个用于设置回调函数, 一个用于删除回调函数, 在本小节里的这种应用场景中, 他的工作原理是这样的:

  • 首先设置回调函数
  • 如果 AT Port 收到数据, 会回调这个函数
  • 我们在回调函数里 Give 信号量
  • AT 命令处理代码一直在等这个信号量
  • Take 到这个信号量之后, 就可以获取到 AT port 的数据了
  • 退出的时候, 删除回调函数, 删除信号量

2.5.1 数据长度不指定

如果用户需要进入输入模式, 直接获取串口的数据, 比如进入透传状态, 我们可以这样做:

定义命令:

static esp_at_cmd_struct at_example_cmd[] = {{"+TESTCMD", NULL, NULL, NULL, at_exeCmdTestCmd},
};

具体实现如下:

static xSemaphoreHandle at_sync_sema   = NULL;static void at_wait_data_callback(void)
{xSemaphoreGive(at_sync_sema);
}#define BUFFER_LEN 2048 uint8_t at_exeCmdExample(uint8_t *cmd_name)
{int32_t temp_len = 0;uint8_t test_buf[BUFFER_LEN] = {0};......vSemaphoreCreateBinary(at_sync_sema);xSemaphoreTake(at_sync_sema, portMAX_DELAY);//打印输入提示符 '>'esp_at_port_write_data((uint8_t*)">", at_strlen(">"));esp_at_port_enter_specific(at_wait_data_callback);while (xSemaphoreTake(at_sync_sema, portMAX_DELAY)) {memset(test_buf, 0x0, BUFFER_LEN);//读取数据到buffertemp_len = esp_at_port_read_data(test_buf, BUFFER_LEN);//下面这段处理逻辑是判读是否推出透传, 这里示例代码的判断条件是 "+++" 三个字节的字符if ((temp_len == 3) && (memcmp((char *) test_buf, "+++", strlen("+++"))==0)) {esp_at_port_exit_specific();temp_len = esp_at_port_get_data_length();if (temp_len > 0) {esp_at_port_recv_data_notify(temp_len, portMAX_DELAY);}break;} else if (temp_len > 0 ){// 这里就把 RAW DATA 交由用户处理了customer_do_something(test_buf, temp_len);}}vSemaphoreDelete(at_sync_sema);at_sync_sema = NULL;// 这里就退出透传了, 同时还会输出OK字符, 如果您不想把OK在这里输出, 而是放在 ‘>’ 之前// 您首先需要在打印输入提示符 '>' 之前, 调用// esp_at_response_result(ESP_AT_RESULT_CODE_OK);// 然后在最后这里 return ESP_AT_RESULT_CODE_IGNORE;return ESP_AT_RESULT_CODE_OK;
}

2.5.2 指定数据长度

如果数据长度是指定, 和上面略微有些差异, 大体思路相同, 我们可以这么做:

定义命令:

static esp_at_cmd_struct at_example_cmd[] = {{"+TESTCMD", NULL, NULL, at_setupCmdTestCmd, NULL},
};

命令带一个整形参数.

具体实现如下:

static xSemaphoreHandle at_sync_sema   = NULL;static void at_wait_data_callback(void)
{xSemaphoreGive(at_sync_sema);
}uint8_t at_setupCmdTestCmd(uint8_t para_num)
{int32_t cnt = 0 , value = 0, len = 0, temp_len = 0;uint8_t *test_buf = NULL;// 获取数据长度if (esp_at_get_para_as_digit(cnt++, &value) != ESP_AT_PARA_PARSE_RESULT_OK) {return ESP_AT_RESULT_CODE_ERROR;}len = value;// 检查参数个数if (para_num != cnt) {return ESP_AT_RESULT_CODE_ERROR;}test_buf = (uint8_t *)malloc(len * sizeof(uint8_t));if (test_buf == NULL) {printf("malloc fail\n");return ESP_AT_RESULT_CODE_ERROR;}vSemaphoreCreateBinary(at_sync_sema);xSemaphoreTake(at_sync_sema, portMAX_DELAY);//打印输入提示符 '>'esp_at_port_write_data((uint8_t*)">", at_strlen(">"));esp_at_port_enter_specific(at_wait_data_callback);temp_len = 0;// 开始截取指定长度的数据, while (xSemaphoreTake(at_sync_sema, portMAX_DELAY)) {temp_len += esp_at_port_read_data(test_buf + temp_len, len - temp_len);if (temp_len == len) {// 走到这里, 说明已经接收到了想要的长度的数据, 但是要要注意, 如果时间输入长度超过指定想要的长度, 也会进到这里esp_at_port_exit_specific();// 这里就是在获取看看还有多少数据没有读取, 如果是0, 那就是正好数据长度就是指定的temp_len = esp_at_port_get_data_length();if (temp_len > 0) {//如果实际输入的长度超过想要的长度, 会走到这里, 在这个例子中, 是直接把多余的数据打印出来, 您的应用中怎么处理取决于您esp_at_port_recv_data_notify(temp_len, portMAX_DELAY);}break;}}vSemaphoreDelete(at_sync_sema);at_sync_sema = NULL;free(test_buf);...return ESP_AT_RESULT_CODE_OK;
}

ESP-AT 系列: 自定义 AT 命令相关推荐

  1. Windows7操作系统自定义运行命令(简单方法之二)

    经过上次的随笔,相信大家已经会通过注册表和环境变量来自定义运行命令了. 其实我们知道:不管是环境变量还是注册表,都是存储的配置信息.当然了,环境变量主要存储的是路径信息.我们对环境变量进行配置,就是给 ...

  2. 仿vue的前端自定义cmd命令拉取项目脚手架

    原文地址:https://github.com/screetBloo... 含纯node或者commander实现自己的前端脚手架 文章码字分享不易,希望如果帮到您的话,帮忙github点个star ...

  3. 为你的AliOS Things应用增加自定义cli命令

    在日常嵌入式开发中,我们经常会用串口命令来使设备进入某种特定的状态,或执行某个特定的操作.如系统自检,模拟运行,或者进入手动模式进行设备点动.linux下有强大的shell工具,可以让用户和片上系统进 ...

  4. linux 定义快捷命令,Linux系统自定义快捷命令的详细说明

    Linux系统用户可以自定义喜欢的快捷键命令.下面由学习啦小编为大家整理了Linux系统自定义快捷键命令的详细说明,希望对大家有帮助! Linux系统自定义快捷命令的详细说明 目前总结到的有两种方式, ...

  5. RT-Thread中自定义MSH命令传入的参数是字符串,需用户自行检查和解析

    如下是在将安富莱的dac8563模块对接到潘多拉开发板RT-Thread SPI设备框架中时导出到RT-Thread的自定义MSH命令,需要注意的是MSH传入的是字符串,需要自行对字符串进行解析处理. ...

  6. react dispatch_React系列自定义Hooks很简单

    React系列-Mixin.HOC.Render Props(上) React系列-轻松学会Hooks(中) React系列-自定义Hooks很简单(下) 我们在第二篇文章中介绍了一些常用的hooks ...

  7. Android调试系列之dumpsys命令

    Android调试系列之dumpsys命令 版权声明:本文为[viclee]原创,如需转载请注明出处~ https://blog.csdn.net/goodlixueyong/article/deta ...

  8. u-boot移植随笔:自定义u-boot命令点灯

    u-boot移植随笔:自定义u-boot命令点灯 前几天一不小心在CSDN论坛上发帖散分,同时许诺完成点灯就结账,经过努力,终于可以在u-boot的shell中输入自定义的命令来点灯了.下面简单讲一下 ...

  9. RT-Thread中自定义 FinSH 命令

    在使用RT-Thread中的FinSH 命令时,除了系统默认的FinSH命令以外,我们还可以自定义FinSH命令.下面就来演示一下如何自定义FinSH命令.关于FinSH命令的详细用法请参考官方资料h ...

最新文章

  1. redis使用epoll
  2. 前端每日实战:60# 视频演示如何用纯 CSS 创作一块乐高积木
  3. 密码学-hash加密
  4. HTML、JSP、Servlet中的相对路径和绝对路径 页面跳转问题
  5. 产品必懂技术术语(后台类)
  6. linux怎么安装vim?
  7. c语言一串大写字母转小写,C语言的基础函数大小写转换
  8. MFC 添加文件路径 遍历文件
  9. 计算机关机又自动重启,为什么w7电脑关机后自动重启_w7电脑关机后自动重启怎么解决...
  10. 透视变换–鸟瞰图_单例设计模式–鸟瞰
  11. 给刚博士毕业的年轻学者9点建议,最后一条:抓紧结婚,生娃!
  12. java异常练习:要求用户输入数字,捕获并处理用户输入错误的异常,给用户进行提示
  13. 使用Eclipse编写Processing小程序
  14. Docker代理设置方法
  15. java楼宇报警器_智能楼宇包含哪些安防子系统
  16. 从零开始搭建java物联网平台_【攻略】从零开始搭建物联网系统
  17. 数据分析师的工作职责是什么?
  18. F - Endless Walk
  19. 宝付国际严格落实政策要求,助力解决跨境电商交易审核难点
  20. 【机器学习】课程设计布置:某闯关类手游用户流失预测

热门文章

  1. 最小生成树-----------联络员
  2. mysql时出现client does not support auth...upgrading Mysql Client
  3. 10月11日关于煤炭的期货交易
  4. uniapp封装的激励广告和插屏广告以及banner广告
  5. 推荐:国外知名6家大数据领域企业!
  6. 安卓班级同学录校友录系统app
  7. 关于3年买车5年买房的那些事
  8. 深度剖析E680G开发五.移植OPIE操作系统(下)-编译和运行OPIE
  9. SIP系统封装技术浅析
  10. Harmony 开发基础——Harmony 学习笔记