如果做過物聯(lián)網(wǎng)產(chǎn)品的,就知道解析AT指令有時(shí)候也挺痛苦的。
每次解析AT指令都要寫一堆strstr、strcmp,代碼又長又難看,改一個(gè)bug要找半天。
那時(shí)候?qū)慛BIot模塊的AT指令解析,代碼是這樣的:
void Parse_AT_Response(char *response){if(strstr(response, "OK") != NULL){// 處理OK}else if(strstr(response, "ERROR") != NULL){// 處理ERROR}else if(strstr(response, "+CIMI:") != NULL){// 解析CIMI}else if(strstr(response, "+CGSN:") != NULL){// 解析CGSN}// ... 一堆else if}
每次加一個(gè)AT指令,就要加一個(gè)else if。代碼越寫越長,越寫越亂。
后來我花時(shí)間封裝了一個(gè)通用解析函數(shù),代碼量直接砍了一半,維護(hù)起來也舒服多了。
很多初學(xué)者解析字符串的時(shí)候,喜歡用strstr到處找:
char response[] = "AT+CGSN: 123456789012345\r\n\r\nOK";// 找OKif(strstr(response, "OK") != NULL){// ...}// 找+CGSNif(strstr(response, "+CGSN:") != NULL){char *p = strstr(response, "+CGSN:");p += strlen("+CGSN:");// 解析數(shù)據(jù)...}
這種寫法有幾個(gè)問題:
每次解析都要寫strstr、指針偏移、數(shù)據(jù)提取...同樣的邏輯重復(fù)無數(shù)次。
char *p = strstr(response, "+CGSN:");p += strlen("+CGSN:"); // 容易算錯(cuò)sscanf(p, "%s", buffer); // 容易溢出
else if(strstr(response, "+CIMI:") != NULL){// 50行代碼}else if(strstr(response, "+CGSN:") != NULL){// 50行代碼}else if(strstr(response, "+QMTCFG:") != NULL){// 50行代碼}
找bug的時(shí)候要在幾百行代碼里翻來翻去。
能不能封裝一個(gè)通用函數(shù),不管是什么AT指令,都能用同一種方式解析?
答案是:能!
核心思路:
我給你寫一個(gè)實(shí)際的例子:
// 定義AT指令類型枚舉typedef enum {AT_TYPE_UNKNOWN = 0,AT_TYPE_OK,AT_TYPE_ERROR,AT_TYPE_CIMI,AT_TYPE_CGSN,AT_TYPE_QMTCFG,} AT_Type_e;// 定義AT指令解析結(jié)果typedef struct {AT_Type_e type; // 指令類型char params[5][32]; // 參數(shù)(最多5個(gè),每個(gè)最多32字符)unsigned char param_cnt; // 參數(shù)數(shù)量} AT_Result_t;// AT指令識(shí)別表typedef struct {const char *prefix; // 指令前綴AT_Type_e type; // 指令類型} AT_Prefix_t;const AT_Prefix_t g_atPrefix[] = {{"OK", AT_TYPE_OK},{"ERROR", AT_TYPE_ERROR},{"+CIMI:", AT_TYPE_CIMI},{"+CGSN:", AT_TYPE_CGSN},{"+QMTCFG:", AT_TYPE_QMTCFG},};// 通用AT指令解析函數(shù)AT_Type_e AT_Parse_Response(char *response, AT_Result_t *result){unsigned char i;// 1. 識(shí)別指令類型for(i = 0; i < 5; i++){if(strstr(response, g_atPrefix[i].prefix) != NULL){result->type = g_atPrefix[i].type;break;}}if(i >= 5){result->type = AT_TYPE_UNKNOWN;return AT_TYPE_UNKNOWN;}// 2. 提取參數(shù)char *p = strstr(response, g_atPrefix[i].prefix);if(p == NULL){result->param_cnt = 0;return result->type;}p += strlen(g_atPrefix[i].prefix);// 解析參數(shù)(假設(shè)參數(shù)用逗號(hào)分隔)result->param_cnt = 0;char *token;token = strtok(p, ",\r\n");while(token != NULL && result->param_cnt < 5){strncpy(result->params[result->param_cnt], token, 31);result->params[result->param_cnt][31] = '\0';result->param_cnt++;token = strtok(NULL, ",\r\n");}return result->type;}// 使用示例void Parse_Example(char *response){AT_Result_t result;AT_Type_e type = AT_Parse_Response(response, &result);switch(type){case AT_TYPE_OK:// 處理OKbreak;case AT_TYPE_ERROR:// 處理ERRORbreak;case AT_TYPE_CIMI:// 獲取IMSI號(hào)printf("IMSI: %s\n", result.params[0]);break;case AT_TYPE_CGSN:// 獲取IMEI號(hào)printf("IMEI: %s\n", result.params[0]);break;default:break;}}
看這個(gè)設(shè)計(jì):
g_atPrefix數(shù)組存儲(chǔ)所有要識(shí)別的指令AT_Parse_Response處理所有AT指令AT_Result_t存儲(chǔ)解析結(jié)果要加新指令?只需要在g_atPrefix數(shù)組里加一行!
用封裝前:
void Parse_CIMI(char *response){char *p = strstr(response, "+CIMI:");if(p != NULL){p += 6;char imsi[32];sscanf(p, "%s", imsi);// 處理IMSI...}}void Parse_CGSN(char *response){char *p = strstr(response, "+CGSN:");if(p != NULL){p += 6;char imei[32];sscanf(p, "%s", imei);// 處理IMEI...}}void Parse_QMTCFG(char *response){char *p = strstr(response, "+QMTCFG:");if(p != NULL){p += 8;char param1[32], param2[32];sscanf(p, "%s,%s", param1, param2);// 處理參數(shù)...}}// ... 每個(gè)AT指令一個(gè)函數(shù),幾十個(gè)函數(shù)...
用封裝后:
void Process_AT_Response(char *response){AT_Result_t result;AT_Parse_Response(response, &result);switch(result.type){case AT_TYPE_CIMI:// 處理IMSI: result.params[0]break;case AT_TYPE_CGSN:// 處理IMEI: result.params[0]break;case AT_TYPE_QMTCFG:// 處理參數(shù): result.params[0], result.params[1]break;// ... 其他指令}}
代碼量直接砍了一半!
而且,加新指令的時(shí)候,只需要在switch里加一個(gè)case,不用再寫新的解析函數(shù)。
封裝的時(shí)候,還可以加上參數(shù)校驗(yàn):
typedef enum {PARSE_OK = 0,PARSE_ERR_NO_PREFIX,PARSE_ERR_PARAM_COUNT,PARSE_ERR_PARAM_FORMAT,} Parse_Status_e;Parse_Status_e AT_Parse_With_Check(char *response, AT_Result_t *result,unsignedchar expect_param_cnt){// ... 解析邏輯// 校驗(yàn)參數(shù)數(shù)量if(result->param_cnt != expect_param_cnt){return PARSE_ERR_PARAM_COUNT;}// 校驗(yàn)參數(shù)格式for(int i = 0; i < result->param_cnt; i++){if(strlen(result->params[i]) == 0){return PARSE_ERR_PARAM_FORMAT;}}return PARSE_OK;}
這樣解析的時(shí)候就能知道:
封裝字符串解析函數(shù)的時(shí)候,要注意幾個(gè)坑:
// 錯(cuò)誤寫法char buffer[32];sscanf(p, "%s", buffer); // 如果p指向的字符串很長,會(huì)溢出// 正確寫法char buffer[32];sscanf(p, "%31s", buffer); // 限制最多讀取31個(gè)字符
// 錯(cuò)誤寫法char *p = strstr(response, "+CIMI:");*p += 6; // 如果p是NULL,會(huì)崩潰// 正確寫法char *p = strstr(response, "+CIMI:");if(p != NULL){p += 6;}
// 錯(cuò)誤寫法char buffer[32];strcpy(buffer, p); // buffer可能沒有'\0'結(jié)尾// 正確寫法char buffer[32] = {0}; // 初始化為全0strncpy(buffer, p, 31);buffer[31] = '\0';
每次解析字符串都要寫一堆strstr?
封裝一個(gè)通用函數(shù),處理AT指令舒服多了。
封裝的核心思想:
封裝的好處:
WG01項(xiàng)目的NBIot模塊、WG02項(xiàng)目的WiFi模塊...AT指令解析都是用這種方式處理的。
end

下面是更多無際原創(chuàng)的個(gè)人成長經(jīng)歷、行業(yè)經(jīng)驗(yàn)、技術(shù)干貨。
1.電子工程師是怎樣的成長之路?10年5000字總結(jié)
2.如何快速看懂別人的代碼和思維
3.單片機(jī)開發(fā)項(xiàng)目全局變量太多怎么管理?
4.C語言開發(fā)單片機(jī)為什么大多數(shù)都采用全局變量的形式?
5.單片機(jī)怎么實(shí)現(xiàn)模塊化編程?實(shí)用程度讓人發(fā)指!
6.c語言回調(diào)函數(shù)的使用及實(shí)際作用詳解
7.手把手教你c語言隊(duì)列實(shí)現(xiàn)代碼,通俗易懂超詳細(xì)!
8.c語言指針用法詳解,通俗易懂超詳細(xì)!