From: 孙春阳 Date: Mon, 24 Nov 2025 03:06:05 +0000 (+0800) Subject: 尝试使用AT指令替代 标准socket 目前为止很不稳定,URC数据解析问题还很突出。 X-Git-Url: http://git.snicker.com.cn/?a=commitdiff_plain;h=refs%2Fchanges%2F2793%2Fhead;p=mirrors%2FFD230.git 尝试使用AT指令替代 标准socket 目前为止很不稳定,URC数据解析问题还很突出。 --- diff --git a/code/sdk/application/wearable/ln/nativeapp/nativeui/thunderbird/src/ThunderBirdModel.cpp b/code/sdk/application/wearable/ln/nativeapp/nativeui/thunderbird/src/ThunderBirdModel.cpp index 8a5ef02e8..5a592352a 100644 --- a/code/sdk/application/wearable/ln/nativeapp/nativeui/thunderbird/src/ThunderBirdModel.cpp +++ b/code/sdk/application/wearable/ln/nativeapp/nativeui/thunderbird/src/ThunderBirdModel.cpp @@ -46,9 +46,9 @@ #endif //最低视频帧率 - #define LOW_FREQUENCE 25 + #define LOW_FREQUENCE 18 //最高视频帧率 - #define HIGH_FREQUENCE 60 + #define HIGH_FREQUENCE 30 namespace OHOS { enum OTA_STATUS diff --git a/code/sdk/build/script/parse_tool/config.py b/code/sdk/build/script/parse_tool/config.py index 70aaf5d2d..5936c128a 100644 --- a/code/sdk/build/script/parse_tool/config.py +++ b/code/sdk/build/script/parse_tool/config.py @@ -4,7 +4,7 @@ LOSCFG_BASE_IPC_QUEUE_LIMIT = 50 LOSCFG_BASE_CORE_SWTMR_LIMIT = 50 -LOSCFG_BASE_IPC_SEM_LIMIT = 30 +LOSCFG_BASE_IPC_SEM_LIMIT = 50 LOSCFG_PLATFORM_HWI_LIMIT = 96 configHWI_MAX_VECTOR_CNT = 152 configQUEUE_REGISTRY_SIZE = 8 diff --git a/code/sdk/middleware/chips/brandy/pm/include/pm_definition.h b/code/sdk/middleware/chips/brandy/pm/include/pm_definition.h index dc8745eb7..6436793e8 100644 --- a/code/sdk/middleware/chips/brandy/pm/include/pm_definition.h +++ b/code/sdk/middleware/chips/brandy/pm/include/pm_definition.h @@ -53,6 +53,7 @@ typedef enum { PM_ID_BP,//血压 PM_ID_4G_AI,//4g AI PM_ID_GPS,//4g AI + PM_ID_NET_APP,//网络应用 #ifdef SUPPORT_ALIPAY_SEC PM_ID_ALIPAY,//支付宝 #endif diff --git a/code/sdk/middleware/chips/brandy/pm/pm.c b/code/sdk/middleware/chips/brandy/pm/pm.c index 1ab7b5d17..14c73c7e1 100644 --- a/code/sdk/middleware/chips/brandy/pm/pm.c +++ b/code/sdk/middleware/chips/brandy/pm/pm.c @@ -243,13 +243,14 @@ const char* pm_id_name_arr[] ={ "PM_ID_SPI_DMA", "PM_ID_I2C_DMA", "PM_ID_UART_DMA", - "PM_ID_HEART",//����������оƬ - "PM_ID_MOTOR",//���������� - "PM_ID_GSENSOR",//���������ټ� + "PM_ID_HEART",//心率芯片 + "PM_ID_MOTOR",//马达 + "PM_ID_GSENSOR",//加速计 "PM_ID_CAT1",//Cat1 communication "PM_ID_P_TEST", - "PM_ID_BUTTON_GPIO",//GPIO���� + "PM_ID_BUTTON_GPIO",//GPIO按键 "PM_ID_4G_AI",//AI投票 + "PM_ID_NET_APP",//网络应用 #ifdef SUPPORT_ALIPAY_SEC "PM_ID_ALIPAY",//支付宝 #endif diff --git a/code/sdk/middleware/chips/brandy/pm/service/pm_sleep.c b/code/sdk/middleware/chips/brandy/pm/service/pm_sleep.c index 034288829..e1abc2ab0 100644 --- a/code/sdk/middleware/chips/brandy/pm/service/pm_sleep.c +++ b/code/sdk/middleware/chips/brandy/pm/service/pm_sleep.c @@ -237,12 +237,13 @@ void pm_enter_deep_sleep_only_ulp_wakeup(void) #endif if (status != 0) { pm_wkup_event_by_uart_callback(); + #ifdef SUPPORT_CAT1_UART_WAKEUP + check_cat1_uart_wakeup(); + #endif } #endif -#ifdef SUPPORT_CAT1_UART_WAKEUP - check_cat1_uart_wakeup(); -#endif + pm_wkup_prepare(PM_SLEEP_DS); pm_sleep_info_record(SLP_INFO_DEEPSLEEP); reg16_setbit(0x520003e4, 0x3); // open mdam clock after deepsleep @@ -298,9 +299,7 @@ static void pm_enter_deep_sleep(void) if (status != 0) { pm_wkup_event_by_uart_callback(); } -#ifdef SUPPORT_CAT1_UART_WAKEUP - check_cat1_uart_wakeup(); -#endif + pm_wkup_prepare(PM_SLEEP_DS); reg16_setbit(0x520003e4, 0x3); // open mdam clock after deepsleep } diff --git a/code/sdk/zhihui/leiniao/imsl_ln/CMakeLists.txt b/code/sdk/zhihui/leiniao/imsl_ln/CMakeLists.txt index d4da6c36e..08d4d1391 100644 --- a/code/sdk/zhihui/leiniao/imsl_ln/CMakeLists.txt +++ b/code/sdk/zhihui/leiniao/imsl_ln/CMakeLists.txt @@ -21,6 +21,7 @@ set(SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/ln_api_emoji.c ${CMAKE_CURRENT_SOURCE_DIR}/ln_api_timeline_activities.c ${CMAKE_CURRENT_SOURCE_DIR}/ln_api_web_ack.c + ${CMAKE_CURRENT_SOURCE_DIR}/ln_http_download.c ) # 公共头文件路径(供其他模块引用) diff --git a/code/sdk/zhihui/leiniao/imsl_ln/leiniao_api_at.c b/code/sdk/zhihui/leiniao/imsl_ln/leiniao_api_at.c index 8a78fc121..b1ca89b79 100644 --- a/code/sdk/zhihui/leiniao/imsl_ln/leiniao_api_at.c +++ b/code/sdk/zhihui/leiniao/imsl_ln/leiniao_api_at.c @@ -1199,16 +1199,18 @@ static uint32_t ln_download_emoji_at(uint8_t *para, uint32_t paraLen, char *strC LN_UNUSED(paraLen); LN_UNUSED(strCommonCmd); printf("[LN][DOWNLOAD_EMOJI]\n"); - char *json = (char*)malloc(1024); - if (json == NULL) { - printf("[LN][DOWNLOAD_EMOJI] OOM\n"); - return ERRCODE_FAIL; - } + const char *deviceId = uapi_ln_get_device_id(); const char *id = (deviceId && deviceId[0]) ? deviceId : "DEVICE_LOCAL"; const char *characterId = "char_001"; const char *eyesColor = "blue"; const char *eyesShape = "calm-eyes"; + #if 0 + char *json = (char*)malloc(1024); + if (json == NULL) { + printf("[LN][DOWNLOAD_EMOJI] OOM\n"); + return ERRCODE_FAIL; + } int n = snprintf(json, 1024, "{\n" " \"deviceId\": \"%s\",\n" @@ -1221,12 +1223,12 @@ static uint32_t ln_download_emoji_at(uint8_t *para, uint32_t paraLen, char *strC " \"eyesShape\": \"%s\",\n" " \"emotionEmojis\": [\n" " {\n" - " \"emotion\": \"happy\",\n" - " \"emojiUrl\": \"https://example.com/emoji/happy.mp4\"\n" + " \"emotion\": \"dizzy\",\n" + " \"emojiUrl\": \"http://snicker.com.cn:8089/LEINIAO/blue/dizzy.zip\"\n" " },\n" " {\n" - " \"emotion\": \"sad\",\n" - " \"emojiUrl\": \"https://example.com/emoji/sad.mp4\"\n" + " \"emotion\": \"wronged\",\n" + " \"emojiUrl\": \"http://snicker.com.cn:8089/LEINIAO/blue/wronged.zip\"\n" " }\n" " ]\n" " }\n" @@ -1237,9 +1239,54 @@ static uint32_t ln_download_emoji_at(uint8_t *para, uint32_t paraLen, char *strC printf("[LN][DOWNLOAD_EMOJI] build json failed\n"); return ERRCODE_FAIL; } + #else + // 修正json_str的定义,格式整理、转义和换行,方便维护和后续修改 + const char *json_str = "{" + "\"deviceId\":\"860132064629751\"," + "\"timestamp\":1763690727," + "\"messageId\":\"33d5ba5f6fe9499e\"," + "\"command\":\"DownloadEmoji\"," + "\"data\":{" + "\"characterId\":\"bf1b16908b33\"," + "\"eyesColor\":\"yellow-green-gradient\"," + "\"eyesShape\":\"calm-eyes\"," + "\"emotionEmojis\":[" + "{\"emotion\":\"happy\",\"emojiUrl\":\"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Smile.mp4\",\"size\":730359}," + "{\"emotion\":\"laughing\",\"emojiUrl\":\"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Laugh.mp4\",\"size\":777316}," + "{\"emotion\":\"angry\",\"emojiUrl\":\"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Angry.mp4\",\"size\":770986}," + "{\"emotion\":\"wronged\",\"emojiUrl\":\"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Wronged.mp4\",\"size\":1191639}," + "{\"emotion\":\"heartbeat\",\"emojiUrl\":\"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Heartbeat.mp4\",\"size\":952770}," + "{\"emotion\":\"awkward\",\"emojiUrl\":\"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Embarrassed.mp4\",\"size\":807173}," + "{\"emotion\":\"confused\",\"emojiUrl\":\"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Doubt.mp4\",\"size\":833790}," + "{\"emotion\":\"shocked\",\"emojiUrl\":\"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Shocked.mp4\",\"size\":844919}," + "{\"emotion\":\"crying\",\"emojiUrl\":\"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Tears.mp4\",\"size\":910162}," + "{\"emotion\":\"blowing-kiss\",\"emojiUrl\":\"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Kiss.mp4\",\"size\":544297}," + "{\"emotion\":\"sleepy\",\"emojiUrl\":\"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Sleepy.mp4\",\"size\":674698}," + "{\"emotion\":\"mischievous\",\"emojiUrl\":\"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Naughty.mp4\",\"size\":646516}," + "{\"emotion\":\"speechless\",\"emojiUrl\":\"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Speechless.mp4\",\"size\":899444}," + "{\"emotion\":\"dizzy\",\"emojiUrl\":\"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Dizzy.mp4\",\"size\":1135889}," + "{\"emotion\":\"thinking\",\"emojiUrl\":\"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Think.mp4\",\"size\":963503}," + "{\"emotion\":\"working\",\"emojiUrl\":\"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/WorkAndStudy.mp4\",\"size\":1052905}," + "{\"emotion\":\"exercising\",\"emojiUrl\":\"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Exercise.mp4\",\"size\":1077737}," + "{\"emotion\":\"playing\",\"emojiUrl\":\"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Play.mp4\",\"size\":1098020}," + "{\"emotion\":\"daydreaming\",\"emojiUrl\":\"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Daze.mp4\",\"size\":1057085}" + "]," + "\"size\":16969208" + "}" + "}"; + int n = strlen(json_str); + char *json = (char*)malloc(n + 1); + if (json == NULL) { + printf("[LN][DOWNLOAD_EMOJI] OOM\n"); + return ERRCODE_FAIL; + } + memcpy(json, json_str, n + 1); + json[n] = '\0'; + #endif printf("[LN][DOWNLOAD_EMOJI] json = %s\n", json); - // ln_parse_download_emoji(json, n); + ln_parse_download_emoji(json, n); push_download_emoji_notification(characterId, eyesColor, eyesShape); + ln_start_emoji_download(); free(json); return ERRCODE_SUCC; } diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api.c index 6b864e1dd..b9ea27e74 100644 --- a/code/sdk/zhihui/leiniao/imsl_ln/ln_api.c +++ b/code/sdk/zhihui/leiniao/imsl_ln/ln_api.c @@ -1,6 +1,8 @@ #include "ln_api.h" /* 智慧网络统一接口(已包含 OpenHarmony 类型定义) */ #include "zhihui_net.h" +/* HTTP下载模块 */ +#include "ln_http_download.h" /* for HMAC-SHA256 */ #include "mbedtls/md.h" /* for MD5 file verify */ @@ -244,15 +246,14 @@ static int websocket_retry_connect_count = 0; /* 获取设备token: 优先读本地缓存,判断过期,否则刷新 */ char *uapi_ln_get_device_token(void) { - // return "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJXWVNaIiwiZGV2aWNlTW9kZWwiOiJhYm15Ym94XzEiLCJkZXZpY2VWZXJzaW9uIjoiMS4wLjAiLCJkZXZpY2VJZCI6IkRFVklDRV9QQ19URVNUXzAwMSIsImV4cCI6MTc2MTQ2MjQ5Mn0.Oeyu8BDopDTvtAcdNpzz46U2w1UwzEzqcvPwD3Dp08Y"; - /* 若内存已有且非占位,直接返回 */ + // 若内存已有且非占位,直接返回 if (g_device_token && strcmp(g_device_token, "") != 0) { WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "uapi_ln_get_device_token: cached token=%s", g_device_token); return g_device_token; } - /* 尝试从本地加载 */ + // 尝试从本地加载 char *cached = NULL; uint64_t exp_ts = 0; if (ln_token_load_json(&cached, &exp_ts) == ERRCODE_SUCC && cached && cached[0]) @@ -263,62 +264,112 @@ char *uapi_ln_get_device_token(void) { if (g_device_token) free(g_device_token); - g_device_token = cached; /* 接管指针 */ + g_device_token = cached; // 接管指针 WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "uapi_ln_get_device_token: loaded from file, not expired(未过期)"); + websocket_retry_connect_count = 0; /* 重置重试计数 */ return g_device_token; } - /* 已过期:释放缓存并尝试刷新 */ + // 已过期:释放缓存并尝试刷新 free(cached); + + /* 检查网络状态,只有网络在线时才执行HTTPS刷新 */ + if (!uapi_dev_runtime_get_cat1_networkstat()) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TOKEN] 本地token已过期,但网络离线,无法刷新 Token"); + websocket_retry_connect_count++; + if (websocket_retry_connect_count >= 100) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TOKEN] 网络离线,无法刷新 Token,重试次数超过100次,进入休眠"); + ln_ws_enter_sleep(); + } + else + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TOKEN] 网络离线,无法刷新 Token,重试次数: %d", websocket_retry_connect_count); + } + return NULL; + } + + /* 网络在线,通过HTTPS刷新token */ + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TOKEN] 本地token已过期,网络在线,通过HTTPS刷新 Token"); if (uapi_ln_refresh_device_token() == ERRCODE_SUCC) { if (g_device_token && strcmp(g_device_token, "") != 0) { WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "uapi_ln_get_device_token: refreshed expired token"); + websocket_retry_connect_count = 0; /* 重置重试计数 */ return g_device_token; } } - return NULL; - } - - /* 没有,尝试获取 Token */ - WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TOKEN] 刷新 Token"); - if (uapi_dev_runtime_get_cat1_networkstat()) - { - WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TOKEN] 网络在线,刷新 Token"); - if (uapi_ln_refresh_device_token() == ERRCODE_SUCC) - { - WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TOKEN] 刷新 Token 成功"); - return g_device_token; - } else { - WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TOKEN] 刷新 Token 失败"); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TOKEN] HTTPS刷新 Token 失败"); websocket_retry_connect_count++; if (websocket_retry_connect_count >= 100) { - WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TOKEN] 网络离线,无法刷新 Token,重试次数超过10次,进入休眠"); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TOKEN] 刷新 Token 失败,重试次数超过100次,进入休眠"); ln_ws_enter_sleep(); } else { - WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TOKEN] 网络离线,无法刷新 Token,重试次数: %d", websocket_retry_connect_count); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TOKEN] 刷新 Token 失败,重试次数: %d", websocket_retry_connect_count); } - return NULL; } + return NULL; } - else + + // 没有本地token,尝试获取 Token + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TOKEN] 没有本地token,尝试刷新 Token"); + + /* 检查网络状态,只有网络在线时才执行HTTPS获取 */ + if (!uapi_dev_runtime_get_cat1_networkstat()) { WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TOKEN] 网络离线,无法刷新 Token"); websocket_retry_connect_count++; if (websocket_retry_connect_count >= 100) { - WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TOKEN] 网络离线,无法刷新 Token,重试次数超过10次,进入休眠"); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TOKEN] 网络离线,无法刷新 Token,重试次数超过100次,进入休眠"); ln_ws_enter_sleep(); } else { WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TOKEN] 网络离线,无法刷新 Token,重试次数: %d", websocket_retry_connect_count); } + return NULL; + } + + /* 网络在线,通过HTTPS获取token */ + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TOKEN] 网络在线,通过HTTPS刷新 Token (当前重试计数: %d)", websocket_retry_connect_count); + if (uapi_ln_refresh_device_token() == ERRCODE_SUCC) + { + if (g_device_token && strcmp(g_device_token, "") != 0) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TOKEN] 刷新 Token 成功 (token长度: %zu)", strlen(g_device_token)); + websocket_retry_connect_count = 0; /* 重置重试计数 */ + return g_device_token; + } + else + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TOKEN] HTTPS刷新 Token 返回空值 (g_device_token=%p)", g_device_token); + } + } + else + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TOKEN] 刷新 Token 失败 (uapi_ln_refresh_device_token返回失败)"); + } + + websocket_retry_connect_count++; + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TOKEN] 重试计数递增: %d (最大100次)", websocket_retry_connect_count); + if (websocket_retry_connect_count >= 100) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TOKEN] 刷新 Token 失败,重试次数超过100次,进入休眠"); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TOKEN] 注意:重试计数只有在uapi_ln_get_device_token被调用时才会递增"); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TOKEN] 如果WebSocket连接失败后没有再次尝试连接,重试计数不会继续增加"); + ln_ws_enter_sleep(); + } + else + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TOKEN] 刷新 Token 失败,重试次数: %d/100", websocket_retry_connect_count); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TOKEN] 下次WebSocket连接时会再次尝试获取token"); } return NULL; } @@ -434,12 +485,15 @@ void uapi_ln_set_device_bound(int bound) g_ln_is_device_bound = bound; } -/* 下载文件 -@param url 文件URL -@param save_path 保存路径 -@return ERRCODE_SUCC 成功,ERRCODE_FAIL 失败 -*/ +/* 下载文件 - 已迁移到 ln_http_download.c,使用统一的高级接口 */ int uapi_ln_download_file(const char *url, const char *save_path) +{ + return ln_http_download_file(url, save_path); +} + +#if 0 +/* 原始实现已迁移到 ln_http_download.c */ +int uapi_ln_download_file_old(const char *url, const char *save_path) { if (!url || !save_path) { @@ -564,9 +618,17 @@ int uapi_ln_download_file(const char *url, const char *save_path) WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 下载过程结束,ret=%d", ret); return ret; } +#endif -/* 下载函数封装:带重试,不做UI通知与资源释放 */ +/* 下载函数封装:带重试,不做UI通知与资源释放 - 已迁移到 ln_http_download.c */ int uapi_ln_download_with_retry(const char *url, const char *save_path, int max_retries) +{ + return ln_http_download_with_retry(url, save_path, max_retries); +} + +#if 0 +/* 原始实现已迁移到 ln_http_download.c */ +int uapi_ln_download_with_retry_old(const char *url, const char *save_path, int max_retries) { if (url == NULL || save_path == NULL || (max_retries <= 0 || max_retries > 10 )) { @@ -592,13 +654,17 @@ int uapi_ln_download_with_retry(const char *url, const char *save_path, int max_ uapi_pm_remove_sleep_veto(PM_ID_DEBUG); return ret; } +#endif -/* 下载文件(支持断点续传) -@param url 文件URL -@param save_path 保存路径(若存在则尝试从已下载大小处续传) -@return ERRCODE_SUCC 成功,ERRCODE_FAIL 失败(失败不删除部分文件) -*/ +/* 下载文件(支持断点续传)- 已迁移到 ln_http_download.c */ int uapi_ln_download_file_resume(const char *url, const char *save_path) +{ + return ln_http_download_file_resume(url, save_path); +} + +#if 0 +/* 原始实现已迁移到 ln_http_download.c */ +int uapi_ln_download_file_resume_old(const char *url, const char *save_path) { if (!url || !save_path) { @@ -790,8 +856,17 @@ int uapi_ln_download_file_resume(const char *url, const char *save_path) fclose(fd); return ret; } -/* 断点续传下载函数封装:带重试,不做UI通知与资源释放 */ +#endif + +/* 断点续传下载函数封装:带重试,不做UI通知与资源释放 - 已迁移到 ln_http_download.c */ int uapi_ln_download_file_resume_with_retry(const char *url, const char *save_path, int max_retries) +{ + return ln_http_download_file_resume_with_retry(url, save_path, max_retries); +} + +#if 0 +/* 原始实现已迁移到 ln_http_download.c */ +int uapi_ln_download_file_resume_with_retry_old(const char *url, const char *save_path, int max_retries) { if (url == NULL || save_path == NULL || (max_retries <= 0 || max_retries > 10 )) { @@ -828,8 +903,14 @@ int uapi_ln_download_file_resume_with_retry(const char *url, const char *save_pa uapi_pm_remove_sleep_veto(PM_ID_DEBUG); return ret; } -/* 自适应申请HttpClient缓冲(保证header>=2048),失败则逐步扩容重试 */ -int ln_http_prepare_auto(HttpClientData *clientData, int header_size, int resp_size) +#endif + +/* 自适应申请HttpClient缓冲(保证header>=2048),失败则逐步扩容重试 - 已迁移到 ln_http_download.c */ +/* 注意:此函数在 ln_http_download.c 中实现,这里不再重复定义 */ + +#if 0 +/* 原始实现已迁移到 ln_http_download.c */ +int ln_http_prepare_auto_old(HttpClientData *clientData, int header_size, int resp_size) { if (header_size < 2048) header_size = 2048; @@ -850,6 +931,7 @@ int ln_http_prepare_auto(HttpClientData *clientData, int header_size, int resp_s } return ret; } +#endif /* ============================ WebSocket 业务消息 上层处理 ============================ */ void ln_ws_process_message(const ws_data_msg_t *data_msg, ws_command_t type) diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_device_info.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_device_info.c index 4c7b50594..7438942d2 100644 --- a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_device_info.c +++ b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_device_info.c @@ -482,11 +482,6 @@ uint32_t ln_device_get_character_http(const char *device_id, LOS_Msleep(200); #endif - HttpClient client = (HttpClient){0}; - HttpClientData clientData = (HttpClientData){0}; - client.isHttp = false; - client.remotePort = 443; - char *header_buf = (char *)malloc(512); if (!header_buf) return ERRCODE_FAIL; @@ -500,28 +495,82 @@ uint32_t ln_device_get_character_http(const char *device_id, free(header_buf); return ERRCODE_FAIL; } - ZhihuiHttpClientSetCustomHeader(&client, header_buf); printf("device getCharacter header %s\n", header_buf); - HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, 8 * 1024); - if (prep != HTTP_SUCCESS) + /* 初始化HTTP客户端(使用通用接口) */ + /* 注意:HTTPS 证书和 SSL 配置已自动在 n706a_http_client_setup_connection 中处理 */ + ZhihuiHttpClientHandle http_client = NULL; + int init_ret = ZhihuiHttpClientInitAdvanced(&http_client); + if (init_ret != 0) + { + printf("device getCharacter: Failed to init HTTP client\n"); + free(header_buf); + return ERRCODE_FAIL; + } + + /* 设置URL */ + int url_ret = ZhihuiHttpClientSetUrl(http_client, LN_DEVICE_GET_CHARACTER_URL, 0); + if (url_ret != 0) + { + printf("device getCharacter: Failed to set URL\n"); + free(header_buf); + ZhihuiHttpClientClose(http_client); + return ERRCODE_FAIL; + } + + /* 建立HTTPS连接 */ + int conn_ret = ZhihuiHttpClientSetupConnection(http_client); + if (conn_ret != 0) + { + printf("device getCharacter: Failed to setup HTTPS connection\n"); + free(header_buf); + ZhihuiHttpClientClose(http_client); + return ERRCODE_FAIL; + } + + /* 设置请求头 */ + int header_ret = ZhihuiHttpClientSetHeader(http_client, header_buf); + if (header_ret != 0) + { + printf("device getCharacter: Failed to set header\n"); + free(header_buf); + ZhihuiHttpClientClose(http_client); + return ERRCODE_FAIL; + } + + /* 准备响应数据缓冲区 */ + HttpClientData clientData = {0}; + clientData.responseBuf = (char *)malloc(8 * 1024); + clientData.responseBufLen = 8 * 1024; + if (!clientData.responseBuf) { - printf("device getCharacter prepare failed %d\n", prep); + printf("device getCharacter: Failed to allocate response buffer\n"); + free(header_buf); + ZhihuiHttpClientClose(http_client); return ERRCODE_FAIL; } - HTTPC_RESULT res = ZhihuiHttpClientGetRequest(&client, LN_DEVICE_GET_CHARACTER_URL, &clientData); - if (res != HTTP_SUCCESS) + /* 发送GET请求 */ + int get_ret = ZhihuiHttpClientGet(http_client, NULL, &clientData); + if (get_ret != 0) { - printf("device getCharacter get failed %d\n", res); - goto end; + printf("device getCharacter: GET request failed %d\n", get_ret); + free(header_buf); + free(clientData.responseBuf); + ZhihuiHttpClientClose(http_client); + return ERRCODE_FAIL; } - int code = HttpClientGetResponseCode(&client); + + /* 检查HTTP响应码 */ + int code = ZhihuiHttpClientGetResponseCode(http_client); if (code != 200) { printf("device getCharacter: HTTP %d\n", code); - goto end; + free(header_buf); + free(clientData.responseBuf); + ZhihuiHttpClientClose(http_client); + return ERRCODE_FAIL; } if (clientData.responseBuf && clientData.responseBufLen > 0) @@ -530,10 +579,9 @@ uint32_t ln_device_get_character_http(const char *device_id, ln_parse_character_config_startUp(clientData.responseBuf, real_len); } -end: - ZhihuiHttpClientUnprepare(&clientData); - ZhihuiHttpClientClse(&client); free(header_buf); + free(clientData.responseBuf); + ZhihuiHttpClientClose(http_client); return ERRCODE_SUCC; } @@ -551,46 +599,95 @@ uint32_t ln_device_get_user_id_http(const char *device_id, LOS_Msleep(200); #endif - HttpClient client = (HttpClient){0}; - HttpClientData clientData = (HttpClientData){0}; - client.isHttp = false; - client.remotePort = 443; - - char *header_buf2 = (char *)malloc(512); - if (!header_buf2) + char *header_buf = (char *)malloc(512); + if (!header_buf) return ERRCODE_FAIL; - int hdr = snprintf(header_buf2, 512, + int hdr = snprintf(header_buf, 512, "deviceId: %s\r\n" "deviceToken: %s\r\n" "Content-Type: application/json\r\n", device_id, device_token); if (hdr <= 0) { - free(header_buf2); + free(header_buf); + return ERRCODE_FAIL; + } + + printf("device getUserId header %s\n", header_buf); + + /* 初始化HTTP客户端(使用通用接口) */ + /* 注意:HTTPS 证书和 SSL 配置已自动在 n706a_http_client_setup_connection 中处理 */ + ZhihuiHttpClientHandle http_client = NULL; + int init_ret = ZhihuiHttpClientInitAdvanced(&http_client); + if (init_ret != 0) + { + printf("device getUserId: Failed to init HTTP client\n"); + free(header_buf); + return ERRCODE_FAIL; + } + + /* 设置URL */ + int url_ret = ZhihuiHttpClientSetUrl(http_client, LN_DEVICE_GET_USER_ID_URL, 0); + if (url_ret != 0) + { + printf("device getUserId: Failed to set URL\n"); + free(header_buf); + ZhihuiHttpClientClose(http_client); + return ERRCODE_FAIL; + } + + /* 建立HTTPS连接 */ + int conn_ret = ZhihuiHttpClientSetupConnection(http_client); + if (conn_ret != 0) + { + printf("device getUserId: Failed to setup HTTPS connection\n"); + free(header_buf); + ZhihuiHttpClientClose(http_client); return ERRCODE_FAIL; } - ZhihuiHttpClientSetCustomHeader(&client, header_buf2); - printf("device getUserId header %s\n", header_buf2); + /* 设置请求头 */ + int header_ret = ZhihuiHttpClientSetHeader(http_client, header_buf); + if (header_ret != 0) + { + printf("device getUserId: Failed to set header\n"); + free(header_buf); + ZhihuiHttpClientClose(http_client); + return ERRCODE_FAIL; + } - HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, 8 * 1024); - if (prep != HTTP_SUCCESS) + /* 准备响应数据缓冲区 */ + HttpClientData clientData = {0}; + clientData.responseBuf = (char *)malloc(8 * 1024); + clientData.responseBufLen = 8 * 1024; + if (!clientData.responseBuf) { - printf("device getUserId prepare failed %d\n", prep); + printf("device getUserId: Failed to allocate response buffer\n"); + free(header_buf); + ZhihuiHttpClientClose(http_client); return ERRCODE_FAIL; } - HTTPC_RESULT res = ZhihuiHttpClientGetRequest(&client, LN_DEVICE_GET_USER_ID_URL, &clientData); - if (res != HTTP_SUCCESS) + /* 发送GET请求 */ + int get_ret = ZhihuiHttpClientGet(http_client, NULL, &clientData); + if (get_ret != 0) { - printf("device getUserId get failed %d\n", res); - goto end; + printf("device getUserId: GET request failed %d\n", get_ret); + free(header_buf); + free(clientData.responseBuf); + ZhihuiHttpClientClose(http_client); + return ERRCODE_FAIL; } - int code = HttpClientGetResponseCode(&client); + + /* 检查HTTP响应码 */ + int code = ZhihuiHttpClientGetResponseCode(http_client); if (code != 200) { printf("device getUserId: HTTP %d\n", code); - goto end; + free(header_buf); + free(clientData.responseBuf); + ZhihuiHttpClientClose(http_client); + return ERRCODE_FAIL; } if (clientData.responseBuf && clientData.responseBufLen > 0) @@ -636,10 +733,9 @@ uint32_t ln_device_get_user_id_http(const char *device_id, } } -end: - ZhihuiHttpClientUnprepare(&clientData); - ZhihuiHttpClientClse(&client); - free(header_buf2); + free(header_buf); + free(clientData.responseBuf); + ZhihuiHttpClientClose(http_client); return ERRCODE_SUCC; } @@ -658,11 +754,6 @@ uint32_t ln_device_get_chat_stamina_http(const char *device_id, const char *devi LOS_Msleep(200); #endif - HttpClient client = (HttpClient){0}; - HttpClientData clientData = (HttpClientData){0}; - client.isHttp = false; - client.remotePort = 443; - char *header_buf = (char *)malloc(512); if (!header_buf) { @@ -680,44 +771,90 @@ uint32_t ln_device_get_chat_stamina_http(const char *device_id, const char *devi WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[CHAT_STAMINA] snprintf failed for header_buf"); return ERRCODE_FAIL; } - ZhihuiHttpClientSetCustomHeader(&client, header_buf); WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[CHAT_STAMINA] header_buf: %s", header_buf); - HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, LN_CLIENT_PREPARE_SIZE * 2); - if (prep != HTTP_SUCCESS) + /* 初始化HTTP客户端(使用通用接口) */ + /* 注意:HTTPS 证书和 SSL 配置已自动在 n706a_http_client_setup_connection 中处理 */ + ZhihuiHttpClientHandle http_client = NULL; + int init_ret = ZhihuiHttpClientInitAdvanced(&http_client); + if (init_ret != 0) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[CHAT_STAMINA] Failed to init HTTP client"); + free(header_buf); + return ERRCODE_FAIL; + } + + /* 设置URL */ + int url_ret = ZhihuiHttpClientSetUrl(http_client, LN_DEVICE_GET_CHAT_STAMINA_URL, 0); + if (url_ret != 0) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[CHAT_STAMINA] Failed to set URL"); + free(header_buf); + ZhihuiHttpClientClose(http_client); + return ERRCODE_FAIL; + } + + /* 建立HTTPS连接 */ + int conn_ret = ZhihuiHttpClientSetupConnection(http_client); + if (conn_ret != 0) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[CHAT_STAMINA] Failed to setup HTTPS connection"); + free(header_buf); + ZhihuiHttpClientClose(http_client); + return ERRCODE_FAIL; + } + + /* 设置请求头 */ + int header_ret = ZhihuiHttpClientSetHeader(http_client, header_buf); + if (header_ret != 0) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[CHAT_STAMINA] Failed to set header"); + free(header_buf); + ZhihuiHttpClientClose(http_client); + return ERRCODE_FAIL; + } + + /* 准备响应数据缓冲区 */ + HttpClientData clientData = {0}; + clientData.responseBuf = (char *)malloc(LN_CLIENT_PREPARE_SIZE * 2); + clientData.responseBufLen = LN_CLIENT_PREPARE_SIZE * 2; + if (!clientData.responseBuf) { - WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[CHAT_STAMINA] prepare failed: %d", prep); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[CHAT_STAMINA] Failed to allocate response buffer"); free(header_buf); + ZhihuiHttpClientClose(http_client); return ERRCODE_FAIL; } - HTTPC_RESULT res = ZhihuiHttpClientGetRequest(&client, LN_DEVICE_GET_CHAT_STAMINA_URL, &clientData); - if (res != HTTP_SUCCESS) + /* 发送GET请求 */ + int get_ret = ZhihuiHttpClientGet(http_client, NULL, &clientData); + if (get_ret != 0) { - WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[CHAT_STAMINA] request failed: %d", res); - ZhihuiHttpClientUnprepare(&clientData); - ZhihuiHttpClientClse(&client); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[CHAT_STAMINA] GET request failed: %d", get_ret); free(header_buf); + free(clientData.responseBuf); + ZhihuiHttpClientClose(http_client); return ERRCODE_FAIL; } - int code = HttpClientGetResponseCode(&client); + /* 检查HTTP响应码 */ + int code = ZhihuiHttpClientGetResponseCode(http_client); int body_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen; if (body_len <= 0 || !clientData.responseBuf) { WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[CHAT_STAMINA] empty body, code=%d", code); - ZhihuiHttpClientUnprepare(&clientData); - ZhihuiHttpClientClse(&client); free(header_buf); + free(clientData.responseBuf); + ZhihuiHttpClientClose(http_client); return ERRCODE_FAIL; } char *json_text = (char *)malloc((size_t)body_len + 1); if (!json_text) { - ZhihuiHttpClientUnprepare(&clientData); - ZhihuiHttpClientClse(&client); free(header_buf); + free(clientData.responseBuf); + ZhihuiHttpClientClose(http_client); return ERRCODE_FAIL; } memcpy(json_text, clientData.responseBuf, (size_t)body_len); @@ -736,9 +873,9 @@ uint32_t ln_device_get_chat_stamina_http(const char *device_id, const char *devi WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[CHAT_STAMINA] json parse failed"); WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[CHAT_STAMINA] raw body: %s", json_text); free(json_text); - ZhihuiHttpClientUnprepare(&clientData); - ZhihuiHttpClientClse(&client); free(header_buf); + free(clientData.responseBuf); + ZhihuiHttpClientClose(http_client); return ERRCODE_FAIL; } @@ -754,9 +891,9 @@ uint32_t ln_device_get_chat_stamina_http(const char *device_id, const char *devi cJSON_Delete(root); free(json_text); - ZhihuiHttpClientUnprepare(&clientData); - ZhihuiHttpClientClse(&client); free(header_buf); + free(clientData.responseBuf); + ZhihuiHttpClientClose(http_client); return ERRCODE_FAIL; } @@ -766,9 +903,9 @@ uint32_t ln_device_get_chat_stamina_http(const char *device_id, const char *devi WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[CHAT_STAMINA] no data"); cJSON_Delete(root); free(json_text); - ZhihuiHttpClientUnprepare(&clientData); - ZhihuiHttpClientClse(&client); free(header_buf); + free(clientData.responseBuf); + ZhihuiHttpClientClose(http_client); return ERRCODE_FAIL; } @@ -795,8 +932,8 @@ uint32_t ln_device_get_chat_stamina_http(const char *device_id, const char *devi cJSON_Delete(root); free(json_text); - ZhihuiHttpClientUnprepare(&clientData); - ZhihuiHttpClientClse(&client); free(header_buf); + free(clientData.responseBuf); + ZhihuiHttpClientClose(http_client); return ERRCODE_SUCC; } \ No newline at end of file diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_emoji.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_emoji.c index abb8e1ee1..7b9688ae7 100644 --- a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_emoji.c +++ b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_emoji.c @@ -35,6 +35,8 @@ typedef struct /* 全局变量 */ EmojiPackage g_emoji_package = {0}; +static app_task_definition_t g_emoji_download_taskcfg = {0}; +static int g_emoji_download_task_running = 0; /* ========== ETA 粗略版辅助函数 ========== */ /* 前置声明,便于在本节工具函数中调用 */ static int ln_emoji_is_cached(const EmojiPackage *pkg, const EmotionEmoji *emoji); @@ -418,7 +420,24 @@ static int ln_emoji_is_cached(const EmojiPackage *pkg, const EmotionEmoji *emoji char path[MAX_PATH_LEN] = {0}; ln_gen_emoji_cache_path(pkg, emoji, path, sizeof(path)); struct stat st; - return (stat(path, &st) == 0 && S_ISREG(st.st_mode)) ? 1 : 0; + if (stat(path, &st) != 0 || !S_ISREG(st.st_mode)) + { + return 0; // 文件不存在或不是普通文件 + } + + /* 如果知道文件大小,检查文件大小是否匹配 */ + if (emoji->size > 0) + { + unsigned long long file_size = ln_get_file_size(path); + if (file_size != (unsigned long long)emoji->size) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 文件大小不匹配: 期望=%lld, 实际=%llu, 需要重新下载", + emoji->emotion, emoji->size, file_size); + return 0; // 文件大小不匹配,需要重新下载 + } + } + + return 1; // 文件存在且大小匹配(如果知道大小) } /* 批量下载表情并上报结果 */ @@ -476,7 +495,19 @@ static void ln_emoji_download(void *arg) } } downloaded_count = cached_count; - WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 初始统计已下载数量,downloaded_count=%d", downloaded_count); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 初始统计已下载数量,downloaded_count=%d/%d", downloaded_count, pkg->emojiCount); + + /* 如果所有表情都已缓存,直接返回成功,不需要下载 */ + if (downloaded_count >= pkg->emojiCount) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 所有表情都已缓存,无需下载"); + all_success = 1; + /* 上报100%进度 */ + push_download_progress_notification(100); + /* 直接跳转到上报状态 */ + goto report_status; + } + if (pkg->emojiCount > 0) { int init_progress = (int)(((double)downloaded_count / pkg->emojiCount) * 100); @@ -492,7 +523,7 @@ static void ln_emoji_download(void *arg) // 跳过已缓存的文件 if (ln_emoji_is_cached(pkg, &pkg->emojis[i])) { - WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 已缓存", pkg->emojis[i].emotion); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 已缓存,跳过", pkg->emojis[i].emotion); continue; } @@ -528,6 +559,9 @@ static void ln_emoji_download(void *arg) int progress = (int)(((double)downloaded_count / pkg->emojiCount) * 100); push_download_progress_notification(progress); WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 下载完成", pkg->emojis[i].emotion); + + /* 下载完成后让出CPU,避免PPP缓冲区溢出 */ + LOS_Msleep(50); /* 50ms延迟,让PPP数据处理任务有机会运行 */ /* 粗略版 ETA(平滑版):在单个文件完成后估算一次 */ if (total_bytes_all > 0) @@ -618,6 +652,7 @@ static void ln_emoji_download(void *arg) pkg->emojiCount, downloaded_count, failed_count, remaining_count); } +report_status: WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "上报表情下载状态"); // 下载完成后上报状态 @@ -648,19 +683,57 @@ static void ln_emoji_download(void *arg) } } +/* 表情下载任务包装函数(在独立线程中执行) */ +static void ln_emoji_download_task_wrapper(void *arg) +{ + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 表情下载任务线程启动"); + uapi_ln_set_file_download_type(LN_FILE_DOWNLOAD_TYPE_EMOJI); + + // 执行下载 + ln_emoji_download(&g_emoji_package); + + // 清理 + uapi_ln_set_file_download_type(LN_FILE_DOWNLOAD_TYPE_NONE); + g_emoji_download_task_running = 0; + + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 表情下载任务线程结束"); +} + /* 启动表情下载任务 */ void ln_start_emoji_download(void) { WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 启动表情下载任务"); - uapi_ln_set_file_download_type(LN_FILE_DOWNLOAD_TYPE_EMOJI); + if (g_emoji_package.emojiCount <= 0 || !g_emoji_package.emojis) { WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] no emojis to download"); return; } - // 下载表情 - ln_emoji_download(&g_emoji_package); - uapi_ln_set_file_download_type(LN_FILE_DOWNLOAD_TYPE_NONE); + + // 如果任务已经在运行,不重复创建 + if (g_emoji_download_task_running) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 表情下载任务已在运行中"); + return; + } + + // 创建独立任务执行下载,避免在GUI线程中执行导致栈溢出 + g_emoji_download_taskcfg.attr.name = "ln_emoji_download_task"; + g_emoji_download_taskcfg.attr.priority = osPriorityNormal; + g_emoji_download_taskcfg.attr.stack_size = 0x3000; // 12KB栈空间,足够下载任务使用 + g_emoji_download_taskcfg.func = ln_emoji_download_task_wrapper; + g_emoji_download_taskcfg.use_mem = USE_EXTEND_MEM; // 使用扩展内存(PSRAM) + g_emoji_download_taskcfg.create_queue_handle = NULL; // 不需要消息队列 + + errcode_t ret = uapi_create_osThread_custom(&g_emoji_download_taskcfg); + if (ret != ERRCODE_SUCC) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 创建表情下载任务失败: %d", ret); + return; + } + + g_emoji_download_task_running = 1; + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 表情下载任务已创建"); } /* 解析下载表情命令JSON */ @@ -1026,13 +1099,31 @@ static uint32_t ln_emoji_report_status_http(const char *device_id, LOS_Msleep(200); #endif - HttpClient client = (HttpClient){0}; - HttpClientData clientData = (HttpClientData){0}; + /* 组装 JSON */ + cJSON *root = cJSON_CreateObject(); + if (!root) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[EMOJI_REPORT_STATUS] create root object failed"); + return ERRCODE_FAIL; + } + cJSON_AddStringToObject(root, "characterId", character_id); + cJSON_AddStringToObject(root, "eyesColor", eyes_color); + cJSON_AddStringToObject(root, "eyesShape", eyes_shape); + cJSON_AddStringToObject(root, "status", status); + cJSON_AddStringToObject(root, "description", description ? description : ""); + char *body = cJSON_PrintUnformatted(root); + cJSON_Delete(root); + if (!body) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[EMOJI_REPORT_STATUS] print unformatted failed"); + return ERRCODE_FAIL; + } - /* 直接构造自定义请求头,生命周期贯穿整个请求,结束后再释放 */ + /* 构造请求头 */ char *header_buf = (char *)malloc(512); if (!header_buf) { + free(body); WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[EMOJI_REPORT_STATUS] malloc header_buf failed"); return ERRCODE_FAIL; } @@ -1043,45 +1134,71 @@ static uint32_t ln_emoji_report_status_http(const char *device_id, device_id, device_token); if (hdr <= 0) { + free(body); free(header_buf); WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[EMOJI_REPORT_STATUS] build header failed"); return ERRCODE_FAIL; } - ZhihuiHttpClientSetCustomHeader(&client, header_buf); - /* 组装 JSON */ - cJSON *root = cJSON_CreateObject(); - if (!root) + /* 初始化HTTP客户端(使用通用接口) */ + /* 注意:HTTPS 证书和 SSL 配置已自动在 n706a_http_client_setup_connection 中处理 */ + ZhihuiHttpClientHandle http_client = NULL; + int init_ret = ZhihuiHttpClientInitAdvanced(&http_client); + if (init_ret != 0) { - WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[EMOJI_REPORT_STATUS] create root object failed"); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[EMOJI_REPORT_STATUS] Failed to init HTTP client"); + free(body); + free(header_buf); return ERRCODE_FAIL; } - cJSON_AddStringToObject(root, "characterId", character_id); - cJSON_AddStringToObject(root, "eyesColor", eyes_color); - cJSON_AddStringToObject(root, "eyesShape", eyes_shape); - cJSON_AddStringToObject(root, "status", status); - cJSON_AddStringToObject(root, "description", description ? description : ""); - char *body = cJSON_PrintUnformatted(root); - cJSON_Delete(root); - if (!body) + + /* 设置URL */ + int url_ret = ZhihuiHttpClientSetUrl(http_client, LN_EMOJI_REPORT_URL, 0); + if (url_ret != 0) { - WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[EMOJI_REPORT_STATUS] print unformatted failed"); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[EMOJI_REPORT_STATUS] Failed to set URL"); + free(body); + free(header_buf); + ZhihuiHttpClientClose(http_client); + return ERRCODE_FAIL; + } + + /* 建立HTTPS连接 */ + int conn_ret = ZhihuiHttpClientSetupConnection(http_client); + if (conn_ret != 0) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[EMOJI_REPORT_STATUS] Failed to setup HTTPS connection"); + free(body); + free(header_buf); + ZhihuiHttpClientClose(http_client); return ERRCODE_FAIL; } - HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, LN_CLIENT_PREPARE_SIZE / 2); - if (prep != HTTP_SUCCESS) + /* 设置请求头 */ + int header_ret = ZhihuiHttpClientSetHeader(http_client, header_buf); + if (header_ret != 0) { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[EMOJI_REPORT_STATUS] Failed to set header"); free(body); free(header_buf); - WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[EMOJI_REPORT_STATUS] prepare auto failed"); + ZhihuiHttpClientClose(http_client); return ERRCODE_FAIL; } - clientData.postBuf = body; - clientData.postBufLen = (int)strlen(body); - clientData.postContentType = (char *)"application/json"; - // 打印请求请求响应 + /* 准备响应数据缓冲区 */ + HttpClientData clientData = {0}; + clientData.responseBuf = (char *)malloc(LN_CLIENT_PREPARE_SIZE / 2); + clientData.responseBufLen = LN_CLIENT_PREPARE_SIZE / 2; + if (!clientData.responseBuf) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[EMOJI_REPORT_STATUS] Failed to allocate response buffer"); + free(body); + free(header_buf); + ZhihuiHttpClientClose(http_client); + return ERRCODE_FAIL; + } + + // 打印请求体 WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[EMOJI_REPORT_STATUS] request body: %s", body); // 使用说明: // - 该接口用于设备向服务器同步表情下载/删除状态 @@ -1089,25 +1206,30 @@ static uint32_t ln_emoji_report_status_http(const char *device_id, // - 服务器会根据上报的信息更新小程序端的表情状态显示 // - characterId、eyesColor、eyesShape三个字段组合标识一个唯一的表情组 - HTTPC_RESULT res = HttpClientPostRequest(&client, LN_EMOJI_REPORT_URL, &clientData); - if (res != HTTP_SUCCESS) + /* 发送POST请求 */ + int post_ret = ZhihuiHttpClientPost(http_client, NULL, "application/json", body, strlen(body), &clientData); + if (post_ret != 0) { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[EMOJI_REPORT_STATUS] POST request failed"); free(body); - ZhihuiHttpClientUnprepare(&clientData); free(header_buf); - WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[EMOJI_REPORT_STATUS] request failed"); + free(clientData.responseBuf); + ZhihuiHttpClientClose(http_client); return ERRCODE_FAIL; } - int code = HttpClientGetResponseCode(&client); + /* 检查HTTP响应码 */ + int code = ZhihuiHttpClientGetResponseCode(http_client); if (code != 200) { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[EMOJI_REPORT_STATUS] response code not 200"); free(body); - ZhihuiHttpClientUnprepare(&clientData); free(header_buf); - WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[EMOJI_REPORT_STATUS] response code not 200"); + free(clientData.responseBuf); + ZhihuiHttpClientClose(http_client); return ERRCODE_FAIL; } + if (clientData.responseBuf && clientData.responseBufLen > 0) { // { @@ -1121,6 +1243,10 @@ static uint32_t ln_emoji_report_status_http(const char *device_id, if (!response_buf) { WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[EMOJI_REPORT_STATUS] malloc failed for response_buf"); + free(body); + free(header_buf); + free(clientData.responseBuf); + ZhihuiHttpClientClose(http_client); return ERRCODE_FAIL; } memcpy(response_buf, clientData.responseBuf, real_len); @@ -1129,10 +1255,10 @@ static uint32_t ln_emoji_report_status_http(const char *device_id, free(response_buf); } - free(body); - ZhihuiHttpClientUnprepare(&clientData); free(header_buf); + free(clientData.responseBuf); + ZhihuiHttpClientClose(http_client); return ERRCODE_SUCC; } /* 7.2 上报设备所有表情组状态 */ @@ -1150,27 +1276,6 @@ static uint32_t ln_emoji_report_all_http(const char *device_id, LOS_Msleep(200); #endif - HttpClient client = (HttpClient){0}; - HttpClientData clientData = (HttpClientData){0}; - client.isHttp = false; - client.remotePort = 443; - - /* 直接构造自定义请求头,生命周期贯穿整个请求,结束后再释放 */ - char *header_buf = (char *)malloc(512); - if (!header_buf) - return ERRCODE_FAIL; - int hdr = snprintf(header_buf, 512, - "deviceId: %s\r\n" - "deviceToken: %s\r\n" - "Content-Type: application/json\r\n", - device_id, device_token); - if (hdr <= 0) - { - free(header_buf); - return ERRCODE_FAIL; - } - ZhihuiHttpClientSetCustomHeader(&client, header_buf); - /* 组装 JSON 数组 */ cJSON *root = cJSON_CreateObject(); if (!root) @@ -1200,56 +1305,133 @@ static uint32_t ln_emoji_report_all_http(const char *device_id, if (!body) return ERRCODE_FAIL; - HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, LN_CLIENT_PREPARE_SIZE * 2); - if (prep != HTTP_SUCCESS) + /* 构造请求头 */ + char *header_buf = (char *)malloc(512); + if (!header_buf) { + free(body); + return ERRCODE_FAIL; + } + int hdr = snprintf(header_buf, 512, + "deviceId: %s\r\n" + "deviceToken: %s\r\n" + "Content-Type: application/json\r\n", + device_id, device_token); + if (hdr <= 0) + { + free(body); + free(header_buf); + return ERRCODE_FAIL; + } + + /* 初始化HTTP客户端(使用通用接口) */ + /* 注意:HTTPS 证书和 SSL 配置已自动在 n706a_http_client_setup_connection 中处理 */ + ZhihuiHttpClientHandle http_client = NULL; + int init_ret = ZhihuiHttpClientInitAdvanced(&http_client); + if (init_ret != 0) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[EMOJI_REPORT_ALL] Failed to init HTTP client"); free(body); free(header_buf); return ERRCODE_FAIL; } - clientData.postBuf = body; - clientData.postBufLen = (int)strlen(body); - clientData.postContentType = (char *)"application/json"; + + /* 设置URL */ + int url_ret = ZhihuiHttpClientSetUrl(http_client, LN_EMOJI_REPORT_ALL_URL, 0); + if (url_ret != 0) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[EMOJI_REPORT_ALL] Failed to set URL"); + free(body); + free(header_buf); + ZhihuiHttpClientClose(http_client); + return ERRCODE_FAIL; + } + + /* 建立HTTPS连接 */ + int conn_ret = ZhihuiHttpClientSetupConnection(http_client); + if (conn_ret != 0) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[EMOJI_REPORT_ALL] Failed to setup HTTPS connection"); + free(body); + free(header_buf); + ZhihuiHttpClientClose(http_client); + return ERRCODE_FAIL; + } + + /* 设置请求头 */ + int header_ret = ZhihuiHttpClientSetHeader(http_client, header_buf); + if (header_ret != 0) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[EMOJI_REPORT_ALL] Failed to set header"); + free(body); + free(header_buf); + ZhihuiHttpClientClose(http_client); + return ERRCODE_FAIL; + } + + /* 准备响应数据缓冲区 */ + HttpClientData clientData = {0}; + clientData.responseBuf = (char *)malloc(LN_CLIENT_PREPARE_SIZE * 2); + clientData.responseBufLen = LN_CLIENT_PREPARE_SIZE * 2; + if (!clientData.responseBuf) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[EMOJI_REPORT_ALL] Failed to allocate response buffer"); + free(body); + free(header_buf); + ZhihuiHttpClientClose(http_client); + return ERRCODE_FAIL; + } //打印请求体 WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[EMOJI_REPORT_ALL] request body: %s", body); - HTTPC_RESULT res = HttpClientPostRequest(&client, LN_EMOJI_REPORT_ALL_URL, &clientData); - if (res != HTTP_SUCCESS) + /* 发送POST请求 */ + int post_ret = ZhihuiHttpClientPost(http_client, NULL, "application/json", body, strlen(body), &clientData); + if (post_ret != 0) { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[EMOJI_REPORT_ALL] POST request failed"); free(body); - ZhihuiHttpClientUnprepare(&clientData); free(header_buf); + free(clientData.responseBuf); + ZhihuiHttpClientClose(http_client); return ERRCODE_FAIL; } - int code = HttpClientGetResponseCode(&client); + /* 检查HTTP响应码 */ + int code = ZhihuiHttpClientGetResponseCode(http_client); if (code != 200) { WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[EMOJI_REPORT_ALL] HTTP %d", code); free(body); - ZhihuiHttpClientUnprepare(&clientData); free(header_buf); + free(clientData.responseBuf); + ZhihuiHttpClientClose(http_client); return ERRCODE_FAIL; } + if (clientData.responseBuf && clientData.responseBufLen > 0) { int real_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen; char *response_buf = (char *)malloc(real_len + 1); if (!response_buf) { - WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[EMOJI_REPORT_STATUS] malloc failed for response_buf"); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[EMOJI_REPORT_ALL] malloc failed for response_buf"); + free(body); + free(header_buf); + free(clientData.responseBuf); + ZhihuiHttpClientClose(http_client); return ERRCODE_FAIL; } memcpy(response_buf, clientData.responseBuf, real_len); response_buf[real_len] = '\0'; - WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[EMOJI_REPORT_STATUS] response body: %s", response_buf); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[EMOJI_REPORT_ALL] response body: %s", response_buf); free(response_buf); } free(body); - ZhihuiHttpClientUnprepare(&clientData); free(header_buf); + free(clientData.responseBuf); + ZhihuiHttpClientClose(http_client); return ERRCODE_SUCC; } diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_ota.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_ota.c index 7d99321c0..4e3af9105 100644 --- a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_ota.c +++ b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_ota.c @@ -244,11 +244,6 @@ uint32_t ln_ota_get_task_http(const char *device_id, LOS_Msleep(200); #endif - HttpClient client = (HttpClient){0}; - HttpClientData clientData = (HttpClientData){0}; - client.isHttp = false; - client.remotePort = 443; - /* 头:与 device info 一致,额外带 model/version 可辅助服务端日志定位 */ size_t hdr_cap = 64 + strlen(device_id) + strlen(device_token) + (uapi_ln_get_device_model() ? strlen(uapi_ln_get_device_model()) : 5) + @@ -270,21 +265,12 @@ uint32_t ln_ota_get_task_http(const char *device_id, free(header_buf); return ERRCODE_FAIL; } - ZhihuiHttpClientSetCustomHeader(&client, header_buf); - - HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, 8 * 1024); - if (prep != HTTP_SUCCESS) - { - printf("[OTA] getTask prepare failed %d\n", prep); - return ERRCODE_FAIL; - } /* 拼 GET URL 参数 */ size_t url_cap = strlen(LN_OTA_GET_TASK_URL) + strlen(device_id) + strlen(source_version) + 64; char *url_with_params = (char *)malloc(url_cap); if (!url_with_params) { - ZhihuiHttpClientUnprepare(&clientData); free(header_buf); return ERRCODE_FAIL; } @@ -293,7 +279,6 @@ uint32_t ln_ota_get_task_http(const char *device_id, LN_OTA_GET_TASK_URL, device_id, source_version); if (ulen <= 0 || (size_t)ulen >= url_cap) { - ZhihuiHttpClientUnprepare(&clientData); free(header_buf); free(url_with_params); return ERRCODE_FAIL; @@ -301,23 +286,91 @@ uint32_t ln_ota_get_task_http(const char *device_id, printf("[OTA] GET %s\n", url_with_params); - HTTPC_RESULT res = ZhihuiHttpClientGetRequest(&client, url_with_params, &clientData); - if (res != HTTP_SUCCESS) + /* 初始化HTTP客户端(使用通用接口) */ + /* 注意:HTTPS 证书和 SSL 配置已自动在 n706a_http_client_setup_connection 中处理 */ + ZhihuiHttpClientHandle http_client = NULL; + int init_ret = ZhihuiHttpClientInitAdvanced(&http_client); + if (init_ret != 0) { - printf("[OTA] HttpClientGetRequest error %d\n", res); - goto end; + printf("[OTA] Failed to init HTTP client\n"); + free(header_buf); + free(url_with_params); + return ERRCODE_FAIL; } - int code = HttpClientGetResponseCode(&client); + /* 设置URL(带参数) */ + int url_ret = ZhihuiHttpClientSetUrl(http_client, url_with_params, 0); + if (url_ret != 0) + { + printf("[OTA] Failed to set URL\n"); + free(header_buf); + free(url_with_params); + ZhihuiHttpClientClose(http_client); + return ERRCODE_FAIL; + } + + /* 建立HTTPS连接 */ + int conn_ret = ZhihuiHttpClientSetupConnection(http_client); + if (conn_ret != 0) + { + printf("[OTA] Failed to setup HTTPS connection\n"); + free(header_buf); + free(url_with_params); + ZhihuiHttpClientClose(http_client); + return ERRCODE_FAIL; + } + + /* 设置请求头 */ + int header_ret = ZhihuiHttpClientSetHeader(http_client, header_buf); + if (header_ret != 0) + { + printf("[OTA] Failed to set header\n"); + free(header_buf); + free(url_with_params); + ZhihuiHttpClientClose(http_client); + return ERRCODE_FAIL; + } + + /* 准备响应数据缓冲区 */ + HttpClientData clientData = {0}; + clientData.responseBuf = (char *)malloc(8 * 1024); + clientData.responseBufLen = 8 * 1024; + if (!clientData.responseBuf) + { + printf("[OTA] Failed to allocate response buffer\n"); + free(header_buf); + free(url_with_params); + ZhihuiHttpClientClose(http_client); + return ERRCODE_FAIL; + } + + /* 发送GET请求 */ + int get_ret = ZhihuiHttpClientGet(http_client, NULL, &clientData); + if (get_ret != 0) + { + printf("[OTA] GET request error %d\n", get_ret); + free(header_buf); + free(url_with_params); + free(clientData.responseBuf); + ZhihuiHttpClientClose(http_client); + return ERRCODE_FAIL; + } + + /* 检查HTTP响应码 */ + int code = ZhihuiHttpClientGetResponseCode(http_client); if (code != 200) { - printf("[OTA] HttpClientGetResponseCode %d\n", code); + printf("[OTA] HTTP response code %d\n", code); if (clientData.responseBuf && clientData.responseBufLen > 0) { int real_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen; ln_handle_ota_task_response(clientData.responseBuf, real_len); } - goto end; + free(header_buf); + free(url_with_params); + free(clientData.responseBuf); + ZhihuiHttpClientClose(http_client); + return ERRCODE_FAIL; } if (clientData.responseBuf && clientData.responseBufLen > 0) @@ -326,13 +379,10 @@ uint32_t ln_ota_get_task_http(const char *device_id, ln_handle_ota_task_response(clientData.responseBuf, real_len); } -end: - ZhihuiHttpClientUnprepare(&clientData); - ZhihuiHttpClientClse(&client); - if (header_buf) - free(header_buf); - if (url_with_params) - free(url_with_params); + free(header_buf); + free(url_with_params); + free(clientData.responseBuf); + ZhihuiHttpClientClose(http_client); return ERRCODE_SUCC; } diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_timeline_activities.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_timeline_activities.c index 42604fe66..e81054649 100644 --- a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_timeline_activities.c +++ b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_timeline_activities.c @@ -69,11 +69,6 @@ uint32_t ln_timeline_get_status_http(const char *device_id, #endif uapi_pm_add_sleep_veto(PM_ID_DEBUG); - HttpClient client = (HttpClient){0}; - HttpClientData clientData = (HttpClientData){0}; - client.isHttp = false; - client.remotePort = 443; - char *header_buf = (char *)malloc(512); if (!header_buf) { @@ -91,36 +86,96 @@ uint32_t ln_timeline_get_status_http(const char *device_id, uapi_pm_remove_sleep_veto(PM_ID_DEBUG); return ERRCODE_FAIL; } - ZhihuiHttpClientSetCustomHeader(&client, header_buf); - HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, LN_CLIENT_PREPARE_SIZE*2); - if (prep != HTTP_SUCCESS) + /* 初始化HTTP客户端(使用通用接口) */ + /* 注意:HTTPS 证书和 SSL 配置已自动在 n706a_http_client_setup_connection 中处理 */ + ZhihuiHttpClientHandle http_client = NULL; + int init_ret = ZhihuiHttpClientInitAdvanced(&http_client); + if (init_ret != 0) { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TIMELINE] Failed to init HTTP client"); free(header_buf); uapi_pm_remove_sleep_veto(PM_ID_DEBUG); return ERRCODE_FAIL; } - HTTPC_RESULT res = ZhihuiHttpClientGetRequest(&client, LN_TIMELINE_GET_STATUS_URL, &clientData); - if (res != HTTP_SUCCESS) + /* 设置URL */ + int url_ret = ZhihuiHttpClientSetUrl(http_client, LN_TIMELINE_GET_STATUS_URL, 0); + if (url_ret != 0) { - goto end; + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TIMELINE] Failed to set URL"); + free(header_buf); + ZhihuiHttpClientClose(http_client); + uapi_pm_remove_sleep_veto(PM_ID_DEBUG); + return ERRCODE_FAIL; } - int code = HttpClientGetResponseCode(&client); + + /* 建立HTTPS连接 */ + int conn_ret = ZhihuiHttpClientSetupConnection(http_client); + if (conn_ret != 0) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TIMELINE] Failed to setup HTTPS connection"); + free(header_buf); + ZhihuiHttpClientClose(http_client); + uapi_pm_remove_sleep_veto(PM_ID_DEBUG); + return ERRCODE_FAIL; + } + + /* 设置请求头 */ + int header_ret = ZhihuiHttpClientSetHeader(http_client, header_buf); + if (header_ret != 0) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TIMELINE] Failed to set header"); + free(header_buf); + ZhihuiHttpClientClose(http_client); + uapi_pm_remove_sleep_veto(PM_ID_DEBUG); + return ERRCODE_FAIL; + } + + /* 准备响应数据缓冲区 */ + HttpClientData clientData = {0}; + clientData.responseBuf = (char *)malloc(LN_CLIENT_PREPARE_SIZE * 2); + clientData.responseBufLen = LN_CLIENT_PREPARE_SIZE * 2; + if (!clientData.responseBuf) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TIMELINE] Failed to allocate response buffer"); + free(header_buf); + ZhihuiHttpClientClose(http_client); + uapi_pm_remove_sleep_veto(PM_ID_DEBUG); + return ERRCODE_FAIL; + } + + /* 发送GET请求 */ + int get_ret = ZhihuiHttpClientGet(http_client, NULL, &clientData); + if (get_ret != 0) + { + free(header_buf); + free(clientData.responseBuf); + ZhihuiHttpClientClose(http_client); + uapi_pm_remove_sleep_veto(PM_ID_DEBUG); + return ERRCODE_FAIL; + } + + /* 检查HTTP响应码 */ + int code = ZhihuiHttpClientGetResponseCode(http_client); if (code != 200) { - goto end; + free(header_buf); + free(clientData.responseBuf); + ZhihuiHttpClientClose(http_client); + uapi_pm_remove_sleep_veto(PM_ID_DEBUG); + return ERRCODE_FAIL; } + if (clientData.responseBuf && clientData.responseBufLen > 0) { int real_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen; ln_parse_timeline_status_and_notify(clientData.responseBuf, real_len); } -end: - ZhihuiHttpClientUnprepare(&clientData); - ZhihuiHttpClientClse(&client); free(header_buf); + free(clientData.responseBuf); + ZhihuiHttpClientClose(http_client); uapi_pm_remove_sleep_veto(PM_ID_DEBUG); return ERRCODE_SUCC; } diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_token.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_token.c index bbfb36908..bcb8e600f 100644 --- a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_token.c +++ b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_token.c @@ -9,6 +9,8 @@ #include "mbedtls/md5.h" #include #include +/* 智慧网络统一接口(已包含 HTTP 接口) */ +#include "zhihui_net.h" static void ln_to_hex_lower(const uint8_t *in, size_t in_len, char *out) { @@ -236,11 +238,14 @@ uint32_t ln_get_device_token_http(const char *device_id, /* 1. 生成时间戳(ms)与nonce */ uint32_t ts_s = 0; /* epoch seconds */ - uapi_get_timestampe_ss(&ts_s); + if (uapi_get_timestampe_ss(&ts_s) != ERRCODE_SUCC) { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: Failed to get timestamp"); + return ERRCODE_FAIL; + } char timestamp[32] = {0}; - (void)snprintf(timestamp, sizeof(timestamp), "%u000", ts_s); /* 转毫秒字符串 */ + (void)snprintf(timestamp, sizeof(timestamp), "%u000", ts_s); /* 毫秒级时间戳字符串 */ - char nonce[17] = {0}; /* 12~16位,取固定16以简化 */ + char nonce[13] = {0}; /* 12位,与Python实现保持一致 */ ln_gen_nonce(nonce, sizeof(nonce)); /* 2. 计算签名: deviceId:deviceIccid:timestamp:nonce */ @@ -249,12 +254,21 @@ uint32_t ln_get_device_token_http(const char *device_id, if (!sign_data) return ERRCODE_FAIL; (void)snprintf(sign_data, sd_cap, "%s:%s:%s:%s", device_id, device_iccid, timestamp, nonce); + + /* 调试日志:打印签名数据 */ + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TOKEN] Sign data: %s", sign_data); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TOKEN] Timestamp: %s, Nonce: %s (len=%zu)", timestamp, nonce, strlen(nonce)); + char signature[65] = {0}; if (ln_hmac_sha256_hex(LN_SIGN_SECRET, sign_data, signature, sizeof(signature)) != 0) { WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: hmac calc failed"); + free(sign_data); return ERRCODE_FAIL; } + + /* 调试日志:打印生成的签名 */ + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[TOKEN] Generated signature: %s", signature); /* 3. 组织自定义请求头 */ char *header_buf = (char *)malloc(1024); @@ -291,84 +305,166 @@ uint32_t ln_get_device_token_http(const char *device_id, #endif uapi_pm_add_sleep_veto(PM_ID_DEBUG); - /* 5. 发起HTTPS GET */ - HttpClient client = (HttpClient){0}; - HttpClientData clientData = (HttpClientData){0}; - ZhihuiHttpClientSetCustomHeader(&client, header_buf); + /* 5. 初始化HTTP客户端(使用通用接口) */ + /* 注意:HTTPS 证书和 SSL 配置已自动在 n706a_http_client_setup_connection 中处理 */ + ZhihuiHttpClientHandle http_client = NULL; + int init_ret = ZhihuiHttpClientInitAdvanced(&http_client); + if (init_ret != 0) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: Failed to init HTTP client"); + free(sign_data); + free(header_buf); + uapi_pm_remove_sleep_veto(PM_ID_DEBUG); + return ERRCODE_FAIL; + } + + /* 7. 设置URL */ + int url_ret = ZhihuiHttpClientSetUrl(http_client, LN_FULL_URL, 0); + if (url_ret != 0) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: Failed to set URL"); + ZhihuiHttpClientClose(http_client); + free(sign_data); + free(header_buf); + uapi_pm_remove_sleep_veto(PM_ID_DEBUG); + return ERRCODE_FAIL; + } + + /* 8. 建立HTTPS连接 */ + int conn_ret = ZhihuiHttpClientSetupConnection(http_client); + if (conn_ret != 0) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: Failed to setup HTTPS connection"); + ZhihuiHttpClientClose(http_client); + free(sign_data); + free(header_buf); + uapi_pm_remove_sleep_veto(PM_ID_DEBUG); + return ERRCODE_FAIL; + } - HTTPC_RESULT prep = ZhihuiHttpClientPrepare(&clientData, 2048, 4096); - if (prep != HTTP_SUCCESS) + /* 9. 设置自定义请求头 */ + int header_ret = ZhihuiHttpClientSetHeader(http_client, header_buf); + if (header_ret != 0) { - WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: HttpClientPrepare failed %d", prep); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: Failed to set header"); + ZhihuiHttpClientClose(http_client); free(sign_data); free(header_buf); - uapi_pm_remove_sleep_veto(PM_ID_DEBUG); + uapi_pm_remove_sleep_veto(PM_ID_DEBUG); return ERRCODE_FAIL; } - HTTPC_RESULT res = ZhihuiHttpClientGetRequest(&client, LN_FULL_URL, &clientData); - if (res != HTTP_SUCCESS) + /* 10. 准备响应数据缓冲区(使用通用接口的数据结构) */ + HttpClientData client_data = {0}; + client_data.responseBuf = (char *)malloc(4096); + client_data.responseBufLen = 4096; + if (!client_data.responseBuf) { - WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: request failed %d", res); - ZhihuiHttpClientUnprepare(&clientData); - ZhihuiHttpClientClse(&client); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: Failed to allocate response buffer"); + ZhihuiHttpClientClose(http_client); free(sign_data); free(header_buf); - uapi_pm_remove_sleep_veto(PM_ID_DEBUG); + uapi_pm_remove_sleep_veto(PM_ID_DEBUG); return ERRCODE_FAIL; } - int code = HttpClientGetResponseCode(&client); + /* 11. 发送GET请求(带自定义headers,使用mode=99) */ + /* 注意:URL已经在set_url中设置完整,这里传入NULL表示使用已设置的URL路径 */ + int get_ret = ZhihuiHttpClientGet(http_client, NULL, &client_data); + if (get_ret != 0) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: GET request failed %d", get_ret); + free(client_data.responseBuf); + ZhihuiHttpClientClose(http_client); + free(sign_data); + free(header_buf); + uapi_pm_remove_sleep_veto(PM_ID_DEBUG); + return ERRCODE_FAIL; + } + + /* 12. 检查HTTP响应码 */ + int code = ZhihuiHttpClientGetResponseCode(http_client); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: HTTP response code=%d", code); if (code != 200) { - WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: HTTP %d", code); - ZhihuiHttpClientUnprepare(&clientData); - ZhihuiHttpClientClse(&client); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: HTTP %d (expected 200)", code); + if (client_data.responseBuf && client_data.responseBufLen > 0) + { + int print_len = (client_data.responseBufLen > 512) ? 512 : client_data.responseBufLen; + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: Response body (first %d bytes): %.512s", print_len, client_data.responseBuf); + } + free(client_data.responseBuf); + ZhihuiHttpClientClose(http_client); free(sign_data); free(header_buf); - uapi_pm_remove_sleep_veto(PM_ID_DEBUG); + uapi_pm_remove_sleep_veto(PM_ID_DEBUG); return ERRCODE_FAIL; } - /* 6. 解析响应JSON */ - if (clientData.responseBuf == NULL || clientData.responseBufLen <= 0) + /* 13. 解析响应JSON */ + if (client_data.responseBuf == NULL || client_data.responseBufLen <= 0) { - WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: empty body"); - ZhihuiHttpClientUnprepare(&clientData); - ZhihuiHttpClientClse(&client); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: empty body (responseBuf=%p, responseBufLen=%d)", + client_data.responseBuf, client_data.responseBufLen); + free(client_data.responseBuf); + ZhihuiHttpClientClose(http_client); free(sign_data); free(header_buf); - uapi_pm_remove_sleep_veto(PM_ID_DEBUG); + uapi_pm_remove_sleep_veto(PM_ID_DEBUG); return ERRCODE_FAIL; } - int body_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen; + /* 使用实际接收到的响应体长度,而不是Content-Length头部值 + * 因为Content-Length可能不准确,或者响应体可能包含额外的数据 + * responseBufLen是实际从HTTP响应中提取的响应体长度 + */ + int body_len = client_data.responseBufLen > 0 ? client_data.responseBufLen : (int)strlen(client_data.responseBuf); if (body_len <= 0) - body_len = (int)strlen(clientData.responseBuf); + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: Invalid body length (responseBufLen=%d)", client_data.responseBufLen); + free(client_data.responseBuf); + ZhihuiHttpClientClose(http_client); + free(sign_data); + free(header_buf); + uapi_pm_remove_sleep_veto(PM_ID_DEBUG); + return ERRCODE_FAIL; + } + + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: Response body length=%d bytes", body_len); + if (body_len > 0 && body_len <= 512) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: Response body %d bytes: %.*s", body_len, client_data.responseBuf); + } + else if (body_len > 512) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: Response body (first 512 bytes): %.512s", client_data.responseBuf); + } char *json_text = (char *)malloc((size_t)body_len + 1); if (json_text == NULL) { - ZhihuiHttpClientUnprepare(&clientData); - ZhihuiHttpClientClse(&client); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: failed to allocate memory for JSON text (%d bytes)", body_len); + free(client_data.responseBuf); + ZhihuiHttpClientClose(http_client); free(sign_data); free(header_buf); - uapi_pm_remove_sleep_veto(PM_ID_DEBUG); + uapi_pm_remove_sleep_veto(PM_ID_DEBUG); return ERRCODE_FAIL; } - memcpy(json_text, clientData.responseBuf, (size_t)body_len); + memcpy(json_text, client_data.responseBuf, (size_t)body_len); json_text[body_len] = '\0'; cJSON *root = cJSON_Parse(json_text); if (!root) { WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: JSON parse failed"); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: JSON text (first 256 chars): %.256s", json_text); free(json_text); - ZhihuiHttpClientUnprepare(&clientData); - ZhihuiHttpClientClse(&client); + free(client_data.responseBuf); + ZhihuiHttpClientClose(http_client); free(sign_data); free(header_buf); - uapi_pm_remove_sleep_veto(PM_ID_DEBUG); + uapi_pm_remove_sleep_veto(PM_ID_DEBUG); return ERRCODE_FAIL; } @@ -377,13 +473,17 @@ uint32_t ln_get_device_token_http(const char *device_id, { cJSON *msg = cJSON_GetObjectItem(root, "message"); /* 失败响应,打印errorCode和message 并返回失败 */ - WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: failed, err=%s", errorCode && cJSON_IsString(errorCode) ? errorCode->valuestring : "unknown"); - WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: failed, message=%s", msg && cJSON_IsString(msg) ? msg->valuestring : "unknown"); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: Server returned error, errorCode=%s", + errorCode && cJSON_IsString(errorCode) ? errorCode->valuestring : "unknown"); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: Server error message=%s", + msg && cJSON_IsString(msg) ? msg->valuestring : "unknown"); cJSON_Delete(root); free(json_text); - ZhihuiHttpClientUnprepare(&clientData); - ZhihuiHttpClientClse(&client); - uapi_pm_remove_sleep_veto(PM_ID_DEBUG); + free(client_data.responseBuf); + ZhihuiHttpClientClose(http_client); + free(sign_data); + free(header_buf); + uapi_pm_remove_sleep_veto(PM_ID_DEBUG); return ERRCODE_FAIL; } @@ -392,43 +492,80 @@ uint32_t ln_get_device_token_http(const char *device_id, { cJSON *token = cJSON_GetObjectItem(data, "token"); cJSON *expire = cJSON_GetObjectItem(data, "expireTime"); + + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: Found 'data' field in response"); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: token field exists=%s, expireTime field exists=%s", + token ? "yes" : "no", expire ? "yes" : "no"); + if (g_device_token) { free(g_device_token); g_device_token = NULL; } + if (token && cJSON_IsString(token)) { size_t tlen = strlen(token->valuestring); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: Extracted token from response, length=%zu", tlen); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: Token (first 64 chars)=%.64s", token->valuestring); + g_device_token = (char *)malloc(tlen + 1); if (g_device_token) { memcpy(g_device_token, token->valuestring, tlen + 1); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: Token copied to g_device_token successfully"); + } + else + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: ERROR - failed to allocate memory for token (%zu bytes)", tlen); } } + else + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: WARNING - token field is missing or not a string"); + } + if (g_device_token == NULL) { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: WARNING - g_device_token is NULL, setting to "); g_device_token = strdup(""); } + /* 落盘保存 */ uint64_t exp_ts = 0; if (expire && cJSON_IsNumber(expire)) { exp_ts = (uint64_t)expire->valuedouble; + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: Expire time from server: %llu", (unsigned long long)exp_ts); + } + else + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: WARNING - expireTime field is missing or not a number"); + } + + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: Saving token to file..."); + uint32_t save_ret = ln_token_save_json(g_device_token, exp_ts); + if (save_ret == ERRCODE_SUCC) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: Token saved successfully, length=%zu", strlen(g_device_token)); + } + else + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: WARNING - Token save failed (ret=%u)", save_ret); } - (void)ln_token_save_json(g_device_token, exp_ts); } else { - WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: no data field"); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: ERROR - no 'data' field in response"); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "ln_get_device_token_http: Response JSON: %s", json_text); } cJSON_Delete(root); free(json_text); - ZhihuiHttpClientUnprepare(&clientData); - ZhihuiHttpClientClse(&client); + free(client_data.responseBuf); + ZhihuiHttpClientClose(http_client); free(sign_data); free(header_buf); - uapi_pm_remove_sleep_veto(PM_ID_DEBUG); + uapi_pm_remove_sleep_veto(PM_ID_DEBUG); return ERRCODE_SUCC; } diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_websocket.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_websocket.c index 1a0f9a735..de464d865 100644 --- a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_websocket.c +++ b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_websocket.c @@ -36,7 +36,8 @@ typedef enum { } ln_ws_log_level_t; // 当前日志等级(可通过配置修改) -static ln_ws_log_level_t g_log_level = LN_WS_LOG_INFO; +// static ln_ws_log_level_t g_log_level = LN_WS_LOG_INFO; +static ln_ws_log_level_t g_log_level = LN_WS_LOG_DEBUG; // 打印函数 // 日志等级字符串数组 @@ -237,7 +238,17 @@ static void ln_ws_message_callback(const char *raw_data, uint32_t timestamp) (void)timestamp; if (raw_data == NULL) return; + /* 注意:raw_data 应该已经是以 \0 结尾的字符串(由 zhihui_net_websocket.c 保证) + * 但为了安全,使用 strlen 获取长度,并限制最大长度避免读取超出范围 + */ size_t length = strlen(raw_data); + /* 限制最大长度,避免读取超出实际数据范围(例如 64KB) */ + const size_t MAX_MESSAGE_LENGTH = 65536; + if (length > MAX_MESSAGE_LENGTH) { + LN_WS_WARN("Message length (%zu) exceeds maximum (%zu), truncating\n", length, MAX_MESSAGE_LENGTH); + length = MAX_MESSAGE_LENGTH; + } + char *buf = (char *)malloc(length + 1); if (!buf) return; memcpy(buf, raw_data, length); @@ -266,7 +277,29 @@ static void ln_ws_message_callback(const char *raw_data, uint32_t timestamp) ln_ws_incomplete_callback(raw_data,length,false); LN_WS_WARN("Invalid message\n"); } - LN_WS_DEBUG("on_message: %s\n", buf); + + /* 打印消息内容(限制长度,避免打印超出实际数据范围的内容) */ + /* 注意:buf 已经是以 \0 结尾的,length 是实际数据长度 */ + if (length > 0 && length <= MAX_MESSAGE_LENGTH) { + /* 只打印实际数据长度,避免打印缓冲区中的额外数据 */ + char *print_buf = (char *)malloc(length + 1); + if (print_buf) { + memcpy(print_buf, buf, length); + print_buf[length] = '\0'; + LN_WS_DEBUG("on_message: %s\n", print_buf); + free(print_buf); + } else { + /* 如果分配失败,直接打印(但限制长度) */ + const size_t MAX_PRINT_LEN = 256; + size_t print_len = (length < MAX_PRINT_LEN) ? length : MAX_PRINT_LEN; + char temp_buf[MAX_PRINT_LEN + 1]; + memcpy(temp_buf, buf, print_len); + temp_buf[print_len] = '\0'; + LN_WS_DEBUG("on_message: %s%s\n", temp_buf, (length > MAX_PRINT_LEN) ? "..." : ""); + } + } else { + LN_WS_DEBUG("on_message: (empty or invalid length=%zu)\n", length); + } free(buf); } @@ -276,8 +309,13 @@ static void ln_ws_event_callback(ws_event_type_t event_type) { switch (event_type) { case WS_EVENT_CONNECTING: - LN_WS_INFO("Event: Connecting\n"); - g_ws_connected = WS_STATE_CONNECTING; + // 如果当前已经是CONNECTED状态,不应该被重置为CONNECTING + if (g_ws_connected != WS_STATE_CONNECTED) { + LN_WS_INFO("Event: Connecting\n"); + g_ws_connected = WS_STATE_CONNECTING; + } else { + LN_WS_WARN("Event: Connecting ignored, already CONNECTED\n"); + } break; case WS_EVENT_CONNECTED: LN_WS_INFO("Event: Connected\n"); @@ -314,15 +352,29 @@ static void ln_ws_event_callback(ws_event_type_t event_type) static void ln_ws_url_callback(char *url, size_t url_size, char *header_buf, size_t header_size) { const char *base = "ws://ambybox-api.test.leiniao.com/ambybox-device-api/v1/websocket"; + // const char *base = "ws://snicker.com.cn:8910/ambybox-device-api/v1/websocket"; const char *device_id = uapi_ln_get_device_id(); const char *token = uapi_ln_get_device_token(); - if (!device_id) device_id = ""; - if (!token) token = ""; + + /* 检查device_id和token是否有效 */ + if (!device_id) { + device_id = ""; + LN_WS_WARN("Device ID is NULL, using empty string\n"); + } + + if (!token || strlen(token) == 0) { + LN_WS_WARN("Device token is empty or NULL, WebSocket connection will fail\n"); + LN_WS_WARN("Please ensure token is obtained via HTTPS before connecting\n"); + /* 即使token为空,也构建URL,但会在服务器端被拒绝(返回400) */ + token = ""; + } /* 仅使用 deviceToken 也可通过;额外带上 deviceId 与 reconnect 便于后端管理 */ snprintf(url, url_size, "%s?deviceToken=%s&deviceId=%s&reconnect=%u", base, token, device_id, g_ws_reconnect_count); + LN_WS_DEBUG("WebSocket URL: %s\n", url); + // /* 构建头部信息 */ snprintf(header_buf, header_size, "deviceId: %s\r\n", device_id); // 110.42.224.129 8910 diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_http_download.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_http_download.c new file mode 100644 index 000000000..41afaba5a --- /dev/null +++ b/code/sdk/zhihui/leiniao/imsl_ln/ln_http_download.c @@ -0,0 +1,891 @@ +/** + * @file ln_http_download.c + * @brief HTTP文件下载模块实现 - 使用统一的zhihui_net高级接口 + */ + +#include "ln_http_download.h" +#include "wearable_log.h" +#include "ln_api.h" /* 用于 LN_CLIENT_PREPARE_SIZE 等常量 */ +#include "soc_osal.h" /* 用于 osal_msleep */ +#include "custom_utils.h" /* 用于 app_task_definition_t, USE_EXTEND_MEM 和 PM 投票接口 */ +#include "thread_init.h" /* 确保包含 app_task_definition_t 和 USE_EXTEND_MEM */ +#include "cmsis_os2.h" /* 用于 osPriorityNormal */ +#include +#include +#include +#include +#include +#include + +/* 流式下载上下文结构(前向声明) */ +typedef struct { + FILE *fd; + size_t total_written; + int expected_content_length; + bool download_complete; + bool download_error; + size_t last_progress_bytes; /* 上次更新进度时的字节数(用于写入任务中的进度跟踪) */ +} ln_http_download_ctx_t; + +/* HTTP下载数据块结构(用于队列传递) */ +typedef struct { + FILE *fd; /* 文件句柄 */ + uint8_t *data; /* 数据指针(动态分配) */ + size_t length; /* 数据长度 */ + bool is_finished; /* 是否最后一个数据块 */ + ln_http_download_ctx_t *ctx; /* 下载上下文指针(用于更新统计) */ +} ln_http_download_block_t; + +/* 全局变量:文件写入队列和任务 */ +static void *g_http_download_queue = NULL; +static app_task_definition_t g_http_download_write_taskcfg = {0}; +static int g_http_download_write_task_running = 0; + +/* 自适应申请HttpClient缓冲(保证header>=2048),失败则逐步扩容重试 */ +int ln_http_prepare_auto(HttpClientData *clientData, int header_size, int resp_size) +{ + if (header_size < 2048) + header_size = 2048; + if (resp_size < 2048) + resp_size = 2048; + HTTPC_RESULT ret = ZhihuiHttpClientPrepare(clientData, header_size, resp_size); + if (ret == HTTP_SUCCESS) + return ret; + int tries = 0; + while (ret != HTTP_SUCCESS && tries < 3) + { + header_size = header_size < 4096 ? 4096 : header_size * 2; + resp_size = resp_size < (16 * 1024) ? (16 * 1024) : resp_size * 2; + if (resp_size > (256 * 1024)) + resp_size = (256 * 1024); + ret = ZhihuiHttpClientPrepare(clientData, header_size, resp_size); + tries++; + } + return ret; +} + +/* 流式下载上下文结构(已在前面定义) */ + +/* HTTP下载文件写入任务(从队列中取出数据块并写入文件) */ +static void ln_http_download_write_task_wrapper(void *arg) +{ + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] HTTP下载文件写入任务启动, queue=%p", g_http_download_queue); + + while (g_http_download_write_task_running) + { + ln_http_download_block_t block = {0}; + uint32_t msgsize = sizeof(ln_http_download_block_t); + + /* 从队列中读取数据块(阻塞等待) */ + errcode_t ret = osal_msg_queue_read_copy(g_http_download_queue, (void *)&block, &msgsize, + OSAL_MSGQ_WAIT_FOREVER); + if (ret != OSAL_SUCCESS) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] Write task: queue read failed: ret=%d", ret); + LOS_Msleep(10); + continue; + } + + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] Write task: received block: length=%zu, fd=%p, ctx=%p", + block.length, block.fd, block.ctx); + + /* 检查是否是结束标记(fd为NULL表示结束) */ + if (!block.fd || !block.data || block.length == 0) + { + if (block.data) + { + free(block.data); + } + continue; + } + + /* 写入文件 */ + size_t written = fwrite(block.data, 1, block.length, block.fd); + if (written != block.length) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] Write task: write failed: written=%zu, expected=%zu", + written, block.length); + if (block.ctx) + { + block.ctx->download_error = true; + } + } + else + { + /* 强制刷新文件缓冲区,确保数据写入磁盘 */ + fflush(block.fd); + + /* 更新统计 */ + if (block.ctx) + { + block.ctx->total_written += written; + + /* 如果是最后一个数据块,标记下载完成 */ + if (block.is_finished) + { + block.ctx->download_complete = true; + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] Write task: download complete: %zu bytes written", + block.ctx->total_written); + } + else + { + /* 定期打印进度(每100KB或每1%,取较小值) */ + size_t progress_interval = 100 * 1024; /* 100KB */ + if (block.ctx->expected_content_length > 0) { + /* 如果有期望大小,使用1%的间隔 */ + size_t percent_interval = block.ctx->expected_content_length / 100; + if (percent_interval < progress_interval) { + progress_interval = percent_interval; + } + } + + /* 检查是否需要更新进度 */ + size_t last_progress = block.ctx->last_progress_bytes; + if (block.ctx->total_written - last_progress >= progress_interval || + last_progress == 0) { + block.ctx->last_progress_bytes = block.ctx->total_written; + + /* 计算百分比 */ + int percent = 0; + if (block.ctx->expected_content_length > 0) { + percent = (int)(((double)block.ctx->total_written / block.ctx->expected_content_length) * 100); + if (percent > 100) percent = 100; + } + + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] Write task: progress: %zu/%d bytes written (%d%%)", + block.ctx->total_written, + block.ctx->expected_content_length, + percent); + } + } + } + } + + /* 释放数据内存 */ + free(block.data); + } + + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] HTTP下载文件写入任务结束"); +} + +/* 初始化HTTP下载文件写入队列和任务 */ +static int ln_http_download_init_write_queue(void) +{ + if (g_http_download_queue != NULL) + { + return ERRCODE_SUCC; /* 已经初始化 */ + } + + /* 创建消息队列 */ + if (osal_msg_queue_create("ln_http_download_queue", 32, &g_http_download_queue, + 0, sizeof(ln_http_download_block_t)) != ERRCODE_SUCC) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 创建HTTP下载队列失败"); + return ERRCODE_FAIL; + } + + /* 创建文件写入任务 */ + if (!g_http_download_write_task_running) + { + g_http_download_write_taskcfg.attr.name = "ln_http_download_write_task"; + g_http_download_write_taskcfg.attr.priority = osPriorityNormal; + g_http_download_write_taskcfg.attr.stack_size = 0x2000; /* 8KB栈空间 */ + g_http_download_write_taskcfg.func = ln_http_download_write_task_wrapper; + g_http_download_write_taskcfg.use_mem = USE_EXTEND_MEM; /* 使用扩展内存 */ + g_http_download_write_taskcfg.create_queue_handle = NULL; + + errcode_t ret = uapi_create_osThread_custom(&g_http_download_write_taskcfg); + if (ret != ERRCODE_SUCC) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 创建HTTP下载写入任务失败: %d", ret); + return ERRCODE_FAIL; + } + + g_http_download_write_task_running = 1; + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] HTTP下载文件写入队列和任务初始化成功"); + } + + return ERRCODE_SUCC; +} + +/* 流式接收回调函数(在URC到达时立即调用,将数据放入队列而不是直接写入文件) */ +static int ln_http_download_stream_callback(void *user_data, + const uint8_t *data, + size_t length, + bool is_finished) +{ + ln_http_download_ctx_t *ctx = (ln_http_download_ctx_t *)user_data; + + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] Stream callback called: length=%zu, is_finished=%d, ctx=%p, fd=%p", + length, is_finished, ctx, ctx ? ctx->fd : NULL); + + if (!ctx || !ctx->fd || !data || length == 0) { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] Stream callback: invalid parameters"); + return -1; + } + + /* 如果队列未初始化,降级处理:直接写入文件 */ + if (g_http_download_queue == NULL) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 队列未初始化,直接写入文件"); + /* 降级处理:直接写入文件 */ + size_t written = fwrite(data, 1, length, ctx->fd); + if (written != length) { + ctx->download_error = true; + return -1; + } + ctx->total_written += written; + if (is_finished) { + ctx->download_complete = true; + } + return 0; + } + + /* 分配内存并复制数据(避免在PPP缓冲区处理上下文中长时间持有数据) */ + /* 优化:使用calloc避免memset,减少栈使用 */ + uint8_t *data_copy = (uint8_t *)malloc(length); + if (!data_copy) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 分配内存失败,直接写入文件"); + /* 降级处理:直接写入文件 */ + size_t written = fwrite(data, 1, length, ctx->fd); + if (written != length) { + ctx->download_error = true; + return -1; + } + ctx->total_written += written; + if (is_finished) { + ctx->download_complete = true; + } + return 0; + } + + memcpy(data_copy, data, length); + + /* 构造数据块(优化:使用局部变量而非结构体初始化,减少栈使用) */ + /* 注意:osal_msg_queue_write_copy需要连续内存,所以不能直接传递指针 */ + /* 使用静态缓冲区存储结构体,避免在栈上分配 */ + static ln_http_download_block_t block_storage; /* 静态存储,避免栈分配 */ + ln_http_download_block_t *block = &block_storage; + block->fd = ctx->fd; + block->data = data_copy; + block->length = length; + block->is_finished = is_finished; + block->ctx = ctx; + + /* 将数据块放入队列(非阻塞,如果队列满则降级处理) */ + errcode_t ret = osal_msg_queue_write_copy(g_http_download_queue, (void *)block, + sizeof(ln_http_download_block_t), 0); + if (ret != OSAL_SUCCESS) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 队列写入失败(队列可能已满,ret=%d),直接写入文件", ret); + /* 降级处理:直接写入文件并释放内存 */ + size_t written = fwrite(data_copy, 1, length, ctx->fd); + free(data_copy); + if (written != length) { + ctx->download_error = true; + return -1; + } + ctx->total_written += written; + if (is_finished) { + ctx->download_complete = true; + } + return 0; + } + + /* 成功放入队列,返回0继续接收 */ + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 数据块已放入队列: length=%zu, total_written=%zu, is_finished=%d, queue=%p", + length, ctx->total_written, is_finished, g_http_download_queue); + return 0; +} + +/* 下载文件(基本功能) */ +int ln_http_download_file(const char *url, const char *save_path) +{ + if (!url || !save_path) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] url or save_path is NULL"); + return ERRCODE_FAIL; + } + + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 开始下载: %s -> %s", url, save_path); + + /* 添加 PM 投票,确保 HTTP 下载过程中有足够的 CPU 资源 */ + uapi_net_app_add_net_app_vote(); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 添加 PM_ID_NET_APP 投票,防止系统睡眠"); + + /* 确保队列和任务已初始化(在下载开始时初始化,而不是在回调中) */ + if (ln_http_download_init_write_queue() != ERRCODE_SUCC) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 初始化写入队列失败"); + /* 继续执行,回调中会降级处理 */ + } + + ZhihuiHttpClientHandle http_client = NULL; + HttpClientData client_data = {0}; + FILE *fd = NULL; + int ret = ERRCODE_FAIL; + ln_http_download_ctx_t download_ctx = {0}; + bool need_unlink = false; /* 标记是否需要删除文件 */ + + /* 打开文件准备写入 */ + fd = fopen(save_path, "wb+"); + if (!fd) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] fopen failed for %s", save_path); + /* 移除 PM 投票 */ + uapi_net_app_remove_net_app_vote(); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 移除 PM_ID_NET_APP 投票(fopen失败)"); + return ERRCODE_FAIL; + } + + /* 准备HTTP客户端缓冲区(用于响应头解析) */ + HTTPC_RESULT prep = ln_http_prepare_auto(&client_data, 2048, 2048); + if (prep != HTTP_SUCCESS) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] prepare failed: %d", prep); + goto cleanup; + } + + /* 初始化高级HTTP客户端 */ + if (ZhihuiHttpClientInitAdvanced(&http_client) != 0) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] init failed"); + goto cleanup; + } + + /* 设置流式接收回调(在URC到达时立即写入文件) */ + download_ctx.fd = fd; + download_ctx.total_written = 0; + download_ctx.expected_content_length = 0; + download_ctx.download_complete = false; + download_ctx.download_error = false; + download_ctx.last_progress_bytes = 0; + + if (ZhihuiHttpClientSetStreamCallback(http_client, + ln_http_download_stream_callback, + &download_ctx) != 0) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] set stream callback failed"); + goto cleanup; + } + + /* 设置URL */ + if (ZhihuiHttpClientSetUrl(http_client, url, 0) != 0) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] set url failed"); + need_unlink = true; /* 下载失败,需要删除文件 */ + goto cleanup; + } + + /* 建立连接(自动处理HTTPS证书) */ + if (ZhihuiHttpClientSetupConnection(http_client) != 0) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] setup connection failed"); + need_unlink = true; /* 下载失败,需要删除文件 */ + goto cleanup; + } + + /* 发送GET请求(响应头会立即解析,响应体数据会通过回调写入文件) */ + if (ZhihuiHttpClientGet(http_client, NULL, &client_data) != 0) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] send GET request failed"); + need_unlink = true; /* 下载失败,需要删除文件 */ + goto cleanup; + } + + /* 检查响应码 */ + int response_code = ZhihuiHttpClientGetResponseCode(http_client); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] response code: %d", response_code); + + if (response_code != 200) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] HTTP error: %d", response_code); + need_unlink = true; /* 下载失败,需要删除文件 */ + goto cleanup; + } + + /* 保存期望的内容长度(用于验证) */ + download_ctx.expected_content_length = client_data.responseContentLen; + + /* 流式模式下,数据已经通过回调写入文件 */ + /* 需要等待所有数据接收完成(通过循环调用 ZhihuiHttpClientRecvResponseAdvanced 等待后续URC) */ + size_t last_logged_bytes = download_ctx.total_written; + int wait_count = 0; + while (client_data.isMore && !download_ctx.download_complete && !download_ctx.download_error) + { + /* 等待后续URC数据到达(数据会通过回调写入文件) */ + int recv_res = ZhihuiHttpClientRecvResponseAdvanced(http_client, &client_data); + if (recv_res != HTTP_SUCCESS && recv_res != HTTP_EAGAIN) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] recv failed: %d", recv_res); + download_ctx.download_error = true; + break; + } + + /* 检查是否完成(通过回调函数设置的标志) */ + if (download_ctx.download_complete) { + break; + } + + /* 定期打印进度(每500ms或每100KB增加时) */ + wait_count++; + if (wait_count % 50 == 0 || (download_ctx.total_written - last_logged_bytes) >= (100 * 1024)) { + /* 检查实际文件大小(通过ftell) */ + long file_pos = -1; + if (download_ctx.fd) { + file_pos = ftell(download_ctx.fd); + } + + int percent = 0; + if (download_ctx.expected_content_length > 0) { + percent = (int)(((double)download_ctx.total_written / download_ctx.expected_content_length) * 100); + if (percent > 100) percent = 100; + } + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 下载进度: total_written=%zu, file_pos=%ld, expected=%d bytes (%d%%), isMore=%d", + download_ctx.total_written, + file_pos, + download_ctx.expected_content_length, + percent, + client_data.isMore); + last_logged_bytes = download_ctx.total_written; + } + + /* 短暂延迟,避免CPU占用过高 */ + osal_msleep(10); + } + + /* 检查下载是否成功 */ + if (download_ctx.download_error) { + ret = ERRCODE_FAIL; + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 文件下载失败(写入错误),删除不完整文件: %s", save_path); + need_unlink = true; + } else if (download_ctx.download_complete || !client_data.isMore) { + /* 验证下载完整性 */ + /* 注意:download_complete 可能在没有 Content-Length 时被错误设置为 true(第一个数据块) */ + /* 因此,需要同时检查 isMore 是否为 false,确保所有数据都已接收 */ + if (download_ctx.expected_content_length > 0) { + /* 有 Content-Length,验证长度是否匹配 */ + if (download_ctx.total_written == (size_t)download_ctx.expected_content_length) { + ret = ERRCODE_SUCC; + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 文件下载成功: %zu bytes", download_ctx.total_written); + } else { + ret = ERRCODE_FAIL; + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 文件下载失败(长度不匹配),删除不完整文件: %s", save_path); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] Expected: %d, Actual: %zu", + download_ctx.expected_content_length, download_ctx.total_written); + need_unlink = true; + } + } else { + /* 没有 Content-Length,需要确保 isMore 为 false(所有数据已接收) */ + if (!client_data.isMore) { + /* isMore 为 false,说明服务器已关闭连接,所有数据已接收 */ + ret = ERRCODE_SUCC; + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 文件下载成功: %zu bytes (no Content-Length, connection closed)", + download_ctx.total_written); + } else { + /* isMore 仍为 true,说明还有更多数据,但 download_complete 被错误设置为 true */ + ret = ERRCODE_FAIL; + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 文件下载失败(未完成,isMore=true),删除不完整文件: %s", save_path); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] Written: %zu bytes, isMore: %d", + download_ctx.total_written, client_data.isMore); + need_unlink = true; + } + } + } else { + ret = ERRCODE_FAIL; + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 文件下载失败(未完成),删除不完整文件: %s", save_path); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] download_complete: %d, isMore: %d", + download_ctx.download_complete, client_data.isMore); + need_unlink = true; + } + +cleanup: + /* 统一的资源清理路径,确保HTTP客户端完整生命周期结束 */ + /* 注意:清理顺序很重要,必须先禁用回调,再关闭HTTP客户端,最后释放缓冲区 */ + + /* 1. 禁用流式回调(如果已设置) */ + if (http_client != NULL) + { + ZhihuiHttpClientSetStreamCallback(http_client, NULL, NULL); + } + + /* 2. 关闭HTTP客户端(确保g_http_client被正确清理) */ + if (http_client != NULL) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 关闭HTTP客户端,确保完整生命周期结束"); + ZhihuiHttpClientClose(http_client); + http_client = NULL; /* 防止重复关闭 */ + } + + /* 3. 释放HTTP客户端缓冲区 */ + ZhihuiHttpClientUnprepare(&client_data); + + /* 4. 关闭文件句柄 */ + if (fd != NULL) + { + fclose(fd); + fd = NULL; + } + + /* 5. 如果下载失败,删除不完整的文件 */ + if (need_unlink && save_path != NULL) + { + unlink(save_path); + } + + /* 6. 移除 PM 投票,允许系统进入睡眠 */ + uapi_net_app_remove_net_app_vote(); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 移除 PM_ID_NET_APP 投票,允许系统睡眠"); + + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] HTTP客户端完整生命周期结束,返回码: %d", ret); + + return ret; +} + +/* 下载文件(带重试) */ +int ln_http_download_with_retry(const char *url, const char *save_path, int max_retries) +{ + if (url == NULL || save_path == NULL || (max_retries <= 0 || max_retries > 10)) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 参数无效"); + return ERRCODE_FAIL; + } + + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 开始下载(带重试): %s -> %s", url, save_path); + + int ret = ln_http_download_file(url, save_path); + if (ERRCODE_SUCC != ret) + { + for (int i = 1; i <= max_retries; i++) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 重试 %d/%d", i, max_retries); + ret = ln_http_download_file(url, save_path); + if (ERRCODE_SUCC == ret) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 重试成功 %d", i); + break; + } + } + } + + return ret; +} + +/* 下载文件(支持断点续传) */ +int ln_http_download_file_resume(const char *url, const char *save_path) +{ + if (!url || !save_path) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME] url or save_path is NULL"); + return ERRCODE_FAIL; + } + + long long resume_offset = 0; + /* 判断是否存在部分文件,计算续传偏移 */ + { + FILE *fexist = fopen(save_path, "rb"); + if (fexist) + { + if (fseek(fexist, 0, SEEK_END) == 0) + { + long pos = ftell(fexist); + if (pos > 0) + resume_offset = (long long)pos; + } + fclose(fexist); + } + } + + ZhihuiHttpClientHandle http_client = NULL; + HttpClientData client_data = {0}; + FILE *fd = NULL; + char *header_buf = NULL; + int ret = ERRCODE_FAIL; + size_t total_written = 0; + ln_http_download_ctx_t download_ctx = {0}; + + if (resume_offset > 0) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME] 断点续传,resume_offset=%lld", resume_offset); + } + + /* 添加 PM 投票,确保 HTTP 下载过程中有足够的 CPU 资源 */ + uapi_net_app_add_net_app_vote(); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME] 添加 PM_ID_NET_APP 投票,防止系统睡眠"); + + /* 以追加模式打开(若服务端返回200将切换为覆盖写)*/ + fd = fopen(save_path, resume_offset > 0 ? "ab+" : "wb+"); + if (!fd) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME] fopen failed for %s", save_path); + return ERRCODE_FAIL; + } + + /* 准备HTTP客户端缓冲区 */ + HTTPC_RESULT prep = ln_http_prepare_auto(&client_data, 2048, LN_CLIENT_PREPARE_SIZE * 2); + if (prep != HTTP_SUCCESS) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME] prepare failed: %d", prep); + goto cleanup; + } + + /* 初始化高级HTTP客户端 */ + if (ZhihuiHttpClientInitAdvanced(&http_client) != 0) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME] init failed"); + goto cleanup; + } + + /* 设置URL */ + if (ZhihuiHttpClientSetUrl(http_client, url, 0) != 0) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME] set url failed"); + goto cleanup; + } + + /* 设置Range请求头(仅当存在偏移时)*/ + if (resume_offset > 0) + { + header_buf = (char *)malloc(128); + if (!header_buf) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME] malloc header_buf failed"); + goto cleanup; + } + int hdr = snprintf(header_buf, 128, "Range: bytes=%lld-\r\n", resume_offset); + if (hdr <= 0) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME] snprintf header_buf failed"); + goto cleanup; + } + ZhihuiHttpClientSetHeader(http_client, header_buf); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME] set header %s", header_buf); + } + + /* 确保队列和任务已初始化(在下载开始时初始化,而不是在回调中) */ + if (ln_http_download_init_write_queue() != ERRCODE_SUCC) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME] 初始化写入队列失败"); + /* 继续执行,回调中会降级处理 */ + } + + /* 设置流式接收回调(在URC到达时立即写入文件) */ + download_ctx.fd = fd; + download_ctx.total_written = resume_offset; /* 初始化为续传偏移 */ + download_ctx.expected_content_length = 0; + download_ctx.download_complete = false; + download_ctx.download_error = false; + download_ctx.last_progress_bytes = resume_offset; /* 初始化进度跟踪 */ + + if (ZhihuiHttpClientSetStreamCallback(http_client, + ln_http_download_stream_callback, + &download_ctx) != 0) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME] set stream callback failed"); + goto cleanup; + } + + /* 建立连接(自动处理HTTPS证书) */ + if (ZhihuiHttpClientSetupConnection(http_client) != 0) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME] setup connection failed"); + goto cleanup; + } + + /* 发送GET请求(响应头会立即解析,响应体数据会通过回调写入文件) */ + if (ZhihuiHttpClientGet(http_client, NULL, &client_data) != 0) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME] send GET request failed"); + goto cleanup; + } + + /* 首次接收,判定状态码(200则改为覆盖写)*/ + int code = ZhihuiHttpClientGetResponseCode(http_client); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME] response code: %d (offset=%lld)", code, resume_offset); + if (code == 200 && resume_offset > 0) + { + /* 服务端未按Range返回,改为覆盖写 */ + fclose(fd); + fd = NULL; + fd = fopen(save_path, "wb+"); + if (!fd) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME] fopen failed for %s (overwrite mode)", save_path); + goto cleanup; + } + resume_offset = 0; + download_ctx.fd = fd; /* 更新文件句柄 */ + download_ctx.total_written = 0; /* 重置写入计数 */ + } + + /* 保存期望的内容长度(用于验证) */ + download_ctx.expected_content_length = client_data.responseContentLen; + + /* 流式模式下,数据已经通过回调写入文件 */ + /* 需要等待所有数据接收完成(通过循环调用 ZhihuiHttpClientRecvResponseAdvanced 等待后续URC) */ + size_t last_logged_bytes = download_ctx.total_written; + int wait_count = 0; + while (client_data.isMore && !download_ctx.download_complete && !download_ctx.download_error) + { + /* 等待后续URC数据到达(数据会通过回调写入文件) */ + int recv_res = ZhihuiHttpClientRecvResponseAdvanced(http_client, &client_data); + if (recv_res != HTTP_SUCCESS && recv_res != HTTP_EAGAIN) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME] recv failed: %d", recv_res); + download_ctx.download_error = true; + break; + } + + /* 检查是否完成(通过回调函数设置的标志) */ + if (download_ctx.download_complete) { + break; + } + + /* 定期打印进度(每500ms或每100KB增加时) */ + wait_count++; + if (wait_count % 50 == 0 || (download_ctx.total_written - last_logged_bytes) >= (100 * 1024)) { + /* 检查实际文件大小(通过ftell) */ + long file_pos = -1; + if (download_ctx.fd) { + file_pos = ftell(download_ctx.fd); + } + + int percent = 0; + if (download_ctx.expected_content_length > 0) { + percent = (int)(((double)download_ctx.total_written / download_ctx.expected_content_length) * 100); + if (percent > 100) percent = 100; + } + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME] 下载进度: total_written=%zu, file_pos=%ld, expected=%d bytes (%d%%), isMore=%d", + download_ctx.total_written, + file_pos, + download_ctx.expected_content_length, + percent, + client_data.isMore); + last_logged_bytes = download_ctx.total_written; + } + + /* 短暂延迟,避免CPU占用过高 */ + osal_msleep(10); + } + + /* 使用回调中的写入计数 */ + total_written = download_ctx.total_written; + + /* 禁用流式回调(必须在关闭HTTP客户端之前) */ + ZhihuiHttpClientSetStreamCallback(http_client, NULL, NULL); + + /* 验证下载完整性 */ + int final_code = ZhihuiHttpClientGetResponseCode(http_client); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME] final code=%d, total_written=%zu", final_code, total_written); + + /* 检查下载是否成功 */ + if (download_ctx.download_error) { + ret = ERRCODE_FAIL; + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME] 文件下载失败(写入错误),保留部分文件供下次续传"); + } else if (download_ctx.download_complete || !client_data.isMore) { + /* 验证下载完整性 */ + if (download_ctx.expected_content_length > 0) { + if (total_written == (size_t)download_ctx.expected_content_length) { + ret = ERRCODE_SUCC; + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME] 下载成功 (%s): %zu bytes", + resume_offset > 0 ? "ç»­ä¼ " : "全量", total_written); + } else { + ret = ERRCODE_FAIL; + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME] 下载失败(长度不匹配),保留部分文件供下次续传"); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME] Expected: %d, Actual: %zu", + download_ctx.expected_content_length, total_written); + } + } else { + /* 没有 Content-Length,需要确保 isMore 为 false(所有数据已接收) */ + if (!client_data.isMore) { + ret = ERRCODE_SUCC; + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME] 下载成功 (%s): %zu bytes (no Content-Length, connection closed)", + resume_offset > 0 ? "ç»­ä¼ " : "全量", total_written); + } else { + ret = ERRCODE_FAIL; + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME] 下载失败(未完成,isMore=true),保留部分文件供下次续传"); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME] Written: %zu bytes, isMore: %d", + total_written, client_data.isMore); + } + } + } else { + ret = ERRCODE_FAIL; + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME] 下载失败(未完成),保留部分文件供下次续传"); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME] download_complete: %d, isMore: %d", + download_ctx.download_complete, client_data.isMore); + } + +cleanup: + /* 统一的资源清理路径,确保HTTP客户端完整生命周期结束 */ + /* 注意:清理顺序很重要,必须先禁用回调,再关闭HTTP客户端,最后释放缓冲区 */ + + /* 1. 禁用流式回调(如果已设置) */ + if (http_client != NULL) + { + ZhihuiHttpClientSetStreamCallback(http_client, NULL, NULL); + } + + /* 2. 关闭HTTP客户端(确保g_http_client被正确清理) */ + if (http_client != NULL) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME] 关闭HTTP客户端,确保完整生命周期结束"); + ZhihuiHttpClientClose(http_client); + http_client = NULL; /* 防止重复关闭 */ + } + + /* 3. 释放HTTP客户端缓冲区 */ + ZhihuiHttpClientUnprepare(&client_data); + + /* 4. 释放请求头缓冲区 */ + if (header_buf != NULL) + { + free(header_buf); + header_buf = NULL; + } + + /* 5. 关闭文件句柄 */ + if (fd != NULL) + { + fclose(fd); + fd = NULL; + } + + /* 6. 移除 PM 投票,允许系统进入睡眠 */ + uapi_net_app_remove_net_app_vote(); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME] 移除 PM_ID_NET_APP 投票,允许系统睡眠"); + + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME] HTTP客户端完整生命周期结束,返回码: %d", ret); + + return ret; +} + +/* 下载文件(支持断点续传,带重试) */ +int ln_http_download_file_resume_with_retry(const char *url, const char *save_path, int max_retries) +{ + if (url == NULL || save_path == NULL || (max_retries <= 0 || max_retries > 10)) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME_WITH_RETRY] 参数无效"); + return ERRCODE_FAIL; + } + + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME_WITH_RETRY] 开始下载(带重试): %s -> %s", url, save_path); + + int ret = ln_http_download_file_resume(url, save_path); + if (ERRCODE_SUCC != ret) + { + for (int i = 1; i <= max_retries; i++) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME_WITH_RETRY] 重试 %d/%d", i, max_retries); + ret = ln_http_download_file_resume(url, save_path); + if (ERRCODE_SUCC == ret) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_RESUME_WITH_RETRY] 重试成功 %d", i); + break; + } + } + } + + return ret; +} + diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_http_download.h b/code/sdk/zhihui/leiniao/imsl_ln/ln_http_download.h new file mode 100644 index 000000000..2d16b9117 --- /dev/null +++ b/code/sdk/zhihui/leiniao/imsl_ln/ln_http_download.h @@ -0,0 +1,76 @@ +/** + * @file ln_http_download.h + * @brief HTTP文件下载模块 - 使用统一的zhihui_net高级接口 + * + * 此模块提供HTTP/HTTPS文件下载功能,支持: + * - 基本下载 + * - 带重试的下载 + * - 断点续传下载 + * - 带重试的断点续传下载 + * + * 所有下载功能均使用统一的zhihui_net高级接口,支持流式下载以节省内存。 + */ + +#ifndef LN_HTTP_DOWNLOAD_H +#define LN_HTTP_DOWNLOAD_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include "errcode.h" +#include "zhihui_net_http.h" + +/** + * @brief 下载文件(基本功能) + * @param url 文件URL + * @param save_path 保存路径 + * @return ERRCODE_SUCC 成功,ERRCODE_FAIL 失败 + * @note 如果下载失败,会自动删除不完整的文件 + */ +int ln_http_download_file(const char *url, const char *save_path); + +/** + * @brief 下载文件(带重试) + * @param url 文件URL + * @param save_path 保存路径 + * @param max_retries 最大重试次数(1-10) + * @return ERRCODE_SUCC 成功,ERRCODE_FAIL 失败 + */ +int ln_http_download_with_retry(const char *url, const char *save_path, int max_retries); + +/** + * @brief 下载文件(支持断点续传) + * @param url 文件URL + * @param save_path 保存路径(若存在则尝试从已下载大小处续传) + * @return ERRCODE_SUCC 成功,ERRCODE_FAIL 失败(失败不删除部分文件) + * @note 如果服务端不支持Range请求(返回200而非206),会从头开始下载 + */ +int ln_http_download_file_resume(const char *url, const char *save_path); + +/** + * @brief 下载文件(支持断点续传,带重试) + * @param url 文件URL + * @param save_path 保存路径 + * @param max_retries 最大重试次数(1-10) + * @return ERRCODE_SUCC 成功,ERRCODE_FAIL 失败 + */ +int ln_http_download_file_resume_with_retry(const char *url, const char *save_path, int max_retries); + +/** + * @brief 自适应申请HTTP客户端缓冲区 + * @param clientData HTTP客户端数据结构 + * @param header_size 响应头缓冲区大小(最小2048) + * @param resp_size 响应体缓冲区大小(最小2048) + * @return HTTP_SUCCESS 成功,其他值失败 + * @note 如果申请失败,会自动扩容重试(最多3次) + */ +int ln_http_prepare_auto(HttpClientData *clientData, int header_size, int resp_size); + +#ifdef __cplusplus +} +#endif + +#endif /* LN_HTTP_DOWNLOAD_H */ + diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/at_socket.py b/code/sdk/zhihui/leiniao/leiniao_python_test/at_socket.py new file mode 100644 index 000000000..69f1cd4be --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/at_socket.py @@ -0,0 +1,375 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +基于AT指令的标准Socket接口实现 +提供类似Python标准socket.socket的接口,支持TCP和TCP+SSL +""" + +import socket as std_socket +import time +from n706a_tcp_client import N706ATCPClient +from n706a_at_handler import ATCommandHandler + + +class ATSocket: + """ + 基于AT指令的标准Socket接口 + 提供类似Python标准socket.socket的接口,支持TCP和TCP+SSL + """ + + # Socket类型常量(兼容标准socket) + AF_INET = std_socket.AF_INET + SOCK_STREAM = std_socket.SOCK_STREAM + + def __init__(self, family=AF_INET, type=SOCK_STREAM, at_handler=None, socket_id=0): + """ + 创建AT Socket对象 + + Args: + family: 地址族(目前仅支持AF_INET) + type: Socket类型(目前仅支持SOCK_STREAM/TCP) + at_handler: ATCommandHandler实例(如果为None,需要后续调用set_at_handler设置) + socket_id: Socket ID(0-5),默认0 + """ + if family != self.AF_INET: + raise ValueError(f"不支持的地址族: {family},目前仅支持AF_INET") + if type != self.SOCK_STREAM: + raise ValueError(f"不支持的Socket类型: {type},目前仅支持SOCK_STREAM") + + self.family = family + self.type = type + self.at_handler = at_handler + self.socket_id = socket_id + self._tcp_client = None + self._timeout = None + self._connected = False + self._use_ssl = False + self._ssl_config = None + + # 如果提供了at_handler,立即创建TCP客户端 + if at_handler: + self._tcp_client = N706ATCPClient(at_handler, socket_id=socket_id) + + def set_at_handler(self, at_handler): + """ + 设置AT指令处理器(如果初始化时未提供) + + Args: + at_handler: ATCommandHandler实例 + """ + self.at_handler = at_handler + if not self._tcp_client: + self._tcp_client = N706ATCPClient(at_handler, socket_id=self.socket_id) + + def settimeout(self, timeout): + """ + 设置socket超时时间 + + Args: + timeout: 超时时间(秒),None表示阻塞模式 + """ + self._timeout = timeout + + def gettimeout(self): + """ + 获取socket超时时间 + + Returns: + float或None: 超时时间(秒),None表示阻塞模式 + """ + return self._timeout + + def configure_ssl(self, ssl_version=3, auth_mode=0, ca_cert=None, client_cert=None, client_key=None): + """ + 配置SSL/TLS参数(用于TCP+SSL连接) + + Args: + ssl_version: SSL版本,0=SSL3.0, 1=TLS1.0, 2=TLS1.1, 3=TLS1.2, 4=TLS1.3 + auth_mode: 认证模式,0=不认证, 1=单向认证, 2=双向认证 + ca_cert: CA证书文件名(单向/双向认证需要) + client_cert: 客户端证书文件名(双向认证需要) + client_key: 客户端密钥文件名(双向认证需要) + + Returns: + bool: 是否配置成功 + """ + if not self._tcp_client: + raise RuntimeError("AT handler未设置,无法配置SSL") + + self._use_ssl = True + self._ssl_config = { + 'ssl_version': ssl_version, + 'auth_mode': auth_mode, + 'ca_cert': ca_cert, + 'client_cert': client_cert, + 'client_key': client_key + } + + return self._tcp_client.configure_ssl( + ssl_version=ssl_version, + auth_mode=auth_mode, + ca_cert=ca_cert, + client_cert=client_cert, + client_key=client_key + ) + + def connect(self, address): + """ + 连接到服务器 + + Args: + address: (host, port) 元组,host可以是IP地址或域名 + + Raises: + OSError: 连接失败时抛出 + """ + if not self._tcp_client: + raise RuntimeError("AT handler未设置,无法连接") + + if len(address) != 2: + raise ValueError("address必须是(host, port)元组") + + host, port = address + + # 配置SSL(如果需要) + if self._use_ssl and self._ssl_config: + if not self._tcp_client.configure_ssl(**self._ssl_config): + raise OSError("SSL配置失败") + + # 建立连接 + if not self._tcp_client.connect( + server_ip=host, + server_port=port, + use_ssl=self._use_ssl, + transparent_mode=False # 非透传模式 + ): + raise OSError("连接失败") + + self._connected = True + + def send(self, data, flags=0): + """ + 发送数据 + + Args: + data: 要发送的数据(bytes) + flags: 标志位(目前未使用,为兼容性保留) + + Returns: + int: 实际发送的字节数 + + Raises: + OSError: 发送失败时抛出 + """ + if not self._connected: + raise OSError("Socket未连接") + + if not isinstance(data, bytes): + raise TypeError("data必须是bytes类型") + + if self._tcp_client.send(data): + return len(data) + else: + raise OSError("数据发送失败") + + def recv(self, bufsize, flags=0): + """ + 接收数据 + + Args: + bufsize: 最大接收字节数 + flags: 标志位(目前未使用,为兼容性保留) + + Returns: + bytes: 接收到的数据,如果连接关闭返回空bytes + + Raises: + OSError: 接收失败时抛出 + socket.timeout: 超时时抛出 + """ + if not self._connected: + raise OSError("Socket未连接") + + timeout = self._timeout if self._timeout is not None else 30 + + try: + data = self._tcp_client.receive(max_length=min(bufsize, 2048), timeout=timeout) + if data is None: + # 超时或连接关闭 + if self._timeout is not None: + raise std_socket.timeout("接收超时") + return b"" + return data + except Exception as e: + if isinstance(e, std_socket.timeout): + raise + raise OSError(f"接收数据失败: {e}") + + def recv_into(self, buffer, nbytes=0, flags=0): + """ + 接收数据到缓冲区 + + Args: + buffer: 接收缓冲区(可写字节对象) + nbytes: 最大接收字节数(0表示使用buffer长度) + flags: 标志位(目前未使用,为兼容性保留) + + Returns: + int: 实际接收的字节数 + """ + if nbytes == 0: + nbytes = len(buffer) + + data = self.recv(nbytes, flags) + n = len(data) + buffer[:n] = data + return n + + def close(self): + """ + 关闭socket连接 + """ + if self._connected and self._tcp_client: + self._tcp_client.close() + self._connected = False + + def shutdown(self, how): + """ + 关闭socket的发送/接收功能 + + Args: + how: SHUT_RD, SHUT_WR, 或 SHUT_RDWR + """ + # AT指令不支持部分关闭,直接关闭连接 + self.close() + + def getpeername(self): + """ + 获取远程地址 + + Returns: + tuple: (host, port) 元组 + """ + if not self._connected or not self._tcp_client: + raise OSError("Socket未连接") + + return (self._tcp_client.server_ip, self._tcp_client.server_port) + + def getsockname(self): + """ + 获取本地地址(AT指令不支持,返回默认值) + + Returns: + tuple: (host, port) 元组 + """ + # AT指令无法获取本地地址,返回默认值 + return ("0.0.0.0", 0) + + def fileno(self): + """ + 获取文件描述符(AT Socket不支持,抛出异常) + + Raises: + OSError: AT Socket不支持文件描述符 + """ + raise OSError("AT Socket不支持文件描述符") + + def setblocking(self, flag): + """ + 设置阻塞/非阻塞模式 + + Args: + flag: True表示阻塞模式,False表示非阻塞模式 + """ + if flag: + self._timeout = None + else: + # 非阻塞模式使用很小的超时时间 + self._timeout = 0.0 + + def getblocking(self): + """ + 获取阻塞/非阻塞模式 + + Returns: + bool: True表示阻塞模式,False表示非阻塞模式 + """ + return self._timeout is None + + def makefile(self, mode='r', buffering=-1, encoding=None, errors=None, newline=None): + """ + 创建文件对象(AT Socket不支持,抛出异常) + + Raises: + OSError: AT Socket不支持makefile + """ + raise OSError("AT Socket不支持makefile") + + def __enter__(self): + """上下文管理器入口""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """上下文管理器出口""" + self.close() + return False + + def __del__(self): + """析构函数""" + try: + self.close() + except: + pass + + +# 兼容标准socket模块的函数 +def create_connection(address, timeout=None, source_address=None, at_handler=None, socket_id=0, use_ssl=False, ssl_config=None): + """ + 创建TCP连接(兼容标准socket.create_connection) + + Args: + address: (host, port) 元组 + timeout: 超时时间(秒) + source_address: 源地址(AT Socket不支持,忽略) + at_handler: ATCommandHandler实例(必需) + socket_id: Socket ID(0-5),默认0 + use_ssl: 是否使用SSL/TLS,默认False + ssl_config: SSL配置字典,包含ssl_version, auth_mode, ca_cert等 + + Returns: + ATSocket: 已连接的AT Socket对象 + + Raises: + OSError: 连接失败时抛出 + """ + if at_handler is None: + raise ValueError("at_handler参数是必需的") + + sock = ATSocket(at_handler=at_handler, socket_id=socket_id) + + if timeout is not None: + sock.settimeout(timeout) + + if use_ssl and ssl_config: + sock.configure_ssl(**ssl_config) + + sock.connect(address) + return sock + + +def socket(family=ATSocket.AF_INET, type=ATSocket.SOCK_STREAM, proto=0, at_handler=None, socket_id=0): + """ + 创建AT Socket对象(兼容标准socket.socket) + + Args: + family: 地址族(目前仅支持AF_INET) + type: Socket类型(目前仅支持SOCK_STREAM) + proto: 协议(未使用) + at_handler: ATCommandHandler实例(可选,可后续设置) + socket_id: Socket ID(0-5),默认0 + + Returns: + ATSocket: AT Socket对象 + """ + return ATSocket(family=family, type=type, at_handler=at_handler, socket_id=socket_id) + diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/at_test_connect_tcp.py b/code/sdk/zhihui/leiniao/leiniao_python_test/at_test_connect_tcp.py index d68a08e58..4bb0dc326 100644 --- a/code/sdk/zhihui/leiniao/leiniao_python_test/at_test_connect_tcp.py +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/at_test_connect_tcp.py @@ -16,7 +16,7 @@ SERVER_HOST = "snicker.com.cn" SERVER_PORT = 8910 # AT模块串口配置(根据实际硬件配置修改) -SERIAL_PORT = "COM19" # 修改为实际的串口设备 +SERIAL_PORT = "COM8" # 修改为实际的串口设备 SERIAL_BAUDRATE = 1000000 SERIAL_TIMEOUT = 10 @@ -80,12 +80,12 @@ def connect_test_server(): print("\n" + "=" * 50) print("【步骤2: 配置网络省电模式】") print("=" * 50) - success, response = at_handler.send_at_command("AT+NWENPWRSAVE=1", timeout=5) + success, response = at_handler.send_at_command("AT+NWENPWRSAVE=0", timeout=5) if not success: print(f"⚠️ 网络省电模式配置失败: {response}") print("继续执行...") else: - print("✅ 网络省电模式已启用 (AT+NWENPWRSAVE=1)") + print("✅ 网络省电模式已启用 (AT+NWENPWRSAVE=0)") # 初始化TCP客户端 print("\n" + "=" * 50) @@ -126,43 +126,196 @@ def connect_test_server(): print(f"✅ {protocol_name}连接成功") - # 被动接收服务器消息 + # 交互式发送和接收 print("\n" + "=" * 50) - print("【步骤6: 被动接收服务器消息】") + print("【步骤6: 交互式发送和接收数据】") print("=" * 50) - print(f"等待服务器发送消息(超时时间: {RECEIVE_TIMEOUT}秒)...") - print("(按Ctrl+C中断)") + print("命令说明:") + print(" send - 发送HEX数据(例如: send 48656C6C6F)") + print(" send_ascii - 发送ASCII文本(例如: send_ascii Hello)") + print(" recv - 主动接收数据") + print(" status - 查询TCP连接状态(AT+IPSTATUS)") + print(" send_status - 查询TCP发送状态(AT+TCPACK)") + print(" wait - 切换到被动等待模式(自动接收)") + print(" interactive - 切换到交互模式(手动输入命令)") + print(" quit - 退出程序") print("=" * 50) message_count = 0 start_time = time.time() + wait_mode = False # 默认交互模式,方便用户输入命令 + + def print_received_data(data, msg_num, elapsed): + """打印接收到的数据""" + print(f"\n【接收消息 #{msg_num}】 (连接后 {elapsed:.1f}秒)") + print(f"数据长度: {len(data)} 字节") + + # 打印ASCII格式 + try: + text_data = data.decode('utf-8', errors='replace') + # 检查是否是可打印的ASCII文本 + if all(32 <= ord(c) < 127 or c in '\n\r\t' for c in text_data): + print(f"ASCII文本: {text_data}") + else: + print(f"ASCII文本 (含非打印字符): {repr(text_data)}") + except: + print("ASCII文本: (无法解码为UTF-8)") + + # 打印HEX格式 + hex_str = data.hex().upper() + if len(hex_str) > 200: + print(f"HEX字符串: {hex_str[:200]}... (共{len(hex_str)}字符)") + else: + print(f"HEX字符串: {hex_str}") + print("-" * 50) try: while True: - # 等待接收数据 - data = tcp_client.wait_for_data(timeout=RECEIVE_TIMEOUT) - - if data: - message_count += 1 - elapsed_time = time.time() - start_time + if wait_mode: + # 被动等待接收数据 + data = tcp_client.wait_for_data(timeout=RECEIVE_TIMEOUT) - print(f"\n【消息 #{message_count}】 (连接后 {elapsed_time:.1f}秒)") - print(f"数据长度: {len(data)} 字节") - - # 尝试解码为文本 - try: - text_data = data.decode('utf-8') - print(f"文本内容: {text_data}") - except: - print(f"二进制数据(hex): {data[:100].hex()}...") # 只显示前100字节 - - print("-" * 50) + if data: + message_count += 1 + elapsed_time = time.time() - start_time + print_received_data(data, message_count, elapsed_time) + else: + print(f"\n⚠️ 接收超时({RECEIVE_TIMEOUT}秒),继续等待...") + print("提示: 按 Ctrl+C 中断,然后输入 'interactive' 切换到交互模式") + print("-" * 50) else: - print(f"\n⚠️ 接收超时({RECEIVE_TIMEOUT}秒),继续等待...") - print("-" * 50) + # 交互模式:等待用户输入 + try: + user_input = input("\n请输入命令 (send/send_ascii/recv/status/send_status/wait/interactive/quit): ").strip() + + if not user_input: + continue + + if user_input.lower() == 'quit': + break + elif user_input.lower() == 'status': + # 查询TCP连接状态 + print("\n" + "=" * 50) + print("【查询TCP连接状态】") + print("=" * 50) + status_info = tcp_client.get_status() + if status_info['success']: + print(f" Socket ID: {status_info['socket_id']}") + print(f" 连接状态: {status_info['status']}") + if status_info['type']: + print(f" 链路类型: {status_info['type']}") + if status_info['buffer_size'] is not None: + print(f" 可用Buffer: {status_info['buffer_size']:,} 字节") + + # 根据状态给出提示 + if status_info['status'] == 'CONNECT': + print(" ✅ 连接正常") + if status_info['buffer_size'] is not None: + if status_info['buffer_size'] < 100: + print(" ⚠️ 警告: Buffer空间较小,可能影响数据发送") + elif status_info['status'] == 'DISCONNECT': + print(" ❌ 连接已断开") + elif status_info['status'] == 'CONNECTING': + print(" ⏳ 正在连接...") + elif status_info['status'] == 'DISCONNECTING': + print(" ⏳ 正在断开...") + else: + print(f"❌ 查询状态失败: {status_info.get('error', '未知错误')}") + print("=" * 50) + elif user_input.lower() == 'send_status': + # 查询TCP发送状态 + print("\n" + "=" * 50) + print("【查询TCP发送状态】") + print("=" * 50) + send_status = tcp_client.get_send_status() + if send_status['success']: + print(f" Socket ID: {send_status['socket_id']}") + print(f" 已发送数据: {send_status['data_sent']:,} 字节") + print(f" 已确认接收: {send_status['acked_recv']:,} 字节") + + if send_status['data_sent'] == send_status['acked_recv']: + print(" ✅ 所有数据已成功发送并被确认") + else: + diff = send_status['data_sent'] - send_status['acked_recv'] + pending_ratio = (diff / send_status['data_sent']) * 100 if send_status['data_sent'] > 0 else 0 + print(f" ⚠️ 警告: 有 {diff:,} 字节数据未确认 ({pending_ratio:.1f}%)") + print(f" 可能原因: 数据正在传输中或网络延迟") + if pending_ratio > 50: + print(f" ⚠️ 严重警告: 超过50%的数据未确认,建议检查网络连接") + else: + error_msg = send_status.get('error', '未知错误') + print(f"❌ 查询发送状态失败: {error_msg}") + if 'SSL TCP' in error_msg: + print(" 提示: SSL TCP连接不支持AT+TCPACK查询") + elif 'DISCONNECT' in error_msg: + print(" 提示: 连接已断开,请先建立TCP连接") + elif 'NO TCP LINK' in error_msg: + print(" 提示: 当前链路不是TCP连接(可能是UDP连接)") + print("=" * 50) + elif user_input.lower() == 'recv': + # 主动接收数据 + data = tcp_client.receive() + if data: + message_count += 1 + elapsed_time = time.time() - start_time + print_received_data(data, message_count, elapsed_time) + else: + print("❌ 未接收到数据") + elif user_input.lower() == 'wait': + wait_mode = True + print("✅ 切换到被动等待模式(自动接收数据)") + print("提示: 按 Ctrl+C 中断,然后输入 'interactive' 切换回交互模式") + elif user_input.lower() == 'interactive': + wait_mode = False + print("✅ 切换到交互模式(手动输入命令)") + elif user_input.startswith('send '): + # 发送HEX数据 + hex_str = user_input[5:].strip().replace(' ', '').upper() + if not hex_str: + print("❌ 错误: send 命令需要HEX字符串参数") + print(" 示例: send 48656C6C6F") + continue + try: + send_data = bytes.fromhex(hex_str) + print(f"发送HEX数据: {hex_str} (二进制长度: {len(send_data)} 字节)") + if tcp_client.send(send_data): + print("✅ 数据发送成功") + else: + print("❌ 数据发送失败") + except ValueError as e: + print(f"❌ HEX字符串格式错误: {e}") + print(" 提示: HEX字符串只能包含 0-9, A-F 字符") + elif user_input.startswith('send_ascii '): + # 发送ASCII文本 + ascii_text = user_input[11:].strip() + if not ascii_text: + print("❌ 错误: send_ascii 命令需要文本参数") + print(" 示例: send_ascii Hello World") + continue + send_data = ascii_text.encode('utf-8') + hex_str = send_data.hex().upper() + print(f"发送ASCII文本: {ascii_text}") + print(f" -> 二进制: {send_data}") + print(f" -> HEX字符串: {hex_str}") + if tcp_client.send(send_data): + print("✅ 数据发送成功") + else: + print("❌ 数据发送失败") + else: + print("❌ 未知命令") + print(" 可用命令: send, send_ascii, recv, wait, interactive, quit") + except EOFError: + # 如果没有交互式输入,切换到被动等待模式 + wait_mode = True + print("\n⚠️ 检测到非交互式环境,切换到被动等待模式...") + except KeyboardInterrupt: + # 用户按 Ctrl+C,切换到交互模式 + wait_mode = False + print("\n\n⚠️ 用户中断,切换到交互模式") + print("提示: 输入 'wait' 可切换回被动等待模式") except KeyboardInterrupt: - print("\n\n用户中断接收") + print("\n\n用户中断") # 关闭连接 print("\n" + "=" * 50) diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/at_test_download_file.py b/code/sdk/zhihui/leiniao/leiniao_python_test/at_test_download_file.py index edbec903a..d7f2ee7a6 100644 --- a/code/sdk/zhihui/leiniao/leiniao_python_test/at_test_download_file.py +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/at_test_download_file.py @@ -1,12 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -使用AT指令通过HTTP/HTTPS下载文件 +使用AT指令通过HTTP/HTTPS批量下载文件 使用重构后的公共模块实现 +支持从JSON数据中提取文件列表,批量下载并显示进度 """ import os import time +import json +import threading from n706a_at_handler import ATCommandHandler from n706a_http_client import N706AHTTPClient, load_certificate_file @@ -20,7 +23,7 @@ OUTPUT_DIR = "downloads" OUTPUT_FILENAME = "downloaded_file" # AT模块串口配置(根据实际硬件配置修改) -SERIAL_PORT = "COM19" # 修改为实际的串口设备 +SERIAL_PORT = "COM8" # 修改为实际的串口设备 SERIAL_BAUDRATE = 1000000 SERIAL_TIMEOUT = 10 @@ -30,17 +33,158 @@ AUTH_MODE = 1 # 0=无需认证, 1=单向认证, 2=双向认证 # CA_CERT_PATH = "certs/ca_chain.pem" CA_CERT_PATH = "certs/intermediate_cas.pem" -# -------------------------- 2. 下载文件函数 -------------------------- -def download_file(url, output_path, auth_mode=0, ca_cert_path=None): +# -------------------------- 2. 进度统计类 -------------------------- +class DownloadProgressTracker: + """下载进度跟踪器""" + def __init__(self, total_size): + self.total_size = total_size + self.downloaded_size = 0 + self.start_time = time.time() + self.last_print_time = time.time() + self.lock = threading.Lock() + self.print_interval = 2.0 # 每2秒打印一次进度 + + def update(self, bytes_downloaded): + """更新已下载字节数""" + with self.lock: + self.downloaded_size += bytes_downloaded + + def get_progress(self): + """获取当前进度信息""" + with self.lock: + elapsed = time.time() - self.start_time + percentage = (self.downloaded_size / self.total_size * 100) if self.total_size > 0 else 0 + speed = (self.downloaded_size / elapsed) if elapsed > 0 else 0 + return { + 'downloaded': self.downloaded_size, + 'total': self.total_size, + 'percentage': percentage, + 'elapsed': elapsed, + 'speed': speed + } + + def should_print(self): + """判断是否应该打印进度""" + current_time = time.time() + if current_time - self.last_print_time >= self.print_interval: + self.last_print_time = current_time + return True + return False + + def print_progress(self, current_file_name="", current_file_index=0, total_files=0): + """打印进度信息""" + progress = self.get_progress() + downloaded_mb = progress['downloaded'] / 1024 / 1024 + total_mb = progress['total'] / 1024 / 1024 + speed_mbps = progress['speed'] / 1024 / 1024 + + print(f"\r[进度] {current_file_index}/{total_files} 文件 | " + f"{progress['percentage']:.1f}% | " + f"{downloaded_mb:.2f}/{total_mb:.2f} MB | " + f"速度: {speed_mbps:.2f} MB/s | " + f"已用: {progress['elapsed']:.1f}s | " + f"当前: {current_file_name}", end='', flush=True) + +# -------------------------- 3. 下载单个文件函数 -------------------------- +def download_single_file(url, output_path, http_client, auth_mode=0, ca_cert_name=None, progress_tracker=None): """ - 使用AT指令通过HTTP/HTTPS下载文件 + 下载单个文件 Args: url: 文件的HTTP/HTTPS URL output_path: 保存路径 - auth_mode: 认证模式,0=无需认证, 1=单向认证, 2=双向认证(仅HTTPS需要) - ca_cert_path: CA证书路径(HTTPS单向认证时需要) + http_client: 已初始化的HTTP客户端 + auth_mode: 认证模式 + ca_cert_name: CA证书名称 + progress_tracker: 进度跟踪器 + + Returns: + (success, downloaded_size): 下载是否成功,下载的字节数 + """ + file_data = None + try: + # 设置URL + http_client.set_url(url) + protocol_name = "HTTPS" if http_client.is_https else "HTTP" + + # 配置HTTPS(如果需要) + if http_client.is_https and ca_cert_name: + if not http_client.configure_https( + ssl_version=3, # TLS1.2 + auth_mode=auth_mode, + ca_cert=ca_cert_name + ): + return False, 0 + + # 建立连接 + if not http_client.setup_connection(): + return False, 0 + + # 发送GET请求 + file_data = http_client.send_get_request(headers=None, timeout=600) + + if not file_data: + return False, 0 + + # 更新进度 + if progress_tracker: + progress_tracker.update(len(file_data)) + + # 保存文件 + os.makedirs(os.path.dirname(output_path) if os.path.dirname(output_path) else '.', exist_ok=True) + with open(output_path, 'wb') as f: + f.write(file_data) + + # 关闭连接 + try: + http_client.close_connection() + except: + pass # 忽略关闭连接时的错误 + + return True, len(file_data) + + except Exception as e: + # 尝试关闭连接 + try: + http_client.close_connection() + except: + pass + return False, 0 + +# -------------------------- 4. 批量下载函数 -------------------------- +def download_files_from_json(json_data, output_dir, auth_mode=0, ca_cert_path=None): """ + 从JSON数据中提取文件列表并批量下载 + + Args: + json_data: JSON字符串或字典,包含emotionEmojis列表 + output_dir: 输出目录 + auth_mode: 认证模式 + ca_cert_path: CA证书路径 + """ + # 解析JSON + if isinstance(json_data, str): + data = json.loads(json_data) + else: + data = json_data + + # 提取文件列表 + emotion_emojis = data.get('data', {}).get('emotionEmojis', []) + if not emotion_emojis: + print("❌ JSON数据中没有找到emotionEmojis列表") + return False + + # 计算总大小 + total_size = sum(emoji.get('size', 0) for emoji in emotion_emojis) + total_files = len(emotion_emojis) + + print(f"\n{'='*60}") + print(f"【批量下载任务】") + print(f"{'='*60}") + print(f"文件总数: {total_files}") + print(f"总大小: {total_size / 1024 / 1024:.2f} MB ({total_size} 字节)") + print(f"{'='*60}\n") + # 初始化AT指令处理器 at_handler = ATCommandHandler(SERIAL_PORT, SERIAL_BAUDRATE, SERIAL_TIMEOUT) @@ -58,200 +202,191 @@ def download_file(url, output_path, auth_mode=0, ca_cert_path=None): print("✅ AT模块连接正常") # 检查PPP拨号状态 - print("\n" + "=" * 50) - print("【步骤1: 检查PPP拨号状态】") - print("=" * 50) + print("\n【检查PPP拨号状态】") is_ppp_connected, ppp_ip = at_handler.check_ppp_status() if is_ppp_connected: print(f"✅ PPP已连接,IP地址: {ppp_ip}") else: - print(f"❌ PPP未连接,IP地址: {ppp_ip or '未知'}") - print("\n尝试建立PPP连接...") - + print(f"❌ PPP未连接,尝试建立连接...") apn_name = None success, ip = at_handler.establish_ppp_connection(apn_name=apn_name) - if not success: - print(f"❌ PPP连接建立失败,无法继续HTTP/HTTPS访问") + print(f"❌ PPP连接建立失败") return False - ppp_ip = ip + print(f"✅ PPP连接建立成功,IP地址: {ppp_ip}") - # 初始化HTTP/HTTPS客户端(自动判断协议类型) + # 初始化HTTP客户端 http_client = N706AHTTPClient(at_handler) - # 先设置URL以判断协议类型 - http_client.set_url(url) - # 处理证书(如果需要) ca_cert_name = None - if http_client.is_https and auth_mode == 1 and ca_cert_path: - print("\n" + "=" * 50) - print("【步骤2: 处理证书】") - print("=" * 50) - + if ca_cert_path: + print("\n【处理证书】") ca_cert_name = os.path.basename(ca_cert_path) - # 检查证书是否已存在 - if http_client.certificate_exists(ca_cert_name): - print(f"✅ 证书 {ca_cert_name} 已存在于模组中,跳过添加") - else: - # 证书不存在,需要添加 - print(f"证书 {ca_cert_name} 不存在,开始添加...") - - # 加载证书文件 - print(f"加载证书文件: {ca_cert_path}") - ca_cert_data = load_certificate_file(ca_cert_path) - if not ca_cert_data: - print("❌ 无法加载CA证书,终止流程") - return False - - # 添加证书到模组 - print(f"添加证书到模组: {ca_cert_name}") - if not http_client.add_certificate(ca_cert_name, ca_cert_data): - print("❌ CA证书添加失败,终止流程") - return False - - # 确认证书添加成功 - if not http_client.check_certificate(ca_cert_name): - print("❌ CA证书确认失败,终止流程") - return False - - print(f"✅ 证书 {ca_cert_name} 已成功添加到模组") - - # ========== 开始计时:HTTP/HTTPS访问耗时 ========== - access_start_time = time.time() - - # 配置HTTPS参数(如果需要) - # 重要:这里会明确指定使用哪个证书,确保不会混用其他证书 - if http_client.is_https: - print("\n" + "=" * 50) - print("【步骤3: 配置HTTPS参数】") - print("=" * 50) - if ca_cert_name: - print(f"明确指定使用证书: {ca_cert_name}") - if not http_client.configure_https( - ssl_version=3, # TLS1.2 - auth_mode=auth_mode, - ca_cert=ca_cert_name # 明确指定:本次HTTPS连接使用此证书 - ): - print("❌ HTTPS配置失败,终止流程") - return False + # 检查第一个URL判断是否需要HTTPS + first_url = emotion_emojis[0].get('emojiUrl', '') + http_client.set_url(first_url) - # 确认HTTPS配置 - print("\n确认HTTPS配置...") - config = http_client.verify_https_config() - if config: - print("✅ HTTPS配置确认成功") - else: - print("⚠️ HTTPS配置确认失败,但继续执行") + if http_client.is_https and auth_mode == 1: + if http_client.certificate_exists(ca_cert_name): + print(f"✅ 证书 {ca_cert_name} 已存在") + else: + print(f"添加证书 {ca_cert_name}...") + ca_cert_data = load_certificate_file(ca_cert_path) + if not ca_cert_data: + print("❌ 无法加载CA证书") + return False + if not http_client.add_certificate(ca_cert_name, ca_cert_data): + print("❌ CA证书添加失败") + return False + if not http_client.check_certificate(ca_cert_name): + print("❌ CA证书确认失败") + return False + print(f"✅ 证书 {ca_cert_name} 添加成功") - # URL已在前面设置,这里只显示信息 - protocol_name = "HTTPS" if http_client.is_https else "HTTP" - print("\n" + "=" * 50) - print(f"【步骤{3 if not http_client.is_https else 4}: {protocol_name}请求参数已设置】") - print("=" * 50) - print(f"请求URL: {url}") + # 创建进度跟踪器 + progress_tracker = DownloadProgressTracker(total_size) - # 建立连接 - print("\n" + "=" * 50) - print(f"【步骤{4 if not http_client.is_https else 5}: 建立{protocol_name}连接】") - print("=" * 50) - http_client.setup_connection() - print(f"✅ {protocol_name}连接建立成功") + # 开始批量下载 + print(f"\n{'='*60}") + print(f"【开始批量下载】") + print(f"{'='*60}\n") - # 发送GET请求 - print("\n" + "=" * 50) - print(f"【步骤{5 if not http_client.is_https else 6}: 发送{protocol_name} GET请求】") - print("=" * 50) + success_count = 0 + failed_count = 0 - # 使用标准GET模式(不传headers参数) - file_data = http_client.send_get_request(headers=None, timeout=600) - - # 保存文件 - print("\n" + "=" * 50) - print("【步骤7: 保存文件】") - print("=" * 50) - - # 创建输出目录 - os.makedirs(os.path.dirname(output_path), exist_ok=True) - - # 写入文件 - with open(output_path, 'wb') as f: - f.write(file_data) - - file_size = len(file_data) - print(f"✅ 文件保存成功: {output_path}") - print(f" 文件大小: {file_size} 字节 ({file_size/1024/1024:.2f} MB)") - - # 关闭连接 - print("\n" + "=" * 50) - print(f"【步骤8: 关闭{protocol_name}连接】") - print("=" * 50) - try: - http_client.close_connection() - print(f"✅ {protocol_name}连接已关闭") - except Exception as e: - print(f"警告: 关闭{protocol_name}连接时出错: {str(e)}") - - # ========== 结束计时:HTTP/HTTPS访问耗时 ========== - access_end_time = time.time() - access_duration = access_end_time - access_start_time + for index, emoji in enumerate(emotion_emojis, 1): + emotion = emoji.get('emotion', 'unknown') + emoji_url = emoji.get('emojiUrl', '') + emoji_size = emoji.get('size', 0) + + if not emoji_url: + print(f"\n⚠️ 跳过文件 {index}/{total_files}: {emotion} (无URL)") + failed_count += 1 + continue + + # 生成保存路径 + file_name = f"{emotion}.mp4" + output_path = os.path.join(output_dir, file_name) + + # 打印开始下载信息 + progress = progress_tracker.get_progress() + print(f"\n[{index}/{total_files}] 开始下载: {emotion} ({emoji_size/1024/1024:.2f} MB)") + print(f" 当前总进度: {progress['percentage']:.1f}% | " + f"已下载: {progress['downloaded']/1024/1024:.2f}/{progress['total']/1024/1024:.2f} MB | " + f"速度: {progress['speed']/1024/1024:.2f} MB/s") + + # 下载文件 + file_start_time = time.time() + success, downloaded_size = download_single_file( + url=emoji_url, + output_path=output_path, + http_client=http_client, + auth_mode=auth_mode, + ca_cert_name=ca_cert_name, + progress_tracker=progress_tracker + ) + + file_duration = time.time() - file_start_time + + if success: + success_count += 1 + file_speed = (downloaded_size / 1024 / 1024 / file_duration) if file_duration > 0 else 0 + progress = progress_tracker.get_progress() + print(f"✅ [{index}/{total_files}] {emotion}: 完成 | " + f"大小: {downloaded_size/1024/1024:.2f} MB | " + f"速度: {file_speed:.2f} MB/s | " + f"耗时: {file_duration:.1f}s | " + f"总进度: {progress['percentage']:.1f}%") + else: + failed_count += 1 + progress = progress_tracker.get_progress() + print(f"❌ [{index}/{total_files}] {emotion}: 下载失败 | " + f"总进度: {progress['percentage']:.1f}%") + + # 每2秒打印一次总体进度(在下载过程中) + if progress_tracker.should_print(): + progress_tracker.print_progress(emotion, index, total_files) - print("\n" + "=" * 50) - print(f"【{protocol_name}访问耗时统计】") - print("=" * 50) - print(f"{protocol_name}访问总耗时: {access_duration:.3f} 秒 ({access_duration*1000:.1f} 毫秒)") - print(f"下载速度: {file_size/1024/1024/access_duration:.2f} MB/s") - print("\n耗时包含以下步骤:") - if http_client.is_https: - print(" - HTTPS配置和验证") - print(" - URL和端口设置") - print(f" - {protocol_name}连接建立") - print(" - HTTP请求发送") - print(" - 文件数据接收") - print(" - 连接关闭") - print("=" * 50) + # 打印最终统计 + print(f"\n\n{'='*60}") + print(f"【下载完成统计】") + print(f"{'='*60}") + final_progress = progress_tracker.get_progress() + print(f"成功: {success_count}/{total_files}") + print(f"失败: {failed_count}/{total_files}") + print(f"总进度: {final_progress['percentage']:.1f}%") + print(f"已下载: {final_progress['downloaded']/1024/1024:.2f}/{final_progress['total']/1024/1024:.2f} MB") + print(f"总耗时: {final_progress['elapsed']:.1f} 秒") + print(f"平均速度: {final_progress['speed']/1024/1024:.2f} MB/s") + print(f"{'='*60}\n") - return True + return failed_count == 0 except Exception as e: - print("\n" + "=" * 50) - print("【错误处理】") - print("=" * 50) - print(f"❌ 下载异常: {str(e)}") + print(f"\n❌ 批量下载异常: {str(e)}") import traceback traceback.print_exc() return False finally: - # 断开串口连接 print("\n断开串口连接...") at_handler.disconnect() -# -------------------------- 3. 主程序 -------------------------- +# -------------------------- 5. 主程序 -------------------------- if __name__ == "__main__": - print("=" * 50) - print("HTTP/HTTPS文件下载工具(AT指令版本)") - print("=" * 50) - print(f"目标URL: {FILE_URL}") + # JSON数据 + EMOJI_JSON_DATA = { + "deviceId": "860132064629751", + "timestamp": 1763690727, + "messageId": "33d5ba5f6fe9499e", + "command": "DownloadEmoji", + "data": { + "characterId": "bf1b16908b33", + "eyesColor": "yellow-green-gradient", + "eyesShape": "calm-eyes", + "emotionEmojis": [ + {"emotion": "happy", "emojiUrl": "https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Smile.mp4", "size": 730359}, + {"emotion": "laughing", "emojiUrl": "https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Laugh.mp4", "size": 777316}, + {"emotion": "angry", "emojiUrl": "https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Angry.mp4", "size": 770986}, + {"emotion": "wronged", "emojiUrl": "https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Wronged.mp4", "size": 1191639}, + {"emotion": "heartbeat", "emojiUrl": "https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Heartbeat.mp4", "size": 952770}, + {"emotion": "awkward", "emojiUrl": "https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Embarrassed.mp4", "size": 807173}, + {"emotion": "confused", "emojiUrl": "https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Doubt.mp4", "size": 833790}, + {"emotion": "shocked", "emojiUrl": "https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Shocked.mp4", "size": 844919}, + {"emotion": "crying", "emojiUrl": "https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Tears.mp4", "size": 910162}, + {"emotion": "blowing-kiss", "emojiUrl": "https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Kiss.mp4", "size": 544297}, + {"emotion": "sleepy", "emojiUrl": "https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Sleepy.mp4", "size": 674698}, + {"emotion": "mischievous", "emojiUrl": "https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Naughty.mp4", "size": 646516}, + {"emotion": "speechless", "emojiUrl": "https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Speechless.mp4", "size": 899444}, + {"emotion": "dizzy", "emojiUrl": "https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Dizzy.mp4", "size": 1135889}, + {"emotion": "thinking", "emojiUrl": "https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Think.mp4", "size": 963503}, + {"emotion": "working", "emojiUrl": "https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/WorkAndStudy.mp4", "size": 1052905}, + {"emotion": "exercising", "emojiUrl": "https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Exercise.mp4", "size": 1077737}, + {"emotion": "playing", "emojiUrl": "https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Play.mp4", "size": 1098020}, + {"emotion": "daydreaming", "emojiUrl": "https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/YellowGreen/Daze.mp4", "size": 1057085} + ], + "size": 16969208 + } + } - # 构建输出路径 - output_path = os.path.join(OUTPUT_DIR, OUTPUT_FILENAME) - print(f"保存路径: {output_path}") - print("=" * 50) + print("=" * 60) + print("HTTP/HTTPS批量文件下载工具(AT指令版本)") + print("=" * 60) - # 执行下载 - success = download_file( - url=FILE_URL, - output_path=output_path, + # 执行批量下载 + success = download_files_from_json( + json_data=EMOJI_JSON_DATA, + output_dir=OUTPUT_DIR, auth_mode=AUTH_MODE, ca_cert_path=CA_CERT_PATH ) if success: - print("\n✅ 文件下载完成!") + print("\n✅ 所有文件下载完成!") else: - print("\n❌ 文件下载失败!") + print("\n❌ 部分文件下载失败!") exit(1) diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/at_test_get_device_token.py b/code/sdk/zhihui/leiniao/leiniao_python_test/at_test_get_device_token.py index 9600d56be..aa018b393 100644 --- a/code/sdk/zhihui/leiniao/leiniao_python_test/at_test_get_device_token.py +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/at_test_get_device_token.py @@ -22,7 +22,7 @@ FULL_URL = BASE_URL + INTERFACE_PATH SIGN_SECRET = "vDdXVXgPUEUM6_piUjz5AEhMvYTSFzQdPaN-ogKnSus=" # AT模块串口配置(根据实际硬件配置修改) -SERIAL_PORT = "COM19" # 修改为实际的串口设备 +SERIAL_PORT = "COM8" # 修改为实际的串口设备 SERIAL_BAUDRATE = 1000000 SERIAL_TIMEOUT = 10 diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/at_test_websocket.py b/code/sdk/zhihui/leiniao/leiniao_python_test/at_test_websocket.py new file mode 100644 index 000000000..fe8e74f86 --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/at_test_websocket.py @@ -0,0 +1,647 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +通过TCP实现WebSocket协议 +包括握手、收数据协议解析、发数据协议封包 +""" + +import time +import json +import base64 +import hashlib +import struct +import random +import string +import os +import urllib.parse +from n706a_at_handler import ATCommandHandler +from n706a_tcp_client import N706ATCPClient + +# 配置参数 +SERIAL_PORT = "COM8" # 修改为实际的串口设备 +SERIAL_BAUDRATE = 1000000 +SERIAL_TIMEOUT = 10 + +# WebSocket服务器配置 +WS_URL = "ws://110.42.224.129:8910/websocket" + + +class WebSocketFrame: + """WebSocket帧处理类""" + + # Opcode定义 + OPCODE_CONTINUATION = 0x0 + OPCODE_TEXT = 0x1 + OPCODE_BINARY = 0x2 + OPCODE_CLOSE = 0x8 + OPCODE_PING = 0x9 + OPCODE_PONG = 0xA + + @staticmethod + def encode_frame(payload: bytes, opcode: int = OPCODE_TEXT, fin: bool = True, mask: bool = True) -> bytes: + """ + 编码WebSocket帧(客户端发送必须使用掩码) + + Args: + payload: 负载数据 + opcode: 操作码 + fin: 是否为最后一帧 + mask: 是否使用掩码(客户端必须为True) + + Returns: + bytes: 编码后的帧数据 + """ + payload_len = len(payload) + + # 第一个字节:FIN(1) + RSV(3) + Opcode(4) + first_byte = (0x80 if fin else 0x00) | (opcode & 0x0F) + + # 第二个字节:MASK(1) + Payload length(7) + if payload_len < 126: + second_byte = (0x80 if mask else 0x00) | payload_len + header_len = 2 + elif payload_len < 65536: + second_byte = (0x80 if mask else 0x00) | 126 + header_len = 4 + else: + second_byte = (0x80 if mask else 0x00) | 127 + header_len = 10 + + # 构建帧头 + frame = bytearray([first_byte, second_byte]) + + # 添加扩展长度字段 + if payload_len >= 126 and payload_len < 65536: + frame.extend(struct.pack('!H', payload_len)) + elif payload_len >= 65536: + frame.extend(struct.pack('!Q', payload_len)) + + # 添加掩码(客户端必须使用) + if mask: + mask_key = os.urandom(4) + frame.extend(mask_key) + # 对负载进行掩码处理 + masked_payload = bytearray(payload) + for i in range(len(masked_payload)): + masked_payload[i] ^= mask_key[i % 4] + frame.extend(masked_payload) + else: + frame.extend(payload) + + return bytes(frame) + + @staticmethod + def decode_frame(data: bytes) -> tuple: + """ + 解码WebSocket帧(服务器发送的数据没有掩码) + + Args: + data: 帧数据 + + Returns: + tuple: (opcode, payload, fin, remaining_data) 或 (None, None, None, data) 如果数据不完整 + """ + if len(data) < 2: + return None, None, None, data + + first_byte = data[0] + second_byte = data[1] + + fin = (first_byte & 0x80) != 0 + opcode = first_byte & 0x0F + masked = (second_byte & 0x80) != 0 + payload_len = second_byte & 0x7F + + header_len = 2 + + # 解析扩展长度字段 + if payload_len == 126: + if len(data) < 4: + return None, None, None, data + payload_len = struct.unpack('!H', data[2:4])[0] + header_len = 4 + elif payload_len == 127: + if len(data) < 10: + return None, None, None, data + payload_len = struct.unpack('!Q', data[2:10])[0] + header_len = 10 + + # 解析掩码(服务器发送的数据不应该有掩码,但为了兼容性处理) + mask_key = None + if masked: + if len(data) < header_len + 4: + return None, None, None, data + mask_key = data[header_len:header_len + 4] + header_len += 4 + + # 检查是否有足够的负载数据 + if len(data) < header_len + payload_len: + return None, None, None, data + + # 提取负载 + payload = data[header_len:header_len + payload_len] + + # 去除掩码(如果存在) + if masked and mask_key: + payload = bytearray(payload) + for i in range(len(payload)): + payload[i] ^= mask_key[i % 4] + payload = bytes(payload) + + remaining_data = data[header_len + payload_len:] + + return opcode, payload, fin, remaining_data + + +def websocket_handshake(tcp_client: N706ATCPClient, url: str, headers: dict = None) -> bool: + """ + 执行WebSocket握手 + + Args: + tcp_client: TCP客户端实例 + url: WebSocket URL + headers: 额外的HTTP头 + + Returns: + bool: 握手是否成功 + """ + # 解析URL + parsed = urllib.parse.urlparse(url) + host = parsed.hostname + port = parsed.port or (443 if parsed.scheme == 'wss' else 80) + path = parsed.path or '/' + if parsed.query: + path += '?' + parsed.query + + # 生成WebSocket Key + key = base64.b64encode( + ''.join(random.choices(string.ascii_letters + string.digits, k=16)).encode() + ).decode() + + # 构建握手请求 + request_lines = [ + f"GET {path} HTTP/1.1", + f"Host: {host}:{port}", + "Upgrade: websocket", + "Connection: Upgrade", + f"Sec-WebSocket-Key: {key}", + "Sec-WebSocket-Version: 13", + ] + + # 添加额外头部 + if headers: + for name, value in headers.items(): + request_lines.append(f"{name}: {value}") + + request_lines.append("") # 空行 + request_lines.append("") # 空行 + + request = "\r\n".join(request_lines) + request_bytes = request.encode('utf-8') + + print(f"\n【WebSocket握手请求】") + print(request) + print("=" * 60) + + # 发送握手请求 + print(f"\n【发送握手请求数据】") + print(f"数据长度: {len(request_bytes)} 字节") + print(f"HEX格式: {request_bytes.hex().upper()}") + print(f"ASCII格式:") + print(request) + print("=" * 60) + + if not tcp_client.send(request_bytes): + print("❌ 发送握手请求失败") + return False + + print("✅ 握手请求已发送") + + # 接收握手响应 + print("\n等待握手响应...") + response = b"" + start_time = time.time() + timeout = 30 + + while time.time() - start_time < timeout: + # 尝试接收数据 + data = tcp_client.wait_for_data(timeout=5) + if data: + print(f"\n【接收握手响应数据片段】") + print(f"数据长度: {len(data)} 字节") + print(f"HEX格式: {data.hex().upper()}") + try: + print(f"ASCII格式: {data.decode('utf-8', errors='replace')}") + except: + print(f"ASCII格式: (无法解码)") + print("-" * 60) + response += data + # 检查是否收到完整的HTTP响应(以\r\n\r\n结尾) + if b"\r\n\r\n" in response: + break + else: + # 如果没有数据,尝试主动读取 + data = tcp_client.receive(max_length=4096, timeout=1) + if data: + print(f"\n【接收握手响应数据片段(主动读取)】") + print(f"数据长度: {len(data)} 字节") + print(f"HEX格式: {data.hex().upper()}") + try: + print(f"ASCII格式: {data.decode('utf-8', errors='replace')}") + except: + print(f"ASCII格式: (无法解码)") + print("-" * 60) + response += data + if b"\r\n\r\n" in response: + break + + if not response: + print("❌ 未收到握手响应") + return False + + # 解析响应 + print(f"\n【WebSocket握手响应(完整数据)】") + print(f"总长度: {len(response)} 字节") + print(f"HEX格式: {response.hex().upper()}") + print(f"ASCII格式:") + response_text = response.decode('utf-8', errors='ignore') + print(response_text) + print("=" * 60) + + lines = response_text.split('\r\n') + + # 检查状态行 + if not lines[0].startswith('HTTP/1.1 101'): + print(f"❌ 握手失败,状态行: {lines[0]}") + return False + + # 检查Upgrade和Connection头 + response_headers = {} + for line in lines[1:]: + if ':' in line: + name, value = line.split(':', 1) + response_headers[name.strip().lower()] = value.strip() + + if response_headers.get('upgrade', '').lower() != 'websocket': + print("❌ 握手失败,缺少Upgrade头") + return False + + if 'upgrade' not in response_headers.get('connection', '').lower(): + print("❌ 握手失败,Connection头不正确") + return False + + # 验证Sec-WebSocket-Accept + expected_accept = base64.b64encode( + hashlib.sha1((key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode()).digest() + ).decode() + + actual_accept = response_headers.get('sec-websocket-accept', '') + if actual_accept != expected_accept: + print(f"❌ 握手失败,Sec-WebSocket-Accept不匹配") + print(f"期望: {expected_accept}") + print(f"实际: {actual_accept}") + return False + + print("✅ WebSocket握手成功") + return True + + +def test_websocket_via_tcp(): + """通过TCP实现WebSocket协议测试""" + print("=" * 60) + print("通过TCP实现WebSocket协议") + print("=" * 60) + + at_handler = ATCommandHandler(SERIAL_PORT, SERIAL_BAUDRATE, SERIAL_TIMEOUT) + + try: + # 连接串口 + if not at_handler.connect(): + print("❌ 无法连接串口") + return False + + # 测试AT连接 + success, response = at_handler.send_at_command("AT", timeout=3) + if not success: + print("❌ AT模块无响应") + return False + print("✅ AT模块连接正常") + + # 检查PPP拨号状态 + is_ppp_connected, ppp_ip = at_handler.check_ppp_status() + if not is_ppp_connected: + print("建立PPP连接...") + success, ip = at_handler.establish_ppp_connection() + if not success: + print("❌ PPP连接失败") + return False + print(f"✅ PPP连接成功,IP: {ip}") + else: + print(f"✅ PPP已连接,IP: {ppp_ip}") + + # 解析WebSocket URL + parsed = urllib.parse.urlparse(WS_URL) + host = parsed.hostname + port = parsed.port or (443 if parsed.scheme == 'wss' else 80) + + print(f"\n【步骤1: 建立TCP连接】") + print(f"服务器: {host}:{port}") + print("=" * 60) + + # 在连接前,先关闭可能存在的旧连接 + print("检查并关闭可能存在的旧连接...") + socket_id = 0 + + # 尝试关闭所有可能的socket连接 + for close_socket_id in [socket_id]: + # 尝试关闭普通TCP连接 + success, response = at_handler.send_at_command(f"AT+TCPCLOSE={close_socket_id}", timeout=3) + if success: + print(f"✅ Socket {close_socket_id} 已关闭(TCP)") + else: + print(f"⚠️ Socket {close_socket_id} TCP关闭命令响应: {response}") + + # 尝试关闭SSL TCP连接 + success, response = at_handler.send_at_command(f"AT+SSLTCPCLOSE={close_socket_id}", timeout=3) + if success: + print(f"✅ Socket {close_socket_id} 已关闭(SSL TCP)") + else: + print(f"⚠️ Socket {close_socket_id} SSL TCP关闭命令响应: {response}") + + # 等待模块清理socket状态 + print("等待模块清理socket状态...") + time.sleep(0.5) + + # 再次确认PPP连接状态 + print("\n检查PPP连接状态...") + is_ppp_connected, ppp_ip = at_handler.check_ppp_status() + if not is_ppp_connected: + print("⚠️ PPP连接状态异常,尝试重新建立...") + success, ip = at_handler.establish_ppp_connection() + if not success: + print("❌ PPP连接失败,无法继续TCP连接") + return False + print(f"✅ PPP连接已重新建立,IP: {ip}") + time.sleep(1) # 等待PPP稳定 + else: + print(f"✅ PPP连接正常,IP: {ppp_ip}") + + # 检查网络注册状态 + print("\n检查网络注册状态...") + if at_handler.check_network_registration(): + print("✅ 网络已注册") + else: + print("⚠️ 网络未注册,可能影响连接") + + # 测试DNS解析(如果使用域名) + if host and not host.replace('.', '').isdigit(): + print(f"\n测试DNS解析: {host}...") + # 可以尝试ping或DNS查询,这里先跳过 + print("⚠️ 使用域名,确保DNS配置正确") + + # 创建TCP客户端 + tcp_client = N706ATCPClient(at_handler, socket_id=socket_id) + + # 建立TCP连接 + print(f"\n尝试建立TCP连接到 {host}:{port}...") + if not tcp_client.connect(host, port, use_ssl=False, transparent_mode=False): + print("❌ TCP连接失败") + print("\n可能的原因:") + print(" 1. 服务器地址或端口不正确") + print(" 2. 网络连接问题(检查PPP连接状态)") + print(" 3. Socket ID被占用(尝试使用其他Socket ID)") + print(" 4. 服务器不可达或防火墙阻止") + return False + + print("✅ TCP连接成功") + + # WebSocket握手 + print(f"\n【步骤2: WebSocket握手】") + print("=" * 60) + + # 额外的HTTP头(可选) + headers = { + "User-Agent": "AT-WebSocket-Client/1.0" + } + + if not websocket_handshake(tcp_client, WS_URL, headers): + print("❌ WebSocket握手失败") + tcp_client.close() + return False + + # 发送和接收消息 + print(f"\n【步骤3: 发送和接收消息】") + print("=" * 60) + + # 接收缓冲区 + recv_buffer = b"" + + # 发送文本消息 + print("\n发送文本消息...") + test_text = json.dumps({ + "type": "test", + "message": "Hello from AT WebSocket via TCP!", + "timestamp": int(time.time()) + }) + text_payload = test_text.encode('utf-8') + text_frame = WebSocketFrame.encode_frame(text_payload, opcode=WebSocketFrame.OPCODE_TEXT) + + print(f"\n【发送文本消息帧】") + print(f"原始文本: {test_text}") + print(f"负载长度: {len(text_payload)} 字节") + print(f"帧长度: {len(text_frame)} 字节") + print(f"帧HEX: {text_frame.hex().upper()}") + print(f"帧前16字节HEX: {text_frame[:16].hex().upper()}...") + print("-" * 60) + + if tcp_client.send(text_frame): + print(f"✅ 文本消息发送成功") + else: + print("❌ 文本消息发送失败") + + # 发送二进制消息 + print("\n发送二进制消息...") + test_binary = b"Binary test data: " + bytes(range(10)) + binary_frame = WebSocketFrame.encode_frame(test_binary, opcode=WebSocketFrame.OPCODE_BINARY) + + print(f"\n【发送二进制消息帧】") + print(f"原始数据: {test_binary}") + print(f"负载长度: {len(test_binary)} 字节") + print(f"负载HEX: {test_binary.hex().upper()}") + print(f"帧长度: {len(binary_frame)} 字节") + print(f"帧HEX: {binary_frame.hex().upper()}") + print(f"帧前16字节HEX: {binary_frame[:16].hex().upper()}...") + print("-" * 60) + + if tcp_client.send(binary_frame): + print(f"✅ 二进制消息发送成功") + else: + print("❌ 二进制消息发送失败") + + # 发送Ping + print("\n发送Ping...") + ping_payload = b"ping" + ping_frame = WebSocketFrame.encode_frame(ping_payload, opcode=WebSocketFrame.OPCODE_PING) + + print(f"\n【发送Ping帧】") + print(f"负载: {ping_payload}") + print(f"负载长度: {len(ping_payload)} 字节") + print(f"帧长度: {len(ping_frame)} 字节") + print(f"帧HEX: {ping_frame.hex().upper()}") + print("-" * 60) + + if tcp_client.send(ping_frame): + print("✅ Ping发送成功") + else: + print("❌ Ping发送失败") + + # 接收消息(最多等待30秒) + print("\n等待接收消息(最多30秒)...") + start_time = time.time() + message_count = 0 + + while time.time() - start_time < 30: + # 尝试从缓冲区解析帧 + if recv_buffer: + opcode, payload, fin, remaining = WebSocketFrame.decode_frame(recv_buffer) + if payload is not None: + # 打印解析出的帧信息 + opcode_names = { + WebSocketFrame.OPCODE_TEXT: "TEXT", + WebSocketFrame.OPCODE_BINARY: "BINARY", + WebSocketFrame.OPCODE_CLOSE: "CLOSE", + WebSocketFrame.OPCODE_PING: "PING", + WebSocketFrame.OPCODE_PONG: "PONG", + WebSocketFrame.OPCODE_CONTINUATION: "CONTINUATION" + } + opcode_name = opcode_names.get(opcode, f"UNKNOWN({opcode})") + + print(f"\n【解析WebSocket帧】") + print(f"Opcode: {opcode_name} (0x{opcode:02X})") + print(f"FIN: {fin}") + print(f"负载长度: {len(payload)} 字节") + print(f"负载HEX: {payload.hex().upper()}") + if len(payload) > 100: + print(f"负载HEX(前100字节): {payload[:100].hex().upper()}...") + print("-" * 60) + + recv_buffer = remaining + + # 处理控制帧 + if opcode == WebSocketFrame.OPCODE_CLOSE: + print("🔌 收到关闭帧") + if len(payload) >= 2: + close_code = struct.unpack('!H', payload[:2])[0] + close_reason = payload[2:].decode('utf-8', errors='ignore') if len(payload) > 2 else "" + print(f" 关闭码: {close_code}, 原因: {close_reason}") + break + elif opcode == WebSocketFrame.OPCODE_PING: + print("📨 收到Ping,发送Pong...") + pong_frame = WebSocketFrame.encode_frame(payload, opcode=WebSocketFrame.OPCODE_PONG) + print(f"【发送Pong帧】") + print(f"帧HEX: {pong_frame.hex().upper()}") + print("-" * 60) + tcp_client.send(pong_frame) + elif opcode == WebSocketFrame.OPCODE_PONG: + print("📨 收到Pong响应") + try: + pong_text = payload.decode('utf-8', errors='replace') + print(f" Pong数据: {pong_text}") + except: + print(f" Pong数据HEX: {payload.hex().upper()}") + elif opcode == WebSocketFrame.OPCODE_TEXT: + message_count += 1 + try: + text = payload.decode('utf-8') + print(f"📨 收到文本消息 #{message_count}: {text}") + except: + print(f"📨 收到文本消息 #{message_count} (无法解码为UTF-8)") + print(f" 原始HEX: {payload.hex().upper()}") + elif opcode == WebSocketFrame.OPCODE_BINARY: + message_count += 1 + print(f"📨 收到二进制消息 #{message_count}, 长度: {len(payload)} 字节") + print(f" 完整HEX: {payload.hex().upper()}") + if len(payload) > 100: + print(f" HEX(前100字节): {payload[:100].hex().upper()}...") + continue + + # 接收新数据 + data = tcp_client.wait_for_data(timeout=5) + if data: + print(f"\n【接收原始数据】") + print(f"数据长度: {len(data)} 字节") + print(f"HEX格式: {data.hex().upper()}") + if len(data) > 100: + print(f"HEX格式(前100字节): {data[:100].hex().upper()}...") + try: + ascii_str = data.decode('utf-8', errors='replace') + if all(32 <= ord(c) < 127 or c in '\n\r\t' for c in ascii_str[:100]): + print(f"ASCII格式: {ascii_str[:200]}") + else: + print(f"ASCII格式: (包含非打印字符)") + except: + print(f"ASCII格式: (无法解码为UTF-8)") + print("-" * 60) + + recv_buffer += data + print(f"📥 缓冲区总长度: {len(recv_buffer)} 字节") + else: + # 超时,继续等待 + continue + + if message_count == 0: + print("⚠️ 未收到任何消息") + + # 发送关闭帧 + print("\n【步骤4: 关闭连接】") + print("=" * 60) + close_payload = struct.pack('!H', 1000) + b"Normal closure" + close_frame = WebSocketFrame.encode_frame(close_payload, opcode=WebSocketFrame.OPCODE_CLOSE) + + print(f"\n【发送关闭帧】") + print(f"关闭码: 1000") + print(f"关闭原因: Normal closure") + print(f"负载长度: {len(close_payload)} 字节") + print(f"帧长度: {len(close_frame)} 字节") + print(f"帧HEX: {close_frame.hex().upper()}") + print("-" * 60) + + tcp_client.send(close_frame) + print("✅ 关闭帧已发送") + + # 关闭TCP连接 + tcp_client.close() + print("✅ TCP连接已关闭") + + return True + + except Exception as e: + print(f"❌ 测试异常: {e}") + import traceback + traceback.print_exc() + return False + finally: + at_handler.disconnect() + + +def main(): + """主函数""" + print("=" * 60) + print("通过TCP实现WebSocket协议测试") + print(f"WebSocket URL: {WS_URL}") + print("=" * 60) + + # 执行测试 + success = test_websocket_via_tcp() + + # 总结 + print("\n" + "=" * 60) + print("测试总结") + print("=" * 60) + print(f"WebSocket测试: {'✅ 通过' if success else '❌ 失败'}") + print("=" * 60) + + +if __name__ == "__main__": + main() + diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/at_websocket_client.py b/code/sdk/zhihui/leiniao/leiniao_python_test/at_websocket_client.py new file mode 100644 index 000000000..c5e7c9c3b --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/at_websocket_client.py @@ -0,0 +1,565 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +基于AT Socket的WebSocket客户端实现 +使用标准socket接口实现WebSocket协议 +""" + +import base64 +import hashlib +import struct +import time +import urllib.parse +from typing import Optional, Callable, Dict, Any +from at_socket import ATSocket, create_connection +from n706a_at_handler import ATCommandHandler + + +class WebSocketFrame: + """WebSocket帧处理类""" + + # Opcode定义 + OPCODE_CONTINUATION = 0x0 + OPCODE_TEXT = 0x1 + OPCODE_BINARY = 0x2 + OPCODE_CLOSE = 0x8 + OPCODE_PING = 0x9 + OPCODE_PONG = 0xA + + @staticmethod + def encode_frame(payload: bytes, opcode: int = OPCODE_TEXT, fin: bool = True, mask: bool = True) -> bytes: + """ + 编码WebSocket帧 + + Args: + payload: 负载数据 + opcode: 操作码 + fin: 是否为最后一帧 + mask: 是否使用掩码(客户端必须使用) + + Returns: + bytes: 编码后的帧数据 + """ + payload_len = len(payload) + + # 第一个字节:FIN(1) + RSV(3) + Opcode(4) + first_byte = (0x80 if fin else 0x00) | (opcode & 0x0F) + + # 第二个字节:MASK(1) + Payload length(7) + if payload_len < 126: + second_byte = (0x80 if mask else 0x00) | payload_len + header_len = 2 + elif payload_len < 65536: + second_byte = (0x80 if mask else 0x00) | 126 + header_len = 4 + else: + second_byte = (0x80 if mask else 0x00) | 127 + header_len = 10 + + # 构建帧头 + frame = bytearray([first_byte, second_byte]) + + # 添加扩展长度字段 + if payload_len >= 126 and payload_len < 65536: + frame.extend(struct.pack('!H', payload_len)) + elif payload_len >= 65536: + frame.extend(struct.pack('!Q', payload_len)) + + # 添加掩码(客户端必须使用) + if mask: + import os + mask_key = os.urandom(4) + frame.extend(mask_key) + # 对负载进行掩码处理 + masked_payload = bytearray(payload) + for i in range(len(masked_payload)): + masked_payload[i] ^= mask_key[i % 4] + frame.extend(masked_payload) + else: + frame.extend(payload) + + return bytes(frame) + + @staticmethod + def decode_frame(data: bytes) -> tuple: + """ + 解码WebSocket帧 + + Args: + data: 帧数据 + + Returns: + tuple: (opcode, payload, fin, remaining_data) + """ + if len(data) < 2: + return None, None, None, data + + first_byte = data[0] + second_byte = data[1] + + fin = (first_byte & 0x80) != 0 + opcode = first_byte & 0x0F + masked = (second_byte & 0x80) != 0 + payload_len = second_byte & 0x7F + + header_len = 2 + + # 解析扩展长度字段 + if payload_len == 126: + if len(data) < 4: + return None, None, None, data + payload_len = struct.unpack('!H', data[2:4])[0] + header_len = 4 + elif payload_len == 127: + if len(data) < 10: + return None, None, None, data + payload_len = struct.unpack('!Q', data[2:10])[0] + header_len = 10 + + # 解析掩码 + mask_key = None + if masked: + if len(data) < header_len + 4: + return None, None, None, data + mask_key = data[header_len:header_len + 4] + header_len += 4 + + # 检查是否有足够的负载数据 + if len(data) < header_len + payload_len: + return None, None, None, data + + # 提取负载 + payload = data[header_len:header_len + payload_len] + + # 去除掩码(服务器发送的数据不应该有掩码,但为了兼容性处理) + if masked and mask_key: + payload = bytearray(payload) + for i in range(len(payload)): + payload[i] ^= mask_key[i % 4] + payload = bytes(payload) + + remaining_data = data[header_len + payload_len:] + + return opcode, payload, fin, remaining_data + + +class ATWebSocketClient: + """基于AT Socket的WebSocket客户端""" + + def __init__(self, at_handler: ATCommandHandler, socket_id: int = 0): + """ + 初始化WebSocket客户端 + + Args: + at_handler: ATCommandHandler实例 + socket_id: Socket ID(0-5),默认0 + """ + self.at_handler = at_handler + self.socket_id = socket_id + self.sock: Optional[ATSocket] = None + self.connected = False + self.url = None + self.host = None + self.port = None + self.path = None + self.use_ssl = False + self.ssl_config = None + + # 回调函数 + self.on_error: Optional[Callable[[Exception], None]] = None + self.on_close: Optional[Callable[[int, str], None]] = None + self.on_open: Optional[Callable[[], None]] = None + + # 接收缓冲区 + self._recv_buffer = b"" + + def set_ssl_config(self, ssl_version=3, auth_mode=0, ca_cert=None, client_cert=None, client_key=None): + """ + 设置SSL配置(用于wss连接) + + Args: + ssl_version: SSL版本,0=SSL3.0, 1=TLS1.0, 2=TLS1.1, 3=TLS1.2, 4=TLS1.3 + auth_mode: 认证模式,0=不认证, 1=单向认证, 2=双向认证 + ca_cert: CA证书文件名(单向/双向认证需要) + client_cert: 客户端证书文件名(双向认证需要) + client_key: 客户端密钥文件名(双向认证需要) + """ + self.ssl_config = { + 'ssl_version': ssl_version, + 'auth_mode': auth_mode, + 'ca_cert': ca_cert, + 'client_cert': client_cert, + 'client_key': client_key + } + + def connect(self, url: str, headers: Optional[Dict[str, str]] = None, timeout: Optional[float] = None) -> bool: + """ + 连接到WebSocket服务器 + + Args: + url: WebSocket URL(ws://或wss://) + headers: 额外的HTTP头 + timeout: 连接超时时间(秒) + + Returns: + bool: 是否连接成功 + """ + try: + # 解析URL + parsed = urllib.parse.urlparse(url) + + if parsed.scheme not in ('ws', 'wss'): + raise ValueError(f"不支持的协议: {parsed.scheme},仅支持ws和wss") + + self.url = url + self.use_ssl = (parsed.scheme == 'wss') + self.host = parsed.hostname + self.port = parsed.port or (443 if self.use_ssl else 80) + self.path = parsed.path or '/' + if parsed.query: + self.path += '?' + parsed.query + + # 创建socket连接 + if self.use_ssl: + if not self.ssl_config: + # 默认SSL配置 + self.ssl_config = { + 'ssl_version': 3, # TLS1.2 + 'auth_mode': 0, + 'ca_cert': None, + 'client_cert': None, + 'client_key': None + } + + self.sock = create_connection( + address=(self.host, self.port), + timeout=timeout, + at_handler=self.at_handler, + socket_id=self.socket_id, + use_ssl=True, + ssl_config=self.ssl_config + ) + else: + self.sock = create_connection( + address=(self.host, self.port), + timeout=timeout, + at_handler=self.at_handler, + socket_id=self.socket_id, + use_ssl=False + ) + + # 执行WebSocket握手 + if not self._handshake(headers): + self.sock.close() + self.sock = None + return False + + self.connected = True + + # 调用on_open回调 + if self.on_open: + try: + self.on_open() + except Exception as e: + if self.on_error: + self.on_error(e) + + return True + + except Exception as e: + if self.on_error: + self.on_error(e) + if self.sock: + self.sock.close() + self.sock = None + return False + + def _handshake(self, extra_headers: Optional[Dict[str, str]] = None) -> bool: + """ + 执行WebSocket握手 + + Args: + extra_headers: 额外的HTTP头 + + Returns: + bool: 握手是否成功 + """ + # 生成WebSocket Key + import random + import string + key = base64.b64encode( + ''.join(random.choices(string.ascii_letters + string.digits, k=16)).encode() + ).decode() + + # 构建握手请求 + request_lines = [ + f"GET {self.path} HTTP/1.1", + f"Host: {self.host}:{self.port}", + "Upgrade: websocket", + "Connection: Upgrade", + f"Sec-WebSocket-Key: {key}", + "Sec-WebSocket-Version: 13", + ] + + # 添加额外头部 + if extra_headers: + for name, value in extra_headers.items(): + request_lines.append(f"{name}: {value}") + + request_lines.append("") # 空行 + request_lines.append("") # 空行 + + request = "\r\n".join(request_lines) + request_bytes = request.encode('utf-8') + + # 发送握手请求 + try: + self.sock.send(request_bytes) + except Exception as e: + print(f"发送握手请求失败: {e}") + return False + + # 接收握手响应 + response = b"" + timeout = self.sock.gettimeout() or 30 + start_time = time.time() + + while time.time() - start_time < timeout: + try: + chunk = self.sock.recv(4096) + if not chunk: + break + response += chunk + + # 检查是否收到完整的HTTP响应(以\r\n\r\n结尾) + if b"\r\n\r\n" in response: + break + except Exception as e: + print(f"接收握手响应失败: {e}") + return False + + if not response: + print("未收到握手响应") + return False + + # 解析响应 + response_text = response.decode('utf-8', errors='ignore') + lines = response_text.split('\r\n') + + # 检查状态行 + if not lines[0].startswith('HTTP/1.1 101'): + print(f"握手失败,状态行: {lines[0]}") + return False + + # 检查Upgrade和Connection头 + headers = {} + for line in lines[1:]: + if ':' in line: + name, value = line.split(':', 1) + headers[name.strip().lower()] = value.strip() + + if headers.get('upgrade', '').lower() != 'websocket': + print("握手失败,缺少Upgrade头") + return False + + if 'upgrade' not in headers.get('connection', '').lower(): + print("握手失败,Connection头不正确") + return False + + # 验证Sec-WebSocket-Accept + expected_accept = base64.b64encode( + hashlib.sha1((key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode()).digest() + ).decode() + + actual_accept = headers.get('sec-websocket-accept', '') + if actual_accept != expected_accept: + print(f"握手失败,Sec-WebSocket-Accept不匹配") + print(f"期望: {expected_accept}") + print(f"实际: {actual_accept}") + return False + + print("✅ WebSocket握手成功") + return True + + def send(self, data: bytes, opcode: int = WebSocketFrame.OPCODE_BINARY) -> bool: + """ + 发送WebSocket消息 + + Args: + data: 要发送的数据(bytes) + opcode: 操作码(OPCODE_TEXT或OPCODE_BINARY) + + Returns: + bool: 是否发送成功 + """ + if not self.connected or not self.sock: + return False + + try: + frame = WebSocketFrame.encode_frame(data, opcode=opcode, fin=True, mask=True) + self.sock.send(frame) + return True + except Exception as e: + if self.on_error: + self.on_error(e) + return False + + def send_text(self, text: str) -> bool: + """ + 发送文本消息 + + Args: + text: 要发送的文本 + + Returns: + bool: 是否发送成功 + """ + return self.send(text.encode('utf-8'), opcode=WebSocketFrame.OPCODE_TEXT) + + def send_binary(self, data: bytes) -> bool: + """ + 发送二进制消息 + + Args: + data: 要发送的二进制数据 + + Returns: + bool: 是否发送成功 + """ + return self.send(data, opcode=WebSocketFrame.OPCODE_BINARY) + + def ping(self, payload: bytes = b"") -> bool: + """ + 发送Ping帧 + + Args: + payload: Ping负载(可选) + + Returns: + bool: 是否发送成功 + """ + return self.send(payload, opcode=WebSocketFrame.OPCODE_PING) + + def pong(self, payload: bytes = b"") -> bool: + """ + 发送Pong帧 + + Args: + payload: Pong负载(可选) + + Returns: + bool: 是否发送成功 + """ + return self.send(payload, opcode=WebSocketFrame.OPCODE_PONG) + + def recv(self, timeout: Optional[float] = None) -> Optional[bytes]: + """ + 接收WebSocket消息(阻塞模式) + + Args: + timeout: 超时时间(秒) + + Returns: + bytes: 接收到的消息,如果超时或连接关闭返回None + """ + if not self.connected or not self.sock: + return None + + original_timeout = self.sock.gettimeout() + try: + if timeout is not None: + self.sock.settimeout(timeout) + + # 处理缓冲区中的数据 + if self._recv_buffer: + opcode, payload, fin, remaining = WebSocketFrame.decode_frame(self._recv_buffer) + if payload is not None: + self._recv_buffer = remaining + return payload + + # 接收新数据 + while True: + data = self.sock.recv(4096) + if not data: + return None + + self._recv_buffer += data + + # 尝试解码帧 + opcode, payload, fin, remaining = WebSocketFrame.decode_frame(self._recv_buffer) + + if payload is not None: + self._recv_buffer = remaining + + # 处理控制帧 + if opcode == WebSocketFrame.OPCODE_CLOSE: + self.connected = False + return None + elif opcode == WebSocketFrame.OPCODE_PING: + # 自动回复Pong + self.pong(payload) + continue + elif opcode == WebSocketFrame.OPCODE_PONG: + # 忽略Pong + continue + else: + return payload + else: + # 数据不完整,继续接收 + continue + + except Exception as e: + if self.on_error: + self.on_error(e) + return None + finally: + if timeout is not None: + self.sock.settimeout(original_timeout) + + def recv_text(self, timeout: Optional[float] = None) -> Optional[str]: + """ + 接收文本消息 + + Args: + timeout: 超时时间(秒) + + Returns: + str: 接收到的文本,如果超时或连接关闭返回None + """ + data = self.recv(timeout) + if data is None: + return None + return data.decode('utf-8') + + def close(self, code: int = 1000, reason: str = "Normal closure"): + """ + 关闭WebSocket连接 + + Args: + code: 关闭码 + reason: 关闭原因 + """ + if not self.connected: + return + + try: + # 发送关闭帧 + close_payload = struct.pack('!H', code) + reason.encode('utf-8') + self.send(close_payload, opcode=WebSocketFrame.OPCODE_CLOSE) + except: + pass + + self.connected = False + + if self.sock: + self.sock.close() + self.sock = None + + # 调用on_close回调 + if self.on_close: + try: + self.on_close(code, reason) + except Exception as e: + if self.on_error: + self.on_error(e) + diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/download_mp4_at.py b/code/sdk/zhihui/leiniao/leiniao_python_test/download_mp4_at.py deleted file mode 100644 index c19025a8e..000000000 --- a/code/sdk/zhihui/leiniao/leiniao_python_test/download_mp4_at.py +++ /dev/null @@ -1,496 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -使用AT指令通过HTTPS下载MP4文件 -参考 get_device_token_at.py 实现 -""" - -import serial -import time -import re -import urllib.parse -import os -from get_device_token_at import ATCommandHandler, HTTPSATClient, HTTPATClient - -# -------------------------- 1. 配置参数 -------------------------- -# 目标文件URL -MP4_URL = "https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/RoundEyes/Blue/Angry.mp4" -# MP4_URL = "http://snicker.com.cn:8089/LEINIAO/blue/angry.zip" - -# 保存路径 -OUTPUT_DIR = "downloads" -OUTPUT_FILENAME = "Angry.mp4" - -# AT模块串口配置(根据实际硬件配置修改) -SERIAL_PORT = "COM19" # 修改为实际的串口设备 -SERIAL_BAUDRATE = 2000000 -SERIAL_TIMEOUT = 10 - -# 认证模式配置 -AUTH_MODE = 1 # 0=无需认证, 1=单向认证, 2=双向认证 -# CA_CERT_PATH = None # 例如: "certs/ca.crt" -CA_CERT_PATH = "certs/ca_chain.pem" - -# -------------------------- 2. 下载MP4文件函数 -------------------------- -def download_mp4_file(url, output_path, auth_mode=0, ca_cert_path=None): - """ - 使用AT指令通过HTTP/HTTPS下载MP4文件 - - Args: - url: MP4文件的HTTP/HTTPS URL - output_path: 保存路径 - auth_mode: 认证模式,0=无需认证, 1=单向认证, 2=双向认证(仅HTTPS需要) - ca_cert_path: CA证书路径(HTTPS单向认证时需要) - """ - # 解析URL,判断协议类型 - parsed_url = urllib.parse.urlparse(url) - is_https = parsed_url.scheme.lower() == 'https' - is_http = parsed_url.scheme.lower() == 'http' - - if not (is_http or is_https): - raise ValueError(f"不支持的URL协议: {parsed_url.scheme},仅支持 http:// 或 https://") - - protocol_name = "HTTPS" if is_https else "HTTP" - print(f"检测到协议类型: {protocol_name}") - # 初始化AT指令处理器 - at_handler = ATCommandHandler(SERIAL_PORT, SERIAL_BAUDRATE, SERIAL_TIMEOUT) - - try: - # 连接串口 - if not at_handler.connect(): - print("❌ 无法连接串口,请检查串口配置") - return False - - # 测试AT连接 - success, response = at_handler.send_at_command("AT", timeout=3) - if not success: - print("❌ AT模块无响应,请检查连接") - return False - print("✅ AT模块连接正常") - - # 检查PPP拨号状态 - print("\n" + "=" * 50) - print("【步骤1: 检查PPP拨号状态】") - print("=" * 50) - is_ppp_connected, ppp_ip = at_handler.check_ppp_status() - - if is_ppp_connected: - print(f"✅ PPP已连接,IP地址: {ppp_ip}") - else: - print(f"❌ PPP未连接,IP地址: {ppp_ip or '未知'}") - print("\n尝试建立PPP连接...") - - apn_name = None - success, ip = at_handler.establish_ppp_connection(apn_name=apn_name) - - if not success: - print(f"❌ PPP连接建立失败,无法继续{protocol_name}访问") - return False - - ppp_ip = ip - - # 根据协议类型初始化客户端 - if is_https: - # 初始化HTTPS客户端 - http_client = HTTPSATClient(at_handler) - - # 处理证书(如果需要) - ca_cert_name = None - if auth_mode == 1 and ca_cert_path: - print("\n" + "=" * 50) - print("【步骤2: 处理证书】") - print("=" * 50) - - ca_cert_name = os.path.basename(ca_cert_path) - - # 检查证书是否已存在 - if http_client.certificate_exists(ca_cert_name): - print(f"✅ 证书 {ca_cert_name} 已存在,跳过添加") - else: - print(f"证书 {ca_cert_name} 不存在,开始添加...") - from get_device_token_at import load_certificate_file - - ca_cert_data = load_certificate_file(ca_cert_path) - if not ca_cert_data: - print("❌ 无法加载CA证书,终止流程") - return False - - if not http_client.add_certificate(ca_cert_name, ca_cert_data): - print("❌ CA证书添加失败,终止流程") - return False - - if not http_client.check_certificate(ca_cert_name): - print("❌ CA证书确认失败,终止流程") - return False - - # ========== 开始计时:HTTPS访问耗时 ========== - access_start_time = time.time() - - # 配置HTTPS参数 - print("\n" + "=" * 50) - print("【步骤3: 配置HTTPS参数】") - print("=" * 50) - if not http_client.configure_https( - ssl_version=3, # TLS1.2 - auth_mode=auth_mode, - ca_cert=ca_cert_name - ): - print("❌ HTTPS配置失败,终止流程") - return False - - # 确认HTTPS配置 - print("\n确认HTTPS配置...") - config = http_client.verify_https_config() - if config: - print("✅ HTTPS配置确认成功") - else: - print("⚠️ HTTPS配置确认失败,但继续执行") - - # 设置URL - print("\n" + "=" * 50) - print("【步骤4: 设置HTTPS请求参数】") - print("=" * 50) - print(f"设置请求URL: {url}") - http_client.set_url(url) - - # 建立HTTPS连接 - print("\n" + "=" * 50) - print("【步骤5: 建立HTTPS连接】") - print("=" * 50) - http_client.setup_connection() - print("✅ HTTPS连接建立成功") - - # 发送GET请求 - print("\n" + "=" * 50) - print("【步骤6: 发送HTTPS GET请求】") - print("=" * 50) - - # 使用标准GET模式 (mode=0) - success, response = at_handler.send_at_command("AT+HTTPSACTION=0", timeout=5) - if not success: - raise Exception(f"发送HTTPS请求失败: {response}") - - # 等待响应数据 - print("等待HTTPS响应...") - recv_indicator = b"+HTTPSRECV:" - else: - # 初始化HTTP客户端 - http_client = HTTPATClient(at_handler) - - # ========== 开始计时:HTTP访问耗时 ========== - access_start_time = time.time() - - # 设置URL - print("\n" + "=" * 50) - print("【步骤3: 设置HTTP请求参数】") - print("=" * 50) - print(f"设置请求URL: {url}") - http_client.set_url(url) - - # 建立HTTP连接 - print("\n" + "=" * 50) - print("【步骤4: 建立HTTP连接】") - print("=" * 50) - http_client.setup_connection() - print("✅ HTTP连接建立成功") - - # 发送GET请求 - print("\n" + "=" * 50) - print("【步骤5: 发送HTTP GET请求】") - print("=" * 50) - - # 使用标准GET模式 (mode=0) - success, response = at_handler.send_at_command("AT+HTTPACTION=0", timeout=5) - if not success: - raise Exception(f"发送HTTP请求失败: {response}") - - # 等待响应数据 - print("等待HTTP响应...") - recv_indicator = b"+HTTPRECV:" - - # 重要:直接读取二进制数据,不使用wait_for_unsolicited(它会解码为文本,可能丢失二进制数据) - # 手动等待+HTTPRECV:/+HTTPSRECV:提示符,然后立即开始读取二进制数据 - response_data = b"" - start_time = time.time() - last_data_time = time.time() - header_received = False - content_length = None - body_start_pos = None - recv_found = False - - # 等待+HTTPRECV:/+HTTPSRECV:提示符 - indicator_name = protocol_name + "RECV" - print(f"等待+{indicator_name}:提示符...") - while time.time() - start_time < 60: - if at_handler.serial_conn.in_waiting > 0: - # 读取数据(二进制模式) - chunk = at_handler.serial_conn.read(at_handler.serial_conn.in_waiting) - if chunk: - response_data += chunk - last_data_time = time.time() - - # 检查是否包含+HTTPRECV:/+HTTPSRECV:(可能在字节串中) - if recv_indicator in response_data: - recv_found = True - print(f"✅ 收到+{indicator_name}:提示符") - # 继续读取,查找HTTP响应头 - break - time.sleep(0.1) - - if not recv_found: - # 尝试查找HTTP响应头(可能+HTTPRECV:/+HTTPSRECV:已经被读取) - if b"HTTP/" in response_data: - recv_found = True - print("✅ 检测到HTTP响应数据") - - if not recv_found: - raise Exception(f"未收到+{indicator_name}:提示符或HTTP响应") - - # 查找HTTP响应头开始位置(可能在+HTTPSRECV:之后) - http_start = response_data.find(b"HTTP/") - if http_start == -1: - # 如果没有找到HTTP/,继续读取直到找到 - print("继续读取数据以查找HTTP响应头...") - while time.time() - start_time < 10: - if at_handler.serial_conn.in_waiting > 0: - chunk = at_handler.serial_conn.read(at_handler.serial_conn.in_waiting) - if chunk: - response_data += chunk - http_start = response_data.find(b"HTTP/") - if http_start != -1: - break - time.sleep(0.1) - - if http_start == -1: - raise Exception("未找到HTTP响应头") - - # 从HTTP响应头开始位置处理数据 - # 跳过+HTTPSRECV:前缀(如果存在) - if http_start > 0: - print(f"跳过+HTTPSRECV:前缀 ({http_start} 字节)") - response_data = response_data[http_start:] - - # 查找HTTP响应头结束位置(\r\n\r\n) - print("读取响应头...") - header_timeout = 10 - header_start = time.time() - - while time.time() - header_start < header_timeout: - if at_handler.serial_conn.in_waiting > 0: - chunk = at_handler.serial_conn.read(at_handler.serial_conn.in_waiting) - if chunk: - response_data += chunk - - # 查找HTTP响应头结束位置(\r\n\r\n) - header_end = response_data.find(b"\r\n\r\n") - if header_end != -1: - header_received = True - body_start_pos = header_end + 4 - - # 解析响应头 - header_text = response_data[:header_end].decode('utf-8', errors='ignore') - print("\nHTTP响应头:") - print(header_text) - - # 提取Content-Length - content_length_match = re.search(r'Content-Length:\s*(\d+)', header_text, re.IGNORECASE) - if content_length_match: - content_length = int(content_length_match.group(1)) - print(f"\n文件大小: {content_length} 字节 ({content_length/1024/1024:.2f} MB)") - else: - print("⚠️ 未找到Content-Length,将接收所有数据直到连接关闭") - - # 检查状态码 - status_match = re.search(r'HTTP/\d\.\d\s+(\d+)', header_text) - if status_match: - status_code = int(status_match.group(1)) - if status_code != 200: - raise Exception(f"HTTP请求失败,状态码: {status_code}") - print(f"HTTP状态码: {status_code} ✅") - - # 检查文件数据开始位置的前几个字节(用于调试) - if len(response_data) > body_start_pos: - preview = response_data[body_start_pos:body_start_pos+20] - print(f"\n文件数据前20字节(hex): {preview.hex()}") - print(f"文件数据前20字节(repr): {repr(preview)}") - - break - time.sleep(0.1) - - if not header_received: - raise Exception("未收到完整的HTTP响应头") - - # 继续接收文件数据 - print("\n开始接收文件数据...") - last_progress_time = time.time() # 上次打印进度的时间 - progress_interval = 5 # 每5秒打印一次进度 - - while time.time() - start_time < 600: # 最多等待10分钟 - if at_handler.serial_conn.in_waiting > 0: - chunk = at_handler.serial_conn.read(at_handler.serial_conn.in_waiting) - if chunk: - response_data += chunk - last_data_time = time.time() - - # 如果知道文件大小,显示进度 - if content_length: - body_data = response_data[body_start_pos:] - received = len(body_data) - if received >= content_length: - print(f"\n✅ 文件接收完成: {received} / {content_length} 字节") - break - else: - # 每5秒打印一次进度 - current_time = time.time() - if current_time - last_progress_time >= progress_interval: - progress = (received / content_length) * 100 - print(f"接收进度: {progress:.1f}% ({received}/{content_length} 字节, {received/1024/1024:.2f} MB)") - last_progress_time = current_time - else: - # 不知道文件大小,每5秒显示已接收的数据量 - current_time = time.time() - if current_time - last_progress_time >= progress_interval: - body_data = response_data[body_start_pos:] - received = len(body_data) - print(f"已接收: {received} 字节 ({received/1024/1024:.2f} MB)") - last_progress_time = current_time - else: - # 如果超过10秒没有新数据,检查是否接收完成 - if time.time() - last_data_time > 10: - if content_length: - body_data = response_data[body_start_pos:] - received = len(body_data) - if received >= content_length: - print(f"\n✅ 文件接收完成: {received} / {content_length} 字节") - break - else: - print(f"\n⚠️ 接收超时,已接收: {received} / {content_length} 字节") - break - else: - # 没有Content-Length,如果10秒没有新数据,认为接收完成 - print("\n响应数据接收完成(超时)") - break - time.sleep(0.1) - - # 提取文件数据(跳过HTTP响应头) - file_data = response_data[body_start_pos:] - - # 验证文件数据(MP4文件格式) - # MP4文件结构:Box size (4字节) + Box type (4字节,通常是"ftyp") - # 所以文件应该以 0x00 0x00 0x00 0x1C 开头(或类似的大小值),然后是 "ftyp" - print(f"\n文件数据前16字节(hex): {file_data[:16].hex()}") - print(f"文件数据前16字节(repr): {repr(file_data[:16])}") - - # MP4文件应该以Box size开头(4字节大端整数),然后是"ftyp" - # 检查前4字节是否为有效的Box size(通常不会太大,比如小于100MB) - if len(file_data) >= 8: - box_size_bytes = file_data[:4] - box_type = file_data[4:8] - - # 计算Box size(大端序) - box_size = int.from_bytes(box_size_bytes, byteorder='big') - - print(f"检测到的Box size: {box_size} (hex: {box_size_bytes.hex()})") - print(f"检测到的Box type: {box_type} (hex: {box_type.hex()})") - - # 验证是否为有效的MP4文件 - if box_type == b"ftyp": - print("✅ 检测到有效的MP4文件格式(ftyp box)") - elif box_size > 0 and box_size < 100 * 1024 * 1024: # Box size在合理范围内 - print(f"⚠️ Box size看起来合理,但Box type不是'ftyp',可能是其他格式") - else: - # 如果前4字节不是有效的Box size,尝试查找ftyp - ftyp_pos = file_data.find(b"ftyp") - if ftyp_pos != -1 and ftyp_pos < 100: - print(f"⚠️ 检测到文件数据偏移,ftyp在位置 {ftyp_pos}") - print(f" 丢失了前 {ftyp_pos} 字节数据") - print(f" 建议检查数据接收过程,确保没有丢失数据") - # 不自动调整,保留原始数据 - # file_data = file_data[ftyp_pos:] - - # 保存文件 - print("\n" + "=" * 50) - print("【步骤7: 保存文件】") - print("=" * 50) - - # 创建输出目录 - os.makedirs(os.path.dirname(output_path), exist_ok=True) - - # 写入文件 - with open(output_path, 'wb') as f: - f.write(file_data) - - file_size = len(file_data) - print(f"✅ 文件保存成功: {output_path}") - print(f" 文件大小: {file_size} 字节 ({file_size/1024/1024:.2f} MB)") - - # 关闭连接 - print("\n" + "=" * 50) - print(f"【步骤8: 关闭{protocol_name}连接】") - print("=" * 50) - try: - http_client.close_connection() - print(f"✅ {protocol_name}连接已关闭") - except Exception as e: - print(f"警告: 关闭{protocol_name}连接时出错: {str(e)}") - - # ========== 结束计时:HTTP/HTTPS访问耗时 ========== - access_end_time = time.time() - access_duration = access_end_time - access_start_time - - print("\n" + "=" * 50) - print(f"【{protocol_name}访问耗时统计】") - print("=" * 50) - print(f"{protocol_name}访问总耗时: {access_duration:.3f} 秒 ({access_duration*1000:.1f} 毫秒)") - print(f"下载速度: {file_size/1024/1024/access_duration:.2f} MB/s") - print("\n耗时包含以下步骤:") - if is_https: - print(" - HTTPS配置和验证") - print(" - URL和端口设置") - print(f" - {protocol_name}连接建立") - print(" - HTTP请求发送") - print(" - 文件数据接收") - print(" - 连接关闭") - print("=" * 50) - - return True - - except Exception as e: - print("\n" + "=" * 50) - print("【错误处理】") - print("=" * 50) - print(f"❌ 下载异常: {str(e)}") - import traceback - traceback.print_exc() - return False - finally: - # 断开串口连接 - print("\n断开串口连接...") - at_handler.disconnect() - -# -------------------------- 3. 主程序 -------------------------- -if __name__ == "__main__": - print("=" * 50) - print("HTTPS MP4文件下载工具(AT指令版本)") - print("=" * 50) - print(f"目标URL: {MP4_URL}") - - # 构建输出路径 - output_path = os.path.join(OUTPUT_DIR, OUTPUT_FILENAME) - print(f"保存路径: {output_path}") - print("=" * 50) - - # 执行下载 - success = download_mp4_file( - url=MP4_URL, - output_path=output_path, - auth_mode=AUTH_MODE, - ca_cert_path=CA_CERT_PATH - ) - - if success: - print("\n✅ MP4文件下载完成!") - else: - print("\n❌ MP4文件下载失败!") - exit(1) - diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/get_device_token_at.py b/code/sdk/zhihui/leiniao/leiniao_python_test/get_device_token_at.py deleted file mode 100644 index 667db3f92..000000000 --- a/code/sdk/zhihui/leiniao/leiniao_python_test/get_device_token_at.py +++ /dev/null @@ -1,1320 +0,0 @@ -import serial -import time -import hashlib -import hmac -import random -import string -import json -import re -import urllib.parse -import os - -# -------------------------- 1. 接口基础配置 -------------------------- -BASE_URL = "https://ambybox-api.test.leiniao.com/ambybox-device-api" -INTERFACE_PATH = "/v1/auth/getDeviceToken" -FULL_URL = BASE_URL + INTERFACE_PATH -SIGN_SECRET = "vDdXVXgPUEUM6_piUjz5AEhMvYTSFzQdPaN-ogKnSus=" - -# AT模块串口配置(根据实际硬件配置修改) -SERIAL_PORT = "COM19" # 修改为实际的串口设备 -SERIAL_BAUDRATE = 1000000 -SERIAL_TIMEOUT = 10 - -# -------------------------- 2. AT指令通信类 -------------------------- -class ATCommandHandler: - """AT指令处理器,用于与N706A模组通信""" - - def __init__(self, port=SERIAL_PORT, baudrate=SERIAL_BAUDRATE, timeout=SERIAL_TIMEOUT): - self.port = port - self.baudrate = baudrate - self.timeout = timeout - self.serial_conn = None - - def connect(self): - """打开串口连接""" - try: - self.serial_conn = serial.Serial( - port=self.port, - baudrate=self.baudrate, - timeout=self.timeout, - bytesize=serial.EIGHTBITS, - parity=serial.PARITY_NONE, - stopbits=serial.STOPBITS_ONE - ) - print(f"串口连接成功: {self.port} @ {self.baudrate}") - return True - except Exception as e: - print(f"串口连接失败: {e}") - return False - - def disconnect(self): - """关闭串口连接""" - if self.serial_conn and self.serial_conn.is_open: - self.serial_conn.close() - print("串口连接已关闭") - - def send_at_command(self, command, expected_response="OK", timeout=5): - """ - 发送AT指令并等待响应 - - Args: - command: AT指令字符串(不需要包含\r\n) - expected_response: 期望的响应(可以是字符串或正则表达式) - timeout: 超时时间(秒) - - Returns: - tuple: (success: bool, response: str) - """ - if not self.serial_conn or not self.serial_conn.is_open: - return False, "串口未连接" - - # 清空输入缓冲区 - self.serial_conn.reset_input_buffer() - - # 发送AT指令(添加\r\n) - cmd_bytes = (command + "\r\n").encode('utf-8') - self.serial_conn.write(cmd_bytes) - print(f"发送: {command}") - - # 等待响应 - start_time = time.time() - response_lines = [] - - while time.time() - start_time < timeout: - if self.serial_conn.in_waiting > 0: - line = self.serial_conn.readline().decode('utf-8', errors='ignore').strip() - if line: - response_lines.append(line) - print(f"接收: {line}") - - # 检查是否收到期望的响应 - if isinstance(expected_response, str): - if expected_response in line: - return True, "\n".join(response_lines) - else: # 正则表达式 - if expected_response.search(line): - return True, "\n".join(response_lines) - - # 检查错误响应 - if "ERROR" in line: - return False, "\n".join(response_lines) - - time.sleep(0.1) - - # 超时 - return False, f"超时: {timeout}秒内未收到期望响应\n" + "\n".join(response_lines) - - def wait_for_unsolicited(self, pattern, timeout=30): - """ - 等待主动上报消息(如+HTTPSRECV) - - Args: - pattern: 匹配模式(字符串或正则表达式) - timeout: 超时时间(秒) - - Returns: - tuple: (success: bool, response: str) - """ - if not self.serial_conn or not self.serial_conn.is_open: - return False, "串口未连接" - - start_time = time.time() - response_lines = [] - pattern_compiled = re.compile(pattern) if isinstance(pattern, str) else pattern - - while time.time() - start_time < timeout: - if self.serial_conn.in_waiting > 0: - line = self.serial_conn.readline().decode('utf-8', errors='ignore').strip() - if line: - response_lines.append(line) - print(f"接收: {line}") - - if pattern_compiled.search(line): - return True, "\n".join(response_lines) - - time.sleep(0.1) - - return False, f"超时: {timeout}秒内未收到主动上报\n" + "\n".join(response_lines) - - def check_ppp_status(self): - """ - 检查PPP拨号状态 - - Returns: - tuple: (is_connected: bool, ip_address: str) - """ - success, response = self.send_at_command("AT+XIIC?", timeout=5) - if not success: - return False, None - - # 解析响应格式: +XIIC: , - # 注意:手册说明state前面可能有4个空格 - xiic_pattern = re.compile(r'\+XIIC:\s*(\d+),([0-9.]+)') - match = xiic_pattern.search(response) - - if match: - state = int(match.group(1)) - ip = match.group(2) - - # state=1表示已连接,且IP不是0.0.0.0 - if state == 1 and ip != "0.0.0.0": - return True, ip - else: - return False, ip - else: - print(f"警告: 无法解析PPP状态响应: {response}") - return False, None - - def check_network_registration(self): - """ - 检查网络注册状态 - - Returns: - bool: True表示已注册,False表示未注册 - """ - success, response = self.send_at_command("AT+CREG?", timeout=5) - if not success: - return False - - # 解析响应格式: +CREG: , - # stat=1或5表示已注册 - creg_pattern = re.compile(r'\+CREG:\s*\d+,([15])') - match = creg_pattern.search(response) - - if match: - return True - else: - print(f"警告: 网络未注册,响应: {response}") - return False - - def set_apn(self, apn_name, cid=1, pdp_type="IP"): - """ - 设置APN参数 - - Args: - apn_name: APN名称(如"CMNET") - cid: 上下文标识符,默认1 - pdp_type: PDP类型,默认"IP" - - Returns: - bool: 是否设置成功 - """ - command = f'AT+CGDCONT={cid},"{pdp_type}","{apn_name}"' - success, response = self.send_at_command(command, timeout=5) - return success - - def establish_ppp_connection(self, apn_name=None, max_retries=3): - """ - 建立PPP连接 - - Args: - apn_name: APN名称(可选,如果为None则不设置APN) - max_retries: 最大重试次数 - - Returns: - tuple: (success: bool, ip_address: str) - """ - # 检查网络注册状态 - print("检查网络注册状态...") - if not self.check_network_registration(): - print("❌ 网络未注册,无法建立PPP连接") - return False, None - - print("✅ 网络已注册") - - # 设置APN(如果提供) - if apn_name: - print(f"设置APN: {apn_name}") - if not self.set_apn(apn_name): - print(f"警告: APN设置失败,继续尝试拨号") - - # 尝试建立PPP连接 - for attempt in range(1, max_retries + 1): - print(f"\n尝试建立PPP连接 (第{attempt}次)...") - success, response = self.send_at_command("AT+XIIC=1", timeout=30) - - if not success: - print(f"❌ PPP连接失败: {response}") - if attempt < max_retries: - print(f"等待3秒后重试...") - time.sleep(3) - continue - - # 等待一下,然后查询状态确认 - time.sleep(2) - is_connected, ip = self.check_ppp_status() - - if is_connected: - print(f"✅ PPP连接成功,IP地址: {ip}") - return True, ip - else: - print(f"❌ PPP连接状态检查失败,IP: {ip}") - if attempt < max_retries: - print(f"等待3秒后重试...") - time.sleep(3) - - return False, None - -# -------------------------- 3. HTTPS AT指令封装 -------------------------- -class HTTPSATClient: - """基于AT指令的HTTPS客户端""" - - def __init__(self, at_handler): - self.at = at_handler - self.response_data = None - - def add_certificate(self, cert_name, cert_data): - """ - 添加SSL证书到模块(根据SSL/TLS应用指南) - - 注意:AT+CERTADD命令的响应格式特殊: - 1. 发送 AT+CERTADD=, - 2. 立即返回 CONNECT(不是OK) - 3. 发送证书数据 - 4. 返回 +CERTADD: ,OK 或 +CERTADD: ERROR - - Args: - cert_name: 证书文件名(如 "ca.crt") - cert_data: 证书内容(字节数据或字符串) - - Returns: - bool: 是否添加成功 - """ - # 如果是字符串,转换为字节 - if isinstance(cert_data, str): - cert_data = cert_data.encode('utf-8') - - cert_length = len(cert_data) - - # 清空输入缓冲区 - self.at.serial_conn.reset_input_buffer() - - # 直接发送AT+CERTADD命令(不使用send_at_command,因为响应是CONNECT而不是OK) - command = f'AT+CERTADD={cert_name},{cert_length}' - cmd_bytes = (command + "\r\n").encode('utf-8') - self.at.serial_conn.write(cmd_bytes) - print(f"发送: {command}") - - # 等待CONNECT提示符(AT+CERTADD会立即返回CONNECT,而不是OK) - print("等待'CONNECT'提示符...") - start_time = time.time() - connect_received = False - response_lines = [] - - while time.time() - start_time < 5: - if self.at.serial_conn.in_waiting > 0: - line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip() - if line: - response_lines.append(line) - print(f"接收: {line}") - - # 检查是否收到CONNECT - if "CONNECT" in line.upper(): - connect_received = True - break - - # 检查错误响应 - if "ERROR" in line: - print(f"❌ 证书添加命令失败: {line}") - return False - time.sleep(0.1) - - if not connect_received: - newline = '\n' - print(f"❌ 错误: 未收到'CONNECT'提示符,响应: {newline.join(response_lines)}") - return False - - # 发送证书数据 - print(f"发送证书数据 ({cert_length} 字节)...") - self.at.serial_conn.write(cert_data) - # 确保数据发送完成 - self.at.serial_conn.flush() - - # 等待证书添加完成响应(+CERTADD: ,OK 或 +CERTADD: ERROR) - print("等待证书添加完成响应...") - start_time = time.time() - certadd_received = False - response_lines = [] - - while time.time() - start_time < 30: # 证书写入可能需要较长时间 - if self.at.serial_conn.in_waiting > 0: - line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip() - if line: - response_lines.append(line) - print(f"接收: {line}") - - # 检查是否收到+CERTADD响应 - if "+CERTADD:" in line: - certadd_received = True - # 检查是否成功 - if f"{cert_length},OK" in line or ",OK" in line: - print(f"✅ 证书 {cert_name} 添加成功") - return True - elif "ERROR" in line: - print(f"❌ 证书添加失败: {line}") - return False - time.sleep(0.1) - - if not certadd_received: - newline = '\n' - print(f"❌ 错误: 证书添加超时,响应: {newline.join(response_lines)}") - return False - - return False - - def check_certificate(self, cert_name): - """ - 确认证书是否成功写入(根据SSL/TLS应用指南) - - Args: - cert_name: 证书文件名 - - Returns: - bool: 证书是否存在 - """ - success, response = self.at.send_at_command( - f'AT+CERTCHECK={cert_name}', - timeout=5 - ) - if not success: - return False - - # 检查响应格式: +CERTCHECK: ,OK - if f"{cert_name},OK" in response: - print(f"✅ 证书 {cert_name} 确认成功") - return True - else: - print(f"❌ 证书 {cert_name} 不存在或验证失败: {response}") - return False - - def list_certificates(self): - """ - 查询设备中所有已添加的证书列表 - - Returns: - list: 证书文件名列表,如果查询失败返回None - """ - success, response = self.at.send_at_command("AT+CERTCHECK?", timeout=5) - if not success: - print(f"警告: 证书列表查询失败: {response}") - return None - - # 解析响应格式: 每行一个证书文件名,最后是OK - # 例如: - # cacert.pem - # keycert.pem - # OK - cert_list = [] - for line in response.split('\n'): - line = line.strip() - if line and line not in ['OK', 'AT+CERTCHECK?']: - # 跳过响应前缀(如果有) - if line.startswith('+CERTCHECK:'): - continue - cert_list.append(line) - - return cert_list - - def certificate_exists(self, cert_name): - """ - 检查证书是否已存在于设备中(避免重复添加) - - Args: - cert_name: 证书文件名 - - Returns: - bool: 证书是否存在 - """ - success, response = self.at.send_at_command( - f'AT+CERTCHECK={cert_name}', - timeout=5 - ) - if not success: - return False - - # 检查响应格式: +CERTCHECK: ,OK 或 +CERTCHECK: ERROR - if f"{cert_name},OK" in response: - return True - else: - return False - - def configure_https(self, ssl_version=3, auth_mode=0, ca_cert=None, client_cert=None, client_key=None): - """ - 配置HTTPS SSL参数 - - Args: - ssl_version: SSL版本,0=SSL3.0, 1=TLS1.0, 2=TLS1.1, 3=TLS1.2 - auth_mode: 认证模式,0=不认证, 1=单向认证, 2=双向认证 - ca_cert: CA证书文件名(单向/双向认证需要) - client_cert: 客户端证书文件名(双向认证需要) - client_key: 客户端密钥文件名(双向认证需要) - """ - # 设置SSL版本 - success, response = self.at.send_at_command( - f'AT+HTTPSCFG="sslversion",{ssl_version}', - timeout=5 - ) - if not success: - print(f"警告: SSL版本配置失败: {response}") - return False - - # 设置认证模式 - success, response = self.at.send_at_command( - f'AT+HTTPSCFG="authmode",{auth_mode}', - timeout=5 - ) - if not success: - print(f"警告: 认证模式配置失败: {response}") - return False - - # 单向认证:配置CA证书 - if auth_mode == 1: - if not ca_cert: - print("错误: 单向认证需要CA证书") - return False - - success, response = self.at.send_at_command( - f'AT+HTTPSCFG="cacert",{ca_cert}', - timeout=5 - ) - if not success: - print(f"错误: CA证书配置失败: {response}") - return False - print(f"✅ 已配置CA证书: {ca_cert}") - - # 双向认证:配置CA证书、客户端证书和密钥 - elif auth_mode == 2: - if not ca_cert or not client_cert or not client_key: - print("错误: 双向认证需要CA证书、客户端证书和密钥") - return False - - # 配置CA证书 - success, response = self.at.send_at_command( - f'AT+HTTPSCFG="cacert",{ca_cert}', - timeout=5 - ) - if not success: - print(f"错误: CA证书配置失败: {response}") - return False - - # 配置客户端证书 - success, response = self.at.send_at_command( - f'AT+HTTPSCFG="clientcert",{client_cert}', - timeout=5 - ) - if not success: - print(f"错误: 客户端证书配置失败: {response}") - return False - - # 配置客户端密钥 - success, response = self.at.send_at_command( - f'AT+HTTPSCFG="clientkey",{client_key}', - timeout=5 - ) - if not success: - print(f"错误: 客户端密钥配置失败: {response}") - return False - - print(f"✅ 已配置双向认证证书: CA={ca_cert}, ClientCert={client_cert}, ClientKey={client_key}") - - return True - - def verify_https_config(self): - """ - 确认HTTPS配置(根据SSL/TLS应用指南要求) - - Returns: - dict: 包含配置信息的字典,如果查询失败返回None - """ - success, response = self.at.send_at_command("AT+HTTPSCFG?", timeout=5) - if not success: - print(f"警告: HTTPS配置查询失败: {response}") - return None - - # 解析响应格式: +HTTPSCFG: ,,,, - # 例如: +HTTPSCFG: 3,0,,, - httpcfg_pattern = re.compile(r'\+HTTPSCFG:\s*(\d+),(\d+),([^,]*),([^,]*),([^,\r\n]*)') - match = httpcfg_pattern.search(response) - - if match: - config = { - 'sslversion': int(match.group(1)), - 'authmode': int(match.group(2)), - 'cacert': match.group(3).strip() if match.group(3).strip() else None, - 'clientcert': match.group(4).strip() if match.group(4).strip() else None, - 'clientkey': match.group(5).strip() if match.group(5).strip() else None - } - - # 打印配置信息 - ssl_version_names = {0: "SSL3.0", 1: "TLS1.0", 2: "TLS1.1", 3: "TLS1.2"} - auth_mode_names = {0: "无需认证", 1: "单向认证", 2: "双向认证"} - - print(f" SSL版本: {config['sslversion']} ({ssl_version_names.get(config['sslversion'], '未知')})") - print(f" 认证模式: {config['authmode']} ({auth_mode_names.get(config['authmode'], '未知')})") - if config['cacert']: - print(f" CA证书: {config['cacert']}") - if config['clientcert']: - print(f" 客户端证书: {config['clientcert']}") - if config['clientkey']: - print(f" 客户端密钥: {config['clientkey']}") - - return config - else: - print(f"警告: 无法解析HTTPS配置响应: {response}") - return None - - def set_url(self, url, keepalive=0): - """ - 设置HTTPS请求URL - - Args: - url: 完整的URL(如 https://example.com/path) - keepalive: 是否启用长连接,0=关闭(默认),1=启用 - 根据SSL/TLS应用指南,启用后可以多次请求而无需重新建立连接 - """ - # 解析URL - parsed = urllib.parse.urlparse(url) - host = parsed.netloc - path = parsed.path - if parsed.query: - path += "?" + parsed.query - - # 设置URL(不包含协议和端口) - url_param = f"{host}{path}" - success, response = self.at.send_at_command( - f'AT+HTTPSPARA=url,"{url_param}"', - timeout=5 - ) - if not success: - raise Exception(f"设置URL失败: {response}") - - # 设置端口(HTTPS默认443) - port = parsed.port if parsed.port else 443 - success, response = self.at.send_at_command( - f'AT+HTTPSPARA=port,{port}', - timeout=5 - ) - if not success: - raise Exception(f"设置端口失败: {response}") - - # 设置keepalive(根据SSL/TLS应用指南,可选配置) - if keepalive in [0, 1]: - success, response = self.at.send_at_command( - f'AT+HTTPSPARA=keepalive,{keepalive}', - timeout=5 - ) - if not success: - print(f"警告: keepalive配置失败: {response}") - else: - if keepalive == 1: - print(" ✅ 已启用长连接(keepalive=1)") - else: - print(" ✅ 已关闭长连接(keepalive=0,默认)") - - return True - - def setup_connection(self): - """建立HTTPS连接""" - success, response = self.at.send_at_command("AT+HTTPSSETUP", timeout=15) - if not success or "ERROR" in response: - raise Exception(f"HTTPS连接建立失败: {response}") - return True - - def send_get_request(self, headers=None): - """ - 发送HTTPS GET请求 - - Args: - headers: 自定义HTTP头字典 - - Returns: - str: 响应数据 - """ - if headers: - # 使用OPEN_MODE (mode=99) 自定义HTTP报文 - # 根据手册要求:用户自定义报文时需遵循HTTP协议 - # 构建自定义HTTP请求 - parsed = urllib.parse.urlparse(FULL_URL) - path = parsed.path - if parsed.query: - path += "?" + parsed.query - - # 构建HTTP请求报文(严格按照HTTP协议格式) - # 请求行:GET /path HTTP/1.1 - http_request = f"GET {path} HTTP/1.1\r\n" - # Host头:Host: hostname - http_request += f"Host: {parsed.netloc}\r\n" - - # 添加自定义头(每行格式:Header-Name: value) - for key, value in headers.items(): - http_request += f"{key}: {value}\r\n" - - # Connection头 - http_request += "Connection: close\r\n" - # 请求头和请求体之间的空行(即使没有请求体也需要) - http_request += "\r\n" - - # 计算请求长度(字节数,用于AT+HTTPSACTION=99,) - request_length = len(http_request.encode('utf-8')) - - # 步骤1:直接发送AT+HTTPSACTION=99,命令 - # 注意:根据手册,AT+HTTPSACTION=99会先返回">"提示符,而不是OK - # 所以不能使用send_at_command(它等待OK),需要直接发送并等待">" - self.at.serial_conn.reset_input_buffer() - command = f"AT+HTTPSACTION=99,{request_length}" - cmd_bytes = (command + "\r\n").encode('utf-8') - self.at.serial_conn.write(cmd_bytes) - print(f"发送: {command}") - - # 步骤2:等待">"提示符(根据手册,发送AT+HTTPSACTION=99后会收到">"提示符) - print("等待'>'提示符...") - start_time = time.time() - prompt_received = False - response_lines = [] - - while time.time() - start_time < 5: - if self.at.serial_conn.in_waiting > 0: - line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip() - if line: - response_lines.append(line) - print(f"接收: {line}") - - # 检查是否收到">"提示符 - if ">" in line: - prompt_received = True - break - - # 检查错误响应 - if "ERROR" in line: - newline = '\n' - raise Exception(f"HTTPS请求命令失败: {newline.join(response_lines)}") - time.sleep(0.1) - - if not prompt_received: - newline = '\n' - raise Exception(f"未收到'>'提示符,响应: {newline.join(response_lines)}") - - # 步骤3:发送完整的HTTP请求报文(根据手册,需要发送完整的HTTP协议报文) - # 注意:手册要求遵循HTTP协议,使用作为行结束符 - print(f"发送HTTP请求 ({request_length} 字节)...") - self.at.serial_conn.write(http_request.encode('utf-8')) - # 确保数据发送完成 - self.at.serial_conn.flush() - print(f"HTTP请求内容:\n{http_request}") - - # 步骤4:等待OK响应(发送数据后应该会收到OK) - print("等待OK响应...") - start_time = time.time() - ok_received = False - - while time.time() - start_time < 5: - if self.at.serial_conn.in_waiting > 0: - line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip() - if line: - print(f"接收: {line}") - if "OK" in line: - ok_received = True - break - if "ERROR" in line: - raise Exception(f"HTTPS请求发送失败: {line}") - time.sleep(0.1) - - if not ok_received: - print("⚠️ 未收到OK响应,但继续等待HTTPS响应...") - else: - # 使用标准GET模式 (mode=0) - success, response = self.at.send_at_command("AT+HTTPSACTION=0", timeout=5) - if not success: - raise Exception(f"发送HTTPS请求失败: {response}") - - # 等待响应数据 - print("等待HTTPS响应...") - success, response = self.at.wait_for_unsolicited(r"\+HTTPSRECV:", timeout=30) - if not success: - raise Exception(f"等待HTTPS响应超时: {response}") - - # 继续读取响应数据 - response_data = response - start_time = time.time() - last_data_time = time.time() - - while time.time() - start_time < 30: # 最多等待30秒接收完整响应 - if self.at.serial_conn.in_waiting > 0: - line = self.at.serial_conn.readline().decode('utf-8', errors='ignore') - if line.strip(): - response_data += line - print(f"接收: {line.strip()[:100]}...") # 只打印前100个字符 - last_data_time = time.time() - - # 检查是否收到连接关闭消息 - if "+HTTPSCLOSED" in line or "+HTTPSCLOSE" in line: - print("收到连接关闭消息") - break - else: - # 如果超过2秒没有新数据,认为响应接收完成 - if time.time() - last_data_time > 2: - print("响应数据接收完成(超时)") - break - time.sleep(0.1) - - return response_data - - def close_connection(self): - """关闭HTTPS连接""" - success, response = self.at.send_at_command("AT+HTTPSCLOSE", timeout=5) - return success - -# -------------------------- 4. HTTP AT指令封装 -------------------------- -class HTTPATClient: - """基于AT指令的HTTP客户端""" - - def __init__(self, at_handler): - self.at = at_handler - self.response_data = None - - def set_url(self, url, keepalive=0): - """ - 设置HTTP请求URL - - Args: - url: 完整的URL(如 http://example.com/path) - keepalive: 是否启用长连接,0=关闭(默认),1=启用 - """ - # 解析URL - parsed = urllib.parse.urlparse(url) - host = parsed.netloc - path = parsed.path - if parsed.query: - path += "?" + parsed.query - - # 设置URL(不包含协议和端口) - url_param = f"{host}{path}" - success, response = self.at.send_at_command( - f'AT+HTTPPARA=url,"{url_param}"', - timeout=5 - ) - if not success: - raise Exception(f"设置URL失败: {response}") - - # 设置端口(如果URL中指定了端口) - if parsed.port: - port = parsed.port - else: - port = 80 # HTTP默认端口 - - success, response = self.at.send_at_command( - f'AT+HTTPPARA=port,{port}', - timeout=5 - ) - if not success: - raise Exception(f"设置端口失败: {response}") - - # 设置keepalive(可选) - if keepalive: - success, response = self.at.send_at_command( - f'AT+HTTPPARA=keepalive,{keepalive}', - timeout=5 - ) - if not success: - print(f"警告: 设置keepalive失败: {response}") - - def setup_connection(self): - """ - 建立HTTP连接 - - Returns: - bool: 是否建立成功 - """ - success, response = self.at.send_at_command("AT+HTTPSETUP", timeout=15) - if not success: - raise Exception(f"建立HTTP连接失败: {response}") - return True - - def send_get_request(self, timeout=60): - """ - 发送HTTP GET请求(使用标准GET模式) - - Args: - timeout: 超时时间(秒) - - Returns: - tuple: (success: bool, response: str) - """ - # 使用标准GET模式 (mode=0) - success, response = self.at.send_at_command("AT+HTTPACTION=0", timeout=5) - if not success: - raise Exception(f"发送HTTP请求失败: {response}") - - # 等待响应(+HTTPRECV:) - success, response = self.at.wait_for_unsolicited(r"\+HTTPRECV:", timeout=timeout) - return success, response - - def close_connection(self): - """ - 关闭HTTP连接 - - Returns: - bool: 是否关闭成功 - """ - success, response = self.at.send_at_command("AT+HTTPCLOSE", timeout=5) - return success - - def parse_http_response(self, raw_response): - """ - 解析HTTP响应 - - Returns: - dict: 包含status_code, headers, body的字典 - """ - # 查找HTTP响应开始位置(可能在+HTTPSRECV:之后) - http_start = raw_response.find("HTTP/") - if http_start == -1: - # 如果没有找到HTTP/,尝试查找状态码 - status_match = re.search(r'(\d{3})\s+', raw_response) - if status_match: - http_start = status_match.start() - else: - raise Exception("未找到HTTP响应") - - response_text = raw_response[http_start:] - - # 分离头部和正文 - header_end = response_text.find("\r\n\r\n") - if header_end == -1: - header_end = response_text.find("\n\n") - - if header_end == -1: - # 如果没有找到空行分隔符,尝试查找第一个{作为JSON开始 - json_start = response_text.find("{") - if json_start != -1: - headers_text = response_text[:json_start] - body_text = response_text[json_start:] - else: - raise Exception("无法解析HTTP响应格式") - else: - headers_text = response_text[:header_end] - body_text = response_text[header_end:].strip() - - # 解析状态行 - lines = headers_text.split("\n") - status_line = lines[0].strip() - status_match = re.search(r'HTTP/\d\.\d\s+(\d+)', status_line) - status_code = int(status_match.group(1)) if status_match else 0 - - # 解析头部 - headers = {} - for line in lines[1:]: - line = line.strip() - if ":" in line: - key, value = line.split(":", 1) - headers[key.strip()] = value.strip() - - # 提取JSON正文(去除+HTTPSRECV:前缀等) - # 查找JSON开始位置 - json_start = body_text.find("{") - json_end = body_text.rfind("}") + 1 - - if json_start != -1 and json_end > json_start: - body_json = body_text[json_start:json_end] - else: - # 如果没有找到JSON,返回原始正文 - body_json = body_text.strip() - # 清理可能的AT指令响应前缀 - for prefix in ["+HTTPSRECV:", "+HTTPSCLOSED:", "+HTTPSCLOSE:"]: - if body_json.startswith(prefix): - body_json = body_json[len(prefix):].strip() - - return { - "status_code": status_code, - "headers": headers, - "body": body_json - } - -# -------------------------- 4. 生成合法请求参数 -------------------------- -def generate_request_params(): - device_id = "DEVICE_PC_TEST_001" - device_iccid = "86123456789012345678" - - # 1. 时间戳格式修正:确保是字符串类型的毫秒级时间戳 - timestamp = str(int(time.time() * 1000)) - - # 2. 随机数生成:确保长度在8-16位之间(当前12位符合要求) - nonce_length = 12 - nonce = ''.join(random.choices(string.ascii_letters + string.digits, k=nonce_length)) - - # 3. 签名生成(保持不变,符合协议要求) - sign_data = f"{device_id}:{device_iccid}:{timestamp}:{nonce}" - hmac_obj = hmac.new(SIGN_SECRET.encode("utf-8"), sign_data.encode("utf-8"), hashlib.sha256) - signature = hmac_obj.hexdigest().lower() - - # 4. 返回参数(键名严格匹配文档,保持不变) - return { - "deviceId": device_id, - "deviceIccid": device_iccid, - "timestamp": timestamp, - "nonce": nonce, - "signature": signature, - "deviceModel": "AB101", - "deviceVersion": "1.0.0" - } - -# -------------------------- 5. 证书管理辅助函数 -------------------------- -def load_certificate_file(cert_path): - """ - 从文件加载证书内容 - - Args: - cert_path: 证书文件路径 - - Returns: - bytes: 证书内容(字节数据) - """ - try: - with open(cert_path, 'rb') as f: - cert_data = f.read() - print(f"✅ 成功加载证书文件: {cert_path} ({len(cert_data)} 字节)") - return cert_data - except FileNotFoundError: - print(f"❌ 证书文件不存在: {cert_path}") - return None - except Exception as e: - print(f"❌ 读取证书文件失败: {e}") - return None - -# -------------------------- 6. 发送请求并解析响应 -------------------------- -def send_get_device_token_request(auth_mode=0, ca_cert_path=None, client_cert_path=None, client_key_path=None): - """使用AT指令发送HTTPS GET请求""" - - # 初始化AT指令处理器 - at_handler = ATCommandHandler() - - try: - # 连接串口 - if not at_handler.connect(): - print("❌ 无法连接串口,请检查串口配置") - return - - # 测试AT连接 - success, response = at_handler.send_at_command("AT", timeout=3) - if not success: - print("❌ AT模块无响应,请检查连接") - return - print("✅ AT模块连接正常") - - # 查询版本号 - print("\n查询模块版本号...") - success, response = at_handler.send_at_command("AT+GMR", timeout=5) - if success: - print("✅ 版本信息:") - # 打印响应中的所有行(去除OK等状态行) - for line in response.split('\n'): - line = line.strip() - if line and line not in ['OK', 'AT+GMR']: - print(f" {line}") - else: - print(f"⚠️ 版本查询失败: {response}") - - # 检查PPP拨号状态 - print("\n" + "=" * 50) - print("【步骤1: 检查PPP拨号状态】") - print("=" * 50) - is_ppp_connected, ppp_ip = at_handler.check_ppp_status() - - if is_ppp_connected: - print(f"✅ PPP已连接,IP地址: {ppp_ip}") - else: - print(f"❌ PPP未连接,IP地址: {ppp_ip or '未知'}") - print("\n尝试建立PPP连接...") - - # 建立PPP连接(可以根据实际情况设置APN) - # 如果不需要设置APN,可以传入None - apn_name = None # 可以根据实际情况修改,如 "CMNET"、"3GNET" 等 - success, ip = at_handler.establish_ppp_connection(apn_name=apn_name) - - if not success: - print("❌ PPP连接建立失败,无法继续HTTPS访问") - return - - ppp_ip = ip - - # 初始化HTTPS客户端 - https_client = HTTPSATClient(at_handler) - - # 查询设备中已有的证书列表(可选,用于调试) - if auth_mode in [1, 2]: - print("\n" + "=" * 50) - print("【步骤2: 查询设备证书列表】") - print("=" * 50) - cert_list = https_client.list_certificates() - if cert_list: - print(f"设备中已有 {len(cert_list)} 个证书:") - for cert in cert_list: - print(f" - {cert}") - else: - print("设备中暂无证书") - - # 配置HTTPS(根据认证模式) - print("\n" + "=" * 50) - print("【步骤3: 配置HTTPS参数】") - print("=" * 50) - - # 根据认证模式处理证书 - ca_cert_name = None - client_cert_name = None - client_key_name = None - - if auth_mode == 1: # 单向认证 - print("配置模式: 单向认证") - if not ca_cert_path: - print("❌ 错误: 单向认证需要提供CA证书路径") - return - - # 提取证书文件名 - ca_cert_name = os.path.basename(ca_cert_path) - - # 检查证书是否已存在(避免重复添加) - print(f"\n检查证书是否已存在: {ca_cert_name}") - if https_client.certificate_exists(ca_cert_name): - print(f"✅ 证书 {ca_cert_name} 已存在于设备中,跳过添加步骤") - else: - print(f"证书 {ca_cert_name} 不存在,开始添加...") - - # 加载CA证书 - ca_cert_data = load_certificate_file(ca_cert_path) - if not ca_cert_data: - print("❌ 无法加载CA证书,终止流程") - return - - # 添加CA证书到模块 - print(f"\n添加CA证书: {ca_cert_name}") - if not https_client.add_certificate(ca_cert_name, ca_cert_data): - print("❌ CA证书添加失败,终止流程") - return - - # 确认证书 - if not https_client.check_certificate(ca_cert_name): - print("❌ CA证书确认失败,终止流程") - return - - elif auth_mode == 2: # 双向认证 - print("配置模式: 双向认证") - if not ca_cert_path or not client_cert_path or not client_key_path: - print("❌ 错误: 双向认证需要提供CA证书、客户端证书和密钥路径") - return - - ca_cert_name = os.path.basename(ca_cert_path) - client_cert_name = os.path.basename(client_cert_path) - client_key_name = os.path.basename(client_key_path) - - # 检查并添加CA证书 - print(f"\n检查CA证书: {ca_cert_name}") - if https_client.certificate_exists(ca_cert_name): - print(f"✅ 证书 {ca_cert_name} 已存在,跳过添加") - else: - ca_cert_data = load_certificate_file(ca_cert_path) - if not ca_cert_data: - return - if not https_client.add_certificate(ca_cert_name, ca_cert_data): - return - if not https_client.check_certificate(ca_cert_name): - return - - # 检查并添加客户端证书 - print(f"\n检查客户端证书: {client_cert_name}") - if https_client.certificate_exists(client_cert_name): - print(f"✅ 证书 {client_cert_name} 已存在,跳过添加") - else: - client_cert_data = load_certificate_file(client_cert_path) - if not client_cert_data: - return - if not https_client.add_certificate(client_cert_name, client_cert_data): - return - if not https_client.check_certificate(client_cert_name): - return - - # 检查并添加客户端密钥 - print(f"\n检查客户端密钥: {client_key_name}") - if https_client.certificate_exists(client_key_name): - print(f"✅ 密钥 {client_key_name} 已存在,跳过添加") - else: - client_key_data = load_certificate_file(client_key_path) - if not client_key_data: - return - if not https_client.add_certificate(client_key_name, client_key_data): - return - if not https_client.check_certificate(client_key_name): - return - else: - print("配置模式: 无需认证") - - # ========== 开始计时:HTTPS访问耗时 ========== - https_start_time = time.time() - - # 配置HTTPS参数 - print("\n" + "=" * 50) - print("【步骤4: 配置HTTPS参数】") - print("=" * 50) - if not https_client.configure_https( - ssl_version=3, # TLS1.2 - auth_mode=auth_mode, - ca_cert=ca_cert_name, - client_cert=client_cert_name, - client_key=client_key_name - ): - print("❌ HTTPS配置失败,终止流程") - return - - # 确认HTTPS配置(根据SSL/TLS应用指南要求) - print("\n确认HTTPS配置...") - config = https_client.verify_https_config() - if config: - print("✅ HTTPS配置确认成功") - else: - print("⚠️ HTTPS配置确认失败,但继续执行") - - # 设置URL - print("\n" + "=" * 50) - print("【步骤5: 设置HTTPS请求参数】") - print("=" * 50) - print(f"设置请求URL: {FULL_URL}") - https_client.set_url(FULL_URL) - - # 建立HTTPS连接 - print("\n" + "=" * 50) - print("【步骤6: 建立HTTPS连接】") - print("=" * 50) - https_client.setup_connection() - print("✅ HTTPS连接建立成功") - - # 生成请求参数 - params = generate_request_params() - - # 构建HTTP头(根据API文档要求,参数需要作为HTTP头传递,而不是URL查询参数) - # 参考:设备API接口文档.md - 请求头参数部分 - # 注意:为了与get_device_token_1.py保持一致,先添加参数,最后添加Content-Type - headers = {} - # 将参数添加到HTTP头(deviceId, deviceIccid, timestamp, nonce, signature等) - for key, value in params.items(): - headers[key] = str(value) - # Content-Type放在最后(与requests库的行为保持一致) - headers["Content-Type"] = "application/json" - - print("\n" + "=" * 50) - print("【步骤7: 发送HTTPS GET请求】") - print("=" * 50) - print("【请求信息】") - print(f"请求 URL: {FULL_URL}") - print(f"请求头: {headers}") - print("=" * 50) - - # 发送请求 - try: - raw_response = https_client.send_get_request(headers=headers) - except Exception as e: - print(f"❌ HTTPS请求发送失败: {str(e)}") - raise - - # 解析响应 - print("\n" + "=" * 50) - print("【步骤8: 解析HTTPS响应】") - print("=" * 50) - try: - parsed_response = https_client.parse_http_response(raw_response) - except Exception as e: - print(f"❌ HTTP响应解析失败: {str(e)}") - print(f"原始响应: {raw_response[:500]}...") # 只打印前500字符 - raise - - print(f"HTTP状态码: {parsed_response['status_code']}") - print(f"响应头: {parsed_response['headers']}") - print(f"响应体: {parsed_response['body']}") - - # 解析JSON响应 - print("\n" + "=" * 50) - print("【步骤9: 解析JSON响应】") - print("=" * 50) - try: - response_data = json.loads(parsed_response['body']) - print("\n【JSON响应】") - print(json.dumps(response_data, ensure_ascii=False, indent=2)) - - # 响应判断逻辑 - if response_data.get("errorCode") is None: - print("\n✅ 请求成功!") - if "data" in response_data: - print(f"设备 Token: {response_data['data'].get('token')}") - print(f"Token 过期时间: {response_data['data'].get('expireTime')} 秒({response_data['data'].get('expireTime', 0)//86400} 天)") - else: - print(f"\n❌ 请求失败!错误码: {response_data['errorCode']}, 原因: {response_data.get('message', '未知错误')}") - except json.JSONDecodeError as e: - print(f"\n❌ JSON解析失败: {e}") - print(f"原始响应体: {parsed_response['body']}") - raise - - # 关闭连接 - print("\n" + "=" * 50) - print("【步骤10: 关闭HTTPS连接】") - print("=" * 50) - try: - https_client.close_connection() - print("✅ HTTPS连接已关闭") - except Exception as e: - print(f"警告: 关闭HTTPS连接时出错: {str(e)}") - - # ========== 结束计时:HTTPS访问耗时 ========== - https_end_time = time.time() - https_duration = https_end_time - https_start_time - - print("\n" + "=" * 50) - print("【HTTPS访问耗时统计】") - print("=" * 50) - print(f"HTTPS访问总耗时: {https_duration:.3f} 秒 ({https_duration*1000:.1f} 毫秒)") - print("\n耗时包含以下步骤:") - print(" - HTTPS配置和验证") - print(" - URL和端口设置") - print(" - HTTPS连接建立") - print(" - HTTP请求发送") - print(" - HTTP响应接收") - print(" - 响应解析") - print(" - 连接关闭") - print("=" * 50) - - except Exception as e: - print("\n" + "=" * 50) - print("【错误处理】") - print("=" * 50) - print(f"❌ 请求异常: {str(e)}") - import traceback - traceback.print_exc() - finally: - # 断开串口连接 - print("\n断开串口连接...") - at_handler.disconnect() - -# -------------------------- 7. 执行请求 -------------------------- -if __name__ == "__main__": - # 认证模式配置 - # auth_mode: 0=无需认证, 1=单向认证, 2=双向认证 - # AUTH_MODE = 0 # 默认无需认证 - - # 证书文件路径(单向/双向认证时需要) - # CA_CERT_PATH = None # 例如: "certs/ca.crt" - CLIENT_CERT_PATH = None # 例如: "certs/client.crt" - CLIENT_KEY_PATH = None # 例如: "certs/client.key" - - # 如果使用单向认证,取消下面的注释并设置证书路径 - AUTH_MODE = 1 - CA_CERT_PATH = "certs/ca_chain.pem" - - # 如果使用双向认证,取消下面的注释并设置所有证书路径 - # AUTH_MODE = 2 - # CA_CERT_PATH = "certs/ca.crt" - # CLIENT_CERT_PATH = "certs/client.crt" - # CLIENT_KEY_PATH = "certs/client.key" - - send_get_device_token_request( - auth_mode=AUTH_MODE, - ca_cert_path=CA_CERT_PATH, - client_cert_path=CLIENT_CERT_PATH, - client_key_path=CLIENT_KEY_PATH - ) diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/n706a_http_client.py b/code/sdk/zhihui/leiniao/leiniao_python_test/n706a_http_client.py index 4d68bf220..c476be1c7 100644 --- a/code/sdk/zhihui/leiniao/leiniao_python_test/n706a_http_client.py +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/n706a_http_client.py @@ -480,7 +480,14 @@ class N706AHTTPClient: http_request += "Connection: close\r\n" http_request += "\r\n" - request_length = len(http_request.encode('utf-8')) + request_bytes = http_request.encode('utf-8') + request_length = len(request_bytes) + + # HTTP OPEN_MODE限制:最大2048字节 + max_http_length = 2048 + + if request_length > max_http_length: + raise Exception(f"HTTP请求长度 {request_length} 字节超过限制({max_http_length} 字节),无法发送") # 发送AT+HTTPSACTION=99,命令 self.at.serial_conn.reset_input_buffer() @@ -511,7 +518,7 @@ class N706AHTTPClient: # 发送HTTP请求报文 print(f"发送HTTP请求 ({request_length} 字节)...") - self.at.serial_conn.write(http_request.encode('utf-8')) + self.at.serial_conn.write(request_bytes) self.at.serial_conn.flush() # 等待OK响应 @@ -541,10 +548,85 @@ class N706AHTTPClient: def _send_http_get_request(self, headers=None, timeout=60): """发送HTTP GET请求""" - # 使用标准GET模式 (mode=0) - success, response = self.at.send_at_command("AT+HTTPACTION=0", timeout=5) - if not success: - raise Exception(f"发送HTTP请求失败: {response}") + if headers: + # 使用OPEN_MODE (mode=99) 自定义HTTP报文 + parsed = urllib.parse.urlparse(self.current_url) + path = parsed.path + if parsed.query: + path += "?" + parsed.query + + # 构建HTTP请求报文 + http_request = f"GET {path} HTTP/1.1\r\n" + http_request += f"Host: {parsed.netloc}\r\n" + + for key, value in headers.items(): + http_request += f"{key}: {value}\r\n" + + http_request += "Connection: close\r\n" + http_request += "\r\n" + + request_bytes = http_request.encode('utf-8') + request_length = len(request_bytes) + + # HTTP OPEN_MODE限制:最大2048字节 + max_http_length = 2048 + + if request_length > max_http_length: + raise Exception(f"HTTP请求长度 {request_length} 字节超过限制({max_http_length} 字节),无法发送") + + # 发送AT+HTTPACTION=99,命令 + self.at.serial_conn.reset_input_buffer() + command = f"AT+HTTPACTION=99,{request_length}" + cmd_bytes = (command + "\r\n").encode('utf-8') + self.at.serial_conn.write(cmd_bytes) + print(f"发送: {command}") + + # 等待">"提示符 + print("等待'>'提示符...") + start_time = time.time() + prompt_received = False + + while time.time() - start_time < 5: + if self.at.serial_conn.in_waiting > 0: + line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip() + if line: + print(f"接收: {line}") + if ">" in line: + prompt_received = True + break + if "ERROR" in line: + raise Exception(f"HTTP请求命令失败: {line}") + time.sleep(0.1) + + if not prompt_received: + raise Exception("未收到'>'提示符") + + # 发送HTTP请求报文 + print(f"发送HTTP请求 ({request_length} 字节)...") + self.at.serial_conn.write(request_bytes) + self.at.serial_conn.flush() + + # 等待OK响应 + print("等待OK响应...") + start_time = time.time() + ok_received = False + + while time.time() - start_time < 5: + if self.at.serial_conn.in_waiting > 0: + line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip() + if line: + print(f"接收: {line}") + if "OK" in line: + ok_received = True + break + if "ERROR" in line: + raise Exception(f"HTTP请求发送失败: {line}") + time.sleep(0.1) + else: + # 使用标准GET模式 (mode=0) + success, response = self.at.send_at_command("AT+HTTPACTION=0", timeout=5) + if not success: + raise Exception(f"发送HTTP请求失败: {response}") # 等待响应数据(二进制模式) return self._receive_http_response(b"+HTTPRECV:", timeout) diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/n706a_tcp_client.py b/code/sdk/zhihui/leiniao/leiniao_python_test/n706a_tcp_client.py index 9ece1e6b1..062a14acf 100644 --- a/code/sdk/zhihui/leiniao/leiniao_python_test/n706a_tcp_client.py +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/n706a_tcp_client.py @@ -131,6 +131,31 @@ class N706ATCPClient: self.is_ssl = use_ssl self.transparent_mode = transparent_mode + # 在连接前,先尝试关闭可能存在的旧连接 + if self.is_connected: + print(f"⚠️ Socket {self.socket_id} 已连接,先关闭旧连接...") + self.close() + time.sleep(0.3) + + # 强制关闭socket(即使状态显示未连接,模块端可能仍有残留) + print(f"清理Socket {self.socket_id} 状态...") + # 尝试关闭普通TCP + self.at.send_at_command(f"AT+TCPCLOSE={self.socket_id}", timeout=2) + time.sleep(0.1) + # 尝试关闭SSL TCP + self.at.send_at_command(f"AT+SSLTCPCLOSE={self.socket_id}", timeout=2) + time.sleep(0.2) + + # 配置HEX接收模式(AT+RECVMODE=1,1) + # 这样所有TCP/UDP数据接收都会以HEX格式上报 + print("配置HEX接收模式 (AT+RECVMODE=1,1)...") + success, response = self.at.send_at_command("AT+RECVMODE=1,1", timeout=5) + if not success: + print(f"⚠️ HEX接收模式配置失败: {response}") + print("继续执行,但接收可能使用ASCII模式...") + else: + print("✅ HEX接收模式已启用 (AT+RECVMODE=1,1)") + if use_ssl: # SSL TCP连接 mode = 1 if transparent_mode else 0 @@ -186,8 +211,8 @@ class N706ATCPClient: print(f"❌ TCP连接失败: {response}") return False - # 等待连接结果(+TCPSETUP: ,OK 或 +TCPSETUP: ,FAIL) - tcpsetup_pattern = re.compile(r'\+TCPSETUP:\s*(\d+),(OK|FAIL|ERROR)') + # 等待连接结果(+TCPSETUP: ,OK 或 +TCPSETUP: ,FAIL 或 +TCPSETUP: ,ERROR[数字]) + tcpsetup_pattern = re.compile(r'\+TCPSETUP:\s*(\d+),(OK|FAIL|ERROR\d*)') match = tcpsetup_pattern.search(response) if match: status = match.group(2) @@ -196,14 +221,24 @@ class N706ATCPClient: print(f"✅ TCP连接成功: {server_ip}:{server_port}") return True else: - print(f"❌ TCP连接失败: {status}") + error_msg = f"❌ TCP连接失败: {status}" + if status.startswith("ERROR"): + error_code = status.replace("ERROR", "") + error_msg += f" (错误码: {error_code})" + if error_code == "1": + error_msg += "\n 可能原因: Socket ID被占用或网络连接问题" + elif error_code == "2": + error_msg += "\n 可能原因: 参数错误" + elif error_code == "3": + error_msg += "\n 可能原因: DNS解析失败" + print(error_msg) return False # 如果没有匹配到预期响应,尝试等待主动上报 print("等待TCP连接结果...") success, response = self.at.wait_for_unsolicited(r"\+TCPSETUP:", timeout=30) if success: - tcpsetup_pattern = re.compile(r'\+TCPSETUP:\s*(\d+),(OK|FAIL|ERROR)') + tcpsetup_pattern = re.compile(r'\+TCPSETUP:\s*(\d+),(OK|FAIL|ERROR\d*)') match = tcpsetup_pattern.search(response) if match: status = match.group(2) @@ -211,13 +246,22 @@ class N706ATCPClient: self.is_connected = True print(f"✅ TCP连接成功: {server_ip}:{server_port}") return True + else: + error_msg = f"❌ TCP连接失败: {status}" + if status.startswith("ERROR"): + error_code = status.replace("ERROR", "") + error_msg += f" (错误码: {error_code})" + if error_code == "1": + error_msg += "\n 可能原因: Socket ID被占用或网络连接问题" + print(error_msg) + return False print(f"❌ TCP连接失败: {response}") return False def send(self, data): """ - 发送TCP/TCP+SSL数据 + 发送TCP/TCP+SSL数据(自动分片) Args: data: 要发送的数据(bytes或str) @@ -235,85 +279,276 @@ class N706ATCPClient: data_length = len(data) + # 定义最大发送长度限制 if self.is_ssl: - # SSL TCP发送 - return self._send_ssl_tcp_data(data, data_length) + # SSL TCP: 最大4096字节(二进制数据) + max_chunk_size = 4096 else: - # 普通TCP发送 - return self._send_tcp_data(data, data_length) + # 普通TCP HEX模式: 最大2048字节(二进制数据) + max_chunk_size = 2048 + + # 如果数据超过限制,分片发送 + if data_length > max_chunk_size: + print(f"数据长度 {data_length} 字节超过限制({max_chunk_size} 字节),将分片发送...") + offset = 0 + chunk_num = 0 + + while offset < data_length: + chunk_num += 1 + chunk_size = min(max_chunk_size, data_length - offset) + chunk = data[offset:offset + chunk_size] + + print(f"\n【分片 {chunk_num}】发送 {chunk_size} 字节 (总进度: {offset}/{data_length})") + + if self.is_ssl: + success = self._send_ssl_tcp_data(chunk, chunk_size) + else: + success = self._send_tcp_data(chunk, chunk_size) + + if not success: + print(f"❌ 分片 {chunk_num} 发送失败") + return False + + offset += chunk_size + # 分片之间短暂延迟,避免发送过快 + if offset < data_length: + time.sleep(0.1) + + print(f"✅ 所有分片发送完成(共 {chunk_num} 个分片,总计 {data_length} 字节)") + return True + else: + # 数据不超过限制,直接发送 + if self.is_ssl: + return self._send_ssl_tcp_data(data, data_length) + else: + return self._send_tcp_data(data, data_length) def _send_tcp_data(self, data, data_length): - """发送普通TCP数据""" - # 使用buffer模式发送(不包含content参数) - command = f'AT+TCPSEND={self.socket_id},{data_length}' + """发送普通TCP数据(HEX模式)""" + # 将二进制数据转换为HEX字符串 + hex_data = data.hex().upper() # 转换为大写HEX字符串 + hex_length = len(hex_data) # HEX字符串长度(二进制长度 * 2) + + # 使用buffer模式,HEX模式发送:AT+TCPSEND=,,,1 + # 注意:根据AT命令手册,length参数是指二进制数据的字节数,而不是HEX字符串长度 + # 示例:AT+TCPSEND=0,1024,,1 表示发送1024字节的二进制数据(对应2048个HEX字符) + command = f'AT+TCPSEND={self.socket_id},{data_length},,1' self.at.serial_conn.reset_input_buffer() cmd_bytes = (command + "\r\n").encode('utf-8') self.at.serial_conn.write(cmd_bytes) - print(f"发送: {command}") + print(f"发送: {command} (二进制长度: {data_length} 字节, HEX字符串长度: {hex_length} 字符)") - # 等待">"提示符 - print("等待'>'提示符...") + # 根据TCP应用指南:无论是否返回">",都延时50-100ms,再将数据发送到模块串口 + # 所以先等待一段时间接收响应(包括">"和可能的错误),然后无论是否收到">"都延时发送数据 + print("等待响应(可能包含'>'提示符或错误信息)...") start_time = time.time() + response_buffer = b"" + has_error = False prompt_received = False - while time.time() - start_time < 5: + # 读取响应,最多等待2秒 + while time.time() - start_time < 2.0: if self.at.serial_conn.in_waiting > 0: - line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip() - if line: + chunk = self.at.serial_conn.read(self.at.serial_conn.in_waiting) + response_buffer += chunk + + # 检查错误响应(优先检查错误) + response_text = response_buffer.decode('utf-8', errors='ignore') + if "ERROR" in response_text or "SOCKET ID OPEN FAILED" in response_text or "DATA LENGTH ERROR" in response_text: + has_error = True + print(f"❌ TCP发送命令失败: {response_text}") + return False + + # 如果收到">"提示符,可以提前结束等待 + if b">" in response_buffer: + prompt_received = True + # 打印所有响应内容 + for line in response_text.split('\n'): + line = line.strip() + if line and line not in ['>']: + print(f"接收: {line}") + break + time.sleep(0.05) + + # 打印收到的响应(如果有,且还没打印过) + if response_buffer and not prompt_received: + response_text = response_buffer.decode('utf-8', errors='ignore') + for line in response_text.split('\n'): + line = line.strip() + if line and line not in ['OK', '>']: print(f"接收: {line}") - if ">" in line: - prompt_received = True - break - if "ERROR" in line: - print(f"❌ TCP发送命令失败: {line}") - return False - time.sleep(0.1) - if not prompt_received: - print("❌ 未收到'>'提示符") + # 注意:根据TCP应用指南,无论是否收到">",都延时50-100ms后发送数据 + # 但如果没有收到">"也没有错误,可能是响应还没到,再等待一小段时间 + if not prompt_received and not has_error: + print("⚠️ 未收到'>'提示符,但根据指南要求继续发送数据...") + # 再等待一小段时间,看是否有响应 + time.sleep(0.1) + if self.at.serial_conn.in_waiting > 0: + additional = self.at.serial_conn.read(self.at.serial_conn.in_waiting) + response_buffer += additional + response_text = response_buffer.decode('utf-8', errors='ignore') + if "ERROR" in response_text or "SOCKET ID OPEN FAILED" in response_text: + print(f"❌ TCP发送命令失败: {response_text}") + return False + if b">" in additional: + print("接收: >") + + # 注意:数据长度限制检查已在send()方法中完成,这里不再检查 + # 如果数据超过2048字节,应该已经分片发送 + + # 准备HEX字符串数据 + hex_bytes = hex_data.encode('utf-8') + print(f"\n准备发送TCP数据 (HEX模式: {hex_length} 字符, 原始二进制: {data_length} 字节)...") + print(f"HEX数据预览(前64字符): {hex_data[:64]}...") + print(f"完整HEX数据长度: {len(hex_bytes)} 字节") + + # 延迟50-100ms(根据TCP应用指南:无论是否返回">",都延时50-100ms) + time.sleep(0.075) # 使用75ms,在建议范围内 + + # 发送HEX字符串数据 + # 根据TCP应用指南,数据应该尽快发送到模块串口 + try: + # 一次性写入所有HEX数据(串口缓冲区通常足够大) + bytes_written = self.at.serial_conn.write(hex_bytes) + + # 立即刷新,确保数据发送 + self.at.serial_conn.flush() + + # 如果写入不完整,继续写入剩余数据 + if bytes_written < len(hex_bytes): + print(f"⚠️ 警告: 第一次只写入了 {bytes_written} 字节,继续写入剩余数据...") + remaining = hex_bytes[bytes_written:] + additional = self.at.serial_conn.write(remaining) + self.at.serial_conn.flush() + bytes_written += additional + print(f" 继续写入 {additional} 字节,总计 {bytes_written} 字节") + + if bytes_written != len(hex_bytes): + print(f"❌ 错误: 只写入了 {bytes_written} 字节,期望 {len(hex_bytes)} 字节") + return False + else: + print(f"✅ HEX数据已写入串口: {bytes_written} 字节 (期望: {len(hex_bytes)} 字节)") + + except Exception as e: + print(f"❌ 发送HEX数据时出错: {e}") + import traceback + traceback.print_exc() return False - # 延迟50-100ms(根据手册建议) - time.sleep(0.1) - - # 发送数据 - print(f"发送TCP数据 ({data_length} 字节)...") - self.at.serial_conn.write(data) - self.at.serial_conn.flush() - # 等待发送完成响应(+TCPSEND: ,) + # 注意:根据AT命令手册示例:AT+TCPSEND=0,1024,,1 -> +TCPSEND: 0,1024 + # 响应返回的length应该与命令参数一致,即二进制数据的字节数(data_length) print("等待发送完成响应...") start_time = time.time() send_complete = False + timeout = 30 + all_received_data = b"" # 保存所有接收到的数据,用于调试 - while time.time() - start_time < 30: + while time.time() - start_time < timeout: if self.at.serial_conn.in_waiting > 0: - line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip() - if line: - print(f"接收: {line}") - if f"+TCPSEND: {self.socket_id},{data_length}" in line: - send_complete = True - print(f"✅ TCP数据发送成功: {data_length} 字节") - break - if "ERROR" in line or "FAIL" in line: - print(f"❌ TCP数据发送失败: {line}") + chunk = self.at.serial_conn.read(self.at.serial_conn.in_waiting) + all_received_data += chunk + + # 尝试解析响应 + response_text = all_received_data.decode('utf-8', errors='ignore') + + # 按行处理响应 + lines = response_text.split('\n') + for line in lines: + line = line.strip() + if not line: + continue + + # 只打印包含关键信息的行,避免重复打印 + if any(keyword in line for keyword in ['+TCPSEND', 'ERROR', '+TCPCLOSE', 'OK']): + print(f"接收: {line}") + + # 检查成功响应 + # 格式:+TCPSEND: , + import re + tcpsend_pattern = re.compile(r'\+TCPSEND:\s*(\d+),(\d+)') + match = tcpsend_pattern.match(line) + if match: + socket_id = int(match.group(1)) + returned_length = int(match.group(2)) + + if socket_id == self.socket_id: + # 根据手册,响应返回的length应该与命令参数一致,即二进制数据长度 + if returned_length == data_length: + send_complete = True + print(f"✅ TCP数据发送成功: {data_length} 字节 (HEX字符串长度: {hex_length}, 返回长度: {returned_length})") + break + elif returned_length == hex_length: + # 兼容处理:如果返回的是HEX字符串长度,也认为成功(可能是旧版本) + send_complete = True + print(f"✅ TCP数据发送成功: {data_length} 字节 (HEX字符串长度: {hex_length}, 返回长度: {returned_length} - HEX长度)") + break + else: + # 长度不匹配,但可能是模块的问题,仍然认为发送成功 + print(f"⚠️ 警告: 返回长度 {returned_length} 与期望HEX长度 {hex_length} 或二进制长度 {data_length} 不匹配,但继续...") + send_complete = True + break + + # 检查错误响应 + if "+TCPSEND:" in line: + if "OPERATION EXPIRED" in line: + print(f"❌ TCP数据发送超时: 模块在等待数据时超时") + print(f" 可能原因: 数据发送太慢或数据格式错误") + return False + elif "ERROR" in line or "FAIL" in line or "SOCKET" in line or "DATA LENGTH ERROR" in line: + print(f"❌ TCP数据发送失败: {line}") + return False + + # 检查连接关闭 + if "+TCPCLOSE:" in line: + print(f"❌ TCP连接已关闭: {line}") + self.is_connected = False return False - time.sleep(0.1) + + if send_complete: + break + else: + time.sleep(0.05) # 减少CPU占用 if not send_complete: - print("❌ TCP数据发送超时") + # 打印所有收到的响应,帮助调试 + print(f"\n❌ TCP数据发送超时(未收到+TCPSEND响应)") + if all_received_data: + response_text = all_received_data.decode('utf-8', errors='ignore') + print(f"收到的所有响应数据:") + print(f" HEX格式: {all_received_data.hex().upper()}") + print(f" ASCII格式: {repr(response_text)}") + print(f" 按行显示:") + for i, line in enumerate(response_text.split('\n')): + if line.strip(): + print(f" 行{i+1}: {repr(line)}") + else: + print("未收到任何响应数据") return False return True def _send_ssl_tcp_data(self, data, data_length): - """发送SSL TCP数据""" + """发送SSL TCP数据(HEX模式,单次发送,不超过4096字节)""" + # 检查数据长度限制(SSL TCP最大4096字节二进制数据) + if data_length > 4096: + print(f"❌ 数据长度 {data_length} 字节超过SSL TCP限制(4096字节)") + return False + + # 将二进制数据转换为HEX字符串 + hex_data = data.hex().upper() # 转换为大写HEX字符串 + hex_length = len(hex_data) # HEX字符串长度(二进制长度 * 2) + # SSL TCP发送命令格式: AT+SSLTCPSEND=, + # 注意:根据AT命令手册,SSL TCP没有HEX模式参数,HEX模式通过AT+RECVMODE设置 + # length参数是指二进制数据的字节数,而不是HEX字符串长度 + # 示例:AT+SSLTCPSEND=0,1024 表示发送1024字节的二进制数据 command = f'AT+SSLTCPSEND={self.socket_id},{data_length}' self.at.serial_conn.reset_input_buffer() cmd_bytes = (command + "\r\n").encode('utf-8') self.at.serial_conn.write(cmd_bytes) - print(f"发送: {command}") + print(f"发送: {command} (二进制长度: {data_length} 字节, HEX字符串长度: {hex_length} 字符)") # 等待">"提示符 print("等待'>'提示符...") @@ -337,9 +572,9 @@ class N706ATCPClient: print("❌ 未收到'>'提示符") return False - # 发送数据 - print(f"发送SSL TCP数据 ({data_length} 字节)...") - self.at.serial_conn.write(data) + # 发送HEX字符串数据 + print(f"发送SSL TCP数据 (HEX模式: {hex_length} 字符, 原始二进制: {data_length} 字节)...") + self.at.serial_conn.write(hex_data.encode('utf-8')) self.at.serial_conn.flush() # 等待发送完成响应(+SSLTCPSEND: ,OK 或 +SSLTCPSEND: ,FAIL) @@ -354,7 +589,7 @@ class N706ATCPClient: print(f"接收: {line}") if f"+SSLTCPSEND: {self.socket_id},OK" in line: send_complete = True - print(f"✅ SSL TCP数据发送成功: {data_length} 字节") + print(f"✅ SSL TCP数据发送成功: {data_length} 字节 (HEX长度: {hex_length})") break if "FAIL" in line or "ERROR" in line: print(f"❌ SSL TCP数据发送失败: {line}") @@ -369,14 +604,14 @@ class N706ATCPClient: def receive(self, max_length=2048, timeout=30): """ - 接收TCP/TCP+SSL数据(非透传模式) + 接收TCP/TCP+SSL数据(非透传模式,HEX模式) Args: max_length: 最大接收长度(1-2048字节),默认2048 timeout: 超时时间(秒),默认30 Returns: - bytes: 接收到的数据,如果失败返回None + bytes: 接收到的二进制数据,如果失败返回None """ if not self.is_connected: print("❌ 连接未建立,无法接收数据") @@ -398,46 +633,44 @@ class N706ATCPClient: print(f"❌ 读取数据失败: {response}") return None - # 解析响应 + # 解析响应(HEX模式) if self.is_ssl: - # +SSLTCPREAD: ,, + # +SSLTCPREAD: ,, pattern = re.compile(r'\+SSLTCPREAD:\s*(\d+),(\d+),(.+)') else: - # +TCPREAD: ,, + # +TCPREAD: ,, pattern = re.compile(r'\+TCPREAD:\s*(\d+),(\d+),(.+)') match = pattern.search(response) if match: - length = int(match.group(2)) - data_str = match.group(3).strip() + hex_length = int(match.group(2)) # HEX字符串长度 + hex_data_str = match.group(3).strip() - # 尝试解析数据(可能是ASCII或HEX) + # 将HEX字符串转换为二进制数据 try: - # 先尝试作为ASCII字符串 - data = data_str.encode('utf-8') - if len(data) == length: + data = bytes.fromhex(hex_data_str) + # 验证长度:HEX字符串长度应该是二进制长度的2倍 + if len(hex_data_str) == hex_length and len(data) * 2 == hex_length: return data - # 如果长度不匹配,尝试作为HEX字符串 - data = bytes.fromhex(data_str) - if len(data) == length: + else: + print(f"⚠️ HEX数据长度不匹配: hex_str_len={len(hex_data_str)}, hex_len={hex_length}, binary_len={len(data)}") + # 即使长度不匹配,也返回解析的数据 return data - except: - pass - - # 如果解析失败,返回原始字符串的字节 - return data_str.encode('utf-8') + except ValueError as e: + print(f"❌ HEX字符串解析失败: {e}, hex_data={hex_data_str[:100]}...") + return None return None def wait_for_data(self, timeout=30): """ - 被动等待接收数据(透传或非透传模式都支持) + 被动等待接收数据(透传或非透传模式都支持,HEX模式) Args: timeout: 超时时间(秒),默认30 Returns: - bytes: 接收到的数据,如果超时返回None + bytes: 接收到的二进制数据,如果超时返回None """ if not self.is_connected: print("❌ 连接未建立,无法接收数据") @@ -455,45 +688,277 @@ class N706ATCPClient: print(f"等待数据超时: {timeout}秒") return None - # 解析接收到的数据 + # 解析接收到的数据(HEX模式) if self.is_ssl: - # +SSLTCPRECV: ,, + # +SSLTCPRECV: ,, recv_pattern = re.compile(r'\+SSLTCPRECV:\s*(\d+),(\d+),(.+)') else: - # +TCPRECV: ,, 或 +TCPRECV: + # +TCPRECV: ,, 或 +TCPRECV: recv_pattern = re.compile(r'\+TCPRECV:\s*(\d+)(?:,(\d+),(.+))?') match = recv_pattern.search(response) if match: if self.is_ssl: - length = int(match.group(2)) - data_str = match.group(3).strip() + hex_length = int(match.group(2)) # HEX字符串长度 + hex_data_str = match.group(3).strip() else: if match.group(2): # 有数据 - length = int(match.group(2)) - data_str = match.group(3).strip() + hex_length = int(match.group(2)) # HEX字符串长度 + hex_data_str = match.group(3).strip() else: # 只有socket ID,需要主动读取 print("收到+TCPRECV提示,需要主动读取数据") return self.receive() - # 尝试解析数据 + # 将HEX字符串转换为二进制数据 try: - # 先尝试作为ASCII字符串 - data = data_str.encode('utf-8') - if len(data) == length: + data = bytes.fromhex(hex_data_str) + # 验证长度:HEX字符串长度应该是二进制长度的2倍 + if len(hex_data_str) == hex_length and len(data) * 2 == hex_length: return data - # 如果长度不匹配,尝试作为HEX字符串 - data = bytes.fromhex(data_str) - if len(data) == length: + else: + print(f"⚠️ HEX数据长度不匹配: hex_str_len={len(hex_data_str)}, hex_len={hex_length}, binary_len={len(data)}") + # 即使长度不匹配,也返回解析的数据 return data - except: - pass - - # 如果解析失败,返回原始字符串的字节 - return data_str.encode('utf-8') + except ValueError as e: + print(f"❌ HEX字符串解析失败: {e}, hex_data={hex_data_str[:100]}...") + return None return None + def get_status(self): + """ + 查询TCP/UDP链路状态(AT+IPSTATUS) + + Returns: + dict: 包含状态信息的字典,格式: + { + 'success': bool, + 'socket_id': int | None, + 'status': 'CONNECT' | 'DISCONNECT' | 'CONNECTING' | 'DISCONNECTING' | None, + 'type': 'TCP' | 'UDP' | None, + 'buffer_size': int | None, # 可用send buffer大小(字节) + 'error': str | None + } + """ + # 直接发送命令并读取响应,不依赖send_at_command的success标志 + # 因为AT+IPSTATUS可能不返回OK,只返回+IPSTATUS响应 + command = f'AT+IPSTATUS={self.socket_id}' + if not self.at.serial_conn or not self.at.serial_conn.is_open: + return { + 'success': False, + 'socket_id': None, + 'status': None, + 'type': None, + 'buffer_size': None, + 'error': '串口未连接' + } + + # 清空输入缓冲区 + self.at.serial_conn.reset_input_buffer() + + # 发送命令 + cmd_bytes = (command + "\r\n").encode('utf-8') + self.at.serial_conn.write(cmd_bytes) + print(f"发送: {command}") + + # 读取响应(最多等待5秒) + start_time = time.time() + timeout = 5 + response_lines = [] + + while time.time() - start_time < timeout: + if self.at.serial_conn.in_waiting > 0: + line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip() + if line: + response_lines.append(line) + print(f"接收: {line}") + + # 如果收到+IPSTATUS响应,可以提前结束 + if '+IPSTATUS:' in line: + # 继续读取一小段时间,看是否还有更多响应(如OK) + time.sleep(0.1) + if self.at.serial_conn.in_waiting > 0: + additional = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip() + if additional: + response_lines.append(additional) + print(f"接收: {additional}") + break + + # 检查错误响应 + if 'ERROR' in line: + break + + response = "\n".join(response_lines) + + # 解析响应格式: +IPSTATUS: ,[,,] + # 示例: +IPSTATUS: 0,CONNECT,TCP,4096 + # 或: +IPSTATUS: 0,DISCONNECT + pattern = re.compile(r'\+IPSTATUS:\s*(\d+),(\w+)(?:,(\w+)(?:,(\d+))?)?') + match = pattern.search(response) + + if match: + socket_id = int(match.group(1)) + status = match.group(2) # CONNECT, DISCONNECT, CONNECTING, DISCONNECTING + link_type = match.group(3) if match.group(3) else None # TCP, UDP + buffer_size = int(match.group(4)) if match.group(4) else None + + return { + 'success': True, + 'socket_id': socket_id, + 'status': status, + 'type': link_type, + 'buffer_size': buffer_size, + 'error': None + } + elif 'ERROR' in response: + return { + 'success': False, + 'socket_id': None, + 'status': None, + 'type': None, + 'buffer_size': None, + 'error': f'命令执行失败: {response}' + } + else: + return { + 'success': False, + 'socket_id': None, + 'status': None, + 'type': None, + 'buffer_size': None, + 'error': f'未收到有效响应: {response}' + } + + def get_send_status(self): + """ + 查询TCP链路发送数据状态(AT+TCPACK) + 注意:SSL TCP不支持此命令 + + Returns: + dict: 包含发送状态信息的字典,格式: + { + 'success': bool, + 'socket_id': int | None, + 'data_sent': int | None, # 已发送的数据大小(字节) + 'acked_recv': int | None, # 对方已确认接收的数据大小(字节) + 'error': str | None + } + """ + if self.is_ssl: + # SSL TCP不支持AT+TCPACK,返回错误 + return { + 'success': False, + 'socket_id': None, + 'data_sent': None, + 'acked_recv': None, + 'error': 'SSL TCP不支持AT+TCPACK查询' + } + + # 直接发送命令并读取响应,不依赖send_at_command的success标志 + # 因为AT+TCPACK不返回OK,只返回+TCPACK响应 + command = f'AT+TCPACK={self.socket_id}' + if not self.at.serial_conn or not self.at.serial_conn.is_open: + return { + 'success': False, + 'socket_id': None, + 'data_sent': None, + 'acked_recv': None, + 'error': '串口未连接' + } + + # 清空输入缓冲区 + self.at.serial_conn.reset_input_buffer() + + # 发送命令 + cmd_bytes = (command + "\r\n").encode('utf-8') + self.at.serial_conn.write(cmd_bytes) + print(f"发送: {command}") + + # 读取响应(最多等待5秒) + start_time = time.time() + timeout = 5 + response_lines = [] + + while time.time() - start_time < timeout: + if self.at.serial_conn.in_waiting > 0: + line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip() + if line: + response_lines.append(line) + print(f"接收: {line}") + + # 如果收到+TCPACK响应,可以提前结束 + if '+TCPACK:' in line: + # 继续读取一小段时间,看是否还有更多响应 + time.sleep(0.1) + if self.at.serial_conn.in_waiting > 0: + additional = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip() + if additional: + response_lines.append(additional) + print(f"接收: {additional}") + break + + # 检查错误响应 + if 'ERROR' in line: + break + + response = "\n".join(response_lines) + + # 解析响应格式: +TCPACK: ,, + # 或: +TCPACK: ,DISCONNECT + # 或: +TCPACK: NO TCP LINK + pattern = re.compile(r'\+TCPACK:\s*(\d+),(\d+),(\d+)') + match = pattern.search(response) + + if match: + socket_id = int(match.group(1)) + data_sent = int(match.group(2)) + acked_recv = int(match.group(3)) + + return { + 'success': True, + 'socket_id': socket_id, + 'data_sent': data_sent, + 'acked_recv': acked_recv, + 'error': None + } + elif 'DISCONNECT' in response: + # 提取socket_id + disconnect_pattern = re.compile(r'\+TCPACK:\s*(\d+),DISCONNECT') + disconnect_match = disconnect_pattern.search(response) + socket_id = int(disconnect_match.group(1)) if disconnect_match else None + + return { + 'success': False, + 'socket_id': socket_id, + 'data_sent': None, + 'acked_recv': None, + 'error': '连接已断开' + } + elif 'NO TCP LINK' in response: + return { + 'success': False, + 'socket_id': None, + 'data_sent': None, + 'acked_recv': None, + 'error': '未建立TCP连接(可能是UDP连接)' + } + elif 'ERROR' in response: + return { + 'success': False, + 'socket_id': None, + 'data_sent': None, + 'acked_recv': None, + 'error': f'命令执行失败: {response}' + } + else: + return { + 'success': False, + 'socket_id': None, + 'data_sent': None, + 'acked_recv': None, + 'error': f'未收到有效响应: {response}' + } + def close(self): """ 关闭TCP/TCP+SSL连接 diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/tcp_test_client.py b/code/sdk/zhihui/leiniao/leiniao_python_test/tcp_test_client.py new file mode 100644 index 000000000..c74b89545 --- /dev/null +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/tcp_test_client.py @@ -0,0 +1,425 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +TCP测试客户端(标准Python socket版本) +使用标准Python socket接口连接TCP服务器 +支持交互式发送和接收数据 +""" + +import socket +import time +import threading +import sys + +# -------------------------- 1. 配置参数 -------------------------- +# 服务器配置 +SERVER_HOST = "snicker.com.cn" +SERVER_PORT = 8910 + +# 接收超时时间(秒) +RECEIVE_TIMEOUT = 30 + +# -------------------------- 2. TCP客户端类 -------------------------- +class TCPTestClient: + """标准Python socket TCP客户端""" + + def __init__(self, host, port): + """ + 初始化TCP客户端 + + Args: + host: 服务器地址 + port: 服务器端口 + """ + self.host = host + self.port = port + self.socket = None + self.connected = False + self.receive_thread = None + self.running = False + self.received_data_queue = [] + self.data_lock = threading.Lock() + + def connect(self): + """ + 连接到服务器 + + Returns: + bool: 是否连接成功 + """ + try: + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.settimeout(5.0) # 连接超时5秒 + + print(f"正在连接服务器: {self.host}:{self.port}...") + self.socket.connect((self.host, self.port)) + + # 连接成功后,设置接收超时 + self.socket.settimeout(RECEIVE_TIMEOUT) + self.connected = True + + # 启动接收线程 + self.running = True + self.receive_thread = threading.Thread(target=self._receive_loop, daemon=True) + self.receive_thread.start() + + print(f"✅ TCP连接成功: {self.host}:{self.port}") + return True + + except socket.timeout: + print(f"❌ 连接超时: {self.host}:{self.port}") + return False + except socket.error as e: + print(f"❌ 连接失败: {e}") + return False + except Exception as e: + print(f"❌ 连接异常: {e}") + return False + + def _receive_loop(self): + """接收数据循环(后台线程)""" + while self.running and self.connected: + try: + data = self.socket.recv(4096) + if not data: + # 服务器关闭连接 + print("\n⚠️ 服务器关闭了连接") + self.connected = False + break + + # 将接收到的数据放入队列 + with self.data_lock: + self.received_data_queue.append(data) + + except socket.timeout: + # 超时是正常的,继续等待 + continue + except socket.error as e: + if self.running: + print(f"\n❌ 接收数据时出错: {e}") + self.connected = False + break + except Exception as e: + if self.running: + print(f"\n❌ 接收数据异常: {e}") + self.connected = False + break + + def send(self, data): + """ + 发送数据 + + Args: + data: 要发送的数据(bytes) + + Returns: + bool: 是否发送成功 + """ + if not self.connected: + print("❌ 连接未建立,无法发送数据") + return False + + try: + self.socket.sendall(data) + return True + except socket.error as e: + print(f"❌ 发送数据失败: {e}") + self.connected = False + return False + except Exception as e: + print(f"❌ 发送数据异常: {e}") + return False + + def receive(self, timeout=None): + """ + 主动接收数据(从队列中获取) + + Args: + timeout: 超时时间(秒),None表示立即返回 + + Returns: + bytes: 接收到的数据,如果没有数据返回None + """ + if timeout is None: + timeout = 0 + + start_time = time.time() + while time.time() - start_time < timeout: + with self.data_lock: + if self.received_data_queue: + return self.received_data_queue.pop(0) + time.sleep(0.1) + + # 检查是否有数据 + with self.data_lock: + if self.received_data_queue: + return self.received_data_queue.pop(0) + + return None + + def wait_for_data(self, timeout=30): + """ + 被动等待接收数据 + + Args: + timeout: 超时时间(秒) + + Returns: + bytes: 接收到的数据,超时返回None + """ + start_time = time.time() + while time.time() - start_time < timeout: + with self.data_lock: + if self.received_data_queue: + return self.received_data_queue.pop(0) + + if not self.connected: + return None + + time.sleep(0.1) + + return None + + def close(self): + """关闭连接""" + self.running = False + self.connected = False + + if self.receive_thread and self.receive_thread.is_alive(): + # 等待接收线程结束 + self.receive_thread.join(timeout=1.0) + + if self.socket: + try: + self.socket.close() + except: + pass + + print("✅ TCP连接已关闭") + +# -------------------------- 3. 连接测试服务器函数 -------------------------- +def connect_test_server(): + """连接测试服务器并交互式发送和接收消息""" + + # 初始化TCP客户端 + print("\n" + "=" * 50) + print("【步骤1: 初始化TCP客户端】") + print("=" * 50) + tcp_client = TCPTestClient(SERVER_HOST, SERVER_PORT) + + try: + # 建立TCP连接 + print("\n" + "=" * 50) + print("【步骤2: 建立TCP连接】") + print("=" * 50) + print(f"连接服务器: {SERVER_HOST}:{SERVER_PORT}") + + if not tcp_client.connect(): + print(f"❌ TCP连接失败") + return False + + print(f"✅ TCP连接成功") + + # 交互式发送和接收 + print("\n" + "=" * 50) + print("【步骤3: 交互式发送和接收数据】") + print("=" * 50) + print("命令说明:") + print(" send - 发送HEX数据(例如: send 48656C6C6F)") + print(" send_ascii - 发送ASCII文本(例如: send_ascii Hello)") + print(" recv - 主动接收数据") + print(" wait - 切换到被动等待模式(自动接收)") + print(" interactive - 切换到交互模式(手动输入命令)") + print(" quit - 退出程序") + print("=" * 50) + + message_count = 0 + start_time = time.time() + wait_mode = False # 默认交互模式,方便用户输入命令 + + def print_received_data(data, msg_num, elapsed): + """打印接收到的数据""" + print(f"\n【接收消息 #{msg_num}】 (连接后 {elapsed:.1f}秒)") + print(f"数据长度: {len(data)} 字节") + + # 打印首字节信息 + if len(data) > 0: + first_byte = data[0] + print(f"首字节: 0x{first_byte:02X} ({first_byte:3d})") + + # 打印完整数据 - HEX格式 + hex_str = data.hex().upper() + if len(hex_str) > 200: + print(f"HEX格式(前200字符): {hex_str[:200]}...") + print(f"HEX格式(完整,共{len(hex_str)}字符):") + # 每行显示64个字符(32字节) + for i in range(0, len(hex_str), 64): + chunk = hex_str[i:i+64] + offset = i // 2 + print(f" {offset:06X}: {chunk}") + else: + print(f"HEX格式: {hex_str}") + + # 打印完整数据 - ASCII格式 + try: + ascii_str = data.decode('utf-8', errors='replace') + # 检查是否包含可打印字符 + if all(32 <= ord(c) < 127 or c in '\n\r\t' for c in ascii_str): + print(f"ASCII格式: {repr(ascii_str)}") + else: + # 包含非打印字符,显示为repr格式 + print(f"ASCII格式(含非打印字符): {repr(ascii_str)}") + # 同时显示可打印部分 + printable = ''.join(c if 32 <= ord(c) < 127 or c in '\n\r\t' else '.' for c in ascii_str) + if printable.strip(): + print(f"可打印部分: {printable}") + except: + print(f"ASCII格式: (无法解码为UTF-8)") + + # 打印数据的前几个字节(十六进制和十进制) + if len(data) > 0: + preview_bytes = min(16, len(data)) + hex_preview = ' '.join(f'{b:02X}' for b in data[:preview_bytes]) + dec_preview = ' '.join(f'{b:3d}' for b in data[:preview_bytes]) + print(f"前{preview_bytes}字节 HEX: {hex_preview}") + print(f"前{preview_bytes}字节 DEC: {dec_preview}") + + print("-" * 50) + + try: + while True: + if wait_mode: + # 被动等待接收数据 + data = tcp_client.wait_for_data(timeout=RECEIVE_TIMEOUT) + + if data: + message_count += 1 + elapsed_time = time.time() - start_time + print_received_data(data, message_count, elapsed_time) + else: + if not tcp_client.connected: + print("\n⚠️ 连接已断开") + break + print(f"\n⚠️ 接收超时({RECEIVE_TIMEOUT}秒),继续等待...") + print("提示: 按 Ctrl+C 中断,然后输入 'interactive' 切换到交互模式") + print("-" * 50) + else: + # 交互模式:等待用户输入 + try: + user_input = input("\n请输入命令 (send/send_ascii/recv/wait/interactive/quit): ").strip() + + if not user_input: + continue + + if user_input.lower() == 'quit': + break + elif user_input.lower() == 'recv': + # 主动接收数据 + data = tcp_client.receive(timeout=0.1) + if data: + message_count += 1 + elapsed_time = time.time() - start_time + print_received_data(data, message_count, elapsed_time) + else: + print("❌ 未接收到数据") + elif user_input.lower() == 'wait': + wait_mode = True + print("✅ 切换到被动等待模式(自动接收数据)") + print("提示: 按 Ctrl+C 中断,然后输入 'interactive' 切换回交互模式") + elif user_input.lower() == 'interactive': + wait_mode = False + print("✅ 切换到交互模式(手动输入命令)") + elif user_input.startswith('send '): + # 发送HEX数据 + hex_str = user_input[5:].strip().replace(' ', '').upper() + if not hex_str: + print("❌ 错误: send 命令需要HEX字符串参数") + print(" 示例: send 48656C6C6F") + continue + try: + send_data = bytes.fromhex(hex_str) + print(f"发送HEX数据: {hex_str} (二进制长度: {len(send_data)} 字节)") + if tcp_client.send(send_data): + print("✅ 数据发送成功") + else: + print("❌ 数据发送失败") + except ValueError as e: + print(f"❌ HEX字符串格式错误: {e}") + print(" 提示: HEX字符串只能包含 0-9, A-F 字符") + elif user_input.startswith('send_ascii '): + # 发送ASCII文本 + ascii_text = user_input[11:].strip() + if not ascii_text: + print("❌ 错误: send_ascii 命令需要文本参数") + print(" 示例: send_ascii Hello World") + continue + send_data = ascii_text.encode('utf-8') + hex_str = send_data.hex().upper() + print(f"发送ASCII文本: {ascii_text}") + print(f" -> 二进制: {send_data}") + print(f" -> HEX字符串: {hex_str}") + if tcp_client.send(send_data): + print("✅ 数据发送成功") + else: + print("❌ 数据发送失败") + else: + print("❌ 未知命令") + print(" 可用命令: send, send_ascii, recv, wait, interactive, quit") + except EOFError: + # 如果没有交互式输入,切换到被动等待模式 + wait_mode = True + print("\n⚠️ 检测到非交互式环境,切换到被动等待模式...") + except KeyboardInterrupt: + # 用户按 Ctrl+C,切换到交互模式 + wait_mode = False + print("\n\n⚠️ 用户中断,切换到交互模式") + print("提示: 输入 'wait' 可切换回被动等待模式") + + except KeyboardInterrupt: + print("\n\n用户中断") + + # 关闭连接 + print("\n" + "=" * 50) + print("【步骤4: 关闭TCP连接】") + print("=" * 50) + tcp_client.close() + + # 统计信息 + total_time = time.time() - start_time + print("\n" + "=" * 50) + print("【连接统计】") + print("=" * 50) + print(f"连接持续时间: {total_time:.1f} 秒") + print(f"接收消息数量: {message_count}") + if message_count > 0: + print(f"平均消息间隔: {total_time/message_count:.1f} 秒") + print("=" * 50) + + return True + + except Exception as e: + print("\n" + "=" * 50) + print("【错误处理】") + print("=" * 50) + print(f"❌ 连接异常: {str(e)}") + import traceback + traceback.print_exc() + return False + +# -------------------------- 4. 主程序 -------------------------- +if __name__ == "__main__": + print("=" * 50) + print("TCP测试客户端(标准Python socket版本)") + print("=" * 50) + print(f"服务器地址: {SERVER_HOST}:{SERVER_PORT}") + print("=" * 50) + + # 执行连接 + success = connect_test_server() + + if success: + print("\n✅ 连接测试完成!") + else: + print("\n❌ 连接测试失败!") + sys.exit(1) + diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/tcp_test_server.py b/code/sdk/zhihui/leiniao/leiniao_python_test/tcp_test_server.py index c4c30ca06..b3312ddc2 100644 --- a/code/sdk/zhihui/leiniao/leiniao_python_test/tcp_test_server.py +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/tcp_test_server.py @@ -123,30 +123,50 @@ class TCPTestServer: # 显示接收到的数据 timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + packet_size = len(data) + first_byte = data[0] if data else 0 + print(f"\n[{timestamp}] 📥 收到数据 [客户端ID: {client_id}, {address[0]}:{address[1]}]") - print(f" 数据长度: {len(data)} 字节") + print(f"数据长度: {packet_size} 字节, 首字节: 0x{first_byte:02X} ({first_byte:3d})") + + # 打印完整数据 - HEX格式 + hex_str = data.hex().upper() + if len(hex_str) > 200: + print(f"HEX格式(前200字符): {hex_str[:200]}...") + print(f"HEX格式(完整,共{len(hex_str)}字符):") + # 每行显示64个字符(32字节) + for i in range(0, len(hex_str), 64): + chunk = hex_str[i:i+64] + offset = i // 2 + print(f" {offset:06X}: {chunk}") + else: + print(f"HEX格式: {hex_str}") - # 尝试以文本和十六进制显示 + # 打印完整数据 - ASCII格式 try: - text = data.decode('utf-8', errors='ignore') - if text.isprintable() or '\n' in text or '\r' in text: - print(f" 文本内容:") - for line in text.splitlines(): - print(f" {line}") + ascii_str = data.decode('utf-8', errors='replace') + # 检查是否包含可打印字符 + if all(32 <= ord(c) < 127 or c in '\n\r\t' for c in ascii_str): + print(f"ASCII格式: {repr(ascii_str)}") else: - print(f" 十六进制: {data.hex()}") + # 包含非打印字符,显示为repr格式 + print(f"ASCII格式(含非打印字符): {repr(ascii_str)}") + # 同时显示可打印部分 + printable = ''.join(c if 32 <= ord(c) < 127 or c in '\n\r\t' else '.' for c in ascii_str) + if printable.strip(): + print(f"可打印部分: {printable}") except: - print(f" 十六进制: {data.hex()}") + print(f"ASCII格式: (无法解码为UTF-8)") - # 显示前64字节的ASCII表示 - if len(data) <= 64: - ascii_repr = ''.join(chr(b) if 32 <= b < 127 else '.' for b in data) - print(f" ASCII: {ascii_repr}") - else: - ascii_repr = ''.join(chr(b) if 32 <= b < 127 else '.' for b in data[:64]) - print(f" ASCII (前64字节): {ascii_repr}...") + # 打印数据的前几个字节(十六进制和十进制) + if packet_size > 0: + preview_bytes = min(16, packet_size) + hex_preview = ' '.join(f'{b:02X}' for b in data[:preview_bytes]) + dec_preview = ' '.join(f'{b:3d}' for b in data[:preview_bytes]) + print(f"前{preview_bytes}字节 HEX: {hex_preview}") + print(f"前{preview_bytes}字节 DEC: {dec_preview}") - print() + print("-" * 60) except socket.timeout: continue diff --git a/code/sdk/zhihui/n706a_net/AT_COMMAND_FIXES.md b/code/sdk/zhihui/n706a_net/AT_COMMAND_FIXES.md new file mode 100644 index 000000000..ba7e2ec63 --- /dev/null +++ b/code/sdk/zhihui/n706a_net/AT_COMMAND_FIXES.md @@ -0,0 +1,95 @@ +# N706A AT指令实现修复说明 + +## 问题分析 + +对比Python脚本(已验证工作)和C语言实现的AT指令步骤,发现以下关键差异: + +### Python脚本的AT指令流程(TCP/SSL TCP) + +1. **SSL配置(仅HTTPS需要)**: + - `AT+SSLTCPCFG="sslversion",{version}` - 设置SSL版本 + - `AT+SSLTCPCFG="authmode",{mode}` - 设置认证模式 + - `AT+SSLTCPCFG="cacert","{cert_file}"` - 设置CA证书(如果需要) + +2. **建立连接**: + - 普通TCP: `AT+TCPSETUP={socket_id},{host},{port}` + - SSL TCP: `AT+SSLTCPSETUP={socket_id},{host},{port},{mode}` + - **注意**:第4个参数是**透传模式**(mode),不是认证模式 + - mode=0: 非透传模式(默认,推荐) + - mode=1: 透传模式 + +3. **发送数据**: + - 普通TCP: `AT+TCPSEND={socket_id},{length}` + - SSL TCP: `AT+SSLTCPSEND={socket_id},{length}` + +4. **接收数据**: + - 主动读取: `AT+TCPREAD={socket_id},{length}` / `AT+SSLTCPREAD={socket_id},{length}` + - 被动接收: 等待 `+TCPRECV:` / `+SSLTCPRECV:` URC + +### C语言实现的问题 + +**关键错误**:`AT+SSLTCPSETUP` 的第4个参数错误地使用了 `auth_mode`(认证模式),而实际上应该是 `transparent_mode`(透传模式)。 + +```c +// 错误实现(修复前) +snprintf(at_cmd, sizeof(at_cmd), + "AT+SSLTCPSETUP=%d,%s,%d,%d", + sock->socket_id, host, port, sock->auth_mode); // ❌ 错误:使用了auth_mode + +// 正确实现(修复后) +int transparent_mode = 0; // 默认非透传模式 +snprintf(at_cmd, sizeof(at_cmd), + "AT+SSLTCPSETUP=%d,%s,%d,%d", + sock->socket_id, host, port, transparent_mode); // ✅ 正确:使用transparent_mode +``` + +## 修复内容 + +### 1. 修复 `AT+SSLTCPSETUP` 参数错误 + +**文件**: `code/sdk/zhihui/n706a_net/src/n706a_tcp_client.c` + +**修复位置**: `n706a_tcp_connect()` 函数中的SSL TCP连接部分 + +**修复说明**: +- 将第4个参数从 `sock->auth_mode` 改为 `transparent_mode = 0` +- 添加注释说明参数含义 +- SSL认证模式通过 `AT+SSLTCPCFG` 配置,不在连接命令中指定 + +### 2. 其他注意事项 + +1. **SSL配置时机**: + - SSL参数(sslversion, authmode, cacert)通过 `AT+SSLTCPCFG` 配置 + - 应在建立SSL连接**之前**调用 `n706a_tcp_config_ssl()` 配置SSL参数 + - 如果使用HTTPS,建议在 `n706a_http_client_connect()` 中检查并配置SSL + +2. **接收模式配置**: + - C语言实现已在 `n706a_net_api.c` 中配置了 `AT+RECVMODE=1,1`(HEX模式) + - 这与Python脚本一致,无需修改 + +3. **URC处理**: + - C语言实现使用异步URC接收机制 + - 已正确注册 `+TCPSETUP`、`+TCPRECV`、`+SSLTCPRECV` 等URC回调 + - 与Python脚本的同步等待机制不同,但功能等价 + +## 验证建议 + +1. **测试普通TCP连接**: + - 验证 `AT+TCPSETUP` 连接是否正常 + - 验证数据收发是否正常 + +2. **测试SSL TCP连接**: + - 验证修复后的 `AT+SSLTCPSETUP` 连接是否正常 + - 验证SSL握手是否成功 + - 验证HTTPS请求是否正常 + +3. **对比Python脚本**: + - 使用相同的URL和参数测试 + - 对比AT指令序列是否一致 + - 验证响应是否一致 + +## 总结 + +主要修复了 `AT+SSLTCPSETUP` 命令的参数错误,这是导致SSL TCP连接失败的关键原因。修复后,C语言实现的AT指令序列应与Python脚本一致。 + + diff --git a/code/sdk/zhihui/n706a_net/CMakeLists.txt b/code/sdk/zhihui/n706a_net/CMakeLists.txt index 7cb2f32e9..22d8bb0e4 100644 --- a/code/sdk/zhihui/n706a_net/CMakeLists.txt +++ b/code/sdk/zhihui/n706a_net/CMakeLists.txt @@ -27,13 +27,12 @@ set(PUBLIC_HEADER set(PUBLIC_DEFINES SUPPORT_N706A_NET # 定义此宏启用 N706A AT 模式(独立于PPP) SUPPORT_AT_N706A_NET # 开机自动注册 N706A AT 命令 - # SSL实现方式(二选一,仅影响n706a_net内部): - # N706A_SSL_USE_AT_COMMAND # 使用AT指令实现SSL(推荐,利用N706A内置SSL) - N706A_SSL_USE_MBEDTLS # 使用mbedtls库实现SSL(不推荐:与AT模式不兼容) + # SSL实现方式(仅支持AT指令模式): + N706A_SSL_USE_AT_COMMAND # 使用AT指令实现SSL(利用N706A内置SSL) ) set(PRIVATE_HEADER - ${CMAKE_CURRENT_SOURCE_DIR}/../ppp/include + ${CMAKE_CURRENT_SOURCE_DIR}/include # n706a_net自己的头文件优先(避免与ppp同名文件冲突) ) set(PRIVATE_DEFINES diff --git a/code/sdk/zhihui/n706a_net/example/n706a_net_example.c b/code/sdk/zhihui/n706a_net/example/n706a_net_example.c index e48c1310d..874cbfd46 100644 --- a/code/sdk/zhihui/n706a_net/example/n706a_net_example.c +++ b/code/sdk/zhihui/n706a_net/example/n706a_net_example.c @@ -85,7 +85,15 @@ int n706a_net_env_init(void) if (ret != ERRCODE_SUCC) { PRINT("Warning: Failed to enable power save mode\r\n"); } + + #endif + /* 启用数据Ring功能 */ + PRINT("Enabling data ring...\r\n"); + ret = n706a_enable_data_ring(); + if (ret != ERRCODE_SUCC) { + PRINT("Warning: Failed to enable data ring\r\n"); + } PRINT("N706A network ready for use\r\n"); diff --git a/code/sdk/zhihui/n706a_net/include/n706a_at_handler.h b/code/sdk/zhihui/n706a_net/include/n706a_at_handler.h index fca032a79..20c80c4b8 100644 --- a/code/sdk/zhihui/n706a_net/include/n706a_at_handler.h +++ b/code/sdk/zhihui/n706a_net/include/n706a_at_handler.h @@ -183,6 +183,47 @@ void n706a_at_register_tcpsend_urc_callback(n706a_tcpsend_urc_callback_t callbac */ void n706a_at_register_network_status_urc_callback(n706a_network_status_urc_callback_t callback); +/** + * @brief 设置TCP接收模式(HEX模式或ASCII模式) + * @param hex_mode true表示HEX模式(AT+RECVMODE=1,1),false表示ASCII模式(AT+RECVMODE=1,0) + */ +void n706a_at_set_tcp_recv_hex_mode(bool hex_mode); + +/** + * @brief 获取TCP接收模式(HEX模式或ASCII模式) + * @return true表示HEX模式,false表示ASCII模式 + */ +bool n706a_at_get_tcp_recv_hex_mode(void); + +/** + * @brief 获取累积缓冲区长度(用于调试) + * @return 累积缓冲区当前长度 + */ +uint32_t n706a_at_get_accumulate_buffer_length(void); + +/** + * @brief 获取累积缓冲区的最后几个字节(用于调试) + * @param buffer 输出缓冲区 + * @param max_len 最大长度 + * @param actual_len 实际长度(输出) + * @return true=成功,false=失败 + */ +bool n706a_at_get_accumulate_buffer_preview(uint8_t *buffer, uint32_t max_len, uint32_t *actual_len); + +/** + * @brief 获取累积缓冲区的完整内容(从开头开始,用于调试) + * @param buffer 输出缓冲区 + * @param max_len 最大长度 + * @param actual_len 实际长度(输出) + * @return true=成功,false=失败 + */ +bool n706a_at_get_accumulate_buffer_all(uint8_t *buffer, uint32_t max_len, uint32_t *actual_len); + +/** + * @brief 清空累积缓冲区(用于socket关闭后清理残留数据) + */ +void n706a_at_clear_accumulate_buffer(void); + #ifdef __cplusplus } #endif diff --git a/code/sdk/zhihui/n706a_net/include/n706a_http_client.h b/code/sdk/zhihui/n706a_net/include/n706a_http_client.h index 530fd3f91..37e358704 100644 --- a/code/sdk/zhihui/n706a_net/include/n706a_http_client.h +++ b/code/sdk/zhihui/n706a_net/include/n706a_http_client.h @@ -1,15 +1,14 @@ /** * @file n706a_http_client.h - * @brief N706A HTTP/HTTPS客户端接口(基于TCP实现) + * @brief N706A HTTP/HTTPS客户端接口(基于HTTP专用AT指令) * - * 本实现采用统一的TCP底层架构: - * - HTTP通过普通TCP连接实现 - * - HTTPS通过SSL TCP连接实现 - * - 手动构造HTTP协议,突破AT+HTTP*单连接限制 - * - 支持6个并发HTTP/HTTPS连接 + * 本实现使用N706A模块的HTTP专用AT指令: + * - HTTP: AT+HTTPPARA, AT+HTTPSETUP, AT+HTTPACTION + * - HTTPS: AT+HTTPSPARA, AT+HTTPSSETUP, AT+HTTPSACTION + * - 与Python脚本实现逻辑一致 * - * @version 3.0 - * @date 2025-10-30 + * @version 4.0 + * @date 2025-01-XX */ #ifndef PPP_N706A_HTTP_CLIENT_H @@ -18,12 +17,37 @@ #include #include #include -#include "n706a_tcp_client.h" #ifdef __cplusplus extern "C" { #endif +/* ========== HTTP收发分片限制(根据AT指令手册) ========== */ +/** + * @brief HTTP接收单次URC最大数据长度 + * + * 根据AT指令手册: + * - AT+TCPREAD最大长度为2048字节 + * - AT+UDPREAD最大长度为2048字节 + * - HTTP接收通过+HTTPRECV URC主动上报,虽然没有明确说明最大长度, + * 但考虑到TCP/UDP的限制,HTTP接收的URC数据长度也应该遵循类似的限制 + * + * 注意:模块可能会自动分片,单次URC的数据长度不应超过此限制 + */ +#define N706A_HTTP_MAX_URC_DATA_LEN 2048 /**< HTTP接收单次URC最大数据长度(字节) */ + +/** + * @brief HTTP发送请求最大长度 + * + * 根据AT指令手册(第13311行): + * - AT+HTTPACTION=99, 的 最大长度为2048字节 + * - AT+HTTPSACTION=99, 的 最大长度为2048字节 + * + * 注意:HTTP请求(包括头部和body)必须作为一个完整的报文发送,不能分片。 + * 如果请求超过此限制,应该返回错误或使用其他方式发送(如TCP直接发送)。 + */ +#define N706A_HTTP_MAX_REQUEST_LEN 2048 /**< HTTP发送请求最大长度(字节) */ + /** * @brief N706A HTTP请求类型 */ @@ -54,24 +78,58 @@ typedef enum { N706A_HTTP_EUNKOWN = -11, /**< 未知错误 */ N706A_HTTP_ETIMEOUT = -12, /**< 超时 */ N706A_HTTP_ENOMEM = -13, /**< 内存分配失败 */ + N706A_HTTP_ERROR = -14, /**< 通用错误 */ } n706a_httpc_result_t; /** - * @brief N706A HTTP客户端结构(基于TCP实现) + * @brief HTTP流式接收数据回调函数类型 + * @param user_data 用户数据指针(由应用层传入) + * @param data 响应体数据块指针 + * @param length 数据块长度 + * @param is_finished 是否是最后一个数据块(true=接收完成,false=还有更多数据) + * @return 0表示成功,非0表示错误(会导致接收中断) + * + * @note 此回调在URC到达时立即调用,应用层可以在此回调中直接写入文件 + * 回调函数应该尽快返回,避免阻塞后续数据接收 + */ +typedef int (*n706a_http_stream_callback_t)(void *user_data, + const uint8_t *data, + size_t length, + bool is_finished); + +/** + * @brief N706A HTTP客户端结构(基于HTTP专用AT指令) */ typedef struct { - n706a_tcp_socket_t tcp_sock; /**< 底层TCP socket */ - int socket_id; /**< TCP socket编号(0-5) */ - bool is_https; /**< HTTPS标志(决定是否使用SSL) */ + bool is_https; /**< HTTPS标志 */ uint16_t remote_port; /**< 远程端口 */ int response_code; /**< HTTP响应码 */ char *header; /**< 自定义请求头 */ char *auth_user; /**< 基本认证用户名 */ char *auth_password; /**< 基本认证密码 */ char host[256]; /**< 主机名 */ + char url_path[512]; /**< URL路径(包含查询参数) */ bool connected; /**< 连接状态 */ bool keepalive; /**< 长连接模式 */ uint32_t timeout_ms; /**< 超时时间(毫秒) */ + + /* HTTP响应接收相关 */ + uint8_t *response_buffer; /**< 响应数据缓冲区 */ + size_t response_buffer_size; /**< 缓冲区大小 */ + size_t response_received; /**< 已接收的响应数据长度 */ + bool response_complete; /**< 响应是否接收完成 */ + void *response_mutex; /**< 响应缓冲区互斥锁 */ + void *response_sem; /**< 响应数据可用信号量 */ + + /* 流式接收相关(可选) */ + n706a_http_stream_callback_t stream_callback; /**< 流式接收回调函数 */ + void *stream_user_data; /**< 流式接收回调的用户数据 */ + bool stream_mode_enabled; /**< 是否启用流式模式 */ + size_t stream_body_received; /**< 流式模式下已接收的响应体总长度 */ + int stream_content_length; /**< 流式模式下的Content-Length(用于判断是否完成) */ + bool stream_header_parsed; /**< 流式模式下响应头是否已解析 */ + bool stream_is_chunked; /**< 流式模式下是否chunked编码 */ + bool stream_connection_closed; /**< 流式模式下连接是否已关闭(检测到+HTTPSCLOSED/+HTTPCLOSED) */ } n706a_http_client_t; /** @@ -98,23 +156,30 @@ typedef struct { /** * @brief 初始化HTTP客户端 * @param client HTTP客户端结构体指针 - * @param socket_id socket ID (0-5),如果传入-1则自动分配空闲的socket_id(推荐,避免冲突) * @return N706A_HTTP_SUCCESS 成功,其他值失败 + * @note HTTP专用AT指令不占用TCP socket,无需socket_id */ -n706a_httpc_result_t n706a_http_client_init(n706a_http_client_t *client, int socket_id); +n706a_httpc_result_t n706a_http_client_init(n706a_http_client_t *client); + +/** + * @brief 设置HTTP/HTTPS请求URL(自动判断协议类型) + * @param client HTTP客户端结构指针 + * @param url 完整URL(如"http://example.com/path"或"https://example.com/path") + * @param keepalive 是否启用长连接,0=关闭(默认),1=启用 + * @return N706A_HTTP_SUCCESS成功,其他失败 + * @note 此函数会解析URL并设置AT+HTTPPARA/AT+HTTPSPARA参数 + */ +n706a_httpc_result_t n706a_http_client_set_url(n706a_http_client_t *client, + const char *url, + int keepalive); /** - * @brief 连接到HTTP/HTTPS服务器 + * @brief 建立HTTP/HTTPS连接(根据URL自动选择) * @param client HTTP客户端结构指针 - * @param host 主机名或IP地址 - * @param port 端口号(HTTP默认80,HTTPS默认443) - * @param use_ssl 是否使用SSL(HTTPS) * @return N706A_HTTP_SUCCESS成功,其他失败 + * @note 调用AT+HTTPSETUP或AT+HTTPSSETUP建立连接 */ -n706a_httpc_result_t n706a_http_client_connect(n706a_http_client_t *client, - const char *host, - uint16_t port, - bool use_ssl); +n706a_httpc_result_t n706a_http_client_setup_connection(n706a_http_client_t *client); /** * @brief 发送HTTP GET请求 @@ -183,6 +248,57 @@ n706a_httpc_result_t n706a_http_client_head(n706a_http_client_t *client, const char *path, n706a_http_client_data_t *client_data); +/** + * @brief 添加SSL证书到N706A模块(仅HTTPS需要) + * @param cert_name 证书文件名(如"ca.crt") + * @param cert_data 证书内容(PEM格式,字节数据) + * @param cert_len 证书数据长度 + * @return N706A_HTTP_SUCCESS成功,其他失败 + * @note 使用AT+CERTADD命令添加证书 + */ +n706a_httpc_result_t n706a_http_client_add_certificate(const char *cert_name, + const uint8_t *cert_data, + size_t cert_len); + +/** + * @brief 检查证书是否已存在于设备中 + * @param cert_name 证书文件名 + * @return true证书存在,false不存在 + */ +bool n706a_http_client_certificate_exists(const char *cert_name); + +/** + * @brief 删除SSL证书 + * @param cert_name 证书文件名 + * @return N706A_HTTP_SUCCESS成功,其他失败 + * @note 使用AT+CERTDEL命令删除证书 + */ +n706a_httpc_result_t n706a_http_client_delete_certificate(const char *cert_name); + +/** + * @brief 初始化HTTPS通用证书(自动检查并添加) + * @return N706A_HTTP_SUCCESS成功,其他失败 + * @note 此函数会自动检查证书是否存在,如果存在则先删除再重新添加(用于测试) + * 建议在首次使用HTTPS前调用 + */ +n706a_httpc_result_t n706a_http_client_init_certificate(void); + +/** + * @brief 配置HTTPS SSL参数(仅HTTPS需要) + * @param ssl_version SSL版本,0=SSL3.0, 1=TLS1.0, 2=TLS1.1, 3=TLS1.2, 4=TLS1.0/1.1/1.2, 5=TLS1.3 + * @param auth_mode 认证模式,0=不认证, 1=单向认证, 2=双向认证 + * @param ca_cert CA证书文件名(单向/双向认证需要,如果为NULL且auth_mode=1则使用默认证书) + * @param client_cert 客户端证书文件名(双向认证需要,可为NULL) + * @param client_key 客户端密钥文件名(双向认证需要,可为NULL) + * @return N706A_HTTP_SUCCESS成功,其他失败 + * @note 使用AT+HTTPSCFG命令配置SSL参数 + */ +n706a_httpc_result_t n706a_http_client_config_https(int ssl_version, + int auth_mode, + const char *ca_cert, + const char *client_cert, + const char *client_key); + /** * @brief 设置自定义HTTP请求头 * @param client HTTP客户端结构指针 @@ -233,6 +349,35 @@ bool n706a_http_client_is_connected(n706a_http_client_t *client); */ int n706a_http_client_get_response_code(n706a_http_client_t *client); +/** + * @brief 设置HTTP流式接收回调(用于大文件下载等场景) + * @param client HTTP客户端结构指针 + * @param callback 流式接收回调函数(NULL表示禁用流式模式) + * @param user_data 回调函数的用户数据指针 + * @return N706A_HTTP_SUCCESS成功,其他失败 + * + * @note 启用流式模式后,响应体数据会通过回调函数立即传递给应用层 + * 应用层可以在回调中直接写入文件,无需等待所有数据接收完成 + * 流式模式与传统的累积模式互斥,启用流式模式后,responseBuf将不再累积数据 + */ +n706a_httpc_result_t n706a_http_client_set_stream_callback(n706a_http_client_t *client, + n706a_http_stream_callback_t callback, + void *user_data); + +/** + * @brief 流式接收HTTP响应数据(用于在发送请求后继续接收后续数据块) + * @param client HTTP客户端结构指针 + * @param client_data 响应数据结构指针 + * @param timeout_ms 超时时间(毫秒) + * @return N706A_HTTP_SUCCESS成功,N706A_HTTP_EAGAIN还有更多数据,其他失败 + * + * @note 此函数用于在流式模式下等待后续的URC数据到达 + * 数据会通过回调函数传递给应用层,此函数只用于等待和更新状态 + */ +n706a_httpc_result_t n706a_http_client_recv_response_stream(n706a_http_client_t *client, + n706a_http_client_data_t *client_data, + uint32_t timeout_ms); + /* ========== 辅助函数 ========== */ /** diff --git a/code/sdk/zhihui/n706a_net/include/n706a_net_api.h b/code/sdk/zhihui/n706a_net/include/n706a_net_api.h index 3ea7d89d2..f08249ecc 100644 --- a/code/sdk/zhihui/n706a_net/include/n706a_net_api.h +++ b/code/sdk/zhihui/n706a_net/include/n706a_net_api.h @@ -100,6 +100,18 @@ errcode_t n706a_get_network_signal_info(void); */ errcode_t n706a_enable_power_save_mode(void); +/** + * @brief 启用4G模块数据Ring功能 + * + * @return ERRCODE_SUCC 成功,其他值失败 + * + * 功能说明: + * - 执行 AT+NWURCCFG=0,1,200 命令 + * - 使能 data ring,持续 200ms + * - 用于在省电模式下通过 Ring 引脚唤醒 MCU + */ +errcode_t n706a_enable_data_ring(void); + /** * @brief 更新所有runtime变量值 * @@ -109,6 +121,19 @@ errcode_t n706a_enable_power_save_mode(void); */ errcode_t n706a_api_update_all_runtime_status(void); +/** + * @brief 通过AT+CCLK命令获取网络时间并同步到系统时间 + * + * @return ERRCODE_SUCC 成功,其他值失败 + * + * 功能说明: + * - 发送 AT+CCLK? 命令获取网络时间 + * - 解析响应,获取UTC时间和时区偏移 + * - 计算本地时间 = UTC时间 + 时区偏移 + * - 使用 settimeofday 设置系统时间为本地时间 + */ +errcode_t n706a_api_sync_time_from_network(void); + /** * @brief 动态设置N706A模块波特率(在所有模式下) * diff --git a/code/sdk/zhihui/n706a_net/include/n706a_tcp_client.h b/code/sdk/zhihui/n706a_net/include/n706a_tcp_client.h index a54eb9151..504320c57 100644 --- a/code/sdk/zhihui/n706a_net/include/n706a_tcp_client.h +++ b/code/sdk/zhihui/n706a_net/include/n706a_tcp_client.h @@ -6,7 +6,6 @@ * * SSL实现方式: * - N706A_SSL_USE_AT_COMMAND: 使用AT+SSLTCP*指令(默认,推荐) - * - N706A_SSL_USE_MBEDTLS: 使用mbedtls库(需要额外库支持) */ #ifndef PPP_N706A_TCP_CLIENT_H @@ -20,27 +19,12 @@ /** * 编译时选择SSL实现方式: * gcc -DN706A_SSL_USE_AT_COMMAND ... (使用AT指令,默认) - * gcc -DN706A_SSL_USE_MBEDTLS ... (使用mbedtls) */ -#if !defined(N706A_SSL_USE_AT_COMMAND) && !defined(N706A_SSL_USE_MBEDTLS) +#if !defined(N706A_SSL_USE_AT_COMMAND) /* 默认使用AT指令实现 */ #define N706A_SSL_USE_AT_COMMAND 1 #endif -#if defined(N706A_SSL_USE_AT_COMMAND) && defined(N706A_SSL_USE_MBEDTLS) - #error "Cannot define both N706A_SSL_USE_AT_COMMAND and N706A_SSL_USE_MBEDTLS" -#endif - -/* 如果使用mbedtls,包含相关头文件 */ -#ifdef N706A_SSL_USE_MBEDTLS - #include "mbedtls/net_sockets.h" - #include "mbedtls/ssl.h" - #include "mbedtls/entropy.h" - #include "mbedtls/ctr_drbg.h" - #include "mbedtls/x509.h" - #include "mbedtls/error.h" -#endif - #ifdef __cplusplus extern "C" { #endif @@ -98,27 +82,10 @@ typedef struct { void *urc_recv_mutex; /**< 缓冲区互斥锁 */ void *urc_data_available_sem; /**< 数据可用信号量(用于mbedtls recv等待) */ - /* SSL相关字段(根据编译宏不同而不同) */ -#ifdef N706A_SSL_USE_AT_COMMAND - /* AT指令模式:SSL由模组处理 */ + /* SSL相关字段(AT指令模式:SSL由模组处理) */ int ssl_version; /**< SSL版本(0-5) */ int auth_mode; /**< 认证模式(0-2) */ char ca_cert_file[64]; /**< CA证书文件名 */ -#endif - -#ifdef N706A_SSL_USE_MBEDTLS - /* mbedtls模式:需要SSL上下文 */ - int ssl_version; /**< SSL版本 */ - int auth_mode; /**< 认证模式 */ - mbedtls_ssl_context ssl; /**< SSL上下文 */ - mbedtls_ssl_config conf; /**< SSL配置 */ - mbedtls_entropy_context entropy; /**< 熵源 */ - mbedtls_ctr_drbg_context ctr_drbg; /**< 随机数生成器 */ - mbedtls_x509_crt cacert; /**< CA证书 */ - mbedtls_x509_crt clicert; /**< 客户端证书(可选) */ - mbedtls_pk_context pkey; /**< 客户端私钥(可选) */ - bool ssl_initialized; /**< SSL是否已初始化 */ -#endif } n706a_tcp_socket_t; /** @@ -162,15 +129,9 @@ n706a_tcp_result_t n706a_tcp_connect(n706a_tcp_socket_t *sock, const char *host, /** * @brief 配置SSL参数(在SSL连接前调用) * @param sock TCP socket指针 - * @param ssl_version SSL版本 - * AT指令模式:0-5 (0=SSL3.0, 1=TLS1.0, 2=TLS1.1, 3=TLS1.2, 4=TLS1.0/1.1/1.2, 5=TLS1.3) - * mbedtls模式:使用MBEDTLS_SSL_MINOR_VERSION_*常量 - * @param auth_mode 认证模式 - * AT指令模式:0=不认证, 1=服务器认证, 2=双向认证 - * mbedtls模式:MBEDTLS_SSL_VERIFY_NONE/OPTIONAL/REQUIRED - * @param ca_cert CA证书 - * AT指令模式:证书文件名(如"ca.crt") - * mbedtls模式:证书内容(PEM格式字符串) + * @param ssl_version SSL版本:0-5 (0=SSL3.0, 1=TLS1.0, 2=TLS1.1, 3=TLS1.2, 4=TLS1.0/1.1/1.2, 5=TLS1.3) + * @param auth_mode 认证模式:0=不认证, 1=服务器认证, 2=双向认证 + * @param ca_cert CA证书文件名(如"ca.crt") * @return N706A_TCP_SUCCESS成功,其他值表示失败 */ n706a_tcp_result_t n706a_tcp_config_ssl(n706a_tcp_socket_t *sock, @@ -178,19 +139,6 @@ n706a_tcp_result_t n706a_tcp_config_ssl(n706a_tcp_socket_t *sock, int auth_mode, const char *ca_cert); -#ifdef N706A_SSL_USE_MBEDTLS -/** - * @brief 配置客户端证书和私钥(mbedtls模式,双向认证) - * @param sock TCP socket指针 - * @param client_cert 客户端证书(PEM格式) - * @param client_key 客户端私钥(PEM格式) - * @return N706A_TCP_SUCCESS成功,其他值表示失败 - */ -n706a_tcp_result_t n706a_tcp_config_ssl_client_cert(n706a_tcp_socket_t *sock, - const char *client_cert, - const char *client_key); -#endif - /** * @brief 发送TCP数据 * @param sock TCP socket指针 @@ -235,6 +183,15 @@ bool n706a_tcp_is_connected(const n706a_tcp_socket_t *sock); */ n706a_tcp_state_t n706a_tcp_get_state(const n706a_tcp_socket_t *sock); +/** + * @brief 查询socket在模块端的状态(使用AT+IPSTATUS) + * @param socket_id socket ID (0-5) + * @param is_connected 输出:是否已连接(true=CONNECT, false=DISCONNECT) + * @param is_ssl 输出:是否是SSL连接(true=SSL, false=TCP,仅在已连接时有效) + * @return N706A_TCP_SUCCESS=查询成功,其他值=查询失败 + */ +n706a_tcp_result_t n706a_tcp_query_socket_status(int socket_id, bool *is_connected, bool *is_ssl); + /** * @brief 初始化TCP socket的URC接收缓存 * @param sock TCP socket指针 @@ -273,6 +230,31 @@ void n706a_tcp_on_urc_received(int socket_id, const uint8_t *data, size_t length */ void n706a_tcp_on_tcpsetup_urc(int socket_id, bool success); +/** + * @brief +TCPSEND URC回调(由n706a_at_handler调用) + * @param socket_id Socket ID + * @param sent_bytes 实际发送的字节数 + * + * 注意:此函数由n706a_at_handler.c通过URC回调调用,不是应用层API + */ +void n706a_tcp_on_tcpsend_urc(int socket_id, size_t sent_bytes); + +/** + * @brief +TCPCLOSE URC回调(由n706a_at_handler调用) + * @param socket_id Socket ID (0-5) + * + * 注意:此函数由n706a_at_handler.c通过URC回调调用,不是应用层API + * 当服务器主动关闭连接时,会调用此函数更新socket状态 + */ +void n706a_tcp_on_tcpclose_urc(int socket_id); + +/** + * @brief 提前初始化+TCPSEND URC等待机制(供n706a_net_init调用) + * + * 在系统初始化时提前初始化信号量和互斥锁,避免在发送数据时初始化失败 + */ +void n706a_tcp_preinit_tcpsend_urc_wait(void); + #ifdef __cplusplus } #endif diff --git a/code/sdk/zhihui/n706a_net/include/n706a_websocket.h b/code/sdk/zhihui/n706a_net/include/n706a_websocket.h index a710a841b..57999d5d8 100644 --- a/code/sdk/zhihui/n706a_net/include/n706a_websocket.h +++ b/code/sdk/zhihui/n706a_net/include/n706a_websocket.h @@ -21,12 +21,15 @@ #include #include "n706a_tcp_client.h" +/* 包含 WebSocket 解析器头文件(定义 ws_parser_t 类型) */ +#include "../../websocket/include/websocket_parser_wrapper.h" + #ifdef __cplusplus extern "C" { #endif /* WebSocket最大客户端数量 */ -#define N706A_WS_MAX_CLIENTS 6 +#define N706A_WS_MAX_CLIENTS 3 /* WebSocket URL最大长度 */ #define N706A_WS_MAX_URL_LEN 512 @@ -135,7 +138,7 @@ typedef struct n706a_websocket_client { char url[N706A_WS_MAX_URL_LEN]; /**< 完整URL */ char host[128]; /**< 主机名 */ uint16_t port; /**< 端口号 */ - char path[256]; /**< 路径 */ + char path[N706A_WS_MAX_URL_LEN]; /**< 路径 */ /* WebSocket协议 */ char sec_key[32]; /**< Sec-WebSocket-Key */ @@ -156,6 +159,10 @@ typedef struct n706a_websocket_client { bool running; /**< 接收线程运行标志 */ void *recv_thread; /**< 接收线程句柄 */ + /* WebSocket 帧解析器 */ + ws_parser_t *parser; /**< WebSocket 帧解析器实例 */ + n706a_ws_opcode_t fragment_opcode; /**< 分片消息的初始 opcode(用于 CONTINUATION 帧) */ + } n706a_websocket_client_t; /** @@ -256,6 +263,13 @@ int n706a_websocket_close_with_reason(int client_id, */ n706a_ws_state_t n706a_websocket_get_state(int client_id); +/** + * @brief 获取WebSocket的底层TCP socket ID + * @param client_id 客户端ID + * @return socket ID (0-5),失败返回-1 + */ +int n706a_websocket_get_socket_id(int client_id); + /** * @brief 检查WebSocket是否已连接 * @param client_id 客户端ID diff --git a/code/sdk/zhihui/n706a_net/include/ppp_config.h b/code/sdk/zhihui/n706a_net/include/ppp_config.h index a45c70c04..9266e41d0 100644 --- a/code/sdk/zhihui/n706a_net/include/ppp_config.h +++ b/code/sdk/zhihui/n706a_net/include/ppp_config.h @@ -28,11 +28,11 @@ extern "C" { #define SIO_UART_IRQ_NUM UART_0_IRQN // UART IRQ引脚 #endif -#define PPP_UART_BAUDRATE 1000000 +#define PPP_UART_BAUDRATE 921600 /* 支持的波特率列表(自适应模式) */ -#define PPP_BAUDRATE_AUTO_LIST {115200,921600,1000000,2000000} -#define PPP_BAUDRATE_AUTO_COUNT 4 +#define PPP_BAUDRATE_AUTO_LIST {9600,115200,750000,921600,1000000} +#define PPP_BAUDRATE_AUTO_COUNT 5 /* 支持的固定波特率列表 */ #define PPP_BAUDRATE_FIXED_LIST {4800, 9600, 57600, 115200, 230400, 460800, 750000, 921600, 1000000, 2000000, 4000000} @@ -83,20 +83,26 @@ extern "C" { /* 调试配置 */ #define PPP_DEBUG_ENABLE 1 // 启用调试输出 -#define PPP_DEBUG_LEVEL PPP_DEBUG_LEVEL_DEBUG // 默认只显示警告和错误 (1=ERROR, 2=WARN, 3=INFO, 4=DEBUG) -// #define PPP_DEBUG_LEVEL PPP_DEBUG_LEVEL_INFO // 默认只显示警告和错误 (1=ERROR, 2=WARN, 3=INFO, 4=DEBUG) -// #define PPP_DEBUG_LEVEL PPP_DEBUG_LEVEL_WARN // 默认只显示警告和错误 (1=ERROR, 2=WARN, 3=INFO, 4=DEBUG) +// #define PPP_DEBUG_LEVEL PPP_DEBUG_LEVEL_INFO // 默认显示信息、警告和错误 (1=ERROR, 2=WARN, 3=INFO, 4=DEBUG) +#define PPP_DEBUG_LEVEL PPP_DEBUG_LEVEL_DEBUG // 显示所有调试信息 +// #define PPP_DEBUG_LEVEL PPP_DEBUG_LEVEL_WARN // 只显示警告和错误 + +/* 详细调试日志配置(HEX打印、详细状态信息等) */ +#define PPP_DEBUG_VERBOSE_ENABLE 1 // 默认关闭详细调试日志(HEX打印、详细状态等) + // 设置为1以启用详细调试日志 /* * 调试级别说明: * PPP_DEBUG_LEVEL_ERROR (1) - 只显示错误信息 * PPP_DEBUG_LEVEL_WARN (2) - 显示警告和错误信息 - * PPP_DEBUG_LEVEL_INFO (3) - 显示信息、警告和错误 + * PPP_DEBUG_LEVEL_INFO (3) - 显示信息、警告和错误(默认) * PPP_DEBUG_LEVEL_DEBUG (4) - 显示所有调试信息 * * 关键连接状态信息(PPP_DEBUG_CRITICAL)始终显示,不受此级别控制 + * 详细调试日志(PPP_DEBUG_VERBOSE)需要单独启用PPP_DEBUG_VERBOSE_ENABLE=1 * * 如需完整调试信息,可将上面的PPP_DEBUG_LEVEL改为PPP_DEBUG_LEVEL_DEBUG + * 如需详细HEX打印等,可将PPP_DEBUG_VERBOSE_ENABLE改为1 */ /* PPP状态定义 */ diff --git a/code/sdk/zhihui/n706a_net/include/ppp_debug.h b/code/sdk/zhihui/n706a_net/include/ppp_debug.h index ecf7b4ab4..9cceabe61 100644 --- a/code/sdk/zhihui/n706a_net/include/ppp_debug.h +++ b/code/sdk/zhihui/n706a_net/include/ppp_debug.h @@ -70,6 +70,22 @@ extern int ppp_debug_get_level(void); PRINT("[PPP] " fmt "\r\n", ##__VA_ARGS__); \ } while(0) +/* 详细调试日志宏 - 用于HEX打印、详细状态信息等,默认关闭 */ +#ifndef PPP_DEBUG_VERBOSE_ENABLE +#define PPP_DEBUG_VERBOSE_ENABLE 0 /* 默认关闭 */ +#endif + +#if PPP_DEBUG_VERBOSE_ENABLE +#define PPP_DEBUG_VERBOSE(fmt, ...) \ + do { \ + if (ppp_debug_get_level() >= PPP_DEBUG_LEVEL_DEBUG) { \ + PRINT("[PPP VERBOSE] " fmt "\r\n", ##__VA_ARGS__); \ + } \ + } while(0) +#else +#define PPP_DEBUG_VERBOSE(fmt, ...) /* 默认关闭详细调试日志 */ +#endif + #else #define PPP_DEBUG_ERROR(fmt, ...) @@ -79,6 +95,7 @@ extern int ppp_debug_get_level(void); #define PPP_DEBUG_HEXDUMP(data, len) #define PPP_DEBUG_PRINT(fmt, ...) #define PPP_DEBUG_CRITICAL(fmt, ...) PRINT("[PPP] " fmt "\r\n", ##__VA_ARGS__) +#define PPP_DEBUG_VERBOSE(fmt, ...) /* 默认关闭详细调试日志 */ #endif /* PPP_DEBUG_ENABLE */ diff --git a/code/sdk/zhihui/n706a_net/src/n706a_at_handler.c b/code/sdk/zhihui/n706a_net/src/n706a_at_handler.c index 27fd4c8cf..9c64e62aa 100644 --- a/code/sdk/zhihui/n706a_net/src/n706a_at_handler.c +++ b/code/sdk/zhihui/n706a_net/src/n706a_at_handler.c @@ -3,15 +3,17 @@ #include "ppp_types.h" #include "ppp_debug.h" #include "ppp_serial.h" -#include "uart.h" +#include "uart.h" // 用于波特率设置等UART配置操作 #include "schedule/osal_task.h" #include "lock/osal_mutex.h" #include "semaphore/osal_semaphore.h" +#include "time/osal_timer.h" // For osal_get_jiffies and osal_jiffies_to_msecs #include "soc_osal.h" #include "securec.h" #include #include #include // For malloc and free +#include // For UINT32_MAX /* ========== AT响应异步缓冲区(由ppp_interrupt.c写入) ========== */ #define N706A_AT_RESPONSE_RING_BUFFER_SIZE 4096 // 4KB环形缓冲区 @@ -22,6 +24,13 @@ static volatile uint32_t g_at_response_tail = 0; static osal_mutex g_at_response_mutex; static osal_semaphore g_at_response_sem; +/* ========== AT响应累积缓冲区(用于存储不完整的响应) ========== */ +#define N706A_AT_ACCUMULATE_BUFFER_SIZE 8192 // 8KB静态缓冲区(避免频繁malloc/free) + +static uint8_t g_at_accumulate_buffer[N706A_AT_ACCUMULATE_BUFFER_SIZE]; +static uint32_t g_at_accumulate_length = 0; +static osal_mutex g_at_accumulate_mutex; + /* AT命令处理上下文 */ static struct { bool initialized; @@ -41,6 +50,27 @@ static const uint32_t g_fixed_baudrates[] = PPP_BAUDRATE_FIXED_LIST; static uint8_t *g_at_response_buffer = NULL; static uint32_t g_at_buffer_size = 1024; +/* ========== AT命令解析临时缓冲区(动态分配,在初始化时分配,反初始化时释放) ========== */ +/* + * 内存占用说明: + * - 静态变量(static):在程序启动时分配,放在BSS/DATA段,程序运行期间一直占用,无法释放 + * - 动态分配(malloc):在初始化时分配,在反初始化时释放,只在需要时占用内存 + * + * 对于嵌入式MCU,使用动态分配更合适: + * - 只在AT模块初始化时占用内存 + * - 可以在不需要时释放(通过deinit) + * - 避免程序启动时就占用约1.9KB的RAM + * + * 缓冲区大小: + * - g_at_cmd_buffer: 260字节 (AT命令+ \r\n) + * - g_at_printable_buffer: 1124字节 (调试打印) + * - g_at_command_response_buffer: 512字节 (命令响应提取) + * - 总计:约1896字节 (1.85KB) + */ +static char *g_at_cmd_buffer = NULL; // AT命令+ \r\n +static char *g_at_printable_buffer = NULL; // 调试打印缓冲区 +static char *g_at_command_response_buffer = NULL; // 命令响应部分提取缓冲区 + /* TCP URC回调 */ static n706a_tcp_urc_callback_t g_tcp_urc_callback = NULL; @@ -56,8 +86,90 @@ static n706a_tcpsend_urc_callback_t g_tcpsend_urc_callback = NULL; /* 网络状态URC回调 */ static n706a_network_status_urc_callback_t g_network_status_urc_callback = NULL; +/* TCP接收模式标志(HEX模式=1,1,ASCII模式=1,0) */ +static bool g_tcp_recv_hex_mode = true; // 默认使用HEX模式(因为初始化时设置了AT+RECVMODE=1,1) + +/* 累积缓冲区超时清除机制 */ +static unsigned long long g_at_accumulate_last_update_time = 0; // 累积缓冲区最后更新时间(tick数) +#define N706A_AT_ACCUMULATE_TIMEOUT_MS 5000 // 5秒超时,如果累积缓冲区中的数据超过5秒还没被识别,则清除 + +/* 流式解析器开关(新方案) */ +#define USE_STREAM_PARSER 1 // 1=使用流式解析器(新方案),0=使用旧方案 + +/* 丢弃数据统计 */ +static uint32_t g_dropped_data_count = 0; // 丢弃数据块的次数 +static uint32_t g_dropped_data_bytes = 0; // 丢弃数据的总字节数 + +/* ========== 流式解析器状态机(原始设计) ========== */ +typedef enum { + AT_PARSER_STATE_IDLE, // 空闲状态:扫描数据,寻找响应开始 + AT_PARSER_STATE_AT_RESPONSE, // AT响应处理中 + AT_PARSER_STATE_URC_HEADER, // URC头部处理中(解析参数) + AT_PARSER_STATE_URC_DATA, // URC数据处理中 + AT_PARSER_STATE_COMMAND_ECHO // 命令回显处理中 +} at_parser_state_t; + +typedef enum { + URC_TYPE_NONE = 0, + URC_TYPE_TCPRECV, + URC_TYPE_SSLTCPRECV, + URC_TYPE_HTTPSRECV, + URC_TYPE_HTTPRECV, + URC_TYPE_TCPSETUP, + URC_TYPE_SSLTCPSETUP, + URC_TYPE_TCPCLOSE, + URC_TYPE_SSLTCPCLOSE, + URC_TYPE_TCPSEND, + URC_TYPE_SSLTCPEND, + URC_TYPE_CREG, + URC_TYPE_CGREG, + URC_TYPE_CGATT +} urc_type_t; + +/* 解析器上下文(原始设计 + 方案2修复) */ +typedef struct { + at_parser_state_t state; // 当前状态 + + /* 当前处理的响应信息 */ + uint32_t response_start_pos; // 响应开始位置(相对于当前数据块) + uint32_t response_expected_len; // 期望的响应长度(用于URC数据) + uint32_t response_received_len; // 已接收的响应长度 + + /* URC相关信息 */ + urc_type_t urc_type; // URC类型 + bool is_https; // 是否是HTTPS(用于HTTPRECV) + int tcp_socket_id; // TCP socket ID(用于TCPRECV) + + /* 跨数据包状态保持 */ + uint8_t partial_buffer[512]; // 部分数据缓冲区(跨包使用) + uint32_t partial_buffer_len; // 部分数据长度 + + /* URC处理字段 */ + size_t remaining_data_len; // 剩余数据长度(用于URC数据) + size_t received_data_len; // 已接收数据长度 + char urc_header_buf[256]; // URC头部缓冲区(用于解析参数) + uint32_t urc_header_len; // URC头部长度 + + /* 方案2:状态转换标记 */ + bool state_transition_pending; // 标记是否有待处理的状态转换(用于修复consumed=0问题) + + /* URC匹配状态(用于跨数据块匹配) */ + uint32_t urc_match_pos; // URC模式匹配位置 + const char *urc_pattern; // 当前匹配的URC模式 + uint32_t urc_pattern_len; // URC模式长度 + + /* 统计信息 */ + uint32_t total_bytes_processed; // 总处理字节数 + uint32_t urc_count; // URC计数 + uint32_t at_response_count; // AT响应计数 +} at_parser_context_t; + +static at_parser_context_t g_parser_ctx = {0}; + /* AT命令响应解析 */ +static const char *ppp_at_skip_echo(const char *response, size_t response_len, char *cleaned_response, size_t cleaned_size); static bool ppp_at_parse_response(const char *response, const char *expected); +static bool ppp_at_parse_response_with_len(const char *response, size_t response_len, const char *expected); static ppp_error_t ppp_at_wait_response(ppp_at_response_t *response, uint32_t timeout); static ppp_error_t ppp_at_set_baudrate(uint32_t baudrate); static ppp_error_t ppp_at_send_command(const char *command, const char *expected_response, @@ -66,17 +178,37 @@ static ppp_error_t ppp_at_send_command(const char *command, const char *expected /* URC解析函数 */ static bool n706a_parse_tcprecv_urc(const char *urc_data, size_t urc_len, int *socket_id, size_t *data_len, const uint8_t **data); +/* 非static版本,供其他模块使用 */ +bool n706a_parse_tcprecv_urc_public(const char *urc_data, size_t urc_len, + int *socket_id, size_t *data_len, const uint8_t **data); static bool n706a_parse_tcpsetup_urc(const char *urc_data, size_t urc_len, int *socket_id, bool *success); static bool n706a_parse_tcpclose_urc(const char *urc_data, size_t urc_len, int *socket_id); static bool n706a_parse_tcpsend_urc(const char *urc_data, size_t urc_len, int *socket_id, size_t *sent_bytes); +static bool n706a_parse_httprecv_urc(const char *urc_data, size_t urc_len, + size_t *declared_len, size_t *actual_len, const uint8_t **data); static bool n706a_hex_char_to_value(char c, uint8_t *value); static size_t n706a_binary_to_hex_string(const uint8_t *binary_data, size_t binary_len, char *hex_str, size_t hex_buf_size); static size_t n706a_hex_string_to_binary(const char *hex_str, size_t hex_len, uint8_t *binary_buf, size_t binary_buf_size); +/* 改进的AT响应处理函数 */ +static bool n706a_extract_complete_response(uint32_t *response_start, uint32_t *response_len); +static bool n706a_classify_and_dispatch_urc_improved(const uint8_t *response, uint32_t response_len); +static void n706a_remove_processed_data(uint32_t remove_len); + +/* 流式解析器函数(新方案) */ +static void n706a_stream_parser_init(void); +static void n706a_stream_parser_reset(void); +static void n706a_stream_parser_process_data(const uint8_t *data, uint32_t length); +static bool n706a_stream_parser_match_urc_identifier(uint8_t byte, urc_type_t *type, const char **pattern, uint32_t *pattern_len); +static void n706a_stream_parser_process_urc_header(const uint8_t *data, uint32_t length, uint32_t *consumed); +static void n706a_stream_parser_process_urc_data(const uint8_t *data, uint32_t length, uint32_t *consumed); +static void n706a_stream_parser_process_at_response(const uint8_t *data, uint32_t length, uint32_t *consumed); +static void n706a_stream_parser_dispatch_urc(void); + /** * @brief AT响应分类处理和分发 * @param data 数据指针 @@ -103,20 +235,84 @@ static bool n706a_classify_and_dispatch_urc(const uint8_t *data, uint32_t length const char *data_str = (const char *)data; - PPP_DEBUG_DEBUG("[URC分类] Checking data block: length=%u, first 64 bytes: [%.*s]", - length, (length < 64) ? (int)length : 64, data_str); + /* 打印完整的HEX数据用于调试(如果数据较短) */ + /* 优化:使用静态缓冲区,避免在栈上分配大数组 */ + if (length <= 128) { + static char hex_str[257] = {0}; // 静态缓冲区,避免栈分配 + /* 只转换前64字节以节省时间 */ + uint32_t hex_len = (length < 64) ? length : 64; + for (uint32_t i = 0; i < hex_len; i++) { + uint8_t byte = data[i]; + hex_str[i * 2] = "0123456789ABCDEF"[byte >> 4]; + hex_str[i * 2 + 1] = "0123456789ABCDEF"[byte & 0x0F]; + } + hex_str[hex_len * 2] = '\0'; + PPP_DEBUG_DEBUG("[URC分类] Checking data block: length=%u, HEX=[%s], ASCII=[%.*s]", + length, hex_str, (length < 64) ? (int)length : 64, data_str); + } else { + PPP_DEBUG_DEBUG("[URC分类] Checking data block: length=%u, first 64 bytes: [%.*s]", + length, (length < 64) ? (int)length : 64, data_str); + } /* ========== 类别1:TCP数据接收URC(最高优先级,因为包含数据) ========== */ + // 检查数据中是否包含+TCPRECV或+SSLTCPRECV(可能在数据中间,不一定是开头) if (length >= 10 && g_tcp_urc_callback) { - if (memcmp(data, "+TCPRECV: ", 10) == 0 || memcmp(data, "+SSLTCPRECV: ", 13) == 0) { + const char *tcprecv_pos = strstr(data_str, "+TCPRECV: "); + const char *ssltcprecv_pos = strstr(data_str, "+SSLTCPRECV: "); + + if (tcprecv_pos || ssltcprecv_pos) { + const char *urc_start = tcprecv_pos ? tcprecv_pos : ssltcprecv_pos; + // 查找URC结束位置(\r\n或\n或数据结束) + const char *urc_end = strstr(urc_start, "\r\n"); + if (!urc_end) { + urc_end = strstr(urc_start, "\n"); + } + if (!urc_end) { + urc_end = data_str + length; + } + + // 计算URC长度(包括数据部分,需要找到完整的数据) + // +TCPRECV格式:+TCPRECV: ,,\r\n + // 需要解析data_len来确定完整URC长度 + // 注意:urc_end可能只是URC头部的结束(第一个\r\n),实际URC包含数据部分 int socket_id; size_t data_len; const uint8_t *tcp_data; - if (n706a_parse_tcprecv_urc((const char *)data, length, &socket_id, &data_len, &tcp_data)) { + // 先尝试解析URC头部(至少包含socket_id和data_len) + // 使用整个数据块长度来解析,让解析函数自己处理数据长度验证 + // 但需要确保至少能解析到data_len + size_t parse_len = length - (urc_start - data_str); + if (n706a_parse_tcprecv_urc(urc_start, parse_len, &socket_id, &data_len, &tcp_data)) { + // 验证解析结果:tcp_data应该指向数据开始位置 + // data_len是二进制数据长度,在HEX模式下,实际数据部分(HEX字符串)长度 = data_len × 2 + // 完整URC长度 = (tcp_data - urc_start) + actual_data_len + 2(\r\n) + const char *data_start = (const char *)tcp_data; + // 在HEX模式下,实际数据长度是 data_len * 2(HEX字符串长度) + // 在ASCII模式下,实际数据长度是 data_len(ASCII字符串长度) + size_t actual_data_len = g_tcp_recv_hex_mode ? (data_len * 2) : data_len; + const char *data_end = data_start + actual_data_len; + const char *full_urc_end = strstr(data_end, "\r\n"); + if (!full_urc_end) { + full_urc_end = strstr(data_end, "\n"); + } + if (!full_urc_end) { + // 如果没有找到\r\n,检查是否到达数据块末尾 + full_urc_end = data_str + length; + } + + // 验证完整URC长度是否合理 + size_t full_urc_len = full_urc_end - urc_start; + if (full_urc_len > parse_len) { + // 如果完整URC长度超过当前数据块,说明数据不完整,使用当前数据块长度 + full_urc_len = parse_len; + } + + // 使用完整URC重新解析(确保数据完整) + if (n706a_parse_tcprecv_urc(urc_start, full_urc_len, &socket_id, &data_len, &tcp_data)) { // 注意:如果使用HEX模式,tcp_data指向的是HEX字符串,需要转换为二进制 // 这里假设当前使用的是HEX模式(AT+RECVMODE=1,1) - // HEX字符串长度是实际数据长度的2倍 + // data_len是二进制数据长度,HEX字符串长度 = data_len × 2 // 使用双buffer ping-pong机制避免并发冲突 // 注意:解码后的数据会立即拷贝到URC buffer,所以hex buffer可以立即重用 @@ -127,43 +323,98 @@ static bool n706a_classify_and_dispatch_urc(const uint8_t *data, uint32_t length size_t binary_data_len = data_len; const uint8_t *binary_data = tcp_data; - // 检查是否是HEX格式(HEX字符串长度应该是数据长度的2倍) - // 或者可以通过检查是否所有字符都是0-9A-F来判断 + // 如果设置了HEX模式(AT+RECVMODE=1,1),强制使用HEX解码 + // 否则,检查是否是HEX格式(通过检查所有字符是否都是0-9A-F来判断) bool is_hex_format = false; if (data_len > 0) { - // 简单检查:如果数据长度是偶数,且所有字符都是十六进制字符,可能是HEX格式 - size_t hex_str_len = 0; - const char *p = (const char *)tcp_data; - while (hex_str_len < data_len && - ((*p >= '0' && *p <= '9') || (*p >= 'A' && *p <= 'F') || (*p >= 'a' && *p <= 'f'))) { - p++; - hex_str_len++; - } - - // 如果所有字符都是HEX字符,且长度是2的倍数,认为是HEX格式 - if (hex_str_len == data_len && data_len % 2 == 0) { - is_hex_format = true; + // 如果设置了HEX模式,强制尝试HEX解码 + if (g_tcp_recv_hex_mode) { + // HEX模式下,data_len是二进制数据长度,HEX字符串长度 = data_len × 2 + size_t hex_str_len = data_len * 2; + + // 检查实际HEX字符串长度(可能被截断) + size_t actual_hex_str_len = 0; + const char *p = (const char *)tcp_data; + while (actual_hex_str_len < hex_str_len && + ((*p >= '0' && *p <= '9') || (*p >= 'A' && *p <= 'F') || (*p >= 'a' && *p <= 'f'))) { + p++; + actual_hex_str_len++; + } - // 选择非活跃的buffer进行解码(ping-pong机制) - // 由于解码后立即拷贝到URC buffer,active buffer可以立即切换 - int buffer_idx = hex_decode_buffer_active; - uint8_t *decode_buffer = (buffer_idx == 0) ? hex_decode_buffer_0 : hex_decode_buffer_1; + // 如果实际HEX字符串长度足够(至少是期望长度,或者接近期望长度),尝试HEX解码 + if (actual_hex_str_len >= hex_str_len || (actual_hex_str_len > 0 && actual_hex_str_len >= hex_str_len - 1)) { + // 使用实际可用的HEX字符串长度(如果是奇数,截断最后一个字符) + size_t hex_len_to_decode = (actual_hex_str_len % 2 == 0) ? actual_hex_str_len : (actual_hex_str_len - 1); + + if (hex_len_to_decode > 0 && hex_len_to_decode % 2 == 0) { + is_hex_format = true; + + // 选择非活跃的buffer进行解码(ping-pong机制) + // 由于解码后立即拷贝到URC buffer,active buffer可以立即切换 + int buffer_idx = hex_decode_buffer_active; + uint8_t *decode_buffer = (buffer_idx == 0) ? hex_decode_buffer_0 : hex_decode_buffer_1; + + // 将HEX字符串转换为二进制 + binary_data_len = n706a_hex_string_to_binary((const char *)tcp_data, hex_len_to_decode, + decode_buffer, sizeof(hex_decode_buffer_0)); + if (binary_data_len > 0) { + binary_data = decode_buffer; + + // 切换active buffer(虽然不必须,但保持ping-pong逻辑) + // 注意:由于数据会立即拷贝到URC buffer,这里切换是安全的 + hex_decode_buffer_active = 1 - hex_decode_buffer_active; + + if (hex_len_to_decode < hex_str_len) { + PPP_DEBUG_WARN("HEX decoded: binary_len=%u, expected_hex_len=%u, actual_hex_len=%u (truncated) -> decoded=%u bytes (buffer=%d, forced by HEX mode)", + (unsigned int)data_len, (unsigned int)hex_str_len, (unsigned int)hex_len_to_decode, + (unsigned int)binary_data_len, buffer_idx); + } else { + PPP_DEBUG_INFO("HEX decoded: binary_len=%u, hex_len=%u -> decoded=%u bytes (buffer=%d, forced by HEX mode)", + (unsigned int)data_len, (unsigned int)hex_str_len, (unsigned int)binary_data_len, buffer_idx); + } + } else { + PPP_DEBUG_ERROR("Failed to decode HEX string (HEX mode enabled)"); + is_hex_format = false; + } + } else { + PPP_DEBUG_WARN("HEX mode enabled but HEX data length is invalid: expected_hex_len=%u, actual_hex_len=%u", + (unsigned int)hex_str_len, (unsigned int)actual_hex_str_len); + } + } else { + PPP_DEBUG_WARN("HEX mode enabled but HEX data too short: expected_hex_len=%u, actual_hex_len=%u", + (unsigned int)hex_str_len, (unsigned int)actual_hex_str_len); + } + } else { + // ASCII模式:检查是否是HEX格式(自动检测) + size_t hex_str_len = 0; + const char *p = (const char *)tcp_data; + while (hex_str_len < data_len && + ((*p >= '0' && *p <= '9') || (*p >= 'A' && *p <= 'F') || (*p >= 'a' && *p <= 'f'))) { + p++; + hex_str_len++; + } - // 将HEX字符串转换为二进制 - binary_data_len = n706a_hex_string_to_binary((const char *)tcp_data, data_len, - decode_buffer, sizeof(hex_decode_buffer_0)); - if (binary_data_len > 0) { - binary_data = decode_buffer; + // 如果所有字符都是HEX字符,且长度是2的倍数,认为是HEX格式 + if (hex_str_len == data_len && data_len % 2 == 0) { + is_hex_format = true; - // 切换active buffer(虽然不必须,但保持ping-pong逻辑) - // 注意:由于数据会立即拷贝到URC buffer,这里切换是安全的 - hex_decode_buffer_active = 1 - hex_decode_buffer_active; + // 选择非活跃的buffer进行解码(ping-pong机制) + int buffer_idx = hex_decode_buffer_active; + uint8_t *decode_buffer = (buffer_idx == 0) ? hex_decode_buffer_0 : hex_decode_buffer_1; - PPP_DEBUG_DEBUG("HEX decoded: hex_len=%u -> binary_len=%u (buffer=%d)", - (unsigned int)data_len, (unsigned int)binary_data_len, buffer_idx); - } else { - PPP_DEBUG_ERROR("Failed to decode HEX string"); - is_hex_format = false; + // 将HEX字符串转换为二进制 + binary_data_len = n706a_hex_string_to_binary((const char *)tcp_data, data_len, + decode_buffer, sizeof(hex_decode_buffer_0)); + if (binary_data_len > 0) { + binary_data = decode_buffer; + hex_decode_buffer_active = 1 - hex_decode_buffer_active; + + PPP_DEBUG_DEBUG("HEX decoded: hex_len=%u -> binary_len=%u (buffer=%d, auto-detected)", + (unsigned int)data_len, (unsigned int)binary_data_len, buffer_idx); + } else { + PPP_DEBUG_ERROR("Failed to decode HEX string"); + is_hex_format = false; + } } } } @@ -176,13 +427,19 @@ static bool n706a_classify_and_dispatch_urc(const uint8_t *data, uint32_t length // 立即回调应用层(在中断上下文或处理线程中) g_tcp_urc_callback(socket_id, binary_data, binary_data_len); - // URC已处理,不写入AT响应缓冲区 - return true; + // URC已处理,如果是独立URC则不写入缓冲区,否则继续(混合响应) + if (urc_start == data_str && full_urc_end >= data_str + length - 2) { + return true; // 独立URC,不写入缓冲区 + } + // 混合响应:URC已处理,但继续处理其他部分(如OK等) } else { PPP_DEBUG_WARN("Invalid TCP URC data length"); } + } else { + PPP_DEBUG_WARN("Failed to parse +TCPRECV URC (second parse)"); + } } else { - PPP_DEBUG_WARN("Failed to parse +TCPRECV URC"); + PPP_DEBUG_WARN("Failed to parse +TCPRECV URC (first parse)"); } } } @@ -261,35 +518,138 @@ static bool n706a_classify_and_dispatch_urc(const uint8_t *data, uint32_t length } /* ========== 类别3:TCP发送结果URC ========== */ - if (length >= 10 && g_tcpsend_urc_callback) { + if (length >= 10) { const char *tcpsend_pos = strstr(data_str, "+TCPSEND: "); const char *ssltcpend_pos = strstr(data_str, "+SSLTCPEND: "); if (tcpsend_pos || ssltcpend_pos) { const char *urc_start = tcpsend_pos ? tcpsend_pos : ssltcpend_pos; + + // 关键改进:检查URC前面是否有命令回显(AT+) + // 如果URC前面有命令回显,说明这是命令响应的一部分,不是纯异步URC + // 纯异步URC应该以 \r\n 开头,或者直接以 + 开头(没有AT+回显) + bool is_pure_async_urc = true; + if (urc_start > data_str) { + // 检查URC前面是否有 "AT+" 字符串(命令回显) + const char *before_urc = urc_start - 1; + // 向前查找,看是否有 "AT+" 在同一个响应块中 + while (before_urc >= data_str && (*before_urc == ' ' || *before_urc == '\r' || *before_urc == '\n')) { + before_urc--; + } + // 检查是否以 "AT+" 结尾(说明前面有命令回显) + if (before_urc >= data_str + 2) { + if (before_urc[-2] == 'A' && before_urc[-1] == 'T' && *before_urc == '+') { + // 找到命令回显,这不是纯异步URC + is_pure_async_urc = false; + PPP_DEBUG_DEBUG("[URC分类] +TCPSEND URC前面有命令回显,可能是命令响应的一部分"); + } + } + } + + PPP_DEBUG_INFO("[URC分类] +TCPSEND URC candidate found at offset %u (pure_async=%d)", + (unsigned int)(urc_start - data_str), is_pure_async_urc); + + // 查找URC结束符:支持 \r\n, \r\r, \n const char *urc_end = strstr(urc_start, "\r\n"); if (!urc_end) { - urc_end = strstr(urc_start, "\n"); + // 检查 \r\r 格式(异常格式,但需要容错处理) + const char *cr_cr = strstr(urc_start, "\r\r"); + if (cr_cr) { + urc_end = cr_cr + 1; // 指向第二个 \r + PPP_DEBUG_WARN("[URC分类] +TCPSEND URC以 \\r\\r 结尾(异常格式,可能是模组固件问题)"); + } else { + urc_end = strstr(urc_start, "\n"); + } } if (!urc_end) { urc_end = data_str + length; } size_t urc_len = urc_end - urc_start; + PPP_DEBUG_INFO("[URC分类] +TCPSEND URC candidate: urc_len=%u, content=[%.*s]", + (unsigned int)urc_len, (int)(urc_len < 64 ? urc_len : 64), urc_start); + int socket_id; size_t sent_bytes; if (n706a_parse_tcpsend_urc(urc_start, urc_len, &socket_id, &sent_bytes)) { - PPP_DEBUG_INFO("[URC分类] +TCPSEND URC detected: socket=%d, sent=%u", socket_id, (unsigned int)sent_bytes); - g_tcpsend_urc_callback(socket_id, sent_bytes); - // URC已处理,如果是独立URC则不写入缓冲区 - if (urc_start == data_str && urc_end >= data_str + length - 2) { - return true; + PPP_DEBUG_INFO("[URC分类] +TCPSEND URC detected: socket=%d, sent=%u (pure_async=%d)", + socket_id, (unsigned int)sent_bytes, is_pure_async_urc); + + // 如果是纯异步URC(没有命令回显),立即处理并返回 + // 如果是命令响应中的URC(有命令回显),需要检查是否还有其他内容 + if (is_pure_async_urc) { + // 纯异步URC,立即处理 + if (g_tcpsend_urc_callback) { + g_tcpsend_urc_callback(socket_id, sent_bytes); + } else { + PPP_DEBUG_WARN("[URC分类] +TCPSEND URC detected but callback not registered!"); + } + // 如果是独立URC(整个响应块就是这个URC),不写入缓冲区 + if (urc_start == data_str || (urc_start > data_str && + (urc_start[-1] == '\r' || urc_start[-1] == '\n'))) { + // URC前面只有空白字符或换行符,是独立的异步URC + if (urc_end >= data_str + length - 2) { + return true; // 独立URC,已处理,不写入缓冲区 + } + } + } else { + // 命令响应中的URC,需要写入缓冲区(因为可能还有OK等) + PPP_DEBUG_DEBUG("[URC分类] +TCPSEND URC是命令响应的一部分,需要写入缓冲区"); + if (g_tcpsend_urc_callback) { + g_tcpsend_urc_callback(socket_id, sent_bytes); + } + // 继续处理,让后续逻辑写入缓冲区 } + } else { + PPP_DEBUG_WARN("[URC分类] +TCPSEND URC found but parse failed, urc_len=%u", (unsigned int)urc_len); + // 输出原始URC内容用于调试 + char debug_urc[128] = {0}; + size_t debug_len = (urc_len < sizeof(debug_urc) - 1) ? urc_len : (sizeof(debug_urc) - 1); + memcpy(debug_urc, urc_start, debug_len); + PPP_DEBUG_WARN("[URC分类] Raw +TCPSEND URC content: [%.*s]", (int)debug_len, debug_urc); + } + } else { + // 检查是否包含 "TCPSEND" 字符串(可能是命令响应的一部分) + if (strstr(data_str, "TCPSEND") != NULL) { + PPP_DEBUG_DEBUG("[URC分类] Data contains 'TCPSEND' but not URC format (likely AT command response)"); + } + } + } + + /* ========== 类别4:HTTP响应URC(+HTTPRECV: / +HTTPSRECV:) ========== */ + if (length >= 11) { + const char *httprecv_pos = strstr(data_str, "+HTTPRECV: "); + const char *httpsrecv_pos = strstr(data_str, "+HTTPSRECV: "); + + if (httprecv_pos || httpsrecv_pos) { + /* HTTP响应URC格式:+HTTPRECV: , 或 +HTTPSRECV: , */ + /* 注意:HTTP响应数据可能很大,需要完整读取 */ + const char *urc_start = httprecv_pos ? httprecv_pos : httpsrecv_pos; + bool is_https = (httpsrecv_pos != NULL); + + /* 查找URC结束位置(可能是\r\n或数据结束) */ + /* HTTP响应数据可能包含二进制数据,不能简单用\r\n判断结束 */ + /* 需要解析URC格式:+HTTPRECV: , */ + size_t declared_len, actual_len; + const uint8_t *http_data; + + if (n706a_parse_httprecv_urc(urc_start, length - (urc_start - data_str), &declared_len, &actual_len, &http_data)) { + PPP_DEBUG_INFO("[URC分类] HTTP URC detected: %s, declared_len=%u, actual_len=%u", + is_https ? "+HTTPSRECV" : "+HTTPRECV", (unsigned int)declared_len, (unsigned int)actual_len); + + /* 调用HTTP客户端URC回调,使用 actual_len(实际可用数据长度) */ + extern void n706a_http_on_urc_received(const uint8_t *data, size_t length, bool is_https); + n706a_http_on_urc_received(http_data, actual_len, is_https); + + /* HTTP URC已处理,不写入AT响应缓冲区 */ + return true; + } else { + PPP_DEBUG_WARN("[URC分类] HTTP URC found but parse failed"); } } } - /* ========== 类别4:网络状态URC ========== */ + /* ========== 类别5:网络状态URC ========== */ if (length >= 6 && g_network_status_urc_callback) { const char *creg_pos = strstr(data_str, "+CREG: "); const char *cgreg_pos = strstr(data_str, "+CGREG: "); @@ -337,1588 +697,5870 @@ static bool n706a_classify_and_dispatch_urc(const uint8_t *data, uint32_t length } /** - * @brief 写入AT响应数据到环形缓冲区(由ppp_interrupt.c调用) - * @param data 数据指针 - * @param length 数据长度 + * @brief 从累积缓冲区中移除已处理的数据 + * @param remove_len 要移除的数据长度 */ -void n706a_at_response_buffer_write(const uint8_t *data, uint32_t length) +static void n706a_remove_processed_data(uint32_t remove_len) { - if (!data || length == 0) { + if (remove_len == 0 || remove_len > g_at_accumulate_length) { return; } - /* ========== 第一步:分类处理和分发URC ========== */ - /* 如果返回true,说明是URC已处理,不需要写入缓冲区 */ - bool is_urc = n706a_classify_and_dispatch_urc(data, length); - if (is_urc) { - // URC已通过callback处理,不写入AT响应缓冲区 - return; - } - - /* ========== 第二步:普通AT响应写入环形缓冲区 ========== */ - osal_mutex_lock(&g_at_response_mutex); + osal_mutex_lock(&g_at_accumulate_mutex); - for (uint32_t i = 0; i < length; i++) { - g_at_response_ring_buffer[g_at_response_head] = data[i]; - g_at_response_head = (g_at_response_head + 1) % N706A_AT_RESPONSE_RING_BUFFER_SIZE; - - // 检查溢出(缓冲区满时丢弃最老的数据) - if (g_at_response_head == g_at_response_tail) { - g_at_response_tail = (g_at_response_tail + 1) % N706A_AT_RESPONSE_RING_BUFFER_SIZE; - PPP_DEBUG_WARN("AT response buffer overflow, dropping oldest byte"); - } + // 将剩余数据向前移动 + if (remove_len < g_at_accumulate_length) { + memmove(g_at_accumulate_buffer, + g_at_accumulate_buffer + remove_len, + g_at_accumulate_length - remove_len); } - osal_mutex_unlock(&g_at_response_mutex); + g_at_accumulate_length -= remove_len; - // 通知等待线程有新数据 - osal_sem_up(&g_at_response_sem); + osal_mutex_unlock(&g_at_accumulate_mutex); - PPP_DEBUG_DEBUG("AT response buffer: wrote %u bytes", length); + PPP_DEBUG_DEBUG("[累积缓冲区] 移除 %u 字节,剩余 %u 字节", remove_len, g_at_accumulate_length); } /** - * @brief 初始化AT命令处理模块 + * @brief 清空累积缓冲区(用于socket关闭后清理残留数据) */ -ppp_error_t ppp_at_handler_init(uint8_t uart_bus, uint32_t current_baudrate, bool auto_baudrate_mode) +void n706a_at_clear_accumulate_buffer(void) { - if (g_at_ctx.initialized) { - PPP_DEBUG_WARN("AT handler already initialized"); - return PPP_ERR_OK; - } - - g_at_ctx.uart_bus = uart_bus; - g_at_ctx.current_baudrate = current_baudrate; - g_at_ctx.auto_baudrate_mode = auto_baudrate_mode; - - /* 创建AT命令互斥锁 */ - if (osal_mutex_init(&g_at_ctx.at_mutex) != OSAL_SUCCESS) { - PPP_DEBUG_ERROR("Failed to create AT mutex"); - return PPP_ERR_MEMORY; - } - - /* 创建AT响应缓冲区互斥锁 */ - if (osal_mutex_init(&g_at_response_mutex) != OSAL_SUCCESS) { - PPP_DEBUG_ERROR("Failed to create AT response mutex"); - osal_mutex_destroy(&g_at_ctx.at_mutex); - return PPP_ERR_MEMORY; - } - - /* 创建AT响应信号量 */ - if (osal_sem_init(&g_at_response_sem, 0) != OSAL_SUCCESS) { - PPP_DEBUG_ERROR("Failed to create AT response semaphore"); - osal_mutex_destroy(&g_at_ctx.at_mutex); - osal_mutex_destroy(&g_at_response_mutex); - return PPP_ERR_MEMORY; - } - - /* 初始化环形缓冲区 */ - g_at_response_head = 0; - g_at_response_tail = 0; - memset(g_at_response_ring_buffer, 0, sizeof(g_at_response_ring_buffer)); - - /* 分配全局AT响应缓存 */ - g_at_response_buffer = (uint8_t *)malloc(g_at_buffer_size); - if (g_at_response_buffer == NULL) { - PPP_DEBUG_ERROR("Failed to allocate AT response buffer"); - osal_mutex_destroy(&g_at_ctx.at_mutex); - osal_mutex_destroy(&g_at_response_mutex); - osal_sem_destroy(&g_at_response_sem); - return PPP_ERR_MEMORY; - } - - g_at_ctx.initialized = true; - PPP_DEBUG_INFO("n706a_net AT handler initialized on UART%d with async ring buffer", uart_bus); + osal_mutex_lock(&g_at_accumulate_mutex); + uint32_t old_length = g_at_accumulate_length; + g_at_accumulate_length = 0; + g_at_accumulate_last_update_time = 0; + osal_mutex_unlock(&g_at_accumulate_mutex); - return PPP_ERR_OK; + if (old_length > 0) { + PPP_DEBUG_DEBUG("[累积缓冲区] 已清空(socket关闭后清理),清除了 %u 字节残留数据", old_length); + } } /** - * @brief 反初始化AT命令处理模块 + * @brief 从累积缓冲区中提取一个完整的AT指令响应 + * @param response_start 输出:响应在缓冲区中的起始位置(偏移量) + * @param response_len 输出:响应长度 + * @return true=找到完整响应,false=数据不完整 + * + * AT响应格式: + * - 普通AT响应: "AT+CMD\r\n\r\n+RESPONSE\r\nOK\r\n" + * - URC响应: "\r\n+URC: data\r\n" + * - 提示符响应: "AT+TCPSEND=0,100\r\n\r\n> " + * + * 响应头识别: + * - 以 "\r\n" 开头(回显的命令或URC) + * - 以 "+" 开头(URC) + * - 以 "AT" 开头(回显的命令) + * + * 响应尾识别: + * - "\r\nOK\r\n" 或 "\r\nOK" + * - "\r\nERROR\r\n" 或 "\r\nERROR" + * - ">\r\n" 或 "> "(提示符) + * - "\r\n+CME ERROR: ...\r\n" + * - "\r\n+CMS ERROR: ...\r\n" */ -void ppp_at_handler_deinit(void) +static bool n706a_extract_complete_response(uint32_t *response_start, uint32_t *response_len) { - if (!g_at_ctx.initialized) { - return; + if (!response_start || !response_len || g_at_accumulate_length == 0) { + return false; } - - /* 销毁互斥锁和信号量 */ - osal_mutex_destroy(&g_at_ctx.at_mutex); - osal_mutex_destroy(&g_at_response_mutex); - osal_sem_destroy(&g_at_response_sem); - - /* 释放全局AT响应缓存 */ - if (g_at_response_buffer) { - free(g_at_response_buffer); - g_at_response_buffer = NULL; + + osal_mutex_lock(&g_at_accumulate_mutex); + + const char *buffer = (const char *)g_at_accumulate_buffer; + uint32_t length = g_at_accumulate_length; + + // 查找响应头(只跳过开头的 \x00 字节,保留 \r、\n 和空格) + // 注意: + // 1. \x00 字节需要被跳过,因为它们会干扰 strstr 等字符串函数 + // 2. \r、\n 是 AT 协议的重要分隔符,用于识别行边界(如 \r\nOK\r\n) + // 3. 空格也是 AT 协议的一部分,用于提示符识别(如 "> " 是 AT+TCPSEND 的提示符) + // 根据 AT 命令手册,AT+TCPSEND 的响应格式是:> + // 其中 "> " 后面的空格是提示符的一部分,必须保留 + uint32_t start = 0; + while (start < length && buffer[start] == '\0') { + start++; } - - /* 清空环形缓冲区 */ - g_at_response_head = 0; - g_at_response_tail = 0; - - memset_s(&g_at_ctx, sizeof(g_at_ctx), 0, sizeof(g_at_ctx)); - PPP_DEBUG_INFO("n706a_net AT handler deinitialized"); -} - -/** - * @brief 执行波特率自适应握手 - * - * 进入自适应波特率,证明1000000没有正常工作,需要先reset,等5秒,开始设置自动握手的第一个波特率, - * 读取所有缓存数据丢掉,下达at指令,等待结果,重试3次,如果不成功,就设置第二个波特率,读取所有缓存数据丢掉, - * 下达at指令,等结果,重试3次,直至第三个波特率检测。如果都没有成功,则返回失败。 - * 如果任何一次at指令返回正确ok,直接在当前设置下,下达指 AT+IPR 配置 1000000 波特率,并返回成功。 - * 设置当前波特率为1000000,再次reset 模组,让其自动工作在 1000000波特率下。 - */ -ppp_error_t ppp_at_baudrate_handshake(void) -{ - if (!g_at_ctx.initialized) { - return PPP_ERR_INVALID_PARAM; + + if (start >= length) { + // 只有 \x00 字节,保留在缓冲区中(可能是不完整的响应的一部分) + // 不清除,等待更多数据到来 + osal_mutex_unlock(&g_at_accumulate_mutex); + return false; } - - PPP_DEBUG_INFO("Starting baudrate handshake (prove %d not working, then try auto handshake)",g_at_ctx.current_baudrate); - - // uint8_t dummy_buffer[256]={0}; - - // 2. 依次尝试自动波特率列表 - for (int i = 0; i < PPP_BAUDRATE_AUTO_COUNT; i++) { - uint32_t baudrate = g_auto_baudrates[i]; - PPP_DEBUG_DEBUG("=== Trying Baudrate %u (index %d/%d) ===", baudrate, i + 1, PPP_BAUDRATE_AUTO_COUNT); - - // 配置串口波特率 - uart_attr_t uart_attr = { - .baud_rate = baudrate, - .data_bits = UART_DATA_BIT_8, - .parity = UART_PARITY_NONE, - .stop_bits = UART_STOP_BIT_1, - }; - if (uapi_uart_set_attr(g_at_ctx.uart_bus, &uart_attr) != ERRCODE_SUCC) { - PPP_DEBUG_WARN("Failed to set baudrate %u", baudrate); - continue; + + // 跳过 \x00 后,保留所有其他字符(包括 \r、\n、空格),因为它们都是协议的一部分 + // 例如:\r\n+TCPRECV: ... 或 \r\nOK\r\n 或 \r\n> \r\n 等格式 + + // 查找响应尾(从start开始查找终止符) + uint32_t end = start; + bool found_end = false; + + + // 特殊处理:检查是否是包含数据的URC(如+TCPRECV, +HTTPRECV等) + // 这些URC的格式是:+URC: ,,\r\n + // 需要先解析data_len来确定完整响应长度 + // 注意:由于响应开头可能有 \x00 字节,strstr 可能失败,需要手动查找 + const char *tcprecv_pos = NULL; + const char *ssltcprecv_pos = NULL; + const char *httprecv_pos = NULL; + const char *httpsrecv_pos = NULL; + const char *tcpsend_pos = NULL; + const char *ssltcpend_pos = NULL; + + // 手动查找 URC,跳过 \x00 字节 + for (uint32_t i = start; i < length; i++) { + if (buffer[i] == '\0') { + continue; // 跳过 \x00 字节 } - PPP_DEBUG_DEBUG("UART configured for baudrate %u", baudrate); - - // 清空接收缓冲区 - // PPP_DEBUG_DEBUG("Clearing UART RX buffer before AT test..."); - // int clear_cnt = 0; - // memset(dummy_buffer,0,sizeof(dummy_buffer)); - // while (uapi_uart_read(g_at_ctx.uart_bus, dummy_buffer, sizeof(dummy_buffer), 100) > 0) { - // clear_cnt++; - // } - // if (clear_cnt > 0) { - // PPP_DEBUG_DEBUG("Cleared %d RX buffer rounds", clear_cnt); - // } - - // 3次重试AT - ppp_error_t ret = PPP_ERR_AT_FAILED; - for (int retry = 0; retry < 2; retry++) { - PPP_DEBUG_DEBUG("--- AT Test at %u baud, retry %d/3 ---", baudrate, retry + 1); - - // 再次清空缓冲区,防止上次残留 - // memset(dummy_buffer,0,sizeof(dummy_buffer)); - // while (uapi_uart_read(g_at_ctx.uart_bus, dummy_buffer, sizeof(dummy_buffer), 100) > 0) {} - - ppp_at_response_t response; - ret = ppp_at_send_command("AT", "OK", 1000, &response); - - if (ret == PPP_ERR_OK) { - PPP_DEBUG_INFO("AT command successful at baudrate %u", baudrate); - - // 在当前波特率下,设置AT+IPR=1000000 - ret = ppp_at_set_baudrate(g_at_ctx.current_baudrate); - if (ret != PPP_ERR_OK) { + // 检查是否匹配 "+TCPRECV: " + if (i + 10 <= length && strncmp(&buffer[i], "+TCPRECV: ", 10) == 0) { + tcprecv_pos = &buffer[i]; + break; + } + // 检查是否匹配 "+SSLTCPRECV: " + if (i + 13 <= length && strncmp(&buffer[i], "+SSLTCPRECV: ", 13) == 0) { + ssltcprecv_pos = &buffer[i]; + break; + } + // 检查是否匹配 "+HTTPRECV: " + if (i + 11 <= length && strncmp(&buffer[i], "+HTTPRECV: ", 11) == 0) { + httprecv_pos = &buffer[i]; + break; + } + // 检查是否匹配 "+HTTPSRECV: " + if (i + 12 <= length && strncmp(&buffer[i], "+HTTPSRECV: ", 12) == 0) { + httpsrecv_pos = &buffer[i]; + break; + } + // 检查是否匹配 "+TCPSEND: " + if (i + 10 <= length && strncmp(&buffer[i], "+TCPSEND: ", 10) == 0) { + tcpsend_pos = &buffer[i]; + break; + } + // 检查是否匹配 "+SSLTCPEND: " + if (i + 12 <= length && strncmp(&buffer[i], "+SSLTCPEND: ", 12) == 0) { + ssltcpend_pos = &buffer[i]; + break; + } + } + + // 如果手动查找没找到,使用 strstr 作为后备(处理正常情况,没有 \x00 字节) + if (!tcprecv_pos && !ssltcprecv_pos && !httprecv_pos && !httpsrecv_pos && !tcpsend_pos && !ssltcpend_pos) { + tcprecv_pos = strstr(&buffer[start], "+TCPRECV: "); + ssltcprecv_pos = strstr(&buffer[start], "+SSLTCPRECV: "); + httprecv_pos = strstr(&buffer[start], "+HTTPRECV: "); + httpsrecv_pos = strstr(&buffer[start], "+HTTPSRECV: "); + tcpsend_pos = strstr(&buffer[start], "+TCPSEND: "); + ssltcpend_pos = strstr(&buffer[start], "+SSLTCPEND: "); + } + + if (tcprecv_pos || ssltcprecv_pos) { + // +TCPRECV格式:+TCPRECV: ,,\r\n + const char *urc_start = tcprecv_pos ? tcprecv_pos : ssltcprecv_pos; + + // ⚠️ 关键:验证 urc_start 是否在 buffer 范围内(这是导致 data_len 解析错误的根本原因) + const char *buffer_end = buffer + length; + if (urc_start < buffer || urc_start >= buffer_end) { + PPP_DEBUG_ERROR("[提取完整响应] ⚠️ 根本原因:urc_start不在缓冲区范围内!urc_start=%p, buffer=%p, buffer_end=%p, length=%u", + (void *)urc_start, (void *)buffer, (void *)buffer_end, length); + osal_mutex_unlock(&g_at_accumulate_mutex); + return false; + } + + // 计算 parse_len(现在可以安全计算,因为 urc_start 已验证在范围内) + uint32_t urc_start_offset = urc_start - buffer; + if (urc_start_offset > length) { + PPP_DEBUG_ERROR("[提取完整响应] ⚠️ 根本原因:urc_start_offset > length!urc_start_offset=%u, length=%u", + urc_start_offset, length); + osal_mutex_unlock(&g_at_accumulate_mutex); + return false; + } + size_t parse_len = length - urc_start_offset; + + // ⚠️ 关键:验证 parse_len 是否合理(最大64KB) + const size_t MAX_REASONABLE_PARSE_LEN = 65536; + if (parse_len > MAX_REASONABLE_PARSE_LEN) { + PPP_DEBUG_ERROR("[提取完整响应] ⚠️ 根本原因:parse_len异常大!parse_len=%u (max=%u), urc_start_offset=%u, length=%u", + (unsigned int)parse_len, MAX_REASONABLE_PARSE_LEN, urc_start_offset, length); + osal_mutex_unlock(&g_at_accumulate_mutex); + return false; + } + + int socket_id; + size_t data_len; + const uint8_t *tcp_data; + + // 尝试解析URC头部,获取data_len + if (n706a_parse_tcprecv_urc(urc_start, parse_len, &socket_id, &data_len, &tcp_data)) { + // 计算完整URC长度:URC头部 + 数据部分 + \r\n + // 注意:data_len是二进制数据长度,在HEX模式下,实际数据部分(HEX字符串)长度 = data_len × 2 + const char *data_start = (const char *)tcp_data; + uint32_t urc_header_len = data_start - urc_start; + + // ⚠️ 关键:验证 data_len 的合理性,防止溢出或异常大的值 + const uint32_t MAX_REASONABLE_DATA_LEN = 65536; // 最大合理的数据长度(64KB) + if (data_len > MAX_REASONABLE_DATA_LEN) { + PPP_DEBUG_ERROR("[提取完整响应] ⚠️ data_len异常大:%u (max=%u),可能是解析错误!截断到最大值", + (unsigned int)data_len, MAX_REASONABLE_DATA_LEN); + data_len = MAX_REASONABLE_DATA_LEN; + } + + // 在HEX模式下,实际数据长度是 data_len * 2(HEX字符串长度) + // 在ASCII模式下,实际数据长度是 data_len(ASCII字符串长度) + // 为了兼容两种模式,使用 data_len * 2 作为最大可能长度(HEX模式) + // 如果实际数据不足,会在后续的strstr查找中处理 + uint32_t actual_data_len = g_tcp_recv_hex_mode ? (data_len * 2) : data_len; + + // ⚠️ 关键:检查 actual_data_len 是否溢出 + if (g_tcp_recv_hex_mode && data_len > (UINT32_MAX / 2)) { + PPP_DEBUG_ERROR("[提取完整响应] ⚠️ actual_data_len计算溢出:data_len=%u, actual_data_len=%u (max=%u)", + (unsigned int)data_len, (unsigned int)actual_data_len, UINT32_MAX / 2); + osal_mutex_unlock(&g_at_accumulate_mutex); + return false; + } + + uint32_t full_urc_len = urc_header_len + actual_data_len; + uint32_t urc_start_offset = urc_start - buffer; + + // ⚠️ 关键:检查 full_urc_len 是否溢出 + if (full_urc_len < urc_header_len || full_urc_len < actual_data_len) { + PPP_DEBUG_ERROR("[提取完整响应] ⚠️ full_urc_len计算溢出:urc_header_len=%u, actual_data_len=%u, full_urc_len=%u", + urc_header_len, (unsigned int)actual_data_len, full_urc_len); + osal_mutex_unlock(&g_at_accumulate_mutex); + return false; + } + + // 检查数据部分是否完整(需要的数据长度是否在当前缓冲区中) + if (urc_start_offset + full_urc_len > length) { + // 数据不完整,等待更多数据 + osal_mutex_unlock(&g_at_accumulate_mutex); + PPP_DEBUG_DEBUG("[提取完整响应] +TCPRECV数据不完整,等待更多数据 (需要=%u, 当前可用=%u, binary_len=%u, hex_mode=%d)", + full_urc_len, length - urc_start_offset, (unsigned int)data_len, g_tcp_recv_hex_mode); + return false; + } + + // 查找数据后的\r\n(使用实际数据长度) + // ⚠️ 关键:验证 data_end 是否在缓冲区范围内 + const char *buffer_end = buffer + length; + const char *data_end = data_start + actual_data_len; + + // 验证 data_end 是否超出缓冲区范围 + if (data_end > buffer_end) { + PPP_DEBUG_ERROR("[提取完整响应] ⚠️ 计算错误:data_end超出缓冲区范围!data_start=%p, actual_data_len=%u, data_end=%p, buffer_end=%p, length=%u", + (void *)data_start, (unsigned int)actual_data_len, (void *)data_end, (void *)buffer_end, length); + osal_mutex_unlock(&g_at_accumulate_mutex); + return false; + } + + // 验证 data_start 是否在缓冲区范围内 + if (data_start < buffer || data_start >= buffer_end) { + PPP_DEBUG_ERROR("[提取完整响应] ⚠️ 计算错误:data_start不在缓冲区范围内!data_start=%p, buffer=%p, buffer_end=%p", + (void *)data_start, (void *)buffer, (void *)buffer_end); + osal_mutex_unlock(&g_at_accumulate_mutex); + return false; + } + + // 限制 strstr 搜索范围,避免越界 + size_t search_len = buffer_end - data_end; + if (search_len > 1024) { + search_len = 1024; // 限制搜索范围,避免搜索过大区域 + } + + const char *urc_end = NULL; + if (search_len >= 2) { + // 手动搜索 \r\n,限制在缓冲区范围内 + for (size_t i = 0; i <= search_len - 2; i++) { + if (data_end[i] == '\r' && data_end[i + 1] == '\n') { + urc_end = data_end + i; + break; + } + } + } + + if (!urc_end && search_len >= 1) { + // 手动搜索 \n + for (size_t i = 0; i < search_len; i++) { + if (data_end[i] == '\n') { + urc_end = data_end + i; + break; + } + } + } + + if (!urc_end) { + // 数据后的\r\n不在当前缓冲区中,等待更多数据 + osal_mutex_unlock(&g_at_accumulate_mutex); + PPP_DEBUG_DEBUG("[提取完整响应] +TCPRECV数据后的\\r\\n不完整,等待更多数据 (data_end=%p, buffer_end=%p, search_len=%u)", + (void *)data_end, (void *)buffer_end, (unsigned int)search_len); + return false; + } + + // 验证 urc_end 是否在缓冲区范围内 + if (urc_end < buffer || urc_end >= buffer_end) { + PPP_DEBUG_ERROR("[提取完整响应] ⚠️ 计算错误:urc_end不在缓冲区范围内!urc_end=%p, buffer=%p, buffer_end=%p", + (void *)urc_end, (void *)buffer, (void *)buffer_end); + osal_mutex_unlock(&g_at_accumulate_mutex); + return false; + } + + // 找到完整URC,计算 end(验证计算结果) + uint32_t urc_end_offset = urc_end - buffer; + if (urc_end_offset + 2 > length) { + PPP_DEBUG_ERROR("[提取完整响应] ⚠️ 计算错误:urc_end_offset+2超出缓冲区长度!urc_end_offset=%u, length=%u", + urc_end_offset, length); + osal_mutex_unlock(&g_at_accumulate_mutex); + return false; + } + + end = urc_end_offset + 2; // 包括\r\n + found_end = true; + } + } else if (httprecv_pos || httpsrecv_pos) { + // +HTTPRECV格式:+HTTPRECV: , + const char *urc_start = httprecv_pos ? httprecv_pos : httpsrecv_pos; + + // ⚠️ 关键:验证 urc_start 是否在 buffer 范围内 + const char *buffer_end = buffer + length; + if (urc_start < buffer || urc_start >= buffer_end) { + PPP_DEBUG_ERROR("[提取完整响应] ⚠️ 根本原因:+HTTPRECV urc_start不在缓冲区范围内!urc_start=%p, buffer=%p, buffer_end=%p", + (void *)urc_start, (void *)buffer, (void *)buffer_end); + osal_mutex_unlock(&g_at_accumulate_mutex); + return false; + } + + uint32_t urc_start_offset = urc_start - buffer; + if (urc_start_offset > length) { + PPP_DEBUG_ERROR("[提取完整响应] ⚠️ 根本原因:+HTTPRECV urc_start_offset > length!urc_start_offset=%u, length=%u", + urc_start_offset, length); + osal_mutex_unlock(&g_at_accumulate_mutex); + return false; + } + size_t parse_len = length - urc_start_offset; + + size_t declared_len, actual_len; + const uint8_t *http_data; + + // 尝试解析URC,获取 declared_len 和 actual_len + if (n706a_parse_httprecv_urc(urc_start, parse_len, &declared_len, &actual_len, &http_data)) { + // 计算完整URC长度:URC头部 + 数据部分(使用 actual_len,实际可用数据长度) + const char *data_start = (const char *)http_data; + uint32_t urc_header_len = data_start - urc_start; + uint32_t full_urc_len = urc_header_len + actual_len; + uint32_t urc_start_offset = urc_start - buffer; + + // HTTP URC可能没有\r\n结尾,数据结束就是URC结束 + // 检查数据部分是否完整 + // ⚠️ 关键:验证 full_urc_len 是否溢出 + if (full_urc_len < urc_header_len || full_urc_len < actual_len) { + PPP_DEBUG_ERROR("[提取完整响应] ⚠️ 计算错误:+HTTPRECV full_urc_len溢出!urc_header_len=%u, actual_len=%u, full_urc_len=%u", + urc_header_len, (unsigned int)actual_len, full_urc_len); + osal_mutex_unlock(&g_at_accumulate_mutex); + return false; + } + + // ⚠️ 关键:验证 urc_start_offset + full_urc_len 是否溢出 + if (urc_start_offset + full_urc_len < urc_start_offset || + urc_start_offset + full_urc_len < full_urc_len) { + PPP_DEBUG_ERROR("[提取完整响应] ⚠️ 计算错误:+HTTPRECV urc_start_offset+full_urc_len溢出!urc_start_offset=%u, full_urc_len=%u", + urc_start_offset, full_urc_len); + osal_mutex_unlock(&g_at_accumulate_mutex); + return false; + } + + if (urc_start_offset + full_urc_len <= length) { + end = urc_start_offset + full_urc_len; + found_end = true; + } else { + // 数据不完整,等待更多数据 + osal_mutex_unlock(&g_at_accumulate_mutex); + PPP_DEBUG_DEBUG("[提取完整响应] +HTTPRECV数据不完整,等待更多数据 (需要=%u, 当前可用=%u)", + full_urc_len, length - urc_start_offset); + return false; + } + } + } else if (tcpsend_pos || ssltcpend_pos) { + // +TCPSEND格式:+TCPSEND: ,\r\n 或 +TCPSEND: ,OPERATION EXPIRED\r\r + const char *urc_start = tcpsend_pos ? tcpsend_pos : ssltcpend_pos; + + // ⚠️ 关键:验证 urc_start 是否在 buffer 范围内 + const char *buffer_end = buffer + length; + if (urc_start < buffer || urc_start >= buffer_end) { + PPP_DEBUG_ERROR("[提取完整响应] ⚠️ 根本原因:+TCPSEND urc_start不在缓冲区范围内!urc_start=%p, buffer=%p, buffer_end=%p", + (void *)urc_start, (void *)buffer, (void *)buffer_end); + osal_mutex_unlock(&g_at_accumulate_mutex); + return false; + } + + uint32_t urc_start_offset = urc_start - buffer; + if (urc_start_offset > length) { + PPP_DEBUG_ERROR("[提取完整响应] ⚠️ 根本原因:+TCPSEND urc_start_offset > length!urc_start_offset=%u, length=%u", + urc_start_offset, length); + osal_mutex_unlock(&g_at_accumulate_mutex); + return false; + } + size_t parse_len = length - urc_start_offset; + + // 查找URC结束符:支持 \r\n, \r\r, 单个\r, \n + const char *urc_end = strstr(urc_start, "\r\n"); + bool is_crlf = false; + bool is_crcr = false; + bool is_single_cr = false; + + if (urc_end) { + is_crlf = true; // 找到 \r\n + } else { + // 检查 \r\r 格式(异常格式,但需要容错处理) + const char *cr_cr = strstr(urc_start, "\r\r"); + if (cr_cr) { + urc_end = cr_cr + 1; // 指向第二个 \r + is_crcr = true; + PPP_DEBUG_WARN("[提取完整响应] +TCPSEND URC以 \\r\\r 结尾(异常格式,可能是模组固件问题)"); + } else { + // 检查 \n + urc_end = strstr(urc_start, "\n"); + if (!urc_end) { + // 检查单个 \r(在URC末尾,后面没有 \n 或 \r) + // 从URC末尾向前查找,找到最后一个 \r + const char *urc_end_pos = urc_start + parse_len; + for (const char *p = urc_end_pos - 1; p >= urc_start; p--) { + if (*p == '\r') { + // 检查这个 \r 后面是否有 \n 或 \r + if (p + 1 >= urc_end_pos) { + // 这是最后一个字符,是单个 \r + urc_end = p; + is_single_cr = true; + PPP_DEBUG_DEBUG("[提取完整响应] +TCPSEND URC以单个 \\r 结尾"); + break; + } else if (p[1] != '\n' && p[1] != '\r') { + // 后面不是 \n 或 \r,是单个 \r + urc_end = p; + is_single_cr = true; + PPP_DEBUG_DEBUG("[提取完整响应] +TCPSEND URC以单个 \\r 结尾"); + break; + } + } + } + } + } + } + + if (urc_end) { + // ⚠️ 关键:验证 urc_end 是否在缓冲区范围内 + const char *buffer_end = buffer + length; + if (urc_end < buffer || urc_end >= buffer_end) { + PPP_DEBUG_ERROR("[提取完整响应] ⚠️ 计算错误:+TCPSEND URC的urc_end不在缓冲区范围内!urc_end=%p, buffer=%p, buffer_end=%p, length=%u", + (void *)urc_end, (void *)buffer, (void *)buffer_end, length); + osal_mutex_unlock(&g_at_accumulate_mutex); + return false; + } + + // 找到完整URC,根据结束符类型计算end(验证计算结果) + uint32_t urc_end_offset = urc_end - buffer; + if (is_crlf) { + // \r\n 结束符:urc_end指向\r,需要+2包含\n + if (urc_end_offset + 2 > length) { + PPP_DEBUG_ERROR("[提取完整响应] ⚠️ 计算错误:urc_end_offset+2超出缓冲区长度!urc_end_offset=%u, length=%u", + urc_end_offset, length); + osal_mutex_unlock(&g_at_accumulate_mutex); + return false; + } + end = urc_end_offset + 2; + } else if (is_crcr) { + // \r\r 结束符:urc_end已经指向第二个\r,需要+1包含第二个\r + if (urc_end_offset + 1 > length) { + PPP_DEBUG_ERROR("[提取完整响应] ⚠️ 计算错误:urc_end_offset+1超出缓冲区长度!urc_end_offset=%u, length=%u", + urc_end_offset, length); + osal_mutex_unlock(&g_at_accumulate_mutex); + return false; + } + end = urc_end_offset + 1; + } else if (is_single_cr) { + // 单个 \r 结束符:urc_end指向\r,需要+1包含\r + if (urc_end_offset + 1 > length) { + PPP_DEBUG_ERROR("[提取完整响应] ⚠️ 计算错误:urc_end_offset+1超出缓冲区长度!urc_end_offset=%u, length=%u", + urc_end_offset, length); + osal_mutex_unlock(&g_at_accumulate_mutex); + return false; + } + end = urc_end_offset + 1; + } else { + // \n 结束符:urc_end指向\n,需要+1包含\n + if (urc_end_offset + 1 > length) { + PPP_DEBUG_ERROR("[提取完整响应] ⚠️ 计算错误:urc_end_offset+1超出缓冲区长度!urc_end_offset=%u, length=%u", + urc_end_offset, length); + osal_mutex_unlock(&g_at_accumulate_mutex); + return false; + } + end = urc_end_offset + 1; + } + found_end = true; + } else { + // URC不完整,等待更多数据 + osal_mutex_unlock(&g_at_accumulate_mutex); + PPP_DEBUG_DEBUG("[提取完整响应] +TCPSEND URC不完整,等待更多数据"); + return false; + } + } + + // 如果没有找到包含数据的URC,查找标准终止符 + // 关键:每次调用都是从缓冲区开头(start位置)开始查找第一个完整响应 + // 找到第一个终止符后立即返回,不需要检查"新的AT命令" + // 因为处理完一个响应后,会清除它,然后重新调用此函数查找下一个响应 + if (!found_end) { + // 通用性检查:如果检测到连续两个AT指令回复的头出现,而中间没有尾, + // 说明第一个响应不完整(可能是无效数据或损坏的数据) + // 应该丢弃第一个头到第二个头之间的数据,从第二个头开始查找尾 + // + // 重要:为了避免误报(响应内容中可能包含类似头的字符串), + // 需要先尝试从第一个头开始查找终止符,只有在找不到终止符的情况下才考虑丢弃 + uint32_t second_header_pos = 0; + bool found_second_header = false; + bool first_response_has_terminator = false; + + // 先尝试从start位置查找终止符(在合理范围内,比如200字节) + // 如果找到了终止符,说明第一个响应可能是完整的,不应该丢弃 + uint32_t search_limit = (length - start < 200) ? length : (start + 200); + for (uint32_t i = start; i < search_limit; i++) { + // 检查 "\r\nOK\r\n" 或 "\r\nOK" + if (i + 1 < length && buffer[i] == '\r' && buffer[i+1] == '\n') { + if (i + 3 < length && buffer[i+2] == 'O' && buffer[i+3] == 'K') { + first_response_has_terminator = true; + break; + } + } + // 检查 "\r\nERROR\r\n" 或 "\r\nERROR" + if (i + 1 < length && buffer[i] == '\r' && buffer[i+1] == '\n') { + if (i + 6 < length && strncmp(&buffer[i+2], "ERROR", 5) == 0) { + first_response_has_terminator = true; + break; + } + } + // 检查 ">\r\n" 或 "> " + if (buffer[i] == '>') { + if (i == 0 || buffer[i-1] == '\n' || buffer[i-1] == '\r' || buffer[i-1] == ' ') { + if (i + 1 == length || buffer[i+1] == '\r' || buffer[i+1] == '\n' || buffer[i+1] == ' ') { + first_response_has_terminator = true; + break; + } + } + } + // 检查命令回显格式:AT+...\r\n(启用ATE1时,命令回显本身就是一个完整响应) + if (i + 2 < length && buffer[i] == 'A' && buffer[i+1] == 'T' && buffer[i+2] == '+') { + // 查找命令回显的结束位置(\r\n) + uint32_t cmd_end = i + 3; + while (cmd_end < search_limit && cmd_end < length) { + if (cmd_end + 1 < length && buffer[cmd_end] == '\r' && buffer[cmd_end+1] == '\n') { + // 找到命令回显的终止符,说明这是一个完整的响应 + first_response_has_terminator = true; + break; + } + cmd_end++; + } + if (first_response_has_terminator) { + break; + } + } + } + + // 只有在第一个响应没有找到终止符的情况下,才查找第二个头 + if (!first_response_has_terminator) { + // 从start+1开始查找第二个响应头(URC格式:+XXX: 或 AT命令回显:AT+) + // 限制搜索范围,避免在很长的响应内容中误报 + uint32_t search_end = (length - start < 200) ? length : (start + 200); + for (uint32_t i = start + 1; i < search_end; i++) { + // 检查是否是URC格式的头(以+开头,包含:) + if (i < length && buffer[i] == '+') { + uint32_t colon_pos = i; + while (colon_pos < length && colon_pos < i + 20 && buffer[colon_pos] != ':') { + colon_pos++; + } + if (colon_pos < length && buffer[colon_pos] == ':') { + // 检查前面是否有换行符(确保是新的响应头,而不是响应内容中的"+XXX:") + // 同时检查这个URC是否在第一个响应的合理范围内(避免误报) + if ((i == 0 || buffer[i-1] == '\n' || buffer[i-1] == '\r') && + (i - start > 10)) { // 至少间隔10字节,避免误报 + second_header_pos = i; + found_second_header = true; + break; + } + } + } + + // 检查是否是AT命令回显的头(AT+) + if (i + 2 < length && buffer[i] == 'A' && buffer[i+1] == 'T' && buffer[i+2] == '+') { + // 检查前面是否有换行符,或者前面是字母/数字/空格/冒号等(说明是URC参数或响应内容) + // 这样可以检测到紧跟在URC后面的命令回显(如+TCPSETUP: 0,O后面直接跟AT+TCPSEND) + // 同时检查这个命令是否在第一个响应的合理范围内(避免误报) + bool is_new_command = false; + if (i == 0) { + is_new_command = true; + } else { + uint8_t prev_char = buffer[i-1]; + // 前面是换行符,肯定是新命令 + if (prev_char == '\n' || prev_char == '\r') { + is_new_command = true; + } + // 前面是字母/数字/空格/冒号/逗号等,说明是URC参数或响应内容,后面跟AT+应该是新命令 + else if ((prev_char >= 'A' && prev_char <= 'Z') || + (prev_char >= 'a' && prev_char <= 'z') || + (prev_char >= '0' && prev_char <= '9') || + prev_char == ' ' || prev_char == ':' || prev_char == ',' || prev_char == 'O') { + // 如果前面是'O',可能是+TCPSETUP: 0,O,后面跟AT+应该是新命令 + // 如果前面是其他字符,也可能是URC参数,后面跟AT+应该是新命令 + is_new_command = true; + } + } + + if (is_new_command && (i - start > 10)) { // 至少间隔10字节,避免误报 + second_header_pos = i; + found_second_header = true; + break; + } + } + } + } + + // 如果找到了第二个响应头,且第一个响应没有找到终止符,说明第一个响应不完整 + // 丢弃第一个头到第二个头之间的数据,从第二个头开始查找尾 + if (found_second_header && second_header_pos > start && !first_response_has_terminator) { + uint32_t dropped_bytes = second_header_pos - start; + g_dropped_data_count++; + g_dropped_data_bytes += dropped_bytes; + + // 打印丢弃的数据(前64字节,用于调试) + char dropped_preview[129] = {0}; + uint32_t preview_len = (dropped_bytes < 64) ? dropped_bytes : 64; + for (uint32_t j = 0; j < preview_len; j++) { + uint8_t c = buffer[start + j]; + if (c >= 32 && c < 127) { + dropped_preview[j] = c; + } else if (c == '\r') { + dropped_preview[j] = '\\'; + if (j + 1 < 128) dropped_preview[++j] = 'r'; + } else if (c == '\n') { + dropped_preview[j] = '\\'; + if (j + 1 < 128) dropped_preview[++j] = 'n'; + } else { + dropped_preview[j] = '.'; + } + } + + PPP_DEBUG_WARN("[提取完整响应] 检测到连续两个响应头(第一个无终止符),丢弃不完整数据: %u 字节 (累计丢弃: %u 次, %u 字节), 预览: [%.*s]", + dropped_bytes, g_dropped_data_count, g_dropped_data_bytes, + (int)preview_len, dropped_preview); + + // 从第二个头开始查找尾 + start = second_header_pos; + // 跳过第二个头前面的空白字符 + while (start < length && (buffer[start] == '\r' || buffer[start] == '\n' || buffer[start] == ' ')) { + start++; + } + if (start >= length) { + osal_mutex_unlock(&g_at_accumulate_mutex); + return false; + } + } + + for (uint32_t i = start; i < length; i++) { + // 优先检查URC格式的终止符(如 +TCPCLOSE: ERROR\r\n 或 +TCPSETUP: 0,OK\r\r) + // 这些URC通常以 \r\n 或 \r\r 结尾 + if (i + 16 <= length) { + // 检查 +TCPCLOSE: ERROR + if (strncmp(&buffer[i], "+TCPCLOSE: ERROR", 16) == 0) { + if (i + 16 < length && buffer[i+16] == '\r' && i + 17 < length && buffer[i+17] == '\n') { + end = i + 18; + found_end = true; + break; // 找到第一个终止符,立即返回 + } else if (i + 16 == length) { + // ERROR后面没有数据,可能是缓冲区末尾,等待更多数据 + // 但如果是完整的ERROR(没有\r\n),也可以认为是结束 + end = i + 16; + found_end = true; + break; + } + } + // 检查 +TCPCLOSE: ERR(缩写形式) + if (strncmp(&buffer[i], "+TCPCLOSE: ERR", 14) == 0) { + if (i + 14 < length && buffer[i+14] == '\r') { + // 支持 \r\n 或 \r\r 结尾 + if (i + 15 < length && buffer[i+15] == '\n') { + end = i + 16; + } else if (i + 15 < length && buffer[i+15] == '\r') { + end = i + 16; // \r\r 结尾 + } else { + end = i + 15; // 只有 \r + } + found_end = true; + break; + } else if (i + 14 == length) { + end = i + 14; + found_end = true; + break; + } + } + } + // 检查 +TCPSETUP: 0,OK\r\r 或 +TCPCLOSE: 0,OK\r\r 格式 + if (i + 15 <= length) { + // 检查 +TCPSETUP: 0,OK + if (strncmp(&buffer[i], "+TCPSETUP: ", 11) == 0) { + // 查找 OK 或 ERR + uint32_t j = i + 11; + while (j < length && j < i + 20 && buffer[j] != '\r' && buffer[j] != '\n') { + j++; + } + if (j > i + 11) { + // 检查是否包含 OK 或 ERR + const char *urc_data = (const char *)&buffer[i + 11]; + uint32_t urc_data_len = j - (i + 11); + if (strstr(urc_data, "OK") != NULL || strstr(urc_data, "ERR") != NULL) { + // 找到 OK 或 ERR,检查结束符 + if (j < length && buffer[j] == '\r') { + if (j + 1 < length && buffer[j+1] == '\n') { + end = j + 2; // \r\n 结尾(正常格式) + } else if (j + 1 < length && buffer[j+1] == '\r') { + // ⚠️ \r\r 结尾(异常格式,可能是模组固件问题) + PPP_DEBUG_WARN("[提取完整响应] 检测到异常格式:响应以 \\r\\r 结尾而不是 \\r\\n(可能是模组固件问题)"); + end = j + 2; // \r\r 结尾(容错处理) + } else { + end = j + 1; // 只有 \r + } + found_end = true; + break; + } else if (j == length) { + // 缓冲区末尾,等待更多数据 + break; + } + } + } + } + // 检查 +TCPCLOSE: 或 +TCPCLOSE: , + // 格式:+TCPCLOSE: 0,Link Closed\r\n 或 +TCPCLOSE: 0,OK\r\n 或 +TCPCLOSE: 0\r\n + if (strncmp(&buffer[i], "+TCPCLOSE: ", 11) == 0) { + // 查找结束符 \r\n 或 \n + uint32_t j = i + 11; + // 跳过 socket_id 和可能的逗号及原因字符串 + while (j < length && buffer[j] != '\r' && buffer[j] != '\n') { + j++; + } + if (j > i + 11) { + // 找到了结束符,检查格式 + if (j < length && buffer[j] == '\r') { + if (j + 1 < length && buffer[j+1] == '\n') { + end = j + 2; // \r\n 结尾(正常格式) + found_end = true; + break; + } else if (j + 1 < length && buffer[j+1] == '\r') { + // ⚠️ \r\r 结尾(异常格式,可能是模组固件问题) + PPP_DEBUG_WARN("[提取完整响应] 检测到异常格式:响应以 \\r\\r 结尾而不是 \\r\\n(可能是模组固件问题)"); + end = j + 2; // \r\r 结尾(容错处理) + found_end = true; + break; + } else { + end = j + 1; // 只有 \r + found_end = true; + break; + } + } else if (j < length && buffer[j] == '\n') { + end = j + 1; // \n 结尾 + found_end = true; + break; + } else if (j == length) { + // 缓冲区末尾,等待更多数据 + break; + } + } + } + } + if (i + 19 <= length) { + // 检查 +SSLTCPCLOSE: ERROR + if (strncmp(&buffer[i], "+SSLTCPCLOSE: ERROR", 19) == 0) { + if (i + 19 < length && buffer[i+19] == '\r' && i + 20 < length && buffer[i+20] == '\n') { + end = i + 21; + found_end = true; + break; // 找到第一个终止符,立即返回 + } else if (i + 19 == length) { + end = i + 19; + found_end = true; + break; + } + } + } + + // 检查直接以"OK"开头的情况(如果start跳过了开头的\r\n,那么start位置可能就是"OK") + // 例如:\r\nOK\r\n 经过跳过空白字符后,start=2,buffer[2]='O',buffer[3]='K' + if (i == start && i + 1 < length && buffer[i] == 'O' && buffer[i+1] == 'K') { + // 检查OK后面是否是结束符或换行符 + if (i + 2 == length || (i + 2 < length && buffer[i+2] == '\r')) { + // 找到OK终止符 + end = (i + 2 < length && buffer[i+2] == '\r' && i + 3 < length && buffer[i+3] == '\n') ? + (i + 4) : (i + 2); + found_end = true; + break; // 立即退出循环,返回第一个完整响应 + } + } + + // 检查 "\r\nOK\r\n" 或 "\r\nOK" + if (i + 1 < length && buffer[i] == '\r' && buffer[i+1] == '\n') { + if (i + 3 < length && buffer[i+2] == 'O' && buffer[i+3] == 'K') { + if (i + 4 == length || (i + 4 < length && buffer[i+4] == '\r')) { + // 找到第一个OK终止符,立即返回(流处理:找到第一个完整响应就处理) + end = (i + 4 < length && buffer[i+4] == '\r' && i + 5 < length && buffer[i+5] == '\n') ? + (i + 6) : (i + 4); + found_end = true; + break; // 立即退出循环,返回第一个完整响应 + } + } + } + + // 检查 "\r\nERROR\r\n" 或 "\r\nERROR"(标准格式,不是URC格式) + if (i + 1 < length && buffer[i] == '\r' && buffer[i+1] == '\n') { + if (i + 6 < length && strncmp(&buffer[i+2], "ERROR", 5) == 0) { + // 检查是否是URC格式的ERROR(如 +TCPCLOSE: ERROR) + // 注意:+TCPCLOSE: ERROR 中的ERROR前面是 ": ",不是 "\r\n" + // 所以如果ERROR前面是 "\r\n",说明是标准的ERROR响应 + if (i + 7 == length || (i + 7 < length && buffer[i+7] == '\r')) { + end = (i + 7 < length && buffer[i+7] == '\r' && i + 8 < length && buffer[i+8] == '\n') ? + (i + 9) : (i + 7); + found_end = true; + break; // 立即退出循环,返回第一个完整响应 + } + } + } + + // 检查 ">\r\n" 或 "> \r" 或 "> "(提示符) + // 格式可能是:\r\n>\r\n 或 \r\n> \r 或 \r\n> \r\n 等 + if (buffer[i] == '>') { + if (i == 0 || buffer[i-1] == '\n' || buffer[i-1] == '\r' || buffer[i-1] == ' ') { + // 检查">"后面的字符 + if (i + 1 == length) { + // ">"是最后一个字符 + end = i + 1; + found_end = true; + break; + } else if (buffer[i+1] == '\r') { + // ">\r" 或 ">\r\n" + if (i + 2 < length && buffer[i+2] == '\n') { + end = i + 3; // ">\r\n" + } else { + end = i + 2; // ">\r" + } + found_end = true; + break; + } else if (buffer[i+1] == '\n') { + // ">\n" + end = i + 2; + found_end = true; + break; + } else if (buffer[i+1] == ' ') { + // "> " 后面可能有 \r 或 \r\n + if (i + 2 < length && buffer[i+2] == '\r') { + if (i + 3 < length && buffer[i+3] == '\n') { + end = i + 4; // "> \r\n" + } else { + end = i + 3; // "> \r" + } + } else { + end = i + 2; // "> "(后面没有\r) + } + found_end = true; + break; + } else { + // ">"后面是其他字符,也认为是提示符(容错处理) + end = i + 1; + found_end = true; + break; + } + } + } + + // 检查 "\r\n+CME ERROR" 或 "\r\n+CMS ERROR" + if (i + 1 < length && buffer[i] == '\r' && buffer[i+1] == '\n') { + if (i + 11 < length && strncmp(&buffer[i+2], "+CME ERROR", 10) == 0) { + // 查找下一个 \r\n + uint32_t j = i + 12; + while (j + 1 < length && !(buffer[j] == '\r' && buffer[j+1] == '\n')) { + j++; + } + if (j + 1 < length) { + // 找到第一个+CME ERROR终止符,立即返回(流处理:找到第一个完整响应就处理) + end = j + 2; + found_end = true; + break; // 立即退出循环,返回第一个完整响应 + } + } else if (i + 11 < length && strncmp(&buffer[i+2], "+CMS ERROR", 10) == 0) { + // 查找下一个 \r\n + uint32_t j = i + 12; + while (j + 1 < length && !(buffer[j] == '\r' && buffer[j+1] == '\n')) { + j++; + } + if (j + 1 < length) { + // 找到第一个+CMS ERROR终止符,立即返回(流处理:找到第一个完整响应就处理) + end = j + 2; + found_end = true; + break; // 立即退出循环,返回第一个完整响应 + } + } + // 检查 "\r\nCONNECT\r\n" 或 "\r\nCONNECT" (如ATD*99#) + else if (i + 8 < length && strncmp(&buffer[i+2], "CONNECT", 7) == 0) { + if (i + 9 == length || (i + 9 < length && buffer[i+9] == '\r')) { + end = (i + 9 < length && buffer[i+9] == '\r' && i + 10 < length && buffer[i+10] == '\n') ? + (i + 11) : (i + 9); + found_end = true; + break; + } + } + // 检查 "\r\nNO CARRIER\r\n" 或 "\r\nNO CARRIER" + else if (i + 11 < length && strncmp(&buffer[i+2], "NO CARRIER", 10) == 0) { + if (i + 12 == length || (i + 12 < length && buffer[i+12] == '\r')) { + end = (i + 12 < length && buffer[i+12] == '\r' && i + 13 < length && buffer[i+13] == '\n') ? + (i + 14) : (i + 12); + found_end = true; + break; + } + } + // 检查 "\r\nBUSY\r\n" 或 "\r\nBUSY" + else if (i + 6 < length && strncmp(&buffer[i+2], "BUSY", 4) == 0) { + if (i + 6 == length || (i + 6 < length && buffer[i+6] == '\r')) { + end = (i + 6 < length && buffer[i+6] == '\r' && i + 7 < length && buffer[i+7] == '\n') ? + (i + 8) : (i + 6); + found_end = true; + break; + } + } + // 检查 "\r\nNO DIALTONE\r\n" 或 "\r\nNO DIALTONE" + else if (i + 12 < length && strncmp(&buffer[i+2], "NO DIALTONE", 11) == 0) { + if (i + 13 == length || (i + 13 < length && buffer[i+13] == '\r')) { + end = (i + 13 < length && buffer[i+13] == '\r' && i + 14 < length && buffer[i+14] == '\n') ? + (i + 15) : (i + 13); + found_end = true; + break; + } + } + // 检查 "\r\nGPRS DISCONNECTION\r\n" 或 "\r\nGPRS DISCONNECTION" (如AT+CGATT=0) + else if (i + 20 < length && strncmp(&buffer[i+2], "GPRS DISCONNECTION", 18) == 0) { + if (i + 20 == length || (i + 20 < length && buffer[i+20] == '\r')) { + end = (i + 20 < length && buffer[i+20] == '\r' && i + 21 < length && buffer[i+21] == '\n') ? + (i + 22) : (i + 20); + found_end = true; + break; + } + } + } + } + } + + osal_mutex_unlock(&g_at_accumulate_mutex); + + if (found_end) { + // ⚠️ 关键:验证 end 和 start 的合理性,防止计算错误 + if (end < start) { + PPP_DEBUG_ERROR("[提取完整响应] ⚠️ 计算错误:end < start! start=%u, end=%u, length=%u", + start, end, length); + osal_mutex_unlock(&g_at_accumulate_mutex); + return false; + } + + if (end > length) { + PPP_DEBUG_ERROR("[提取完整响应] ⚠️ 计算错误:end > length! start=%u, end=%u, length=%u", + start, end, length); + osal_mutex_unlock(&g_at_accumulate_mutex); + return false; + } + + uint32_t calculated_len = end - start; + + // ⚠️ 关键:验证计算出的长度是否合理(最大64KB) + const uint32_t MAX_REASONABLE_RESPONSE_LEN = 65536; + if (calculated_len > MAX_REASONABLE_RESPONSE_LEN) { + PPP_DEBUG_ERROR("[提取完整响应] ⚠️ 计算错误:response_len异常大!start=%u, end=%u, len=%u (max=%u)", + start, end, calculated_len, MAX_REASONABLE_RESPONSE_LEN); + osal_mutex_unlock(&g_at_accumulate_mutex); + return false; + } + + *response_start = start; + *response_len = calculated_len; + PPP_DEBUG_DEBUG("[提取完整响应] 找到完整响应: start=%u (跳过 %u 个\\x00), len=%u", + start, start, *response_len); + /* 调试:检查提取的响应开头是否还有 \x00 */ + if (start < length && buffer[start] == '\0') { + PPP_DEBUG_WARN("[提取完整响应] WARNING: 提取的响应开头仍然是 \\x00! start=%u, buffer[start]=0x%02X", + start, (unsigned char)buffer[start]); + } + return true; + } + + // 数据不完整 + PPP_DEBUG_DEBUG("[提取完整响应] 数据不完整,等待更多数据 (当前长度=%u)", length); + return false; +} + +/** + * @brief 改进的URC分类和分发(逐个函数判定) + * @param response 完整的AT响应 + * @param response_len 响应长度 + * @return true=已处理(URC),false=未识别(传给通用处理) + * + * 流程: + * 1. 调用 n706a_parse_tcprecv_urc() - 如果返回true,处理并返回 + * 2. 调用 n706a_parse_tcpsetup_urc() - 如果返回true,处理并返回 + * 3. 调用 n706a_parse_tcpclose_urc() - 如果返回true,处理并返回 + * 4. 调用 n706a_parse_tcpsend_urc() - 如果返回true,处理并返回 + * 5. 调用 n706a_parse_httprecv_urc() - 如果返回true,处理并返回 + * 6. 调用网络状态URC解析 - 如果返回true,处理并返回 + * 7. 返回false(未识别,传给通用处理) + */ +static bool n706a_classify_and_dispatch_urc_improved(const uint8_t *response, uint32_t response_len) +{ + if (!response || response_len == 0) { + return false; + } + + const char *response_str = (const char *)response; + + // 类别1:TCP数据接收URC(最高优先级) + if (response_len >= 10 && g_tcp_urc_callback) { + const char *tcprecv_pos = strstr(response_str, "+TCPRECV: "); + const char *ssltcprecv_pos = strstr(response_str, "+SSLTCPRECV: "); + + if (tcprecv_pos || ssltcprecv_pos) { + const char *urc_start = tcprecv_pos ? tcprecv_pos : ssltcprecv_pos; + size_t parse_len = response_len - (urc_start - response_str); + + int socket_id; + size_t data_len; + const uint8_t *tcp_data; + + if (n706a_parse_tcprecv_urc(urc_start, parse_len, &socket_id, &data_len, &tcp_data)) { + // 记录解析到的 socket_id,用于诊断 + PPP_DEBUG_INFO("[URC分类] Parsed +TCPRECV URC: socket_id=%d, data_len=%u, response_len=%u", + socket_id, (unsigned int)data_len, (unsigned int)response_len); + + // HEX解码处理(与原来的逻辑相同) + static uint8_t hex_decode_buffer_0[2048]; + static uint8_t hex_decode_buffer_1[2048]; + static volatile int hex_decode_buffer_active = 0; + + size_t binary_data_len = data_len; + const uint8_t *binary_data = tcp_data; + bool is_hex_format = false; + + if (data_len > 0 && g_tcp_recv_hex_mode) { + // HEX模式下,data_len是二进制数据长度,HEX字符串长度 = data_len × 2 + size_t hex_str_len = data_len * 2; + + // 检查实际HEX字符串长度(可能被截断) + size_t actual_hex_str_len = 0; + const char *p = (const char *)tcp_data; + while (actual_hex_str_len < hex_str_len && + ((*p >= '0' && *p <= '9') || (*p >= 'A' && *p <= 'F') || (*p >= 'a' && *p <= 'f'))) { + p++; + actual_hex_str_len++; + } + + // 如果实际HEX字符串长度足够(至少是期望长度,或者接近期望长度),尝试HEX解码 + if (actual_hex_str_len >= hex_str_len || (actual_hex_str_len > 0 && actual_hex_str_len >= hex_str_len - 1)) { + // 使用实际可用的HEX字符串长度(如果是奇数,截断最后一个字符) + size_t hex_len_to_decode = (actual_hex_str_len % 2 == 0) ? actual_hex_str_len : (actual_hex_str_len - 1); + + if (hex_len_to_decode > 0 && hex_len_to_decode % 2 == 0) { + is_hex_format = true; + int buffer_idx = hex_decode_buffer_active; + uint8_t *decode_buffer = (buffer_idx == 0) ? hex_decode_buffer_0 : hex_decode_buffer_1; + + binary_data_len = n706a_hex_string_to_binary((const char *)tcp_data, hex_len_to_decode, + decode_buffer, sizeof(hex_decode_buffer_0)); + if (binary_data_len > 0) { + binary_data = decode_buffer; + hex_decode_buffer_active = 1 - hex_decode_buffer_active; + if (hex_len_to_decode < hex_str_len) { + PPP_DEBUG_WARN("[URC分类] HEX data truncated: expected_hex_len=%u, actual_hex_len=%u, decoded=%u bytes", + (unsigned int)hex_str_len, (unsigned int)hex_len_to_decode, (unsigned int)binary_data_len); + } + } + } else { + PPP_DEBUG_WARN("[URC分类] HEX data length is invalid: expected_hex_len=%u, actual_hex_len=%u", + (unsigned int)hex_str_len, (unsigned int)actual_hex_str_len); + } + } else { + PPP_DEBUG_WARN("[URC分类] HEX data too short: expected_hex_len=%u, actual_hex_len=%u", + (unsigned int)hex_str_len, (unsigned int)actual_hex_str_len); + } + } + + if (binary_data_len > 0) { + // 检查响应中是否包含命令回显(如 "AT+TCPREAD=0,2047") + // 如果包含命令回显,说明这是 AT+TCPREAD 命令的响应,+TCPRECV URC 数据应该保留在响应中 + // 供 n706a_tcp_recv 函数提取,而不是通过异步 URC 回调处理 + // 这样可以确保即使 socket 未注册,也能从 AT+TCPREAD 响应中提取数据 + bool has_command_echo = (strstr(response_str, "AT+TCPREAD") != NULL || + strstr(response_str, "AT+SSLTCPREAD") != NULL); + + if (has_command_echo) { + // 这是 AT+TCPREAD 命令的响应,+TCPRECV URC 数据应该保留在响应中 + // 不通过异步 URC 回调处理,而是让 n706a_tcp_recv 从响应中提取 + PPP_DEBUG_INFO("[URC分类] +TCPRECV URC 在 AT+TCPREAD 响应中,保留在响应中供 n706a_tcp_recv 提取: URC_socket_id=%d, len=%u", + socket_id, (unsigned int)binary_data_len); + PPP_DEBUG_INFO("[URC分类] Note: n706a_tcp_recv will verify socket_id match before extracting data"); + // 不调用 g_tcp_urc_callback,不返回 true,继续处理响应 + } else { + // 这是纯异步 URC,通过回调处理 + PPP_DEBUG_INFO("[URC分类] Pure async +TCPRECV URC, calling callback: socket_id=%d, len=%u", + socket_id, (unsigned int)binary_data_len); + // 注意:socket 注册检查在 n706a_tcp_on_urc_received 回调函数中进行 + // 这里直接调用回调,让回调函数自己处理未注册的情况 + g_tcp_urc_callback(socket_id, binary_data, binary_data_len); + PPP_DEBUG_INFO("[URC分类] TCP URC已处理: socket=%d, len=%u", socket_id, (unsigned int)binary_data_len); + return true; // 已处理,清除数据 + } + } + } + } + } + + // 类别2:TCP连接状态URC + if (response_len >= 11) { + const char *tcpsetup_pos = strstr(response_str, "+TCPSETUP: "); + const char *ssltcpsetup_pos = strstr(response_str, "+SSLTCPSETUP: "); + + if (tcpsetup_pos || ssltcpsetup_pos) { + const char *urc_start = tcpsetup_pos ? tcpsetup_pos : ssltcpsetup_pos; + const char *urc_end = strstr(urc_start, "\r\n"); + if (!urc_end) { + urc_end = strstr(urc_start, "\n"); + } + if (!urc_end) { + urc_end = response_str + response_len; + } + + size_t urc_len = urc_end - urc_start; + if (urc_len >= 15) { + int socket_id; + bool success; + if (n706a_parse_tcpsetup_urc(urc_start, urc_len, &socket_id, &success)) { + if (g_tcpsetup_urc_callback) { + g_tcpsetup_urc_callback(socket_id, success); + } + PPP_DEBUG_INFO("[URC分类] +TCPSETUP URC已处理: socket=%d, success=%d", socket_id, success); + return true; // 已处理,清除数据 + } + } + } + + // +TCPCLOSE + const char *tcpclose_pos = strstr(response_str, "+TCPCLOSE: "); + const char *ssltcpclose_pos = strstr(response_str, "+SSLTCPCLOSE: "); + + if (tcpclose_pos || ssltcpclose_pos) { + const char *urc_start = tcpclose_pos ? tcpclose_pos : ssltcpclose_pos; + const char *urc_end = strstr(urc_start, "\r\n"); + if (!urc_end) { + urc_end = strstr(urc_start, "\n"); + } + if (!urc_end) { + urc_end = response_str + response_len; + } + + size_t urc_len = urc_end - urc_start; + int socket_id; + if (n706a_parse_tcpclose_urc(urc_start, urc_len, &socket_id)) { + if (g_tcpclose_urc_callback) { + g_tcpclose_urc_callback(socket_id); + } + PPP_DEBUG_INFO("[URC分类] +TCPCLOSE URC已处理: socket=%d", socket_id); + return true; // 已处理,清除数据 + } + } + } + + // 类别3:TCP发送结果URC + if (response_len >= 10) { + const char *tcpsend_pos = strstr(response_str, "+TCPSEND: "); + const char *ssltcpend_pos = strstr(response_str, "+SSLTCPEND: "); + + if (tcpsend_pos || ssltcpend_pos) { + const char *urc_start = tcpsend_pos ? tcpsend_pos : ssltcpend_pos; + + // 关键改进:检查URC前面是否有命令回显(AT+) + // 如果URC前面有命令回显,说明这是命令响应的一部分,不是纯异步URC + // 纯异步URC应该以 \r\n 开头,或者直接以 + 开头(没有AT+回显) + bool is_pure_async_urc = true; + if (urc_start > response_str) { + // 检查URC前面是否有 "AT+" 字符串(命令回显) + const char *before_urc = urc_start - 1; + // 向前查找,看是否有 "AT+" 在同一个响应块中 + while (before_urc >= response_str && (*before_urc == ' ' || *before_urc == '\r' || *before_urc == '\n')) { + before_urc--; + } + // 检查是否以 "AT+" 结尾(说明前面有命令回显) + if (before_urc >= response_str + 2) { + if (before_urc[-2] == 'A' && before_urc[-1] == 'T' && *before_urc == '+') { + // 找到命令回显,这不是纯异步URC + is_pure_async_urc = false; + PPP_DEBUG_DEBUG("[URC分类] +TCPSEND URC前面有命令回显,可能是命令响应的一部分"); + } + } + } + + const char *urc_end = strstr(urc_start, "\r\n"); + if (!urc_end) { + urc_end = strstr(urc_start, "\n"); + } + if (!urc_end) { + // 如果没有找到换行符,尝试查找下一个URC或响应结束 + // 对于 +TCPSEND: , 格式,至少需要找到逗号后的数字 + const char *colon = strstr(urc_start, ": "); + if (colon) { + const char *comma = strstr(colon + 2, ","); + if (comma) { + // 找到逗号,尝试找到数字的结束位置 + const char *num_end = comma + 1; + while (num_end < response_str + response_len && + *num_end >= '0' && *num_end <= '9') { + num_end++; + } + // 如果找到数字结束位置,使用它作为URC结束位置 + if (num_end < response_str + response_len) { + urc_end = num_end; + } else { + urc_end = response_str + response_len; + } + } else { + urc_end = response_str + response_len; + } + } else { + urc_end = response_str + response_len; + } + } + + size_t urc_len = urc_end - urc_start; + // 调试:打印URC内容 + if (urc_len < 100) { + char urc_preview[101] = {0}; + size_t preview_len = (urc_len < 100) ? urc_len : 100; + for (size_t i = 0; i < preview_len && i < sizeof(urc_preview) - 1; i++) { + urc_preview[i] = (urc_start[i] >= 32 && urc_start[i] < 127) ? urc_start[i] : '.'; + } + PPP_DEBUG_INFO("[URC分类] Attempting to parse +TCPSEND URC: len=%u, preview=[%.*s]", + (unsigned int)urc_len, (int)preview_len, urc_preview); + } + int socket_id; + size_t sent_bytes; + if (n706a_parse_tcpsend_urc(urc_start, urc_len, &socket_id, &sent_bytes)) { + PPP_DEBUG_INFO("[URC分类] +TCPSEND URC已处理: socket=%d, sent=%u (pure_async=%d)", + socket_id, (unsigned int)sent_bytes, is_pure_async_urc); + + // 如果是纯异步URC(没有命令回显),立即处理并返回 + // 如果是命令响应中的URC(有命令回显),需要检查是否还有其他内容 + if (is_pure_async_urc) { + // 纯异步URC,立即处理 + if (g_tcpsend_urc_callback) { + g_tcpsend_urc_callback(socket_id, sent_bytes); + } + // 如果是独立URC(整个响应块就是这个URC),不写入缓冲区 + if (urc_start == response_str || (urc_start > response_str && + (urc_start[-1] == '\r' || urc_start[-1] == '\n'))) { + // URC前面只有空白字符或换行符,是独立的异步URC + if (urc_end >= response_str + response_len - 2) { + return true; // 独立URC,已处理,不写入缓冲区 + } + } + } else { + // 命令响应中的URC,也需要处理回调,但继续写入缓冲区 + if (g_tcpsend_urc_callback) { + g_tcpsend_urc_callback(socket_id, sent_bytes); + } + // 继续处理,让后续逻辑写入缓冲区 + } + } + } + } + + // 类别4:HTTP响应URC + if (response_len >= 11) { + const char *httprecv_pos = strstr(response_str, "+HTTPRECV: "); + const char *httpsrecv_pos = strstr(response_str, "+HTTPSRECV: "); + + if (httprecv_pos || httpsrecv_pos) { + const char *urc_start = httprecv_pos ? httprecv_pos : httpsrecv_pos; + bool is_https = (httpsrecv_pos != NULL); + + size_t data_len; + const uint8_t *http_data; + + size_t declared_len, actual_len; + if (n706a_parse_httprecv_urc(urc_start, response_len - (urc_start - response_str), &declared_len, &actual_len, &http_data)) { + extern void n706a_http_on_urc_received(const uint8_t *data, size_t length, bool is_https); + n706a_http_on_urc_received(http_data, actual_len, is_https); + PPP_DEBUG_INFO("[URC分类] HTTP URC已处理: %s, declared_len=%u, actual_len=%u", + is_https ? "+HTTPSRECV" : "+HTTPRECV", (unsigned int)declared_len, (unsigned int)actual_len); + return true; // 已处理,清除数据 + } + } + } + + // 类别5:网络状态URC + if (response_len >= 6 && g_network_status_urc_callback) { + const char *creg_pos = strstr(response_str, "+CREG: "); + const char *cgreg_pos = strstr(response_str, "+CGREG: "); + const char *cgatt_pos = strstr(response_str, "+CGATT: "); + + const char *urc_start = NULL; + const char *urc_type = NULL; + + if (creg_pos) { + urc_start = creg_pos; + urc_type = "+CREG"; + } else if (cgreg_pos) { + urc_start = cgreg_pos; + urc_type = "+CGREG"; + } else if (cgatt_pos) { + urc_start = cgatt_pos; + urc_type = "+CGATT"; + } + + if (urc_start) { + const char *colon = strchr(urc_start, ':'); + if (colon) { + const char *urc_data_start = colon + 2; + const char *urc_end = strstr(urc_data_start, "\r\n"); + if (!urc_end) { + urc_end = strstr(urc_data_start, "\n"); + } + if (!urc_end) { + urc_end = response_str + response_len; + } + + size_t urc_data_len = urc_end - urc_data_start; + g_network_status_urc_callback(urc_type, urc_data_start, urc_data_len); + PPP_DEBUG_INFO("[URC分类] 网络状态URC已处理: %s", urc_type); + return true; // 已处理,清除数据 + } + } + } + + // 未识别为URC,返回false,传给通用处理 + return false; +} + +/** + * @brief 写入AT响应数据到环形缓冲区(由ppp_interrupt.c调用) + * + * 改进的处理流程: + * 1. 将新数据追加到累积缓冲区 + * 2. 循环提取完整的AT指令响应(有正确的头和尾) + * 3. 逐个函数判定,匹配就处理并清除,不匹配就传给下一个 + * 4. 最后都不匹配,进入通用AT指令处理 + * 5. 继续处理剩余数据(支持流式处理多个响应) + * + * @param data 数据指针 + * @param length 数据长度 + */ +void n706a_at_response_buffer_write(const uint8_t *data, uint32_t length) +{ + if (!data || length == 0) { + return; + } + + PPP_DEBUG_DEBUG("[AT响应缓冲区] 接收到数据块: length=%u", length); + +#if USE_STREAM_PARSER + /* 使用流式解析器(新方案) */ + n706a_stream_parser_process_data(data, length); + return; /* 新方案直接处理,不需要后续的旧逻辑 */ +#else + /* 使用旧方案(保持向后兼容) */ + + /* ========== 第一步:将新数据追加到累积缓冲区 ========== */ + osal_mutex_lock(&g_at_accumulate_mutex); + + // 检查累积缓冲区超时(如果数据超过5秒还没被识别,清除) + unsigned long long current_jiffies = osal_get_jiffies(); + if (g_at_accumulate_length > 0 && g_at_accumulate_last_update_time > 0) { + unsigned long long elapsed_jiffies = current_jiffies - g_at_accumulate_last_update_time; + uint32_t elapsed_ms = osal_jiffies_to_msecs((unsigned int)elapsed_jiffies); + if (elapsed_ms > N706A_AT_ACCUMULATE_TIMEOUT_MS) { + /* 打印最后几个字节的十六进制,帮助分析问题 */ + uint8_t last_bytes[16] = {0}; + uint32_t preview_len = (g_at_accumulate_length < 16) ? g_at_accumulate_length : 16; + if (g_at_accumulate_length > 0 && preview_len > 0) { + uint32_t start_offset = (g_at_accumulate_length > preview_len) ? (g_at_accumulate_length - preview_len) : 0; + memcpy(last_bytes, g_at_accumulate_buffer + start_offset, preview_len); + } + char hex_str[48] = {0}; + for (uint32_t i = 0; i < preview_len; i++) { + snprintf(hex_str + i * 3, 4, "%02X ", last_bytes[i]); + } + PPP_DEBUG_WARN("[累积缓冲区] 超时清除无效数据: %u 字节 (超时: %u ms), 最后 %u 字节: %s", + g_at_accumulate_length, elapsed_ms, preview_len, hex_str); + g_at_accumulate_length = 0; + g_at_accumulate_last_update_time = 0; + } + } + + // 检查缓冲区空间 + uint32_t available_space = N706A_AT_ACCUMULATE_BUFFER_SIZE - g_at_accumulate_length; + if (length > available_space) { + // 缓冲区空间不足,丢弃最老的数据 + uint32_t overflow = length - available_space; + if (overflow < g_at_accumulate_length) { + // 移除最老的数据 + memmove(g_at_accumulate_buffer, + g_at_accumulate_buffer + overflow, + g_at_accumulate_length - overflow); + g_at_accumulate_length -= overflow; + PPP_DEBUG_WARN("[累积缓冲区] 空间不足,丢弃最老的 %u 字节", overflow); + } else { + // 缓冲区完全清空 + g_at_accumulate_length = 0; + PPP_DEBUG_WARN("[累积缓冲区] 空间不足,清空缓冲区"); + } + } + + // 追加新数据 + if (g_at_accumulate_length + length <= N706A_AT_ACCUMULATE_BUFFER_SIZE) { + memcpy(g_at_accumulate_buffer + g_at_accumulate_length, data, length); + g_at_accumulate_length += length; + g_at_accumulate_last_update_time = current_jiffies; // 更新最后更新时间(tick数) + + // 打印接收到的数据的前几个字节(用于调试+TCPSEND URC问题) + if (length > 0) { + char hex_preview[64] = {0}; + uint32_t preview_len = (length < 16) ? length : 16; + for (uint32_t i = 0; i < preview_len; i++) { + snprintf(hex_preview + i * 3, 4, "%02X ", data[i]); + } + // 检查是否包含+TCPSEND相关数据 + const char *data_str = (const char *)data; + if (strstr(data_str, "TCPSEND") != NULL || strstr(data_str, "TCPSETUP") != NULL) { + PPP_DEBUG_INFO("[累积缓冲区] 追加 %u 字节,总长度=%u,数据预览: %s (包含TCP相关URC)", + length, g_at_accumulate_length, hex_preview); + } else { + PPP_DEBUG_DEBUG("[累积缓冲区] 追加 %u 字节,总长度=%u,数据预览: %s", + length, g_at_accumulate_length, hex_preview); + } + } else { + PPP_DEBUG_DEBUG("[累积缓冲区] 追加 %u 字节,总长度=%u", length, g_at_accumulate_length); + } + } else { + PPP_DEBUG_ERROR("[累积缓冲区] 追加失败:空间不足 (当前=%u, 需要=%u)", + g_at_accumulate_length, length); + } + + osal_mutex_unlock(&g_at_accumulate_mutex); + + /* ========== 第二步:循环提取和处理完整的AT响应 ========== */ + while (true) { + uint32_t response_start = 0; + uint32_t response_len = 0; + + // 提取完整的AT响应 + if (!n706a_extract_complete_response(&response_start, &response_len)) { + // 数据不完整,等待更多数据 + break; + } + + // 检查是否有无效数据需要清除(用户方法:如果第一个完整响应的起始位置不是0,清除前面的无效数据) + if (response_start > 0) { + // 检查 response_start 之前是否有非空白字符(无效数据) + const char *buffer = (const char *)g_at_accumulate_buffer; + bool has_invalid_data = false; + for (uint32_t i = 0; i < response_start; i++) { + if (buffer[i] != '\r' && buffer[i] != '\n' && buffer[i] != ' ') { + // 发现非空白字符,说明前面有无效数据 + has_invalid_data = true; + break; + } + } + + if (has_invalid_data) { + // 清除前面的无效数据(从0到response_start) + PPP_DEBUG_WARN("[累积缓冲区] 检测到开头无效数据,清除 %u 字节", response_start); + n706a_remove_processed_data(response_start); + // 重新计算 response_start(现在应该是0,因为前面的无效数据已被清除) + response_start = 0; + } + } + + // 获取响应数据指针 + const uint8_t *response = g_at_accumulate_buffer + response_start; + + PPP_DEBUG_DEBUG("[AT响应处理] 提取到完整响应: start=%u, len=%u", response_start, response_len); + /* 调试:检查响应开头是否还有 \x00 */ + if (response_len > 0 && response[0] == '\0') { + PPP_DEBUG_WARN("[AT响应处理] WARNING: 响应开头仍然是 \\x00! response_start=%u, response[0]=0x%02X", + response_start, (unsigned char)response[0]); + } + + /* ========== 第三步:逐个函数判定 ========== */ + bool is_urc = n706a_classify_and_dispatch_urc_improved(response, response_len); + + if (is_urc) { + // URC已处理,但需要检查响应中是否还包含OK(AT命令响应) + // 例如:AT+TCPSETUP的响应可能是:\r\n+TCPSETUP: 0,OK\r\nOK\r\n + // 这种情况下,URC被处理了,但OK响应需要写入通用缓冲区 + // + // 注意:需要区分URC中的"OK"(如+TCPSETUP: 0,OK)和独立的OK响应 + // 独立的OK响应格式:\r\nOK\r\n 或 \r\nOK(响应结尾) + const char *response_str = (const char *)response; + bool has_ok = false; + bool has_command_echo = false; // 检查是否包含命令回显(说明是AT命令的响应,不是异步URC) + bool is_command_response_urc = false; // 检查是否是命令响应中的URC(如+TCPCLOSE: ERR或+TCPCLOSE: 0,OK) + + // 检查是否是命令响应中的URC(这些URC格式通常表示对命令的响应,而不是异步URC) + // 例如:+TCPCLOSE: ERR, +TCPCLOSE: 0,OK 等 + // 注意:+TCPSETUP: 0,OK 不应该作为命令响应处理,因为它是异步URC + // 只有 +TCPCLOSE 和 +SSLTCPCLOSE 的响应格式才作为命令响应处理 + const char *command_response_urc_patterns[] = { + "+TCPCLOSE: ERR", "+TCPCLOSE: 0,OK", "+TCPCLOSE: ERROR", + "+SSLTCPCLOSE: ERR", "+SSLTCPCLOSE: 0,OK", "+SSLTCPCLOSE: ERROR" + }; + for (size_t k = 0; k < sizeof(command_response_urc_patterns) / sizeof(command_response_urc_patterns[0]); k++) { + if (strstr(response_str, command_response_urc_patterns[k]) != NULL) { + is_command_response_urc = true; + PPP_DEBUG_DEBUG("[AT响应处理] 检测到命令响应URC格式: %s,这是AT命令的响应", command_response_urc_patterns[k]); + break; + } + } + + // 特殊处理:+TCPSETUP 和 +SSLTCPSETUP 是异步URC,不应该作为命令响应 + // 这些URC应该通过信号量机制处理,而不是写入通用AT响应缓冲区 + // 即使响应中包含命令回显(可能是之前命令的回显),也不应该作为命令响应处理 + bool is_tcpsetup_urc = false; + if (strstr(response_str, "+TCPSETUP: ") != NULL || + strstr(response_str, "+SSLTCPSETUP: ") != NULL) { + is_tcpsetup_urc = true; + // 这些是异步URC,不应该写入通用AT响应缓冲区 + PPP_DEBUG_DEBUG("[AT响应处理] +TCPSETUP/+SSLTCPSETUP URC是异步URC,不写入通用AT响应缓冲区"); + is_command_response_urc = false; // 确保不作为命令响应处理 + has_command_echo = false; // 即使有命令回显,也不作为命令响应处理 + } + + // 查找所有可能的URC位置(用于排除URC中的OK) + const char *urc_markers[] = { + "+TCPRECV: ", "+SSLTCPRECV: ", + "+TCPSETUP: ", "+SSLTCPSETUP: ", + "+TCPCLOSE: ", "+SSLTCPCLOSE: ", + "+TCPSEND: ", "+SSLTCPEND: ", + "+HTTPRECV: ", "+HTTPSRECV: ", + "+CREG: ", "+CGREG: ", "+CGATT: " + }; + const char *urc_start_pos = NULL; + const char *urc_end_pos = NULL; + + // 找到第一个URC的位置 + for (size_t j = 0; j < sizeof(urc_markers) / sizeof(urc_markers[0]); j++) { + const char *pos = strstr(response_str, urc_markers[j]); + if (pos && (!urc_start_pos || pos < urc_start_pos)) { + urc_start_pos = pos; + // 找到URC的结束位置(下一个\r\n或\r\r) + urc_end_pos = strstr(urc_start_pos, "\r\n"); + if (!urc_end_pos) { + urc_end_pos = strstr(urc_start_pos, "\r\r"); + } + if (!urc_end_pos) { + urc_end_pos = strstr(urc_start_pos, "\n"); + } + if (!urc_end_pos) { + urc_end_pos = response_str + response_len; + } else { + urc_end_pos += 2; // 包含\r\n或\r\r + } + } + } + + // 检查响应中是否包含命令回显(如 "AT+TCPCLOSE=0\r\n") + // 如果包含命令回显,说明这是AT命令的响应,而不是异步URC + // 常见的命令回显格式:AT+XXX\r\n 或 AT+XXX=...\r\n + // 但是,+TCPSETUP URC是异步的,即使有命令回显也不应该作为命令响应处理 + if (!is_tcpsetup_urc && strstr(response_str, "AT+") != NULL) { + has_command_echo = true; + PPP_DEBUG_DEBUG("[AT响应处理] 检测到命令回显,这是AT命令的响应,不是异步URC"); + } + + // 检查响应中是否包含独立的OK(不在URC中) + // 查找 "\r\nOK\r\n" 或 "\r\nOK"(响应结尾) + for (uint32_t i = 0; i + 1 < response_len; i++) { + if (response_str[i] == '\r' && response_str[i+1] == '\n') { + if (i + 3 < response_len && + response_str[i+2] == 'O' && response_str[i+3] == 'K') { + // 检查OK后面是否是结束符或换行符 + if (i + 4 >= response_len || + response_str[i+4] == '\r' || response_str[i+4] == '\n') { + // 检查这个OK是否在URC范围内 + const char *ok_pos = response_str + i + 2; // OK的起始位置 + if (!urc_start_pos || ok_pos < urc_start_pos || ok_pos >= urc_end_pos) { + // OK不在URC范围内,是独立的OK响应 + has_ok = true; + break; + } + // OK在URC范围内(如+TCPSETUP: 0,OK),跳过 + } + } + } + } + + // 特殊处理:如果响应包含 +TCPRECV 或 +SSLTCPRECV URC,且响应长度超过环形缓冲区大小 + // 这种情况下,响应应该直接从累积缓冲区提取(n706a_tcp_recv 会处理),而不是写入环形缓冲区 + // 因为环形缓冲区只有 4KB,无法容纳包含大量 HEX 数据的响应 + bool is_tcprecv_urc = (strstr(response_str, "+TCPRECV: ") != NULL || + strstr(response_str, "+SSLTCPRECV: ") != NULL); + bool response_too_large = (response_len > N706A_AT_RESPONSE_RING_BUFFER_SIZE); + + // 如果包含命令回显、独立的OK,或者是命令响应中的URC,说明这是AT命令的响应,需要写入通用缓冲区 + // 但是,+TCPSETUP URC是异步的,即使有命令回显也不应该作为命令响应处理 + // 如果响应包含 +TCPRECV URC 且响应太大,不应该写入环形缓冲区(会导致溢出),应该直接从累积缓冲区提取 + if (!is_tcpsetup_urc && (has_ok || has_command_echo || is_command_response_urc)) { + // 检查是否是包含 +TCPRECV URC 的大响应 + if (is_tcprecv_urc && response_too_large) { + // 这是包含 +TCPRECV URC 的大响应(如 AT+TCPREAD 响应),不应该写入环形缓冲区 + // n706a_tcp_recv 会直接从累积缓冲区提取数据 + PPP_DEBUG_INFO("[AT响应处理] 响应包含 +TCPRECV URC 且响应太大 (%u bytes > %u bytes),不写入环形缓冲区,由 n706a_tcp_recv 直接从累积缓冲区提取", + response_len, N706A_AT_RESPONSE_RING_BUFFER_SIZE); + } else { + // 响应中包含OK、命令回显或命令响应URC,需要写入通用AT响应缓冲区 + if (has_ok) { + PPP_DEBUG_DEBUG("[AT响应处理] URC已处理,但响应中包含OK,写入通用AT响应缓冲区"); + } else if (has_command_echo) { + PPP_DEBUG_DEBUG("[AT响应处理] URC已处理,但响应中包含命令回显(AT命令响应),写入通用AT响应缓冲区"); + } else if (is_command_response_urc) { + PPP_DEBUG_DEBUG("[AT响应处理] URC已处理,但这是命令响应中的URC(如+TCPCLOSE: ERR),写入通用AT响应缓冲区"); + } + + osal_mutex_lock(&g_at_response_mutex); + + // 检查响应大小,如果仍然太大,只写入部分数据(避免溢出) + uint32_t available_space = (g_at_response_head >= g_at_response_tail) ? + (N706A_AT_RESPONSE_RING_BUFFER_SIZE - (g_at_response_head - g_at_response_tail) - 1) : + (g_at_response_tail - g_at_response_head - 1); + + uint32_t write_len = (response_len <= available_space) ? response_len : available_space; + if (write_len < response_len) { + PPP_DEBUG_WARN("[AT响应处理] 响应太大 (%u bytes),环形缓冲区空间不足 (%u bytes available),只写入前 %u 字节", + response_len, available_space, write_len); + } + + for (uint32_t i = 0; i < write_len; i++) { + g_at_response_ring_buffer[g_at_response_head] = response[i]; + g_at_response_head = (g_at_response_head + 1) % N706A_AT_RESPONSE_RING_BUFFER_SIZE; + + // 检查溢出(缓冲区满时丢弃最老的数据) + if (g_at_response_head == g_at_response_tail) { + g_at_response_tail = (g_at_response_tail + 1) % N706A_AT_RESPONSE_RING_BUFFER_SIZE; + PPP_DEBUG_WARN("AT response buffer overflow, dropping oldest byte"); + } + } + + osal_mutex_unlock(&g_at_response_mutex); + + // 通知等待线程有新数据 + osal_sem_up(&g_at_response_sem); + } + } else { + // 纯URC,没有OK响应 + PPP_DEBUG_DEBUG("[AT响应处理] URC已处理(纯URC,无OK响应)"); + } + + // 清除数据 + n706a_remove_processed_data(response_start + response_len); + // 继续处理剩余数据 + continue; + } + + /* ========== 第四步:通用AT指令处理 ========== */ + // 未识别为URC,写入环形缓冲区供AT命令响应读取 + PPP_DEBUG_DEBUG("[AT响应处理] 未识别为URC,写入通用AT响应缓冲区"); + + // 检查响应大小,如果太大,只写入部分数据(避免溢出) + osal_mutex_lock(&g_at_response_mutex); + + uint32_t available_space = (g_at_response_head >= g_at_response_tail) ? + (N706A_AT_RESPONSE_RING_BUFFER_SIZE - (g_at_response_head - g_at_response_tail) - 1) : + (g_at_response_tail - g_at_response_head - 1); + + uint32_t write_len = (response_len <= available_space) ? response_len : available_space; + if (write_len < response_len) { + PPP_DEBUG_WARN("[AT响应处理] 响应太大 (%u bytes),环形缓冲区空间不足 (%u bytes available),只写入前 %u 字节", + response_len, available_space, write_len); + } + + for (uint32_t i = 0; i < write_len; i++) { + g_at_response_ring_buffer[g_at_response_head] = response[i]; + g_at_response_head = (g_at_response_head + 1) % N706A_AT_RESPONSE_RING_BUFFER_SIZE; + + // 检查溢出(缓冲区满时丢弃最老的数据) + if (g_at_response_head == g_at_response_tail) { + g_at_response_tail = (g_at_response_tail + 1) % N706A_AT_RESPONSE_RING_BUFFER_SIZE; + PPP_DEBUG_WARN("AT response buffer overflow, dropping oldest byte"); + } + } + + osal_mutex_unlock(&g_at_response_mutex); + + // 通知等待线程有新数据 + osal_sem_up(&g_at_response_sem); + + // 清除已处理的数据(从0到response_start+response_len) + n706a_remove_processed_data(response_start + response_len); + + // 继续处理剩余数据 + } + + PPP_DEBUG_DEBUG("[AT响应缓冲区] 处理完成,累积缓冲区剩余 %u 字节", g_at_accumulate_length); +#endif /* USE_STREAM_PARSER */ +} + +/** + * @brief 初始化AT命令处理模块 + */ +ppp_error_t ppp_at_handler_init(uint8_t uart_bus, uint32_t current_baudrate, bool auto_baudrate_mode) +{ + if (g_at_ctx.initialized) { + PPP_DEBUG_WARN("AT handler already initialized"); + return PPP_ERR_OK; + } + + g_at_ctx.uart_bus = uart_bus; + g_at_ctx.current_baudrate = current_baudrate; + g_at_ctx.auto_baudrate_mode = auto_baudrate_mode; + + /* 创建AT命令互斥锁 */ + if (osal_mutex_init(&g_at_ctx.at_mutex) != OSAL_SUCCESS) { + PPP_DEBUG_ERROR("Failed to create AT mutex"); + return PPP_ERR_MEMORY; + } + + /* 创建AT响应缓冲区互斥锁 */ + if (osal_mutex_init(&g_at_response_mutex) != OSAL_SUCCESS) { + PPP_DEBUG_ERROR("Failed to create AT response mutex"); + osal_mutex_destroy(&g_at_ctx.at_mutex); + return PPP_ERR_MEMORY; + } + + /* 创建AT响应信号量 */ + if (osal_sem_init(&g_at_response_sem, 0) != OSAL_SUCCESS) { + PPP_DEBUG_ERROR("Failed to create AT response semaphore"); + osal_mutex_destroy(&g_at_ctx.at_mutex); + osal_mutex_destroy(&g_at_response_mutex); + return PPP_ERR_MEMORY; + } + + /* 创建累积缓冲区互斥锁 */ + if (osal_mutex_init(&g_at_accumulate_mutex) != OSAL_SUCCESS) { + PPP_DEBUG_ERROR("Failed to create AT accumulate mutex"); + osal_mutex_destroy(&g_at_ctx.at_mutex); + osal_mutex_destroy(&g_at_response_mutex); + osal_sem_destroy(&g_at_response_sem); + return PPP_ERR_MEMORY; + } + + /* 初始化环形缓冲区 */ + g_at_response_head = 0; + g_at_response_tail = 0; + memset(g_at_response_ring_buffer, 0, sizeof(g_at_response_ring_buffer)); + + /* 初始化累积缓冲区 */ + g_at_accumulate_length = 0; + g_at_accumulate_last_update_time = 0; + memset(g_at_accumulate_buffer, 0, sizeof(g_at_accumulate_buffer)); + +#if USE_STREAM_PARSER + /* 初始化流式解析器(新方案) */ + n706a_stream_parser_init(); + PPP_DEBUG_INFO("Stream parser initialized (new scheme)"); +#else + PPP_DEBUG_INFO("Using legacy parser (old scheme)"); +#endif + + /* 分配全局AT响应缓存 */ + g_at_response_buffer = (uint8_t *)malloc(g_at_buffer_size); + if (g_at_response_buffer == NULL) { + PPP_DEBUG_ERROR("Failed to allocate AT response buffer"); + osal_mutex_destroy(&g_at_ctx.at_mutex); + osal_mutex_destroy(&g_at_response_mutex); + osal_sem_destroy(&g_at_response_sem); + osal_mutex_destroy(&g_at_accumulate_mutex); + return PPP_ERR_MEMORY; + } + + /* 分配AT命令解析临时缓冲区(避免频繁malloc/free,但只在初始化时分配一次) */ + g_at_cmd_buffer = (char *)malloc(N706A_AT_CMD_MAX_LEN + 4); + g_at_printable_buffer = (char *)malloc(PPP_AT_RESPONSE_MAX_LEN + 100); + g_at_command_response_buffer = (char *)malloc(512); + + if (!g_at_cmd_buffer || !g_at_printable_buffer || !g_at_command_response_buffer) { + PPP_DEBUG_ERROR("Failed to allocate AT parsing buffers"); + // 清理已分配的内存 + if (g_at_cmd_buffer) { + free(g_at_cmd_buffer); + g_at_cmd_buffer = NULL; + } + if (g_at_printable_buffer) { + free(g_at_printable_buffer); + g_at_printable_buffer = NULL; + } + if (g_at_command_response_buffer) { + free(g_at_command_response_buffer); + g_at_command_response_buffer = NULL; + } + if (g_at_response_buffer) { + free(g_at_response_buffer); + g_at_response_buffer = NULL; + } + osal_mutex_destroy(&g_at_ctx.at_mutex); + osal_mutex_destroy(&g_at_response_mutex); + osal_sem_destroy(&g_at_response_sem); + osal_mutex_destroy(&g_at_accumulate_mutex); + return PPP_ERR_MEMORY; + } + + PPP_DEBUG_INFO("Allocated AT parsing buffers: cmd=%u, printable=%u, response=%u bytes (total ~1.9KB)", + (unsigned int)(N706A_AT_CMD_MAX_LEN + 4), + (unsigned int)(PPP_AT_RESPONSE_MAX_LEN + 100), + (unsigned int)512); + + g_at_ctx.initialized = true; + PPP_DEBUG_INFO("n706a_net AT handler initialized on UART%d with async ring buffer", uart_bus); + + return PPP_ERR_OK; +} + +/** + * @brief 反初始化AT命令处理模块 + */ +void ppp_at_handler_deinit(void) +{ + if (!g_at_ctx.initialized) { + return; + } + + /* 销毁互斥锁和信号量 */ + osal_mutex_destroy(&g_at_ctx.at_mutex); + osal_mutex_destroy(&g_at_response_mutex); + osal_sem_destroy(&g_at_response_sem); + osal_mutex_destroy(&g_at_accumulate_mutex); + + /* 释放全局AT响应缓存 */ + if (g_at_response_buffer) { + free(g_at_response_buffer); + g_at_response_buffer = NULL; + } + + /* 释放AT命令解析临时缓冲区 */ + if (g_at_cmd_buffer) { + free(g_at_cmd_buffer); + g_at_cmd_buffer = NULL; + } + if (g_at_printable_buffer) { + free(g_at_printable_buffer); + g_at_printable_buffer = NULL; + } + if (g_at_command_response_buffer) { + free(g_at_command_response_buffer); + g_at_command_response_buffer = NULL; + } + + /* 清空环形缓冲区 */ + g_at_response_head = 0; + g_at_response_tail = 0; + + memset_s(&g_at_ctx, sizeof(g_at_ctx), 0, sizeof(g_at_ctx)); + PPP_DEBUG_INFO("n706a_net AT handler deinitialized"); +} + +/** + * @brief 执行波特率自适应握手 + * + * 进入自适应波特率,证明1000000没有正常工作,需要先reset,等5秒,开始设置自动握手的第一个波特率, + * 读取所有缓存数据丢掉,下达at指令,等待结果,重试3次,如果不成功,就设置第二个波特率,读取所有缓存数据丢掉, + * 下达at指令,等结果,重试3次,直至第三个波特率检测。如果都没有成功,则返回失败。 + * 如果任何一次at指令返回正确ok,直接在当前设置下,下达指 AT+IPR 配置 1000000 波特率,并返回成功。 + * 设置当前波特率为1000000,再次reset 模组,让其自动工作在 1000000波特率下。 + */ +ppp_error_t ppp_at_baudrate_handshake(void) +{ + if (!g_at_ctx.initialized) { + return PPP_ERR_INVALID_PARAM; + } + + PPP_DEBUG_INFO("Starting baudrate handshake (prove %d not working, then try auto handshake)",g_at_ctx.current_baudrate); + + // uint8_t dummy_buffer[256]={0}; + + // 2. 依次尝试自动波特率列表 + for (int i = 0; i < PPP_BAUDRATE_AUTO_COUNT; i++) { + uint32_t baudrate = g_auto_baudrates[i]; + PPP_DEBUG_DEBUG("=== Trying Baudrate %u (index %d/%d) ===", baudrate, i + 1, PPP_BAUDRATE_AUTO_COUNT); + + // 配置串口波特率 + uart_attr_t uart_attr = { + .baud_rate = baudrate, + .data_bits = UART_DATA_BIT_8, + .parity = UART_PARITY_NONE, + .stop_bits = UART_STOP_BIT_1, + }; + if (uapi_uart_set_attr(g_at_ctx.uart_bus, &uart_attr) != ERRCODE_SUCC) { + PPP_DEBUG_WARN("Failed to set baudrate %u", baudrate); + continue; + } + PPP_DEBUG_DEBUG("UART configured for baudrate %u", baudrate); + + // 清空接收缓冲区 + // PPP_DEBUG_DEBUG("Clearing UART RX buffer before AT test..."); + // int clear_cnt = 0; + // memset(dummy_buffer,0,sizeof(dummy_buffer)); + // while (uapi_uart_read(g_at_ctx.uart_bus, dummy_buffer, sizeof(dummy_buffer), 100) > 0) { + // clear_cnt++; + // } + // if (clear_cnt > 0) { + // PPP_DEBUG_DEBUG("Cleared %d RX buffer rounds", clear_cnt); + // } + + // 3次重试AT + ppp_error_t ret = PPP_ERR_AT_FAILED; + for (int retry = 0; retry < 2; retry++) { + PPP_DEBUG_DEBUG("--- AT Test at %u baud, retry %d/3 ---", baudrate, retry + 1); + + // 再次清空缓冲区,防止上次残留 + // memset(dummy_buffer,0,sizeof(dummy_buffer)); + // while (uapi_uart_read(g_at_ctx.uart_bus, dummy_buffer, sizeof(dummy_buffer), 100) > 0) {} + + ppp_at_response_t response; + ret = ppp_at_send_command("AT", "OK", 1000, &response); + + if (ret == PPP_ERR_OK) { + PPP_DEBUG_INFO("AT command successful at baudrate %u", baudrate); + + // 在当前波特率下,设置AT+IPR=1000000 + ret = ppp_at_set_baudrate(g_at_ctx.current_baudrate); + if (ret != PPP_ERR_OK) { ret = ppp_at_set_baudrate(g_at_ctx.current_baudrate); if (ret != PPP_ERR_OK) { PPP_DEBUG_ERROR("Failed to set baudrate %u", g_at_ctx.current_baudrate); return ret; } } - // 再次reset模组,让其自动工作在1000000波特率下 - PPP_DEBUG_DEBUG("Resetting module again to switch to baudrate %d...",g_at_ctx.current_baudrate); - // ppp_power_reset_cat1(); - // osal_msleep(5000); - ret = ppp_at_send_command("AT", "OK", 1000, &response); - if(ret != PPP_ERR_OK) { - PPP_DEBUG_INFO("Baudrate handshake failed, now at %d baud",g_at_ctx.current_baudrate); - }else{ - PPP_DEBUG_INFO("Baudrate handshake successful, now at %d baud",g_at_ctx.current_baudrate); + // 再次reset模组,让其自动工作在1000000波特率下 + PPP_DEBUG_DEBUG("Resetting module again to switch to baudrate %d...",g_at_ctx.current_baudrate); + // ppp_power_reset_cat1(); + // osal_msleep(5000); + ret = ppp_at_send_command("AT", "OK", 1000, &response); + if(ret != PPP_ERR_OK) { + PPP_DEBUG_INFO("Baudrate handshake failed, now at %d baud",g_at_ctx.current_baudrate); + }else{ + PPP_DEBUG_INFO("Baudrate handshake successful, now at %d baud",g_at_ctx.current_baudrate); + } + return PPP_ERR_OK; + } else { + PPP_DEBUG_WARN("AT command failed at baudrate %u, retry %d/3 (error: %d)", baudrate, retry + 1, ret); + osal_msleep(500); + } + } + + PPP_DEBUG_WARN("Baudrate %u failed after 3 retries, trying next...", baudrate); + osal_msleep(100); + } + + PPP_DEBUG_ERROR("Baudrate handshake failed: all baudrates tried, no success"); + return PPP_ERR_AT_FAILED; +} + +/** + * @brief 解析AT+IPR?响应,提取当前波特率 + * @param response AT+IPR?的响应字符串 + * @return 解析出的波特率,失败返回0 + */ +static uint32_t ppp_parse_ipr_response(const char *response) +{ + if (!response) { + return 0; + } + + PPP_DEBUG_DEBUG("Parsing IPR response: [%s]", response); + + /* 查找 "+IPR: " 字符串 */ + const char *ipr_pos = strstr(response, "+IPR: "); + if (!ipr_pos) { + PPP_DEBUG_WARN("IPR response format error: no '+IPR: ' found in response"); + return 0; + } + + /* 跳过 "+IPR: " 部分 */ + ipr_pos += 6; + + /* 跳过可能的空格 */ + while (*ipr_pos == ' ' || *ipr_pos == '\t') { + ipr_pos++; + } + + /* 解析波特率数字,只读取数字部分 */ + uint32_t baudrate = 0; + const char *num_start = ipr_pos; + + /* 找到数字的结束位置(遇到非数字字符停止) */ + while (*ipr_pos >= '0' && *ipr_pos <= '9') { + baudrate = baudrate * 10 + (*ipr_pos - '0'); + ipr_pos++; + } + + /* 检查是否成功解析到数字 */ + if (ipr_pos == num_start) { + PPP_DEBUG_WARN("IPR response format error: no valid number found after '+IPR: '"); + return 0; + } + + /* 验证波特率是否合理 */ + if (baudrate < 1200 || baudrate > 4000000) { + PPP_DEBUG_WARN("IPR response contains unreasonable baudrate: %u", baudrate); + return 0; + } + + PPP_DEBUG_DEBUG("Successfully parsed current baudrate from IPR response: %u", baudrate); + return baudrate; +} + +/** + * @brief 查询当前modem波特率 + * @return 当前modem波特率,失败返回0 + */ +static uint32_t ppp_query_current_modem_baudrate(void) +{ + ppp_at_response_t response; + ppp_error_t ret = ppp_at_send_command("AT+IPR?", "OK", PPP_AT_CMD_TIMEOUT, &response); + + if (ret != PPP_ERR_OK) { + PPP_DEBUG_WARN("Failed to query current baudrate with AT+IPR?, error: %d", ret); + return 0; + } + + return ppp_parse_ipr_response(response.response); +} + +/** + * @brief 设置固定波特率(内部函数) + */ +static ppp_error_t ppp_at_set_baudrate(uint32_t baudrate) +{ + if (!g_at_ctx.initialized) { + return PPP_ERR_INVALID_PARAM; + } + + /* 检查波特率是否支持 */ + bool supported = false; + for (int i = 0; i < PPP_BAUDRATE_FIXED_COUNT; i++) { + if (g_fixed_baudrates[i] == baudrate) { + supported = true; + break; + } + } + + if (!supported) { + PPP_DEBUG_ERROR("Unsupported baudrate: %u", baudrate); + return PPP_ERR_INVALID_PARAM; + } +#if defined(PPP_DO_SERIAL_TEST) && (PPP_DO_SERIAL_TEST == 1) + uint32_t current_modem_baudrate = baudrate; // 测试模式下,直接使用输入的波特率 +#else + /* 先查询当前modem波特率 */ + uint32_t current_modem_baudrate = ppp_query_current_modem_baudrate(); + if (current_modem_baudrate == baudrate) { + PPP_DEBUG_INFO("Modem already at target baudrate %u, skipping AT+IPR command", baudrate); + + /* 仍需要更新本地UART配置以确保一致性 */ + uart_attr_t uart_attr = { + .baud_rate = baudrate, + .data_bits = UART_DATA_BIT_8, + .parity = UART_PARITY_NONE, + .stop_bits = UART_STOP_BIT_1, + }; + + if (uapi_uart_set_attr(g_at_ctx.uart_bus, &uart_attr) != ERRCODE_SUCC) { + PPP_DEBUG_ERROR("Failed to update UART baudrate"); + return PPP_ERR_UART; + } + + g_at_ctx.current_baudrate = baudrate; + g_at_ctx.auto_baudrate_mode = false; + + PPP_DEBUG_DEBUG("Baudrate confirmed at %u (no change needed)", baudrate); + return PPP_ERR_OK; + } + + /* 需要设置新波特率 */ + PPP_DEBUG_INFO("Changing modem baudrate from %u to %u", current_modem_baudrate, baudrate); + + /* 发送AT+IPR命令设置波特率 */ + char cmd[64]; + snprintf_s(cmd, sizeof(cmd), sizeof(cmd) - 1, "AT+IPR=%u", baudrate); + + ppp_at_response_t response; + ppp_error_t ret = ppp_at_send_command(cmd, "OK", PPP_AT_CMD_TIMEOUT, &response); + + if (ret != PPP_ERR_OK) { + PPP_DEBUG_ERROR("Failed to set baudrate to %u", baudrate); + return ret; + } +#endif + /* 更新串口配置 */ + uart_attr_t uart_attr = { + .baud_rate = baudrate, + .data_bits = UART_DATA_BIT_8, + .parity = UART_PARITY_NONE, + .stop_bits = UART_STOP_BIT_1, + }; + + if (uapi_uart_set_attr(g_at_ctx.uart_bus, &uart_attr) != ERRCODE_SUCC) { + PPP_DEBUG_ERROR("Failed to update UART baudrate"); + return PPP_ERR_UART; + } + + g_at_ctx.current_baudrate = baudrate; + g_at_ctx.auto_baudrate_mode = false; + + PPP_DEBUG_DEBUG("Baudrate set bus(%d) to %u (changed from %u)", g_at_ctx.uart_bus, baudrate, current_modem_baudrate); + return PPP_ERR_OK; +} + +/** + * @brief 发送AT命令(内部函数) + * @note 检查内存泄露:所有malloc分配的内存都应在所有分支free + */ +static ppp_error_t ppp_at_send_command(const char *command, const char *expected_response, + uint32_t timeout, ppp_at_response_t *response) +{ + /* 优化:使用单个时间戳结构体,减少栈使用 */ + struct { + uint32_t start; // 开始时间戳 + uint32_t mutex_acquire; // 互斥锁获取时间 + uint32_t mutex_release; // 互斥锁释放时间 + uint32_t response_wait; // 响应等待时间 + uint32_t mutex_reacquire; // 重新获取互斥锁时间 + uint32_t uart_write; // UART写入时间 + uint32_t mutex_unlock; // 互斥锁解锁时间 + } timestamps = {0}; + + timestamps.start = (uint32_t)osal_get_jiffies(); + +#if PPP_DEBUG_ENABLE && (PPP_DEBUG_LEVEL >= PPP_DEBUG_LEVEL_DEBUG) + PPP_DEBUG_CRITICAL("=== AT COMMAND START ==="); + PPP_DEBUG_CRITICAL("Timestamp: %u", timestamps.start); + PPP_DEBUG_CRITICAL("Command: [%s]", command ? command : "NULL"); + PPP_DEBUG_CRITICAL("Expected: [%s]", expected_response ? expected_response : "NULL"); + PPP_DEBUG_CRITICAL("Timeout: %u ms", timeout); +#else + /* Release版本:只记录关键信息 */ + PPP_DEBUG_CRITICAL("AT CMD: [%s]", command ? command : "NULL"); +#endif + + if (!g_at_ctx.initialized || !command || !response) { + PPP_DEBUG_ERROR("Invalid parameters: initialized=%d, command=%p, response=%p", + g_at_ctx.initialized, command, response); + return PPP_ERR_INVALID_PARAM; + } + + /* 防止系统卡死的安全措施 */ + const uint32_t MAX_COMMAND_TIMEOUT = 60000; // 绝对最大超时时间60秒 + if (timeout > MAX_COMMAND_TIMEOUT) { + PPP_DEBUG_WARN("Command timeout %u ms exceeds maximum %u ms, capping it", + timeout, MAX_COMMAND_TIMEOUT); + timeout = MAX_COMMAND_TIMEOUT; + } + +#if PPP_DEBUG_ENABLE && (PPP_DEBUG_LEVEL >= PPP_DEBUG_LEVEL_DEBUG) + PPP_DEBUG_CRITICAL("Step 1: Attempting to acquire AT mutex..."); +#endif + timestamps.mutex_acquire = (uint32_t)osal_get_jiffies(); + + /* 使用带超时的互斥锁获取,防止永久阻塞 */ + int mutex_result = osal_mutex_lock_timeout(&g_at_ctx.at_mutex, timeout + 5000); + if (mutex_result != OSAL_SUCCESS) { + PPP_DEBUG_ERROR("Failed to acquire AT mutex within timeout, this indicates a serious problem"); + PPP_DEBUG_ERROR("Possible causes: 1) Previous AT command hung 2) Mutex corruption 3) Deadlock"); + return PPP_ERR_TIMEOUT; + } + + uint32_t mutex_acquire_end = (uint32_t)osal_get_jiffies(); +#if PPP_DEBUG_ENABLE && (PPP_DEBUG_LEVEL >= PPP_DEBUG_LEVEL_DEBUG) + PPP_DEBUG_CRITICAL("Step 1: AT mutex acquired in %u ms", + osal_jiffies_to_msecs(mutex_acquire_end - timestamps.mutex_acquire)); +#endif + +#if PPP_DEBUG_ENABLE && (PPP_DEBUG_LEVEL >= PPP_DEBUG_LEVEL_DEBUG) + PPP_DEBUG_DEBUG("=== AT Command Send ==="); + PPP_DEBUG_DEBUG("Command: %s", command); + PPP_DEBUG_DEBUG("Expected: %s", expected_response ? expected_response : "None"); + PPP_DEBUG_DEBUG("Timeout: %u ms", timeout); +#endif + + /* 清空响应缓冲区 */ + memset_s(response, sizeof(ppp_at_response_t), 0, sizeof(ppp_at_response_t)); + + /* 清空累积缓冲区(用于组装完整响应的临时缓冲区) + * 注意:累积缓冲区只用于组装响应,不存储已处理的URC数据 + * 纯异步URC(如+TCPSETUP、+TCPRECV等)不会进入响应缓冲区,所以清空不会丢失 + * 但是,命令响应中的URC(如+TCPCLOSE: ERR)会进入响应缓冲区,需要保留 + * 因此,我们只清空累积缓冲区,不清空响应环形缓冲区 + */ + osal_mutex_lock(&g_at_accumulate_mutex); + g_at_accumulate_length = 0; + g_at_accumulate_last_update_time = 0; + memset(g_at_accumulate_buffer, 0, sizeof(g_at_accumulate_buffer)); + osal_mutex_unlock(&g_at_accumulate_mutex); + + /* 读取并丢弃响应环形缓冲区中的旧数据(如果有) + * 这样可以避免旧响应干扰新命令,同时不会丢失正在等待的响应 + * 注意:如果缓冲区中有数据,说明可能有之前的命令响应还在等待读取 + * 这种情况下,我们应该先读取并丢弃,而不是直接清空 + */ + osal_mutex_lock(&g_at_response_mutex); + uint32_t buffer_data_count = (g_at_response_head >= g_at_response_tail) ? + (g_at_response_head - g_at_response_tail) : + (N706A_AT_RESPONSE_RING_BUFFER_SIZE - g_at_response_tail + g_at_response_head); + if (buffer_data_count > 0) { + PPP_DEBUG_WARN("Response ring buffer has %u bytes of old data, discarding before sending new command", buffer_data_count); + /* 清空响应环形缓冲区(丢弃旧数据) + * 注意:这些数据可能是之前命令的延迟响应,或者是命令响应中的URC + * 由于我们已经获取了互斥锁,说明没有其他线程在等待这些数据 + * 如果这些数据是重要的,应该在发送新命令前被读取 + */ + g_at_response_head = 0; + g_at_response_tail = 0; + } + osal_mutex_unlock(&g_at_response_mutex); + +#if PPP_DEBUG_ENABLE && (PPP_DEBUG_LEVEL >= PPP_DEBUG_LEVEL_DEBUG) + PPP_DEBUG_DEBUG("Cleared accumulate buffer and old response data before sending new command"); +#endif + + /* 发送命令 */ + size_t command_len = strlen(command); + // 使用动态分配的缓冲区(在初始化时分配,避免栈上分配大缓冲区) + if (!g_at_cmd_buffer) { + PPP_DEBUG_ERROR("AT command buffer not allocated (module not initialized?)"); + osal_mutex_unlock(&g_at_ctx.at_mutex); + return PPP_ERR_MEMORY; + } + if (command_len >= (N706A_AT_CMD_MAX_LEN + 4) - 3) { + PPP_DEBUG_ERROR("AT command too long: %u bytes (max: %u)", + (unsigned int)command_len, (unsigned int)(N706A_AT_CMD_MAX_LEN + 1)); + osal_mutex_unlock(&g_at_ctx.at_mutex); + return PPP_ERR_INVALID_PARAM; + } + snprintf_s(g_at_cmd_buffer, N706A_AT_CMD_MAX_LEN + 4, N706A_AT_CMD_MAX_LEN + 3, "%s\r\n", command); + + uint32_t cmd_len = strlen(g_at_cmd_buffer); + +#if PPP_DEBUG_ENABLE && (PPP_DEBUG_LEVEL >= PPP_DEBUG_LEVEL_DEBUG) + /* 打印发送的字符串(可见字符)- 使用动态分配的缓冲区 */ + if (g_at_printable_buffer) { + uint32_t j = 0; + for (uint32_t i = 0; i < cmd_len && j < (PPP_AT_RESPONSE_MAX_LEN + 100) - 10; i++) { + if (g_at_cmd_buffer[i] == '\r') { + g_at_printable_buffer[j++] = '\\'; + g_at_printable_buffer[j++] = 'r'; + } else if (g_at_cmd_buffer[i] == '\n') { + g_at_printable_buffer[j++] = '\\'; + g_at_printable_buffer[j++] = 'n'; + } else if (g_at_cmd_buffer[i] >= 32 && g_at_cmd_buffer[i] <= 126) { + g_at_printable_buffer[j++] = g_at_cmd_buffer[i]; + } else { + j += snprintf_s(g_at_printable_buffer + j, (PPP_AT_RESPONSE_MAX_LEN + 100) - j, (PPP_AT_RESPONSE_MAX_LEN + 100) - j - 1, "\\x%02X", (uint8_t)g_at_cmd_buffer[i]); + } + } + g_at_printable_buffer[j] = '\0'; + PPP_DEBUG_DEBUG("Send String: %s", g_at_printable_buffer); + } +#endif + +#if PPP_DEBUG_ENABLE && (PPP_DEBUG_LEVEL >= PPP_DEBUG_LEVEL_DEBUG) + PPP_DEBUG_CRITICAL("Step 2: Sending AT command via ppp_serial_write..."); +#endif + + timestamps.uart_write = (uint32_t)osal_get_jiffies(); + + /* 使用 ppp_serial_write 发送 AT 命令(会自动处理 wakeup) */ + ppp_error_t write_result = ppp_serial_write((const uint8_t *)g_at_cmd_buffer, cmd_len); + uint32_t uart_write_end = (uint32_t)osal_get_jiffies(); + +#if PPP_DEBUG_ENABLE && (PPP_DEBUG_LEVEL >= PPP_DEBUG_LEVEL_DEBUG) + PPP_DEBUG_CRITICAL("Step 2: ppp_serial_write completed in %u ms, result: %d (expected: %u)", + osal_jiffies_to_msecs(uart_write_end - timestamps.uart_write), write_result, cmd_len); +#endif + + if (write_result != PPP_ERR_OK) { + PPP_DEBUG_ERROR("Failed to send AT command via ppp_serial_write: error=%d", write_result); + osal_mutex_unlock(&g_at_ctx.at_mutex); + return write_result; + } + +#if PPP_DEBUG_ENABLE && (PPP_DEBUG_LEVEL >= PPP_DEBUG_LEVEL_DEBUG) + PPP_DEBUG_CRITICAL("Step 2: AT command sent successfully"); +#endif + + /* ========== 方案1优化:发送后立即释放互斥锁,允许其他命令发送 ========== */ + /* 释放互斥锁,允许其他AT命令在等待响应期间发送 + * 这样可以避免长时间阻塞(等待响应可能需要数秒) + * URC数据使用独立的 g_at_accumulate_mutex,不受影响 + */ +#if PPP_DEBUG_ENABLE && (PPP_DEBUG_LEVEL >= PPP_DEBUG_LEVEL_DEBUG) + PPP_DEBUG_CRITICAL("Step 2.5: Releasing AT mutex before waiting for response (optimization: allow other commands)..."); +#endif + timestamps.mutex_release = (uint32_t)osal_get_jiffies(); + osal_mutex_unlock(&g_at_ctx.at_mutex); + uint32_t mutex_release_end = (uint32_t)osal_get_jiffies(); +#if PPP_DEBUG_ENABLE && (PPP_DEBUG_LEVEL >= PPP_DEBUG_LEVEL_DEBUG) + PPP_DEBUG_CRITICAL("Step 2.5: AT mutex released in %u ms (will re-acquire after response)", + osal_jiffies_to_msecs(mutex_release_end - timestamps.mutex_release)); +#endif + + /* 等待响应(不占用互斥锁,使用信号量机制) + * 注意:ppp_at_wait_response 使用信号量等待,响应是顺序的 + * 即使有其他命令发送,响应也不会混淆(因为信号量保证顺序) + */ +#if PPP_DEBUG_ENABLE && (PPP_DEBUG_LEVEL >= PPP_DEBUG_LEVEL_DEBUG) + PPP_DEBUG_CRITICAL("Step 3: Starting to wait for AT response (mutex released, using semaphore)..."); +#endif + timestamps.response_wait = (uint32_t)osal_get_jiffies(); + ppp_error_t ret = ppp_at_wait_response(response, timeout); + uint32_t response_wait_end = (uint32_t)osal_get_jiffies(); +#if PPP_DEBUG_ENABLE && (PPP_DEBUG_LEVEL >= PPP_DEBUG_LEVEL_DEBUG) + PPP_DEBUG_CRITICAL("Step 3: AT response wait completed in %u ms, result: %d", + osal_jiffies_to_msecs(response_wait_end - timestamps.response_wait), ret); +#endif + + /* ========== 方案1优化:响应到达后重新获取互斥锁进行验证 ========== */ + /* 重新获取互斥锁,用于验证响应(如果需要保护验证过程) + * 注意:验证过程通常很快(<10ms),不会长时间阻塞 + */ +#if PPP_DEBUG_ENABLE && (PPP_DEBUG_LEVEL >= PPP_DEBUG_LEVEL_DEBUG) + PPP_DEBUG_CRITICAL("Step 3.5: Re-acquiring AT mutex for response validation..."); +#endif + timestamps.mutex_reacquire = (uint32_t)osal_get_jiffies(); + int mutex_reacquire_result = osal_mutex_lock_timeout(&g_at_ctx.at_mutex, 1000); + if (mutex_reacquire_result != OSAL_SUCCESS) { + PPP_DEBUG_ERROR("Failed to re-acquire AT mutex for validation (timeout 1s), but response already received"); + /* 即使获取互斥锁失败,响应已经收到,可以继续验证(验证过程不需要互斥锁保护) */ + } else { + uint32_t mutex_reacquire_end = (uint32_t)osal_get_jiffies(); +#if PPP_DEBUG_ENABLE && (PPP_DEBUG_LEVEL >= PPP_DEBUG_LEVEL_DEBUG) + PPP_DEBUG_CRITICAL("Step 3.5: AT mutex re-acquired in %u ms", + osal_jiffies_to_msecs(mutex_reacquire_end - timestamps.mutex_reacquire)); +#endif + } + +#if PPP_DEBUG_ENABLE && (PPP_DEBUG_LEVEL >= PPP_DEBUG_LEVEL_DEBUG) + /* 打印响应结果 */ + PPP_DEBUG_DEBUG("=== AT Command Response ==="); + if (ret == PPP_ERR_OK) { + PPP_DEBUG_DEBUG("Response received (%u bytes):", response->length); + + /* 打印接收的字符串(可见字符)- 使用动态分配的缓冲区 */ + if (g_at_printable_buffer) { + uint32_t j = 0; + for (uint32_t i = 0; i < response->length && j < (PPP_AT_RESPONSE_MAX_LEN + 100) - 10; i++) { + if (response->response[i] == '\r') { + g_at_printable_buffer[j++] = '\\'; + g_at_printable_buffer[j++] = 'r'; + } else if (response->response[i] == '\n') { + g_at_printable_buffer[j++] = '\\'; + g_at_printable_buffer[j++] = 'n'; + } else if (response->response[i] >= 32 && response->response[i] <= 126) { + g_at_printable_buffer[j++] = response->response[i]; + } else { + j += snprintf_s(g_at_printable_buffer + j, (PPP_AT_RESPONSE_MAX_LEN + 100) - j, (PPP_AT_RESPONSE_MAX_LEN + 100) - j - 1, "\\x%02X", (uint8_t)response->response[i]); + } + } + g_at_printable_buffer[j] = '\0'; + PPP_DEBUG_DEBUG("Response String: %s", g_at_printable_buffer); + } + PPP_DEBUG_DEBUG("Raw Response: %s", response->response); + } else { + PPP_DEBUG_ERROR("No response received (error: %d)", ret); + } +#else + /* Release版本:只记录错误 */ + if (ret != PPP_ERR_OK) { + PPP_DEBUG_ERROR("AT command failed: error=%d", ret); + } +#endif + + if (ret == PPP_ERR_OK) { + if (expected_response) { + if (!ppp_at_parse_response_with_len(response->response, response->length, expected_response)) { + PPP_DEBUG_WARN("Response validation failed - Expected: %s", expected_response); + PPP_DEBUG_WARN("Response validation failed - Received: %s", response->response); + response->success = false; + response->error_code = PPP_ERR_AT_FAILED; + ret = PPP_ERR_AT_FAILED; + } else { + PPP_DEBUG_DEBUG("Response validation successful - Expected: %s", expected_response); + response->success = true; + response->error_code = PPP_ERR_OK; + } + } else { + // 没有期望响应时,不验证特定字符串,但至少要检查是否有错误 + // 如果响应中包含 ERROR(且不是命令响应URC格式),认为是失败 + // 否则认为成功(可能是 OK、提示符 >、或其他有效响应) + if (!ppp_at_parse_response_with_len(response->response, response->length, NULL)) { + // ppp_at_parse_response 返回 false 表示有错误(ERROR/BUSY/NO CARRIER等) + PPP_DEBUG_WARN("Response validation failed - Contains error (expected is NULL)"); + PPP_DEBUG_WARN("Response: %s", response->response); + response->success = false; + response->error_code = PPP_ERR_AT_FAILED; + ret = PPP_ERR_AT_FAILED; + } else { + // 没有错误,认为成功 + PPP_DEBUG_DEBUG("Response validation successful - No error found (expected is NULL)"); + response->success = true; + response->error_code = PPP_ERR_OK; + } + } + } else { + response->success = false; + response->error_code = ret; + } + +#if PPP_DEBUG_ENABLE && (PPP_DEBUG_LEVEL >= PPP_DEBUG_LEVEL_DEBUG) + PPP_DEBUG_DEBUG("=== AT Command Complete (Result: %s) ===", ret == PPP_ERR_OK ? "SUCCESS" : "FAILED"); +#endif + + /* 释放互斥锁(如果重新获取成功) */ +#if PPP_DEBUG_ENABLE && (PPP_DEBUG_LEVEL >= PPP_DEBUG_LEVEL_DEBUG) + PPP_DEBUG_CRITICAL("Step 4: Releasing AT mutex..."); +#endif + timestamps.mutex_unlock = (uint32_t)osal_get_jiffies(); + if (mutex_reacquire_result == OSAL_SUCCESS) { + osal_mutex_unlock(&g_at_ctx.at_mutex); + } + uint32_t mutex_unlock_end = (uint32_t)osal_get_jiffies(); + + uint32_t total_time = osal_jiffies_to_msecs(mutex_unlock_end - timestamps.start); +#if PPP_DEBUG_ENABLE && (PPP_DEBUG_LEVEL >= PPP_DEBUG_LEVEL_DEBUG) + PPP_DEBUG_CRITICAL("Step 4: AT mutex released in %u ms", + osal_jiffies_to_msecs(mutex_unlock_end - timestamps.mutex_unlock)); + PPP_DEBUG_CRITICAL("=== AT COMMAND END (Total: %u ms, Result: %d) ===", total_time, ret); +#else + /* Release版本:只记录总时间和结果(如果失败) */ + if (ret != PPP_ERR_OK) { + PPP_DEBUG_CRITICAL("AT CMD END: %u ms, FAILED (err=%d)", total_time, ret); + } +#endif + + return ret; +} + +/** + * @brief 等待 '>' 提示符(用于 AT+TCPSEND/AT+SSLTCPSEND) + * @param timeout 超时时间(毫秒) + * @return PPP_ERR_OK 成功,其他值失败 + * + * 根据N706A AT命令手册,AT+TCPSEND发送后会收到 '>' 提示符, + * 此时需要延迟50-100ms后再发送数据。 + */ +static ppp_error_t ppp_at_wait_for_prompt(uint32_t timeout) +{ + ppp_at_response_t response; + ppp_error_t ret = ppp_at_wait_response(&response, timeout); + + if (ret != PPP_ERR_OK) { + PPP_DEBUG_ERROR("Failed to wait for '>' prompt: error %d", ret); + return ret; + } + + /* 检查响应中是否包含 '>' */ + if (strstr((const char *)response.response, ">") == NULL) { + PPP_DEBUG_ERROR("Response does not contain '>' prompt: %s", response.response); + return PPP_ERR_AT_FAILED; + } + + PPP_DEBUG_DEBUG("Received '>' prompt, delaying 75ms before sending data..."); + + /* 延迟75ms(在50-100ms范围内,取中间值) */ + osal_msleep(75); + + return PPP_ERR_OK; +} + +/** + * @brief 发送原始数据(不带AT命令格式) + * @note 用于 TCP/SSL 数据发送,在 AT+TCPSEND/AT+SSLTCPSEND 命令后使用 + * @note 调用此函数前,必须先调用 ppp_at_send_command 发送 AT+TCPSEND 命令并等待 '>' 提示符 + */ +ppp_error_t ppp_at_send_raw_data(const void *data, size_t len, uint32_t timeout) +{ + if (!g_at_ctx.initialized || !data || len == 0) { + PPP_DEBUG_ERROR("Invalid parameters: initialized=%d, data=%p, len=%zu", + g_at_ctx.initialized, data, len); + return PPP_ERR_INVALID_PARAM; + } + + PPP_DEBUG_DEBUG("=== Sending Raw Data (HEX Mode) ==="); + PPP_DEBUG_DEBUG("Binary length: %zu bytes", len); + + /* 将二进制数据编码为HEX字符串 */ + size_t hex_str_len = len * 2; // 每个字节转换为2个HEX字符 + char *hex_str = (char *)malloc(hex_str_len + 1); // +1 for null terminator + if (!hex_str) { + PPP_DEBUG_ERROR("Failed to allocate memory for HEX string"); + return PPP_ERR_MEMORY; + } + + size_t encoded_len = n706a_binary_to_hex_string((const uint8_t *)data, len, hex_str, hex_str_len + 1); + if (encoded_len == 0) { + free(hex_str); + PPP_DEBUG_ERROR("Failed to encode binary data to HEX string"); + return PPP_ERR_INVALID_PARAM; + } + + PPP_DEBUG_DEBUG("HEX encoded: binary_len=%zu -> hex_len=%zu", len, encoded_len); + + /* 使用 ppp_serial_write 发送HEX字符串(ASCII字符),不添加 \r\n(会自动处理 wakeup) */ + ppp_error_t write_result = ppp_serial_write((const uint8_t *)hex_str, encoded_len); + + free(hex_str); + + if (write_result != PPP_ERR_OK) { + PPP_DEBUG_ERROR("Failed to send HEX data via ppp_serial_write: error=%d", write_result); + return write_result; + } + + PPP_DEBUG_DEBUG("HEX data sent successfully (%zu hex chars, %zu binary bytes)", + encoded_len, len); + return PPP_ERR_OK; +} + +/** + * @brief 跳过开机日志 (对应chat文件中的 '' \rAT) + */ +static ppp_error_t ppp_at_skip_startup_logs(void) +{ + PPP_DEBUG_DEBUG("Skipping startup logs..."); + + // 清空接收缓冲区,跳过开机时的各种日志输出 + uint8_t dummy_buffer[256]; + uint32_t bytes_read; + + // 连续读取并丢弃开机日志,直到没有更多数据 + int retry_count = 0; + const int max_retries = 10; + + do { + ppp_error_t ret = ppp_serial_read(dummy_buffer, sizeof(dummy_buffer), &bytes_read); + if (ret != PPP_ERR_OK) { + break; + } + + if (bytes_read > 0) { + PPP_DEBUG_DEBUG("Skipped %u bytes of startup logs", bytes_read); + retry_count = 0; // 重置重试计数 + } else { + retry_count++; + } + + osal_msleep(100); + } while (retry_count < max_retries); + + PPP_DEBUG_DEBUG("Startup logs skipped"); + return PPP_ERR_OK; +} + +/* ========== PPP拨号函数已删除 ========== */ +/* N706A_NET 使用纯AT命令模式,不需要PPP拨号功能 */ +/* 如需PPP功能,请使用 ppp 组件 */ +/** + * @brief 发送AT关机指令,支持快速关机和正常关机 + * @param shutdown_type 关机类型: 0-快速关机, 1-正常流程关机 + * @return ppp_error_t + */ +ppp_error_t ppp_at_poweroff_modem(uint8_t shutdown_type) +{ + if (!g_at_ctx.initialized) { + return PPP_ERR_INVALID_PARAM; + } + + PPP_DEBUG_INFO("Sending AT+NWPWROFF=%d to power off modem", shutdown_type); + + char cmd[32]; + snprintf(cmd, sizeof(cmd), "AT+NWPWROFF=%d", shutdown_type); + + ppp_at_response_t response; + ppp_error_t ret = ppp_at_send_command(cmd, "OK", 10000, &response); + + if (ret == PPP_ERR_OK) { + PPP_DEBUG_INFO("Modem power off command sent successfully"); + } else { + PPP_DEBUG_ERROR("Failed to send modem power off command"); + } + ppp_power_off_cat1(); + return ret; +} + +/* ========== PPP挂断函数已删除 ========== */ +/* N706A_NET 使用纯AT命令模式,不需要PPP挂断功能 */ + +/** + * @brief 获取当前波特率 + */ +uint32_t ppp_at_get_current_baudrate(void) +{ + return g_at_ctx.current_baudrate; +} + +/** + * @brief 检查是否为自动波特率模式 + */ +bool ppp_at_is_auto_baudrate_mode(void) +{ + return g_at_ctx.auto_baudrate_mode; +} + +/** + * @brief 测试AT通信是否正常 + */ +ppp_error_t ppp_at_test_communication(void) +{ + PPP_DEBUG_INFO("Testing AT communication..."); + + ppp_at_response_t response; + + // 发送简单的AT命令测试通信 + ppp_error_t ret = ppp_at_send_command("AT", "OK", 1000, &response); + if (ret == PPP_ERR_OK) { + PPP_DEBUG_INFO("AT communication test passed"); + } else { + PPP_DEBUG_ERROR("AT communication test failed: %d", ret); + } + + return ret; +} + +/* 内部函数实现 */ + +/** + * @brief 检查AT响应是否完整 + * @param buffer 响应缓冲区 + * @param length 缓冲区长度 + * @return true 如果响应完整,false 否则 + */ +static bool ppp_at_check_response_complete(const char *buffer, uint32_t length) +{ + if (!buffer || length == 0) { + return false; + } + + /* 搜索">"提示符(用于AT+TCPSEND/AT+SSLTCPSEND等命令) */ + /* 这些命令的响应格式是:AT+TCPSEND=,\r\n\r\n> */ + /* 注意:">"可能单独在一行,后面可能有\r\n,也可能没有(如果响应被截断) */ + if (length >= 1) { + for (uint32_t i = 0; i < length; i++) { + if (buffer[i] == '>') { + /* 检查">"前面是否有换行符或空格(避免误匹配) */ + if (i == 0 || buffer[i-1] == '\n' || buffer[i-1] == '\r' || buffer[i-1] == ' ') { + /* 检查">"后面是否有换行符或字符串结束(确保是独立的提示符) */ + /* 注意:如果">"是最后一个字符,也认为是有效的提示符(响应可能被截断) */ + if (i == length - 1 || buffer[i+1] == '\n' || buffer[i+1] == '\r' || buffer[i+1] == '\0' || buffer[i+1] == ' ') { + PPP_DEBUG_DEBUG("Found '>' prompt at position %u (length=%u, next_char=0x%02x)", + i, length, (i < length - 1) ? (unsigned char)buffer[i+1] : 0); + return true; + } + } + } + } + } + + /* 检查命令响应URC格式的结束符(如 +TCPSETUP: 0,OK\r\r 或 +TCPCLOSE: 0,OK\r\r) */ + /* 这些格式可能以 \r\r 结尾(异常格式,但需要容错处理) */ + if (length >= 11) { + // 检查 +TCPSETUP: 或 +TCPCLOSE: 格式 + if (strstr(buffer, "+TCPSETUP: ") || strstr(buffer, "+TCPCLOSE: ") || + strstr(buffer, "+SSLTCPSETUP: ") || strstr(buffer, "+SSLTCPCLOSE: ")) { + // 检查是否包含 OK 或 ERR,并且以 \r\n 或 \r\r 结尾 + const char *ok_pos = strstr(buffer, "OK"); + const char *err_pos = strstr(buffer, "ERR"); + if (ok_pos || err_pos) { + // 检查响应是否以 \r\n 或 \r\r 结尾 + if (length >= 2) { + if (buffer[length - 2] == '\r' && (buffer[length - 1] == '\n' || buffer[length - 1] == '\r')) { + PPP_DEBUG_DEBUG("Found command response URC terminator (ends with \\r\\%c)", + buffer[length - 1] == '\n' ? 'n' : 'r'); + return true; + } } - return PPP_ERR_OK; - } else { - PPP_DEBUG_WARN("AT command failed at baudrate %u, retry %d/3 (error: %d)", baudrate, retry + 1, ret); - osal_msleep(500); } } - - PPP_DEBUG_WARN("Baudrate %u failed after 3 retries, trying next...", baudrate); - osal_msleep(100); } - - PPP_DEBUG_ERROR("Baudrate handshake failed: all baudrates tried, no success"); - return PPP_ERR_AT_FAILED; + + /* 搜索OK终止符 - 修复无符号整数下溢问题 */ + if (length >= 2) { + for (uint32_t i = 0; i <= length - 2; i++) { + if (buffer[i] == 'O' && buffer[i+1] == 'K') { + /* 检查OK前面是否有换行符(避免误匹配) */ + if (i == 0 || buffer[i-1] == '\n' || buffer[i-1] == '\r') { + PPP_DEBUG_DEBUG("Found OK terminator at position %u", i); + return true; + } + } + } + } + + /* 搜索ERROR终止符 - 已经有长度检查,但确保安全 */ + if (length >= 5) { + for (uint32_t i = 0; i <= length - 5; i++) { + if (strncmp(&buffer[i], "ERROR", 5) == 0) { + /* 检查ERROR前面是否有换行符 */ + if (i == 0 || buffer[i-1] == '\n' || buffer[i-1] == '\r') { + PPP_DEBUG_DEBUG("Found ERROR terminator at position %u", i); + return true; + } + } + } + } + + /* 检查其他终止符 */ + if (strstr(buffer, "CONNECT") || + strstr(buffer, "NO CARRIER") || + strstr(buffer, "BUSY") || + strstr(buffer, "NO DIALTONE") || + strstr(buffer, "+CME ERROR") || + strstr(buffer, "+CMS ERROR")) { + PPP_DEBUG_DEBUG("Found other terminator in response"); + return true; + } + + return false; } /** - * @brief 解析AT+IPR?响应,提取当前波特率 - * @param response AT+IPR?的响应字符串 - * @return 解析出的波特率,失败返回0 + * @brief 跳过命令回显行,返回实际响应内容 + * @param response 原始响应(可能包含回显) + * @param cleaned_response 输出缓冲区(用于存储清理后的响应) + * @param cleaned_size 输出缓冲区大小 + * @return 清理后的响应指针(指向cleaned_response或原始response) + * + * 启用回显后,响应格式为:AT+XXX\r\n\r\n响应内容\r\nOK\r\n + * 此函数会跳过回显行(AT+XXX\r\n),只保留实际响应内容 */ -static uint32_t ppp_parse_ipr_response(const char *response) +static const char *ppp_at_skip_echo(const char *response, size_t response_len, char *cleaned_response, size_t cleaned_size) { if (!response) { - return 0; + return NULL; + } + + // 如果 response_len 为 0,尝试使用 strlen(处理正常情况) + if (response_len == 0) { + response_len = strlen(response); + if (response_len == 0) { + return response; + } + } + + // 检查是否包含命令回显(AT+开头) + // 注意:响应已经过 trim 处理,不再包含开头的 \x00 字节,可以直接使用 strstr + const char *at_pos = strstr(response, "AT+"); + + if (!at_pos) { + // 没有回显,直接返回原始响应 + return response; + } + + // 查找回显行的结束位置(\r\n) + // 注意:响应已经过 trim 处理,可以直接使用 strstr + const char *echo_end = strstr(at_pos, "\r\n"); + + if (!echo_end) { + // 没有找到回显结束符,返回原始响应 + return response; + } + + // 跳过回显行和后续的空白行(\r\n\r\n) + const char *actual_response = echo_end + 2; // 跳过第一个\r\n + while (actual_response < response + response_len && + (*actual_response == '\r' || *actual_response == '\n' || *actual_response == '\0')) { + actual_response++; + } + + // 如果实际响应为空或超出范围,返回原始响应 + if (actual_response >= response + response_len || *actual_response == '\0') { + return response; + } + + // 将清理后的响应复制到输出缓冲区 + if (cleaned_response && cleaned_size > 0) { + // 计算实际响应长度(到字符串结束或缓冲区结束) + size_t remaining_len = response_len - (actual_response - response); + size_t copy_len = remaining_len; + + // 注意:响应已经过 trim 处理,但可能中间仍包含 \x00(虽然不应该) + // 为了安全,仍然检查第一个 \x00 的位置 + for (size_t i = 0; i < remaining_len; i++) { + if (actual_response[i] == '\0') { + copy_len = i; + break; + } + } + + if (copy_len >= cleaned_size) { + copy_len = cleaned_size - 1; + } + memcpy(cleaned_response, actual_response, copy_len); + cleaned_response[copy_len] = '\0'; + return cleaned_response; + } + + // 如果没有提供输出缓冲区,返回实际响应的指针(注意:这要求原始response不会被修改) + return actual_response; +} + +/** + * @brief 去除响应字符串两头的 \x00 字节(保留其他空白字符如 \r\n、空格等) + * @param response 原始响应字符串 + * @param response_len 原始响应长度 + * @param trimmed_start 输出:trim后的起始位置(相对于原始response的偏移) + * @param trimmed_len 输出:trim后的长度 + * @return true=成功,false=失败 + * + * 此函数只去除响应字符串两头的 \x00 字节,保留其他字符(包括 \r、\n、空格等)。 + * + * 原因: + * 1. \x00 字节会导致 strstr 等函数无法正常工作(遇到 \x00 会停止) + * 2. \r\n 是 AT 响应的重要分隔符,用于识别行边界(如 "\r\nOK\r\n") + * 3. 空格在某些情况下也是有效的(如 "> " 提示符) + * + * 这样可以解决响应开头包含 \x00 字节的问题,同时不影响 AT 响应的正常解析。 + */ +static bool ppp_at_trim_response(const char *response, size_t response_len, + size_t *trimmed_start, size_t *trimmed_len) +{ + if (!response || response_len == 0 || !trimmed_start || !trimmed_len) { + return false; + } + + // 从开头跳过 \x00 字节(只跳过 \x00,保留其他字符) + size_t start = 0; + while (start < response_len && response[start] == '\0') { + start++; + } + + // 如果全部是 \x00 字节 + if (start >= response_len) { + *trimmed_start = 0; + *trimmed_len = 0; + return false; + } + + // 从结尾跳过 \x00 字节(只跳过 \x00,保留其他字符) + size_t end = response_len; + while (end > start && response[end - 1] == '\0') { + end--; + } + + *trimmed_start = start; + *trimmed_len = end - start; + return true; +} + +static bool ppp_at_parse_response(const char *response, const char *expected) +{ + return ppp_at_parse_response_with_len(response, response ? strlen(response) : 0, expected); +} + +static bool ppp_at_parse_response_with_len(const char *response, size_t response_len, const char *expected) +{ + if (!response || response_len == 0) { + // 如果 response_len 为 0,尝试使用 strlen(处理正常情况) + if (response) { + response_len = strlen(response); + if (response_len == 0) { + return false; + } + } else { + return false; + } } - PPP_DEBUG_DEBUG("Parsing IPR response: [%s]", response); + // 第一步:trim 响应字符串,去除两头的 \x00 和空白字符 + size_t trimmed_start = 0; + size_t trimmed_len = 0; + const char *trimmed_response = response; + size_t trimmed_response_len = response_len; - /* 查找 "+IPR: " 字符串 */ - const char *ipr_pos = strstr(response, "+IPR: "); - if (!ipr_pos) { - PPP_DEBUG_WARN("IPR response format error: no '+IPR: ' found in response"); - return 0; + if (ppp_at_trim_response(response, response_len, &trimmed_start, &trimmed_len)) { + if (trimmed_len > 0) { + trimmed_response = response + trimmed_start; + trimmed_response_len = trimmed_len; + PPP_DEBUG_DEBUG("[响应解析] Trimmed response: original_len=%u, trimmed_start=%u, trimmed_len=%u", + (unsigned int)response_len, (unsigned int)trimmed_start, (unsigned int)trimmed_len); + } else { + // 全部是无效字符 + return false; + } } - /* 跳过 "+IPR: " 部分 */ - ipr_pos += 6; - - /* 跳过可能的空格 */ - while (*ipr_pos == ' ' || *ipr_pos == '\t') { - ipr_pos++; + // 第二步:跳过命令回显,获取实际响应内容 + // 注意:现在使用 trim 后的响应,不再需要手动跳过 \x00 字节 + char cleaned_response[PPP_AT_RESPONSE_MAX_LEN] = {0}; + const char *actual_response = ppp_at_skip_echo(trimmed_response, trimmed_response_len, cleaned_response, sizeof(cleaned_response)); + const char *search_response = trimmed_response; // 使用 trim 后的响应来查找 URC + size_t search_response_len = trimmed_response_len; // 使用 trim 后的响应长度 + if (!actual_response) { + actual_response = trimmed_response; // 如果清理失败,使用 trim 后的响应 } + + // 检查是否是命令响应中的URC格式(这些格式表示对命令的响应,即使包含ERROR也是有效的) + // 例如:+TCPCLOSE: ERROR, +TCPCLOSE: 0,OK, +TCPSETUP: 0,OK 等 + // 这些格式通常出现在发送AT命令后立即收到的响应中,即使包含ERROR也应该认为是有效的响应 - /* 解析波特率数字,只读取数字部分 */ - uint32_t baudrate = 0; - const char *num_start = ipr_pos; + // 特殊处理:+TCPRECV 和 +SSLTCPRECV URC 是异步数据接收,不应该作为命令响应来验证 + // 这些URC包含十六进制数据,可能会被误判为包含"ERROR"字符串 + // 在验证响应之前,需要排除这些URC,只验证命令本身的响应部分 + // 注意:+TCPRECV URC中的数据(如HTTP响应)是重要的协议内容,需要在应用层处理,不应该在这里验证 + // 注意:响应已经过 trim 处理,不再包含开头的 \x00 字节,可以直接使用 strstr + const char *tcprecv_pos = strstr(search_response, "+TCPRECV: "); + const char *ssltcprecv_pos = strstr(search_response, "+SSLTCPRECV: "); - /* 找到数字的结束位置(遇到非数字字符停止) */ - while (*ipr_pos >= '0' && *ipr_pos <= '9') { - baudrate = baudrate * 10 + (*ipr_pos - '0'); - ipr_pos++; + if (tcprecv_pos || ssltcprecv_pos) { + // 找到 +TCPRECV 或 +SSLTCPRECV URC,只验证URC之前的部分(命令响应部分) + const char *urc_start = tcprecv_pos ? tcprecv_pos : ssltcprecv_pos; + // 计算命令响应部分的长度(从原始响应开头到URC开始位置) + size_t command_response_len = urc_start - search_response; + if (command_response_len > 0) { + // 使用动态分配的缓冲区(在初始化时分配,避免栈上分配大缓冲区) + if (!g_at_command_response_buffer) { + // 缓冲区未分配,跳过验证(不影响功能,但可能影响错误检测) + PPP_DEBUG_WARN("[响应解析] Command response buffer not allocated, skipping validation"); + } else { + // 如果超过缓冲区大小,截断处理(这种情况很少见) + size_t max_copy_len = (command_response_len < 511) ? command_response_len : 511; + + // 从原始响应复制,跳过开头的 \x00 字节 + size_t copy_idx = 0; + for (size_t i = 0; i < command_response_len && copy_idx < max_copy_len; i++) { + if (search_response[i] != '\0') { + g_at_command_response_buffer[copy_idx++] = search_response[i]; + } + } + g_at_command_response_buffer[copy_idx] = '\0'; + + // 如果数据被截断,记录警告 + if (copy_idx >= max_copy_len && command_response_len > max_copy_len) { + PPP_DEBUG_WARN("[响应解析] Command response part truncated: original_len=%u, copied=%u", + (unsigned int)command_response_len, (unsigned int)copy_idx); + } + + // 递归调用,只验证命令响应部分 + bool result = ppp_at_parse_response(g_at_command_response_buffer, expected); + if (result) { + PPP_DEBUG_DEBUG("Response contains +TCPRECV/+SSLTCPRECV URC, validated command response part only (URC data will be processed by application layer)"); + return true; + } + // 如果命令响应部分验证失败,继续下面的错误检查(但排除URC部分) + // 注意:这里不直接返回false,而是继续检查,因为URC可能包含数据 + } + } + // 如果无法分离命令响应部分,继续下面的检查,但需要排除URC中的错误检查 } - /* 检查是否成功解析到数字 */ - if (ipr_pos == num_start) { - PPP_DEBUG_WARN("IPR response format error: no valid number found after '+IPR: '"); - return 0; + const char *command_response_urc_patterns[] = { + "+TCPCLOSE: ERR", "+TCPCLOSE: 0,OK", "+TCPCLOSE: ERROR", + "+SSLTCPCLOSE: ERR", "+SSLTCPCLOSE: 0,OK", "+SSLTCPCLOSE: ERROR", + "+TCPSETUP: 0,OK", "+TCPSETUP: 0,ERR", + "+SSLTCPSETUP: 0,OK", "+SSLTCPSETUP: 0,ERR" + }; + for (size_t i = 0; i < sizeof(command_response_urc_patterns) / sizeof(command_response_urc_patterns[0]); i++) { + if (strstr(actual_response, command_response_urc_patterns[i]) != NULL) { + // 命令响应URC格式(如+TCPCLOSE: ERROR)表示命令已确认,无论expected是什么都应该接受 + // 因为对于某些命令(如AT+TCPCLOSE),ERROR表示socket已经关闭或不存在,这是正常的 + PPP_DEBUG_DEBUG("Response contains command response URC format: %s, accepting as valid response", command_response_urc_patterns[i]); + return true; + } + } + + // 如果期望响应是 NULL(表示不验证特定字符串,但至少要检查是否有错误) + if (!expected) { + // 检查是否是提示符响应(如 AT+TCPSEND 命令返回的 ">" 提示符) + if (strstr(actual_response, ">") != NULL) { + PPP_DEBUG_DEBUG("Response contains '>' prompt (expected is NULL), accepting as valid response"); + return true; + } + // 检查是否包含 OK(表示成功) + if (strstr(actual_response, "OK") != NULL) { + PPP_DEBUG_DEBUG("Response contains 'OK' (expected is NULL), accepting as valid response"); + return true; + } + // 如果既没有 > 也没有 OK,继续检查是否有错误(ERROR/BUSY/NO CARRIER) + // 这些错误检查在下面的代码中处理,如果都没有,最后返回 false + } + + // 检查期望响应,优先检测期望响应 + // 特殊处理:如果期望是">"提示符,需要更精确的匹配(避免误匹配) + if (expected) { + if (strcmp(expected, ">") == 0) { + // 期望">"提示符,检查响应中是否包含独立的">"提示符 + // 注意:">"前面应该有换行符或空格,后面应该有换行符或字符串结束 + const char *prompt_pos = strstr(actual_response, ">"); + if (prompt_pos != NULL) { + // 检查">"前面是否有换行符或空格(避免误匹配) + if (prompt_pos == actual_response || + prompt_pos[-1] == '\n' || prompt_pos[-1] == '\r' || prompt_pos[-1] == ' ') { + // 检查">"后面是否有换行符或字符串结束(确保是独立的提示符) + if (prompt_pos[1] == '\0' || prompt_pos[1] == '\n' || + prompt_pos[1] == '\r' || prompt_pos[1] == ' ') { + PPP_DEBUG_DEBUG("Response contains '>' prompt (expected: >), accepting as valid response"); + return true; + } + } + } + } else { + // 普通字符串匹配 + if (strstr(actual_response, expected)) { + return true; + } + } } + + // 检查ABORT条件 (对应chat文件中的ABORT指令) + // 注意:如果响应包含命令响应URC格式,即使包含ERROR也不应该认为是ABORT + // 因为命令响应URC格式已经在上面处理了 + // 同时,需要排除 +TCPRECV/+SSLTCPRECV URC 部分,因为它们是异步数据,不应该影响命令响应验证 - /* 验证波特率是否合理 */ - if (baudrate < 1200 || baudrate > 4000000) { - PPP_DEBUG_WARN("IPR response contains unreasonable baudrate: %u", baudrate); - return 0; + // 确定要检查的响应范围(排除 +TCPRECV/+SSLTCPRECV URC) + const char *check_start = actual_response; + size_t check_len = search_response_len; // 使用原始响应长度 + const char *tcprecv_check = NULL; + const char *ssltcprecv_check = NULL; + + // 手动查找 +TCPRECV 或 +SSLTCPRECV,跳过 \x00 字节 + // 注意:在原始响应中查找,因为清理后的响应可能不包含 \x00 字节 + for (size_t i = 0; i < check_len; i++) { + if (search_response[i] == '\0') { + continue; // 跳过 \x00 字节 + } + if (i + 10 <= check_len && strncmp(search_response + i, "+TCPRECV: ", 10) == 0) { + tcprecv_check = search_response + i; + break; + } + if (i + 13 <= check_len && strncmp(search_response + i, "+SSLTCPRECV: ", 13) == 0) { + ssltcprecv_check = search_response + i; + break; + } } - PPP_DEBUG_DEBUG("Successfully parsed current baudrate from IPR response: %u", baudrate); - return baudrate; -} - -/** - * @brief 查询当前modem波特率 - * @return 当前modem波特率,失败返回0 - */ -static uint32_t ppp_query_current_modem_baudrate(void) -{ - ppp_at_response_t response; - ppp_error_t ret = ppp_at_send_command("AT+IPR?", "OK", PPP_AT_CMD_TIMEOUT, &response); + // 如果手动查找没找到,使用 strstr 作为后备(处理正常情况) + // 注意:strstr 在遇到 \x00 时会停止,所以可能找不到 + if (!tcprecv_check && !ssltcprecv_check) { + tcprecv_check = strstr(actual_response, "+TCPRECV: "); + ssltcprecv_check = strstr(actual_response, "+SSLTCPRECV: "); + } - if (ret != PPP_ERR_OK) { - PPP_DEBUG_WARN("Failed to query current baudrate with AT+IPR?, error: %d", ret); - return 0; + if (tcprecv_check || ssltcprecv_check) { + // 如果响应中包含 +TCPRECV/+SSLTCPRECV URC,只检查URC之前的部分 + const char *urc_check_start = tcprecv_check ? tcprecv_check : ssltcprecv_check; + check_len = urc_check_start - search_response; // 使用原始响应位置 } - return ppp_parse_ipr_response(response.response); + // 在排除URC后的响应范围内检查错误 + if (check_len > 0) { + // 检查 BUSY 和 NO CARRIER(只在命令响应部分检查,不包括URC) + if ((strstr(actual_response, "BUSY") && + (strstr(actual_response, "BUSY") - actual_response) < (int)check_len) || + (strstr(actual_response, "NO CARRIER") && + (strstr(actual_response, "NO CARRIER") - actual_response) < (int)check_len)) { + PPP_DEBUG_ERROR("AT command aborted: %.*s", (int)check_len, actual_response); + return false; + } + + // 对于普通的ERROR(不是命令响应URC格式),认为是ABORT + const char *error_pos = strstr(actual_response, "ERROR"); + if (error_pos && (error_pos - actual_response) < (int)check_len) { + // 检查是否是命令响应URC格式中的ERROR(已经在上面处理了) + bool is_command_response_error = false; + for (size_t i = 0; i < sizeof(command_response_urc_patterns) / sizeof(command_response_urc_patterns[0]); i++) { + const char *pattern_pos = strstr(actual_response, command_response_urc_patterns[i]); + if (pattern_pos && (pattern_pos - actual_response) < (int)check_len) { + is_command_response_error = true; + break; + } + } + if (!is_command_response_error) { + PPP_DEBUG_ERROR("AT command aborted: %.*s", (int)check_len, actual_response); + return false; + } + } + } + + return false; } /** - * @brief 设置固定波特率(内部函数) + * @brief 等待AT命令响应 - 异步版本(从环形缓冲区读取) + * + * 该函数从ppp_interrupt.c填充的环形缓冲区中异步读取AT响应数据 + * 提供同步的函数接口,但底层是异步的高速串口接收 */ -static ppp_error_t ppp_at_set_baudrate(uint32_t baudrate) +static ppp_error_t ppp_at_wait_response(ppp_at_response_t *response, uint32_t timeout) { - if (!g_at_ctx.initialized) { + PPP_DEBUG_DEBUG(">>> N706A AT WAIT_RESPONSE START (async mode, timeout: %u ms) <<<", timeout); + + if (!response || !g_at_response_buffer) { + PPP_DEBUG_ERROR("Invalid parameters: response=%p, buffer=%p", response, g_at_response_buffer); return PPP_ERR_INVALID_PARAM; } - /* 检查波特率是否支持 */ - bool supported = false; - for (int i = 0; i < PPP_BAUDRATE_FIXED_COUNT; i++) { - if (g_fixed_baudrates[i] == baudrate) { - supported = true; + uint32_t start_time = (uint32_t)osal_get_jiffies(); + uint32_t bytes_received = 0; + bool response_complete = false; + uint32_t last_accumulate_check_elapsed = 0; // 上次检查累积缓冲区的时间(相对于start_time) + uint32_t last_accumulate_length = 0; // 上次检查时的累积缓冲区长度(用于减少重复日志) + uint32_t accumulate_stuck_start_elapsed = 0; // 累积缓冲区开始卡住的时间(相对于start_time) + bool has_checked_accumulate_buffer_on_timeout = false; // 标记是否在超时时已检查过累积缓冲区 + + /* 清空全局缓存 */ + memset(g_at_response_buffer, 0, g_at_buffer_size); + + PPP_DEBUG_DEBUG("N706A AT: Waiting for response from async ring buffer (timeout: %u ms)", timeout); + + /* 循环从环形缓冲区读取数据直到完整响应或超时 */ + while (!response_complete) { + uint32_t current_time = (uint32_t)osal_get_jiffies(); + uint32_t elapsed = osal_jiffies_to_msecs(current_time - start_time); + + /* 检查超时 */ + if (elapsed >= timeout) { + /* 在超时前,检查累积缓冲区是否还有未处理的数据 */ + /* 如果有,说明响应可能还在处理中,再等待一段时间 */ + if (!has_checked_accumulate_buffer_on_timeout) { + osal_mutex_lock(&g_at_accumulate_mutex); + uint32_t accumulate_length = g_at_accumulate_length; + osal_mutex_unlock(&g_at_accumulate_mutex); + + if (accumulate_length > 0) { + /* 累积缓冲区还有数据,可能是响应还在处理中,再等待200ms */ + PPP_DEBUG_WARN("N706A AT: Timeout reached but accumulate buffer has %u bytes, waiting 200ms more", accumulate_length); + has_checked_accumulate_buffer_on_timeout = true; + timeout += 200; // 延长超时时间200ms + continue; // 继续等待 + } + has_checked_accumulate_buffer_on_timeout = true; + } + + PPP_DEBUG_WARN("N706A AT: Response timeout after %u ms", elapsed); break; } - } - - if (!supported) { - PPP_DEBUG_ERROR("Unsupported baudrate: %u", baudrate); - return PPP_ERR_INVALID_PARAM; - } -#if defined(PPP_DO_SERIAL_TEST) && (PPP_DO_SERIAL_TEST == 1) - uint32_t current_modem_baudrate = baudrate; // 测试模式下,直接使用输入的波特率 -#else - /* 先查询当前modem波特率 */ - uint32_t current_modem_baudrate = ppp_query_current_modem_baudrate(); - if (current_modem_baudrate == baudrate) { - PPP_DEBUG_INFO("Modem already at target baudrate %u, skipping AT+IPR command", baudrate); - /* 仍需要更新本地UART配置以确保一致性 */ - uart_attr_t uart_attr = { - .baud_rate = baudrate, - .data_bits = UART_DATA_BIT_8, - .parity = UART_PARITY_NONE, - .stop_bits = UART_STOP_BIT_1, - }; + /* 定期检查累积缓冲区是否有数据(每1秒检查一次,减少日志输出) */ + /* 如果累积缓冲区有数据但环形缓冲区没有,说明响应可能还在处理中 */ + uint32_t time_since_last_check = elapsed - last_accumulate_check_elapsed; + if (time_since_last_check >= 1000) { + osal_mutex_lock(&g_at_accumulate_mutex); + uint32_t accumulate_length = g_at_accumulate_length; + /* 获取最后几个字节用于分析(最多16字节) */ + uint8_t last_bytes[16] = {0}; + uint32_t preview_len = (accumulate_length < 16) ? accumulate_length : 16; + if (accumulate_length > 0 && preview_len > 0) { + uint32_t start_offset = (accumulate_length > preview_len) ? (accumulate_length - preview_len) : 0; + memcpy(last_bytes, g_at_accumulate_buffer + start_offset, preview_len); + } + osal_mutex_unlock(&g_at_accumulate_mutex); + + /* 只在累积缓冲区大小变化时打印日志,减少重复输出 */ + if (accumulate_length > 0 && accumulate_length != last_accumulate_length) { + /* 累积缓冲区大小变化,重置卡住计时 */ + accumulate_stuck_start_elapsed = elapsed; + /* 打印最后几个字节的十六进制,帮助分析问题(使用静态缓冲区避免栈溢出) */ + static char hex_str[48] = {0}; // 静态缓冲区,避免栈分配 + memset(hex_str, 0, sizeof(hex_str)); + /* 使用查表法替代snprintf,减少栈使用 */ + static const char hex_chars[] = "0123456789ABCDEF"; + for (uint32_t i = 0; i < preview_len && i * 3 + 3 < sizeof(hex_str); i++) { + hex_str[i * 3] = hex_chars[(last_bytes[i] >> 4) & 0x0F]; + hex_str[i * 3 + 1] = hex_chars[last_bytes[i] & 0x0F]; + hex_str[i * 3 + 2] = ' '; + } + PPP_DEBUG_DEBUG("N706A AT: Accumulate buffer has %u bytes, response may be processing, last %u bytes: %s", + accumulate_length, preview_len, hex_str); + last_accumulate_length = accumulate_length; + } else if (accumulate_length == 0 && last_accumulate_length > 0) { + /* 累积缓冲区清空时也打印一次 */ + accumulate_stuck_start_elapsed = 0; + PPP_DEBUG_DEBUG("N706A AT: Accumulate buffer cleared"); + last_accumulate_length = 0; + } else if (accumulate_length > 0 && accumulate_length == last_accumulate_length) { + /* 累积缓冲区大小不变,检查是否卡住超过5秒 */ + if (accumulate_stuck_start_elapsed == 0) { + accumulate_stuck_start_elapsed = elapsed; // 第一次检测到卡住 + } else { + uint32_t stuck_duration = elapsed - accumulate_stuck_start_elapsed; + if (stuck_duration >= 5000) { + /* 累积缓冲区卡住超过5秒,打印警告和最后几个字节(使用静态缓冲区避免栈溢出) */ + static char hex_str[48] = {0}; // 静态缓冲区,避免栈分配 + memset(hex_str, 0, sizeof(hex_str)); + /* 使用查表法替代snprintf,减少栈使用 */ + static const char hex_chars[] = "0123456789ABCDEF"; + for (uint32_t i = 0; i < preview_len && i * 3 + 3 < sizeof(hex_str); i++) { + hex_str[i * 3] = hex_chars[(last_bytes[i] >> 4) & 0x0F]; + hex_str[i * 3 + 1] = hex_chars[last_bytes[i] & 0x0F]; + hex_str[i * 3 + 2] = ' '; + } + PPP_DEBUG_WARN("N706A AT: Accumulate buffer stuck at %u bytes for %u ms, last %u bytes: %s", + accumulate_length, stuck_duration, preview_len, hex_str); + accumulate_stuck_start_elapsed = elapsed; // 重置计时,避免重复打印 + } + } + } + last_accumulate_check_elapsed = elapsed; + } - if (uapi_uart_set_attr(g_at_ctx.uart_bus, &uart_attr) != ERRCODE_SUCC) { - PPP_DEBUG_ERROR("Failed to update UART baudrate"); - return PPP_ERR_UART; + /* 等待信号量(有新数据到达),使用剩余超时时间 */ + uint32_t remaining_timeout = timeout - elapsed; + if (remaining_timeout > 100) { + remaining_timeout = 100; // 最多等100ms,然后检查一次 } - - g_at_ctx.current_baudrate = baudrate; - g_at_ctx.auto_baudrate_mode = false; - PPP_DEBUG_DEBUG("Baudrate confirmed at %u (no change needed)", baudrate); - return PPP_ERR_OK; - } - - /* 需要设置新波特率 */ - PPP_DEBUG_INFO("Changing modem baudrate from %u to %u", current_modem_baudrate, baudrate); - - /* 发送AT+IPR命令设置波特率 */ - char cmd[64]; - snprintf_s(cmd, sizeof(cmd), sizeof(cmd) - 1, "AT+IPR=%u", baudrate); - - ppp_at_response_t response; - ppp_error_t ret = ppp_at_send_command(cmd, "OK", PPP_AT_CMD_TIMEOUT, &response); - - if (ret != PPP_ERR_OK) { - PPP_DEBUG_ERROR("Failed to set baudrate to %u", baudrate); - return ret; + int sem_result = osal_sem_down_timeout(&g_at_response_sem, remaining_timeout); + if (sem_result == OSAL_SUCCESS || sem_result == OSAL_FAILURE) { + /* 有新数据到达或信号量操作失败,尝试读取环形缓冲区 */ + osal_mutex_lock(&g_at_response_mutex); + + /* 从环形缓冲区读取数据到响应缓冲区 */ + while (g_at_response_tail != g_at_response_head && + bytes_received < (g_at_buffer_size - 1)) { + g_at_response_buffer[bytes_received] = g_at_response_ring_buffer[g_at_response_tail]; + g_at_response_tail = (g_at_response_tail + 1) % N706A_AT_RESPONSE_RING_BUFFER_SIZE; + bytes_received++; + } + + osal_mutex_unlock(&g_at_response_mutex); + + /* 设置字符串结束符 */ + if (bytes_received > 0) { + g_at_response_buffer[bytes_received] = '\0'; + + PPP_DEBUG_DEBUG("N706A AT: Read %u bytes from ring buffer", bytes_received); + + /* 检查是否收到完整响应 */ + if (ppp_at_check_response_complete((char*)g_at_response_buffer, bytes_received)) { + PPP_DEBUG_DEBUG("N706A AT: Complete response detected"); + response_complete = true; + break; + } + } + } + + /* 短暂延迟避免CPU占用过高 */ + if (!response_complete) { + osal_msleep(10); + } } -#endif - /* 更新串口配置 */ - uart_attr_t uart_attr = { - .baud_rate = baudrate, - .data_bits = UART_DATA_BIT_8, - .parity = UART_PARITY_NONE, - .stop_bits = UART_STOP_BIT_1, - }; - if (uapi_uart_set_attr(g_at_ctx.uart_bus, &uart_attr) != ERRCODE_SUCC) { - PPP_DEBUG_ERROR("Failed to update UART baudrate"); - return PPP_ERR_UART; + /* 处理结果 */ + if (bytes_received > 0) { + uint32_t copy_len = (bytes_received < PPP_AT_RESPONSE_MAX_LEN - 1) ? + bytes_received : PPP_AT_RESPONSE_MAX_LEN - 1; + + memcpy_s(response->response, PPP_AT_RESPONSE_MAX_LEN, + g_at_response_buffer, copy_len); + response->response[copy_len] = '\0'; + response->length = copy_len; + + uint32_t final_time = (uint32_t)osal_get_jiffies(); + uint32_t total_elapsed = osal_jiffies_to_msecs(final_time - start_time); + + PPP_DEBUG_DEBUG("N706A AT: SUCCESS - %u bytes in %u ms", bytes_received, total_elapsed); + PPP_DEBUG_DEBUG(">>> N706A AT WAIT_RESPONSE END (SUCCESS) <<<"); + return PPP_ERR_OK; + } else { + uint32_t final_time = (uint32_t)osal_get_jiffies(); + uint32_t total_elapsed = osal_jiffies_to_msecs(final_time - start_time); + + PPP_DEBUG_WARN("N706A AT: FAILED - No response after %u ms", total_elapsed); + PPP_DEBUG_DEBUG(">>> N706A AT WAIT_RESPONSE END (FAILED) <<<"); + response->length = 0; + response->response[0] = '\0'; + return PPP_ERR_TIMEOUT; } - - g_at_ctx.current_baudrate = baudrate; - g_at_ctx.auto_baudrate_mode = false; - - PPP_DEBUG_DEBUG("Baudrate set bus(%d) to %u (changed from %u)", g_at_ctx.uart_bus, baudrate, current_modem_baudrate); - return PPP_ERR_OK; } /** - * @brief 发送AT命令(内部函数) - * @note 检查内存泄露:所有malloc分配的内存都应在所有分支free + * @brief 解析AT+CESQ响应 + * 参考N706A AT命令手册: +CESQ: ,,,,, */ -static ppp_error_t ppp_at_send_command(const char *command, const char *expected_response, - uint32_t timeout, ppp_at_response_t *response) +static ppp_error_t ppp_parse_cesq_response(const char *response, n706a_network_signal_info_t *info) { - uint32_t start_timestamp = (uint32_t)osal_get_jiffies(); - - PPP_DEBUG_CRITICAL("=== AT COMMAND START ==="); - PPP_DEBUG_CRITICAL("Timestamp: %u", start_timestamp); - PPP_DEBUG_CRITICAL("Command: [%s]", command ? command : "NULL"); - PPP_DEBUG_CRITICAL("Expected: [%s]", expected_response ? expected_response : "NULL"); - PPP_DEBUG_CRITICAL("Timeout: %u ms", timeout); - - if (!g_at_ctx.initialized || !command || !response) { - PPP_DEBUG_ERROR("Invalid parameters: initialized=%d, command=%p, response=%p", - g_at_ctx.initialized, command, response); + if (!response || !info) { return PPP_ERR_INVALID_PARAM; } - /* 防止系统卡死的安全措施 */ - const uint32_t MAX_COMMAND_TIMEOUT = 60000; // 绝对最大超时时间60秒 - if (timeout > MAX_COMMAND_TIMEOUT) { - PPP_DEBUG_WARN("Command timeout %u ms exceeds maximum %u ms, capping it", - timeout, MAX_COMMAND_TIMEOUT); - timeout = MAX_COMMAND_TIMEOUT; + memset_s(info, sizeof(n706a_network_signal_info_t), 0, sizeof(n706a_network_signal_info_t)); + + // 查找+CESQ:开头的行 + const char *cesq_line = strstr(response, "+CESQ:"); + if (!cesq_line) { + PPP_DEBUG_WARN("No +CESQ: found in response"); + return PPP_ERR_AT_FAILED; } - PPP_DEBUG_CRITICAL("Step 1: Attempting to acquire AT mutex..."); - uint32_t mutex_start = (uint32_t)osal_get_jiffies(); + // 跳过"+CESQ:" + cesq_line += 6; + while (*cesq_line == ' ' || *cesq_line == '\t') cesq_line++; - /* 使用带超时的互斥锁获取,防止永久阻塞 */ - int mutex_result = osal_mutex_lock_timeout(&g_at_ctx.at_mutex, timeout + 5000); - if (mutex_result != OSAL_SUCCESS) { - PPP_DEBUG_ERROR("Failed to acquire AT mutex within timeout, this indicates a serious problem"); - PPP_DEBUG_ERROR("Possible causes: 1) Previous AT command hung 2) Mutex corruption 3) Deadlock"); - return PPP_ERR_TIMEOUT; + // +CESQ: ,,,,, + int rxlev = 99, ber = 99, rscp = 255, ecno = 255, rsrq = 255, rsrp = 255; + int parsed = sscanf(cesq_line, "%d,%d,%d,%d,%d,%d", &rxlev, &ber, &rscp, &ecno, &rsrq, &rsrp); + + if (parsed == 6) { + info->valid = true; + info->rxlev = rxlev; + info->ber = ber; + info->rscp = rscp; + info->ecno = ecno; + info->rsrq = rsrq; + info->rsrp = rsrp; + PPP_DEBUG_INFO("CESQ parsed successfully: rxlev=%d, ber=%d, rscp=%d, ecno=%d, rsrq=%d, rsrp=%d", + rxlev, ber, rscp, ecno, rsrq, rsrp); + return PPP_ERR_OK; + } else { + PPP_DEBUG_WARN("Failed to parse CESQ response, parsed: %d", parsed); + return PPP_ERR_AT_FAILED; } +} - uint32_t mutex_end = (uint32_t)osal_get_jiffies(); - PPP_DEBUG_CRITICAL("Step 1: AT mutex acquired in %u ms", osal_jiffies_to_msecs(mutex_end - mutex_start)); +/** + * @brief 获取网络信号信息 (AT+CESQ) + */ +ppp_error_t ppp_at_get_network_signal_info(n706a_network_signal_info_t *info) +{ + if (!g_at_ctx.initialized || !info) { + return PPP_ERR_INVALID_PARAM; + } + // N706A_NET模式下始终处于AT模式,无需检查 + // (PPP模式需要检查是否成功切换到AT模式) + + PPP_DEBUG_INFO("Getting network signal information (AT+CESQ)..."); - PPP_DEBUG_DEBUG("=== AT Command Send ==="); - PPP_DEBUG_DEBUG("Command: %s", command); - PPP_DEBUG_DEBUG("Expected: %s", expected_response ? expected_response : "None"); - PPP_DEBUG_DEBUG("Timeout: %u ms", timeout); + ppp_at_response_t response; + ppp_error_t ret = ppp_at_send_command("AT+CESQ", "OK", 5000, &response); - /* 清空响应缓冲区 */ - memset_s(response, sizeof(ppp_at_response_t), 0, sizeof(ppp_at_response_t)); + if (ret != PPP_ERR_OK) { + PPP_DEBUG_ERROR("AT+CESQ command failed: %d", ret); + return ret; + } - /* 发送命令 */ - size_t command_len = strlen(command); - char *cmd_with_crlf = (char *)malloc(command_len + 4); - if (!cmd_with_crlf) { - PPP_DEBUG_ERROR("Failed to allocate memory for cmd_with_crlf"); - osal_mutex_unlock(&g_at_ctx.at_mutex); - return PPP_ERR_MEMORY; + // 解析响应 + ret = ppp_parse_cesq_response(response.response, info); + if (ret != PPP_ERR_OK) { + PPP_DEBUG_ERROR("Failed to parse CESQ response"); + return ret; } - snprintf_s(cmd_with_crlf, command_len + 4, command_len + 3, "%s\r\n", command); - uint32_t cmd_len = strlen(cmd_with_crlf); + // 打印详细信息 + PPP_DEBUG_INFO("=== Network Signal Information (AT+CESQ) ==="); + PPP_DEBUG_INFO("rxlev: %d", info->rxlev); + PPP_DEBUG_INFO("ber: %d", info->ber); + PPP_DEBUG_INFO("rscp: %d", info->rscp); + PPP_DEBUG_INFO("ecno: %d", info->ecno); + PPP_DEBUG_INFO("rsrq: %d", info->rsrq); + PPP_DEBUG_INFO("rsrp: %d", info->rsrp); - /* 打印发送的字符串(可见字符) */ - char *printable_cmd = (char *)malloc(PPP_AT_CMD_MAX_LEN + 20); - if (printable_cmd == NULL) { - PPP_DEBUG_ERROR("Failed to allocate memory for printable command"); - free(cmd_with_crlf); // 防止内存泄露 - osal_mutex_unlock(&g_at_ctx.at_mutex); - return PPP_ERR_MEMORY; + // LTE信号质量评估 - 仅关注RSRQ和RSRP + PPP_DEBUG_INFO("=== LTE Signal Quality Analysis ==="); + + // RSRQ评估 (0-49有效,255无效) + if (info->rsrq == 255) { + PPP_DEBUG_INFO("RSRQ: 无法获取LTE信号质量"); + } else if (info->rsrq >= 0 && info->rsrq <= 49) { + // RSRQ转换公式: 实际值(dB) = (rsrq - 94)/10 + float rsrq_db = (info->rsrq - 94) / 10.0f; + PPP_DEBUG_INFO("RSRQ: %d (%.1f dB)", info->rsrq, rsrq_db); + + // RSRQ评估标准 (范围: -19.5dB ~ -3dB,数值越大质量越好) + if (rsrq_db >= -6.0) { + PPP_DEBUG_INFO("RSRQ评级: 极佳 (信号质量优秀)"); + } else if (rsrq_db >= -9.0) { + PPP_DEBUG_INFO("RSRQ评级: 良好 (信号质量稳定)"); + } else if (rsrq_db >= -12.0) { + PPP_DEBUG_INFO("RSRQ评级: 一般 (信号质量可用)"); + } else { + PPP_DEBUG_INFO("RSRQ评级: 较差 (信号质量不佳)"); + } + } else { + PPP_DEBUG_INFO("RSRQ: %d (超出有效范围0-49)", info->rsrq); } - - uint32_t j = 0; - for (uint32_t i = 0; i < cmd_len && j < (PPP_AT_CMD_MAX_LEN + 20) - 10; i++) { - if (cmd_with_crlf[i] == '\r') { - printable_cmd[j++] = '\\'; - printable_cmd[j++] = 'r'; - } else if (cmd_with_crlf[i] == '\n') { - printable_cmd[j++] = '\\'; - printable_cmd[j++] = 'n'; - } else if (cmd_with_crlf[i] >= 32 && cmd_with_crlf[i] <= 126) { - printable_cmd[j++] = cmd_with_crlf[i]; + + // RSRP评估 (0-97有效,255无效) + if (info->rsrp == 255) { + PPP_DEBUG_INFO("RSRP: 无法获取LTE信号强度"); + } else if (info->rsrp >= 0 && info->rsrp <= 97) { + // RSRP转换公式: 实际值(dBm) = rsrp - 140 + float rsrp_dbm = info->rsrp - 140.0f; + PPP_DEBUG_INFO("RSRP: %d (%.0f dBm)", info->rsrp, rsrp_dbm); + + // RSRP评估标准 (范围: -140dBm ~ -43dBm,数值越大信号越强) + if (rsrp_dbm >= -80) { + PPP_DEBUG_INFO("RSRP评级: 极佳 (信号强度优秀)"); + } else if (rsrp_dbm >= -90) { + PPP_DEBUG_INFO("RSRP评级: 良好 (信号强度稳定)"); + } else if (rsrp_dbm >= -100) { + PPP_DEBUG_INFO("RSRP评级: 一般 (信号强度可用)"); } else { - j += snprintf_s(printable_cmd + j, (PPP_AT_CMD_MAX_LEN + 20) - j, (PPP_AT_CMD_MAX_LEN + 20) - j - 1, "\\x%02X", (uint8_t)cmd_with_crlf[i]); + PPP_DEBUG_INFO("RSRP评级: 较差 (信号强度不足)"); } + } else { + PPP_DEBUG_INFO("RSRP: %d (超出有效范围0-97)", info->rsrp); } - printable_cmd[j] = '\0'; - PPP_DEBUG_DEBUG("Send String: %s", printable_cmd); - free(printable_cmd); - - PPP_DEBUG_CRITICAL("Step 2: Sending AT command via UART..."); - uint32_t uart_write_start = (uint32_t)osal_get_jiffies(); - - int32_t write_result = uapi_uart_write(g_at_ctx.uart_bus, (const uint8_t *)cmd_with_crlf, cmd_len, 0); - uint32_t uart_write_end = (uint32_t)osal_get_jiffies(); - - free(cmd_with_crlf); - - PPP_DEBUG_CRITICAL("Step 2: UART write completed in %u ms, result: %d (expected: %u)", - osal_jiffies_to_msecs(uart_write_end - uart_write_start), write_result, cmd_len); - - if (write_result != cmd_len) { - PPP_DEBUG_ERROR("Failed to send AT command: wrote %d bytes, expected %u", write_result, cmd_len); - osal_mutex_unlock(&g_at_ctx.at_mutex); - return PPP_ERR_UART; + + // 综合LTE信号质量评估 + if (info->rsrq != 255 && info->rsrp != 255 && + info->rsrq >= 0 && info->rsrq <= 49 && + info->rsrp >= 0 && info->rsrp <= 97) { + + float rsrq_db = (info->rsrq - 94) / 10.0f; + float rsrp_dbm = info->rsrp - 140.0f; + + const char *overall_quality = "未知"; + if (rsrq_db >= -6.0 && rsrp_dbm >= -80) { + overall_quality = "LTE信号极佳 (质量和强度都优秀)"; + } else if (rsrq_db >= -9.0 && rsrp_dbm >= -90) { + overall_quality = "LTE信号良好 (质量和强度都稳定)"; + } else if (rsrq_db >= -12.0 && rsrp_dbm >= -100) { + overall_quality = "LTE信号一般 (质量和强度可用)"; + } else { + overall_quality = "LTE信号较差 (需要改善网络环境)"; + } + + PPP_DEBUG_INFO("=== 综合评估: %s ===", overall_quality); + } else { + PPP_DEBUG_INFO("=== 综合评估: LTE参数无效,无法评估 ==="); } - PPP_DEBUG_CRITICAL("Step 2: AT command sent successfully on bus %d", g_at_ctx.uart_bus); + PPP_DEBUG_INFO("=== End Network Signal Information ==="); - /* 等待响应 */ - PPP_DEBUG_CRITICAL("Step 3: Starting to wait for AT response..."); - uint32_t response_wait_start = (uint32_t)osal_get_jiffies(); - ppp_error_t ret = ppp_at_wait_response(response, timeout); - uint32_t response_wait_end = (uint32_t)osal_get_jiffies(); - PPP_DEBUG_CRITICAL("Step 3: AT response wait completed in %u ms, result: %d", - osal_jiffies_to_msecs(response_wait_end - response_wait_start), ret); + return PPP_ERR_OK; +} - /* 打印响应结果 */ - PPP_DEBUG_DEBUG("=== AT Command Response ==="); - if (ret == PPP_ERR_OK) { - PPP_DEBUG_DEBUG("Response received (%u bytes):", response->length); +/* ========== 导出的N706A API包装函数(用于n706a_tcp_client和n706a_net_at_cmd) ========== */ - /* 打印接收的字符串(可见字符) */ - char *printable_resp = (char *)malloc(PPP_AT_RESPONSE_MAX_LEN + 100); - if (printable_resp != NULL) { - uint32_t j = 0; - for (uint32_t i = 0; i < response->length && j < (PPP_AT_RESPONSE_MAX_LEN + 100) - 10; i++) { - if (response->response[i] == '\r') { - printable_resp[j++] = '\\'; - printable_resp[j++] = 'r'; - } else if (response->response[i] == '\n') { - printable_resp[j++] = '\\'; - printable_resp[j++] = 'n'; - } else if (response->response[i] >= 32 && response->response[i] <= 126) { - printable_resp[j++] = response->response[i]; - } else { - j += snprintf_s(printable_resp + j, (PPP_AT_RESPONSE_MAX_LEN + 100) - j, (PPP_AT_RESPONSE_MAX_LEN + 100) - j - 1, "\\x%02X", (uint8_t)response->response[i]); - } +/** + * @brief N706A对外导出:发送AT命令并等待响应 + */ +n706a_at_error_t n706a_at_send_command(const char *command, + const char *expected_response, + uint32_t timeout, + n706a_at_response_t *response) +{ + ppp_at_response_t ppp_resp; + ppp_error_t ret = ppp_at_send_command(command, expected_response, timeout, &ppp_resp); + + if (response) { + /* 转换ppp_at_response_t到n706a_at_response_t */ + memcpy_s(response->response, sizeof(response->response), ppp_resp.response, sizeof(ppp_resp.response)); + response->length = ppp_resp.length; + response->success = ppp_resp.success; + + /* 检查响应中是否包含ERROR字符串(即使ppp_at_send_command返回成功) */ + bool has_error = (strstr((const char *)response->response, "ERROR") != NULL); + + if (ret == PPP_ERR_OK && !has_error && ppp_resp.success) { + response->error_code = N706A_AT_OK; + } else { + response->error_code = N706A_AT_ERR_AT_FAILED; + response->success = false; + if (has_error) { + PPP_DEBUG_ERROR("[AT] Response contains ERROR: %s", response->response); } - printable_resp[j] = '\0'; - PPP_DEBUG_DEBUG("Response String: %s", printable_resp); - free(printable_resp); } - PPP_DEBUG_DEBUG("Raw Response: %s", response->response); + } + + /* 如果响应中包含ERROR,即使ppp_at_send_command返回成功,也应该返回失败 */ + bool has_error = (response && strstr((const char *)response->response, "ERROR") != NULL); + if (ret == PPP_ERR_OK && !has_error) { + return N706A_AT_OK; } else { - PPP_DEBUG_ERROR("No response received (error: %d)", ret); + return N706A_AT_ERR_AT_FAILED; + } +} + +/** + * @brief N706A对外导出:测试AT通信是否正常 + */ +n706a_at_error_t n706a_at_test_communication(void) +{ + if (!g_at_ctx.initialized) { + return N706A_AT_ERR_INVALID_PARAM; + } + + n706a_at_response_t response; + n706a_at_error_t ret = n706a_at_send_command("AT", "OK", 3000, &response); + + if (ret == N706A_AT_OK && response.success) { + PPP_DEBUG_INFO("AT communication test passed\n"); + return N706A_AT_OK; + } else { + PPP_DEBUG_ERROR("AT communication test failed\n"); + return N706A_AT_ERR_AT_FAILED; } +} +/** + * @brief N706A对外导出:获取网络信号信息 + */ +n706a_at_error_t n706a_at_get_network_signal_info(n706a_network_signal_info_t *signal_info) +{ + if (!signal_info) { + return N706A_AT_ERR_INVALID_PARAM; + } + + /* 调用内部的ppp_at_get_network_signal_info */ + n706a_network_signal_info_t ppp_signal; + ppp_error_t ret = ppp_at_get_network_signal_info(&ppp_signal); + if (ret == PPP_ERR_OK) { - if (expected_response) { - if (!ppp_at_parse_response(response->response, expected_response)) { - PPP_DEBUG_WARN("Response validation failed - Expected: %s", expected_response); - PPP_DEBUG_WARN("Response validation failed - Received: %s", response->response); - response->success = false; - response->error_code = PPP_ERR_AT_FAILED; - ret = PPP_ERR_AT_FAILED; - } else { - PPP_DEBUG_DEBUG("Response validation successful - Expected: %s", expected_response); - response->success = true; - response->error_code = PPP_ERR_OK; - } - } else { - // 没有期望响应时,只要收到响应就认为成功 - response->success = true; - response->error_code = PPP_ERR_OK; - } + /* 转换数据结构 */ + signal_info->rxlev = ppp_signal.rxlev; + signal_info->ber = ppp_signal.ber; + signal_info->rscp = ppp_signal.rscp; + signal_info->ecno = ppp_signal.ecno; + signal_info->rsrq = ppp_signal.rsrq; + signal_info->rsrp = ppp_signal.rsrp; + signal_info->valid = ppp_signal.valid; + return N706A_AT_OK; } else { - response->success = false; - response->error_code = ret; + signal_info->valid = false; + return N706A_AT_ERR_AT_FAILED; } +} - PPP_DEBUG_DEBUG("=== AT Command Complete (Result: %s) ===", ret == PPP_ERR_OK ? "SUCCESS" : "FAILED"); - - PPP_DEBUG_CRITICAL("Step 4: Releasing AT mutex..."); - uint32_t mutex_unlock_start = (uint32_t)osal_get_jiffies(); - osal_mutex_unlock(&g_at_ctx.at_mutex); - uint32_t mutex_unlock_end = (uint32_t)osal_get_jiffies(); +/** + * @brief N706A对外导出:设置波特率 + */ +n706a_at_error_t n706a_at_set_baudrate(uint32_t baudrate) +{ + ppp_error_t ret = ppp_at_set_baudrate(baudrate); + return (ret == PPP_ERR_OK) ? N706A_AT_OK : N706A_AT_ERR_UART; +} - uint32_t total_time = osal_jiffies_to_msecs(mutex_unlock_end - start_timestamp); - PPP_DEBUG_CRITICAL("Step 4: AT mutex released in %u ms", osal_jiffies_to_msecs(mutex_unlock_end - mutex_unlock_start)); - PPP_DEBUG_CRITICAL("=== AT COMMAND END (Total: %u ms, Result: %d) ===", total_time, ret); +/** + * @brief N706A对外导出:获取当前波特率 + */ +uint32_t n706a_at_get_current_baudrate(void) +{ + return g_at_ctx.current_baudrate; +} - return ret; +/** + * @brief N706A对外导出:发送原始数据(不带AT命令格式) + * @param data 要发送的数据 + * @param len 数据长度 + * @param timeout 超时时间(ms) + * @return 错误码 + * @note 用于 HTTP/HTTPS POST/PUT/DELETE/HEAD 请求,在 AT+HTTPACTION=99 命令后使用 + */ +n706a_at_error_t n706a_at_send_raw_data(const void *data, size_t len, uint32_t timeout) +{ + ppp_error_t ret = ppp_at_send_raw_data(data, len, timeout); + if (ret == PPP_ERR_OK) { + return N706A_AT_OK; + } else if (ret == PPP_ERR_TIMEOUT) { + return N706A_AT_ERR_TIMEOUT; + } else if (ret == PPP_ERR_MEMORY) { + return N706A_AT_ERR_MEMORY; + } else { + return N706A_AT_ERR_UART; + } } /** - * @brief 等待 '>' 提示符(用于 AT+TCPSEND/AT+SSLTCPSEND) - * @param timeout 超时时间(毫秒) - * @return PPP_ERR_OK 成功,其他值失败 + * @brief 解析 +TCPRECV URC + * @param urc_data URC数据(以 "+TCPRECV: " 开头) + * @param urc_len URC数据长度 + * @param socket_id 输出:socket ID + * @param data_len 输出:数据长度 + * - ASCII模式:二进制数据长度(字节数) + * - HEX模式:二进制数据长度(字节数),HEX字符串长度 = data_len * 2 + * @param data 输出:数据指针(指向 urc_data 内部) + * - ASCII模式:指向ASCII数据 + * - HEX模式:指向HEX字符串 + * @return true=解析成功,false=失败 * - * 根据N706A AT命令手册,AT+TCPSEND发送后会收到 '>' 提示符, - * 此时需要延迟50-100ms后再发送数据。 + * URC格式(ASCII模式):+TCPRECV: ,,\r\n + * 例如:+TCPRECV: 0,10,1234567890\r\n (10字节ASCII数据) + * 注意:是二进制数据长度(10) + * + * URC格式(HEX模式):+TCPRECV: ,,\r\n + * 例如:+TCPRECV: 0,3,313233\r\n (表示3字节二进制数据:0x31, 0x32, 0x33,HEX字符串长度为6) + * 注意:是二进制数据长度(3),HEX字符串长度 = binary_len × 2 = 6 + * + * 与AT+TCPSEND的对应关系: + * - AT+TCPSEND的length参数:二进制数据长度(字节数) + * - +TCPRECV的length参数:二进制数据长度(字节数),HEX字符串长度 = length × 2 */ -static ppp_error_t ppp_at_wait_for_prompt(uint32_t timeout) +static bool n706a_parse_tcprecv_urc(const char *urc_data, size_t urc_len, + int *socket_id, size_t *data_len, const uint8_t **data) { - ppp_at_response_t response; - ppp_error_t ret = ppp_at_wait_response(&response, timeout); + if (!urc_data || !socket_id || !data_len || !data || urc_len < 15) { + return false; + } - if (ret != PPP_ERR_OK) { - PPP_DEBUG_ERROR("Failed to wait for '>' prompt: error %d", ret); - return ret; + /* ⚠️ 关键:正确识别标识符类型,只跳过标识符本身,不跳过标识符后的任何字符 */ + const char *p = urc_data; + if (memcmp(p, "+TCPRECV: ", 10) == 0) { + p += 10; /* 跳过 "+TCPRECV: " (10字节) */ + } else if (memcmp(p, "+SSLTCPRECV: ", 13) == 0) { + p += 13; /* 跳过 "+SSLTCPRECV: " (13字节) */ + } else { + PPP_DEBUG_ERROR("URC parse error: invalid identifier, expected '+TCPRECV: ' or '+SSLTCPRECV: '"); + return false; } - /* 检查响应中是否包含 '>' */ - if (strstr((const char *)response.response, ">") == NULL) { - PPP_DEBUG_ERROR("Response does not contain '>' prompt: %s", response.response); - return PPP_ERR_AT_FAILED; + const char *end = urc_data + urc_len; + + // 解析 socket_id + *socket_id = 0; + while (p < end && *p >= '0' && *p <= '9') { + *socket_id = *socket_id * 10 + (*p - '0'); + p++; } - PPP_DEBUG_DEBUG("Received '>' prompt, delaying 75ms before sending data..."); + if (p >= end || *p != ',') { + PPP_DEBUG_ERROR("URC parse error: no comma after socket_id"); + return false; + } + p++; // 跳过逗号 - /* 延迟75ms(在50-100ms范围内,取中间值) */ - osal_msleep(75); + // 解析 length(HEX模式下这是二进制数据的长度,HEX字符串长度 = length × 2) + // ⚠️ 关键:添加溢出检查,防止 data_len 解析错误导致后续计算错误 + *data_len = 0; + const size_t MAX_REASONABLE_DATA_LEN = 65536; // 最大合理的数据长度(64KB) + while (p < end && *p >= '0' && *p <= '9') { + size_t old_data_len = *data_len; + *data_len = *data_len * 10 + (*p - '0'); + // 检查溢出:如果新值小于旧值,说明溢出 + if (*data_len < old_data_len || *data_len > MAX_REASONABLE_DATA_LEN) { + PPP_DEBUG_ERROR("URC parse error: data_len overflow or too large! value=%u (max=%u), stopping parse at position %u", + (unsigned int)*data_len, (unsigned int)MAX_REASONABLE_DATA_LEN, (unsigned int)(p - urc_data)); + // 截断到最大值,避免后续计算错误 + *data_len = MAX_REASONABLE_DATA_LEN; + // 跳过剩余的数字 + while (p < end && *p >= '0' && *p <= '9') { + p++; + } + break; + } + p++; + } - return PPP_ERR_OK; + if (p >= end || *p != ',') { + PPP_DEBUG_ERROR("URC parse error: no comma after length (parsed data_len=%u)", (unsigned int)*data_len); + return false; + } + p++; // 跳过逗号 + + // 数据起始位置 + *data = (const uint8_t *)p; + + // 验证数据长度是否合理 + // 注意:data_len是二进制数据长度,在HEX模式下,实际需要的HEX字符串长度 = data_len × 2 + // 这里只做基本验证(检查是否有至少 data_len 字节的数据),具体模式判断由调用者处理 + size_t remaining = end - p; + // 在HEX模式下,实际需要的数据长度是 data_len * 2,但这里只检查是否有至少 data_len 字节 + // 如果数据不足,会在后续的HEX解码中处理(通过检查实际HEX字符串长度) + if (*data_len > remaining) { + PPP_DEBUG_WARN("URC data_len=%u (binary) exceeds remaining=%u, truncating (hex_mode may need %u bytes)", + (unsigned int)*data_len, (unsigned int)remaining, (unsigned int)(*data_len * 2)); + *data_len = remaining; + /* ⚠️ 关键:不移除尾部的 \r\n,因为它们是URC数据的一部分 */ + /* 如果移除,会导致数据丢失,特别是TCP数据以换行符结尾的情况 */ + } + + PPP_DEBUG_DEBUG("Parsed URC: socket=%d, data_len=%u", *socket_id, (unsigned int)*data_len); + return true; } /** - * @brief 发送原始数据(不带AT命令格式) - * @note 用于 TCP/SSL 数据发送,在 AT+TCPSEND/AT+SSLTCPSEND 命令后使用 - * @note 调用此函数前,必须先调用 ppp_at_send_command 发送 AT+TCPSEND 命令并等待 '>' 提示符 + * @brief 解析 +TCPRECV URC(公共版本,供其他模块使用) */ -ppp_error_t ppp_at_send_raw_data(const void *data, size_t len, uint32_t timeout) +bool n706a_parse_tcprecv_urc_public(const char *urc_data, size_t urc_len, + int *socket_id, size_t *data_len, const uint8_t **data) { - if (!g_at_ctx.initialized || !data || len == 0) { - PPP_DEBUG_ERROR("Invalid parameters: initialized=%d, data=%p, len=%zu", - g_at_ctx.initialized, data, len); - return PPP_ERR_INVALID_PARAM; - } - - PPP_DEBUG_DEBUG("=== Sending Raw Data (HEX Mode) ==="); - PPP_DEBUG_DEBUG("Binary length: %zu bytes", len); + return n706a_parse_tcprecv_urc(urc_data, urc_len, socket_id, data_len, data); +} - /* 将二进制数据编码为HEX字符串 */ - size_t hex_str_len = len * 2; // 每个字节转换为2个HEX字符 - char *hex_str = (char *)malloc(hex_str_len + 1); // +1 for null terminator - if (!hex_str) { - PPP_DEBUG_ERROR("Failed to allocate memory for HEX string"); - return PPP_ERR_MEMORY; +/** + * @brief 解析 +TCPCLOSE URC + * @param urc_data URC数据(以 "+TCPCLOSE: " 或 "+SSLTCPCLOSE: " 开头) + * @param urc_len URC数据长度 + * @param socket_id 输出:socket ID(如果格式是ERROR,socket_id设为-1) + * @return true=解析成功,false=失败 + * + * URC格式: + * +TCPCLOSE: + * +TCPCLOSE: ,Link Closed + * +TCPCLOSE: ERROR(错误格式,socket_id设为-1) + * 或:+SSLTCPCLOSE: + */ +static bool n706a_parse_tcpclose_urc(const char *urc_data, size_t urc_len, int *socket_id) +{ + if (!urc_data || !socket_id || urc_len < 12) { + return false; } - size_t encoded_len = n706a_binary_to_hex_string((const uint8_t *)data, len, hex_str, hex_str_len + 1); - if (encoded_len == 0) { - free(hex_str); - PPP_DEBUG_ERROR("Failed to encode binary data to HEX string"); - return PPP_ERR_INVALID_PARAM; + /* ⚠️ 关键:正确识别标识符类型,只跳过标识符本身,不跳过标识符后的任何字符 */ + const char *p = urc_data; + if (memcmp(p, "+TCPCLOSE: ", 11) == 0) { + p += 11; /* 跳过 "+TCPCLOSE: " (11字节) */ + } else if (memcmp(p, "+SSLTCPCLOSE: ", 14) == 0) { + p += 14; /* 跳过 "+SSLTCPCLOSE: " (14字节) */ + } else { + /* 尝试使用strstr作为后备(兼容性) */ + const char *colon_space = strstr(urc_data, ": "); + if (!colon_space) { + return false; + } + p = colon_space + 2; /* 跳过 ": " (2字节) */ } - PPP_DEBUG_DEBUG("HEX encoded: binary_len=%zu -> hex_len=%zu", len, encoded_len); + const char *end = urc_data + urc_len; - /* 通过 UART 发送HEX字符串(ASCII字符),不添加 \r\n */ - int32_t write_result = uapi_uart_write(g_at_ctx.uart_bus, (const uint8_t *)hex_str, encoded_len, 0); - - free(hex_str); - - if (write_result != (int32_t)encoded_len) { - PPP_DEBUG_ERROR("Failed to send HEX data: wrote %d bytes, expected %zu", write_result, encoded_len); - return PPP_ERR_UART; + // 检查是否是ERROR格式 + if (p + 5 <= end && memcmp(p, "ERROR", 5) == 0 && + (p[5] == '\r' || p[5] == '\n' || p[5] == '\0' || p[5] == ' ')) { + *socket_id = -1; // ERROR格式,socket_id设为-1表示错误 + return true; } - - PPP_DEBUG_DEBUG("HEX data sent successfully (%zu hex chars, %zu binary bytes) on bus %d", - encoded_len, len, g_at_ctx.uart_bus); - return PPP_ERR_OK; + + // 解析 socket_id + *socket_id = 0; + while (p < end && *p >= '0' && *p <= '9') { + *socket_id = *socket_id * 10 + (*p - '0'); + p++; + } + + // 可能后面有 ",Link Closed" 或其他信息,但socket_id已经解析完成 + return (*socket_id >= 0); // 确保至少解析到一个有效的socket_id或ERROR } /** - * @brief 跳过开机日志 (对应chat文件中的 '' \rAT) + * @brief 解析 +TCPSEND URC + * @param urc_data URC数据(以 "+TCPSEND: " 或 "+SSLTCPEND: " 开头) + * @param urc_len URC数据长度 + * @param socket_id 输出:socket ID + * @param sent_bytes 输出:实际发送的字节数 + * @return true=解析成功,false=失败 + * + * URC格式:+TCPSEND: , + * 或:+SSLTCPEND: , */ -static ppp_error_t ppp_at_skip_startup_logs(void) +static bool n706a_parse_tcpsend_urc(const char *urc_data, size_t urc_len, + int *socket_id, size_t *sent_bytes) { - PPP_DEBUG_DEBUG("Skipping startup logs..."); + if (!urc_data || !socket_id || !sent_bytes || urc_len < 15) { + return false; + } - // 清空接收缓冲区,跳过开机时的各种日志输出 - uint8_t dummy_buffer[256]; - uint32_t bytes_read; + /* ⚠️ 关键:正确识别标识符类型,只跳过标识符本身,不跳过标识符后的任何字符 */ + const char *p = urc_data; + if (memcmp(p, "+TCPSEND: ", 10) == 0) { + p += 10; /* 跳过 "+TCPSEND: " (10字节) */ + } else if (memcmp(p, "+SSLTCPEND: ", 12) == 0) { + p += 12; /* 跳过 "+SSLTCPEND: " (12字节) */ + } else { + /* 尝试使用strstr作为后备(兼容性) */ + const char *colon_space = strstr(urc_data, ": "); + if (!colon_space) { + return false; + } + p = colon_space + 2; /* 跳过 ": " (2字节) */ + } - // 连续读取并丢弃开机日志,直到没有更多数据 - int retry_count = 0; - const int max_retries = 10; + const char *end = urc_data + urc_len; - do { - ppp_error_t ret = ppp_serial_read(dummy_buffer, sizeof(dummy_buffer), &bytes_read); - if (ret != PPP_ERR_OK) { - break; - } - - if (bytes_read > 0) { - PPP_DEBUG_DEBUG("Skipped %u bytes of startup logs", bytes_read); - retry_count = 0; // 重置重试计数 - } else { - retry_count++; + // 解析 socket_id + *socket_id = 0; + while (p < end && *p >= '0' && *p <= '9') { + *socket_id = *socket_id * 10 + (*p - '0'); + p++; + } + + if (p >= end || *p != ',') { + PPP_DEBUG_ERROR("TCPSEND URC parse error: no comma after socket_id"); + return false; + } + p++; // 跳过逗号 + + // 解析 sent_bytes(可能是数字,也可能是错误消息如 "OPERATION EXPIRED") + *sent_bytes = 0; + if (p < end && *p >= '0' && *p <= '9') { + // 数字格式:正常情况 + while (p < end && *p >= '0' && *p <= '9') { + *sent_bytes = *sent_bytes * 10 + (*p - '0'); + p++; } - - osal_msleep(100); - } while (retry_count < max_retries); + } else { + // 文本格式:错误消息(如 "OPERATION EXPIRED") + // 这种情况下,sent_bytes 保持为 0,表示发送失败 + PPP_DEBUG_WARN("TCPSEND URC contains error message instead of sent_bytes: [%.*s]", + (int)(end - p < 64 ? end - p : 64), p); + } - PPP_DEBUG_DEBUG("Startup logs skipped"); - return PPP_ERR_OK; + return true; } -/* ========== PPP拨号函数已删除 ========== */ -/* N706A_NET 使用纯AT命令模式,不需要PPP拨号功能 */ -/* 如需PPP功能,请使用 ppp 组件 */ /** - * @brief 发送AT关机指令,支持快速关机和正常关机 - * @param shutdown_type 关机类型: 0-快速关机, 1-正常流程关机 - * @return ppp_error_t + * @brief 解析 +TCPSETUP URC + * @param urc_data URC数据(以 "+TCPSETUP: " 或 "+SSLTCPSETUP: " 开头) + * @param urc_len URC数据长度 + * @param socket_id 输出:socket ID + * @param success 输出:true表示OK,false表示FAIL + * @return true=解析成功,false=失败 + * + * URC格式:+TCPSETUP: ,OK 或 +TCPSETUP: ,FAIL + * 或:+SSLTCPSETUP: ,OK 或 +SSLTCPSETUP: ,FAIL + * 例如:+TCPSETUP: 0,OK 或 +TCPSETUP: 0,FAIL */ -ppp_error_t ppp_at_poweroff_modem(uint8_t shutdown_type) +static bool n706a_parse_tcpsetup_urc(const char *urc_data, size_t urc_len, + int *socket_id, bool *success) { - if (!g_at_ctx.initialized) { - return PPP_ERR_INVALID_PARAM; + if (!urc_data || !socket_id || !success || urc_len < 15) { + return false; } - - PPP_DEBUG_INFO("Sending AT+NWPWROFF=%d to power off modem", shutdown_type); - - char cmd[32]; - snprintf(cmd, sizeof(cmd), "AT+NWPWROFF=%d", shutdown_type); - - ppp_at_response_t response; - ppp_error_t ret = ppp_at_send_command(cmd, "OK", 10000, &response); - - if (ret == PPP_ERR_OK) { - PPP_DEBUG_INFO("Modem power off command sent successfully"); + + /* ⚠️ 关键:正确识别标识符类型,只跳过标识符本身,不跳过标识符后的任何字符 */ + const char *p = urc_data; + if (memcmp(p, "+TCPSETUP: ", 11) == 0) { + p += 11; /* 跳过 "+TCPSETUP: " (11字节) */ + } else if (memcmp(p, "+SSLTCPSETUP: ", 14) == 0) { + p += 14; /* 跳过 "+SSLTCPSETUP: " (14字节) */ } else { - PPP_DEBUG_ERROR("Failed to send modem power off command"); + /* 尝试使用strstr作为后备(兼容性) */ + const char *colon_space = strstr(urc_data, ": "); + if (!colon_space) { + return false; + } + p = colon_space + 2; /* 跳过 ": " (2字节) */ } - ppp_power_off_cat1(); - return ret; -} - -/* ========== PPP挂断函数已删除 ========== */ -/* N706A_NET 使用纯AT命令模式,不需要PPP挂断功能 */ - -/** - * @brief 获取当前波特率 - */ -uint32_t ppp_at_get_current_baudrate(void) -{ - return g_at_ctx.current_baudrate; + + const char *end = urc_data + urc_len; + + /* 注意:对于TCPSETUP,参数之间可能有空格(如 "+TCPSETUP: 0, OK"), + * 这些空格是参数分隔符,跳过它们是合理的,因为它们不是数据的一部分 */ + /* 跳过可能的空格(参数分隔符) */ + while (p < end && (*p == ' ' || *p == '\t')) { + p++; + } + + // 解析 socket_id + *socket_id = 0; + while (p < end && *p >= '0' && *p <= '9') { + *socket_id = *socket_id * 10 + (*p - '0'); + p++; + } + + if (p >= end || *p != ',') { + PPP_DEBUG_ERROR("TCPSETUP URC parse error: no comma after socket_id (found: 0x%02x)", + p < end ? (unsigned char)*p : 0); + return false; + } + p++; // 跳过逗号 + + // 跳过可能的空格 + while (p < end && (*p == ' ' || *p == '\t')) { + p++; + } + + // 解析结果(OK或FAIL),忽略前后空白字符 + if (p < end) { + // 检查OK + if (p + 2 <= end && (p[0] == 'O' || p[0] == 'o') && (p[1] == 'K' || p[1] == 'k')) { + // 检查OK后面是否是结束符或空白字符 + if (p + 2 >= end || p[2] == '\r' || p[2] == '\n' || p[2] == '\0' || + p[2] == ' ' || p[2] == '\t') { + *success = true; + return true; + } + } + + // 检查FAIL + if (p + 4 <= end && + (p[0] == 'F' || p[0] == 'f') && + (p[1] == 'A' || p[1] == 'a') && + (p[2] == 'I' || p[2] == 'i') && + (p[3] == 'L' || p[3] == 'l')) { + // 检查FAIL后面是否是结束符或空白字符 + if (p + 4 >= end || p[4] == '\r' || p[4] == '\n' || p[4] == '\0' || + p[4] == ' ' || p[4] == '\t') { + *success = false; + return true; + } + } + } + + // 如果都不匹配,输出详细的调试信息 + char debug_buf[64] = {0}; + size_t debug_len = (end - p < sizeof(debug_buf) - 1) ? (end - p) : (sizeof(debug_buf) - 1); + memcpy(debug_buf, p, debug_len); + PPP_DEBUG_ERROR("TCPSETUP URC parse error: invalid result (expected OK/FAIL), found: [%.*s] (len=%u)", + (int)debug_len, debug_buf, (unsigned int)(end - p)); + return false; } /** - * @brief 检查是否为自动波特率模式 + * @brief 将单个HEX字符转换为数值 + * @param c HEX字符('0'-'9', 'A'-'F', 'a'-'f') + * @param value 输出的数值 + * @return true=成功,false=失败 */ -bool ppp_at_is_auto_baudrate_mode(void) +static bool n706a_hex_char_to_value(char c, uint8_t *value) { - return g_at_ctx.auto_baudrate_mode; + if (c >= '0' && c <= '9') { + *value = c - '0'; + return true; + } else if (c >= 'A' && c <= 'F') { + *value = c - 'A' + 10; + return true; + } else if (c >= 'a' && c <= 'f') { + *value = c - 'a' + 10; + return true; + } + return false; } /** - * @brief 测试AT通信是否正常 + * @brief 将二进制数据转换为HEX字符串 + * @param binary_data 二进制数据 + * @param binary_len 二进制数据长度 + * @param hex_str 输出的HEX字符串缓冲区 + * @param hex_buf_size HEX缓冲区大小(至少需要binary_len*2+1) + * @return 转换后的HEX字符串长度(不含结束符),0表示失败 */ -ppp_error_t ppp_at_test_communication(void) +static size_t n706a_binary_to_hex_string(const uint8_t *binary_data, size_t binary_len, + char *hex_str, size_t hex_buf_size) { - PPP_DEBUG_INFO("Testing AT communication..."); + if (!binary_data || !hex_str || binary_len == 0) { + return 0; + } - ppp_at_response_t response; + // 需要binary_len * 2字节(每个字节转换为2个HEX字符),再加1个结束符 + if (hex_buf_size < binary_len * 2 + 1) { + PPP_DEBUG_ERROR("HEX encode: buffer too small (need %u, got %u)", + (unsigned int)(binary_len * 2 + 1), (unsigned int)hex_buf_size); + return 0; + } - // 发送简单的AT命令测试通信 - ppp_error_t ret = ppp_at_send_command("AT", "OK", 1000, &response); - if (ret == PPP_ERR_OK) { - PPP_DEBUG_INFO("AT communication test passed"); - } else { - PPP_DEBUG_ERROR("AT communication test failed: %d", ret); + static const char hex_chars[] = "0123456789ABCDEF"; + + for (size_t i = 0; i < binary_len; i++) { + hex_str[i * 2] = hex_chars[(binary_data[i] >> 4) & 0x0F]; + hex_str[i * 2 + 1] = hex_chars[binary_data[i] & 0x0F]; } - return ret; + hex_str[binary_len * 2] = '\0'; + return binary_len * 2; } -/* 内部函数实现 */ - /** - * @brief 检查AT响应是否完整 - * @param buffer 响应缓冲区 - * @param length 缓冲区长度 - * @return true 如果响应完整,false 否则 + * @brief 将HEX字符串转换为二进制数据 + * @param hex_str HEX字符串(如 "313233") + * @param hex_len HEX字符串长度(字节数,必须是偶数) + * @param binary_buf 输出的二进制缓冲区 + * @param binary_buf_size 二进制缓冲区大小 + * @return 转换后的二进制数据长度(字节数),0表示失败 */ -static bool ppp_at_check_response_complete(const char *buffer, uint32_t length) +static size_t n706a_hex_string_to_binary(const char *hex_str, size_t hex_len, + uint8_t *binary_buf, size_t binary_buf_size) { - if (!buffer || length == 0) { - return false; + if (!hex_str || !binary_buf || hex_len == 0 || hex_len % 2 != 0) { + return 0; } - /* 搜索OK终止符 - 修复无符号整数下溢问题 */ - if (length >= 2) { - for (uint32_t i = 0; i <= length - 2; i++) { - if (buffer[i] == 'O' && buffer[i+1] == 'K') { - /* 检查OK前面是否有换行符(避免误匹配) */ - if (i == 0 || buffer[i-1] == '\n' || buffer[i-1] == '\r') { - PPP_DEBUG_DEBUG("Found OK terminator at position %u", i); - return true; - } - } - } + size_t binary_len = hex_len / 2; + if (binary_len > binary_buf_size) { + PPP_DEBUG_ERROR("HEX decode: binary_len=%u exceeds buffer size=%u", + (unsigned int)binary_len, (unsigned int)binary_buf_size); + return 0; } - /* 搜索ERROR终止符 - 已经有长度检查,但确保安全 */ - if (length >= 5) { - for (uint32_t i = 0; i <= length - 5; i++) { - if (strncmp(&buffer[i], "ERROR", 5) == 0) { - /* 检查ERROR前面是否有换行符 */ - if (i == 0 || buffer[i-1] == '\n' || buffer[i-1] == '\r') { - PPP_DEBUG_DEBUG("Found ERROR terminator at position %u", i); - return true; - } - } + for (size_t i = 0; i < binary_len; i++) { + uint8_t high_nibble, low_nibble; + + if (!n706a_hex_char_to_value(hex_str[i * 2], &high_nibble) || + !n706a_hex_char_to_value(hex_str[i * 2 + 1], &low_nibble)) { + PPP_DEBUG_ERROR("Invalid HEX character at position %u", (unsigned int)(i * 2)); + return 0; } + + binary_buf[i] = (high_nibble << 4) | low_nibble; } - /* 检查其他终止符 */ - if (strstr(buffer, "CONNECT") || - strstr(buffer, "NO CARRIER") || - strstr(buffer, "BUSY") || - strstr(buffer, "NO DIALTONE") || - strstr(buffer, "+CME ERROR") || - strstr(buffer, "+CMS ERROR")) { - PPP_DEBUG_DEBUG("Found other terminator in response"); - return true; - } - - return false; + return binary_len; +} + +/** + * @brief 注册TCP URC接收回调 + * @param callback 回调函数指针,NULL表示取消注册 + */ +void n706a_at_register_tcp_urc_callback(n706a_tcp_urc_callback_t callback) +{ + g_tcp_urc_callback = callback; + PPP_DEBUG_INFO("TCP URC callback %s", callback ? "registered" : "unregistered"); +} + +/** + * @brief 注册+TCPSETUP URC回调 + * @param callback 回调函数指针,NULL表示取消注册 + */ +void n706a_at_register_tcpsetup_urc_callback(n706a_tcpsetup_urc_callback_t callback) +{ + g_tcpsetup_urc_callback = callback; + PPP_DEBUG_INFO("+TCPSETUP URC callback %s", callback ? "registered" : "unregistered"); } /** - * @brief 解析AT命令响应 + * @brief 注册+TCPCLOSE URC回调 */ -static bool ppp_at_parse_response(const char *response, const char *expected) +void n706a_at_register_tcpclose_urc_callback(n706a_tcpclose_urc_callback_t callback) { - if (!response) { - return false; - } - + g_tcpclose_urc_callback = callback; + PPP_DEBUG_INFO("+TCPCLOSE URC callback %s", callback ? "registered" : "unregistered"); +} - // 检查期望响应 ,优先检测期望响应 - if (expected && strstr(response, expected)) { - return true; - } +/** + * @brief 注册+TCPSEND URC回调 + */ +void n706a_at_register_tcpsend_urc_callback(n706a_tcpsend_urc_callback_t callback) +{ + g_tcpsend_urc_callback = callback; + PPP_DEBUG_INFO("+TCPSEND URC callback %s", callback ? "registered" : "unregistered"); +} - // 检查ABORT条件 (对应chat文件中的ABORT指令) - if (strstr(response, "BUSY") || - strstr(response, "ERROR") || - strstr(response, "NO CARRIER")) { - PPP_DEBUG_ERROR("AT command aborted: %s", response); - return false; - } +/** + * @brief 注册网络状态URC回调 + */ +void n706a_at_register_network_status_urc_callback(n706a_network_status_urc_callback_t callback) +{ + g_network_status_urc_callback = callback; + PPP_DEBUG_INFO("Network status URC callback %s", callback ? "registered" : "unregistered"); +} - +/** + * @brief 设置TCP接收模式(HEX模式或ASCII模式) + */ +void n706a_at_set_tcp_recv_hex_mode(bool hex_mode) +{ + g_tcp_recv_hex_mode = hex_mode; + PPP_DEBUG_INFO("TCP receive mode set to %s", hex_mode ? "HEX" : "ASCII"); +} - return false; +/** + * @brief 获取TCP接收模式(HEX模式或ASCII模式) + */ +bool n706a_at_get_tcp_recv_hex_mode(void) +{ + return g_tcp_recv_hex_mode; } /** - * @brief 等待AT命令响应 - 异步版本(从环形缓冲区读取) + * @brief 解析 +HTTPRECV: / +HTTPSRECV: URC + * @param urc_data URC数据(以 "+HTTPRECV: " 或 "+HTTPSRECV: " 开头) + * @param urc_len URC数据长度 + * @param declared_len 输出:URC中声明的数据长度(从 字段解析,二进制长度) + * @param actual_len 输出:实际可用的数据长度(从数据开始位置到URC结束位置) + * @param data 输出:数据指针(指向 urc_data 内部) + * @return true=解析成功,false=失败 * - * 该函数从ppp_interrupt.c填充的环形缓冲区中异步读取AT响应数据 - * 提供同步的函数接口,但底层是异步的高速串口接收 + * URC格式(两种可能,由 AT+HTTPPARA=recvmode 决定): + * 1. recvmode=1:+HTTPRECV: , 或 +HTTPSRECV: , (带长度字段) + * 2. recvmode=0(默认):+HTTPRECV: 或 +HTTPSRECV: (无长度字段,直接是数据) + * + * 例如: + * +HTTPSRECV: 1024,HTTP/1.1 200 OK\r\n... (格式1,recvmode=1) + * +HTTPSRECV: HTTP/1.1 404 Not Found\r\n... (格式2,recvmode=0) + * + * 重要说明: + * - HTTPRECV 不支持分片,每个 URC 是独立的 + * - declared_len:URC中声明的 字段,表示当前URC的数据长度(二进制长度) + * - 如果是 HEX 模式,实际 HEX 字符串长度 = declared_len × 2 + * - 如果是 ASCII 模式,实际 ASCII 字符串长度 = declared_len + * - actual_len:实际可用的数据长度(从数据开始位置到URC结束位置) + * - 正常情况下,declared_len 应该等于 actual_len(如果数据完整) + * - 如果 declared_len != actual_len,说明数据不完整(URC 被截断) + * - 如果 declared_len > 2048,这是正常的,因为模块不会自动分片 */ -static ppp_error_t ppp_at_wait_response(ppp_at_response_t *response, uint32_t timeout) +static bool n706a_parse_httprecv_urc(const char *urc_data, size_t urc_len, + size_t *declared_len, size_t *actual_len, const uint8_t **data) { - PPP_DEBUG_DEBUG(">>> N706A AT WAIT_RESPONSE START (async mode, timeout: %u ms) <<<", timeout); + if (!urc_data || !declared_len || !actual_len || !data || urc_len < 12) { + return false; + } - if (!response || !g_at_response_buffer) { - PPP_DEBUG_ERROR("Invalid parameters: response=%p, buffer=%p", response, g_at_response_buffer); - return PPP_ERR_INVALID_PARAM; + /* 调试:打印原始URC数据(前256字节) */ + size_t debug_len = (urc_len < 256) ? urc_len : 256; + char debug_buf[257] = {0}; + for (size_t i = 0; i < debug_len; i++) { + if (urc_data[i] >= 32 && urc_data[i] < 127) { + debug_buf[i] = urc_data[i]; + } else if (urc_data[i] == '\r') { + debug_buf[i] = '\\'; + if (i + 1 < 256) debug_buf[++i] = 'r'; + } else if (urc_data[i] == '\n') { + debug_buf[i] = '\\'; + if (i + 1 < 256) debug_buf[++i] = 'n'; + } else { + debug_buf[i] = '.'; + } } - - uint32_t start_time = (uint32_t)osal_get_jiffies(); - uint32_t bytes_received = 0; - bool response_complete = false; + PPP_DEBUG_INFO("[HTTPRECV URC] Raw URC data (first %u bytes): %s", (unsigned int)debug_len, debug_buf); - /* 清空全局缓存 */ - memset(g_at_response_buffer, 0, g_at_buffer_size); + /* 跳过 "+HTTPRECV: " 或 "+HTTPSRECV: " */ + const char *p = urc_data; + if (memcmp(p, "+HTTPRECV: ", 11) == 0) { + p += 11; + } else if (memcmp(p, "+HTTPSRECV: ", 12) == 0) { + p += 12; + } else { + return false; + } - PPP_DEBUG_DEBUG("N706A AT: Waiting for response from async ring buffer (timeout: %u ms)", timeout); + const char *end = urc_data + urc_len; + const char *data_start = p; - /* 循环从环形缓冲区读取数据直到完整响应或超时 */ - while (!response_complete) { - uint32_t current_time = (uint32_t)osal_get_jiffies(); - uint32_t elapsed = osal_jiffies_to_msecs(current_time - start_time); + /* 尝试解析格式1:+HTTPSRECV: , */ + size_t parsed_len = 0; + const char *len_start = p; + + /* 解析数据长度(如果存在) */ + while (p < end && *p >= '0' && *p <= '9') { + parsed_len = parsed_len * 10 + (*p - '0'); + p++; + } + + /* 检查是否有逗号(格式1) */ + if (p < end && *p == ',' && parsed_len > 0) { + /* 格式1:+HTTPSRECV: , */ + PPP_DEBUG_INFO("[HTTPRECV URC] Format 1 detected: len=%u, comma found", (unsigned int)parsed_len); + p++; /* 跳过逗号 */ + data_start = p; - /* 检查超时 */ - if (elapsed >= timeout) { - PPP_DEBUG_WARN("N706A AT: Response timeout after %u ms", elapsed); - break; + /* ⚠️ 关键:使用URC中声明的 作为 declared_len(这是最准确的) */ + /* 根据用户确认:HTTPRECV 不支持分片, 字段就是当前URC的数据长度(二进制长度) */ + /* 如果是 HEX 模式,实际 HEX 字符串长度 = declared_len × 2 */ + /* 如果是 ASCII 模式,实际 ASCII 字符串长度 = declared_len */ + *declared_len = parsed_len; + + /* ⚠️ remaining 的作用:计算实际可用的数据长度,防止内存越界 */ + /* remaining = end - p,表示从数据开始位置到URC结束位置之间的实际可用字节数 */ + /* 正常情况下,declared_len 应该等于 actual_len(如果数据完整) */ + /* 如果 declared_len != actual_len,说明数据不完整(URC 被截断) */ + size_t remaining = end - p; + *actual_len = remaining; /* 实际可用的数据长度 */ + + /* 验证数据完整性:正常情况下 declared_len 应该等于 actual_len */ + if (*declared_len != *actual_len) { + PPP_DEBUG_WARN("HTTPRECV URC: declared len=%u != actual len=%u (data may be incomplete/truncated)", + (unsigned int)*declared_len, (unsigned int)*actual_len); } - /* 等待信号量(有新数据到达),使用剩余超时时间 */ - uint32_t remaining_timeout = timeout - elapsed; - if (remaining_timeout > 100) { - remaining_timeout = 100; // 最多等100ms,然后检查一次 + /* 注意:HTTPRECV 不支持分片,所以 declared_len > 2048 是正常的 */ + /* 这与 TCPRECV 不同,TCPRECV 有明确的 2048 字节限制并会自动分片 */ + if (*declared_len > 2048) { + PPP_DEBUG_INFO("HTTPRECV URC: declared len=%u exceeds 2048 bytes (normal, HTTPRECV doesn't auto-fragment)", + (unsigned int)*declared_len); } - int sem_result = osal_sem_down_timeout(&g_at_response_sem, remaining_timeout); - if (sem_result == OSAL_SUCCESS || sem_result == OSAL_FAILURE) { - /* 有新数据到达或信号量操作失败,尝试读取环形缓冲区 */ - osal_mutex_lock(&g_at_response_mutex); - - /* 从环形缓冲区读取数据到响应缓冲区 */ - while (g_at_response_tail != g_at_response_head && - bytes_received < (g_at_buffer_size - 1)) { - g_at_response_buffer[bytes_received] = g_at_response_ring_buffer[g_at_response_tail]; - g_at_response_tail = (g_at_response_tail + 1) % N706A_AT_RESPONSE_RING_BUFFER_SIZE; - bytes_received++; - } - - osal_mutex_unlock(&g_at_response_mutex); - - /* 设置字符串结束符 */ - if (bytes_received > 0) { - g_at_response_buffer[bytes_received] = '\0'; - - PPP_DEBUG_DEBUG("N706A AT: Read %u bytes from ring buffer", bytes_received); - - /* 检查是否收到完整响应 */ - if (ppp_at_check_response_complete((char*)g_at_response_buffer, bytes_received)) { - PPP_DEBUG_DEBUG("N706A AT: Complete response detected"); - response_complete = true; - break; - } + /* 调试:打印解析后的数据起始位置(前128字节) */ + size_t preview_len = (*actual_len < 128) ? *actual_len : 128; + char preview_buf[129] = {0}; + for (size_t i = 0; i < preview_len && i < 128; i++) { + if (data_start[i] >= 32 && data_start[i] < 127) { + preview_buf[i] = data_start[i]; + } else if (data_start[i] == '\r') { + preview_buf[i] = '\\'; + if (i + 1 < 128) preview_buf[++i] = 'r'; + } else if (data_start[i] == '\n') { + preview_buf[i] = '\\'; + if (i + 1 < 128) preview_buf[++i] = 'n'; + } else { + preview_buf[i] = '.'; } } + PPP_DEBUG_INFO("[HTTPRECV URC] Parsed data start (first %u bytes): %s", (unsigned int)preview_len, preview_buf); + } else { + /* 格式2:+HTTPSRECV: (无长度字段) */ + PPP_DEBUG_INFO("[HTTPRECV URC] Format 2 detected: no comma, parsed_len=%u (may be part of data)", (unsigned int)parsed_len); + /* 数据从URC标识符后直接开始,直到遇到 +HTTPSCLOSED 或 +HTTPCLOSED 或数据结束 */ + data_start = len_start; /* 回退到长度解析开始位置(实际是数据开始位置) */ - /* 短暂延迟避免CPU占用过高 */ - if (!response_complete) { - osal_msleep(10); + /* 查找数据结束位置(可能是 +HTTPSCLOSED、+HTTPCLOSED 或缓冲区结束) */ + const char *data_end = end; + const char *closed_pos = strstr(data_start, "+HTTPSCLOSED:"); + if (!closed_pos) { + closed_pos = strstr(data_start, "+HTTPCLOSED:"); + } + if (closed_pos && closed_pos < end) { + data_end = closed_pos; + PPP_DEBUG_INFO("[HTTPRECV URC] Found +HTTPSCLOSED/+HTTPCLOSED at offset %u", + (unsigned int)(closed_pos - data_start)); } - } - - /* 处理结果 */ - if (bytes_received > 0) { - uint32_t copy_len = (bytes_received < PPP_AT_RESPONSE_MAX_LEN - 1) ? - bytes_received : PPP_AT_RESPONSE_MAX_LEN - 1; - memcpy_s(response->response, PPP_AT_RESPONSE_MAX_LEN, - g_at_response_buffer, copy_len); - response->response[copy_len] = '\0'; - response->length = copy_len; + /* 计算数据长度 */ + /* ⚠️ 关键:不移除尾部的换行符,因为它们是HTTP响应数据的一部分 */ + /* 如果移除,会导致数据丢失,特别是HTTP响应体以换行符结尾的情况 */ + size_t calculated_len = data_end - data_start; - uint32_t final_time = (uint32_t)osal_get_jiffies(); - uint32_t total_elapsed = osal_jiffies_to_msecs(final_time - start_time); + /* 格式2没有声明的长度,所以 declared_len = actual_len = 计算出的长度 */ + *declared_len = calculated_len; + *actual_len = calculated_len; - PPP_DEBUG_DEBUG("N706A AT: SUCCESS - %u bytes in %u ms", bytes_received, total_elapsed); - PPP_DEBUG_DEBUG(">>> N706A AT WAIT_RESPONSE END (SUCCESS) <<<"); - return PPP_ERR_OK; - } else { - uint32_t final_time = (uint32_t)osal_get_jiffies(); - uint32_t total_elapsed = osal_jiffies_to_msecs(final_time - start_time); + /* 注意:HTTPRECV 不支持分片,所以 declared_len > 2048 是正常的 */ + /* 这与 TCPRECV 不同,TCPRECV 有明确的 2048 字节限制并会自动分片 */ + if (*declared_len > 2048) { + PPP_DEBUG_INFO("HTTPRECV URC: declared len=%u exceeds 2048 bytes (normal, HTTPRECV doesn't auto-fragment)", + (unsigned int)*declared_len); + } - PPP_DEBUG_WARN("N706A AT: FAILED - No response after %u ms", total_elapsed); - PPP_DEBUG_DEBUG(">>> N706A AT WAIT_RESPONSE END (FAILED) <<<"); - response->length = 0; - response->response[0] = '\0'; - return PPP_ERR_TIMEOUT; + PPP_DEBUG_INFO("[HTTPRECV URC] Format 2: declared_len=%u, actual_len=%u", + (unsigned int)*declared_len, (unsigned int)*actual_len); + + /* 调试:打印解析后的数据起始位置(前128字节) */ + size_t preview_len = (*actual_len < 128) ? *actual_len : 128; + char preview_buf[129] = {0}; + for (size_t i = 0; i < preview_len && i < 128; i++) { + if (data_start[i] >= 32 && data_start[i] < 127) { + preview_buf[i] = data_start[i]; + } else if (data_start[i] == '\r') { + preview_buf[i] = '\\'; + if (i + 1 < 128) preview_buf[++i] = 'r'; + } else if (data_start[i] == '\n') { + preview_buf[i] = '\\'; + if (i + 1 < 128) preview_buf[++i] = 'n'; + } else { + preview_buf[i] = '.'; + } + } + PPP_DEBUG_INFO("[HTTPRECV URC] Parsed data start (first %u bytes): %s", (unsigned int)preview_len, preview_buf); } + + /* 数据起始位置 */ + *data = (const uint8_t *)data_start; + + if (*actual_len == 0) { + PPP_DEBUG_WARN("HTTPRECV URC actual_len is 0"); + return false; + } + + PPP_DEBUG_INFO("[HTTPRECV URC] Final parsed: declared_len=%u, actual_len=%u, data_start offset=%u", + (unsigned int)*declared_len, (unsigned int)*actual_len, (unsigned int)(data_start - urc_data)); + return true; } /** - * @brief 解析AT+CESQ响应 - * 参考N706A AT命令手册: +CESQ: ,,,,, + * @brief 获取累积缓冲区长度(用于调试) + * @return 累积缓冲区当前长度 */ -static ppp_error_t ppp_parse_cesq_response(const char *response, n706a_network_signal_info_t *info) +uint32_t n706a_at_get_accumulate_buffer_length(void) { - if (!response || !info) { - return PPP_ERR_INVALID_PARAM; - } - - memset_s(info, sizeof(n706a_network_signal_info_t), 0, sizeof(n706a_network_signal_info_t)); - - // 查找+CESQ:开头的行 - const char *cesq_line = strstr(response, "+CESQ:"); - if (!cesq_line) { - PPP_DEBUG_WARN("No +CESQ: found in response"); - return PPP_ERR_AT_FAILED; - } - - // 跳过"+CESQ:" - cesq_line += 6; - while (*cesq_line == ' ' || *cesq_line == '\t') cesq_line++; - - // +CESQ: ,,,,, - int rxlev = 99, ber = 99, rscp = 255, ecno = 255, rsrq = 255, rsrp = 255; - int parsed = sscanf(cesq_line, "%d,%d,%d,%d,%d,%d", &rxlev, &ber, &rscp, &ecno, &rsrq, &rsrp); - - if (parsed == 6) { - info->valid = true; - info->rxlev = rxlev; - info->ber = ber; - info->rscp = rscp; - info->ecno = ecno; - info->rsrq = rsrq; - info->rsrp = rsrp; - PPP_DEBUG_INFO("CESQ parsed successfully: rxlev=%d, ber=%d, rscp=%d, ecno=%d, rsrq=%d, rsrp=%d", - rxlev, ber, rscp, ecno, rsrq, rsrp); - return PPP_ERR_OK; - } else { - PPP_DEBUG_WARN("Failed to parse CESQ response, parsed: %d", parsed); - return PPP_ERR_AT_FAILED; - } + osal_mutex_lock(&g_at_accumulate_mutex); + uint32_t len = g_at_accumulate_length; + osal_mutex_unlock(&g_at_accumulate_mutex); + return len; } /** - * @brief 获取网络信号信息 (AT+CESQ) + * @brief 获取累积缓冲区的最后几个字节(用于调试) + * @param buffer 输出缓冲区 + * @param max_len 最大长度 + * @param actual_len 实际长度(输出) + * @return true=成功,false=失败 */ -ppp_error_t ppp_at_get_network_signal_info(n706a_network_signal_info_t *info) +bool n706a_at_get_accumulate_buffer_preview(uint8_t *buffer, uint32_t max_len, uint32_t *actual_len) { - if (!g_at_ctx.initialized || !info) { - return PPP_ERR_INVALID_PARAM; + if (!buffer || !actual_len || max_len == 0) { + return false; } - // N706A_NET模式下始终处于AT模式,无需检查 - // (PPP模式需要检查是否成功切换到AT模式) - PPP_DEBUG_INFO("Getting network signal information (AT+CESQ)..."); - - ppp_at_response_t response; - ppp_error_t ret = ppp_at_send_command("AT+CESQ", "OK", 5000, &response); - - if (ret != PPP_ERR_OK) { - PPP_DEBUG_ERROR("AT+CESQ command failed: %d", ret); - return ret; - } - - // 解析响应 - ret = ppp_parse_cesq_response(response.response, info); - if (ret != PPP_ERR_OK) { - PPP_DEBUG_ERROR("Failed to parse CESQ response"); - return ret; + osal_mutex_lock(&g_at_accumulate_mutex); + uint32_t len = g_at_accumulate_length; + if (len == 0) { + *actual_len = 0; + osal_mutex_unlock(&g_at_accumulate_mutex); + return true; } - - // 打印详细信息 - PPP_DEBUG_INFO("=== Network Signal Information (AT+CESQ) ==="); - PPP_DEBUG_INFO("rxlev: %d", info->rxlev); - PPP_DEBUG_INFO("ber: %d", info->ber); - PPP_DEBUG_INFO("rscp: %d", info->rscp); - PPP_DEBUG_INFO("ecno: %d", info->ecno); - PPP_DEBUG_INFO("rsrq: %d", info->rsrq); - PPP_DEBUG_INFO("rsrp: %d", info->rsrp); - - // LTE信号质量评估 - 仅关注RSRQ和RSRP - PPP_DEBUG_INFO("=== LTE Signal Quality Analysis ==="); - // RSRQ评估 (0-49有效,255无效) - if (info->rsrq == 255) { - PPP_DEBUG_INFO("RSRQ: 无法获取LTE信号质量"); - } else if (info->rsrq >= 0 && info->rsrq <= 49) { - // RSRQ转换公式: 实际值(dB) = (rsrq - 94)/10 - float rsrq_db = (info->rsrq - 94) / 10.0f; - PPP_DEBUG_INFO("RSRQ: %d (%.1f dB)", info->rsrq, rsrq_db); - - // RSRQ评估标准 (范围: -19.5dB ~ -3dB,数值越大质量越好) - if (rsrq_db >= -6.0) { - PPP_DEBUG_INFO("RSRQ评级: 极佳 (信号质量优秀)"); - } else if (rsrq_db >= -9.0) { - PPP_DEBUG_INFO("RSRQ评级: 良好 (信号质量稳定)"); - } else if (rsrq_db >= -12.0) { - PPP_DEBUG_INFO("RSRQ评级: 一般 (信号质量可用)"); - } else { - PPP_DEBUG_INFO("RSRQ评级: 较差 (信号质量不佳)"); - } - } else { - PPP_DEBUG_INFO("RSRQ: %d (超出有效范围0-49)", info->rsrq); + uint32_t preview_len = (len < max_len) ? len : max_len; + uint32_t start_offset = (len > preview_len) ? (len - preview_len) : 0; + memcpy(buffer, g_at_accumulate_buffer + start_offset, preview_len); + *actual_len = preview_len; + osal_mutex_unlock(&g_at_accumulate_mutex); + return true; +} + +/** + * @brief 获取累积缓冲区的完整内容(从开头开始,用于调试) + * @param buffer 输出缓冲区 + * @param max_len 最大长度 + * @param actual_len 实际长度(输出) + * @return true=成功,false=失败 + */ +bool n706a_at_get_accumulate_buffer_all(uint8_t *buffer, uint32_t max_len, uint32_t *actual_len) +{ + if (!buffer || !actual_len || max_len == 0) { + return false; } - // RSRP评估 (0-97有效,255无效) - if (info->rsrp == 255) { - PPP_DEBUG_INFO("RSRP: 无法获取LTE信号强度"); - } else if (info->rsrp >= 0 && info->rsrp <= 97) { - // RSRP转换公式: 实际值(dBm) = rsrp - 140 - float rsrp_dbm = info->rsrp - 140.0f; - PPP_DEBUG_INFO("RSRP: %d (%.0f dBm)", info->rsrp, rsrp_dbm); - - // RSRP评估标准 (范围: -140dBm ~ -43dBm,数值越大信号越强) - if (rsrp_dbm >= -80) { - PPP_DEBUG_INFO("RSRP评级: 极佳 (信号强度优秀)"); - } else if (rsrp_dbm >= -90) { - PPP_DEBUG_INFO("RSRP评级: 良好 (信号强度稳定)"); - } else if (rsrp_dbm >= -100) { - PPP_DEBUG_INFO("RSRP评级: 一般 (信号强度可用)"); - } else { - PPP_DEBUG_INFO("RSRP评级: 较差 (信号强度不足)"); - } - } else { - PPP_DEBUG_INFO("RSRP: %d (超出有效范围0-97)", info->rsrp); + osal_mutex_lock(&g_at_accumulate_mutex); + uint32_t len = g_at_accumulate_length; + if (len == 0) { + *actual_len = 0; + osal_mutex_unlock(&g_at_accumulate_mutex); + return true; } - // 综合LTE信号质量评估 - if (info->rsrq != 255 && info->rsrp != 255 && - info->rsrq >= 0 && info->rsrq <= 49 && - info->rsrp >= 0 && info->rsrp <= 97) { - - float rsrq_db = (info->rsrq - 94) / 10.0f; - float rsrp_dbm = info->rsrp - 140.0f; - - const char *overall_quality = "未知"; - if (rsrq_db >= -6.0 && rsrp_dbm >= -80) { - overall_quality = "LTE信号极佳 (质量和强度都优秀)"; - } else if (rsrq_db >= -9.0 && rsrp_dbm >= -90) { - overall_quality = "LTE信号良好 (质量和强度都稳定)"; - } else if (rsrq_db >= -12.0 && rsrp_dbm >= -100) { - overall_quality = "LTE信号一般 (质量和强度可用)"; - } else { - overall_quality = "LTE信号较差 (需要改善网络环境)"; - } - - PPP_DEBUG_INFO("=== 综合评估: %s ===", overall_quality); - } else { - PPP_DEBUG_INFO("=== 综合评估: LTE参数无效,无法评估 ==="); - } + uint32_t copy_len = (len < max_len) ? len : max_len; + memcpy(buffer, g_at_accumulate_buffer, copy_len); + *actual_len = copy_len; + osal_mutex_unlock(&g_at_accumulate_mutex); + return true; +} - PPP_DEBUG_INFO("=== End Network Signal Information ==="); +/* ========== 流式解析器实现(新方案) ========== */ - return PPP_ERR_OK; +/** + * @brief 初始化流式解析器 + */ +static void n706a_stream_parser_init(void) +{ + memset(&g_parser_ctx, 0, sizeof(g_parser_ctx)); + g_parser_ctx.state = AT_PARSER_STATE_IDLE; + g_parser_ctx.urc_type = URC_TYPE_NONE; + g_parser_ctx.tcp_socket_id = -1; + g_parser_ctx.state_transition_pending = false; + PPP_DEBUG_INFO("[StreamParser] Initialized"); } -/* ========== 导出的N706A API包装函数(用于n706a_tcp_client和n706a_net_at_cmd) ========== */ +/** + * @brief 重置流式解析器状态 + */ +static void n706a_stream_parser_reset(void) +{ + /* 重置到空闲状态 */ + g_parser_ctx.state = AT_PARSER_STATE_IDLE; + g_parser_ctx.response_start_pos = 0; + g_parser_ctx.response_expected_len = 0; + g_parser_ctx.response_received_len = 0; + g_parser_ctx.urc_type = URC_TYPE_NONE; + g_parser_ctx.is_https = false; + g_parser_ctx.tcp_socket_id = -1; + g_parser_ctx.partial_buffer_len = 0; + g_parser_ctx.remaining_data_len = 0; + g_parser_ctx.received_data_len = 0; + g_parser_ctx.urc_header_len = 0; + g_parser_ctx.state_transition_pending = false; // 方案2:清除状态转换标记 + g_parser_ctx.urc_match_pos = 0; + g_parser_ctx.urc_pattern = NULL; + g_parser_ctx.urc_pattern_len = 0; + + memset(g_parser_ctx.partial_buffer, 0, sizeof(g_parser_ctx.partial_buffer)); + memset(g_parser_ctx.urc_header_buf, 0, sizeof(g_parser_ctx.urc_header_buf)); +} /** - * @brief N706A对外导出:发送AT命令并等待响应 + * @brief URC标识符模式表 */ -n706a_at_error_t n706a_at_send_command(const char *command, - const char *expected_response, - uint32_t timeout, - n706a_at_response_t *response) +static const struct { + const char *pattern; + uint32_t pattern_len; + urc_type_t type; +} urc_patterns[] = { + {"+HTTPSRECV: ", 12, URC_TYPE_HTTPSRECV}, + {"+HTTPRECV: ", 11, URC_TYPE_HTTPRECV}, + {"+SSLTCPRECV: ", 13, URC_TYPE_SSLTCPRECV}, + {"+TCPRECV: ", 10, URC_TYPE_TCPRECV}, + {"+SSLTCPSETUP: ", 14, URC_TYPE_SSLTCPSETUP}, + {"+TCPSETUP: ", 11, URC_TYPE_TCPSETUP}, + {"+SSLTCPCLOSE: ", 14, URC_TYPE_SSLTCPCLOSE}, + {"+TCPCLOSE: ", 11, URC_TYPE_TCPCLOSE}, + {"+SSLTCPEND: ", 12, URC_TYPE_SSLTCPEND}, + {"+TCPSEND: ", 10, URC_TYPE_TCPSEND}, + {"+CGATT: ", 8, URC_TYPE_CGATT}, + {"+CGREG: ", 8, URC_TYPE_CGREG}, + {"+CREG: ", 7, URC_TYPE_CREG}, + {NULL, 0, URC_TYPE_NONE} +}; + +/** + * @brief 匹配URC标识符(支持跨数据块) + * @param byte 当前字节 + * @param type 输出:匹配到的URC类型 + * @param pattern 输出:匹配到的URC模式 + * @param pattern_len 输出:URC模式长度 + * @return true=匹配成功,false=未匹配 + */ +static bool n706a_stream_parser_match_urc_identifier(uint8_t byte, urc_type_t *type, const char **pattern, uint32_t *pattern_len) { - ppp_at_response_t ppp_resp; - ppp_error_t ret = ppp_at_send_command(command, expected_response, timeout, &ppp_resp); + /* 原始设计:支持跨数据块匹配URC标识符 */ - if (response) { - /* 转换ppp_at_response_t到n706a_at_response_t */ - memcpy_s(response->response, sizeof(response->response), ppp_resp.response, sizeof(ppp_resp.response)); - response->length = ppp_resp.length; - response->success = ppp_resp.success; - response->error_code = (ret == PPP_ERR_OK) ? N706A_AT_OK : N706A_AT_ERR_AT_FAILED; + if (g_parser_ctx.urc_match_pos == 0 && byte != '+') { + /* 不是URC开始字符 */ + *type = URC_TYPE_NONE; + *pattern = NULL; + *pattern_len = 0; + return false; + } + + /* 检查所有URC模式 */ + for (int i = 0; urc_patterns[i].pattern != NULL; i++) { + const char *p = urc_patterns[i].pattern; + uint32_t len = urc_patterns[i].pattern_len; + + /* 如果当前匹配位置为0,检查是否匹配第一个字符 */ + if (g_parser_ctx.urc_match_pos == 0) { + if (byte == p[0]) { + /* 匹配第一个字符,开始匹配 */ + g_parser_ctx.urc_match_pos = 1; + g_parser_ctx.urc_pattern = p; + g_parser_ctx.urc_pattern_len = len; + + if (len == 1) { + /* 单字符模式,匹配成功 */ + *type = urc_patterns[i].type; + *pattern = p; + *pattern_len = len; + g_parser_ctx.urc_match_pos = 0; /* 重置 */ + return true; + } + /* 继续匹配 */ + return false; + } + } else if (g_parser_ctx.urc_pattern == p) { + /* 正在匹配这个模式 */ + if (g_parser_ctx.urc_match_pos < len && byte == p[g_parser_ctx.urc_match_pos]) { + g_parser_ctx.urc_match_pos++; + if (g_parser_ctx.urc_match_pos >= len) { + /* 匹配成功 */ + *type = urc_patterns[i].type; + *pattern = p; + *pattern_len = len; + g_parser_ctx.urc_match_pos = 0; /* 重置 */ + return true; + } + /* 继续匹配 */ + return false; + } else { + /* 匹配失败,重置 */ + g_parser_ctx.urc_match_pos = 0; + g_parser_ctx.urc_pattern = NULL; + g_parser_ctx.urc_pattern_len = 0; + /* 如果当前字节是'+',重新开始匹配 */ + if (byte == '+') { + return n706a_stream_parser_match_urc_identifier(byte, type, pattern, pattern_len); + } + } + } + } + + /* 没有匹配到 */ + if (g_parser_ctx.urc_match_pos > 0) { + /* 部分匹配失败,重置 */ + g_parser_ctx.urc_match_pos = 0; + g_parser_ctx.urc_pattern = NULL; + g_parser_ctx.urc_pattern_len = 0; } - return (ret == PPP_ERR_OK) ? N706A_AT_OK : N706A_AT_ERR_AT_FAILED; + *type = URC_TYPE_NONE; + *pattern = NULL; + *pattern_len = 0; + return false; } /** - * @brief N706A对外导出:测试AT通信是否正常 + * @brief 处理URC头部(解析参数) + * @param data 数据指针 + * @param length 数据长度 + * @param consumed 输出:已消费的字节数 */ -n706a_at_error_t n706a_at_test_communication(void) +static void n706a_stream_parser_process_urc_header(const uint8_t *data, uint32_t length, uint32_t *consumed) { - if (!g_at_ctx.initialized) { - return N706A_AT_ERR_INVALID_PARAM; - } + *consumed = 0; - n706a_at_response_t response; - n706a_at_error_t ret = n706a_at_send_command("AT", "OK", 3000, &response); + /* TCPRECV/SSLTCPRECV 特殊处理:格式为 +TCPRECV: ,,\r\n */ + /* 数据直接跟在长度后面,没有换行符,所以需要解析长度字段,然后直接进入URC_DATA状态 */ + if (g_parser_ctx.urc_type == URC_TYPE_TCPRECV || g_parser_ctx.urc_type == URC_TYPE_SSLTCPRECV) { + /* 检查是否已经跳过标识符 */ + if (g_parser_ctx.urc_header_len == 0) { + /* 第一次处理,需要跳过标识符部分 */ + /* 标识符长度:+SSLTCPRECV: = 13字节,+TCPRECV: = 10字节 */ + uint32_t identifier_len = (g_parser_ctx.urc_type == URC_TYPE_SSLTCPRECV) ? 13 : 10; + + if (length > identifier_len) { + /* 当前数据块包含标识符和参数,解析socket_id和data_len */ + const char *header_str = (const char *)(data + identifier_len); + uint32_t header_remaining = length - identifier_len; + + /* 解析格式:,, */ + int socket_id = 0; /* 修复:初始化为0,而不是-1 */ + bool socket_id_parsed = false; /* 标记是否开始解析socket_id */ + uint32_t data_len = 0; + const char *p = header_str; + const char *end = header_str + header_remaining; + + /* 解析socket_id */ + while (p < end && *p >= '0' && *p <= '9') { + if (!socket_id_parsed) { + socket_id_parsed = true; /* 标记开始解析 */ + } + socket_id = socket_id * 10 + (*p - '0'); + p++; + } + + /* 验证socket_id是否成功解析 */ + if (!socket_id_parsed) { + PPP_DEBUG_ERROR("[StreamParser] Invalid TCPRECV header: socket_id not found"); + *consumed = 0; + return; + } + + /* 检查逗号 */ + if (p < end && *p == ',') { + p++; /* 跳过逗号 */ + + /* 解析data_len */ + while (p < end && *p >= '0' && *p <= '9') { + data_len = data_len * 10 + (*p - '0'); + p++; + } + + /* 检查逗号 */ + if (p < end && *p == ',') { + p++; /* 跳过逗号,p现在指向数据开始位置 */ + + /* 保存解析结果 */ + g_parser_ctx.tcp_socket_id = socket_id; + /* 修复:HEX模式下,remaining_data_len应该是data_len * 2 */ + g_parser_ctx.remaining_data_len = g_tcp_recv_hex_mode ? (data_len * 2) : data_len; + g_parser_ctx.received_data_len = 0; + + /* 计算已消费的字节数(标识符 + socket_id + 逗号 + data_len + 逗号) */ + uint32_t header_consumed = p - (const char *)data; + + /* 如果有数据,转换到URC_DATA状态,让URC_DATA处理函数处理数据(包括HEX解码) */ + if (p < end) { + uint32_t data_offset = p - (const char *)data; + /* 转换到URC_DATA状态,继续接收剩余数据 */ + /* 注意:不在这里传递数据,让URC_DATA状态处理(包括HEX解码) */ + g_parser_ctx.state = AT_PARSER_STATE_URC_DATA; + *consumed = header_consumed; /* 只消费头部,数据部分由URC_DATA状态处理 */ + + PPP_DEBUG_INFO("[StreamParser] TCPRECV header parsed: socket=%d, data_len=%u (binary), remaining_hex_len=%u, data_offset=%u, hex_mode=%d, entering URC_DATA state", + socket_id, data_len, g_parser_ctx.remaining_data_len, data_offset, g_tcp_recv_hex_mode); + } else { + /* 没有数据,只消费头部 */ + g_parser_ctx.state = AT_PARSER_STATE_URC_DATA; + *consumed = header_consumed; + + PPP_DEBUG_INFO("[StreamParser] TCPRECV header parsed: socket=%d, data_len=%u (binary), remaining_hex_len=%u, no data yet, hex_mode=%d, entering URC_DATA state", + socket_id, data_len, g_parser_ctx.remaining_data_len, g_tcp_recv_hex_mode); + } + } else { + /* 没有找到第二个逗号,数据不完整,等待更多数据 */ + *consumed = 0; + PPP_DEBUG_DEBUG("[StreamParser] TCPRECV header incomplete: waiting for second comma"); + } + } else { + /* 没有找到第一个逗号,数据不完整,等待更多数据 */ + *consumed = 0; + PPP_DEBUG_DEBUG("[StreamParser] TCPRECV header incomplete: waiting for first comma"); + } + } else { + /* 当前数据块只包含部分标识符,跳过已接收的部分 */ + *consumed = length; + g_parser_ctx.urc_header_len = length; /* 记录已跳过的字节数 */ + PPP_DEBUG_DEBUG("[StreamParser] TCPRECV identifier partially received: %u/%u bytes", + (unsigned int)length, identifier_len); + } + } else { + /* 已经跳过部分标识符,继续跳过剩余部分 */ + uint32_t identifier_len = (g_parser_ctx.urc_type == URC_TYPE_SSLTCPRECV) ? 13 : 10; + uint32_t remaining_identifier = identifier_len - g_parser_ctx.urc_header_len; + + if (length >= remaining_identifier) { + /* 当前数据块包含剩余标识符和参数,重新解析 */ + const char *header_str = (const char *)(data + remaining_identifier); + uint32_t header_remaining = length - remaining_identifier; + + /* 解析格式:,, */ + int socket_id = 0; /* 修复:初始化为0,而不是-1 */ + bool socket_id_parsed = false; /* 标记是否开始解析socket_id */ + uint32_t data_len = 0; + const char *p = header_str; + const char *end = header_str + header_remaining; + + /* 解析socket_id */ + while (p < end && *p >= '0' && *p <= '9') { + if (!socket_id_parsed) { + socket_id_parsed = true; /* 标记开始解析 */ + } + socket_id = socket_id * 10 + (*p - '0'); + p++; + } + + /* 验证socket_id是否成功解析 */ + if (!socket_id_parsed) { + PPP_DEBUG_ERROR("[StreamParser] Invalid TCPRECV header: socket_id not found"); + *consumed = 0; + return; + } + + /* 检查逗号 */ + if (p < end && *p == ',') { + p++; /* 跳过逗号 */ + + /* 解析data_len */ + while (p < end && *p >= '0' && *p <= '9') { + data_len = data_len * 10 + (*p - '0'); + p++; + } + + /* 检查逗号 */ + if (p < end && *p == ',') { + p++; /* 跳过逗号,p现在指向数据开始位置 */ + + /* 保存解析结果 */ + g_parser_ctx.tcp_socket_id = socket_id; + /* 修复:HEX模式下,remaining_data_len应该是data_len * 2 */ + g_parser_ctx.remaining_data_len = g_tcp_recv_hex_mode ? (data_len * 2) : data_len; + g_parser_ctx.received_data_len = 0; + g_parser_ctx.urc_header_len = 0; /* 标识符已完全跳过,重置 */ + + /* 计算已消费的字节数 */ + uint32_t header_consumed = p - (const char *)data; + + /* 如果有数据,转换到URC_DATA状态,让URC_DATA处理函数处理数据(包括HEX解码) */ + if (p < end) { + uint32_t data_offset = p - (const char *)data; + /* 转换到URC_DATA状态,继续接收剩余数据 */ + /* 注意:不在这里传递数据,让URC_DATA状态处理(包括HEX解码) */ + g_parser_ctx.state = AT_PARSER_STATE_URC_DATA; + *consumed = header_consumed; /* 只消费头部,数据部分由URC_DATA状态处理 */ + + PPP_DEBUG_INFO("[StreamParser] TCPRECV header fully parsed: socket=%d, data_len=%u (binary), remaining_hex_len=%u, data_offset=%u, hex_mode=%d, entering URC_DATA state", + socket_id, data_len, g_parser_ctx.remaining_data_len, data_offset, g_tcp_recv_hex_mode); + } else { + /* 没有数据,只消费头部 */ + g_parser_ctx.state = AT_PARSER_STATE_URC_DATA; + *consumed = header_consumed; + + PPP_DEBUG_INFO("[StreamParser] TCPRECV header fully parsed: socket=%d, data_len=%u (binary), remaining_hex_len=%u, no data yet, hex_mode=%d, entering URC_DATA state", + socket_id, data_len, g_parser_ctx.remaining_data_len, g_tcp_recv_hex_mode); + } + } else { + /* 没有找到第二个逗号,数据不完整,等待更多数据 */ + *consumed = 0; + PPP_DEBUG_DEBUG("[StreamParser] TCPRECV header incomplete: waiting for second comma"); + } + } else { + /* 没有找到第一个逗号,数据不完整,等待更多数据 */ + *consumed = 0; + PPP_DEBUG_DEBUG("[StreamParser] TCPRECV header incomplete: waiting for first comma"); + } + } else { + /* 当前数据块只包含部分剩余标识符,继续跳过 */ + *consumed = length; + g_parser_ctx.urc_header_len += length; + PPP_DEBUG_DEBUG("[StreamParser] TCPRECV identifier partially skipped: %u/%u bytes", + g_parser_ctx.urc_header_len, identifier_len); + } + } + + return; + } - if (ret == N706A_AT_OK && response.success) { - PPP_DEBUG_INFO("AT communication test passed\n"); - return N706A_AT_OK; + if (g_parser_ctx.urc_type == URC_TYPE_HTTPSRECV || g_parser_ctx.urc_type == URC_TYPE_HTTPRECV) { + /* HTTPRECV/HTTPSRECV 特殊处理:格式2(无长度字段),数据直接跟在标识符后面 */ + /* 优化:一旦识别到标识符,立即跳过标识符,直接传递数据,避免缓冲区溢出 */ + /* ⚠️ 注意:标识符后面可能还有空格或 \r\n,这些应该保留,不能跳过 */ + + /* 检查是否已经跳过标识符 */ + if (g_parser_ctx.urc_header_len == 0) { + /* 第一次处理,需要跳过标识符部分 */ + /* 标识符长度:+HTTPSRECV: = 12字节,+HTTPRECV: = 11字节 */ + uint32_t identifier_len = (g_parser_ctx.urc_type == URC_TYPE_HTTPSRECV) ? 12 : 11; + + if (length > identifier_len) { + /* 当前数据块包含标识符和数据,跳过标识符,直接传递数据 */ + /* ⚠️ 关键:只跳过标识符本身,不跳过标识符后的空格或 \r\n */ + /* 这些字符是HTTP响应数据的一部分,必须保留 */ + uint32_t data_offset = identifier_len; + uint32_t data_len = length - data_offset; + + /* 调试:打印跳过标识符后的数据开头(前128字节) */ + size_t debug_len = (data_len < 128) ? data_len : 128; + char debug_buf[129] = {0}; + for (size_t i = 0; i < debug_len && i < 128; i++) { + uint8_t b = data[data_offset + i]; + if (b >= 32 && b < 127) { + debug_buf[i] = b; + } else if (b == '\r') { + debug_buf[i] = '\\'; + if (i + 1 < 128) debug_buf[++i] = 'r'; + } else if (b == '\n') { + debug_buf[i] = '\\'; + if (i + 1 < 128) debug_buf[++i] = 'n'; + } else { + debug_buf[i] = '.'; + } + } + PPP_DEBUG_INFO("[StreamParser] HTTPRECV data after identifier (first %u bytes): %s", + (unsigned int)debug_len, debug_buf); + + /* 检查数据开头是否是HTTP状态行,如果不是,记录警告 */ + if (data_len >= 5 && memcmp(data + data_offset, "HTTP/", 5) != 0) { + /* 数据开头不是 "HTTP/",可能是空格、\r\n 或其他字符 */ + /* 这些字符应该保留,因为它们是HTTP响应数据的一部分 */ + PPP_DEBUG_WARN("[StreamParser] HTTPRECV data does not start with 'HTTP/' after identifier. " + "First bytes: 0x%02X 0x%02X 0x%02X 0x%02X 0x%02X", + data[data_offset], + data_len > 1 ? data[data_offset + 1] : 0, + data_len > 2 ? data[data_offset + 2] : 0, + data_len > 3 ? data[data_offset + 3] : 0, + data_len > 4 ? data[data_offset + 4] : 0); + } + + extern void n706a_http_on_urc_received(const uint8_t *data, size_t length, bool is_https); + n706a_http_on_urc_received(data + data_offset, data_len, + g_parser_ctx.urc_type == URC_TYPE_HTTPSRECV); + g_parser_ctx.received_data_len += data_len; + g_parser_ctx.is_https = (g_parser_ctx.urc_type == URC_TYPE_HTTPSRECV); + g_parser_ctx.remaining_data_len = 0; /* 格式2没有长度字段,使用0表示未知长度 */ + g_parser_ctx.state = AT_PARSER_STATE_URC_DATA; /* 转换到URC_DATA状态,继续接收数据 */ + *consumed = length; + + PPP_DEBUG_INFO("[StreamParser] HTTPRECV identifier skipped, data passed: %u bytes, entering URC_DATA state", + (unsigned int)data_len); + } else { + /* 当前数据块只包含部分标识符,跳过已接收的部分 */ + *consumed = length; + g_parser_ctx.urc_header_len = length; /* 记录已跳过的字节数 */ + PPP_DEBUG_DEBUG("[StreamParser] HTTPRECV identifier partially received: %u/%u bytes", + (unsigned int)length, identifier_len); + } + } else { + /* 已经跳过部分标识符,继续跳过剩余部分 */ + uint32_t identifier_len = (g_parser_ctx.urc_type == URC_TYPE_HTTPSRECV) ? 12 : 11; + uint32_t remaining_identifier = identifier_len - g_parser_ctx.urc_header_len; + + if (length >= remaining_identifier) { + /* 当前数据块包含剩余标识符和数据,跳过标识符,传递数据 */ + uint32_t data_offset = remaining_identifier; + uint32_t data_len = length - data_offset; + + extern void n706a_http_on_urc_received(const uint8_t *data, size_t length, bool is_https); + n706a_http_on_urc_received(data + data_offset, data_len, + g_parser_ctx.urc_type == URC_TYPE_HTTPSRECV); + g_parser_ctx.received_data_len += data_len; + g_parser_ctx.is_https = (g_parser_ctx.urc_type == URC_TYPE_HTTPSRECV); + g_parser_ctx.remaining_data_len = 0; /* 格式2没有长度字段,使用0表示未知长度 */ + g_parser_ctx.urc_header_len = 0; /* 标识符已完全跳过,重置 */ + g_parser_ctx.state = AT_PARSER_STATE_URC_DATA; /* 转换到URC_DATA状态,继续接收数据 */ + *consumed = length; + + PPP_DEBUG_INFO("[StreamParser] HTTPRECV identifier fully skipped, data passed: %u bytes, entering URC_DATA state", + (unsigned int)data_len); + } else { + /* 当前数据块只包含部分剩余标识符,继续跳过 */ + *consumed = length; + g_parser_ctx.urc_header_len += length; + PPP_DEBUG_DEBUG("[StreamParser] HTTPRECV identifier partially skipped: %u/%u bytes", + g_parser_ctx.urc_header_len, identifier_len); + } + } + + return; } else { - PPP_DEBUG_ERROR("AT communication test failed\n"); - return N706A_AT_ERR_AT_FAILED; + /* 其他URC类型,使用简单的头部解析 */ + /* 将数据追加到URC头部缓冲区,直到遇到换行符 */ + uint32_t consumed_before_loop = *consumed; /* 记录循环前的consumed值 */ + + while (*consumed < length && g_parser_ctx.urc_header_len < sizeof(g_parser_ctx.urc_header_buf) - 1) { + uint8_t byte = data[*consumed]; + g_parser_ctx.urc_header_buf[g_parser_ctx.urc_header_len++] = byte; + (*consumed)++; + + /* 检查是否到达URC头部结束(遇到换行符) + * 支持多种格式: + * - \r\n (标准格式) + * - \n (只有\n) + * - \r (只有\r,可能跨数据块) + * - \r\r (双\r,某些模块的特殊格式) + */ + if (byte == '\n') { + /* \n 结尾 */ + g_parser_ctx.urc_header_buf[g_parser_ctx.urc_header_len] = '\0'; + /* 解析并处理URC */ + n706a_stream_parser_dispatch_urc(); + n706a_stream_parser_reset(); + return; + } else if (byte == '\r') { + /* \r 结尾,检查下一个字节 */ + if (*consumed < length) { + uint8_t next_byte = data[*consumed]; + if (next_byte == '\n') { + /* \r\n 结尾 */ + g_parser_ctx.urc_header_buf[g_parser_ctx.urc_header_len] = '\0'; + /* 解析并处理URC */ + n706a_stream_parser_dispatch_urc(); + n706a_stream_parser_reset(); + return; + } else if (next_byte == '\r') { + /* \r\r 结尾 */ + g_parser_ctx.urc_header_buf[g_parser_ctx.urc_header_len] = '\0'; + /* 解析并处理URC */ + n706a_stream_parser_dispatch_urc(); + n706a_stream_parser_reset(); + return; + } + /* 只有 \r,可能跨数据块,继续累积 */ + } else { + /* \r 在数据块末尾,可能跨数据块,继续累积 */ + /* 注意:这里不重置,等待下一个数据块 */ + } + } + } + + /* 修复:确保consumed只反映实际消费的字节数,而不是缓冲区大小 */ + /* 如果while循环因为缓冲区满而退出,consumed应该等于实际累积的字节数 */ + /* 如果while循环因为数据用完而退出,consumed应该等于length */ + /* 如果while循环没有执行(缓冲区已满且consumed=0),至少消费1个字节 */ + if (*consumed == 0 && length > 0) { + /* 缓冲区已满,无法继续累积 */ + /* 这种情况不应该发生,因为URC头部通常不会超过256字节 */ + /* 但如果真的发生了,我们需要处理:累积当前字符到缓冲区(如果还有空间) */ + if (g_parser_ctx.urc_header_len < sizeof(g_parser_ctx.urc_header_buf) - 1) { + /* 还有空间,累积当前字符 */ + g_parser_ctx.urc_header_buf[g_parser_ctx.urc_header_len++] = data[0]; + *consumed = 1; + PPP_DEBUG_WARN("[StreamParser] URC header buffer接近满,累积字符: 0x%02X", data[0]); + } else { + /* 缓冲区真的满了,这是异常情况 */ + PPP_DEBUG_ERROR("[StreamParser] URC header buffer已满,无法继续处理!重置状态"); + n706a_stream_parser_reset(); + *consumed = 1; /* 跳过当前字符,避免死循环 */ + } + } else { + /* consumed > 0,说明while循环执行了 */ + /* 确保consumed不超过length,这是安全的 */ + if (*consumed > length) { + PPP_DEBUG_ERROR("[StreamParser] URC header consumed (%u) > length (%u), 修复为length", + *consumed, length); + *consumed = length; + } + + /* 关键修复:如果缓冲区满了但没有遇到换行符,说明URC头部异常长 */ + /* 这种情况可能是数据错误,或者URC头部真的非常长 */ + /* 如果urc_header_len达到255(缓冲区满),但没有遇到换行符,应该重置状态 */ + if (g_parser_ctx.urc_header_len >= sizeof(g_parser_ctx.urc_header_buf) - 1) { + /* 缓冲区满了,但没有遇到换行符,这是异常情况 */ + /* 可能的原因: + * 1. URC头部真的非常长(超过255字节) + * 2. 数据错误,没有换行符 + * 3. 数据被截断,或者这不是URC头部而是数据 + */ + PPP_DEBUG_ERROR("[StreamParser] URC header buffer满但没有换行符!urc_header_len=%u, consumed=%u, 重置状态并消费数据", + g_parser_ctx.urc_header_len, *consumed); + /* 关键修复:必须消费已累积的数据,否则会死循环 */ + /* 如果consumed=0,说明while循环没有执行,这不应该发生 */ + /* 如果consumed>0,说明while循环执行了,应该保留consumed的值 */ + /* 这样pos会前进,不会死循环 */ + if (*consumed == 0) { + /* 这种情况不应该发生,但如果发生了,至少消费1个字节 */ + PPP_DEBUG_ERROR("[StreamParser] 异常:缓冲区满但consumed=0,强制消费1字节"); + *consumed = 1; + } + /* 重置状态,让解析器重新开始 */ + n706a_stream_parser_reset(); + /* 保留consumed的值,让pos前进,避免死循环 */ + /* 这样虽然会丢失一些数据,但至少不会死循环 */ + } else { + /* 缓冲区未满,正常情况,继续等待更多数据 */ + /* 添加调试信息:如果consumed很大,记录详细信息 */ + if (*consumed > 100) { + PPP_DEBUG_WARN("[StreamParser] URC header consumed很大: %u, length=%u, urc_header_len=%u, buffer_size=%zu", + *consumed, length, g_parser_ctx.urc_header_len, sizeof(g_parser_ctx.urc_header_buf)); + } + } + } } } /** - * @brief N706A对外导出:获取网络信号信息 + * @brief 分发URC(根据URC类型调用相应的回调函数) */ -n706a_at_error_t n706a_at_get_network_signal_info(n706a_network_signal_info_t *signal_info) +static void n706a_stream_parser_dispatch_urc(void) { - if (!signal_info) { - return N706A_AT_ERR_INVALID_PARAM; - } + const char *urc_header = (const char *)g_parser_ctx.urc_header_buf; + uint32_t urc_header_len = g_parser_ctx.urc_header_len; - /* 调用内部的ppp_at_get_network_signal_info */ - n706a_network_signal_info_t ppp_signal; - ppp_error_t ret = ppp_at_get_network_signal_info(&ppp_signal); + PPP_DEBUG_INFO("[StreamParser] Dispatching URC: type=%d, header=[%.*s]", + g_parser_ctx.urc_type, (int)urc_header_len, urc_header); - if (ret == PPP_ERR_OK) { - /* 转换数据结构 */ - signal_info->rxlev = ppp_signal.rxlev; - signal_info->ber = ppp_signal.ber; - signal_info->rscp = ppp_signal.rscp; - signal_info->ecno = ppp_signal.ecno; - signal_info->rsrq = ppp_signal.rsrq; - signal_info->rsrp = ppp_signal.rsrp; - signal_info->valid = ppp_signal.valid; - return N706A_AT_OK; - } else { - signal_info->valid = false; - return N706A_AT_ERR_AT_FAILED; + switch (g_parser_ctx.urc_type) { + case URC_TYPE_TCPRECV: + case URC_TYPE_SSLTCPRECV: { + /* +TCPRECV: ,, 或 +SSLTCPRECV: ,, */ + if (g_tcp_urc_callback) { + int socket_id; + size_t data_len; + const uint8_t *tcp_data; + + if (n706a_parse_tcprecv_urc(urc_header, urc_header_len, &socket_id, &data_len, &tcp_data)) { + /* 注意:TCPRECV的数据部分可能跨数据块,需要在URC_DATA状态中处理 */ + /* 这里只处理头部,数据部分在URC_DATA状态中处理 */ + PPP_DEBUG_INFO("[StreamParser] TCPRECV URC parsed: socket=%d, data_len=%u", + socket_id, (unsigned int)data_len); + /* 保存socket_id到上下文 */ + g_parser_ctx.tcp_socket_id = socket_id; + /* 计算数据在头部缓冲区中的偏移量 */ + uint32_t data_offset = (const uint8_t *)tcp_data - (const uint8_t *)urc_header; + if (data_offset < urc_header_len) { + /* 数据在头部缓冲区中,立即传递这部分数据 */ + uint32_t header_data_len = urc_header_len - data_offset; + if (header_data_len > 0 && header_data_len <= data_len) { + g_tcp_urc_callback(socket_id, (const uint8_t *)tcp_data, header_data_len); + g_parser_ctx.received_data_len = header_data_len; + g_parser_ctx.remaining_data_len = data_len - header_data_len; + } else { + g_parser_ctx.received_data_len = 0; + g_parser_ctx.remaining_data_len = data_len; + } + } else { + /* 数据不在头部缓冲区中,等待后续数据块 */ + g_parser_ctx.received_data_len = 0; + g_parser_ctx.remaining_data_len = data_len; + } + /* 转换到URC_DATA状态 */ + g_parser_ctx.state = AT_PARSER_STATE_URC_DATA; + } else { + PPP_DEBUG_WARN("[StreamParser] Failed to parse TCPRECV URC"); + } + } + break; + } + + case URC_TYPE_TCPSETUP: + case URC_TYPE_SSLTCPSETUP: { + /* +TCPSETUP: ,OK 或 +TCPSETUP: ,FAIL */ + if (g_tcpsetup_urc_callback) { + int socket_id; + bool success; + + if (n706a_parse_tcpsetup_urc(urc_header, urc_header_len, &socket_id, &success)) { + PPP_DEBUG_INFO("[StreamParser] TCPSETUP URC parsed: socket=%d, success=%d", socket_id, success); + g_tcpsetup_urc_callback(socket_id, success); + } else { + PPP_DEBUG_WARN("[StreamParser] Failed to parse TCPSETUP URC"); + } + } + break; + } + + case URC_TYPE_TCPCLOSE: + case URC_TYPE_SSLTCPCLOSE: { + /* +TCPCLOSE: 或 +SSLTCPCLOSE: */ + if (g_tcpclose_urc_callback) { + int socket_id; + + if (n706a_parse_tcpclose_urc(urc_header, urc_header_len, &socket_id)) { + PPP_DEBUG_INFO("[StreamParser] TCPCLOSE URC parsed: socket=%d", socket_id); + g_tcpclose_urc_callback(socket_id); + } else { + PPP_DEBUG_WARN("[StreamParser] Failed to parse TCPCLOSE URC"); + } + } + break; + } + + case URC_TYPE_TCPSEND: + case URC_TYPE_SSLTCPEND: { + /* +TCPSEND: , 或 +SSLTCPEND: , */ + if (g_tcpsend_urc_callback) { + int socket_id; + size_t sent_bytes; + + if (n706a_parse_tcpsend_urc(urc_header, urc_header_len, &socket_id, &sent_bytes)) { + PPP_DEBUG_INFO("[StreamParser] TCPSEND/SSLTCPEND URC parsed: socket=%d, sent=%u", + socket_id, (unsigned int)sent_bytes); + g_tcpsend_urc_callback(socket_id, sent_bytes); + } else { + PPP_DEBUG_WARN("[StreamParser] Failed to parse TCPSEND/SSLTCPEND URC"); + } + } + break; + } + + default: + PPP_DEBUG_DEBUG("[StreamParser] URC type %d not handled in dispatch", g_parser_ctx.urc_type); + break; } } /** - * @brief N706A对外导出:设置波特率 + * @brief 检测HTTPCLOSED标识符(支持跨数据块) + * @param data 数据指针 + * @param length 数据长度 + * @param closed_pos 输出:HTTPCLOSED标识符位置(如果找到) + * @return true=找到,false=未找到 */ -n706a_at_error_t n706a_at_set_baudrate(uint32_t baudrate) +static bool n706a_stream_parser_detect_httpclosed(const uint8_t *data, uint32_t length, uint32_t *closed_pos) { - ppp_error_t ret = ppp_at_set_baudrate(baudrate); - return (ret == PPP_ERR_OK) ? N706A_AT_OK : N706A_AT_ERR_UART; + static const char *closed_patterns[] = {"+HTTPSCLOSED:", "+HTTPCLOSED:"}; + static uint32_t closed_match_pos[2] = {0, 0}; // 两个模式的匹配位置 + + for (uint32_t i = 0; i < length; i++) { + uint8_t byte = data[i]; + + /* 检查两个模式 */ + for (int p = 0; p < 2; p++) { + const char *pattern = closed_patterns[p]; + uint32_t pattern_len = strlen(pattern); + + if (closed_match_pos[p] < pattern_len) { + if (byte == pattern[closed_match_pos[p]]) { + closed_match_pos[p]++; + if (closed_match_pos[p] >= pattern_len) { + /* 匹配成功 */ + *closed_pos = i + 1 - pattern_len; + /* 重置所有匹配位置 */ + closed_match_pos[0] = 0; + closed_match_pos[1] = 0; + return true; + } + } else { + /* 匹配失败,重置 */ + closed_match_pos[p] = 0; + /* 如果当前字节是 '+',重新开始匹配 */ + if (byte == '+') { + closed_match_pos[p] = 1; + } + } + } + } + } + + return false; } /** - * @brief N706A对外导出:获取当前波特率 + * @brief 处理URC数据(直接传递给回调) + * @param data 数据指针 + * @param length 数据长度 + * @param consumed 输出:已消费的字节数 */ -uint32_t n706a_at_get_current_baudrate(void) +static void n706a_stream_parser_process_urc_data(const uint8_t *data, uint32_t length, uint32_t *consumed) { - return g_at_ctx.current_baudrate; + *consumed = 0; + + /* 方案2修复:检测数据是否真的是URC数据,还是新的AT响应 */ + /* 如果检测到AT响应开始(AT+),说明URC数据已结束,应该重置状态 */ + if (length >= 2 && data[0] == 'A' && data[1] == 'T') { + PPP_DEBUG_WARN("[StreamParser] 在URC_DATA状态检测到AT响应开始,重置状态"); + n706a_stream_parser_reset(); + /* 不消费字符,让IDLE状态处理 */ + return; + } + + if (g_parser_ctx.urc_type == URC_TYPE_HTTPSRECV || g_parser_ctx.urc_type == URC_TYPE_HTTPRECV) { + /* HTTPRECV/HTTPSRECV 特殊处理:格式2(无长度字段),需要检测 +HTTPSCLOSED */ + uint32_t closed_pos = 0; + bool found_closed = n706a_stream_parser_detect_httpclosed(data, length, &closed_pos); + + uint32_t to_consume = 0; + if (found_closed) { + /* 找到 +HTTPSCLOSED,数据结束 */ + to_consume = closed_pos; + PPP_DEBUG_INFO("[StreamParser] Found +HTTPSCLOSED at offset %u, URC data complete: total=%u bytes", + closed_pos, (unsigned int)(g_parser_ctx.received_data_len + to_consume)); + } else if (g_parser_ctx.remaining_data_len > 0) { + /* 有剩余数据长度,使用剩余长度 */ + to_consume = (length < g_parser_ctx.remaining_data_len) ? length : g_parser_ctx.remaining_data_len; + } else { + /* 格式2且没有剩余长度,传递所有数据(直到找到 +HTTPSCLOSED) */ + /* 但需要检查数据中是否包含AT响应开始 */ + to_consume = length; + for (uint32_t i = 0; i < length - 1; i++) { + if (data[i] == 'A' && data[i+1] == 'T') { + /* 找到AT响应开始,只处理到此处 */ + to_consume = i; + PPP_DEBUG_WARN("[StreamParser] 在HTTPRECV数据中检测到AT响应开始,提前结束URC数据"); + break; + } + } + } + + if (to_consume > 0) { + extern void n706a_http_on_urc_received(const uint8_t *data, size_t length, bool is_https); + extern void n706a_http_mark_connection_closed(void); + + n706a_http_on_urc_received(data, to_consume, g_parser_ctx.is_https); + g_parser_ctx.received_data_len += to_consume; + *consumed = to_consume; + + if (found_closed) { + /* 检测到+HTTPSCLOSED,标记连接已关闭 */ + n706a_http_mark_connection_closed(); + /* 数据已完整,重置状态 */ + g_parser_ctx.urc_count++; + n706a_stream_parser_reset(); + } else if (g_parser_ctx.remaining_data_len > 0) { + /* 更新剩余数据长度 */ + g_parser_ctx.remaining_data_len -= to_consume; + if (g_parser_ctx.remaining_data_len == 0) { + PPP_DEBUG_INFO("[StreamParser] URC data complete: total=%u bytes", + (unsigned int)g_parser_ctx.received_data_len); + g_parser_ctx.urc_count++; + n706a_stream_parser_reset(); + } + } else if (to_consume < length) { + /* 因为检测到AT响应而提前结束,重置状态 */ + PPP_DEBUG_INFO("[StreamParser] URC data ended early due to AT response, resetting state"); + n706a_stream_parser_reset(); + } + } else if (length > 0) { + /* 没有数据要处理,但至少消费1个字节,避免死循环 */ + /* 这种情况不应该发生,但如果发生了,至少消费1个字节 */ + *consumed = 1; + PPP_DEBUG_WARN("[StreamParser] URC_DATA: no data to process but length>0, consuming 1 byte"); + } + } else if (g_parser_ctx.remaining_data_len > 0) { + /* 其他URC类型,使用剩余数据长度 */ + /* 方案2修复:检测数据中是否包含AT响应开始 */ + uint32_t to_consume = (length < g_parser_ctx.remaining_data_len) ? length : g_parser_ctx.remaining_data_len; + + /* 检查数据中是否包含AT响应开始 */ + for (uint32_t i = 0; i < to_consume - 1; i++) { + if (data[i] == 'A' && data[i+1] == 'T') { + /* 找到AT响应开始,只处理到此处 */ + to_consume = i; + PPP_DEBUG_WARN("[StreamParser] 在URC数据中检测到AT响应开始,提前结束URC数据"); + break; + } + } + + if (g_parser_ctx.urc_type == URC_TYPE_TCPRECV || g_parser_ctx.urc_type == URC_TYPE_SSLTCPRECV) { + /* TCPRECV数据传递 */ + if (g_tcp_urc_callback && to_consume > 0) { + if (g_tcp_recv_hex_mode) { + /* HEX模式:需要解码HEX字符串为二进制数据 */ + /* 处理跨数据块的不完整HEX对 */ + uint32_t hex_data_start = 0; + uint32_t hex_data_len = to_consume; + + /* 如果有部分数据,先合并 */ + if (g_parser_ctx.partial_buffer_len > 0) { + /* 合并部分数据和当前数据 */ + if (g_parser_ctx.partial_buffer_len + hex_data_len > sizeof(g_parser_ctx.partial_buffer)) { + PPP_DEBUG_ERROR("[StreamParser] Partial buffer overflow: partial_len=%u, new_len=%u, max=%u", + g_parser_ctx.partial_buffer_len, hex_data_len, (unsigned int)sizeof(g_parser_ctx.partial_buffer)); + /* 清空部分缓冲区,只处理当前数据 */ + g_parser_ctx.partial_buffer_len = 0; + } else { + memcpy(g_parser_ctx.partial_buffer + g_parser_ctx.partial_buffer_len, data, hex_data_len); + hex_data_len += g_parser_ctx.partial_buffer_len; + hex_data_start = 0; /* 使用partial_buffer */ + } + } + + /* 确保HEX数据长度是偶数(HEX对) */ + uint32_t hex_pairs_len = (hex_data_len / 2) * 2; + uint32_t remaining_hex = hex_data_len - hex_pairs_len; + + /* 解码HEX数据 */ + const char *hex_str = (hex_data_start == 0 && g_parser_ctx.partial_buffer_len > 0) ? + (const char *)g_parser_ctx.partial_buffer : (const char *)data; + /* 优化:使用静态缓冲区,避免在栈上分配2048字节(这是导致栈溢出的主要原因) */ + /* 注意:这个函数在单线程(ppp_irq_proc)中调用,所以静态缓冲区是安全的 */ + static uint8_t binary_buffer[2048]; /* 静态二进制缓冲区,避免栈溢出 */ + size_t binary_len = 0; + + if (hex_pairs_len > 0) { + /* 限制解码长度,避免缓冲区溢出 */ + size_t max_binary_len = hex_pairs_len / 2; + if (max_binary_len > sizeof(binary_buffer)) { + max_binary_len = sizeof(binary_buffer); + hex_pairs_len = max_binary_len * 2; /* 调整HEX长度以匹配缓冲区 */ + } + + binary_len = n706a_hex_string_to_binary(hex_str, hex_pairs_len, binary_buffer, sizeof(binary_buffer)); + if (binary_len > 0) { + g_tcp_urc_callback(g_parser_ctx.tcp_socket_id, binary_buffer, binary_len); + PPP_DEBUG_DEBUG("[StreamParser] TCPRECV HEX data decoded: socket=%d, hex_len=%u, binary_len=%u", + g_parser_ctx.tcp_socket_id, hex_pairs_len, (unsigned int)binary_len); + } else { + PPP_DEBUG_ERROR("[StreamParser] Failed to decode HEX data: hex_len=%u", hex_pairs_len); + } + } + + /* 保存不完整的HEX字符到partial_buffer */ + if (remaining_hex > 0) { + const char *remaining_hex_str = (hex_data_start == 0 && g_parser_ctx.partial_buffer_len > 0) ? + (const char *)(g_parser_ctx.partial_buffer + hex_pairs_len) : + (const char *)(data + hex_pairs_len); + memcpy(g_parser_ctx.partial_buffer, remaining_hex_str, remaining_hex); + g_parser_ctx.partial_buffer_len = remaining_hex; + PPP_DEBUG_DEBUG("[StreamParser] Saved %u incomplete HEX characters to partial buffer", remaining_hex); + } else { + g_parser_ctx.partial_buffer_len = 0; /* 没有不完整的数据 */ + } + + /* 更新consumed:HEX模式下,consumed应该是HEX字符串的长度 */ + /* 注意:to_consume已经是HEX字符串的长度,所以不需要修改 */ + /* 但是,我们需要更新remaining_data_len,减去已处理的HEX字符数 */ + /* 注意:这里to_consume是HEX字符串的长度,remaining_data_len也是HEX字符串的长度 */ + } else { + /* 非HEX模式:直接传递二进制数据 */ + g_tcp_urc_callback(g_parser_ctx.tcp_socket_id, data, to_consume); + PPP_DEBUG_DEBUG("[StreamParser] TCPRECV binary data passed: socket=%d, len=%u", + g_parser_ctx.tcp_socket_id, (unsigned int)to_consume); + } + } + } + + /* 更新remaining_data_len和received_data_len */ + g_parser_ctx.remaining_data_len -= to_consume; + g_parser_ctx.received_data_len += to_consume; + *consumed = to_consume; + + if (g_parser_ctx.remaining_data_len == 0 || to_consume < length) { + /* 数据已完整,或因为检测到AT响应而提前结束 */ + if (to_consume < length) { + PPP_DEBUG_INFO("[StreamParser] URC data ended early due to AT response, resetting state"); + } else { + PPP_DEBUG_INFO("[StreamParser] URC data complete: total=%u bytes", + (unsigned int)g_parser_ctx.received_data_len); + } + g_parser_ctx.urc_count++; + n706a_stream_parser_reset(); + } + } else { + /* 没有剩余数据长度,重置状态 */ + PPP_DEBUG_WARN("[StreamParser] URC_DATA状态但没有剩余数据长度,重置状态"); + n706a_stream_parser_reset(); + *consumed = 1; + } } /** - * @brief 解析 +TCPRECV URC - * @param urc_data URC数据(以 "+TCPRECV: " 开头) - * @param urc_len URC数据长度 - * @param socket_id 输出:socket ID - * @param data_len 输出:数据长度 - * - ASCII模式:二进制数据长度(字节数) - * - HEX模式:HEX字符串长度(字节数),需要除以2得到二进制数据长度 - * @param data 输出:数据指针(指向 urc_data 内部) - * - ASCII模式:指向ASCII数据 - * - HEX模式:指向HEX字符串 - * @return true=解析成功,false=失败 - * - * URC格式(ASCII模式):+TCPRECV: ,,\r\n - * 例如:+TCPRECV: 0,10,1234567890\r\n (10字节ASCII数据) - * 注意:是二进制数据长度(10) - * - * URC格式(HEX模式):+TCPRECV: ,,\r\n - * 例如:+TCPRECV: 0,6,313233\r\n (表示3字节二进制数据:0x31, 0x32, 0x33) - * 注意:是HEX字符串长度(6),不是二进制数据长度(3) + * @brief 检测AT响应结束(支持多种换行符格式) + * @param data 数据指针 + * @param length 数据长度 + * @param terminator_pos 输出:终止符位置(如果找到) + * @return true=找到终止符,false=未找到 * - * 与AT+TCPSEND的对应关系: - * - AT+TCPSEND的length参数:二进制数据长度(字节数) - * - +TCPRECV的length参数:HEX模式下是HEX字符串长度(字节数)= 二进制数据长度 × 2 + * 支持的格式: + * - \r\nOK\r\n (标准格式) + * - \r\nOK\r (只有\r结尾) + * - \r\nOK\n (只有\n结尾) + * - \r\nOK (没有结尾换行符,在数据块末尾) + * - \r\nERROR\r\n + * - \r\nERROR\r + * - \r\nERROR\n + * - \r\nERROR + * - \rOK\r\n (只有\r开头) + * - \nOK\r\n (只有\n开头) + * - \r\r (双\r结尾,某些模块的特殊格式) */ -static bool n706a_parse_tcprecv_urc(const char *urc_data, size_t urc_len, - int *socket_id, size_t *data_len, const uint8_t **data) +static bool n706a_stream_parser_detect_at_response_end(const uint8_t *data, uint32_t length, uint32_t *terminator_pos) { - if (!urc_data || !socket_id || !data_len || !data || urc_len < 15) { - return false; - } - - // 跳过 "+TCPRECV: " - const char *p = urc_data + 10; - const char *end = urc_data + urc_len; - - // 解析 socket_id - *socket_id = 0; - while (p < end && *p >= '0' && *p <= '9') { - *socket_id = *socket_id * 10 + (*p - '0'); - p++; - } - - if (p >= end || *p != ',') { - PPP_DEBUG_ERROR("URC parse error: no comma after socket_id"); - return false; - } - p++; // 跳过逗号 - - // 解析 length(HEX模式下这是HEX字符串的长度) - *data_len = 0; - while (p < end && *p >= '0' && *p <= '9') { - *data_len = *data_len * 10 + (*p - '0'); - p++; - } - - if (p >= end || *p != ',') { - PPP_DEBUG_ERROR("URC parse error: no comma after length"); + if (!data || length == 0) { return false; } - p++; // 跳过逗号 - // 数据起始位置 - *data = (const uint8_t *)p; + /* 方案2:增强检测 - 支持多种AT响应终止符 */ + /* 支持的终止符:OK, ERROR, CONNECT, +COMMAND: params, NO CARRIER 等 */ - // 验证数据长度是否合理 - size_t remaining = end - p; - if (*data_len > remaining) { - PPP_DEBUG_WARN("URC data_len=%u exceeds remaining=%u, truncating", - (unsigned int)*data_len, (unsigned int)remaining); - *data_len = remaining; + for (uint32_t i = 0; i < length; i++) { + /* 检查是否是 OK */ + if (i + 2 <= length && data[i] == 'O' && data[i+1] == 'K') { + /* 检查OK前面的分隔符 */ + bool valid_prefix = false; + uint32_t ok_start = i; + + /* 检查 \r\n 或 \r 或 \n 或开头 */ + if (ok_start >= 2 && data[ok_start-2] == '\r' && data[ok_start-1] == '\n') { + valid_prefix = true; /* \r\nOK */ + } else if (ok_start >= 1 && data[ok_start-1] == '\r') { + valid_prefix = true; /* \rOK */ + } else if (ok_start >= 1 && data[ok_start-1] == '\n') { + valid_prefix = true; /* \nOK */ + } else if (ok_start == 0) { + valid_prefix = true; /* OK在开头 */ + } + + if (valid_prefix) { + /* 检查OK后面的终止符 */ + uint32_t ok_end = i + 2; /* OK结束位置 */ + + /* 情况1: \r\nOK\r\n (标准格式) */ + if (ok_end + 2 <= length && data[ok_end] == '\r' && data[ok_end+1] == '\n') { + *terminator_pos = ok_end + 2; + return true; + } + /* 情况2: \r\nOK\r (只有\r结尾) */ + if (ok_end + 1 <= length && data[ok_end] == '\r') { + /* 检查后面是否还有\n(跨数据块的情况) */ + *terminator_pos = ok_end + 1; + return true; + } + /* 情况3: \r\nOK\n (只有\n结尾) */ + if (ok_end + 1 <= length && data[ok_end] == '\n') { + *terminator_pos = ok_end + 1; + return true; + } + /* 情况4: \r\nOK (没有结尾换行符,在数据块末尾) */ + if (ok_end >= length) { + *terminator_pos = ok_end; + return true; + } + /* 情况5: \r\nOK\r\r (双\r结尾) */ + if (ok_end + 2 <= length && data[ok_end] == '\r' && data[ok_end+1] == '\r') { + *terminator_pos = ok_end + 2; + return true; + } + } + } + + /* 检查是否是 ERROR */ + if (i + 5 <= length && + data[i] == 'E' && data[i+1] == 'R' && data[i+2] == 'R' && + data[i+3] == 'O' && data[i+4] == 'R') { + /* 检查ERROR前面的分隔符 */ + bool valid_prefix = false; + uint32_t error_start = i; + + /* 检查 \r\n 或 \r 或 \n 或开头 */ + if (error_start >= 2 && data[error_start-2] == '\r' && data[error_start-1] == '\n') { + valid_prefix = true; /* \r\nERROR */ + } else if (error_start >= 1 && data[error_start-1] == '\r') { + valid_prefix = true; /* \rERROR */ + } else if (error_start >= 1 && data[error_start-1] == '\n') { + valid_prefix = true; /* \nERROR */ + } else if (error_start == 0) { + valid_prefix = true; /* ERROR在开头 */ + } + + if (valid_prefix) { + /* 检查ERROR后面的终止符 */ + uint32_t error_end = i + 5; /* ERROR结束位置 */ + + /* 情况1: \r\nERROR\r\n (标准格式) */ + if (error_end + 2 <= length && data[error_end] == '\r' && data[error_end+1] == '\n') { + *terminator_pos = error_end + 2; + return true; + } + /* 情况2: \r\nERROR\r (只有\r结尾) */ + if (error_end + 1 <= length && data[error_end] == '\r') { + *terminator_pos = error_end + 1; + return true; + } + /* 情况3: \r\nERROR\n (只有\n结尾) */ + if (error_end + 1 <= length && data[error_end] == '\n') { + *terminator_pos = error_end + 1; + return true; + } + /* 情况4: \r\nERROR (没有结尾换行符,在数据块末尾) */ + if (error_end >= length) { + *terminator_pos = error_end; + return true; + } + /* 情况5: \r\nERROR\r\r (双\r结尾) */ + if (error_end + 2 <= length && data[error_end] == '\r' && data[error_end+1] == '\r') { + *terminator_pos = error_end + 2; + return true; + } + } + } + + /* 方案2:检查是否是 CONNECT(用于AT+CERTADD等) */ + if (i + 7 <= length && + data[i] == 'C' && data[i+1] == 'O' && data[i+2] == 'N' && + data[i+3] == 'N' && data[i+4] == 'E' && data[i+5] == 'C' && data[i+6] == 'T') { + /* 检查CONNECT前面的分隔符 */ + bool valid_prefix = false; + uint32_t connect_start = i; + + if (connect_start >= 2 && data[connect_start-2] == '\r' && data[connect_start-1] == '\n') { + valid_prefix = true; /* \r\nCONNECT */ + } else if (connect_start >= 1 && (data[connect_start-1] == '\r' || data[connect_start-1] == '\n')) { + valid_prefix = true; + } else if (connect_start == 0) { + valid_prefix = true; + } + + if (valid_prefix) { + uint32_t connect_end = i + 7; /* CONNECT结束位置 */ + + /* 检查CONNECT后面的终止符 */ + if (connect_end + 2 <= length && data[connect_end] == '\r' && data[connect_end+1] == '\n') { + *terminator_pos = connect_end + 2; + return true; + } + if (connect_end + 1 <= length && (data[connect_end] == '\r' || data[connect_end] == '\n')) { + *terminator_pos = connect_end + 1; + return true; + } + if (connect_end >= length) { + *terminator_pos = connect_end; + return true; + } + } + } - // 移除尾部的 \r\n(如果存在) - if (*data_len >= 2 && (*data)[*data_len - 2] == '\r' && (*data)[*data_len - 1] == '\n') { - *data_len -= 2; + /* 方案2:检查带参数的响应(如+CERTCHECK: xxx) */ + /* 模式:\r\n+COMMAND: params\r\n */ + if (i + 1 < length && data[i] == '\r' && data[i+1] == '\n') { + /* 查找下一个\r\n作为结束 */ + for (uint32_t j = i + 2; j < length - 1; j++) { + if (data[j] == '\r' && data[j+1] == '\n') { + /* 检查中间是否有OK/ERROR/CONNECT */ + bool has_terminator = false; + for (uint32_t k = i + 2; k < j; k++) { + if (k + 2 <= j && data[k] == 'O' && data[k+1] == 'K') { + has_terminator = true; + break; + } + if (k + 5 <= j && strncmp((char*)&data[k], "ERROR", 5) == 0) { + has_terminator = true; + break; + } + if (k + 7 <= j && strncmp((char*)&data[k], "CONNECT", 7) == 0) { + has_terminator = true; + break; + } + } + if (!has_terminator) { + /* 没有标准终止符,可能是带参数的响应 */ + /* 检查是否以+开头(如+CERTCHECK:) */ + if (i + 2 < length && data[i+2] == '+') { + *terminator_pos = j + 2; + return true; + } + } + break; + } + } } } - PPP_DEBUG_DEBUG("Parsed URC: socket=%d, data_len=%u", *socket_id, (unsigned int)*data_len); - return true; + return false; } /** - * @brief 解析 +TCPCLOSE URC - * @param urc_data URC数据(以 "+TCPCLOSE: " 或 "+SSLTCPCLOSE: " 开头) - * @param urc_len URC数据长度 - * @param socket_id 输出:socket ID(如果格式是ERROR,socket_id设为-1) - * @return true=解析成功,false=失败 - * - * URC格式: - * +TCPCLOSE: - * +TCPCLOSE: ,Link Closed - * +TCPCLOSE: ERROR(错误格式,socket_id设为-1) - * 或:+SSLTCPCLOSE: + * @brief 处理AT命令响应(写入环形缓冲区) + * @param data 数据指针 + * @param length 数据长度 + * @param consumed 输出:已消费的字节数 */ -static bool n706a_parse_tcpclose_urc(const char *urc_data, size_t urc_len, int *socket_id) +static void n706a_stream_parser_process_at_response(const uint8_t *data, uint32_t length, uint32_t *consumed) { - if (!urc_data || !socket_id || urc_len < 12) { - return false; - } + *consumed = 0; - // 查找 ": " 位置 - const char *colon_space = strstr(urc_data, ": "); - if (!colon_space) { - return false; + if (length == 0) { + /* 没有数据,返回0是正常的 */ + return; } - const char *p = colon_space + 2; // 跳过 ": " - const char *end = urc_data + urc_len; + /* 检测响应结束 */ + uint32_t terminator_pos = 0; + bool response_complete = n706a_stream_parser_detect_at_response_end(data, length, &terminator_pos); - // 检查是否是ERROR格式 - if (p + 5 <= end && memcmp(p, "ERROR", 5) == 0 && - (p[5] == '\r' || p[5] == '\n' || p[5] == '\0' || p[5] == ' ')) { - *socket_id = -1; // ERROR格式,socket_id设为-1表示错误 - return true; - } + uint32_t to_write = response_complete ? terminator_pos : length; - // 解析 socket_id - *socket_id = 0; - while (p < end && *p >= '0' && *p <= '9') { - *socket_id = *socket_id * 10 + (*p - '0'); - p++; - } + /* 将数据写入环形缓冲区 */ + osal_mutex_lock(&g_at_response_mutex); - // 可能后面有 ",Link Closed" 或其他信息,但socket_id已经解析完成 - return (*socket_id >= 0); // 确保至少解析到一个有效的socket_id或ERROR -} - -/** - * @brief 解析 +TCPSEND URC - * @param urc_data URC数据(以 "+TCPSEND: " 或 "+SSLTCPEND: " 开头) - * @param urc_len URC数据长度 - * @param socket_id 输出:socket ID - * @param sent_bytes 输出:实际发送的字节数 - * @return true=解析成功,false=失败 - * - * URC格式:+TCPSEND: , - * 或:+SSLTCPEND: , - */ -static bool n706a_parse_tcpsend_urc(const char *urc_data, size_t urc_len, - int *socket_id, size_t *sent_bytes) -{ - if (!urc_data || !socket_id || !sent_bytes || urc_len < 15) { - return false; - } + uint32_t available_space = (g_at_response_head >= g_at_response_tail) ? + (N706A_AT_RESPONSE_RING_BUFFER_SIZE - (g_at_response_head - g_at_response_tail) - 1) : + (g_at_response_tail - g_at_response_head - 1); - // 查找 ": " 位置 - const char *colon_space = strstr(urc_data, ": "); - if (!colon_space) { - return false; + if (to_write > available_space) { + to_write = available_space; } - const char *p = colon_space + 2; // 跳过 ": " - const char *end = urc_data + urc_len; + /* 方案2修复:确保至少消费1个字节,避免死循环 */ + /* 如果缓冲区满了,至少消费1个字节,这样数据不会丢失,只是会覆盖最旧的数据 */ + static uint32_t overflow_warn_count = 0; // 静态计数器,减少警告频率 + static uint32_t last_overflow_warn_time = 0; // 上次警告时间 - // 解析 socket_id - *socket_id = 0; - while (p < end && *p >= '0' && *p <= '9') { - *socket_id = *socket_id * 10 + (*p - '0'); - p++; + if (to_write == 0 && length > 0) { + to_write = 1; + /* 减少警告频率:每100次或每10秒才打印一次警告 */ + uint32_t current_time = osal_jiffies_to_msecs(osal_get_jiffies()); + if (overflow_warn_count == 0 || (current_time - last_overflow_warn_time) > 10000 || + (last_overflow_warn_time > current_time)) { // 处理时间回绕 + PPP_DEBUG_WARN("[StreamParser] AT response buffer full, consuming 1 byte (will overwrite oldest)"); + overflow_warn_count = 0; + last_overflow_warn_time = current_time; + } + overflow_warn_count++; + } + + /* 批量写入,减少溢出检查次数 */ + uint32_t overflow_count = 0; + for (uint32_t i = 0; i < to_write; i++) { + g_at_response_ring_buffer[g_at_response_head] = data[i]; + g_at_response_head = (g_at_response_head + 1) % N706A_AT_RESPONSE_RING_BUFFER_SIZE; + + if (g_at_response_head == g_at_response_tail) { + g_at_response_tail = (g_at_response_tail + 1) % N706A_AT_RESPONSE_RING_BUFFER_SIZE; + overflow_count++; + } } - if (p >= end || *p != ',') { - PPP_DEBUG_ERROR("TCPSEND URC parse error: no comma after socket_id"); - return false; + /* 只在有溢出时打印警告,且减少频率 */ + if (overflow_count > 0) { + static uint32_t last_overflow_log_count = 0; + static uint32_t last_overflow_log_time = 0; + uint32_t current_time = osal_jiffies_to_msecs(osal_get_jiffies()); + + /* 每100次溢出或每10秒才打印一次警告 */ + if (overflow_count - last_overflow_log_count >= 100 || + (current_time - last_overflow_log_time) > 10000 || + (last_overflow_log_time > current_time)) { // 处理时间回绕 + PPP_DEBUG_WARN("[StreamParser] AT response buffer overflow, dropped %u bytes (total dropped: %u)", + overflow_count, overflow_count); + last_overflow_log_count = overflow_count; + last_overflow_log_time = current_time; + } } - p++; // 跳过逗号 - // 解析 sent_bytes - *sent_bytes = 0; - while (p < end && *p >= '0' && *p <= '9') { - *sent_bytes = *sent_bytes * 10 + (*p - '0'); - p++; + osal_mutex_unlock(&g_at_response_mutex); + + /* 通知等待线程 */ + if (to_write > 0) { + osal_sem_up(&g_at_response_sem); } - return true; + *consumed = to_write; + + /* 如果响应完成,重置状态 */ + if (response_complete) { + PPP_DEBUG_DEBUG("[StreamParser] AT response complete: %u bytes", to_write); + g_parser_ctx.at_response_count++; + n706a_stream_parser_reset(); + } } +/* ========== 重构后的解析器辅助函数 ========== */ + /** - * @brief 解析 +TCPSETUP URC - * @param urc_data URC数据(以 "+TCPSETUP: " 或 "+SSLTCPSETUP: " 开头) - * @param urc_len URC数据长度 - * @param socket_id 输出:socket ID - * @param success 输出:true表示OK,false表示FAIL - * @return true=解析成功,false=失败 - * - * URC格式:+TCPSETUP: ,OK 或 +TCPSETUP: ,FAIL - * 或:+SSLTCPSETUP: ,OK 或 +SSLTCPSETUP: ,FAIL - * 例如:+TCPSETUP: 0,OK 或 +TCPSETUP: 0,FAIL + * @brief 检查是否是AT响应开始 */ -static bool n706a_parse_tcpsetup_urc(const char *urc_data, size_t urc_len, - int *socket_id, bool *success) -{ - if (!urc_data || !socket_id || !success || urc_len < 15) { - return false; - } +static bool is_at_response_start(const uint8_t *data, uint32_t length, uint32_t pos) { + if (pos >= length) return false; - // 查找 ": " 位置(跳过 "+TCPSETUP" 或 "+SSLTCPSETUP") - const char *colon_space = strstr(urc_data, ": "); - if (!colon_space) { - return false; + // AT命令回显:AT+... + if (pos + 2 < length && data[pos] == 'A' && data[pos+1] == 'T' && data[pos+2] == '+') { + return true; } - const char *p = colon_space + 2; // 跳过 ": " - const char *end = urc_data + urc_len; - - // 跳过可能的空格 - while (p < end && (*p == ' ' || *p == '\t')) { - p++; + // 直接的响应:+CGATT: 1 或 OK 或 ERROR + if (data[pos] == '+') { + return true; // 可能是响应中的+参数 } - // 解析 socket_id - *socket_id = 0; - while (p < end && *p >= '0' && *p <= '9') { - *socket_id = *socket_id * 10 + (*p - '0'); - p++; + if (pos + 1 < length && data[pos] == 'O' && data[pos+1] == 'K') { + return true; } - if (p >= end || *p != ',') { - PPP_DEBUG_ERROR("TCPSETUP URC parse error: no comma after socket_id (found: 0x%02x)", - p < end ? (unsigned char)*p : 0); - return false; + if (pos + 4 < length && strncmp((char*)&data[pos], "ERROR", 5) == 0) { + return true; } - p++; // 跳过逗号 - // 跳过可能的空格 - while (p < end && (*p == ' ' || *p == '\t')) { - p++; - } + return false; +} + +/** + * @brief 检查是否是URC开始 + */ +static bool is_urc_start(const uint8_t *data, uint32_t length, uint32_t pos) { + if (pos >= length || data[pos] != '+') return false; - // 解析结果(OK或FAIL),忽略前后空白字符 - if (p < end) { - // 检查OK - if (p + 2 <= end && (p[0] == 'O' || p[0] == 'o') && (p[1] == 'K' || p[1] == 'k')) { - // 检查OK后面是否是结束符或空白字符 - if (p + 2 >= end || p[2] == '\r' || p[2] == '\n' || p[2] == '\0' || - p[2] == ' ' || p[2] == '\t') { - *success = true; - return true; - } - } - - // 检查FAIL - if (p + 4 <= end && - (p[0] == 'F' || p[0] == 'f') && - (p[1] == 'A' || p[1] == 'a') && - (p[2] == 'I' || p[2] == 'i') && - (p[3] == 'L' || p[3] == 'l')) { - // 检查FAIL后面是否是结束符或空白字符 - if (p + 4 >= end || p[4] == '\r' || p[4] == '\n' || p[4] == '\0' || - p[4] == ' ' || p[4] == '\t') { - *success = false; - return true; - } + // 检查URC模式 + for (int i = 0; urc_patterns[i].pattern != NULL; i++) { + uint32_t pattern_len = urc_patterns[i].pattern_len; + if (pos + pattern_len <= length && + strncmp((char*)&data[pos], urc_patterns[i].pattern, pattern_len) == 0) { + return true; } } - - // 如果都不匹配,输出详细的调试信息 - char debug_buf[64] = {0}; - size_t debug_len = (end - p < sizeof(debug_buf) - 1) ? (end - p) : (sizeof(debug_buf) - 1); - memcpy(debug_buf, p, debug_len); - PPP_DEBUG_ERROR("TCPSETUP URC parse error: invalid result (expected OK/FAIL), found: [%.*s] (len=%u)", - (int)debug_len, debug_buf, (unsigned int)(end - p)); return false; } /** - * @brief 将单个HEX字符转换为数值 - * @param c HEX字符('0'-'9', 'A'-'F', 'a'-'f') - * @param value 输出的数值 - * @return true=成功,false=失败 + * @brief 查找AT响应结束位置 */ -static bool n706a_hex_char_to_value(char c, uint8_t *value) -{ - if (c >= '0' && c <= '9') { - *value = c - '0'; - return true; - } else if (c >= 'A' && c <= 'F') { - *value = c - 'A' + 10; - return true; - } else if (c >= 'a' && c <= 'f') { - *value = c - 'a' + 10; - return true; +static bool find_at_response_end(const uint8_t *data, uint32_t length, + uint32_t start_pos, uint32_t *end_pos) { + // 查找 \r\nOK\r\n 或 \r\nERROR\r\n + for (uint32_t i = start_pos; i < length - 3; i++) { + if (data[i] == '\r' && data[i+1] == '\n') { + // 检查OK + if (i + 5 < length && data[i+2] == 'O' && data[i+3] == 'K' && + data[i+4] == '\r' && data[i+5] == '\n') { + *end_pos = i + 6; + return true; + } + // 检查ERROR + if (i + 8 < length && strncmp((char*)&data[i+2], "ERROR", 5) == 0 && + data[i+7] == '\r' && data[i+8] == '\n') { + *end_pos = i + 9; + return true; + } + } } return false; } /** - * @brief 将二进制数据转换为HEX字符串 - * @param binary_data 二进制数据 - * @param binary_len 二进制数据长度 - * @param hex_str 输出的HEX字符串缓冲区 - * @param hex_buf_size HEX缓冲区大小(至少需要binary_len*2+1) - * @return 转换后的HEX字符串长度(不含结束符),0表示失败 + * @brief 处理完整的AT响应 */ -static size_t n706a_binary_to_hex_string(const uint8_t *binary_data, size_t binary_len, - char *hex_str, size_t hex_buf_size) -{ - if (!binary_data || !hex_str || binary_len == 0) { - return 0; - } +static void process_complete_at_response(const uint8_t *data, uint32_t length) { + PPP_DEBUG_DEBUG("[StreamParser] 处理完整AT响应: %u字节", length); - // 需要binary_len * 2字节(每个字节转换为2个HEX字符),再加1个结束符 - if (hex_buf_size < binary_len * 2 + 1) { - PPP_DEBUG_ERROR("HEX encode: buffer too small (need %u, got %u)", - (unsigned int)(binary_len * 2 + 1), (unsigned int)hex_buf_size); - return 0; + // 写入环形缓冲区 + osal_mutex_lock(&g_at_response_mutex); + + uint32_t available_space = (g_at_response_head >= g_at_response_tail) ? + (N706A_AT_RESPONSE_RING_BUFFER_SIZE - (g_at_response_head - g_at_response_tail) - 1) : + (g_at_response_tail - g_at_response_head - 1); + + uint32_t to_write = (length <= available_space) ? length : available_space; + + for (uint32_t i = 0; i < to_write; i++) { + g_at_response_ring_buffer[g_at_response_head] = data[i]; + g_at_response_head = (g_at_response_head + 1) % N706A_AT_RESPONSE_RING_BUFFER_SIZE; + + if (g_at_response_head == g_at_response_tail) { + g_at_response_tail = (g_at_response_tail + 1) % N706A_AT_RESPONSE_RING_BUFFER_SIZE; + PPP_DEBUG_WARN("[StreamParser] AT response buffer overflow, dropping oldest byte"); + } } - static const char hex_chars[] = "0123456789ABCDEF"; + osal_mutex_unlock(&g_at_response_mutex); - for (size_t i = 0; i < binary_len; i++) { - hex_str[i * 2] = hex_chars[(binary_data[i] >> 4) & 0x0F]; - hex_str[i * 2 + 1] = hex_chars[binary_data[i] & 0x0F]; + // 通知等待线程 + if (to_write > 0) { + osal_sem_up(&g_at_response_sem); } - hex_str[binary_len * 2] = '\0'; - return binary_len * 2; + g_parser_ctx.at_response_count++; } /** - * @brief 将HEX字符串转换为二进制数据 - * @param hex_str HEX字符串(如 "313233") - * @param hex_len HEX字符串长度(字节数,必须是偶数) - * @param binary_buf 输出的二进制缓冲区 - * @param binary_buf_size 二进制缓冲区大小 - * @return 转换后的二进制数据长度(字节数),0表示失败 + * @brief 流式解析器主处理函数(原始设计 + 方案2修复) + * @param data 数据指针 + * @param length 数据长度 */ -static size_t n706a_hex_string_to_binary(const char *hex_str, size_t hex_len, - uint8_t *binary_buf, size_t binary_buf_size) +static void n706a_stream_parser_process_data(const uint8_t *data, uint32_t length) { - if (!hex_str || !binary_buf || hex_len == 0 || hex_len % 2 != 0) { - return 0; + if (!data || length == 0) { + return; } - size_t binary_len = hex_len / 2; - if (binary_len > binary_buf_size) { - PPP_DEBUG_ERROR("HEX decode: binary_len=%u exceeds buffer size=%u", - (unsigned int)binary_len, (unsigned int)binary_buf_size); - return 0; - } + PPP_DEBUG_DEBUG("[StreamParser] 处理数据: %u字节, 当前状态=%d", length, g_parser_ctx.state); - for (size_t i = 0; i < binary_len; i++) { - uint8_t high_nibble, low_nibble; + uint32_t pos = 0; + + while (pos < length) { + uint32_t consumed = 0; - if (!n706a_hex_char_to_value(hex_str[i * 2], &high_nibble) || - !n706a_hex_char_to_value(hex_str[i * 2 + 1], &low_nibble)) { - PPP_DEBUG_ERROR("Invalid HEX character at position %u", (unsigned int)(i * 2)); - return 0; + switch (g_parser_ctx.state) { + case AT_PARSER_STATE_IDLE: { + /* 空闲状态:扫描数据,寻找响应开始 */ + uint8_t byte = data[pos]; + urc_type_t urc_type; + const char *urc_pattern; + uint32_t urc_pattern_len; + + /* 尝试匹配URC标识符 */ + if (n706a_stream_parser_match_urc_identifier(byte, &urc_type, &urc_pattern, &urc_pattern_len)) { + /* 匹配到URC标识符 */ + g_parser_ctx.urc_type = urc_type; + g_parser_ctx.urc_pattern = urc_pattern; + g_parser_ctx.urc_pattern_len = urc_pattern_len; + g_parser_ctx.state = AT_PARSER_STATE_URC_HEADER; + g_parser_ctx.response_start_pos = pos; + g_parser_ctx.state_transition_pending = true; // 方案2:标记状态转换 + /* 不消费字符,让URC_HEADER状态处理 */ + } else if (byte == 'A' && pos + 1 < length && data[pos + 1] == 'T') { + /* 检测到AT响应开始(AT命令回显) */ + /* 改进:检查AT后面是否有+或空格,避免误判二进制数据 */ + bool is_valid_at = false; + if (pos + 2 < length) { + uint8_t next_byte = data[pos + 2]; + /* AT命令格式:AT+... 或 AT ... 或 AT\r\n */ + if (next_byte == '+' || next_byte == ' ' || next_byte == '\r' || next_byte == '\n') { + is_valid_at = true; + } + } else if (pos + 2 == length) { + /* 数据块末尾,可能是AT,等待更多数据 */ + is_valid_at = true; + } + + if (is_valid_at) { + PPP_DEBUG_INFO("[StreamParser] 检测到AT响应开始"); + g_parser_ctx.state = AT_PARSER_STATE_AT_RESPONSE; + g_parser_ctx.response_start_pos = pos; + g_parser_ctx.state_transition_pending = true; // 方案2:标记状态转换 + /* 不消费字符,让AT_RESPONSE状态处理 */ + } else { + /* 不是有效的AT命令,可能是二进制数据,跳过 */ + PPP_DEBUG_DEBUG("[StreamParser] 跳过疑似AT的二进制数据: 0x%02X 0x%02X 0x%02X", + byte, data[pos + 1], (pos + 2 < length) ? data[pos + 2] : 0); + consumed = 1; + } + } else if (byte == '+' || byte == 'O' || byte == 'E') { + /* 可能是AT响应(+参数、OK、ERROR) */ + /* 检查是否是URC(优先) */ + bool is_urc = false; + for (int i = 0; urc_patterns[i].pattern != NULL; i++) { + if (pos + urc_patterns[i].pattern_len <= length && + strncmp((char*)&data[pos], urc_patterns[i].pattern, urc_patterns[i].pattern_len) == 0) { + /* 是URC */ + g_parser_ctx.urc_type = urc_patterns[i].type; + g_parser_ctx.urc_pattern = urc_patterns[i].pattern; + g_parser_ctx.urc_pattern_len = urc_patterns[i].pattern_len; + g_parser_ctx.state = AT_PARSER_STATE_URC_HEADER; + g_parser_ctx.response_start_pos = pos; + g_parser_ctx.state_transition_pending = true; // 方案2:标记状态转换 + is_urc = true; + break; + } + } + if (!is_urc) { + /* 不是URC,可能是AT响应 */ + g_parser_ctx.state = AT_PARSER_STATE_AT_RESPONSE; + g_parser_ctx.response_start_pos = pos; + g_parser_ctx.state_transition_pending = true; // 方案2:标记状态转换 + } + /* 不消费字符,让相应状态处理 */ + } else { + /* 跳过无关字符 */ + if (byte != '\r' && byte != '\n' && byte != '\0') { + PPP_DEBUG_DEBUG("[StreamParser] 跳过未识别字符: 0x%02X", byte); + } + consumed = 1; + } + break; + } + + case AT_PARSER_STATE_AT_RESPONSE: { + /* AT响应处理 */ + /* 方案2修复:状态转换标记应该在进入新状态后立即清除,而不是等到处理完成 */ + bool was_transition_pending = g_parser_ctx.state_transition_pending; + g_parser_ctx.state_transition_pending = false; // 立即清除标记,状态转换已完成 + + n706a_stream_parser_process_at_response(data + pos, length - pos, &consumed); + + /* 如果处理函数返回consumed=0且这是状态转换后的第一次调用,这是正常的 */ + if (consumed == 0 && was_transition_pending) { + PPP_DEBUG_DEBUG("[StreamParser] AT_RESPONSE状态转换后首次调用,consumed=0(可能数据不完整)"); + } + break; + } + + case AT_PARSER_STATE_URC_HEADER: { + /* URC头部处理 */ + /* 方案2修复:状态转换标记应该在进入新状态后立即清除,而不是等到处理完成 */ + bool was_transition_pending = g_parser_ctx.state_transition_pending; + g_parser_ctx.state_transition_pending = false; // 立即清除标记,状态转换已完成 + + uint32_t consumed_before = consumed; + n706a_stream_parser_process_urc_header(data + pos, length - pos, &consumed); + + /* 添加调试信息,帮助定位pos跳转问题 */ + if (consumed > 100) { + PPP_DEBUG_WARN("[StreamParser] URC_HEADER consumed异常大: %u (pos=%u, length=%u, urc_header_len=%u)", + consumed, pos, length, g_parser_ctx.urc_header_len); + } + + /* 如果处理函数返回consumed=0且这是状态转换后的第一次调用,这是正常的 */ + /* 但如果连续多次都是consumed=0,说明有问题 */ + if (consumed == 0 && was_transition_pending) { + /* 状态转换后的第一次调用返回consumed=0,这是正常的(比如数据不完整) */ + /* 但需要确保下次循环时能继续处理 */ + PPP_DEBUG_DEBUG("[StreamParser] URC_HEADER状态转换后首次调用,consumed=0(可能数据不完整)"); + } + break; + } + + case AT_PARSER_STATE_URC_DATA: { + /* URC数据处理 */ + /* 方案2修复:状态转换标记应该在进入新状态后立即清除,而不是等到处理完成 */ + bool was_transition_pending = g_parser_ctx.state_transition_pending; + g_parser_ctx.state_transition_pending = false; // 立即清除标记,状态转换已完成 + + n706a_stream_parser_process_urc_data(data + pos, length - pos, &consumed); + + /* 如果处理函数返回consumed=0且这是状态转换后的第一次调用,这是正常的 */ + if (consumed == 0 && was_transition_pending) { + PPP_DEBUG_DEBUG("[StreamParser] URC_DATA状态转换后首次调用,consumed=0(可能数据不完整)"); + } + break; + } + + default: + /* 未知状态,重置并跳过字符 */ + PPP_DEBUG_ERROR("[StreamParser] 未知状态: %d,重置解析器", g_parser_ctx.state); + n706a_stream_parser_reset(); + consumed = 1; + break; } - binary_buf[i] = (high_nibble << 4) | low_nibble; + /* 方案2:检测consumed=0的情况 */ + /* 正常情况下,处理函数应该总是消费至少一些数据(如果length > 0) */ + /* 如果consumed=0,说明处理函数认为需要等待更多数据,这是正常的 */ + /* 但如果连续多次都是consumed=0且状态没变,可能是死循环 */ + /* 优化:使用全局静态变量,避免在栈上分配 */ + static uint32_t last_consumed_zero_pos = UINT32_MAX; + static at_parser_state_t last_consumed_zero_state = AT_PARSER_STATE_IDLE; + static uint32_t consumed_zero_count = 0; + + if (consumed == 0) { + if (pos == last_consumed_zero_pos && g_parser_ctx.state == last_consumed_zero_state) { + /* 同一位置同一状态连续出现consumed=0 */ + consumed_zero_count++; + if (consumed_zero_count >= 10) { + /* 连续10次都是consumed=0且状态没变,这是死循环 */ + /* 重置状态,让解析器重新开始扫描 */ + PPP_DEBUG_ERROR("[StreamParser] 死循环检测:pos=%u, state=%d, 连续%u次consumed=0,重置状态", + pos, g_parser_ctx.state, consumed_zero_count); + consumed_zero_count = 0; /* 重置计数 */ + /* 重置状态,让解析器重新开始扫描 */ + n706a_stream_parser_reset(); + /* 不修改consumed,让IDLE状态重新处理当前字符 */ + /* 这样不会丢失数据,只是重新开始解析 */ + } else { + PPP_DEBUG_DEBUG("[StreamParser] consumed=0,pos=%u, state=%d (第%u次,等待更多数据)", + pos, g_parser_ctx.state, consumed_zero_count); + } + } else { + /* 位置或状态发生变化,重置计数 */ + last_consumed_zero_pos = pos; + last_consumed_zero_state = g_parser_ctx.state; + consumed_zero_count = 1; + PPP_DEBUG_DEBUG("[StreamParser] consumed=0,pos=%u, state=%d (可能是数据不完整,等待更多数据)", + pos, g_parser_ctx.state); + } + } else { + /* consumed > 0,重置计数 */ + last_consumed_zero_pos = UINT32_MAX; + last_consumed_zero_state = AT_PARSER_STATE_IDLE; + consumed_zero_count = 0; + } + + pos += consumed; + g_parser_ctx.total_bytes_processed += consumed; } - return binary_len; -} - -/** - * @brief 注册TCP URC接收回调 - * @param callback 回调函数指针,NULL表示取消注册 - */ -void n706a_at_register_tcp_urc_callback(n706a_tcp_urc_callback_t callback) -{ - g_tcp_urc_callback = callback; - PPP_DEBUG_INFO("TCP URC callback %s", callback ? "registered" : "unregistered"); -} - -/** - * @brief 注册+TCPSETUP URC回调 - * @param callback 回调函数指针,NULL表示取消注册 - */ -void n706a_at_register_tcpsetup_urc_callback(n706a_tcpsetup_urc_callback_t callback) -{ - g_tcpsetup_urc_callback = callback; - PPP_DEBUG_INFO("+TCPSETUP URC callback %s", callback ? "registered" : "unregistered"); -} - -/** - * @brief 注册+TCPCLOSE URC回调 - */ -void n706a_at_register_tcpclose_urc_callback(n706a_tcpclose_urc_callback_t callback) -{ - g_tcpclose_urc_callback = callback; - PPP_DEBUG_INFO("+TCPCLOSE URC callback %s", callback ? "registered" : "unregistered"); -} - -/** - * @brief 注册+TCPSEND URC回调 - */ -void n706a_at_register_tcpsend_urc_callback(n706a_tcpsend_urc_callback_t callback) -{ - g_tcpsend_urc_callback = callback; - PPP_DEBUG_INFO("+TCPSEND URC callback %s", callback ? "registered" : "unregistered"); -} - -/** - * @brief 注册网络状态URC回调 - */ -void n706a_at_register_network_status_urc_callback(n706a_network_status_urc_callback_t callback) -{ - g_network_status_urc_callback = callback; - PPP_DEBUG_INFO("Network status URC callback %s", callback ? "registered" : "unregistered"); + PPP_DEBUG_DEBUG("[StreamParser] 数据处理完成,最终状态=%d", g_parser_ctx.state); } \ No newline at end of file diff --git a/code/sdk/zhihui/n706a_net/src/n706a_http_client.c b/code/sdk/zhihui/n706a_net/src/n706a_http_client.c index 2e2cc4b01..9cbc182c9 100644 --- a/code/sdk/zhihui/n706a_net/src/n706a_http_client.c +++ b/code/sdk/zhihui/n706a_net/src/n706a_http_client.c @@ -1,42 +1,170 @@ /** * @file n706a_http_client.c - * @brief N706A HTTP/HTTPS客户端实现(基于TCP) + * @brief N706A HTTP/HTTPS客户端实现(基于HTTP专用AT指令) * - * 基于TCP手动构造HTTP协议,支持: - * - HTTP (普通TCP) - * - HTTPS (SSL TCP) - * - 6个并发连接 - * - 所有标准HTTP方法 + * 使用N706A模块的HTTP专用AT指令,与Python脚本实现逻辑一致: + * - HTTP: AT+HTTPPARA, AT+HTTPSETUP, AT+HTTPACTION + * - HTTPS: AT+HTTPSPARA, AT+HTTPSSETUP, AT+HTTPSACTION + * - 接收: +HTTPRECV: / +HTTPSRECV: URC */ +#define _GNU_SOURCE /* 用于 strcasestr */ #include "n706a_http_client.h" -#include "n706a_tcp_client.h" +#include "n706a_at_handler.h" +#include "ppp_serial.h" #include "ppp_debug.h" +#include "lock/osal_mutex.h" +#include "semaphore/osal_semaphore.h" +#include "soc_osal.h" #include #include #include +/* ========== CA证书链(通用HTTPS证书) ========== */ +/* 证书文件来源:leiniao_python_test/certs/ca_chain.pem */ +/* 包含完整的证书链:服务器证书 -> 中间CA -> æ ¹CA */ +#if 0 +static const char N706A_ROOT_CA_CERT_NAME[] = "ca_chain.pem"; +static const char N706A_ROOT_CA_CERT_DATA[] = +"-----BEGIN CERTIFICATE-----\n" +"MIIF4jCCA8qgAwIBAgIRANVuJGyU7WOrsUbvwZa2T7AwDQYJKoZIhvcNAQEMBQAw\n" +"gYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5MRQwEgYDVQQHEwtK\n" +"ZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMS4wLAYD\n" +"VQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTIw\n" +"MDEwODAwMDAwMFoXDTMwMDEwNzIzNTk1OVowXDELMAkGA1UEBhMCQ04xGjAYBgNV\n" +"BAoTEVdvVHJ1cyBDQSBMaW1pdGVkMTEwLwYDVQQDDChXb1RydXMgRFYgU2VydmVy\n" +"IENBICBbUnVuIGJ5IHRoZSBJc3N1ZXJdMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\n" +"MIIBCgKCAQEA7IE0rmVTVdRdNOzK1jsR/VppyukZ/XbQgakJHOhg6XGDsiHe/l5B\n" +"3PxyXw18jEdN+7YxP0qsGz+HlQbsQh6XlwIyjpz/2gFMiqa7y1v+dHOgj6xNOF5a\n" +"oaPm/Qhb0N+JYQidgaC+1Zp6W+YeC736rzCMr9vL1Usa3QLzRoQEo0DzbG4sPeP1\n" +"US0Ia/i8o6szArH+DAcvrzCZ2kkpTScQ9QfOsvkMBP1W2otICdKUyZHaBc+ztTAd\n" +"ovSlOR+GPf29dYfGQkZAp0tffIRw/na3WB86WGZPpNFfo2QxxsHYoL3oSWKfSWTY\n" +"FgW22J8eA03TFHYowm/NqYuJ7GW553HppQIDAQABo4IBcDCCAWwwHwYDVR0jBBgw\n" +"FoAUU3m/WqorSs9UgOHYm8Cd8rIDZsswHQYDVR0OBBYEFJmbLfaL8KPbidSe++V0\n" +"L2jSkE/kMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1Ud\n" +"JQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAiBgNVHSAEGzAZMA0GCysGAQQBsjEB\n" +"AgIWMAgGBmeBDAECATBQBgNVHR8ESTBHMEWgQ6BBhj9odHRwOi8vY3JsLnVzZXJ0\n" +"cnVzdC5jb20vVVNFUlRydXN0UlNBQ2VydGlmaWNhdGlvbkF1dGhvcml0eS5jcmww\n" +"cQYIKwYBBQUHAQEEZTBjMDoGCCsGAQUFBzAChi5odHRwOi8vY3J0LnVzZXJ0cnVz\n" +"dC5jb20vVVNFUlRydXN0UlNBQUFBQ0EuY3J0MCUGCCsGAQUFBzABhhlodHRwOi8v\n" +"b2NzcC51c2VydHJ1c3QuY29tMA0GCSqGSIb3DQEBDAUAA4ICAQB5t8v3uYzHa4EL\n" +"0rOb9g/YAmptUbILcBMKk1x188ucsGVPaG1DG9bpVamxbmCtFA1MlrA7iUC8SGop\n" +"KBnuWFsNKiC7jCbRoahT1/FSwFsSuDlDmOjr1MqDXE+or08UkXsJB57XxXxdVOPl\n" +"DcZHII4qHi1XKK4iurMqb+kbdpAWadyfidRRCGPopYCVYLLYhRJgpFGtfr6Gk8N0\n" +"j81jq/7QbN0dRSDzMNdadKTc7c3+i9fIrXj79lV5Wvva+OL7nh8MxQhG1Ekek7Rv\n" +"en++jSZvaEhCrMsSedFTA/aIy7oJg85tfglF2ybK61HsobjYzdDNICKJlIm4chlA\n" +"XIDDqw2mw0Kz2snrkp9dpvMBqahF/Uy1kHzPcrq1/w5OqZWAuDKxZ68PuZ/ME2hI\n" +"YbIDG9dWT6Y7eqtjQ2TmAQbOqdAG2LeikPMl2DMrPEa4lcKJzsFbHfHAW3hVgPSQ\n" +"hRfS4TtbNnxijbsp8GguMHxP2R7dpAAYybwfZdXP7WYAnwEr1mzIf0Y3J0m7GDyX\n" +"JhaflN3G2wIm2HzRd39NvnDRmFEraqui/YYO9ym0pwq1d0S+bGG6876QCto0u3Cg\n" +"ItFh3Za2ZeIY+g5mWrejSaDs9LT7eu44iCyebfgekdMRqFeCuGAsJdsun3LOHHJo\n" +"tCVPRjyFg9NDeJeMa4Z8QuXAXLd9cw==\n" +"-----END CERTIFICATE-----\n" +"-----BEGIN CERTIFICATE-----\n" +"MIIFgTCCBGmgAwIBAgIQOXJEOvkit1HX02wQ3TE1lTANBgkqhkiG9w0BAQwFADB7\n" +"MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD\n" +"VQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UE\n" +"AwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTE5MDMxMjAwMDAwMFoXDTI4\n" +"MTIzMTIzNTk1OVowgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5\n" +"MRQwEgYDVQQHEwtKZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBO\n" +"ZXR3b3JrMS4wLAYDVQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0\n" +"aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAgBJlFzYOw9sI\n" +"s9CsVw127c0n00ytUINh4qogTQktZAnczomfzD2p7PbPwdzx07HWezcoEStH2jnG\n" +"vDoZtF+mvX2do2NCtnbyqTsrkfjib9DsFiCQCT7i6HTJGLSR1GJk23+jBvGIGGqQ\n" +"Ijy8/hPwhxR79uQfjtTkUcYRZ0YIUcuGFFQ/vDP+fmyc/xadGL1RjjWmp2bIcmfb\n" +"IWax1Jt4A8BQOujM8Ny8nkz+rwWWNR9XWrf/zvk9tyy29lTdyOcSOk2uTIq3XJq0\n" +"tyA9yn8iNK5+O2hmAUTnAU5GU5szYPeUvlM3kHND8zLDU+/bqv50TmnHa4xgk97E\n" +"xwzf4TKuzJM7UXiVZ4vuPVb+DNBpDxsP8yUmazNt925H+nND5X4OpWaxKXwyhGNV\n" +"icQNwZNUMBkTrNN9N6frXTpsNVzbQdcS2qlJC9/YgIoJk2KOtWbPJYjNhLixP6Q5\n" +"D9kCnusSTJV882sFqV4Wg8y4Z+LoE53MW4LTTLPtW//e5XOsIzstAL81VXQJSdhJ\n" +"WBp/kjbmUZIO8yZ9HE0XvMnsQybQv0FfQKlERPSZ51eHnlAfV1SoPv10Yy+xUGUJ\n" +"5lhCLkMaTLTwJUdZ+gQek9QmRkpQgbLevni3/GcV4clXhB4PY9bpYrrWX1Uu6lzG\n" +"KAgEJTm4Diup8kyXHAc/DVL17e8vgg8CAwEAAaOB8jCB7zAfBgNVHSMEGDAWgBSg\n" +"EQojPpbxB+zirynvgqV/0DCktDAdBgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rID\n" +"ZsswDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0gBAowCDAG\n" +"BgRVHSAAMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29t\n" +"L0FBQUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggr\n" +"BgEFBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBDAUA\n" +"A4IBAQAYh1HcdCE9nIrgJ7cz0C7M7PDmy14R3iJvm3WOnnL+5Nb+qh+cli3vA0p+\n" +"rvSNb3I8QzvAP+u431yqqcau8vzY7qN7Q/aGNnwU4M309z/+3ri0ivCRlv79Q2R+\n" +"/czSAaF9ffgZGclCKxO/WIu6pKJmBHaIkU4MiRTOok3JMrO66BQavHHxW/BBC5gA\n" +"CiIDEOUMsfnNkjcZ7Tvx5Dq2+UUTJnWvu6rvP3t3O9LEApE9GQDTF1w52z97GA1F\n" +"zZOFli9d31kWTz9RvdVFGD/tSo7oBmF0Ixa1DVBzJ0RHfxBdiSprhTEUxOipakyA\n" +"vGp4z7h/jnZymQyd/teRCBaho1+V\n" +"-----END CERTIFICATE-----\n" +"-----BEGIN CERTIFICATE-----\n" +"MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb\n" +"MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow\n" +"GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj\n" +"YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL\n" +"MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE\n" +"BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM\n" +"GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP\n" +"ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua\n" +"BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe\n" +"3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4\n" +"YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR\n" +"rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm\n" +"ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU\n" +"oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF\n" +"MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v\n" +"QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t\n" +"b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF\n" +"AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q\n" +"GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz\n" +"Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2\n" +"G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi\n" +"l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3\n" +"smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg==\n" +"-----END CERTIFICATE-----\n"; +#else +static const char N706A_ROOT_CA_CERT_NAME[] = "root_ca.pem"; +static const char N706A_ROOT_CA_CERT_DATA[] = +"-----BEGIN CERTIFICATE-----\n" +"MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb\n" +"MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow\n" +"GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj\n" +"YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL\n" +"MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE\n" +"BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM\n" +"GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP\n" +"ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua\n" +"BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe\n" +"3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4\n" +"YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR\n" +"rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm\n" +"ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU\n" +"oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF\n" +"MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v\n" +"QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t\n" +"b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF\n" +"AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q\n" +"GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz\n" +"Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2\n" +"G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi\n" +"l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3\n" +"smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg==\n" +"-----END CERTIFICATE-----\n"; + +#endif + + + +#define N706A_ROOT_CA_CERT_LEN (sizeof(N706A_ROOT_CA_CERT_DATA) - 1) + +/* 证书初始化标志(开机执行一次后设置为true,后续不再执行) */ +static bool g_cert_initialized = false; + /* Base64编码表(用于Basic Auth) */ static const char base64_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; -/* ========== 私有函数声明 ========== */ - -static int http_build_request(n706a_http_client_t *client, - const char *method, - const char *path, - const char *body, - size_t body_len, - const char *content_type, - char *request_buf, - size_t buf_size); +/* ========== 全局HTTP客户端实例(HTTP专用AT指令只支持单连接) ========== */ +static n706a_http_client_t *g_http_client = NULL; +static osal_mutex g_http_client_mutex; +static bool g_http_client_mutex_initialized = false; -static n706a_httpc_result_t http_parse_response(const char *response, - size_t response_len, - n706a_http_client_t *client, - n706a_http_client_data_t *data); - -static void http_base64_encode(const char *input, size_t len, char *output); +/* ========== 私有函数声明 ========== */ static int http_parse_url(const char *url, char *protocol, @@ -44,50 +172,494 @@ static int http_parse_url(const char *url, uint16_t *port, char *path); +static n706a_httpc_result_t http_receive_response(n706a_http_client_t *client, + n706a_http_client_data_t *client_data, + uint32_t timeout_ms); + +/** + * @brief 流式接收HTTP响应数据(公开接口,用于在发送请求后继续接收后续数据块) + */ +n706a_httpc_result_t n706a_http_client_recv_response_stream(n706a_http_client_t *client, + n706a_http_client_data_t *client_data, + uint32_t timeout_ms) +{ + if (!client || !client_data) { + return N706A_HTTP_EARG; + } + + if (!client->stream_mode_enabled) { + PPP_DEBUG_ERROR("[HTTP] Stream mode not enabled"); + return N706A_HTTP_EARG; + } + + if (!client->response_mutex || !client->response_sem) { + PPP_DEBUG_ERROR("[HTTP] Response buffer not initialized"); + return N706A_HTTP_EARG; + } + + osal_mutex *mutex = (osal_mutex *)client->response_mutex; + osal_semaphore *sem = (osal_semaphore *)client->response_sem; + + /* 等待后续的URC数据到达 */ + int ret = osal_sem_down_timeout(sem, timeout_ms); + + if (ret != OSAL_SUCCESS) { + /* 超时:检查是否已经接收完成 */ + osal_mutex_lock(mutex); + bool has_more = false; + if (client->stream_content_length > 0) { + has_more = (client->stream_body_received < (size_t)client->stream_content_length); + } else if (client->stream_is_chunked) { + has_more = true; /* Chunked编码,假设还有更多数据 */ + } + client_data->isMore = has_more; + osal_mutex_unlock(mutex); + + if (has_more) { + return N706A_HTTP_EAGAIN; /* 还有更多数据,但超时了 */ + } else { + return N706A_HTTP_SUCCESS; /* 接收完成 */ + } + } + + /* URC数据到达,更新状态 */ + osal_mutex_lock(mutex); + bool has_more = false; + if (client->stream_content_length > 0) { + has_more = (client->stream_body_received < (size_t)client->stream_content_length); + } else if (client->stream_is_chunked) { + has_more = true; /* Chunked编码,假设还有更多数据 */ + } + client_data->isMore = has_more; + client_data->responseContentLen = client->stream_content_length; + osal_mutex_unlock(mutex); + + return has_more ? N706A_HTTP_EAGAIN : N706A_HTTP_SUCCESS; +} + /* ========== 公共函数实现 ========== */ /** * @brief 初始化HTTP客户端 * @param client HTTP客户端结构体指针 - * @param socket_id socket ID (0-5),如果传入-1则自动分配空闲的socket_id * @return N706A_HTTP_SUCCESS 成功,其他值失败 + * @note HTTP专用AT指令不占用TCP socket,无需socket_id */ -n706a_httpc_result_t n706a_http_client_init(n706a_http_client_t *client, int socket_id) +n706a_httpc_result_t n706a_http_client_init(n706a_http_client_t *client) { if (!client) { return N706A_HTTP_EARG; } - memset(client, 0, sizeof(n706a_http_client_t)); - - /* 如果socket_id为-1,自动分配空闲的socket_id(避免与WebSocket冲突) */ - if (socket_id == -1) { - socket_id = n706a_tcp_find_free_socket_id(); - if (socket_id < 0 || socket_id > 5) { - PPP_DEBUG_ERROR("[HTTP] No free socket ID available for HTTP client"); - return N706A_HTTP_ENOBUFS; + /* 初始化互斥锁(首次调用时) */ + if (!g_http_client_mutex_initialized) { + if (osal_mutex_init(&g_http_client_mutex) != OSAL_SUCCESS) { + PPP_DEBUG_ERROR("[HTTP] Failed to init mutex"); + return N706A_HTTP_ERROR; + } + g_http_client_mutex_initialized = true; + } + + osal_mutex_lock(&g_http_client_mutex); + + /* HTTP专用AT指令只支持单连接,检查是否已有客户端在使用 */ + if (g_http_client != NULL && g_http_client != client) { + /* 检查旧的客户端是否仍然有效(已连接或正在使用) */ + /* 注意:如果 g_http_client 指向的客户端已经被 free(),访问其字段是未定义行为 + * 但是,由于我们使用互斥锁保护,且客户端通常不会被外部 free(), + * 这里假设 g_http_client 指向的客户端仍然有效 + * + * 但是,为了安全起见,我们首先检查客户端是否已被完全清理(所有关键字段为NULL) + * 如果已被完全清理,说明客户端已被关闭但 g_http_client 指针未清空,直接清空指针 + */ + bool is_active = false; + n706a_http_client_t *old_client = g_http_client; /* 保存指针,避免在检查过程中被修改 */ + + /* 首先检查客户端是否已被完全清理(所有关键字段为NULL) + * 如果已被完全清理,说明客户端已被关闭但 g_http_client 指针未清空,直接清空指针 + * 注意:这里访问 old_client 的字段是安全的,因为: + * 1. 如果 old_client 已被 free(),但 g_http_client 仍指向它,这是 bug,但我们可以通过检查字段来检测 + * 2. 如果 old_client 未被 free(),访问字段是安全的 + * 3. 如果 old_client 已被部分清理(字段为NULL),访问字段也是安全的(只是读取NULL值) + */ + if (old_client->response_mutex == NULL && old_client->response_sem == NULL && + old_client->response_buffer == NULL) { + /* 客户端已被完全清理,但 g_http_client 指针未清空,直接清空指针 */ + PPP_DEBUG_WARN("[HTTP] Found stale HTTP client pointer (all resources NULL), clearing..."); + g_http_client = NULL; + /* 注意:此时 old_client 可能指向已释放的内存,不能再访问其字段 */ + } else if (old_client->connected) { + /* 客户端仍然连接,说明正在使用中 */ + is_active = true; + PPP_DEBUG_WARN("[HTTP] Another HTTP client is already active (connected=true)"); + } else if (old_client->response_buffer != NULL) { + /* 检查 response_buffer 是否真的在使用中(通过检查 response_mutex 是否存在) */ + if (old_client->response_mutex != NULL) { + /* response_buffer 和 response_mutex 都存在,说明客户端正在使用中 */ + is_active = true; + PPP_DEBUG_WARN("[HTTP] Another HTTP client is already active (response_buffer=%p, response_mutex=%p)", + old_client->response_buffer, old_client->response_mutex); + } else { + /* response_buffer 存在但 response_mutex 不存在,说明客户端已被部分清理,可以安全清理 */ + PPP_DEBUG_WARN("[HTTP] Found stale HTTP client (response_buffer=%p but response_mutex=NULL), cleaning up...", + old_client->response_buffer); + } + } + + if (is_active) { + osal_mutex_unlock(&g_http_client_mutex); + return N706A_HTTP_ECONN; + } + + /* 旧的客户端已经关闭但未清理,清理它 */ + /* 注意:此时 g_http_client 可能已被设置为 NULL(第281行),所以需要检查 */ + if (g_http_client != NULL && g_http_client == old_client) { + PPP_DEBUG_WARN("[HTTP] Found stale HTTP client (connected=%d, response_buffer=%p), cleaning up...", + old_client->connected, old_client->response_buffer); + + /* 清理旧的客户端资源 */ + if (old_client->response_buffer) { + free(old_client->response_buffer); + old_client->response_buffer = NULL; + } + if (old_client->response_mutex) { + osal_mutex *mutex = (osal_mutex *)old_client->response_mutex; + osal_mutex_destroy(mutex); + free(mutex); + old_client->response_mutex = NULL; + } + if (old_client->response_sem) { + osal_semaphore *sem = (osal_semaphore *)old_client->response_sem; + osal_sem_destroy(sem); + free(sem); + old_client->response_sem = NULL; + } + /* 清理其他资源 */ + if (old_client->header) { + free(old_client->header); + old_client->header = NULL; + } + if (old_client->auth_user) { + free(old_client->auth_user); + old_client->auth_user = NULL; + } + if (old_client->auth_password) { + free(old_client->auth_password); + old_client->auth_password = NULL; + } + /* host 和 url_path 是数组,不需要释放,只需清零 */ + memset(old_client->host, 0, sizeof(old_client->host)); + memset(old_client->url_path, 0, sizeof(old_client->url_path)); + + /* 清除全局引用(必须在清理资源之后) */ + g_http_client = NULL; + PPP_DEBUG_INFO("[HTTP] Stale HTTP client cleaned up"); } - PPP_DEBUG_INFO("[HTTP] Auto-allocated socket ID: %d", socket_id); - } else if (socket_id < 0 || socket_id > 5) { - return N706A_HTTP_EARG; } - client->socket_id = socket_id; + memset(client, 0, sizeof(n706a_http_client_t)); + client->is_https = false; client->remote_port = 0; client->response_code = 0; client->connected = false; client->keepalive = false; - client->timeout_ms = 10000; // 默认10秒 + client->timeout_ms = 60000; // 默认60秒(HTTP响应可能需要更长时间) + + /* 初始化响应缓冲区(默认8KB) */ + client->response_buffer_size = 8192; + client->response_buffer = (uint8_t *)malloc(client->response_buffer_size); + if (!client->response_buffer) { + osal_mutex_unlock(&g_http_client_mutex); + return N706A_HTTP_ENOMEM; + } + + client->response_received = 0; + client->response_complete = false; + + /* 初始化流式接收相关字段 */ + client->stream_callback = NULL; + client->stream_user_data = NULL; + client->stream_mode_enabled = false; + client->stream_body_received = 0; + client->stream_content_length = 0; + client->stream_header_parsed = false; + client->stream_connection_closed = false; + client->stream_is_chunked = false; + + /* 初始化响应缓冲区互斥锁和信号量 */ + osal_mutex *mutex = (osal_mutex *)malloc(sizeof(osal_mutex)); + osal_semaphore *sem = (osal_semaphore *)malloc(sizeof(osal_semaphore)); + if (!mutex || !sem) { + free(client->response_buffer); + if (mutex) free(mutex); + if (sem) free(sem); + osal_mutex_unlock(&g_http_client_mutex); + return N706A_HTTP_ENOMEM; + } + + /* 清零结构体,确保mutex->mutex和sem->sem字段为0(LiteOS要求) */ + memset(mutex, 0, sizeof(osal_mutex)); + memset(sem, 0, sizeof(osal_semaphore)); + + if (osal_mutex_init(mutex) != OSAL_SUCCESS || + osal_sem_init(sem, 0) != OSAL_SUCCESS) { + free(client->response_buffer); + free(mutex); + free(sem); + osal_mutex_unlock(&g_http_client_mutex); + return N706A_HTTP_ERROR; + } + + client->response_mutex = mutex; + client->response_sem = sem; + + /* 注册为全局客户端 */ + g_http_client = client; + + osal_mutex_unlock(&g_http_client_mutex); + + PPP_DEBUG_INFO("[HTTP] HTTP client initialized (using HTTP AT commands)"); + + return N706A_HTTP_SUCCESS; +} + +/** + * @brief 设置HTTP/HTTPS请求URL(自动判断协议类型) + * @param client HTTP客户端结构指针 + * @param url 完整URL(如"http://example.com/path"或"https://example.com/path") + * @param keepalive 是否启用长连接,0=关闭(默认),1=启用 + * @return N706A_HTTP_SUCCESS成功,其他失败 + */ +n706a_httpc_result_t n706a_http_client_set_url(n706a_http_client_t *client, + const char *url, + int keepalive) +{ + if (!client || !url) { + return N706A_HTTP_EARG; + } + + /* 动态分配大缓冲区,避免栈溢出 */ + char *protocol = (char *)malloc(8); + char *host = (char *)malloc(256); + char *path = (char *)malloc(512); + char *url_param = (char *)malloc(768); + char *at_cmd = (char *)malloc(1024); + n706a_at_response_t *response = (n706a_at_response_t *)malloc(sizeof(n706a_at_response_t)); + + if (!protocol || !host || !path || !url_param || !at_cmd || !response) { + PPP_DEBUG_ERROR("[HTTP] Failed to allocate memory for URL parsing"); + if (protocol) free(protocol); + if (host) free(host); + if (path) free(path); + if (url_param) free(url_param); + if (at_cmd) free(at_cmd); + if (response) free(response); + return N706A_HTTP_ENOMEM; + } + + memset(response, 0, sizeof(n706a_at_response_t)); + + uint16_t port; + + /* 解析URL */ + if (http_parse_url(url, protocol, host, &port, path) != 0) { + PPP_DEBUG_ERROR("[HTTP] Failed to parse URL: %s", url); + free(protocol); + free(host); + free(path); + free(url_param); + free(at_cmd); + free(response); + return N706A_HTTP_EPARSE; + } + + /* 判断协议类型 */ + client->is_https = (strcmp(protocol, "https") == 0); + strncpy(client->host, host, sizeof(client->host) - 1); + client->remote_port = port; + strncpy(client->url_path, path, sizeof(client->url_path) - 1); + client->keepalive = (keepalive != 0); + + PPP_DEBUG_INFO("[HTTP] Setting URL: %s (protocol=%s, host=%s, port=%d, path=%s)", + url, protocol, host, port, path); + + /* 构建URL参数(host+path) */ + snprintf(url_param, 768, "%s%s", host, path); + + n706a_at_error_t ret; + + if (client->is_https) { + /* HTTPS: 使用AT+HTTPSPARA */ + snprintf(at_cmd, 1024, "AT+HTTPSPARA=url,\"%s\"", url_param); + ret = n706a_at_send_command(at_cmd, "OK", 5000, response); + if (ret != N706A_AT_OK) { + PPP_DEBUG_ERROR("[HTTP] Failed to set HTTPS URL: %s", response->response); + free(protocol); + free(host); + free(path); + free(url_param); + free(at_cmd); + free(response); + return N706A_HTTP_EPARSE; + } + + snprintf(at_cmd, 1024, "AT+HTTPSPARA=port,%d", port); + ret = n706a_at_send_command(at_cmd, "OK", 5000, response); + if (ret != N706A_AT_OK) { + PPP_DEBUG_ERROR("[HTTP] Failed to set HTTPS port: %s", response->response); + free(protocol); + free(host); + free(path); + free(url_param); + free(at_cmd); + free(response); + return N706A_HTTP_EPARSE; + } + + if (keepalive >= 0 && keepalive <= 1) { + snprintf(at_cmd, 1024, "AT+HTTPSPARA=keepalive,%d", keepalive); + ret = n706a_at_send_command(at_cmd, "OK", 5000, response); + if (ret != N706A_AT_OK) { + PPP_DEBUG_WARN("[HTTP] Failed to set HTTPS keepalive: %s", response->response); + } + } + } else { + /* HTTP: 使用AT+HTTPPARA */ + snprintf(at_cmd, 1024, "AT+HTTPPARA=url,\"%s\"", url_param); + ret = n706a_at_send_command(at_cmd, "OK", 5000, response); + if (ret != N706A_AT_OK) { + PPP_DEBUG_ERROR("[HTTP] Failed to set HTTP URL: %s", response->response); + free(protocol); + free(host); + free(path); + free(url_param); + free(at_cmd); + free(response); + return N706A_HTTP_EPARSE; + } + + snprintf(at_cmd, 1024, "AT+HTTPPARA=port,%d", port); + ret = n706a_at_send_command(at_cmd, "OK", 5000, response); + if (ret != N706A_AT_OK) { + PPP_DEBUG_ERROR("[HTTP] Failed to set HTTP port: %s", response->response); + free(protocol); + free(host); + free(path); + free(url_param); + free(at_cmd); + free(response); + return N706A_HTTP_EPARSE; + } + + if (keepalive >= 0 && keepalive <= 1) { + snprintf(at_cmd, 1024, "AT+HTTPPARA=keepalive,%d", keepalive); + ret = n706a_at_send_command(at_cmd, "OK", 5000, response); + if (ret != N706A_AT_OK) { + PPP_DEBUG_WARN("[HTTP] Failed to set HTTP keepalive: %s", response->response); + } + } + } + + /* 释放所有动态分配的内存 */ + free(protocol); + free(host); + free(path); + free(url_param); + free(at_cmd); + free(response); + + PPP_DEBUG_INFO("[HTTP] URL set successfully"); + return N706A_HTTP_SUCCESS; +} + +/** + * @brief 建立HTTP/HTTPS连接(根据URL自动选择) + * @param client HTTP客户端结构指针 + * @return N706A_HTTP_SUCCESS成功,其他失败 + */ +n706a_httpc_result_t n706a_http_client_setup_connection(n706a_http_client_t *client) +{ + if (!client) { + return N706A_HTTP_EARG; + } + + /* 如果是 HTTPS,自动配置 SSL 证书和参数 */ + if (client->is_https) { + /* 自动初始化证书(如果尚未初始化) */ + n706a_httpc_result_t cert_ret = n706a_http_client_init_certificate(); + if (cert_ret != N706A_HTTP_SUCCESS) { + PPP_DEBUG_ERROR("[HTTP] Failed to initialize certificate for HTTPS"); + return cert_ret; + } + + /* 自动配置 HTTPS SSL 参数(默认:TLS1.2,单向认证) */ + n706a_httpc_result_t https_ret = n706a_http_client_config_https( + 3, /* SSL版本:3=TLS1.2 */ + 1, /* 认证模式:1=单向认证 */ + NULL, /* CA证书文件名(NULL表示使用默认证书) */ + NULL, /* 客户端证书(单向认证为NULL) */ + NULL /* 客户端密钥(单向认证为NULL) */ + ); + if (https_ret != N706A_HTTP_SUCCESS) { + PPP_DEBUG_ERROR("[HTTP] Failed to configure HTTPS SSL parameters"); + return https_ret; + } + + PPP_DEBUG_INFO("[HTTP] HTTPS SSL configured automatically (TLS1.2, one-way auth)"); + + /* 等待配置生效(参考Python脚本:配置后可能需要短暂延迟) */ + osal_msleep(100); + } + + /* 动态分配response结构体,避免栈溢出 */ + n706a_at_response_t *response = (n706a_at_response_t *)malloc(sizeof(n706a_at_response_t)); + if (!response) { + PPP_DEBUG_ERROR("[HTTP] Failed to allocate memory for response"); + return N706A_HTTP_ENOMEM; + } + memset(response, 0, sizeof(n706a_at_response_t)); + + n706a_at_error_t ret; - /* 初始化TCP socket */ - n706a_tcp_socket_create(&client->tcp_sock, socket_id); + if (client->is_https) { + ret = n706a_at_send_command("AT+HTTPSSETUP", "OK", 15000, response); + if (ret != N706A_AT_OK || strstr(response->response, "ERROR") != NULL) { + PPP_DEBUG_ERROR("[HTTP] HTTPS connection setup failed: %s", response->response); + free(response); + return N706A_HTTP_ECONN; + } + + /* 检查响应中是否包含 +HTTPSSETUP: AUTHFAIL URC */ + if (strstr(response->response, "+HTTPSSETUP:") != NULL && + strstr(response->response, "AUTHFAIL") != NULL) { + PPP_DEBUG_ERROR("[HTTP] HTTPS certificate authentication failed (AUTHFAIL)"); + PPP_DEBUG_ERROR("[HTTP] Possible causes:"); + PPP_DEBUG_ERROR("[HTTP] 1. Certificate is not the correct root CA certificate"); + PPP_DEBUG_ERROR("[HTTP] 2. Server certificate is not signed by this root CA"); + PPP_DEBUG_ERROR("[HTTP] 3. Certificate chain is incomplete"); + PPP_DEBUG_ERROR("[HTTP] 4. Certificate format is incorrect"); + free(response); + return N706A_HTTP_ECONN; + } + } else { + ret = n706a_at_send_command("AT+HTTPSETUP", "OK", 15000, response); + if (ret != N706A_AT_OK) { + PPP_DEBUG_ERROR("[HTTP] HTTP connection setup failed: %s", response->response); + free(response); + return N706A_HTTP_ECONN; + } + } + + free(response); + client->connected = true; + PPP_DEBUG_INFO("[HTTP] Connection setup successfully (%s)", client->is_https ? "HTTPS" : "HTTP"); return N706A_HTTP_SUCCESS; } /** - * @brief 连接到HTTP/HTTPS服务器 + * @brief 连接到HTTP/HTTPS服务器(保留兼容性,内部调用set_url和setup_connection) */ n706a_httpc_result_t n706a_http_client_connect(n706a_http_client_t *client, const char *host, @@ -103,23 +675,25 @@ n706a_httpc_result_t n706a_http_client_connect(n706a_http_client_t *client, client->remote_port = port; client->is_https = use_ssl; - /* 建立TCP连接 */ - n706a_tcp_result_t ret = n706a_tcp_connect(&client->tcp_sock, - host, - port, - use_ssl, - 30000); // SSL握手需要更长时间 + /* 构建URL并调用set_url和setup_connection */ + char *url = (char *)malloc(512); + if (!url) { + return N706A_HTTP_ENOMEM; + } + + const char *scheme = use_ssl ? "https://" : "http://"; + snprintf(url, 512, "%s%s:%d/", scheme, host, port); - if (ret == N706A_TCP_SUCCESS) { - client->connected = true; - return N706A_HTTP_SUCCESS; - } else if (ret == N706A_TCP_ETIMEDOUT) { - return N706A_HTTP_ETIMEOUT; - } else { - return N706A_HTTP_ECONN; + n706a_httpc_result_t ret = n706a_http_client_set_url(client, url, 0); + free(url); + if (ret != N706A_HTTP_SUCCESS) { + return ret; } + + return n706a_http_client_setup_connection(client); } + /** * @brief 发送HTTP GET请求 */ @@ -127,7 +701,7 @@ n706a_httpc_result_t n706a_http_client_get(n706a_http_client_t *client, const char *path, n706a_http_client_data_t *client_data) { - if (!client || !path || !client_data) { + if (!client || !client_data) { return N706A_HTTP_EARG; } @@ -135,59 +709,151 @@ n706a_httpc_result_t n706a_http_client_get(n706a_http_client_t *client, return N706A_HTTP_ECONN; } - /* 动态分配请求缓冲区 */ - char *request_buf = (char *)malloc(2048); + /* 如果path不为NULL且与当前URL path不同,更新URL */ + if (path != NULL && strcmp(client->url_path, path) != 0) { + char *url = (char *)malloc(768); + if (!url) { + return N706A_HTTP_ENOMEM; + } + const char *scheme = client->is_https ? "https://" : "http://"; + snprintf(url, 768, "%s%s:%d%s", scheme, client->host, client->remote_port, path); + n706a_httpc_result_t ret = n706a_http_client_set_url(client, url, client->keepalive ? 1 : 0); + free(url); + if (ret != N706A_HTTP_SUCCESS) { + return ret; + } + } + + n706a_at_response_t *response = (n706a_at_response_t *)malloc(sizeof(n706a_at_response_t)); + if (!response) { + return N706A_HTTP_ENOMEM; + } + memset(response, 0, sizeof(n706a_at_response_t)); + n706a_at_error_t ret; + + /* 如果有自定义headers,使用OPEN_MODE (mode=99) 发送自定义HTTP报文 */ + if (client->header && strlen(client->header) > 0) { + /* 构建HTTP请求报文 */ + char *request_buf = (char *)malloc(4096); if (!request_buf) { + free(response); return N706A_HTTP_ENOMEM; } - int request_len = http_build_request(client, "GET", path, - NULL, 0, NULL, - request_buf, 2048); + /* 使用path参数(如果为NULL则使用client->url_path) */ + const char *request_path = (path != NULL) ? path : client->url_path; - if (request_len < 0) { + int request_len = snprintf(request_buf, 4096, + "GET %s HTTP/1.1\r\n" + "Host: %s\r\n" + "%s" + "Connection: close\r\n" + "\r\n", + request_path, + client->host, + client->header); + + if (request_len < 0 || request_len >= 4096) { free(request_buf); + free(response); return N706A_HTTP_ENOBUFS; } - /* 发送请求 */ - size_t sent; - n706a_tcp_result_t ret = n706a_tcp_send(&client->tcp_sock, - (uint8_t*)request_buf, - request_len, - &sent); - - free(request_buf); + /* 检查请求长度是否超过AT指令限制 */ + if (request_len > N706A_HTTP_MAX_REQUEST_LEN) { + free(request_buf); + free(response); + PPP_DEBUG_ERROR("[HTTP] GET request length (%d bytes) exceeds AT+HTTPACTION max (%d bytes)", + request_len, N706A_HTTP_MAX_REQUEST_LEN); + return N706A_HTTP_ENOBUFS; + } + + /* 调试:打印构建的HTTP请求(完整内容,使用ERROR级别确保输出) */ + PPP_DEBUG_ERROR("[HTTP] ========== Built GET request (%d bytes) ==========", request_len); + if (request_len <= 1024) { + /* 如果请求小于1KB,完整打印 */ + PPP_DEBUG_ERROR("[HTTP] Request content:\n%.*s", request_len, request_buf); + } else { + /* 如果请求大于1KB,打印前512字节和后128字节 */ + PPP_DEBUG_ERROR("[HTTP] Request content (first 512 bytes):\n%.512s", request_buf); + PPP_DEBUG_ERROR("[HTTP] Request content (last 128 bytes):\n%.128s", request_buf + request_len - 128); + } + PPP_DEBUG_ERROR("[HTTP] ========== End of GET request =========="); - if (ret != N706A_TCP_SUCCESS || sent != (size_t)request_len) { - return N706A_HTTP_ESEND; - } + /* 发送AT+HTTPSACTION=99, 或 AT+HTTPACTION=99, */ + char at_cmd[64]; + if (client->is_https) { + snprintf(at_cmd, sizeof(at_cmd), "AT+HTTPSACTION=99,%d", request_len); + } else { + snprintf(at_cmd, sizeof(at_cmd), "AT+HTTPACTION=99,%d", request_len); + } + + ret = n706a_at_send_command(at_cmd, NULL, 5000, response); + if (ret != N706A_AT_OK || !response->success || strstr((const char *)response->response, "ERROR") != NULL) { + PPP_DEBUG_ERROR("[HTTP] Failed to send GET request (mode=99): ret=%d, success=%d, response=%s", + ret, response ? response->success : 0, response ? response->response : "NULL"); + free(request_buf); + free(response); + return N706A_HTTP_ESEND; + } - /* 动态分配响应缓冲区 */ - uint8_t *response_buf = (uint8_t *)malloc(8192); - if (!response_buf) { - return N706A_HTTP_ENOMEM; + /* 检查响应中是否包含 '>' 提示符 */ + if (strstr((const char *)response->response, ">") == NULL) { + free(request_buf); + free(response); + PPP_DEBUG_ERROR("[HTTP] Response does not contain '>' prompt: %s", response->response); + return N706A_HTTP_ESEND; + } + + free(response); + + /* 延迟75ms(根据AT命令手册要求) */ + osal_msleep(75); + + /* 发送HTTP请求报文(ASCII文本,不是HEX编码) + * 注意:AT+HTTPSACTION=99/AT+HTTPACTION=99期望接收ASCII文本,不是HEX编码! + * Python脚本使用 serial_conn.write(http_request.encode('utf-8')) 直接发送原始字节 + * 而 n706a_at_send_raw_data 会将数据转换为HEX字符串,这是错误的! + * 因此这里使用 ppp_serial_write 发送原始数据(它会自动处理wakeup) + */ + PPP_DEBUG_INFO("[HTTP] Sending HTTP request as ASCII text (%d bytes)...", request_len); + ppp_error_t write_result = ppp_serial_write((const uint8_t *)request_buf, request_len); + free(request_buf); + if (write_result != PPP_ERR_OK) { + PPP_DEBUG_ERROR("[HTTP] Failed to send GET request data via ppp_serial_write: error=%d", write_result); + return N706A_HTTP_ESEND; + } + PPP_DEBUG_INFO("[HTTP] HTTP request sent successfully (%d bytes as ASCII text)", request_len); + } else { + /* 没有自定义headers,使用标准GET模式 (mode=0) */ + if (client->is_https) { + ret = n706a_at_send_command("AT+HTTPSACTION=0", "OK", 5000, response); + } else { + ret = n706a_at_send_command("AT+HTTPACTION=0", "OK", 5000, response); + } + + if (ret != N706A_AT_OK) { + PPP_DEBUG_ERROR("[HTTP] Failed to send GET request: %s", response->response); + free(response); + return N706A_HTTP_ESEND; + } } - size_t received; - ret = n706a_tcp_recv(&client->tcp_sock, - response_buf, - 8192 - 1, - &received, - client->timeout_ms); + free(response); + PPP_DEBUG_INFO("[HTTP] GET request sent, waiting for response..."); - if (ret != N706A_TCP_SUCCESS) { - free(response_buf); - return N706A_HTTP_ERECV; + /* 重置流式接收状态(如果启用了流式模式) */ + if (client->stream_mode_enabled) { + client->stream_body_received = 0; + client->stream_content_length = 0; + client->stream_header_parsed = false; + client->stream_is_chunked = false; + client->stream_connection_closed = false; + client->response_received = 0; /* 清空缓冲区,准备接收新的响应头 */ } - response_buf[received] = '\0'; - - /* 解析响应 */ - n706a_httpc_result_t result = http_parse_response((char*)response_buf, received, client, client_data); - free(response_buf); - - return result; + /* 等待并接收响应(通过URC) */ + return http_receive_response(client, client_data, client->timeout_ms); } /** @@ -208,62 +874,132 @@ n706a_httpc_result_t n706a_http_client_post(n706a_http_client_t *client, return N706A_HTTP_ECONN; } - /* 动态分配请求缓冲区 */ + /* 如果path与当前URL path不同,更新URL */ + if (strcmp(client->url_path, path) != 0) { + char *url = (char *)malloc(768); + if (!url) { + return N706A_HTTP_ENOMEM; + } + const char *scheme = client->is_https ? "https://" : "http://"; + snprintf(url, 768, "%s%s:%d%s", scheme, client->host, client->remote_port, path); + n706a_httpc_result_t ret = n706a_http_client_set_url(client, url, client->keepalive ? 1 : 0); + free(url); + if (ret != N706A_HTTP_SUCCESS) { + return ret; + } + } + + /* POST请求:使用OPEN_MODE (mode=99) 发送自定义HTTP报文 */ + /* 构建HTTP请求报文 */ char *request_buf = (char *)malloc(4096); if (!request_buf) { return N706A_HTTP_ENOMEM; } - int request_len = http_build_request(client, "POST", path, - body, body_len, content_type, - request_buf, 4096); + int request_len = snprintf(request_buf, 4096, + "POST %s HTTP/1.1\r\n" + "Host: %s\r\n" + "Content-Type: %s\r\n" + "Content-Length: %zu\r\n" + "Connection: close\r\n" + "\r\n", + path, + client->host, + content_type ? content_type : "application/octet-stream", + body_len); + + if (request_len < 0 || request_len >= 4096) { + free(request_buf); + return N706A_HTTP_ENOBUFS; + } + + /* 追加请求体 */ + if (body && body_len > 0) { + if (request_len + body_len < 4096) { + memcpy(request_buf + request_len, body, body_len); + request_len += body_len; + } else { + free(request_buf); + return N706A_HTTP_ENOBUFS; + } + } - if (request_len < 0) { + /* 检查请求长度是否超过AT指令限制 */ + if (request_len > N706A_HTTP_MAX_REQUEST_LEN) { free(request_buf); + PPP_DEBUG_ERROR("[HTTP] POST request length (%d bytes) exceeds AT+HTTPACTION max (%d bytes)", + request_len, N706A_HTTP_MAX_REQUEST_LEN); return N706A_HTTP_ENOBUFS; } - /* 发送请求 */ - size_t sent; - n706a_tcp_result_t ret = n706a_tcp_send(&client->tcp_sock, - (uint8_t*)request_buf, - request_len, - &sent); + /* 发送AT+HTTPACTION=99, 或 AT+HTTPSACTION=99, */ + char at_cmd[64]; + if (client->is_https) { + snprintf(at_cmd, sizeof(at_cmd), "AT+HTTPSACTION=99,%d", request_len); + } else { + snprintf(at_cmd, sizeof(at_cmd), "AT+HTTPACTION=99,%d", request_len); + } - free(request_buf); + n706a_at_response_t *response = (n706a_at_response_t *)malloc(sizeof(n706a_at_response_t)); + if (!response) { + free(request_buf); + return N706A_HTTP_ENOMEM; + } + memset(response, 0, sizeof(n706a_at_response_t)); - if (ret != N706A_TCP_SUCCESS) { + n706a_at_error_t ret = n706a_at_send_command(at_cmd, NULL, 5000, response); + if (ret != N706A_AT_OK) { + free(request_buf); + free(response); + PPP_DEBUG_ERROR("[HTTP] Failed to send POST request command: %s", response->response); return N706A_HTTP_ESEND; } - /* 动态分配响应缓冲区 */ - uint8_t *response_buf = (uint8_t *)malloc(8192); - if (!response_buf) { - return N706A_HTTP_ENOMEM; + /* 检查响应中是否包含 '>' 提示符 */ + if (strstr((const char *)response->response, ">") == NULL) { + free(request_buf); + free(response); + PPP_DEBUG_ERROR("[HTTP] Response does not contain '>' prompt: %s", response->response); + return N706A_HTTP_ESEND; } - size_t received; - ret = n706a_tcp_recv(&client->tcp_sock, - response_buf, - 8192 - 1, - &received, - client->timeout_ms); + free(response); - if (ret != N706A_TCP_SUCCESS) { - free(response_buf); - return N706A_HTTP_ERECV; + /* 延迟75ms(根据AT命令手册要求) */ + osal_msleep(75); + + /* 发送HTTP请求报文(ASCII文本,不是HEX编码) + * 注意:AT+HTTPSACTION=99/AT+HTTPACTION=99期望接收ASCII文本,不是HEX编码! + * 因此这里使用 ppp_serial_write 发送原始数据(它会自动处理wakeup) + */ + PPP_DEBUG_INFO("[HTTP] Sending POST request as ASCII text (%d bytes)...", request_len); + ppp_error_t write_result = ppp_serial_write((const uint8_t *)request_buf, request_len); + free(request_buf); + if (write_result != PPP_ERR_OK) { + PPP_DEBUG_ERROR("[HTTP] Failed to send POST request data via ppp_serial_write: error=%d", write_result); + return N706A_HTTP_ESEND; } + PPP_DEBUG_INFO("[HTTP] POST request sent successfully (%d bytes as ASCII text)", request_len); - response_buf[received] = '\0'; + PPP_DEBUG_INFO("[HTTP] POST request sent, waiting for response..."); - n706a_httpc_result_t result = http_parse_response((char*)response_buf, received, client, client_data); - free(response_buf); + /* 重置流式接收状态(如果启用了流式模式) */ + if (client->stream_mode_enabled) { + client->stream_body_received = 0; + client->stream_content_length = 0; + client->stream_header_parsed = false; + client->stream_is_chunked = false; + client->stream_connection_closed = false; + client->response_received = 0; /* 清空缓冲区,准备接收新的响应头 */ + } - return result; + /* 等待并接收响应(通过URC) */ + return http_receive_response(client, client_data, client->timeout_ms); } /** * @brief 发送HTTP PUT请求 + * @note 使用HTTP专用AT指令时,PUT请求需要使用OPEN_MODE (mode=99) 发送自定义HTTP报文 */ n706a_httpc_result_t n706a_http_client_put(n706a_http_client_t *client, const char *path, @@ -280,45 +1016,122 @@ n706a_httpc_result_t n706a_http_client_put(n706a_http_client_t *client, return N706A_HTTP_ECONN; } - /* 动态分配请求缓冲区 */ + /* PUT请求与POST类似,使用OPEN_MODE (mode=99) 发送自定义HTTP报文 */ + /* 如果path与当前URL path不同,更新URL */ + if (strcmp(client->url_path, path) != 0) { + char *url = (char *)malloc(768); + if (!url) { + return N706A_HTTP_ENOMEM; + } + const char *scheme = client->is_https ? "https://" : "http://"; + snprintf(url, 768, "%s%s:%d%s", scheme, client->host, client->remote_port, path); + n706a_httpc_result_t ret = n706a_http_client_set_url(client, url, client->keepalive ? 1 : 0); + free(url); + if (ret != N706A_HTTP_SUCCESS) { + return ret; + } + } + + /* 构建HTTP请求报文 */ char *request_buf = (char *)malloc(4096); if (!request_buf) { return N706A_HTTP_ENOMEM; } - int request_len = http_build_request(client, "PUT", path, - body, body_len, content_type, - request_buf, 4096); + int request_len = snprintf(request_buf, 4096, + "PUT %s HTTP/1.1\r\n" + "Host: %s\r\n" + "Content-Type: %s\r\n" + "Content-Length: %zu\r\n" + "Connection: close\r\n" + "\r\n", + path, + client->host, + content_type ? content_type : "application/octet-stream", + body_len); - if (request_len < 0) { + if (request_len < 0 || request_len >= 4096) { free(request_buf); return N706A_HTTP_ENOBUFS; } - size_t sent; - n706a_tcp_send(&client->tcp_sock, (uint8_t*)request_buf, request_len, &sent); - free(request_buf); + /* 追加请求体 */ + if (body && body_len > 0) { + if (request_len + body_len < 4096) { + memcpy(request_buf + request_len, body, body_len); + request_len += body_len; + } else { + free(request_buf); + return N706A_HTTP_ENOBUFS; + } + } - /* 动态分配响应缓冲区 */ - uint8_t *response_buf = (uint8_t *)malloc(8192); - if (!response_buf) { + /* 检查请求长度是否超过AT指令限制 */ + if (request_len > N706A_HTTP_MAX_REQUEST_LEN) { + free(request_buf); + PPP_DEBUG_ERROR("[HTTP] PUT request length (%d bytes) exceeds AT+HTTPACTION max (%d bytes)", + request_len, N706A_HTTP_MAX_REQUEST_LEN); + return N706A_HTTP_ENOBUFS; + } + + /* 发送AT+HTTPACTION=99, 或 AT+HTTPSACTION=99, */ + char at_cmd[64]; + if (client->is_https) { + snprintf(at_cmd, sizeof(at_cmd), "AT+HTTPSACTION=99,%d", request_len); + } else { + snprintf(at_cmd, sizeof(at_cmd), "AT+HTTPACTION=99,%d", request_len); + } + + n706a_at_response_t *response = (n706a_at_response_t *)malloc(sizeof(n706a_at_response_t)); + if (!response) { + free(request_buf); return N706A_HTTP_ENOMEM; } + memset(response, 0, sizeof(n706a_at_response_t)); + + n706a_at_error_t ret = n706a_at_send_command(at_cmd, NULL, 5000, response); + if (ret != N706A_AT_OK) { + free(request_buf); + free(response); + PPP_DEBUG_ERROR("[HTTP] Failed to send PUT request command: %s", response->response); + return N706A_HTTP_ESEND; + } + + /* 检查响应中是否包含 '>' 提示符 */ + if (strstr((const char *)response->response, ">") == NULL) { + free(request_buf); + free(response); + PPP_DEBUG_ERROR("[HTTP] Response does not contain '>' prompt: %s", response->response); + return N706A_HTTP_ESEND; + } - size_t received; - n706a_tcp_recv(&client->tcp_sock, response_buf, 8192 - 1, - &received, client->timeout_ms); + free(response); - response_buf[received] = '\0'; + /* 延迟75ms(根据AT命令手册要求) */ + osal_msleep(75); - n706a_httpc_result_t result = http_parse_response((char*)response_buf, received, client, client_data); - free(response_buf); + /* 发送HTTP请求报文(ASCII文本,不是HEX编码) + * 注意:AT+HTTPSACTION=99/AT+HTTPACTION=99期望接收ASCII文本,不是HEX编码! + * 因此这里使用 ppp_serial_write 发送原始数据(它会自动处理wakeup) + */ + PPP_DEBUG_INFO("[HTTP] Sending PUT request as ASCII text (%d bytes)...", request_len); + ppp_error_t write_result = ppp_serial_write((const uint8_t *)request_buf, request_len); + free(request_buf); + if (write_result != PPP_ERR_OK) { + PPP_DEBUG_ERROR("[HTTP] Failed to send PUT request data via ppp_serial_write: error=%d", write_result); + return N706A_HTTP_ESEND; + } + PPP_DEBUG_INFO("[HTTP] PUT request sent successfully (%d bytes as ASCII text)", request_len); + + PPP_DEBUG_INFO("[HTTP] PUT request sent, waiting for response..."); - return result; + /* 等待并接收响应(通过URC) */ + return http_receive_response(client, client_data, client->timeout_ms); } /** * @brief 发送HTTP DELETE请求 + * @note 使用HTTP专用AT指令时,DELETE请求需要使用OPEN_MODE (mode=99) 发送自定义HTTP报文 */ n706a_httpc_result_t n706a_http_client_delete(n706a_http_client_t *client, const char *path, @@ -332,45 +1145,107 @@ n706a_httpc_result_t n706a_http_client_delete(n706a_http_client_t *client, return N706A_HTTP_ECONN; } - /* 动态分配请求缓冲区 */ + /* DELETE请求使用OPEN_MODE (mode=99) 发送自定义HTTP报文 */ + /* 如果path与当前URL path不同,更新URL */ + if (strcmp(client->url_path, path) != 0) { + char *url = (char *)malloc(768); + if (!url) { + return N706A_HTTP_ENOMEM; + } + const char *scheme = client->is_https ? "https://" : "http://"; + snprintf(url, 768, "%s%s:%d%s", scheme, client->host, client->remote_port, path); + n706a_httpc_result_t ret = n706a_http_client_set_url(client, url, client->keepalive ? 1 : 0); + free(url); + if (ret != N706A_HTTP_SUCCESS) { + return ret; + } + } + + /* 构建HTTP请求报文 */ char *request_buf = (char *)malloc(2048); if (!request_buf) { return N706A_HTTP_ENOMEM; } - int request_len = http_build_request(client, "DELETE", path, - NULL, 0, NULL, - request_buf, 2048); + int request_len = snprintf(request_buf, 2048, + "DELETE %s HTTP/1.1\r\n" + "Host: %s\r\n" + "Connection: close\r\n" + "\r\n", + path, + client->host); - if (request_len < 0) { + if (request_len < 0 || request_len >= 2048) { free(request_buf); return N706A_HTTP_ENOBUFS; } - size_t sent; - n706a_tcp_send(&client->tcp_sock, (uint8_t*)request_buf, request_len, &sent); - free(request_buf); + /* 检查请求长度是否超过AT指令限制 */ + if (request_len > N706A_HTTP_MAX_REQUEST_LEN) { + free(request_buf); + PPP_DEBUG_ERROR("[HTTP] DELETE request length (%d bytes) exceeds AT+HTTPACTION max (%d bytes)", + request_len, N706A_HTTP_MAX_REQUEST_LEN); + return N706A_HTTP_ENOBUFS; + } - /* 动态分配响应缓冲区 */ - uint8_t *response_buf = (uint8_t *)malloc(8192); - if (!response_buf) { + /* 发送AT+HTTPACTION=99, 或 AT+HTTPSACTION=99, */ + char at_cmd[64]; + if (client->is_https) { + snprintf(at_cmd, sizeof(at_cmd), "AT+HTTPSACTION=99,%d", request_len); + } else { + snprintf(at_cmd, sizeof(at_cmd), "AT+HTTPACTION=99,%d", request_len); + } + + n706a_at_response_t *response = (n706a_at_response_t *)malloc(sizeof(n706a_at_response_t)); + if (!response) { + free(request_buf); return N706A_HTTP_ENOMEM; } + memset(response, 0, sizeof(n706a_at_response_t)); - size_t received; - n706a_tcp_recv(&client->tcp_sock, response_buf, 8192 - 1, - &received, client->timeout_ms); + n706a_at_error_t ret = n706a_at_send_command(at_cmd, NULL, 5000, response); + if (ret != N706A_AT_OK) { + free(request_buf); + free(response); + PPP_DEBUG_ERROR("[HTTP] Failed to send DELETE request command: %s", response->response); + return N706A_HTTP_ESEND; + } + + /* 检查响应中是否包含 '>' 提示符 */ + if (strstr((const char *)response->response, ">") == NULL) { + free(request_buf); + free(response); + PPP_DEBUG_ERROR("[HTTP] Response does not contain '>' prompt: %s", response->response); + return N706A_HTTP_ESEND; + } - response_buf[received] = '\0'; + free(response); - n706a_httpc_result_t result = http_parse_response((char*)response_buf, received, client, client_data); - free(response_buf); + /* 延迟75ms(根据AT命令手册要求) */ + osal_msleep(75); - return result; + /* 发送HTTP请求报文(ASCII文本,不是HEX编码) + * 注意:AT+HTTPSACTION=99/AT+HTTPACTION=99期望接收ASCII文本,不是HEX编码! + * 因此这里使用 ppp_serial_write 发送原始数据(它会自动处理wakeup) + */ + PPP_DEBUG_INFO("[HTTP] Sending DELETE request as ASCII text (%d bytes)...", request_len); + ppp_error_t write_result = ppp_serial_write((const uint8_t *)request_buf, request_len); + free(request_buf); + if (write_result != PPP_ERR_OK) { + PPP_DEBUG_ERROR("[HTTP] Failed to send DELETE request data via ppp_serial_write: error=%d", write_result); + return N706A_HTTP_ESEND; + } + PPP_DEBUG_INFO("[HTTP] DELETE request sent successfully (%d bytes as ASCII text)", request_len); + + PPP_DEBUG_INFO("[HTTP] DELETE request sent, waiting for response..."); + + /* 等待并接收响应(通过URC) */ + return http_receive_response(client, client_data, client->timeout_ms); } /** * @brief 发送HTTP HEAD请求 + * @note 使用HTTP专用AT指令时,HEAD请求需要使用OPEN_MODE (mode=99) 发送自定义HTTP报文 */ n706a_httpc_result_t n706a_http_client_head(n706a_http_client_t *client, const char *path, @@ -384,41 +1259,398 @@ n706a_httpc_result_t n706a_http_client_head(n706a_http_client_t *client, return N706A_HTTP_ECONN; } - /* 动态分配请求缓冲区 */ + /* HEAD请求使用OPEN_MODE (mode=99) 发送自定义HTTP报文 */ + /* 如果path与当前URL path不同,更新URL */ + if (strcmp(client->url_path, path) != 0) { + char *url = (char *)malloc(768); + if (!url) { + return N706A_HTTP_ENOMEM; + } + const char *scheme = client->is_https ? "https://" : "http://"; + snprintf(url, 768, "%s%s:%d%s", scheme, client->host, client->remote_port, path); + n706a_httpc_result_t ret = n706a_http_client_set_url(client, url, client->keepalive ? 1 : 0); + free(url); + if (ret != N706A_HTTP_SUCCESS) { + return ret; + } + } + + /* 构建HTTP请求报文 */ char *request_buf = (char *)malloc(2048); if (!request_buf) { return N706A_HTTP_ENOMEM; } - int request_len = http_build_request(client, "HEAD", path, - NULL, 0, NULL, - request_buf, 2048); + int request_len = snprintf(request_buf, 2048, + "HEAD %s HTTP/1.1\r\n" + "Host: %s\r\n" + "Connection: close\r\n" + "\r\n", + path, + client->host); - if (request_len < 0) { + if (request_len < 0 || request_len >= 2048) { free(request_buf); return N706A_HTTP_ENOBUFS; } - size_t sent; - n706a_tcp_send(&client->tcp_sock, (uint8_t*)request_buf, request_len, &sent); + /* 检查请求长度是否超过AT指令限制 */ + if (request_len > N706A_HTTP_MAX_REQUEST_LEN) { + free(request_buf); + PPP_DEBUG_ERROR("[HTTP] HEAD request length (%d bytes) exceeds AT+HTTPACTION max (%d bytes)", + request_len, N706A_HTTP_MAX_REQUEST_LEN); + return N706A_HTTP_ENOBUFS; + } + + /* 发送AT+HTTPACTION=99, 或 AT+HTTPSACTION=99, */ + char at_cmd[64]; + if (client->is_https) { + snprintf(at_cmd, sizeof(at_cmd), "AT+HTTPSACTION=99,%d", request_len); + } else { + snprintf(at_cmd, sizeof(at_cmd), "AT+HTTPACTION=99,%d", request_len); + } + + n706a_at_response_t *response = (n706a_at_response_t *)malloc(sizeof(n706a_at_response_t)); + if (!response) { + free(request_buf); + return N706A_HTTP_ENOMEM; + } + memset(response, 0, sizeof(n706a_at_response_t)); + + n706a_at_error_t ret = n706a_at_send_command(at_cmd, NULL, 5000, response); + if (ret != N706A_AT_OK) { + free(request_buf); + free(response); + PPP_DEBUG_ERROR("[HTTP] Failed to send HEAD request command: %s", response->response); + return N706A_HTTP_ESEND; + } + + /* 检查响应中是否包含 '>' 提示符 */ + if (strstr((const char *)response->response, ">") == NULL) { + free(request_buf); + free(response); + PPP_DEBUG_ERROR("[HTTP] Response does not contain '>' prompt: %s", response->response); + return N706A_HTTP_ESEND; + } + + free(response); + + /* 延迟75ms(根据AT命令手册要求) */ + osal_msleep(75); + + /* 发送HTTP请求报文(ASCII文本,不是HEX编码) + * 注意:AT+HTTPSACTION=99/AT+HTTPACTION=99期望接收ASCII文本,不是HEX编码! + * 因此这里使用 ppp_serial_write 发送原始数据(它会自动处理wakeup) + */ + PPP_DEBUG_INFO("[HTTP] Sending HEAD request as ASCII text (%d bytes)...", request_len); + ppp_error_t write_result = ppp_serial_write((const uint8_t *)request_buf, request_len); free(request_buf); + if (write_result != PPP_ERR_OK) { + PPP_DEBUG_ERROR("[HTTP] Failed to send HEAD request data via ppp_serial_write: error=%d", write_result); + return N706A_HTTP_ESEND; + } + PPP_DEBUG_INFO("[HTTP] HEAD request sent successfully (%d bytes as ASCII text)", request_len); + + PPP_DEBUG_INFO("[HTTP] HEAD request sent, waiting for response..."); + + /* 等待并接收响应(通过URC) */ + return http_receive_response(client, client_data, client->timeout_ms); +} + +/** + * @brief 添加SSL证书到N706A模块(仅HTTPS需要) + * @note 如果证书已存在,会先删除再重新添加,确保使用最新的证书内容 + */ +n706a_httpc_result_t n706a_http_client_add_certificate(const char *cert_name, + const uint8_t *cert_data, + size_t cert_len) +{ + if (!cert_name || !cert_data || cert_len == 0) { + return N706A_HTTP_EARG; + } + + /* 发送AT+CERTADD命令 */ + char at_cmd[128]; + snprintf(at_cmd, sizeof(at_cmd), "AT+CERTADD=%s,%zu", cert_name, cert_len); + + n706a_at_response_t *response = (n706a_at_response_t *)malloc(sizeof(n706a_at_response_t)); + if (!response) { + return N706A_HTTP_ENOMEM; + } + memset(response, 0, sizeof(n706a_at_response_t)); + + n706a_at_error_t ret = n706a_at_send_command(at_cmd, "CONNECT", 5000, response); + if (ret != N706A_AT_OK) { + PPP_DEBUG_ERROR("[HTTP] Failed to send CERTADD command: %s", response->response); + free(response); + return N706A_HTTP_ESEND; + } + + /* 检查响应中是否包含 'CONNECT' 提示符 */ + if (strstr((const char *)response->response, "CONNECT") == NULL) { + free(response); + PPP_DEBUG_ERROR("[HTTP] Response does not contain 'CONNECT' prompt"); + return N706A_HTTP_ESEND; + } + + free(response); + + /* 延迟75ms(根据AT命令手册要求) */ + osal_msleep(75); + + /* 发送证书数据(ASCII文本,PEM格式) + * 注意:AT+CERTADD期望接收ASCII文本,不是HEX编码! + * Python脚本使用 serial_conn.write(cert_data) 直接发送原始字节 + * 而 n706a_at_send_raw_data 会将数据转换为HEX字符串,这是错误的! + * 因此这里使用 ppp_serial_write 发送原始数据(它会自动处理wakeup) + */ + PPP_DEBUG_INFO("[HTTP] Sending certificate data as ASCII text (%u bytes)...", (unsigned int)cert_len); + + /* 使用 ppp_serial_write 发送原始证书数据(ASCII文本,PEM格式) + * 该函数会自动处理: + * 1. CAT1模块唤醒(低功耗模式下) + * 2. UART总线选择 + * 3. 错误处理 + */ + ppp_error_t write_result = ppp_serial_write(cert_data, cert_len); + if (write_result != PPP_ERR_OK) { + PPP_DEBUG_ERROR("[HTTP] Failed to send certificate data via ppp_serial_write: error=%d", write_result); + return N706A_HTTP_ESEND; + } + + PPP_DEBUG_INFO("[HTTP] Certificate data sent successfully (%u bytes as ASCII text)", (unsigned int)cert_len); + + /* 等待证书添加完成(模块会返回+CERTADD: ,OK URC,但不需要主动检查) + * 给模块一些时间处理证书数据(通常500ms足够) + * 如果证书添加失败,后续HTTPS连接时会返回AUTHFAIL错误,那时再处理 + */ + osal_msleep(500); + + PPP_DEBUG_INFO("[HTTP] Certificate %s add completed (waiting for module to process)", cert_name); + + /* 注意:不进行证书验证,如果证书添加失败,后续HTTPS连接时会返回AUTHFAIL错误 */ + + return N706A_HTTP_SUCCESS; +} + +/** + * @brief 检查证书是否已存在于设备中 + */ +bool n706a_http_client_certificate_exists(const char *cert_name) +{ + if (!cert_name) { + return false; + } + + char at_cmd[64]; + snprintf(at_cmd, sizeof(at_cmd), "AT+CERTCHECK=%s", cert_name); + + n706a_at_response_t *response = (n706a_at_response_t *)malloc(sizeof(n706a_at_response_t)); + if (!response) { + return false; + } + memset(response, 0, sizeof(n706a_at_response_t)); + + n706a_at_error_t ret = n706a_at_send_command(at_cmd, "OK", 5000, response); + bool exists = (ret == N706A_AT_OK && + strstr(response->response, cert_name) != NULL && + strstr(response->response, "OK") != NULL); + + free(response); + return exists; +} + +/** + * @brief 删除SSL证书 + * @param cert_name 证书文件名 + * @return N706A_HTTP_SUCCESS成功,其他失败 + */ +n706a_httpc_result_t n706a_http_client_delete_certificate(const char *cert_name) +{ + if (!cert_name) { + return N706A_HTTP_EARG; + } + + char at_cmd[64]; + snprintf(at_cmd, sizeof(at_cmd), "AT+CERTDEL=%s", cert_name); - /* 动态分配响应缓冲区 */ - uint8_t *response_buf = (uint8_t *)malloc(8192); - if (!response_buf) { + n706a_at_response_t *response = (n706a_at_response_t *)malloc(sizeof(n706a_at_response_t)); + if (!response) { return N706A_HTTP_ENOMEM; } + memset(response, 0, sizeof(n706a_at_response_t)); + - size_t received; - n706a_tcp_recv(&client->tcp_sock, response_buf, 8192 - 1, - &received, client->timeout_ms); + n706a_at_error_t ret = n706a_at_send_command(at_cmd, "OK", 5000, response); + if (ret != N706A_AT_OK) { + PPP_DEBUG_ERROR("[HTTP] Failed to delete certificate: %s", response->response); + free(response); + return N706A_HTTP_ESEND; + } - response_buf[received] = '\0'; + /* 检查响应中是否包含OK */ + if (strstr(response->response, "OK") == NULL) { + PPP_DEBUG_WARN("[HTTP] Certificate delete response may be abnormal: %s", response->response); + /* 即使响应异常,也可能删除成功(如果证书不存在) */ + if (strstr(response->response, "ERROR") != NULL) { + free(response); + return N706A_HTTP_ESEND; + } + } - n706a_httpc_result_t result = http_parse_response((char*)response_buf, received, client, client_data); - free(response_buf); + free(response); + PPP_DEBUG_INFO("[HTTP] Certificate %s deleted successfully", cert_name); + return N706A_HTTP_SUCCESS; +} + +/** + * @brief 初始化HTTPS通用证书(开机执行一次,检查有证书就跳过,无证书就添加) + * @return N706A_HTTP_SUCCESS成功,其他失败 + * @note 此函数只在开机第一次调用时执行,检查证书是否存在 + * 如果证书已存在,跳过添加;如果不存在,添加证书 + * 后续调用直接返回成功,不再执行任何操作 + */ +n706a_httpc_result_t n706a_http_client_init_certificate() +{ + /* 如果已经初始化过,直接返回成功 */ + if (g_cert_initialized) { + PPP_DEBUG_DEBUG("[HTTP] Certificate already initialized, skipping..."); + return N706A_HTTP_SUCCESS; + } + + /* 开机第一次调用:检查证书是否存在 */ + if (n706a_http_client_certificate_exists(N706A_ROOT_CA_CERT_NAME)) { + PPP_DEBUG_INFO("[HTTP] Certificate %s already exists, skipping add...", N706A_ROOT_CA_CERT_NAME); + g_cert_initialized = true; /* 标记已初始化,后续不再执行 */ + return N706A_HTTP_SUCCESS; + } + + /* 证书不存在,添加证书 */ + PPP_DEBUG_INFO("[HTTP] Certificate %s not found, adding (size: %u bytes)...", + N706A_ROOT_CA_CERT_NAME, (unsigned int)N706A_ROOT_CA_CERT_LEN); + n706a_httpc_result_t ret = n706a_http_client_add_certificate( + N706A_ROOT_CA_CERT_NAME, + (const uint8_t *)N706A_ROOT_CA_CERT_DATA, + N706A_ROOT_CA_CERT_LEN + ); + + if (ret == N706A_HTTP_SUCCESS) { + PPP_DEBUG_INFO("[HTTP] Certificate %s added successfully", N706A_ROOT_CA_CERT_NAME); + g_cert_initialized = true; /* 标记已初始化,后续不再执行 */ + } else { + PPP_DEBUG_ERROR("[HTTP] Failed to add certificate %s", N706A_ROOT_CA_CERT_NAME); + /* 即使添加失败,也标记为已初始化,避免重复尝试 */ + g_cert_initialized = true; + } - return result; + return ret; +} + +/** + * @brief 配置HTTPS SSL参数(仅HTTPS需要) + * @param ssl_version SSL版本,0=SSL3.0, 1=TLS1.0, 2=TLS1.1, 3=TLS1.2, 4=TLS1.0/1.1/1.2, 5=TLS1.3 + * @param auth_mode 认证模式,0=不认证, 1=单向认证, 2=双向认证 + * @param ca_cert CA证书文件名(单向/双向认证需要,如果为NULL则使用默认证书) + * @param client_cert 客户端证书文件名(双向认证需要,可为NULL) + * @param client_key 客户端密钥文件名(双向认证需要,可为NULL) + * @return N706A_HTTP_SUCCESS成功,其他失败 + * @note 使用AT+HTTPSCFG命令配置SSL参数 + * 如果ca_cert为NULL且auth_mode=1,则使用默认的root_ca.pem证书 + */ +n706a_httpc_result_t n706a_http_client_config_https(int ssl_version, + int auth_mode, + const char *ca_cert, + const char *client_cert, + const char *client_key) +{ + char at_cmd[256]; + n706a_at_response_t *response = (n706a_at_response_t *)malloc(sizeof(n706a_at_response_t)); + if (!response) { + return N706A_HTTP_ENOMEM; + } + memset(response, 0, sizeof(n706a_at_response_t)); + + /* 设置SSL版本 */ + snprintf(at_cmd, sizeof(at_cmd), "AT+HTTPSCFG=\"sslversion\",%d", ssl_version); + n706a_at_error_t ret = n706a_at_send_command(at_cmd, "OK", 5000, response); + if (ret != N706A_AT_OK) { + PPP_DEBUG_ERROR("[HTTP] Failed to set SSL version: %s", response->response); + free(response); + return N706A_HTTP_ESEND; + } + + /* 设置认证模式 */ + snprintf(at_cmd, sizeof(at_cmd), "AT+HTTPSCFG=\"authmode\",%d", auth_mode); + ret = n706a_at_send_command(at_cmd, "OK", 5000, response); + if (ret != N706A_AT_OK) { + PPP_DEBUG_ERROR("[HTTP] Failed to set auth mode: %s", response->response); + free(response); + return N706A_HTTP_ESEND; + } + + /* 单向认证:配置CA证书 */ + if (auth_mode == 1) { + + /* 如果证书未初始化,尝试初始化(通常应该在开机时已初始化) */ + if (!g_cert_initialized) { + n706a_httpc_result_t cert_init_ret = n706a_http_client_init_certificate(); + if (cert_init_ret != N706A_HTTP_SUCCESS) { + PPP_DEBUG_WARN("[HTTP] Certificate not initialized, attempting to initialize now..."); + /* 即使初始化失败,也继续配置,可能证书已存在 */ + } + } + + snprintf(at_cmd, sizeof(at_cmd), "AT+HTTPSCFG=\"cacert\",%s", N706A_ROOT_CA_CERT_NAME); + ret = n706a_at_send_command(at_cmd, "OK", 5000, response); + if (ret != N706A_AT_OK) { + PPP_DEBUG_ERROR("[HTTP] Failed to set CA certificate: %s", response->response); + free(response); + return N706A_HTTP_ESEND; + } + PPP_DEBUG_INFO("[HTTP] CA certificate configured: %s", N706A_ROOT_CA_CERT_NAME); + } + + /* 双向认证:配置CA证书、客户端证书和密钥 */ + if (auth_mode == 2) { + if (!ca_cert || !client_cert || !client_key) { + PPP_DEBUG_ERROR("[HTTP] CA certificate, client certificate and key required for auth_mode=2"); + free(response); + return N706A_HTTP_EARG; + } + + /* 配置CA证书 */ + snprintf(at_cmd, sizeof(at_cmd), "AT+HTTPSCFG=\"cacert\",%s", ca_cert); + ret = n706a_at_send_command(at_cmd, "OK", 5000, response); + if (ret != N706A_AT_OK) { + PPP_DEBUG_ERROR("[HTTP] Failed to set CA certificate: %s", response->response); + free(response); + return N706A_HTTP_ESEND; + } + + /* 配置客户端证书 */ + snprintf(at_cmd, sizeof(at_cmd), "AT+HTTPSCFG=\"clientcert\",%s", client_cert); + ret = n706a_at_send_command(at_cmd, "OK", 5000, response); + if (ret != N706A_AT_OK) { + PPP_DEBUG_ERROR("[HTTP] Failed to set client certificate: %s", response->response); + free(response); + return N706A_HTTP_ESEND; + } + + /* 配置客户端密钥 */ + snprintf(at_cmd, sizeof(at_cmd), "AT+HTTPSCFG=\"clientkey\",%s", client_key); + ret = n706a_at_send_command(at_cmd, "OK", 5000, response); + if (ret != N706A_AT_OK) { + PPP_DEBUG_ERROR("[HTTP] Failed to set client key: %s", response->response); + free(response); + return N706A_HTTP_ESEND; + } + + PPP_DEBUG_INFO("[HTTP] Mutual authentication configured: CA=%s, ClientCert=%s, ClientKey=%s", + ca_cert, client_cert, client_key); + } + + free(response); + return N706A_HTTP_SUCCESS; } /** @@ -501,8 +1733,20 @@ n706a_httpc_result_t n706a_http_client_close(n706a_http_client_t *client) return N706A_HTTP_EARG; } + osal_mutex_lock(&g_http_client_mutex); + if (client->connected) { - n706a_tcp_close(&client->tcp_sock); + /* 使用HTTP专用AT指令关闭连接 */ + n706a_at_response_t *response = (n706a_at_response_t *)malloc(sizeof(n706a_at_response_t)); + if (response) { + memset(response, 0, sizeof(n706a_at_response_t)); + if (client->is_https) { + n706a_at_send_command("AT+HTTPSCLOSE", "OK", 5000, response); + } else { + n706a_at_send_command("AT+HTTPCLOSE", "OK", 5000, response); + } + free(response); + } client->connected = false; } @@ -521,6 +1765,47 @@ n706a_httpc_result_t n706a_http_client_close(n706a_http_client_t *client) client->auth_password = NULL; } + /* host 和 url_path 是数组,不需要释放,只需清零 */ + memset(client->host, 0, sizeof(client->host)); + memset(client->url_path, 0, sizeof(client->url_path)); + + /* 释放响应缓冲区资源 */ + if (client->response_buffer) { + free(client->response_buffer); + client->response_buffer = NULL; + } + + if (client->response_mutex) { + osal_mutex *mutex = (osal_mutex *)client->response_mutex; + osal_mutex_destroy(mutex); + free(mutex); + client->response_mutex = NULL; + } + + if (client->response_sem) { + osal_semaphore *sem = (osal_semaphore *)client->response_sem; + osal_sem_destroy(sem); + free(sem); + client->response_sem = NULL; + } + + /* 清除全局客户端引用(如果当前客户端是全局客户端) */ + if (g_http_client == client) { + g_http_client = NULL; + PPP_DEBUG_INFO("[HTTP] Global HTTP client cleared"); + } else { + PPP_DEBUG_WARN("[HTTP] Closing client that is not the global client (g_http_client=%p, client=%p)", + g_http_client, client); + } + + /* 确保客户端状态被完全清理(防止后续检查时误判为活跃) */ + client->connected = false; + client->response_buffer = NULL; + client->response_mutex = NULL; + client->response_sem = NULL; + + osal_mutex_unlock(&g_http_client_mutex); + return N706A_HTTP_SUCCESS; } @@ -533,7 +1818,7 @@ bool n706a_http_client_is_connected(n706a_http_client_t *client) return false; } - return client->connected && n706a_tcp_is_connected(&client->tcp_sock); + return client->connected; } /** @@ -548,6 +1833,33 @@ int n706a_http_client_get_response_code(n706a_http_client_t *client) return client->response_code; } +/** + * @brief 设置HTTP流式接收回调 + */ +n706a_httpc_result_t n706a_http_client_set_stream_callback(n706a_http_client_t *client, + n706a_http_stream_callback_t callback, + void *user_data) +{ + if (!client) { + return N706A_HTTP_EARG; + } + + client->stream_callback = callback; + client->stream_user_data = user_data; + client->stream_mode_enabled = (callback != NULL); + + /* 重置流式接收状态 */ + client->stream_body_received = 0; + client->stream_content_length = 0; + client->stream_header_parsed = false; + client->stream_is_chunked = false; + client->stream_connection_closed = false; + + PPP_DEBUG_INFO("[HTTP] Stream mode %s", client->stream_mode_enabled ? "enabled" : "disabled"); + + return N706A_HTTP_SUCCESS; +} + /** * @brief 解析完整URL并发起GET请求 */ @@ -559,23 +1871,19 @@ n706a_httpc_result_t n706a_http_client_get_url(n706a_http_client_t *client, return N706A_HTTP_EARG; } - char protocol[8]; - char host[256]; - char path[512]; - uint16_t port; - - if (http_parse_url(url, protocol, host, &port, path) != 0) { - return N706A_HTTP_EPARSE; + /* 设置URL并建立连接 */ + n706a_httpc_result_t ret = n706a_http_client_set_url(client, url, 0); + if (ret != N706A_HTTP_SUCCESS) { + return ret; } - bool use_ssl = (strcmp(protocol, "https") == 0); - - n706a_httpc_result_t ret = n706a_http_client_connect(client, host, port, use_ssl); + ret = n706a_http_client_setup_connection(client); if (ret != N706A_HTTP_SUCCESS) { return ret; } - return n706a_http_client_get(client, path, client_data); + /* 发送GET请求 */ + return n706a_http_client_get(client, client->url_path, client_data); } /** @@ -592,284 +1900,894 @@ n706a_httpc_result_t n706a_http_client_post_url(n706a_http_client_t *client, return N706A_HTTP_EARG; } - char protocol[8]; - char host[256]; - char path[512]; - uint16_t port; - - if (http_parse_url(url, protocol, host, &port, path) != 0) { - return N706A_HTTP_EPARSE; + /* 设置URL并建立连接 */ + n706a_httpc_result_t ret = n706a_http_client_set_url(client, url, 0); + if (ret != N706A_HTTP_SUCCESS) { + return ret; } - bool use_ssl = (strcmp(protocol, "https") == 0); - - n706a_httpc_result_t ret = n706a_http_client_connect(client, host, port, use_ssl); + ret = n706a_http_client_setup_connection(client); if (ret != N706A_HTTP_SUCCESS) { return ret; } - return n706a_http_client_post(client, path, content_type, body, body_len, client_data); + /* 发送POST请求 */ + return n706a_http_client_post(client, client->url_path, content_type, body, body_len, client_data); } /* ========== 私有函数实现 ========== */ /** - * @brief 构造HTTP请求 + * @brief 解析URL */ -static int http_build_request(n706a_http_client_t *client, - const char *method, - const char *path, - const char *body, - size_t body_len, - const char *content_type, - char *request_buf, - size_t buf_size) +static int http_parse_url(const char *url, + char *protocol, + char *host, + uint16_t *port, + char *path) { - int len = 0; + if (!url || !protocol || !host || !port || !path) { + return -1; + } - /* 请求行 */ - len += snprintf(request_buf + len, buf_size - len, - "%s %s HTTP/1.1\r\n", method, path); + const char *p = url; - /* Host头 */ - len += snprintf(request_buf + len, buf_size - len, - "Host: %s\r\n", client->host); + /* 解析协议 */ + if (strncmp(p, "https://", 8) == 0) { + strcpy(protocol, "https"); + *port = 443; + p += 8; + } else if (strncmp(p, "http://", 7) == 0) { + strcpy(protocol, "http"); + *port = 80; + p += 7; + } else { + return -1; + } - /* User-Agent */ - len += snprintf(request_buf + len, buf_size - len, - "User-Agent: N706A-HTTP-Client/1.0\r\n"); + /* 解析host和port */ + const char *path_start = strchr(p, '/'); + const char *port_start = strchr(p, ':'); - /* Accept */ - len += snprintf(request_buf + len, buf_size - len, - "Accept: */*\r\n"); + if (port_start && (!path_start || port_start < path_start)) { + /* 有端口号 */ + size_t host_len = port_start - p; + strncpy(host, p, host_len); + host[host_len] = '\0'; + + *port = atoi(port_start + 1); + + if (path_start) { + strcpy(path, path_start); + } else { + strcpy(path, "/"); + } + } else { + /* 没有端口号 */ + if (path_start) { + size_t host_len = path_start - p; + strncpy(host, p, host_len); + host[host_len] = '\0'; + strcpy(path, path_start); + } else { + strcpy(host, p); + strcpy(path, "/"); + } + } - /* Connection */ - len += snprintf(request_buf + len, buf_size - len, - "Connection: %s\r\n", - client->keepalive ? "keep-alive" : "close"); + return 0; +} + +/** + * @brief 接收HTTP/HTTPS响应数据(通过URC) + * @param client HTTP客户端结构指针 + * @param client_data 响应数据结构指针 + * @param timeout_ms 超时时间(毫秒) + * @return N706A_HTTP_SUCCESS成功,其他失败 + */ +static n706a_httpc_result_t http_receive_response(n706a_http_client_t *client, + n706a_http_client_data_t *client_data, + uint32_t timeout_ms) +{ + if (!client || !client_data) { + return N706A_HTTP_EARG; + } - /* Content-Type和Content-Length(POST/PUT) */ - if (body && body_len > 0) { - if (content_type) { - len += snprintf(request_buf + len, buf_size - len, - "Content-Type: %s\r\n", content_type); - } - - len += snprintf(request_buf + len, buf_size - len, - "Content-Length: %zu\r\n", body_len); + if (!client->response_mutex || !client->response_sem) { + PPP_DEBUG_ERROR("[HTTP] Response buffer not initialized"); + return N706A_HTTP_EARG; + } + + osal_mutex *mutex = (osal_mutex *)client->response_mutex; + osal_semaphore *sem = (osal_semaphore *)client->response_sem; + + /* 清空响应缓冲区 */ + osal_mutex_lock(mutex); + if (!client->stream_mode_enabled) { + /* 传统模式:清空缓冲区 */ + client->response_received = 0; + client->response_complete = false; + } + /* 流式模式:不清空response_received,因为可能已经有部分响应头数据 */ + osal_mutex_unlock(mutex); + + /* 等待HTTP响应URC(+HTTPRECV: 或 +HTTPSRECV:) */ + /* 注意:URC会在n706a_at_handler中处理并写入response_buffer */ + int ret = osal_sem_down_timeout(sem, timeout_ms); + + if (ret != OSAL_SUCCESS) { + PPP_DEBUG_ERROR("[HTTP] Response receive timeout"); + return N706A_HTTP_ETIMEOUT; } - /* Basic认证 */ - if (client->auth_user && client->auth_password) { - char auth_str[256]; - char auth_b64[512]; + /* 流式模式:等待响应头解析完成,然后检查是否还有更多数据 */ + if (client->stream_mode_enabled) { + osal_mutex_lock(mutex); + /* 等待响应头解析完成(最多等待1秒) */ + uint32_t wait_count = 0; + while (!client->stream_header_parsed && wait_count < 100) { + osal_mutex_unlock(mutex); + osal_msleep(10); /* 等待10ms */ + wait_count++; + osal_mutex_lock(mutex); + } - snprintf(auth_str, sizeof(auth_str), "%s:%s", - client->auth_user, client->auth_password); + if (!client->stream_header_parsed) { + osal_mutex_unlock(mutex); + PPP_DEBUG_ERROR("[HTTP] Stream mode: Header parsing timeout"); + return N706A_HTTP_ETIMEOUT; + } - http_base64_encode(auth_str, strlen(auth_str), auth_b64); + /* 设置响应码和Content-Length到client_data */ + client_data->responseContentLen = client->stream_content_length; + client_data->isChunked = client->stream_is_chunked; - len += snprintf(request_buf + len, buf_size - len, - "Authorization: Basic %s\r\n", auth_b64); + /* 检查是否还有更多数据需要接收 */ + bool has_more = false; + if (client->stream_content_length > 0) { + /* 有Content-Length,检查是否接收完成 */ + has_more = (client->stream_body_received < (size_t)client->stream_content_length); + } else if (client->stream_is_chunked) { + /* Chunked编码,假设还有更多数据(直到收到最后一个chunk) */ + has_more = true; + } else { + /* 没有Content-Length且不是chunked,假设没有更多数据 */ + has_more = false; + } + client_data->isMore = has_more; + + osal_mutex_unlock(mutex); + + PPP_DEBUG_INFO("[HTTP] Stream mode: Header parsed, status=%d, Content-Length=%d, body_received=%zu, isMore=%d", + client->response_code, client->stream_content_length, + client->stream_body_received, has_more ? 1 : 0); + + return N706A_HTTP_SUCCESS; } - /* 自定义请求头 */ - if (client->header) { - len += snprintf(request_buf + len, buf_size - len, - "%s", client->header); - } + /* 传统模式:读取响应数据 */ + osal_mutex_lock(mutex); - /* 空行 */ - len += snprintf(request_buf + len, buf_size - len, "\r\n"); + if (client->response_received == 0) { + osal_mutex_unlock(mutex); + PPP_DEBUG_ERROR("[HTTP] No response data received"); + return N706A_HTTP_ERECV; + } - /* 请求体 */ - if (body && body_len > 0) { - if (len + body_len < buf_size) { - memcpy(request_buf + len, body, body_len); - len += body_len; + /* 调试:打印完整的response_buffer内容(前512字节) */ + size_t debug_buf_len = (client->response_received < 512) ? client->response_received : 512; + char debug_buf[513] = {0}; + for (size_t i = 0; i < debug_buf_len; i++) { + uint8_t c = client->response_buffer[i]; + if (c >= 32 && c < 127) { + debug_buf[i] = c; + } else if (c == '\r') { + debug_buf[i] = '\\'; + if (i + 1 < 512) debug_buf[++i] = 'r'; + } else if (c == '\n') { + debug_buf[i] = '\\'; + if (i + 1 < 512) debug_buf[++i] = 'n'; } else { - return -1; // 缓冲区不足 + debug_buf[i] = '.'; } } - - return len; -} - -/** - * @brief 解析HTTP响应 - */ -static n706a_httpc_result_t http_parse_response(const char *response, - size_t response_len, - n706a_http_client_t *client, - n706a_http_client_data_t *data) -{ - if (!response || !client || !data) { - return N706A_HTTP_EARG; + PPP_DEBUG_INFO("[HTTP] Full response_buffer (first %u/%u bytes): %s", + (unsigned int)debug_buf_len, + (unsigned int)client->response_received, + debug_buf); + + /* 解析HTTP响应 */ + /* 查找HTTP响应头开始位置 */ + const char *http_start = (const char *)client->response_buffer; + const char *http_header = strstr(http_start, "HTTP/"); + if (!http_header) { + osal_mutex_unlock(mutex); + PPP_DEBUG_ERROR("[HTTP] Invalid HTTP response format - 'HTTP/' not found in buffer. " + "Buffer content (first 128 bytes): %.128s", http_start); + return N706A_HTTP_EPROTO; } - /* 解析状态行 */ + size_t http_header_offset = http_header - http_start; + PPP_DEBUG_INFO("[HTTP] Found 'HTTP/' at offset %u (skipped %u bytes before HTTP header)", + (unsigned int)http_header_offset, (unsigned int)http_header_offset); + + /* 解析状态码 */ int major, minor, status_code; - if (sscanf(response, "HTTP/%d.%d %d", &major, &minor, &status_code) != 3) { + if (sscanf(http_header, "HTTP/%d.%d %d", &major, &minor, &status_code) != 3) { + osal_mutex_unlock(mutex); + PPP_DEBUG_ERROR("[HTTP] Failed to parse HTTP status line"); return N706A_HTTP_EPROTO; } client->response_code = status_code; - /* 查找头尾分界 */ - const char *header_end = strstr(response, "\r\n\r\n"); + /* 查找响应头结束位置 */ + const char *header_end = strstr(http_header, "\r\n\r\n"); if (!header_end) { + osal_mutex_unlock(mutex); + PPP_DEBUG_ERROR("[HTTP] HTTP header not complete"); return N706A_HTTP_EPROTO; } /* 提取响应头 */ - size_t header_len = header_end - response; - if (data->headerBuf && data->headerBufLen > 0) { - size_t copy_len = (header_len < (size_t)data->headerBufLen - 1) ? - header_len : (size_t)data->headerBufLen - 1; - memcpy(data->headerBuf, response, copy_len); - data->headerBuf[copy_len] = '\0'; + size_t header_len = header_end - http_header; + if (client_data->headerBuf && client_data->headerBufLen > 0) { + size_t copy_len = (header_len < (size_t)client_data->headerBufLen - 1) ? + header_len : (size_t)client_data->headerBufLen - 1; + memcpy(client_data->headerBuf, http_header, copy_len); + client_data->headerBuf[copy_len] = '\0'; + client_data->headerBufLen = copy_len; } /* 提取Content-Length */ - const char *content_len_str = strcasestr(response, "Content-Length:"); + const char *content_len_str = strcasestr(http_header, "Content-Length:"); if (content_len_str && content_len_str < header_end) { - data->responseContentLen = atoi(content_len_str + 15); + client_data->responseContentLen = atoi(content_len_str + 15); + PPP_DEBUG_INFO("[HTTP] Content-Length found: %d", client_data->responseContentLen); } else { - data->responseContentLen = 0; + client_data->responseContentLen = 0; + PPP_DEBUG_WARN("[HTTP] Content-Length not found in response header"); } /* 检查是否chunked */ - if (strcasestr(response, "Transfer-Encoding: chunked")) { - data->isChunked = true; + bool is_chunked = false; + if (strcasestr(http_header, "Transfer-Encoding: chunked")) { + client_data->isChunked = true; + is_chunked = true; + PPP_DEBUG_INFO("[HTTP] Response uses Transfer-Encoding: chunked"); } else { - data->isChunked = false; + client_data->isChunked = false; } /* 提取响应体 */ const char *body = header_end + 4; - size_t body_len = response_len - (body - response); + size_t body_offset = body - http_start; + size_t body_len = client->response_received - body_offset; + + PPP_DEBUG_INFO("[HTTP] Body extraction: body_offset=%u, body_len=%u, response_received=%u", + (unsigned int)body_offset, + (unsigned int)body_len, + (unsigned int)client->response_received); + + /* 调试:打印响应体原始内容(前256字节) */ + size_t body_preview_len = (body_len < 256) ? body_len : 256; + char body_preview_buf[257] = {0}; + for (size_t i = 0; i < body_preview_len; i++) { + if (body[i] >= 32 && body[i] < 127) { + body_preview_buf[i] = body[i]; + } else if (body[i] == '\r') { + body_preview_buf[i] = '\\'; + if (i + 1 < 256) body_preview_buf[++i] = 'r'; + } else if (body[i] == '\n') { + body_preview_buf[i] = '\\'; + if (i + 1 < 256) body_preview_buf[++i] = 'n'; + } else { + body_preview_buf[i] = '.'; + } + } + PPP_DEBUG_INFO("[HTTP] Body raw content (first %u bytes): %s", (unsigned int)body_preview_len, body_preview_buf); + + /* 如果使用chunked编码,需要解析chunked格式 */ + const char *decoded_body = body; + size_t decoded_body_len = body_len; - if (data->responseBuf && data->responseBufLen > 0) { - size_t copy_len = (body_len < (size_t)data->responseBufLen - 1) ? - body_len : (size_t)data->responseBufLen - 1; - memcpy(data->responseBuf, body, copy_len); - data->responseBuf[copy_len] = '\0'; - data->contentBlockLen = copy_len; + if (is_chunked) { + /* 解析chunked编码:格式为 \r\n\r\n\r\n\r\n...0\r\n\r\n */ + /* 需要跳过chunk size行,只提取chunk data */ + char *decoded_buf = (char *)malloc(body_len + 1); + if (!decoded_buf) { + osal_mutex_unlock(mutex); + PPP_DEBUG_ERROR("[HTTP] Failed to allocate memory for chunked decoding"); + return N706A_HTTP_ENOMEM; + } + + size_t decoded_pos = 0; + const char *p = body; + const char *end = body + body_len; + + while (p < end) { + /* 跳过空白行 */ + while (p < end && (*p == '\r' || *p == '\n')) { + p++; + } + if (p >= end) break; + + /* 读取chunk size(十六进制) */ + const char *chunk_size_start = p; + while (p < end && *p != '\r' && *p != '\n' && *p != ';') { + p++; + } + + if (p >= end) break; + + /* 解析chunk size */ + char chunk_size_str[32] = {0}; + size_t chunk_size_str_len = (p - chunk_size_start < sizeof(chunk_size_str) - 1) ? + (p - chunk_size_start) : (sizeof(chunk_size_str) - 1); + memcpy(chunk_size_str, chunk_size_start, chunk_size_str_len); + + unsigned int chunk_size = 0; + if (sscanf(chunk_size_str, "%x", &chunk_size) != 1) { + PPP_DEBUG_WARN("[HTTP] Failed to parse chunk size: %s", chunk_size_str); + break; + } + + PPP_DEBUG_DEBUG("[HTTP] Parsed chunk size: %u (0x%x)", chunk_size, chunk_size); + + if (chunk_size == 0) { + /* 最后一个chunk,结束 */ + PPP_DEBUG_INFO("[HTTP] Found final chunk (size=0), chunked decoding complete"); + break; + } + + /* 跳过chunk size行(包括\r\n) */ + while (p < end && (*p == '\r' || *p == '\n' || *p == ';')) { + p++; + } + if (p >= end) break; + + /* 复制chunk data */ + size_t chunk_data_len = (p + chunk_size <= end) ? chunk_size : (end - p); + if (decoded_pos + chunk_data_len <= body_len) { + memcpy(decoded_buf + decoded_pos, p, chunk_data_len); + decoded_pos += chunk_data_len; + p += chunk_size; + } else { + PPP_DEBUG_WARN("[HTTP] Chunked decode buffer overflow: decoded_pos=%u, chunk_data_len=%u, body_len=%u", + (unsigned int)decoded_pos, (unsigned int)chunk_data_len, (unsigned int)body_len); + break; + } + + /* 跳过chunk data后的\r\n */ + while (p < end && (*p == '\r' || *p == '\n')) { + p++; + } + } + + decoded_body = decoded_buf; + decoded_body_len = decoded_pos; + + PPP_DEBUG_INFO("[HTTP] Chunked decoding: original=%u bytes, decoded=%u bytes", + (unsigned int)body_len, (unsigned int)decoded_body_len); + + /* 如果Content-Length存在,使用它来限制响应体长度 */ + if (client_data->responseContentLen > 0 && decoded_body_len > (size_t)client_data->responseContentLen) { + decoded_body_len = (size_t)client_data->responseContentLen; + PPP_DEBUG_INFO("[HTTP] Truncating decoded body_len to Content-Length=%d", + client_data->responseContentLen); + } + + /* 将解码后的数据复制到responseBuf */ + if (client_data->responseBuf && client_data->responseBufLen > 0) { + size_t copy_len = (decoded_body_len < (size_t)client_data->responseBufLen - 1) ? + decoded_body_len : (size_t)client_data->responseBufLen - 1; + memcpy(client_data->responseBuf, decoded_body, copy_len); + client_data->responseBuf[copy_len] = '\0'; + client_data->responseBufLen = copy_len; + client_data->contentBlockLen = copy_len; + + /* 调试:打印最终提取的响应体内容(前256字节) */ + size_t final_preview_len = (copy_len < 256) ? copy_len : 256; + char final_preview_buf[257] = {0}; + for (size_t i = 0; i < final_preview_len; i++) { + if (client_data->responseBuf[i] >= 32 && client_data->responseBuf[i] < 127) { + final_preview_buf[i] = client_data->responseBuf[i]; + } else if (client_data->responseBuf[i] == '\r') { + final_preview_buf[i] = '\\'; + if (i + 1 < 256) final_preview_buf[++i] = 'r'; + } else if (client_data->responseBuf[i] == '\n') { + final_preview_buf[i] = '\\'; + if (i + 1 < 256) final_preview_buf[++i] = 'n'; + } else { + final_preview_buf[i] = '.'; + } + } + PPP_DEBUG_INFO("[HTTP] Final extracted body (first %u/%u bytes): %s", + (unsigned int)final_preview_len, + (unsigned int)copy_len, + final_preview_buf); + } + + free(decoded_buf); + + /* 检查是否还有更多数据 */ + if (client_data->responseContentLen > 0 && decoded_body_len < (size_t)client_data->responseContentLen) { + client_data->isMore = true; + } else { + client_data->isMore = false; + } + + osal_mutex_unlock(mutex); + return N706A_HTTP_SUCCESS; + } + + /* 非chunked编码:如果Content-Length存在,使用它来限制响应体长度,避免包含额外的数据(如URC长度字段) */ + size_t body_len_before_truncate = body_len; + if (client_data->responseContentLen > 0 && body_len > (size_t)client_data->responseContentLen) { + body_len = (size_t)client_data->responseContentLen; + PPP_DEBUG_INFO("[HTTP] Truncating body_len from %u to %u (Content-Length=%d)", + (unsigned int)body_len_before_truncate, + (unsigned int)body_len, + client_data->responseContentLen); + } else if (client_data->responseContentLen > 0) { + PPP_DEBUG_INFO("[HTTP] Body length OK: body_len=%u, Content-Length=%d", + (unsigned int)body_len, + client_data->responseContentLen); + } + + if (client_data->responseBuf && client_data->responseBufLen > 0) { + size_t copy_len = (body_len < (size_t)client_data->responseBufLen - 1) ? + body_len : (size_t)client_data->responseBufLen - 1; + memcpy(client_data->responseBuf, decoded_body, copy_len); + client_data->responseBuf[copy_len] = '\0'; + client_data->responseBufLen = copy_len; + client_data->contentBlockLen = copy_len; + + /* 调试:打印最终提取的响应体内容(前256字节) */ + size_t final_preview_len = (copy_len < 256) ? copy_len : 256; + char final_preview_buf[257] = {0}; + for (size_t i = 0; i < final_preview_len; i++) { + if (client_data->responseBuf[i] >= 32 && client_data->responseBuf[i] < 127) { + final_preview_buf[i] = client_data->responseBuf[i]; + } else if (client_data->responseBuf[i] == '\r') { + final_preview_buf[i] = '\\'; + if (i + 1 < 256) final_preview_buf[++i] = 'r'; + } else if (client_data->responseBuf[i] == '\n') { + final_preview_buf[i] = '\\'; + if (i + 1 < 256) final_preview_buf[++i] = 'n'; + } else { + final_preview_buf[i] = '.'; + } + } + PPP_DEBUG_INFO("[HTTP] Final extracted body (first %u/%u bytes): %s", + (unsigned int)final_preview_len, + (unsigned int)copy_len, + final_preview_buf); } /* 检查是否还有更多数据 */ - if (data->responseContentLen > 0 && body_len < (size_t)data->responseContentLen) { - data->isMore = true; + if (client_data->responseContentLen > 0 && body_len < (size_t)client_data->responseContentLen) { + client_data->isMore = true; } else { - data->isMore = false; + client_data->isMore = false; } + osal_mutex_unlock(mutex); + + PPP_DEBUG_INFO("[HTTP] Response received: status=%d, body_len=%u", + status_code, (unsigned int)body_len); + return N706A_HTTP_SUCCESS; } /** - * @brief Base64编码 + * @brief 流式模式:解析HTTP响应头并提取响应体数据块 + * @param client HTTP客户端 + * @param data 接收到的数据 + * @param length 数据长度 + * @return 0成功,非0失败 */ -static void http_base64_encode(const char *input, size_t len, char *output) +static int http_stream_parse_and_deliver(n706a_http_client_t *client, + const uint8_t *data, + size_t length) { - size_t i = 0, j = 0; - unsigned char char_array_3[3]; - unsigned char char_array_4[4]; - - while (len--) { - char_array_3[i++] = *(input++); - if (i == 3) { - char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; - char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); - char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); - char_array_4[3] = char_array_3[2] & 0x3f; - - for (i = 0; i < 4; i++) { - output[j++] = base64_table[char_array_4[i]]; - } - i = 0; - } + PPP_DEBUG_INFO("[HTTP] http_stream_parse_and_deliver: length=%u, stream_header_parsed=%d", + (unsigned int)length, client ? client->stream_header_parsed : 0); + + if (!client || !data || length == 0 || !client->stream_callback) { + PPP_DEBUG_ERROR("[HTTP] http_stream_parse_and_deliver: invalid parameters: client=%p, data=%p, length=%u, callback=%p", + client, data, (unsigned int)length, client ? client->stream_callback : NULL); + return -1; + } + + /* 注意:HTTPRECV 不支持分片,所以 length > 2048 是正常的 */ + /* 这与 TCPRECV 不同,TCPRECV 有明确的 2048 字节限制并会自动分片 */ + /* HTTPRECV 的 URC 中声明的长度就是当前URC的数据长度(二进制长度) */ + /* 如果数据很大,模块会发送多个独立的 URC,每个 URC 的 是该 URC 的数据长度 */ + if (length > 2048) { + PPP_DEBUG_INFO("[HTTP] Stream URC data length=%u exceeds 2048 bytes (normal, HTTPRECV doesn't auto-fragment)", + (unsigned int)length); } - if (i) { - for (size_t k = i; k < 3; k++) { - char_array_3[k] = '\0'; + /* 如果响应头还未解析,先解析响应头 */ + if (!client->stream_header_parsed) { + /* 累积数据到缓冲区(用于解析响应头) */ + /* 注意:响应头解析阶段不能分片,因为响应头可能跨越多个URC */ + size_t available_space = client->response_buffer_size - client->response_received; + size_t copy_len = (length <= available_space) ? length : available_space; + + if (copy_len < length) { + PPP_DEBUG_WARN("[HTTP] Response buffer overflow while parsing header: need=%u, available=%u", + (unsigned int)length, (unsigned int)available_space); + } + + if (copy_len > 0) { + memcpy(client->response_buffer + client->response_received, data, copy_len); + client->response_received += copy_len; + } + + /* 查找HTTP响应头结束位置(使用轻量级手动搜索,避免strstr的栈开销) */ + const char *http_start = (const char *)client->response_buffer; + const char *http_header = NULL; + size_t buf_len = client->response_received; + + /* 手动搜索 "HTTP/" (避免strstr的栈开销) */ + for (size_t i = 0; i + 5 <= buf_len; i++) { + if (http_start[i] == 'H' && http_start[i+1] == 'T' && + http_start[i+2] == 'T' && http_start[i+3] == 'P' && + http_start[i+4] == '/') { + http_header = http_start + i; + break; + } + } + + if (!http_header) { + /* 响应头还未完整,继续等待 */ + return 0; + } + + /* 手动搜索 "\r\n\r\n" (避免strstr的栈开销) */ + const char *header_end = NULL; + size_t header_start_offset = http_header - http_start; + for (size_t i = header_start_offset; i + 4 <= buf_len; i++) { + if (http_start[i] == '\r' && http_start[i+1] == '\n' && + http_start[i+2] == '\r' && http_start[i+3] == '\n') { + header_end = http_start + i; + break; + } + } + + if (!header_end) { + /* 响应头还未完整,继续等待 */ + return 0; + } + + /* 解析状态码(使用轻量级手动解析,避免sscanf的栈开销) */ + int status_code = 0; + const char *p = http_header + 5; // 跳过 "HTTP/" + /* 跳过版本号 "1.1 " */ + while (p < header_end && (*p < '0' || *p > '9')) { + p++; + } + /* 解析状态码 */ + while (p < header_end && *p >= '0' && *p <= '9') { + status_code = status_code * 10 + (*p - '0'); + p++; + } + + if (status_code == 0) { + PPP_DEBUG_ERROR("[HTTP] Failed to parse HTTP status code in stream mode"); + return -1; + } + + client->response_code = status_code; + + /* 提取Content-Length(使用轻量级手动搜索,避免strcasestr的栈开销) */ + client->stream_content_length = 0; + const char *search_start = http_header; + size_t search_len = header_end - search_start; + /* 手动搜索 "Content-Length:" (不区分大小写) */ + for (size_t i = 0; i + 15 <= search_len; i++) { + const char *candidate = search_start + i; + /* 检查 "Content-Length:" (不区分大小写) */ + if ((candidate[0] == 'C' || candidate[0] == 'c') && + (candidate[1] == 'O' || candidate[1] == 'o') && + (candidate[2] == 'N' || candidate[2] == 'n') && + (candidate[3] == 'T' || candidate[3] == 't') && + (candidate[4] == 'E' || candidate[4] == 'e') && + (candidate[5] == 'N' || candidate[5] == 'n') && + (candidate[6] == 'T' || candidate[6] == 't') && + candidate[7] == '-' && + (candidate[8] == 'L' || candidate[8] == 'l') && + (candidate[9] == 'E' || candidate[9] == 'e') && + (candidate[10] == 'N' || candidate[10] == 'n') && + (candidate[11] == 'G' || candidate[11] == 'g') && + (candidate[12] == 'T' || candidate[12] == 't') && + (candidate[13] == 'H' || candidate[13] == 'h') && + candidate[14] == ':') { + /* 找到Content-Length,解析数值 */ + p = candidate + 15; + while (p < header_end && (*p == ' ' || *p == '\t')) { + p++; // 跳过空格 + } + while (p < header_end && *p >= '0' && *p <= '9') { + client->stream_content_length = client->stream_content_length * 10 + (*p - '0'); + p++; + } + PPP_DEBUG_INFO("[HTTP] Stream mode: Content-Length=%d", client->stream_content_length); + break; + } } - char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; - char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); - char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + if (client->stream_content_length == 0) { + PPP_DEBUG_WARN("[HTTP] Stream mode: Content-Length not found"); + } - for (size_t k = 0; k < i + 1; k++) { - output[j++] = base64_table[char_array_4[k]]; + /* 检查是否chunked(使用轻量级手动搜索,避免strcasestr的栈开销) */ + client->stream_is_chunked = false; + /* 手动搜索 "Transfer-Encoding: chunked" (不区分大小写) */ + for (size_t i = 0; i + 28 <= search_len; i++) { + const char *candidate = search_start + i; + /* 检查 "Transfer-Encoding: chunked" (简化检查,只检查关键部分) */ + if ((candidate[0] == 'T' || candidate[0] == 't') && + (candidate[1] == 'R' || candidate[1] == 'r') && + (candidate[2] == 'A' || candidate[2] == 'a') && + (candidate[3] == 'N' || candidate[3] == 'n') && + (candidate[4] == 'S' || candidate[4] == 's') && + (candidate[5] == 'F' || candidate[5] == 'f') && + (candidate[6] == 'E' || candidate[6] == 'e') && + (candidate[7] == 'R' || candidate[7] == 'r') && + candidate[8] == '-' && + (candidate[9] == 'E' || candidate[9] == 'e') && + (candidate[10] == 'N' || candidate[10] == 'n') && + (candidate[11] == 'C' || candidate[11] == 'c') && + (candidate[12] == 'O' || candidate[12] == 'o') && + (candidate[13] == 'D' || candidate[13] == 'd') && + (candidate[14] == 'I' || candidate[14] == 'i') && + (candidate[15] == 'N' || candidate[15] == 'n') && + (candidate[16] == 'G' || candidate[16] == 'g') && + candidate[17] == ':') { + /* 检查后面是否有 "chunked" */ + p = candidate + 18; + while (p < header_end && (*p == ' ' || *p == '\t')) { + p++; // 跳过空格 + } + if (p + 7 <= header_end && + (p[0] == 'c' || p[0] == 'C') && + (p[1] == 'h' || p[1] == 'H') && + (p[2] == 'u' || p[2] == 'U') && + (p[3] == 'n' || p[3] == 'N') && + (p[4] == 'k' || p[4] == 'K') && + (p[5] == 'e' || p[5] == 'E') && + (p[6] == 'd' || p[6] == 'D')) { + client->stream_is_chunked = true; + PPP_DEBUG_INFO("[HTTP] Stream mode: Transfer-Encoding: chunked"); + break; + } + } } - while (i++ < 3) { - output[j++] = '='; + /* 标记响应头已解析 */ + client->stream_header_parsed = true; + + /* 计算响应体起始位置 */ + size_t header_end_offset = (header_end + 4) - http_start; + size_t body_start_offset = header_end_offset; + + /* 提取第一个响应体数据块(如果有) */ + if (client->response_received > body_start_offset) { + size_t body_len = client->response_received - body_start_offset; + const uint8_t *body_data = client->response_buffer + body_start_offset; + + /* ⚠️ 关键:body_len 是从缓冲区中提取的实际数据长度 */ + /* HTTPRECV 不支持分片,所以 body_len > 2048 是正常的 */ + /* 这与 TCPRECV 不同,TCPRECV 有明确的 2048 字节限制并会自动分片 */ + + /* 判断是否完成 */ + bool is_finished = false; + if (client->stream_content_length > 0) { + /* 有Content-Length,检查是否接收完成 */ + is_finished = (client->stream_body_received + body_len >= (size_t)client->stream_content_length); + } else if (!client->stream_is_chunked) { + /* 没有Content-Length且不是chunked,假设这是最后一个数据块 */ + /* ⚠️ 注意:HTTPRECV 不支持分片,每个 URC 是独立的 */ + /* 但是,如果没有Content-Length且不是chunked,我们无法准确判断是否还有更多数据,只能假设 */ + is_finished = true; + } + + /* 调用回调函数 */ + int ret = client->stream_callback(client->stream_user_data, body_data, body_len, is_finished); + if (ret != 0) { + PPP_DEBUG_ERROR("[HTTP] Stream callback returned error: %d", ret); + return ret; + } + + client->stream_body_received += body_len; + + /* 清空缓冲区(响应头已解析,不再需要) */ + client->response_received = 0; } + + return 0; } - output[j] = '\0'; + /* 响应头已解析,直接传递响应体数据 */ + /* ⚠️ 关键:length 参数是 n706a_parse_httprecv_urc 返回的 actual_len(实际可用数据长度) */ + /* HTTPRECV 不支持分片,所以 length > 2048 是正常的 */ + /* 这与 TCPRECV 不同,TCPRECV 有明确的 2048 字节限制并会自动分片 */ + /* 因此,这里不需要再次分片,直接调用回调函数即可 */ + + /* 判断是否完成 */ + bool is_finished = false; + + /* 优先检查连接是否已关闭(检测到+HTTPSCLOSED/+HTTPCLOSED) */ + if (client->stream_connection_closed) { + is_finished = true; + PPP_DEBUG_INFO("[HTTP] Stream connection closed, marking as finished"); + } else if (client->stream_content_length > 0) { + /* 有Content-Length,检查是否接收完成 */ + is_finished = (client->stream_body_received + length >= (size_t)client->stream_content_length); + } else if (!client->stream_is_chunked) { + /* 没有Content-Length且不是chunked,假设这是最后一个数据块 */ + /* ⚠️ 注意:HTTPRECV 不支持分片,每个 URC 是独立的 */ + /* 但是,如果没有Content-Length且不是chunked,我们无法准确判断是否还有更多数据,只能假设 */ + is_finished = true; + } + + /* 调用回调函数 */ + int ret = client->stream_callback(client->stream_user_data, data, length, is_finished); + if (ret != 0) { + PPP_DEBUG_ERROR("[HTTP] Stream callback returned error: %d", ret); + return ret; + } + + client->stream_body_received += length; + + return 0; } /** - * @brief 解析URL + * @brief HTTP URC数据接收回调(由n706a_at_handler调用) + * @param data 接收到的HTTP响应数据 + * @param length 数据长度 */ -static int http_parse_url(const char *url, - char *protocol, - char *host, - uint16_t *port, - char *path) +/** + * @brief 标记HTTP连接已关闭(由n706a_at_handler调用,当检测到+HTTPSCLOSED时) + */ +void n706a_http_mark_connection_closed(void) { - if (!url || !protocol || !host || !port || !path) { - return -1; + osal_mutex_lock(&g_http_client_mutex); + if (g_http_client) { + g_http_client->stream_connection_closed = true; + PPP_DEBUG_INFO("[HTTP] Connection marked as closed (detected +HTTPSCLOSED/+HTTPCLOSED)"); + } + osal_mutex_unlock(&g_http_client_mutex); +} + +void n706a_http_on_urc_received(const uint8_t *data, size_t length, bool is_https) +{ + if (!data || length == 0) { + PPP_DEBUG_WARN("[HTTP] n706a_http_on_urc_received called with invalid parameters: data=%p, length=%u", + data, (unsigned int)length); + return; + } + + PPP_DEBUG_INFO("[HTTP] n706a_http_on_urc_received: length=%u, is_https=%d", + (unsigned int)length, is_https); + + /* 优化:尽快获取client指针并释放全局mutex,减少锁定时间 */ + n706a_http_client_t *client = NULL; + osal_mutex *mutex = NULL; + osal_semaphore *sem = NULL; + bool stream_mode_enabled = false; + void *stream_callback = NULL; + void *stream_user_data = NULL; + + osal_mutex_lock(&g_http_client_mutex); + client = g_http_client; + if (client) { + mutex = (osal_mutex *)client->response_mutex; + sem = (osal_semaphore *)client->response_sem; + stream_mode_enabled = client->stream_mode_enabled; + stream_callback = client->stream_callback; + stream_user_data = client->stream_user_data; + } + osal_mutex_unlock(&g_http_client_mutex); + + if (!client || !mutex || !client->response_buffer) { + PPP_DEBUG_WARN("[HTTP] URC for unregistered client, dropping %u bytes (client=%p, response_mutex=%p, response_buffer=%p)", + (unsigned int)length, client, + client ? client->response_mutex : NULL, + client ? client->response_buffer : NULL); + return; + } + + PPP_DEBUG_INFO("[HTTP] Client found: stream_mode_enabled=%d, stream_callback=%p", + stream_mode_enabled, stream_callback); + + osal_mutex_lock(mutex); + + /* 如果启用了流式模式,立即解析并传递数据 */ + if (stream_mode_enabled && stream_callback) { + PPP_DEBUG_INFO("[HTTP] Stream mode enabled, calling http_stream_parse_and_deliver: length=%u", + (unsigned int)length); + int ret = http_stream_parse_and_deliver(client, data, length); + if (ret != 0) { + PPP_DEBUG_ERROR("[HTTP] Stream parse and deliver failed: %d", ret); + } else { + PPP_DEBUG_INFO("[HTTP] Stream parse and deliver succeeded"); + } + /* 流式模式下仍然通知信号量(用于http_receive_response的同步) */ + if (sem) { + osal_sem_up(sem); + } + osal_mutex_unlock(mutex); + return; + } else { + PPP_DEBUG_WARN("[HTTP] Stream mode NOT enabled or callback not set: stream_mode_enabled=%d, stream_callback=%p", + client->stream_mode_enabled, client->stream_callback); } - const char *p = url; + /* 传统模式:累积数据到缓冲区 */ + /* ⚠️ 关键:length 参数是 n706a_parse_httprecv_urc 返回的 actual_len(实际可用数据长度) */ + /* HTTPRECV 不支持分片,所以 length > 2048 是正常的 */ + /* 这与 TCPRECV 不同,TCPRECV 有明确的 2048 字节限制并会自动分片 */ + /* 因此,我们可以直接使用 length 来复制数据,不需要截断 */ - /* 解析协议 */ - if (strncmp(p, "https://", 8) == 0) { - strcpy(protocol, "https"); - *port = 443; - p += 8; - } else if (strncmp(p, "http://", 7) == 0) { - strcpy(protocol, "http"); - *port = 80; - p += 7; - } else { - return -1; + /* 检查缓冲区空间 */ + size_t available_space = client->response_buffer_size - client->response_received; + size_t copy_len = (length > available_space) ? available_space : length; + + if (copy_len == 0) { + /* 缓冲区已满,无法继续写入 */ + PPP_DEBUG_WARN("[HTTP] Response buffer full: received=%u/%u, dropping %u bytes", + (unsigned int)client->response_received, + (unsigned int)client->response_buffer_size, + (unsigned int)length); + osal_mutex_unlock(mutex); + return; } - /* 解析host和port */ - const char *path_start = strchr(p, '/'); - const char *port_start = strchr(p, ':'); + /* 复制数据到缓冲区 */ + memcpy(client->response_buffer + client->response_received, data, copy_len); + client->response_received += copy_len; + client->response_complete = true; - if (port_start && (!path_start || port_start < path_start)) { - /* 有端口号 */ - size_t host_len = port_start - p; - strncpy(host, p, host_len); - host[host_len] = '\0'; - - *port = atoi(port_start + 1); + if (copy_len < length) { + PPP_DEBUG_WARN("[HTTP] URC data truncated: actual_len=%u, copied=%u, dropped=%u", + (unsigned int)length, (unsigned int)copy_len, (unsigned int)(length - copy_len)); + } + + if (client->response_received > 0) { + PPP_DEBUG_INFO("[HTTP] URC data buffered: received=%u, total=%u/%u", + (unsigned int)length, + (unsigned int)client->response_received, + (unsigned int)client->response_buffer_size); - if (path_start) { - strcpy(path, path_start); - } else { - strcpy(path, "/"); - } - } else { - /* 没有端口号 */ - if (path_start) { - size_t host_len = path_start - p; - strncpy(host, p, host_len); - host[host_len] = '\0'; - strcpy(path, path_start); - } else { - strcpy(host, p); - strcpy(path, "/"); + /* 调试:打印接收到的数据(前256字节) */ + size_t debug_len = (client->response_received < 256) ? client->response_received : 256; + char debug_buf[257] = {0}; + for (size_t i = 0; i < debug_len; i++) { + if (client->response_buffer[i] >= 32 && client->response_buffer[i] < 127) { + debug_buf[i] = client->response_buffer[i]; + } else if (client->response_buffer[i] == '\r') { + debug_buf[i] = '\\'; + if (i + 1 < 256) debug_buf[++i] = 'r'; + } else if (client->response_buffer[i] == '\n') { + debug_buf[i] = '\\'; + if (i + 1 < 256) debug_buf[++i] = 'n'; + } else { + debug_buf[i] = '.'; + } } + PPP_DEBUG_INFO("[HTTP] Received data (first %u bytes): %s", (unsigned int)debug_len, debug_buf); } - return 0; + /* 通知等待的接收函数 */ + if (client->response_received > 0 && sem) { + osal_sem_up(sem); + } + + osal_mutex_unlock(mutex); } diff --git a/code/sdk/zhihui/n706a_net/src/n706a_net_api.c b/code/sdk/zhihui/n706a_net/src/n706a_net_api.c index bdbb00aee..64d3cb7d1 100644 --- a/code/sdk/zhihui/n706a_net/src/n706a_net_api.c +++ b/code/sdk/zhihui/n706a_net/src/n706a_net_api.c @@ -6,19 +6,26 @@ #include "n706a_net_api.h" #include "n706a_at_handler.h" #include "n706a_tcp_client.h" +#include "n706a_http_client.h" /* 用于证书初始化 */ #include "ppp_config.h" #include "ppp_debug.h" #include "ppp_power_ctl.h" #include "ppp_serial.h" #include "ppp_cat1_api.h" #include "ppp_interrupt.h" -#include "ppp_api.h" #include "soc_osal.h" #include "osal_debug.h" #include "schedule/osal_task.h" #include "securec.h" #include #include +#include +#include + +// 时区相关宏定义(参照 ppp_api.c) +#define MINUTES_PER_QUARTER 15 /* minutes per quarter */ +#define QUARTERS_PER_HOUR 4 /* quarters per hour */ +#define MAX_TIMEZONE_HOURS 24 // N706A网络状态 static n706a_net_state_t g_n706a_state = N706A_NET_STATE_IDLE; @@ -53,9 +60,9 @@ errcode_t n706a_net_power_init(void) .reset_pin = S_AGPIO_L24, .reset_pin_en_value = 1, .ring_pin = S_MGPIO21, // CTS cat1 wakeup mcu - .ring_pin_en_value = 1, + .ring_pin_en_value = 2, .wakemodem_pin = S_MGPIO22, // RTS mcu wakeup cat1 - .wakemodem_pin_en_value = 0 + .wakemodem_pin_en_value = 1 }; // 注册4G电源控制引脚配置 @@ -140,7 +147,8 @@ errcode_t n706a_net_init(const n706a_net_config_t *config) PPP_DEBUG_ERROR("Failed to power on N706A module\n"); return ret; } - + #define INIT_SERIAL 1 //外接串口调试模组时,串口要设置input防止干扰。 + #if INIT_SERIAL // 3. 初始化串口 PPP_DEBUG_PRINT("Step 3: Initializing UART (baudrate: %u)\n", config->baudrate); ppp_serial_config_t serial_config = { @@ -157,7 +165,10 @@ errcode_t n706a_net_init(const n706a_net_config_t *config) n706a_net_power_control(false); return ERRCODE_FAIL; } - + #else + ppp_api_leiniao_serial_pin_to_gpio_input(); + PPP_DEBUG_PRINT("Serial pin to gpio input for pc debug\n"); + #endif // 4. 初始化AT命令处理器 PPP_DEBUG_PRINT("Step 4: Initializing AT command handler\n"); ret = ppp_at_handler_init(SIO_UART_BUS, config->baudrate, config->auto_baudrate); @@ -183,21 +194,46 @@ errcode_t n706a_net_init(const n706a_net_config_t *config) return ERRCODE_FAIL; } + // 6.5. 测试AT连接并启用回显(ATE1) + // 启用回显可以更好地区分命令响应和异步URC,通过AT+前缀明确识别命令响应 + PPP_DEBUG_PRINT("Step 6.5: Testing AT communication and enabling echo (ATE1)\n"); + n706a_at_response_t at_resp; + n706a_at_error_t at_ret = n706a_at_send_command("AT", "OK", 3000, &at_resp); + if (at_ret != N706A_AT_OK) { + PPP_DEBUG_WARN("AT test failed, but continuing...\n"); + // 不返回错误,继续尝试 + } else { + PPP_DEBUG_INFO("AT communication test passed\n"); + + // 立即执行ATE1启用回显 + PPP_DEBUG_PRINT("Enabling echo (ATE1) to better distinguish command responses from URCs\n"); + at_ret = n706a_at_send_command("ATE1", "OK", 3000, &at_resp); + if (at_ret != N706A_AT_OK) { + PPP_DEBUG_WARN("Failed to enable echo (ATE1), but continuing...\n"); + // 不返回错误,继续执行(某些模组可能不支持或已启用) + } else { + PPP_DEBUG_INFO("Echo enabled successfully (ATE1)\n"); + PPP_DEBUG_INFO("Note: Command echo is now enabled, responses will include AT+ prefix for better identification\n"); + } + } + // 7. 配置TCP接收模式为直接输出(URC模式,HEX上报) PPP_DEBUG_PRINT("Step 7: Configuring TCP URC mode (HEX format)\n"); - n706a_at_response_t at_resp; - n706a_at_error_t at_ret = n706a_at_send_command("AT+RECVMODE=1,1", "OK", 3000, &at_resp); + at_ret = n706a_at_send_command("AT+RECVMODE=1,1", "OK", 3000, &at_resp); if (at_ret != N706A_AT_OK) { PPP_DEBUG_WARN("Failed to set RECVMODE=1,1 (HEX mode), trying ASCII mode\n"); // 如果HEX模式失败,尝试ASCII模式作为备用 at_ret = n706a_at_send_command("AT+RECVMODE=1,0", "OK", 3000, &at_resp); if (at_ret != N706A_AT_OK) { PPP_DEBUG_WARN("Failed to set RECVMODE=1,0, using default\n"); + n706a_at_set_tcp_recv_hex_mode(false); // 默认使用ASCII模式 } else { PPP_DEBUG_WARN("Using ASCII mode as fallback\n"); + n706a_at_set_tcp_recv_hex_mode(false); // ASCII模式 } } else { PPP_DEBUG_INFO("TCP receive mode set to HEX format\n"); + n706a_at_set_tcp_recv_hex_mode(true); // HEX模式 } PPP_DEBUG_INFO("response: %d , %s\n",at_resp.length,at_resp.response); // 8. 注册TCP URC接收回调 @@ -208,6 +244,20 @@ errcode_t n706a_net_init(const n706a_net_config_t *config) PPP_DEBUG_PRINT("Step 8.1: Registering +TCPSETUP URC callback\n"); // n706a_tcp_on_tcpsetup_urc在n706a_tcp_client.c中实现,直接调用 n706a_at_register_tcpsetup_urc_callback(n706a_tcp_on_tcpsetup_urc); + + // 8.2. 注册+TCPSEND URC回调(用于TCP数据发送确认) + PPP_DEBUG_PRINT("Step 8.2: Registering +TCPSEND URC callback\n"); + // n706a_tcp_on_tcpsend_urc在n706a_tcp_client.c中实现,通过n706a_tcp_client.h声明 + n706a_at_register_tcpsend_urc_callback(n706a_tcp_on_tcpsend_urc); + + // 8.2.1. 注册+TCPCLOSE URC回调(用于TCP连接关闭通知) + PPP_DEBUG_PRINT("Step 8.2.1: Registering +TCPCLOSE URC callback\n"); + // n706a_tcp_on_tcpclose_urc在n706a_tcp_client.c中实现,通过n706a_tcp_client.h声明 + n706a_at_register_tcpclose_urc_callback(n706a_tcp_on_tcpclose_urc); + + // 8.3. 提前初始化+TCPSEND URC等待机制(避免在发送时初始化导致信号量创建失败) + PPP_DEBUG_PRINT("Step 8.3: Pre-initializing +TCPSEND URC wait mechanism\n"); + n706a_tcp_preinit_tcpsend_urc_wait(); // 9. 检查GPRS附着状态(AT+CGATT?) PPP_DEBUG_PRINT("Step 9: Checking GPRS attachment status (AT+CGATT?)\n"); @@ -257,7 +307,23 @@ errcode_t n706a_net_init(const n706a_net_config_t *config) PPP_DEBUG_PRINT("PDP context activated successfully\n"); PPP_DEBUG_INFO("response: %d , %s\n",at_resp.length,at_resp.response); + // 12. 初始化HTTPS证书(开机执行一次,检查有证书就跳过,无证书就添加) + PPP_DEBUG_PRINT("Step 12: Initializing HTTPS certificate (one-time on boot)\n"); + n706a_httpc_result_t cert_ret = n706a_http_client_init_certificate(); + if (cert_ret != N706A_HTTP_SUCCESS) { + PPP_DEBUG_WARN("HTTPS certificate initialization failed, but continuing...\n"); + PPP_DEBUG_WARN("HTTPS connections may fail if certificate is required\n"); + // 不阻止网络初始化,证书可能已存在或后续会添加 + } else { + PPP_DEBUG_PRINT("HTTPS certificate initialized successfully\n"); + } + g_n706a_state = N706A_NET_STATE_READY; + + // 更新网络状态为在线(PDP上下文激活后,网络已可用) + cat1_api_runtime_set_cat1_networkstat(1); + PPP_DEBUG_PRINT("Network status set to ONLINE (network_on=1)\n"); + PPP_DEBUG_PRINT("=== N706A AT Network Initialized Successfully ===\n"); PPP_DEBUG_PRINT("Mode: Pure AT commands (no PPP dialing)\n"); PPP_DEBUG_PRINT("Reception: Asynchronous interrupt + URC push\n"); @@ -385,8 +451,13 @@ errcode_t n706a_get_network_signal_info(void) * * 功能说明: * - 执行 AT+NWENPWRSAVE=1 命令 - * - 配置为:低电平唤醒,高电平进入休眠 + * - 配置为: + * 0:不允许进入休眠模式(默认) + * 1:允许进入休眠模式(DTR 信号低电平进入休眠,高电平退出休眠) + * 2:允许进入休眠模式(DTR 信号高电平进入休眠,低电平退出休眠) * - N706A_NET始终在AT模式 + * AT+NWENPWRSAVE=1 时 ppp_register_ctl_pin 设置 ppp_ctl_pin_4g_t wakemodem_pin_en_value = 1 + * AT+NWENPWRSAVE=2 时 ppp_register_ctl_pin 设置 ppp_ctl_pin_4g_t wakemodem_pin_en_value = 0 */ errcode_t n706a_enable_power_save_mode(void) { @@ -416,6 +487,43 @@ errcode_t n706a_enable_power_save_mode(void) return ERRCODE_SUCC; } +/** + * @brief 启用4G模块数据Ring功能 + * @return ERRCODE_SUCC-成功,其他-失败 + * + * 功能说明: + * - 执行 AT+NWURCCFG=0,1,200 命令 + * - 使能 data ring,持续 200ms + * - 用于在省电模式下通过 Ring 引脚唤醒 MCU + */ +errcode_t n706a_enable_data_ring(void) +{ + if (g_n706a_state == N706A_NET_STATE_IDLE) { + PPP_DEBUG_ERROR("N706A not initialized"); + return ERRCODE_FAIL; + } + + PPP_DEBUG_INFO("Enabling data ring (AT+NWURCCFG=0,1,200)..."); + + n706a_at_response_t response; + n706a_at_error_t ret = n706a_at_send_command("AT+NWURCCFG=0,1,200", "OK", 5000, &response); + + if (ret != N706A_AT_OK) { + PPP_DEBUG_ERROR("Failed to send AT+NWURCCFG command"); + return ERRCODE_FAIL; + } + + if (!response.success) { + PPP_DEBUG_ERROR("AT+NWURCCFG command failed, response: %s", response.response); + return ERRCODE_FAIL; + } + + PPP_DEBUG_CRITICAL("Data ring enabled successfully"); + PPP_DEBUG_INFO(" Configuration: AT+NWURCCFG=0,1,200 (enabled, 200ms duration)"); + + return ERRCODE_SUCC; +} + /* ========== Runtime Status Update Helper Functions ========== */ /** @@ -764,7 +872,10 @@ static errcode_t n706a_api_update_network_status(void) if (ret == ERRCODE_SUCC) { // 根据网络注册状态设置网络类型 cat1_api_runtime_set_cat1_networktype(network_status); - PPP_DEBUG_CRITICAL("Updated network status (CREG): %d", network_status); + // 根据网络注册状态设置网络连接状态(1或5表示已注册) + uint8_t network_on = (network_status == 1 || network_status == 5) ? 1 : 0; + cat1_api_runtime_set_cat1_networkstat(network_on); + PPP_DEBUG_CRITICAL("Updated network status (CREG): %d, network_on: %d", network_status, network_on); return ERRCODE_SUCC; } } @@ -784,7 +895,10 @@ static errcode_t n706a_api_update_network_status(void) // 根据网络注册状态设置网络类型 cat1_api_runtime_set_cat1_networktype(network_status); - PPP_DEBUG_CRITICAL("Updated network status (CEREG): %d", network_status); + // 根据网络注册状态设置网络连接状态(1或5表示已注册) + uint8_t network_on = (network_status == 1 || network_status == 5) ? 1 : 0; + cat1_api_runtime_set_cat1_networkstat(network_on); + PPP_DEBUG_CRITICAL("Updated network status (CEREG): %d, network_on: %d", network_status, network_on); return ERRCODE_SUCC; } @@ -884,10 +998,299 @@ errcode_t n706a_api_update_all_runtime_status(void) ret = ERRCODE_FAIL; } + // 同步网络时间 + PPP_DEBUG_CRITICAL("Step 8: Starting network time synchronization..."); + if (n706a_api_sync_time_from_network() != ERRCODE_SUCC) { + PPP_DEBUG_ERROR("Failed to sync time from network"); + ret = ERRCODE_FAIL; + } else { + PPP_DEBUG_CRITICAL("Step 8: Network time synchronization completed successfully"); + } + PPP_DEBUG_INFO("N706A runtime status update completed"); return ret; } +/** + * @brief 解析AT+CCLK响应并转换为时间戳和时区 + * @param response AT命令响应 + * @param timestamp 输出UTC时间戳(秒) + * @param timezone_quarters 输出时区(quarters,15分钟为单位) + * @return ERRCODE_SUCC-成功,其他-失败 + * + * AT+CCLK响应格式示例: + * +CCLK: "25/10/20,02:59:43+32" + * + * 重要:CCLK返回的时间字段是UTC时间,不是本地时间! + * - 时间字段:02:59:43 是 UTC 时间 + * - 时区字段:+32 表示本地时区为 UTC+8(32 quarters = 8 hours) + * - 本地时间:02:59:43 + 8小时 = 10:59:43 + */ +static errcode_t n706a_parse_cclk_and_convert_to_timestamp(const char *response, + uint32_t *timestamp, + int *timezone_quarters) +{ + if (!response || !timestamp || !timezone_quarters) { + return ERRCODE_INVALID_PARAM; + } + + // 查找 "+CCLK: " + const char *start = strstr(response, "+CCLK: "); + if (start == NULL) { + PPP_DEBUG_ERROR("CCLK response format error: no +CCLK: prefix"); + return ERRCODE_FAIL; + } + + start += 7; // 跳过"+CCLK: " + + // 查找第一个引号 + const char *quote_start = strchr(start, '"'); + if (quote_start == NULL) { + PPP_DEBUG_ERROR("CCLK response format error: no opening quote"); + return ERRCODE_FAIL; + } + quote_start++; // 跳过引号 + + // 查找第二个引号 + const char *quote_end = strchr(quote_start, '"'); + if (quote_end == NULL) { + PPP_DEBUG_ERROR("CCLK response format error: no closing quote"); + return ERRCODE_FAIL; + } + + // 提取并解析时间字符串 + char time_str[32] = {0}; + size_t len = quote_end - quote_start; + if (len >= sizeof(time_str)) { + len = sizeof(time_str) - 1; + } + strncpy_s(time_str, sizeof(time_str), quote_start, len); + time_str[len] = '\0'; + + PPP_DEBUG_INFO("Parsing CCLK time: %s", time_str); + + // 解析CCLK格式: "YY/MM/DD,HH:MM:SS+TZ" + int year, month, day, hour, minute, second, tz_quarters; + int parsed = sscanf_s(time_str, "%d/%d/%d,%d:%d:%d%d", + &year, &month, &day, &hour, &minute, &second, &tz_quarters); + + if (parsed != 7) { + PPP_DEBUG_ERROR("Failed to parse CCLK time format: %s", time_str); + return ERRCODE_FAIL; + } + + // 转换2位年份为4位年份(假设20xx年) + if (year < 100) { + year += 2000; + } + + // 验证年份合法性 + if (year < 2023) { + PPP_DEBUG_ERROR("Invalid year: %d (must be >= 2023)", year); + return ERRCODE_FAIL; + } + + // 构造 struct tm 结构(CCLK返回的是UTC时间) + struct tm time_info = {0}; + time_info.tm_year = year - 1900; // tm_year 是从1900年开始的 + time_info.tm_mon = month - 1; // tm_mon 是 0-11 + time_info.tm_mday = day; + time_info.tm_hour = hour; + time_info.tm_min = minute; + time_info.tm_sec = second; + time_info.tm_isdst = -1; // 让系统自动判断夏令时 + + // 使用 mktime 转换为时间戳 + // 注意:CCLK返回的时间字段是UTC时间,不是本地时间 + time_t _timestamp = mktime(&time_info); + if (_timestamp == -1) { + PPP_DEBUG_ERROR("Failed to convert time to timestamp"); + return ERRCODE_FAIL; + } + + // CCLK返回的时间本身就是UTC时间,直接使用 + // 时区字段表示本地时区相对于UTC的偏移 + uint32_t utc_timestamp = (uint32_t)_timestamp; + + *timestamp = utc_timestamp; + *timezone_quarters = tz_quarters; + + // 计算本地时间用于显示验证 + int timezone_hours = tz_quarters / QUARTERS_PER_HOUR; // 小时部分 + int timezone_minutes = (tz_quarters % QUARTERS_PER_HOUR) * MINUTES_PER_QUARTER; // 分钟部分 + + // 计算本地时间:UTC + 时区偏移 + int local_hour = hour; + int local_minute = minute + timezone_minutes; + int local_day = day; + + // 处理分钟进位 + if (local_minute >= 60) { + local_hour += local_minute / 60; + local_minute = local_minute % 60; + } else if (local_minute < 0) { + local_hour += (local_minute / 60) - 1; + local_minute = 60 + (local_minute % 60); + } + + // 加上时区的小时部分 + local_hour += timezone_hours; + + // 处理小时进位 + if (local_hour >= 24) { + local_day += local_hour / 24; + local_hour = local_hour % 24; + } else if (local_hour < 0) { + local_day += (local_hour / 24) - 1; + local_hour = 24 + (local_hour % 24); + } + + PPP_DEBUG_WARN("Parsed UTC time: %04d-%02d-%02d %02d:%02d:%02d", + year, month, day, hour, minute, second); + + // 显示时区信息(支持非整数小时) + if (timezone_minutes == 0) { + PPP_DEBUG_WARN("Timezone: %+d quarters (UTC%+d)", + tz_quarters, timezone_hours); + } else { + PPP_DEBUG_WARN("Timezone: %+d quarters (UTC%+d:%02d)", + tz_quarters, timezone_hours, timezone_minutes); + } + + PPP_DEBUG_WARN("Local time will be: %04d-%02d-%02d %02d:%02d:%02d", + year, month, local_day, local_hour, local_minute, second); + PPP_DEBUG_WARN("UTC timestamp: %u", utc_timestamp); + + return ERRCODE_SUCC; +} + +/** + * @brief 设置系统时间(参照 ppp_api.c 的 ppp_set_system_time 实现) + * @param utc_timestamp UTC时间戳(秒) + * @param timezone_quarters 时区(quarters,15分钟为单位) + * @return ERRCODE_SUCC-成功,其他-失败 + * + * 时间设置逻辑说明(参照 cat1_porting.c 的 sync_time_utc 实现): + * - 输入:UTC时间戳 + 时区偏移 + * - 设置:tv.tv_sec 设置为本地时间戳(UTC + 时区偏移),tz_minuteswest 设置时区偏移 + * - 系统时间直接设置为本地时间,tz_minuteswest 用于时区信息记录 + * + * 示例(UTC+8): + * - UTC时间:15:06:28 + * - 时区:+32 quarters = +8 hours + * - tv.tv_sec:本地时间戳(UTC时间戳 + 8小时 = 对应23:06:28) + * - tz_minuteswest:+480分钟 + * - 系统显示:23:06:28 + */ +static errcode_t n706a_set_system_time(uint32_t utc_timestamp, int timezone_quarters) +{ + // 参数验证 + if (timezone_quarters > (MAX_TIMEZONE_HOURS * QUARTERS_PER_HOUR) || + timezone_quarters < -(MAX_TIMEZONE_HOURS * QUARTERS_PER_HOUR)) { + PPP_DEBUG_ERROR("Invalid timezone: %d quarters", timezone_quarters); + return ERRCODE_FAIL; + } + + if (utc_timestamp == 0) { + PPP_DEBUG_ERROR("Invalid timestamp: 0"); + return ERRCODE_FAIL; + } + + // 准备时间结构(参照 cat1_porting.c 的 sync_time_utc 实现) + struct timeval tv = {0}; + struct timezone tzone = {0}; + + // settimeofday 的 tv_sec 设置为本地时间戳(UTC + 时区偏移) + // 这样系统时间就是本地时间,tz_minuteswest 用于时区信息记录 + tv.tv_sec = utc_timestamp + timezone_quarters * MINUTES_PER_QUARTER * 60; + tv.tv_usec = 0; + + // 设置时区信息 + tzone.tz_dsttime = 0; // 不使用夏令时 + tzone.tz_minuteswest = timezone_quarters * MINUTES_PER_QUARTER; + + PPP_DEBUG_INFO("Setting system time:"); + PPP_DEBUG_INFO(" UTC timestamp: %u", utc_timestamp); + PPP_DEBUG_INFO(" Local timestamp: %ld (UTC + %d hours)", tv.tv_sec, timezone_quarters / 4); + PPP_DEBUG_INFO(" Timezone: %+d minutes (%+d quarters)", + tzone.tz_minuteswest, timezone_quarters); + + // 调用系统API设置时间 + int ret = settimeofday(&tv, &tzone); + if (ret != 0) { + PPP_DEBUG_ERROR("settimeofday failed: %d", ret); + return ERRCODE_FAIL; + } + + // 验证时间设置成功 + time_t current_time = time(NULL); + struct tm timeinfo; + localtime_r(¤t_time, &timeinfo); + + PPP_DEBUG_CRITICAL("System time set successfully: %04d-%02d-%02d %02d:%02d:%02d", + timeinfo.tm_year + 1900, + timeinfo.tm_mon + 1, + timeinfo.tm_mday, + timeinfo.tm_hour, + timeinfo.tm_min, + timeinfo.tm_sec); + + return ERRCODE_SUCC; +} + +/** + * @brief 通过AT+CCLK命令获取网络时间并更新系统时间 + * @return ERRCODE_SUCC-成功,其他-失败 + * + * 实现说明(参照 ppp_api_update_time): + * 1. 发送 AT+CCLK? 命令获取网络时间 + * 2. 解析响应,获取UTC时间和时区偏移 + * 3. 计算本地时间 = UTC时间 + 时区偏移 + * 4. 使用 settimeofday 设置系统时间为本地时间 + * + * AT+CCLK 返回格式说明: + * - 命令:AT+CCLK? + * - 响应:+CCLK: "25/10/20,02:59:43+32" + * - 时间字段(02:59:43)是 UTC 时间 + * - 时区字段(+32)表示本地时区为 UTC+8 + * - 最终显示时间:02:59:43 + 8小时 = 10:59:43 + */ +errcode_t n706a_api_sync_time_from_network(void) +{ + PPP_DEBUG_INFO("=== Starting network time synchronization ==="); + PPP_DEBUG_INFO("Sending AT+CCLK? command..."); + + // 发送AT+CCLK?命令 + n706a_at_response_t response; + n706a_at_error_t ret = n706a_at_send_command("AT+CCLK?", "OK", 5000, &response); + if (ret != N706A_AT_OK) { + PPP_DEBUG_ERROR("Failed to send AT+CCLK command"); + return ERRCODE_FAIL; + } + + PPP_DEBUG_INFO("AT+CCLK response received"); + + // 解析CCLK响应并转换为时间戳 + uint32_t utc_timestamp = 0; + int timezone_quarters = 0; + errcode_t parse_ret = n706a_parse_cclk_and_convert_to_timestamp(response.response, &utc_timestamp, &timezone_quarters); + + if (parse_ret != ERRCODE_SUCC) { + PPP_DEBUG_ERROR("Failed to parse CCLK response"); + return parse_ret; + } + + // 设置系统时间 + errcode_t set_ret = n706a_set_system_time(utc_timestamp, timezone_quarters); + if (set_ret != ERRCODE_SUCC) { + PPP_DEBUG_ERROR("Failed to set system time"); + return set_ret; + } + + PPP_DEBUG_CRITICAL("=== Network time synchronized successfully ==="); + return ERRCODE_SUCC; +} + /** * @brief 动态设置N706A模块波特率(在所有模式下) * diff --git a/code/sdk/zhihui/n706a_net/src/n706a_tcp_client.c b/code/sdk/zhihui/n706a_net/src/n706a_tcp_client.c index 70e0a5e36..a3da82a72 100644 --- a/code/sdk/zhihui/n706a_net/src/n706a_tcp_client.c +++ b/code/sdk/zhihui/n706a_net/src/n706a_tcp_client.c @@ -20,7 +20,7 @@ #include /* ========== 全局socket管理 ========== */ -#define N706A_MAX_SOCKETS 6 // N706A支持0-5共6个socket +#define N706A_MAX_SOCKETS 3 // N706A支持0-5共6个socket static n706a_tcp_socket_t *g_tcp_sockets[N706A_MAX_SOCKETS] = {NULL}; static osal_mutex g_socket_table_mutex; @@ -36,8 +36,54 @@ static osal_semaphore g_tcpsetup_urc_sems[N706A_MAX_SOCKETS]; static bool g_tcpsetup_urc_results[N706A_MAX_SOCKETS]; // true=OK, false=FAIL static osal_mutex g_tcpsetup_urc_mutex; +/* +TCPSEND URC等待机制(每个socket一个信号量) */ +static osal_semaphore g_tcpsend_urc_sems[N706A_MAX_SOCKETS]; +static size_t g_tcpsend_urc_sent_bytes[N706A_MAX_SOCKETS]; // 实际发送的字节数 +static bool g_tcpsend_urc_received[N706A_MAX_SOCKETS]; // 是否收到URC +static osal_mutex g_tcpsend_urc_mutex; + /* ========== 私有函数声明 ========== */ +/** + * @brief 安全打印AT响应(避免读取超出范围或遇到 \0 字节导致乱码) + * @param response AT响应结构 + * @param max_len 最大打印长度(默认256) + */ +static void n706a_tcp_safe_print_response(const n706a_at_response_t *response, size_t max_len) +{ + /* 注意:response->response 是一个数组,不是指针,所以不需要检查 !response->response */ + /* 数组的地址总是非空,检查它会导致编译警告 */ + if (!response || response->length == 0) { + return; + } + + const size_t MAX_PRINT_LEN = (max_len > 0 && max_len < 512) ? max_len : 256; + size_t print_len = (response->length < MAX_PRINT_LEN) ? response->length : MAX_PRINT_LEN; + + /* 检查长度是否异常(可能是计算错误) */ + if (response->length > 65536) { + PPP_DEBUG_ERROR("[N706A_TCP] Response length异常: %u (可能包含二进制数据或计算错误)", + (unsigned int)response->length); + print_len = MAX_PRINT_LEN; /* 强制限制打印长度 */ + } + + char print_buf[MAX_PRINT_LEN + 1]; + memcpy(print_buf, response->response, print_len); + print_buf[print_len] = '\0'; + + /* 替换不可打印字符为 '.'(保留 \r, \n, \t) */ + for (size_t i = 0; i < print_len; i++) { + if (print_buf[i] < 32 || print_buf[i] >= 127) { + if (print_buf[i] != '\r' && print_buf[i] != '\n' && print_buf[i] != '\t') { + print_buf[i] = '.'; + } + } + } + + PPP_DEBUG_CRITICAL("[N706A_TCP] Response: %s%s", print_buf, + (response->length > MAX_PRINT_LEN) ? "..." : ""); +} + #ifdef N706A_SSL_USE_MBEDTLS static int tcp_mbedtls_send_callback(void *ctx, const unsigned char *buf, size_t len); static int tcp_mbedtls_recv_callback(void *ctx, unsigned char *buf, size_t len); @@ -49,6 +95,8 @@ static void tcp_mbedtls_cleanup_ssl(n706a_tcp_socket_t *sock); static n706a_tcp_result_t n706a_tcp_force_close_socket_by_id(int socket_id); int n706a_tcp_find_free_socket_id(void); /* 公共API,供HTTP和WebSocket使用 */ static bool n706a_tcp_wait_for_tcpsetup_urc(int socket_id, uint32_t timeout_ms, bool *success); +static void n706a_tcp_init_tcpsend_urc_wait(void); +static bool n706a_tcp_wait_for_tcpsend_urc(int socket_id, uint32_t timeout_ms, size_t expected_bytes, size_t *sent_bytes); /* ========== 公共函数实现 ========== */ @@ -91,16 +139,29 @@ n706a_tcp_result_t n706a_tcp_socket_create_ex(n706a_tcp_socket_t *sock, int sock for (int i = 0; i < N706A_MAX_SOCKETS; i++) { if (osal_mutex_init(&g_socket_urc_mutexes[i]) != OSAL_SUCCESS) { PPP_DEBUG_ERROR("Failed to init URC mutex for socket %d", i); + // 清理已创建的mutex + for (int j = 0; j < i; j++) { + osal_mutex_destroy(&g_socket_urc_mutexes[j]); + } return N706A_TCP_ERROR; } - if (osal_sem_init(&g_socket_urc_sems[i], 0) != OSAL_SUCCESS) { - PPP_DEBUG_ERROR("Failed to init URC semaphore for socket %d", i); + int sem_ret = osal_sem_init(&g_socket_urc_sems[i], 0); + if (sem_ret != OSAL_SUCCESS) { + PPP_DEBUG_ERROR("Failed to init URC semaphore for socket %d (ret=0x%x)", i, sem_ret); + // 清理已创建的资源 osal_mutex_destroy(&g_socket_urc_mutexes[i]); + for (int j = 0; j < i; j++) { + osal_mutex_destroy(&g_socket_urc_mutexes[j]); + osal_sem_destroy(&g_socket_urc_sems[j]); + } return N706A_TCP_ERROR; } } g_urc_resources_initialized = true; - PPP_DEBUG_DEBUG("URC resource pool initialized for %d sockets", N706A_MAX_SOCKETS); + PPP_DEBUG_DEBUG("URC resource pool initialized for %d sockets (total %d mutexes + %d semaphores)", + N706A_MAX_SOCKETS, N706A_MAX_SOCKETS, N706A_MAX_SOCKETS); + } else { + PPP_DEBUG_DEBUG("URC resource pool already initialized, skipping"); } osal_mutex_lock(&g_socket_table_mutex); @@ -237,7 +298,116 @@ n706a_tcp_result_t n706a_tcp_config_ssl_client_cert(n706a_tcp_socket_t *sock, #endif /** - * @brief 强制关闭指定socket ID的socket(方案1:连接前强制关闭旧socket) + * @brief 查询socket在模块端的状态(使用AT+IPSTATUS) + * @param socket_id socket ID (0-5) + * @param is_connected 输出:是否已连接(true=CONNECT, false=DISCONNECT) + * @param is_ssl 输出:是否是SSL连接(true=SSL, false=TCP,仅在已连接时有效) + * @return N706A_TCP_SUCCESS=查询成功,其他值=查询失败 + */ +n706a_tcp_result_t n706a_tcp_query_socket_status(int socket_id, bool *is_connected, bool *is_ssl) +{ + if (socket_id < 0 || socket_id >= N706A_MAX_SOCKETS || !is_connected || !is_ssl) { + return N706A_TCP_EINVAL; + } + + *is_connected = false; + *is_ssl = false; + + /* 发送AT+IPSTATUS命令查询socket状态 */ + /* 注意:AT+IPSTATUS可能不返回OK,只返回+IPSTATUS响应,所以使用NULL作为expected */ + char at_cmd[32]; + snprintf(at_cmd, sizeof(at_cmd), "AT+IPSTATUS=%d", socket_id); + + n706a_at_response_t response; + n706a_at_error_t ret = n706a_at_send_command(at_cmd, NULL, 3000, &response); + + if (ret != N706A_AT_OK || response.length == 0) { + PPP_DEBUG_DEBUG("Failed to query socket %d status via AT+IPSTATUS (ret=%d)", socket_id, ret); + return N706A_TCP_ERROR; + } + + /* 解析响应:+IPSTATUS: ,[,[,]] */ + /* 示例:+IPSTATUS: 0,CONNECT,TCP,4096 或 +IPSTATUS: 0,DISCONNECT */ + const char *response_str = response.response; + char status_pattern[32]; + snprintf(status_pattern, sizeof(status_pattern), "+IPSTATUS: %d,", socket_id); + + const char *status_pos = strstr(response_str, status_pattern); + if (!status_pos) { + PPP_DEBUG_DEBUG("Socket %d status response format error: %s", socket_id, response_str); + return N706A_TCP_ERROR; + } + + /* 跳过 "+IPSTATUS: ," */ + const char *status_start = status_pos + strlen(status_pattern); + + /* 检查状态:CONNECT 或 DISCONNECT */ + if (strncmp(status_start, "CONNECT", 7) == 0) { + *is_connected = true; + + /* 检查类型:TCP 或 UDP(SSL连接可能不显示类型,需要根据其他信息判断) */ + /* 注意:AT+IPSTATUS可能不直接显示SSL,但我们可以通过检查socket的is_ssl字段来判断 */ + /* 如果本地socket存在且is_ssl=true,认为是SSL连接 */ + if (g_socket_table_initialized) { + osal_mutex_lock(&g_socket_table_mutex); + n706a_tcp_socket_t *sock = g_tcp_sockets[socket_id]; + if (sock != NULL && sock->is_ssl) { + *is_ssl = true; + } + osal_mutex_unlock(&g_socket_table_mutex); + } + + PPP_DEBUG_DEBUG("Socket %d status: CONNECTED (SSL=%d)", socket_id, *is_ssl); + } else if (strncmp(status_start, "DISCONNECT", 10) == 0) { + *is_connected = false; + PPP_DEBUG_DEBUG("Socket %d status: DISCONNECTED", socket_id); + } else { + PPP_DEBUG_DEBUG("Socket %d status unknown: %s", socket_id, status_start); + return N706A_TCP_ERROR; + } + + return N706A_TCP_SUCCESS; +} + +/** + * @brief 检查socket在模块端是否已关闭 + * @param socket_id socket ID (0-5) + * @return true=已关闭或不存在,false=可能仍打开 + */ +static bool n706a_tcp_check_socket_closed(int socket_id) +{ + if (socket_id < 0 || socket_id >= N706A_MAX_SOCKETS) { + return true; // 无效socket ID,认为已关闭 + } + + /* 检查本地socket状态 */ + if (g_socket_table_initialized) { + osal_mutex_lock(&g_socket_table_mutex); + n706a_tcp_socket_t *sock = g_tcp_sockets[socket_id]; + if (sock != NULL) { + /* 如果本地状态是CLOSED,认为socket已关闭 */ + bool is_closed = (sock->state == N706A_TCP_STATE_CLOSED); + osal_mutex_unlock(&g_socket_table_mutex); + if (is_closed) { + PPP_DEBUG_DEBUG("Socket %d is already CLOSED in local state", socket_id); + return true; + } + } else { + /* socket不在表中,可能已经关闭 */ + osal_mutex_unlock(&g_socket_table_mutex); + PPP_DEBUG_DEBUG("Socket %d not in socket table, assuming closed", socket_id); + return true; + } + } + + /* 本地状态不是CLOSED,需要检查模块端状态 */ + /* 注意:这里不发送AT命令检查,因为可能会干扰正在进行的操作 */ + /* 如果本地状态不是CLOSED,保守地认为socket可能仍打开,需要关闭 */ + return false; +} + +/** + * @brief 强制关闭指定socket ID的socket(先检查状态,再决定是否需要关闭) * @param socket_id socket ID (0-5) * @return N706A_TCP_SUCCESS 成功,其他值失败 */ @@ -247,28 +417,108 @@ static n706a_tcp_result_t n706a_tcp_force_close_socket_by_id(int socket_id) return N706A_TCP_EINVAL; } - /* 尝试通过AT命令强制关闭socket */ + /* 先检查socket是否已经关闭 */ + if (n706a_tcp_check_socket_closed(socket_id)) { + PPP_DEBUG_DEBUG("Socket %d is already closed, skipping close command", socket_id); + return N706A_TCP_SUCCESS; + } + + /* socket可能仍打开,先查询模块端状态,根据状态选择关闭命令 */ + bool is_connected = false; + bool is_ssl = false; + n706a_tcp_result_t query_ret = n706a_tcp_query_socket_status(socket_id, &is_connected, &is_ssl); + + if (query_ret == N706A_TCP_SUCCESS && !is_connected) { + /* 模块端socket已断开,不需要关闭 */ + PPP_DEBUG_DEBUG("Socket %d is already DISCONNECTED at module side, no need to close", socket_id); + return N706A_TCP_SUCCESS; + } + + /* socket可能仍打开,需要发送关闭命令 */ + /* 根据查询到的状态(或本地socket的is_ssl字段)选择关闭命令 */ + if (query_ret != N706A_TCP_SUCCESS) { + /* 查询失败,检查本地socket状态 */ + if (g_socket_table_initialized) { + osal_mutex_lock(&g_socket_table_mutex); + n706a_tcp_socket_t *sock = g_tcp_sockets[socket_id]; + if (sock != NULL) { + is_ssl = sock->is_ssl; + } + osal_mutex_unlock(&g_socket_table_mutex); + } + } + char at_cmd[32]; n706a_at_response_t response; - /* 先尝试关闭普通TCP */ - snprintf(at_cmd, sizeof(at_cmd), "AT+TCPCLOSE=%d", socket_id); - n706a_at_error_t ret = n706a_at_send_command(at_cmd, "OK", 3000, &response); + /* 根据socket类型选择关闭命令 */ + if (is_ssl) { + /* SSL TCP连接,使用AT+SSLTCPCLOSE */ + PPP_DEBUG_DEBUG("Socket %d is SSL connection, using AT+SSLTCPCLOSE", socket_id); + snprintf(at_cmd, sizeof(at_cmd), "AT+SSLTCPCLOSE=%d", socket_id); + } else { + /* 普通TCP连接,使用AT+TCPCLOSE */ + PPP_DEBUG_DEBUG("Socket %d is TCP connection, using AT+TCPCLOSE", socket_id); + snprintf(at_cmd, sizeof(at_cmd), "AT+TCPCLOSE=%d", socket_id); + } + + n706a_at_error_t ret = n706a_at_send_command(at_cmd, NULL, 3000, &response); + + /* 检查响应中是否包含关闭URC(无论是 ERROR 还是 OK,都表示命令已确认) */ + bool close_confirmed = false; + if (ret == N706A_AT_OK && response.length > 0) { + char close_pattern[32]; + if (is_ssl) { + snprintf(close_pattern, sizeof(close_pattern), "+SSLTCPCLOSE: "); + } else { + snprintf(close_pattern, sizeof(close_pattern), "+TCPCLOSE: "); + } + + if (strstr(response.response, close_pattern) != NULL) { + /* 收到关闭URC,命令已确认 */ + close_confirmed = true; + PPP_DEBUG_DEBUG("Socket %d force close confirmed: received %s URC", socket_id, close_pattern); + } else if (strstr(response.response, "OK") != NULL && + strstr(response.response, "ERROR") == NULL) { + /* 没有关闭URC,但有 OK 且没有 ERROR,也认为成功 */ + PPP_DEBUG_DEBUG("Socket %d force close: received OK (no close URC)", socket_id); + close_confirmed = true; + } + } - if (ret == N706A_AT_OK && response.length > 0 && - strstr(response.response, "OK") && !strstr(response.response, "ERROR")) { - PPP_DEBUG_DEBUG("Socket %d force closed successfully (TCP)", socket_id); + if (close_confirmed) { + /* 等待一段时间让模块端完全清理socket状态(避免状态混乱) */ + osal_msleep(100); return N706A_TCP_SUCCESS; } - /* 如果普通TCP关闭失败,尝试关闭SSL TCP */ - snprintf(at_cmd, sizeof(at_cmd), "AT+SSLTCPCLOSE=%d", socket_id); - ret = n706a_at_send_command(at_cmd, "OK", 3000, &response); + /* 如果第一次关闭失败,尝试另一种关闭方式(可能是状态判断错误) */ + if (is_ssl) { + /* SSL关闭失败,尝试普通TCP关闭 */ + PPP_DEBUG_WARN("Socket %d SSL close failed, trying TCP close", socket_id); + snprintf(at_cmd, sizeof(at_cmd), "AT+TCPCLOSE=%d", socket_id); + } else { + /* TCP关闭失败,尝试SSL TCP关闭 */ + PPP_DEBUG_WARN("Socket %d TCP close failed, trying SSL TCP close", socket_id); + snprintf(at_cmd, sizeof(at_cmd), "AT+SSLTCPCLOSE=%d", socket_id); + } - if (ret == N706A_AT_OK && response.length > 0 && - strstr(response.response, "OK") && !strstr(response.response, "ERROR")) { - PPP_DEBUG_DEBUG("Socket %d force closed successfully (SSL TCP)", socket_id); - return N706A_TCP_SUCCESS; + ret = n706a_at_send_command(at_cmd, NULL, 3000, &response); + + if (ret == N706A_AT_OK && response.length > 0) { + char close_pattern[32]; + if (is_ssl) { + snprintf(close_pattern, sizeof(close_pattern), "+TCPCLOSE: "); + } else { + snprintf(close_pattern, sizeof(close_pattern), "+SSLTCPCLOSE: "); + } + + if (strstr(response.response, close_pattern) != NULL || + (strstr(response.response, "OK") != NULL && strstr(response.response, "ERROR") == NULL)) { + PPP_DEBUG_DEBUG("Socket %d force close confirmed via alternative method", socket_id); + osal_msleep(100); + return N706A_TCP_SUCCESS; + } } /* 即使关闭命令失败,也返回成功(模块端可能已经关闭) */ @@ -281,42 +531,348 @@ static n706a_tcp_result_t n706a_tcp_force_close_socket_by_id(int socket_id) } /** - * @brief 查找空闲的socket ID(公共API,供HTTP和WebSocket使用) + * @brief 查找空闲的socket ID(通过AT命令查询模块端状态) * @return 空闲的socket ID (0-5),如果没有空闲则返回-1 */ int n706a_tcp_find_free_socket_id(void) { - if (!g_socket_table_initialized) { - return 0; // 如果表未初始化,返回0(默认) + /* 遍历所有socket ID,通过AT+IPSTATUS查询模块端状态 */ + for (int i = 0; i < N706A_MAX_SOCKETS; i++) { + bool is_connected = false; + bool is_ssl = false; + n706a_tcp_result_t query_ret = n706a_tcp_query_socket_status(i, &is_connected, &is_ssl); + + if (query_ret == N706A_TCP_SUCCESS) { + /* 查询成功,检查socket是否已断开 */ + if (!is_connected) { + /* 模块端socket已断开,可以使用 */ + PPP_DEBUG_DEBUG("Found free socket ID: %d (DISCONNECTED at module side)", i); + return i; + } else { + /* socket仍在使用中,跳过 */ + PPP_DEBUG_DEBUG("Socket %d is in use (CONNECTED at module side)", i); + continue; + } + } else { + /* 查询失败(可能是ERROR响应),检查本地状态 */ + if (g_socket_table_initialized) { + osal_mutex_lock(&g_socket_table_mutex); + n706a_tcp_socket_t *sock = g_tcp_sockets[i]; + if (sock == NULL || sock->state == N706A_TCP_STATE_CLOSED) { + /* 本地socket不存在或已关闭,可以使用 */ + osal_mutex_unlock(&g_socket_table_mutex); + PPP_DEBUG_DEBUG("Found free socket ID: %d (not in local table or CLOSED)", i); + return i; + } + osal_mutex_unlock(&g_socket_table_mutex); + } else { + /* 表未初始化,认为socket可用 */ + PPP_DEBUG_DEBUG("Found free socket ID: %d (table not initialized)", i); + return i; + } + } } - osal_mutex_lock(&g_socket_table_mutex); + /* 没有找到空闲的socket */ + PPP_DEBUG_WARN("No free socket ID available (all sockets are in use)"); + return -1; +} + +/** + * @brief 初始化+TCPSEND URC等待机制(内部函数) + */ +static void n706a_tcp_init_tcpsend_urc_wait(void) +{ + static bool initialized = false; + if (initialized) { + PPP_DEBUG_DEBUG("+TCPSEND URC wait mechanism already initialized, skipping"); + return; + } - /* 查找空闲的socket ID(表中没有注册的socket) */ + osal_mutex_init(&g_tcpsend_urc_mutex); for (int i = 0; i < N706A_MAX_SOCKETS; i++) { - if (g_tcp_sockets[i] == NULL) { - osal_mutex_unlock(&g_socket_table_mutex); - PPP_DEBUG_DEBUG("Found free socket ID: %d", i); - return i; + int sem_ret = osal_sem_init(&g_tcpsend_urc_sems[i], 0); + if (sem_ret != OSAL_SUCCESS) { + PPP_DEBUG_ERROR("Failed to initialize +TCPSEND URC semaphore for socket %d (ret=0x%x)", i, sem_ret); + // 继续初始化其他socket,即使某些失败 + // 注意:如果信号量创建失败,URC回调仍然会被调用,但等待线程无法被唤醒 + // 这会导致超时,但不会影响URC回调的执行 + } else { + PPP_DEBUG_DEBUG("+TCPSEND URC semaphore initialized for socket %d", i); } + g_tcpsend_urc_sent_bytes[i] = 0; + g_tcpsend_urc_received[i] = false; + } + initialized = true; + PPP_DEBUG_DEBUG("+TCPSEND URC wait mechanism initialized (total %d semaphores)", N706A_MAX_SOCKETS); +} + +/** + * @brief 提前初始化+TCPSEND URC等待机制(公共函数,供n706a_net_init调用) + */ +void n706a_tcp_preinit_tcpsend_urc_wait(void) +{ + n706a_tcp_init_tcpsend_urc_wait(); +} + +/** + * @brief +TCPSEND URC回调(由n706a_at_handler调用) + * @param socket_id Socket ID + * @param sent_bytes 实际发送的字节数 + * + * 注意:此函数由n706a_at_handler.c通过URC回调调用,不是应用层API + */ +void n706a_tcp_on_tcpsend_urc(int socket_id, size_t sent_bytes) +{ + if (socket_id < 0 || socket_id >= N706A_MAX_SOCKETS) { + PPP_DEBUG_ERROR("+TCPSEND URC callback: invalid socket_id=%d", socket_id); + return; } - osal_mutex_unlock(&g_socket_table_mutex); + PPP_DEBUG_INFO("+TCPSEND URC callback called: socket=%d, sent_bytes=%u", socket_id, (unsigned int)sent_bytes); - /* 如果所有socket都被占用,尝试查找状态为CLOSED的socket */ - osal_mutex_lock(&g_socket_table_mutex); - for (int i = 0; i < N706A_MAX_SOCKETS; i++) { - if (g_tcp_sockets[i] != NULL && - g_tcp_sockets[i]->state == N706A_TCP_STATE_CLOSED) { + osal_mutex_lock(&g_tcpsend_urc_mutex); + g_tcpsend_urc_sent_bytes[socket_id] = sent_bytes; + g_tcpsend_urc_received[socket_id] = true; + osal_mutex_unlock(&g_tcpsend_urc_mutex); + + /* 通知等待的线程 */ + osal_sem_up(&g_tcpsend_urc_sems[socket_id]); + PPP_DEBUG_INFO("+TCPSEND URC: Semaphore signaled for socket %d", socket_id); + + PPP_DEBUG_INFO("+TCPSEND URC processed: socket=%d, sent_bytes=%u", socket_id, (unsigned int)sent_bytes); +} + +/** + * @brief 等待+TCPSEND URC + * @param socket_id 等待的socket ID + * @param timeout_ms 超时时间(毫秒) + * @param expected_bytes 期望发送的字节数 + * @param sent_bytes 输出:实际发送的字节数 + * @return true=收到URC,false=超时 + */ +static bool n706a_tcp_wait_for_tcpsend_urc(int socket_id, uint32_t timeout_ms, size_t expected_bytes, size_t *sent_bytes) +{ + if (!sent_bytes || socket_id < 0 || socket_id >= N706A_MAX_SOCKETS) { + return false; + } + + /* 初始化等待机制(首次调用时) */ + n706a_tcp_init_tcpsend_urc_wait(); + + /* 清除之前的结果 */ + osal_mutex_lock(&g_tcpsend_urc_mutex); + g_tcpsend_urc_sent_bytes[socket_id] = 0; + g_tcpsend_urc_received[socket_id] = false; + osal_mutex_unlock(&g_tcpsend_urc_mutex); + + /* 等待URC信号量(超时时间) */ + PPP_DEBUG_INFO("Waiting for +TCPSEND URC: socket=%d, timeout=%u ms, expected_bytes=%u", + socket_id, timeout_ms, (unsigned int)expected_bytes); + + /* 检查信号量是否有效(如果创建失败,sem可能无效) */ + /* 注意:即使信号量创建失败,我们也应该继续等待,因为URC回调可能仍然会被调用 */ + /* 如果信号量无效,我们需要使用轮询方式检查URC是否已经到达 */ + + /* 在等待之前,先检查URC是否已经到达(可能在发送数据之前就到了) */ + osal_mutex_lock(&g_tcpsend_urc_mutex); + bool already_received = g_tcpsend_urc_received[socket_id]; + if (already_received) { + *sent_bytes = g_tcpsend_urc_sent_bytes[socket_id]; + osal_mutex_unlock(&g_tcpsend_urc_mutex); + PPP_DEBUG_INFO("+TCPSEND URC already received before waiting: socket=%d, sent_bytes=%u (expected=%u)", + socket_id, (unsigned int)*sent_bytes, (unsigned int)expected_bytes); + return true; + } + osal_mutex_unlock(&g_tcpsend_urc_mutex); + + /* 使用轮询+信号量的混合方式等待URC(因为信号量可能无效或URC可能延迟) */ + uint32_t start_time = (uint32_t)osal_get_jiffies(); + uint32_t elapsed_ms = 0; + uint32_t poll_interval_ms = 100; // 每100ms轮询一次 + bool urc_received = false; + + /* 用于检测累积缓冲区变化的变量 */ + uint32_t last_accumulate_len = 0; + uint8_t last_accumulate_preview[64] = {0}; + uint32_t last_accumulate_preview_len = 0; + uint32_t last_check_time_ms = 0; + uint32_t check_interval_ms = 1000; // 每1秒检查一次变化 + + while (elapsed_ms < timeout_ms && !urc_received) { + /* 尝试等待信号量(使用较短的超时时间,以便定期轮询) */ + uint32_t wait_time = (timeout_ms - elapsed_ms < poll_interval_ms) ? + (timeout_ms - elapsed_ms) : poll_interval_ms; + + int ret = osal_sem_down_timeout(&g_tcpsend_urc_sems[socket_id], wait_time); + + /* 检查URC是否已经到达(无论信号量是否成功) */ + osal_mutex_lock(&g_tcpsend_urc_mutex); + if (g_tcpsend_urc_received[socket_id]) { + *sent_bytes = g_tcpsend_urc_sent_bytes[socket_id]; + urc_received = true; + osal_mutex_unlock(&g_tcpsend_urc_mutex); + + if (ret == OSAL_SUCCESS) { + PPP_DEBUG_INFO("+TCPSEND URC received via semaphore: socket=%d, sent_bytes=%u (expected=%u, elapsed=%u ms)", + socket_id, (unsigned int)*sent_bytes, (unsigned int)expected_bytes, elapsed_ms); + } else { + PPP_DEBUG_INFO("+TCPSEND URC received (polling check): socket=%d, sent_bytes=%u (expected=%u, elapsed=%u ms, sem_ret=%d)", + socket_id, (unsigned int)*sent_bytes, (unsigned int)expected_bytes, elapsed_ms, ret); + } + return true; + } + osal_mutex_unlock(&g_tcpsend_urc_mutex); + + /* 更新已用时间 */ + uint32_t current_time = (uint32_t)osal_get_jiffies(); + elapsed_ms = osal_jiffies_to_msecs(current_time - start_time); + + /* 检查socket状态,如果已关闭则立即返回 */ + osal_mutex_lock(&g_socket_table_mutex); + n706a_tcp_socket_t *sock = g_tcp_sockets[socket_id]; + if (sock && sock->state == N706A_TCP_STATE_CLOSED) { osal_mutex_unlock(&g_socket_table_mutex); - PPP_DEBUG_DEBUG("Found closed socket ID: %d", i); - return i; + PPP_DEBUG_WARN("+TCPSEND URC wait: Socket %d found CLOSED, stopping wait early (elapsed=%u ms)", + socket_id, elapsed_ms); + return false; /* 连接已关闭,URC不会到达 */ + } + osal_mutex_unlock(&g_socket_table_mutex); + + /* 每1秒检查一次累积缓冲区的变化 */ + if (elapsed_ms - last_check_time_ms >= check_interval_ms) { + last_check_time_ms = elapsed_ms; + + // 检查累积缓冲区是否有数据(可能包含+TCPSEND URC但还未被处理) + extern uint32_t n706a_at_get_accumulate_buffer_length(void); + extern bool n706a_at_get_accumulate_buffer_preview(uint8_t *buffer, uint32_t max_len, uint32_t *actual_len); + extern bool n706a_at_get_accumulate_buffer_all(uint8_t *buffer, uint32_t max_len, uint32_t *actual_len); + + uint32_t accumulate_len = n706a_at_get_accumulate_buffer_length(); + + // 检查是否有变化(长度变化或内容变化) + bool has_changed = false; + if (accumulate_len != last_accumulate_len) { + has_changed = true; + } else if (accumulate_len > 0) { + // 长度相同,检查内容是否变化 + uint8_t current_preview[64] = {0}; + uint32_t current_preview_len = 0; + if (n706a_at_get_accumulate_buffer_preview(current_preview, 64, ¤t_preview_len)) { + if (current_preview_len != last_accumulate_preview_len || + memcmp(current_preview, last_accumulate_preview, current_preview_len) != 0) { + has_changed = true; + } + } + } + + if (has_changed || accumulate_len > 0) { + // 获取完整的累积缓冲区内容(用于分析) + // 优化:使用静态缓冲区,减少栈使用(避免在软件定时器任务中栈溢出) + static uint8_t full_buffer[256] = {0}; // 静态缓冲区,避免栈分配 + uint32_t full_len = 0; + if (n706a_at_get_accumulate_buffer_all(full_buffer, 256, &full_len)) { + // 打印完整的累积缓冲区内容(十六进制和ASCII) + // 优化:使用静态缓冲区和查表法,减少栈使用 + static char hex_str[768] = {0}; // 静态缓冲区,避免栈分配 + static char ascii_str[257] = {0}; // 静态缓冲区,避免栈分配 + memset(hex_str, 0, sizeof(hex_str)); + memset(ascii_str, 0, sizeof(ascii_str)); + + /* 使用查表法替代snprintf,减少栈使用 */ + static const char hex_chars[] = "0123456789ABCDEF"; + uint32_t ascii_idx = 0; + for (uint32_t i = 0; i < full_len && i < 256; i++) { + /* HEX转换:使用查表法 */ + if (i * 3 + 3 < sizeof(hex_str)) { + hex_str[i * 3] = hex_chars[(full_buffer[i] >> 4) & 0x0F]; + hex_str[i * 3 + 1] = hex_chars[full_buffer[i] & 0x0F]; + hex_str[i * 3 + 2] = ' '; + } + /* ASCII转换 */ + if (full_buffer[i] >= 32 && full_buffer[i] < 127) { + if (ascii_idx < 256) ascii_str[ascii_idx++] = full_buffer[i]; + } else if (full_buffer[i] == '\r') { + if (ascii_idx < 255) { + ascii_str[ascii_idx++] = '\\'; + ascii_str[ascii_idx++] = 'r'; + } + } else if (full_buffer[i] == '\n') { + if (ascii_idx < 255) { + ascii_str[ascii_idx++] = '\\'; + ascii_str[ascii_idx++] = 'n'; + } + } else { + if (ascii_idx < 256) ascii_str[ascii_idx++] = '.'; + } + } + ascii_str[ascii_idx < 256 ? ascii_idx : 256] = '\0'; + + PPP_DEBUG_INFO("Still waiting for +TCPSEND URC: socket=%d, elapsed=%u ms, timeout=%u ms", + socket_id, elapsed_ms, timeout_ms); + PPP_DEBUG_INFO(" [累积缓冲区变化检测] 长度=%u 字节 (上次=%u), 完整内容:", + accumulate_len, last_accumulate_len); + PPP_DEBUG_INFO(" HEX: %s", hex_str); + PPP_DEBUG_INFO(" ASCII: %s", ascii_str); + + // 检查是否包含+TCPSEND相关字符串 + const char *buffer_str = (const char *)full_buffer; + if (strstr(buffer_str, "TCPSEND") != NULL) { + PPP_DEBUG_WARN(" ⚠️ 累积缓冲区包含 'TCPSEND' 字符串!可能包含+TCPSEND URC但未被识别"); + } + if (strstr(buffer_str, "OPERATION") != NULL || strstr(buffer_str, "EXPIRED") != NULL) { + PPP_DEBUG_WARN(" ⚠️ 累积缓冲区包含 'OPERATION' 或 'EXPIRED' 字符串!可能是错误响应"); + } + } else { + // 如果无法获取完整内容,至少打印预览 + static uint8_t preview_bytes[32] = {0}; // 静态缓冲区,避免栈分配 + static char hex_str[96] = {0}; // 静态缓冲区,避免栈分配 + memset(preview_bytes, 0, sizeof(preview_bytes)); + memset(hex_str, 0, sizeof(hex_str)); + uint32_t preview_len = 0; + if (n706a_at_get_accumulate_buffer_preview(preview_bytes, 32, &preview_len)) { + /* 使用查表法替代snprintf,减少栈使用 */ + static const char hex_chars[] = "0123456789ABCDEF"; + for (uint32_t i = 0; i < preview_len && i * 3 + 3 < sizeof(hex_str); i++) { + hex_str[i * 3] = hex_chars[(preview_bytes[i] >> 4) & 0x0F]; + hex_str[i * 3 + 1] = hex_chars[preview_bytes[i] & 0x0F]; + hex_str[i * 3 + 2] = ' '; + } + PPP_DEBUG_INFO("Still waiting for +TCPSEND URC: socket=%d, elapsed=%u ms, timeout=%u ms, accumulate_buffer=%u bytes, last %u bytes: %s", + socket_id, elapsed_ms, timeout_ms, accumulate_len, preview_len, hex_str); + } + } + + // 更新上次的值 + last_accumulate_len = accumulate_len; + if (accumulate_len > 0) { + uint8_t current_preview[64] = {0}; + uint32_t current_preview_len = 0; + if (n706a_at_get_accumulate_buffer_preview(current_preview, 64, ¤t_preview_len)) { + memcpy(last_accumulate_preview, current_preview, current_preview_len); + last_accumulate_preview_len = current_preview_len; + } + } else { + memset(last_accumulate_preview, 0, sizeof(last_accumulate_preview)); + last_accumulate_preview_len = 0; + } + } else { + // 没有变化,只打印简单的等待信息 + PPP_DEBUG_INFO("Still waiting for +TCPSEND URC: socket=%d, elapsed=%u ms, timeout=%u ms (累积缓冲区无变化,长度=%u)", + socket_id, elapsed_ms, timeout_ms, accumulate_len); + } + } + + /* 如果信号量等待失败且不是超时,记录警告 */ + if (ret != OSAL_SUCCESS && ret != OSAL_ETIME) { + PPP_DEBUG_WARN("+TCPSEND URC semaphore wait failed: ret=%d (0x%x), elapsed=%u ms", ret, ret, elapsed_ms); } } - osal_mutex_unlock(&g_socket_table_mutex); - PPP_DEBUG_WARN("No free socket ID found, will use socket 0"); - return -1; // 没有空闲socket,返回-1表示失败 + /* 超时或失败 */ + PPP_DEBUG_WARN("+TCPSEND URC wait timeout for socket %d (timeout=%u ms, elapsed=%u ms, expected_bytes=%u)", + socket_id, timeout_ms, elapsed_ms, (unsigned int)expected_bytes); + return false; } /** @@ -326,16 +882,23 @@ static void n706a_tcp_init_tcpsetup_urc_wait(void) { static bool initialized = false; if (initialized) { + PPP_DEBUG_DEBUG("+TCPSETUP URC wait mechanism already initialized, skipping"); return; } osal_mutex_init(&g_tcpsetup_urc_mutex); for (int i = 0; i < N706A_MAX_SOCKETS; i++) { - osal_sem_init(&g_tcpsetup_urc_sems[i], 0); + int sem_ret = osal_sem_init(&g_tcpsetup_urc_sems[i], 0); + if (sem_ret != OSAL_SUCCESS) { + PPP_DEBUG_ERROR("Failed to initialize +TCPSETUP URC semaphore for socket %d (ret=0x%x)", i, sem_ret); + // 继续初始化其他socket,即使某些失败 + } else { + PPP_DEBUG_DEBUG("+TCPSETUP URC semaphore initialized for socket %d", i); + } g_tcpsetup_urc_results[i] = false; } initialized = true; - PPP_DEBUG_DEBUG("+TCPSETUP URC wait mechanism initialized"); + PPP_DEBUG_DEBUG("+TCPSETUP URC wait mechanism initialized (total %d semaphores)", N706A_MAX_SOCKETS); } /** @@ -449,8 +1012,8 @@ n706a_tcp_result_t n706a_tcp_connect(n706a_tcp_socket_t *sock, } } - /* 额外检查:即使状态是CLOSED,也强制关闭模块端的socket(防止模块端状态不一致) */ - PPP_DEBUG_DEBUG("Force closing socket %d at module side before new connection", sock->socket_id); + /* 额外检查:检查模块端socket状态,如果需要则关闭(防止模块端状态不一致) */ + PPP_DEBUG_DEBUG("Checking socket %d status at module side before new connection", sock->socket_id); n706a_tcp_force_close_socket_by_id(sock->socket_id); /* 等待模块端完全清理socket状态(避免状态混乱导致连接失败) */ @@ -482,7 +1045,7 @@ n706a_tcp_result_t n706a_tcp_connect(n706a_tcp_socket_t *sock, PPP_DEBUG_CRITICAL("[N706A_TCP] AT+SSLTCPSETUP response: ret=%d, len=%u", ret, response.length); if (response.length > 0) { - PPP_DEBUG_CRITICAL("[N706A_TCP] Response: %s", response.response); + n706a_tcp_safe_print_response(&response, 256); } /* ========== 方案3:验证+TCPSETUP URC响应 ========== */ @@ -533,8 +1096,12 @@ n706a_tcp_result_t n706a_tcp_connect(n706a_tcp_socket_t *sock, return N706A_TCP_ECONNREF; } } else { - PPP_DEBUG_ERROR("[N706A_TCP] SSL TCP connection failed! Response: %s", - response.length > 0 ? response.response : "(empty)"); + PPP_DEBUG_ERROR("[N706A_TCP] SSL TCP connection failed! Response: "); + if (response.length > 0) { + n706a_tcp_safe_print_response(&response, 256); + } else { + PPP_DEBUG_ERROR("(empty)"); + } /* ========== 方案2:如果当前socket连接失败,尝试使用其他socket ID ========== */ int free_socket_id = n706a_tcp_find_free_socket_id(); @@ -587,56 +1154,106 @@ n706a_tcp_result_t n706a_tcp_connect(n706a_tcp_socket_t *sock, PPP_DEBUG_CRITICAL("[N706A_TCP] AT+TCPSETUP response: ret=%d, len=%u", ret, response.length); if (response.length > 0) { - PPP_DEBUG_CRITICAL("[N706A_TCP] Response: %s", response.response); + n706a_tcp_safe_print_response(&response, 256); } /* ========== 方案3:验证+TCPSETUP URC响应 ========== */ - /* 检查响应是否包含OK且不包含ERROR */ - if (ret == N706A_AT_OK && response.length > 0 && - strstr(response.response, "OK") && - !strstr(response.response, "ERROR")) { + /* 检查响应是否包含OK */ + /* 注意:响应中可能包含其他命令的响应(如+TCPCLOSE: ERROR), + * 需要检查AT+TCPSETUP命令本身的响应,而不是整个响应块 */ + if (ret == N706A_AT_OK && response.length > 0) { + /* 查找AT+TCPSETUP命令在响应中的位置 */ + char cmd_pattern[64]; + snprintf(cmd_pattern, sizeof(cmd_pattern), "AT+TCPSETUP=%d,", sock->socket_id); + const char *cmd_pos = strstr(response.response, cmd_pattern); - /* 进一步验证:检查是否有+TCPSETUP URC(可能在响应中,也可能是异步URC) */ - bool urc_ok = false; + /* 检查AT+TCPSETUP命令后是否有OK响应 */ + bool has_ok_after_cmd = false; + bool has_error_after_cmd = false; - /* 检查响应中是否包含+TCPSETUP: n,OK */ - char urc_pattern[32]; - snprintf(urc_pattern, sizeof(urc_pattern), "+TCPSETUP: %d,OK", sock->socket_id); - if (strstr(response.response, urc_pattern)) { - urc_ok = true; - PPP_DEBUG_CRITICAL("[N706A_TCP] Found +TCPSETUP: %d,OK in response", sock->socket_id); + if (cmd_pos) { + /* 在AT+TCPSETUP命令后查找OK或ERROR */ + const char *search_start = cmd_pos; + const char *ok_pos = strstr(search_start, "OK"); + const char *error_pos = strstr(search_start, "ERROR"); + + /* 检查OK是否在AT+TCPSETUP命令之后 */ + if (ok_pos && ok_pos > cmd_pos) { + /* 检查OK前面是否有+TCPSETUP: n,FAIL(表示连接失败) */ + char fail_pattern[32]; + snprintf(fail_pattern, sizeof(fail_pattern), "+TCPSETUP: %d,FAIL", sock->socket_id); + if (strstr(search_start, fail_pattern)) { + has_error_after_cmd = true; + } else { + has_ok_after_cmd = true; + } + } + + /* 检查ERROR是否在AT+TCPSETUP命令之后,且是+TCPSETUP相关的ERROR */ + if (error_pos && error_pos > cmd_pos) { + char error_pattern[32]; + snprintf(error_pattern, sizeof(error_pattern), "+TCPSETUP: %d,FAIL", sock->socket_id); + if (strstr(search_start, error_pattern)) { + has_error_after_cmd = true; + } + } } else { - /* 检查是否有+TCPSETUP: n,FAIL */ - snprintf(urc_pattern, sizeof(urc_pattern), "+TCPSETUP: %d,FAIL", sock->socket_id); + /* 如果找不到AT+TCPSETUP命令,检查整个响应是否包含OK */ + if (strstr(response.response, "OK")) { + has_ok_after_cmd = true; + } + } + + if (has_error_after_cmd) { + PPP_DEBUG_ERROR("[N706A_TCP] Found +TCPSETUP: %d,FAIL in response", sock->socket_id); + sock->state = N706A_TCP_STATE_ERROR; + return N706A_TCP_ECONNREF; + } + + if (has_ok_after_cmd) { + /* 进一步验证:检查是否有+TCPSETUP URC(可能在响应中,也可能是异步URC) */ + bool urc_ok = false; + + /* 检查响应中是否包含+TCPSETUP: n,OK */ + char urc_pattern[32]; + snprintf(urc_pattern, sizeof(urc_pattern), "+TCPSETUP: %d,OK", sock->socket_id); if (strstr(response.response, urc_pattern)) { - PPP_DEBUG_ERROR("[N706A_TCP] Found +TCPSETUP: %d,FAIL in response", sock->socket_id); - sock->state = N706A_TCP_STATE_ERROR; - return N706A_TCP_ECONNREF; + urc_ok = true; + PPP_DEBUG_CRITICAL("[N706A_TCP] Found +TCPSETUP: %d,OK in response", sock->socket_id); + } else { + /* 如果没有找到明确的URC,等待一下看是否有异步URC到达 */ + bool urc_success = false; + if (n706a_tcp_wait_for_tcpsetup_urc(sock->socket_id, 500, &urc_success)) { + urc_ok = urc_success; + } else { + /* 超时,但命令返回OK,认为连接成功(可能是URC还没到达) */ + PPP_DEBUG_WARN("[N706A_TCP] +TCPSETUP URC wait timeout, but command returned OK, assuming success"); + urc_ok = true; + } } - /* 如果没有找到明确的URC,等待一下看是否有异步URC到达 */ - bool urc_success = false; - if (n706a_tcp_wait_for_tcpsetup_urc(sock->socket_id, 500, &urc_success)) { - urc_ok = urc_success; + if (urc_ok) { + PPP_DEBUG_CRITICAL("[N706A_TCP] TCP connection established successfully!"); + sock->state = N706A_TCP_STATE_CONNECTED; + return N706A_TCP_SUCCESS; } else { - /* 超时,但命令返回OK,认为连接成功(可能是URC还没到达) */ - PPP_DEBUG_WARN("[N706A_TCP] +TCPSETUP URC wait timeout, but command returned OK, assuming success"); - urc_ok = true; + PPP_DEBUG_ERROR("[N706A_TCP] TCP connection failed: URC indicates FAIL"); + sock->state = N706A_TCP_STATE_ERROR; + return N706A_TCP_ECONNREF; } - } - - if (urc_ok) { - PPP_DEBUG_CRITICAL("[N706A_TCP] TCP connection established successfully!"); - sock->state = N706A_TCP_STATE_CONNECTED; - return N706A_TCP_SUCCESS; } else { - PPP_DEBUG_ERROR("[N706A_TCP] TCP connection failed: URC indicates FAIL"); + /* AT+TCPSETUP命令后没有OK响应 */ + PPP_DEBUG_ERROR("[N706A_TCP] AT+TCPSETUP command did not return OK"); sock->state = N706A_TCP_STATE_ERROR; return N706A_TCP_ECONNREF; } } else { - PPP_DEBUG_ERROR("[N706A_TCP] TCP connection failed! Response: %s", - response.length > 0 ? response.response : "(empty)"); + PPP_DEBUG_ERROR("[N706A_TCP] TCP connection failed! Response: "); + if (response.length > 0) { + n706a_tcp_safe_print_response(&response, 256); + } else { + PPP_DEBUG_ERROR("(empty)"); + } /* ========== 方案2:如果当前socket连接失败,尝试使用其他socket ID ========== */ /* 检查是否有空闲的socket ID可以使用 */ @@ -695,7 +1312,7 @@ n706a_tcp_result_t n706a_tcp_connect(n706a_tcp_socket_t *sock, PPP_DEBUG_CRITICAL("[N706A_TCP] AT+TCPSETUP response: ret=%d, len=%u", ret, response.length); if (response.length > 0) { - PPP_DEBUG_CRITICAL("[N706A_TCP] Response: %s", response.response); + n706a_tcp_safe_print_response(&response, 256); } /* ========== 方案3:验证+TCPSETUP URC响应 ========== */ @@ -915,9 +1532,237 @@ n706a_tcp_result_t n706a_tcp_connect(n706a_tcp_socket_t *sock, } /** - * @brief 发送TCP数据 + * @brief 单次TCP数据发送(内部辅助函数) + * @param sock TCP socket指针 + * @param data 要发送的数据 + * @param len 要发送的数据长度 + * - HEX模式:必须 <= 2048字节(二进制数据长度) + * - ASCII模式:必须 <= 4096字节 + * @param sent 实际发送的字节数(输出,二进制数据长度) + * @return 操作结果 */ -n706a_tcp_result_t n706a_tcp_send(n706a_tcp_socket_t *sock, +static n706a_tcp_result_t n706a_tcp_send_single(n706a_tcp_socket_t *sock, + const uint8_t *data, + size_t len, + size_t *sent) +{ + if (!sock || !data || len == 0 || !sent) { + return N706A_TCP_EINVAL; + } + + /* 检查是否使用HEX模式(根据AT+RECVMODE设置) */ + bool hex_mode = n706a_at_get_tcp_recv_hex_mode(); + + /* 检查长度限制 */ + size_t max_len = hex_mode ? 2048 : 4096; /* HEX模式:2048字节,ASCII模式:4096字节 */ + if (len > max_len) { + PPP_DEBUG_ERROR("[TCP_SEND] Requested length %u exceeds max (%u for %s mode)", + (unsigned int)len, (unsigned int)max_len, hex_mode ? "HEX" : "ASCII"); + return N706A_TCP_EINVAL; + } + + char at_cmd[64]; + n706a_at_response_t response; + + /* 如果使用HEX模式,需要将二进制数据转换为HEX字符串 */ + uint8_t *send_data = (uint8_t *)data; + size_t send_len = len; + char *hex_buffer = NULL; + + if (hex_mode) { + /* 分配HEX字符串缓冲区(二进制长度 * 2) */ + size_t hex_str_len = len * 2; + hex_buffer = (char *)malloc(hex_str_len); + if (!hex_buffer) { + PPP_DEBUG_ERROR("Failed to allocate HEX buffer (%u bytes)", (unsigned int)hex_str_len); + return N706A_TCP_ENOBUFS; + } + + /* 将二进制数据转换为HEX字符串 */ + static const char hex_chars[] = "0123456789ABCDEF"; + size_t i; + for (i = 0; i < len; i++) { + hex_buffer[i * 2] = hex_chars[(data[i] >> 4) & 0x0F]; + hex_buffer[i * 2 + 1] = hex_chars[data[i] & 0x0F]; + } + + send_data = (uint8_t *)hex_buffer; + send_len = hex_str_len; + + PPP_DEBUG_VERBOSE("[TCP_SEND] TCP send: binary_len=%u -> hex_len=%u (HEX mode)", + (unsigned int)len, (unsigned int)send_len); + } + +#ifdef N706A_SSL_USE_AT_COMMAND + if (sock->is_ssl) { + /* SSL TCP发送命令格式: AT+SSLTCPSEND=, + * 注意:根据AT命令手册和Python验证代码: + * 1. SSL TCP没有HEX模式参数(,,1),HEX模式通过AT+RECVMODE设置 + * 2. length参数是指二进制数据的字节数,而不是HEX字符串长度 + * 示例:AT+SSLTCPSEND=0,1024 表示发送1024字节的二进制数据 + */ + snprintf(at_cmd, sizeof(at_cmd), "AT+SSLTCPSEND=%d,%zu", + sock->socket_id, len); + } else +#endif + { + if (hex_mode) { + /* Buffer模式,HEX模式:AT+TCPSEND=,,,1 + * 注意:根据AT命令手册和Python验证代码,length参数是指二进制数据的字节数,而不是HEX字符串长度 + * 示例:AT+TCPSEND=0,1024,,1 表示发送1024字节的二进制数据(对应2048个HEX字符) + */ + snprintf(at_cmd, sizeof(at_cmd), "AT+TCPSEND=%d,%zu,,1", + sock->socket_id, len); + } else { + /* Buffer模式,ASCII模式(默认,与Python代码一致):AT+TCPSEND=, */ + snprintf(at_cmd, sizeof(at_cmd), "AT+TCPSEND=%d,%zu", + sock->socket_id, len); + } + } + + /* 发送 AT+TCPSEND 命令,期望收到 '>' 提示符 */ + n706a_at_error_t ret = n706a_at_send_command(at_cmd, ">", 5000, &response); + if (ret != N706A_AT_OK) { + if (hex_buffer) { + free(hex_buffer); + } + /* 检查响应中是否包含 "SOCKET ID OPEN FAILED" 错误 */ + if (response.length > 0 && strstr(response.response, "SOCKET ID OPEN FAILED") != NULL) { + PPP_DEBUG_ERROR("Socket %d is closed (SOCKET ID OPEN FAILED), updating state to CLOSED", sock->socket_id); + sock->state = N706A_TCP_STATE_CLOSED; + return N706A_TCP_ENOTCONN; + } + PPP_DEBUG_ERROR("Failed to send %s command (expected '>' prompt not received)", at_cmd); + return N706A_TCP_ERROR; + } + + /* 检查响应中是否包含 "SOCKET ID OPEN FAILED" 错误(即使命令返回 OK) */ + if (response.length > 0 && strstr(response.response, "SOCKET ID OPEN FAILED") != NULL) { + if (hex_buffer) { + free(hex_buffer); + } + PPP_DEBUG_ERROR("Socket %d is closed (SOCKET ID OPEN FAILED in response), updating state to CLOSED", sock->socket_id); + sock->state = N706A_TCP_STATE_CLOSED; + return N706A_TCP_ENOTCONN; + } + + /* 验证成功,说明响应中已包含 '>' 提示符 */ + PPP_DEBUG_VERBOSE("[TCP_SEND] AT+TCPSEND command sent successfully, received '>' prompt"); + + /* 延迟50-100ms(根据AT命令手册要求,Python代码使用100ms) */ + PPP_DEBUG_VERBOSE("[TCP_SEND] Received '>' prompt, delaying 100ms before sending data..."); + osal_msleep(100); + + /* 发送实际数据(HEX模式下发送HEX字符串,ASCII模式下发送原始数据) */ + PPP_DEBUG_VERBOSE("[TCP_SEND] Sending TCP data (%u bytes as %s, original binary=%u bytes)...", + (unsigned int)send_len, hex_mode ? "HEX string" : "raw data", (unsigned int)len); + + /* 打印实际发送的数据内容(用于调试,详细模式) */ +#if PPP_DEBUG_VERBOSE_ENABLE + if (hex_mode) { + /* HEX模式:打印HEX字符串的前128个字符(64字节) */ + size_t print_len = (send_len > 128) ? 128 : send_len; + static char hex_preview[256] = {0}; /* 使用静态缓冲区 */ + memset(hex_preview, 0, sizeof(hex_preview)); + for (size_t i = 0; i < print_len && i < sizeof(hex_preview) - 1; i++) { + hex_preview[i] = send_data[i]; // HEX模式下,send_data是ASCII字符 + } + if (send_len > 128) { + PPP_DEBUG_VERBOSE("[TCP_SEND] HEX data (first 128 chars): %s... (total %u chars)", + hex_preview, (unsigned int)send_len); + } else { + PPP_DEBUG_VERBOSE("[TCP_SEND] HEX data: %s", hex_preview); + } + } else { + /* ASCII模式:打印原始二进制数据的HEX表示(前64字节) */ + size_t print_len = (send_len > 64) ? 64 : send_len; + static char hex_preview[256] = {0}; /* 使用静态缓冲区 */ + memset(hex_preview, 0, sizeof(hex_preview)); + /* 使用查表法替代snprintf,减少栈使用 */ + static const char hex_chars[] = "0123456789ABCDEF"; + for (size_t i = 0; i < print_len && i * 3 + 3 < sizeof(hex_preview); i++) { + hex_preview[i * 3] = hex_chars[(send_data[i] >> 4) & 0x0F]; + hex_preview[i * 3 + 1] = hex_chars[send_data[i] & 0x0F]; + hex_preview[i * 3 + 2] = ' '; + } + if (send_len > 64) { + PPP_DEBUG_VERBOSE("[TCP_SEND] Binary data (first 64 bytes, HEX): %s... (total %u bytes)", + hex_preview, (unsigned int)send_len); + } else { + PPP_DEBUG_VERBOSE("[TCP_SEND] Binary data (HEX): %s", hex_preview); + } + } +#endif + + ppp_error_t write_result = ppp_serial_write(send_data, send_len); + + /* 释放HEX缓冲区 */ + if (hex_buffer) { + free(hex_buffer); + } + + if (write_result != PPP_ERR_OK) { + PPP_DEBUG_ERROR("Failed to send TCP data via ppp_serial_write: error=%d", write_result); + return N706A_TCP_ERROR; + } + PPP_DEBUG_VERBOSE("[TCP_SEND] TCP data sent successfully (%u bytes as %s, original binary=%u bytes)", + (unsigned int)send_len, hex_mode ? "HEX string" : "raw data", (unsigned int)len); + + /* 等待发送完成响应(+TCPSEND: , 或 +TCPSEND: ERROR) */ + /* 按照Python代码逻辑:等待30秒,如果没收到URC就返回失败 */ + /* + * 注意:根据AT命令手册,+TCPSEND URC返回的length参数: + * - HEX模式:返回的是二进制数据长度(不是HEX字符串长度) + * - ASCII模式:返回的是二进制数据长度 + * 因此,无论哪种模式,期望值都应该是原始二进制数据长度(len) + */ + size_t actual_sent = 0; + size_t expected_urc_bytes = len; // 无论HEX还是ASCII模式,URC都返回二进制数据长度 + bool urc_received = n706a_tcp_wait_for_tcpsend_urc(sock->socket_id, 30000, expected_urc_bytes, &actual_sent); + + if (!urc_received) { + PPP_DEBUG_WARN("+TCPSEND URC wait timeout (30s), checking TCP connection state..."); + /* 超时后检查TCP连接状态,而不是立即判定为失败 */ + bool is_connected = false; + bool is_ssl = false; + n706a_tcp_result_t query_ret = n706a_tcp_query_socket_status(sock->socket_id, &is_connected, &is_ssl); + if (query_ret == N706A_TCP_SUCCESS) { + if (is_connected) { + /* 连接仍然有效,可能是URC丢失,但数据已发送成功 */ + PPP_DEBUG_WARN("+TCPSEND URC timeout but socket is still connected, assuming send succeeded"); + *sent = len; // 假设发送成功 + return N706A_TCP_SUCCESS; + } else { + /* 连接已断开 */ + PPP_DEBUG_ERROR("+TCPSEND URC timeout and socket is disconnected, send failed"); + sock->state = N706A_TCP_STATE_CLOSED; + return N706A_TCP_ENOTCONN; + } + } else { + /* 查询失败,保守处理:判定为发送失败 */ + PPP_DEBUG_ERROR("+TCPSEND URC timeout and failed to query socket status, send failed"); + return N706A_TCP_ERROR; + } + } + + /* 检查实际发送的字节数是否与期望一致 */ + /* 注意:根据AT命令手册,+TCPSEND URC返回的length是二进制数据长度,不是HEX字符串长度 */ + if (actual_sent != len) { + PPP_DEBUG_WARN("+TCPSEND URC: sent_bytes=%u (binary), expected=%u (binary), hex_str_len=%u", + (unsigned int)actual_sent, (unsigned int)len, (unsigned int)send_len); + /* 即使字节数不一致,也认为发送成功(可能是模块端的问题) */ + } + *sent = actual_sent; // 返回实际发送的二进制数据长度 + + PPP_DEBUG_VERBOSE("[TCP_SEND] TCP data send confirmed: %u bytes sent successfully (%s mode, URC returned binary length=%u)", + (unsigned int)*sent, hex_mode ? "HEX" : "ASCII", (unsigned int)actual_sent); + return N706A_TCP_SUCCESS; +} + +/** + * @brief 发送TCP数据(支持多次发送,确保发送完所有数据) + */ +n706a_tcp_result_t n706a_tcp_send(n706a_tcp_socket_t *sock, const uint8_t *data, size_t len, size_t *sent) @@ -933,6 +1778,7 @@ n706a_tcp_result_t n706a_tcp_send(n706a_tcp_socket_t *sock, #ifdef N706A_SSL_USE_MBEDTLS /* mbedtls模式:通过SSL发送 */ if (sock->is_ssl && sock->ssl_initialized) { + /* mbedtls会自动处理分片,不需要手动分片 */ int ret = mbedtls_ssl_write(&sock->ssl, data, len); if (ret > 0) { *sent = ret; @@ -944,52 +1790,519 @@ n706a_tcp_result_t n706a_tcp_send(n706a_tcp_socket_t *sock, #endif /* AT指令模式或普通TCP */ - /* 注意:使用HEX模式发送(mode=1),数据需要编码为HEX字符串 */ + /* 注意:根据AT命令手册,AT+TCPSEND的最大长度限制: + * - HEX模式:2048字节(二进制数据长度) + * - ASCII模式:4096字节 + * 如果数据超过限制,需要多次发送,直到发送完所有数据 + */ + + /* 检查是否使用HEX模式(根据AT+RECVMODE设置) */ + bool hex_mode = n706a_at_get_tcp_recv_hex_mode(); + size_t max_len = hex_mode ? 2048 : 4096; /* HEX模式:2048字节,ASCII模式:4096字节 */ + + /* 如果请求长度 <= 最大限制,直接调用单次发送 */ + if (len <= max_len) { + return n706a_tcp_send_single(sock, data, len, sent); + } + + /* 请求长度 > 最大限制,需要多次发送 */ + PPP_DEBUG_INFO("[TCP_SEND] Requested length %u exceeds AT+TCPSEND max (%u for %s mode), will perform multiple sends", + (unsigned int)len, (unsigned int)max_len, hex_mode ? "HEX" : "ASCII"); + + size_t total_sent = 0; + const uint8_t *current_data = data; + size_t remaining = len; + + /* 循环发送,每次最多发送max_len字节 */ + while (remaining > 0 && total_sent < len) { + size_t single_send_len = (remaining > max_len) ? max_len : remaining; + size_t single_sent = 0; + + n706a_tcp_result_t ret = n706a_tcp_send_single(sock, current_data, single_send_len, &single_sent); + + if (ret != N706A_TCP_SUCCESS) { + /* 发送失败,返回已发送的数据 */ + *sent = total_sent; + if (ret == N706A_TCP_ENOTCONN) { + /* 连接已关闭 */ + return N706A_TCP_ENOTCONN; + } + /* 其他错误,返回已发送的数据 */ + if (total_sent > 0) { + PPP_DEBUG_WARN("[TCP_SEND] Partial send completed: %u/%u bytes (error during send)", + (unsigned int)total_sent, (unsigned int)len); + return N706A_TCP_SUCCESS; + } + return ret; + } + + if (single_sent == 0) { + /* 没有数据被发送,可能连接已关闭 */ + *sent = total_sent; + if (total_sent > 0) { + PPP_DEBUG_WARN("[TCP_SEND] Partial send completed: %u/%u bytes (no data sent)", + (unsigned int)total_sent, (unsigned int)len); + return N706A_TCP_SUCCESS; + } + return N706A_TCP_ERROR; + } + + /* 累积已发送的数据 */ + total_sent += single_sent; + current_data += single_sent; + remaining -= single_sent; + + PPP_DEBUG_VERBOSE("[TCP_SEND] Multi-send progress: %u/%u bytes (remaining=%u)", + (unsigned int)total_sent, (unsigned int)len, (unsigned int)remaining); + + /* 如果本次发送的数据少于请求的长度,说明发送失败 */ + if (single_sent < single_send_len) { + PPP_DEBUG_WARN("[TCP_SEND] Multi-send partial: %u/%u bytes (expected %u, got %u)", + (unsigned int)total_sent, (unsigned int)len, + (unsigned int)single_send_len, (unsigned int)single_sent); + *sent = total_sent; + return N706A_TCP_SUCCESS; + } + } + + /* 所有数据发送完成 */ + *sent = total_sent; + PPP_DEBUG_INFO("[TCP_SEND] Multi-send completed successfully: %u/%u bytes", + (unsigned int)total_sent, (unsigned int)len); + return N706A_TCP_SUCCESS; +} + +/** + * @brief 单次TCP数据读取(内部辅助函数,最多读取2048字节) + * @param sock TCP socket指针 + * @param buffer 接收缓冲区 + * @param len 请求读取的长度(必须 <= 2048) + * @param received 实际读取的字节数(输出) + * @param timeout_ms 超时时间(毫秒) + * @return 操作结果 + */ +static n706a_tcp_result_t n706a_tcp_recv_single(n706a_tcp_socket_t *sock, + uint8_t *buffer, + size_t len, + size_t *received, + uint32_t timeout_ms) +{ + if (!sock || !buffer || len == 0 || !received || len > 2048) { + return N706A_TCP_EINVAL; + } + char at_cmd[64]; n706a_at_response_t response; + PPP_DEBUG_VERBOSE("[TCP_RECV] Calling AT+TCPREAD: sock->socket_id=%d, len=%u, timeout=%u ms", + sock->socket_id, (unsigned int)len, timeout_ms); + #ifdef N706A_SSL_USE_AT_COMMAND if (sock->is_ssl) { - /* HEX模式:AT+SSLTCPSEND=,,,1 */ - snprintf(at_cmd, sizeof(at_cmd), "AT+SSLTCPSEND=%d,%zu,,1", + snprintf(at_cmd, sizeof(at_cmd), "AT+SSLTCPREAD=%d,%zu", sock->socket_id, len); } else #endif { - /* HEX模式:AT+TCPSEND=,,,1 */ - snprintf(at_cmd, sizeof(at_cmd), "AT+TCPSEND=%d,%zu,,1", + snprintf(at_cmd, sizeof(at_cmd), "AT+TCPREAD=%d,%zu", sock->socket_id, len); } - /* 发送 AT+TCPSEND 命令 */ - n706a_at_error_t ret = n706a_at_send_command(at_cmd, NULL, 5000, &response); + n706a_at_error_t ret = n706a_at_send_command(at_cmd, NULL, timeout_ms, &response); if (ret != N706A_AT_OK) { - PPP_DEBUG_ERROR("Failed to send %s command", at_cmd); - return N706A_TCP_ERROR; - } - - /* 检查响应中是否包含 '>' 提示符(根据AT命令手册要求) */ - if (strstr((const char *)response.response, ">") == NULL) { - PPP_DEBUG_ERROR("Response does not contain '>' prompt: %s", response.response); - return N706A_TCP_ERROR; + return N706A_TCP_ETIMEDOUT; } - /* 延迟50-100ms(根据AT命令手册要求,这里取75ms) */ - PPP_DEBUG_DEBUG("Received '>' prompt, delaying 75ms before sending data..."); - osal_msleep(75); - - /* 发送实际数据 */ - ret = ppp_at_send_raw_data(data, len, sock->timeout_ms); - if (ret != N706A_AT_OK) { - return N706A_TCP_ERROR; + /* 解析响应,提取数据(HEX模式) */ + if (response.length > 0) { + const char *response_str = response.response; + + /* 优先检查响应中是否包含 +TCPRECV 或 +SSLTCPRECV URC(异步接收的数据) */ + /* 这些URC包含实际的数据,应该优先处理 */ + /* + * 注意:根据AT指令手册,AT+TCPREAD的响应格式应该是: + * +TCPREAD: ,, + * 但实际模块可能在响应中包含 +TCPRECV URC,而不是 +TCPREAD: 格式。 + * 这种情况下,+TCPRECV URC 中的 socket_id 可能与 AT+TCPREAD 命令中的 socket_id 不一致, + * 需要验证 socket_id 匹配,避免数据错误。 + */ + /* 注意:响应开头可能有 \x00 字节,需要手动跳过才能使用 strstr */ + /* 虽然 n706a_extract_complete_response 会跳过开头的 \x00,但写入环形缓冲区的数据可能仍包含 \x00 */ + const char *tcprecv_start = NULL; + const char *ssltcprecv_start = NULL; + + /* 手动查找 +TCPRECV 或 +SSLTCPRECV,跳过 \x00 字节 */ + for (size_t i = 0; i < response.length; i++) { + if (response_str[i] == '\0') { + continue; // 跳过 \x00 字节 + } + /* 检查是否匹配 "+TCPRECV: " */ + if (i + 10 <= response.length && strncmp(response_str + i, "+TCPRECV: ", 10) == 0) { + tcprecv_start = response_str + i; + break; + } + /* 检查是否匹配 "+SSLTCPRECV: " */ + if (i + 13 <= response.length && strncmp(response_str + i, "+SSLTCPRECV: ", 13) == 0) { + ssltcprecv_start = response_str + i; + break; + } + } + + /* 如果手动查找没找到,使用 strstr 作为后备(处理正常情况,没有 \x00 字节) */ + if (!tcprecv_start && !ssltcprecv_start) { + tcprecv_start = strstr(response_str, "+TCPRECV: "); + ssltcprecv_start = strstr(response_str, "+SSLTCPRECV: "); + } + + if (!tcprecv_start && !ssltcprecv_start) { + /* 没有找到 +TCPRECV URC,记录调试信息 */ + PPP_DEBUG_WARN("[TCP_RECV] No +TCPRECV or +SSLTCPRECV URC found in response (len=%u)", + (unsigned int)response.length); + /* 详细调试:打印响应的前32字节(默认关闭,编译期移除) */ +#if PPP_DEBUG_VERBOSE_ENABLE + static char debug_buf[128] = {0}; /* 使用静态缓冲区:32字节 * 4字符/字节(最坏情况)= 128字节 */ + memset(debug_buf, 0, sizeof(debug_buf)); + size_t debug_len = (response.length < 32) ? response.length : 32; /* 只打印前32字节 */ + for (size_t i = 0; i < debug_len && strlen(debug_buf) + 5 < sizeof(debug_buf); i++) { + if (response_str[i] == '\0') { + strncat(debug_buf, "\\x00", sizeof(debug_buf) - strlen(debug_buf) - 1); + } else if (response_str[i] >= 32 && response_str[i] < 127) { + char c = response_str[i]; + size_t pos = strlen(debug_buf); + if (pos + 1 < sizeof(debug_buf)) { + debug_buf[pos] = c; + debug_buf[pos + 1] = '\0'; + } + } else { + char hex_str[5]; + snprintf(hex_str, sizeof(hex_str), "\\x%02X", (uint8_t)response_str[i]); + strncat(debug_buf, hex_str, sizeof(debug_buf) - strlen(debug_buf) - 1); + } + } + PPP_DEBUG_VERBOSE("[TCP_RECV] Response preview (first %u bytes): %s", (unsigned int)debug_len, debug_buf); +#endif + } + + if (tcprecv_start || ssltcprecv_start) { + /* 找到 +TCPRECV 或 +SSLTCPRECV URC,提取其中的数据 */ + const char *urc_start = tcprecv_start ? tcprecv_start : ssltcprecv_start; + size_t parse_len = response.length - (urc_start - response_str); + + PPP_DEBUG_VERBOSE("[TCP_RECV] Found +TCPRECV URC in response: offset=%u, parse_len=%u, response_len=%u", + (unsigned int)(urc_start - response_str), (unsigned int)parse_len, (unsigned int)response.length); + + int socket_id; + size_t data_len; + const uint8_t *tcp_data; + + /* 使用外部函数解析 +TCPRECV URC */ + extern bool n706a_parse_tcprecv_urc_public(const char *urc_data, size_t urc_len, + int *socket_id, size_t *data_len, const uint8_t **data); + + if (n706a_parse_tcprecv_urc_public(urc_start, parse_len, &socket_id, &data_len, &tcp_data)) { + PPP_DEBUG_VERBOSE("[TCP_RECV] Successfully parsed +TCPRECV URC: socket_id=%d, data_len=%u", + socket_id, (unsigned int)data_len); + /* 验证socket_id是否匹配 */ + PPP_DEBUG_VERBOSE("[TCP_RECV] Extracted +TCPRECV URC from AT+TCPREAD response: URC_socket_id=%d, sock->socket_id=%d, data_len=%u", + socket_id, sock->socket_id, (unsigned int)data_len); + if (socket_id != sock->socket_id) { + PPP_DEBUG_WARN("[TCP_RECV] +TCPRECV URC socket_id MISMATCH: expected=%d (sock->socket_id), got=%d (URC_socket_id), dropping data", + sock->socket_id, socket_id); + PPP_DEBUG_WARN("[TCP_RECV] This indicates a potential data corruption risk - URC data belongs to different socket!"); + *received = 0; + return N706A_TCP_SUCCESS; + } + + /* 检查是否使用HEX模式 */ + bool hex_mode = n706a_at_get_tcp_recv_hex_mode(); + size_t binary_data_len = data_len; + const uint8_t *binary_data = tcp_data; + + if (hex_mode) { + /* HEX模式:需要解码HEX字符串 */ + /* 注意:不能使用 strlen,因为 HEX 字符串后面可能跟着 \r\n 和其他数据 */ + /* 应该使用 data_len * 2 作为 HEX 字符串长度,或者手动计算连续 HEX 字符的长度 */ + size_t expected_hex_str_len = data_len * 2; + + /* 手动计算实际 HEX 字符串长度(只计算连续的 HEX 字符) */ + size_t actual_hex_str_len = 0; + const char *hex_str = (const char *)tcp_data; + size_t remaining_len = parse_len - (hex_str - (const char *)urc_start); + + /* 计算连续 HEX 字符的长度 */ + for (size_t i = 0; i < remaining_len && actual_hex_str_len < expected_hex_str_len; i++) { + char c = hex_str[i]; + if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f')) { + actual_hex_str_len++; + } else { + /* 遇到非 HEX 字符,停止计算 */ + break; + } + } + + /* 使用实际 HEX 字符串长度(不能超过期望长度) */ + size_t hex_str_len = (actual_hex_str_len < expected_hex_str_len) ? actual_hex_str_len : expected_hex_str_len; + + if (hex_str_len != expected_hex_str_len) { + PPP_DEBUG_WARN("+TCPRECV HEX length mismatch: expected=%u, got=%u (actual_hex_str_len=%u)", + (unsigned int)expected_hex_str_len, (unsigned int)hex_str_len, (unsigned int)actual_hex_str_len); + /* 如果实际长度小于期望长度,说明数据不完整,返回错误 */ + if (hex_str_len < expected_hex_str_len) { + *received = 0; + return N706A_TCP_SUCCESS; + } + /* 如果实际长度大于期望长度,截断到期望长度(不应该发生,但为了安全) */ + hex_str_len = expected_hex_str_len; + } + + /* 解码HEX字符串 */ + size_t decode_len = (data_len < len) ? data_len : len; + uint8_t *decode_buffer = (uint8_t *)malloc(decode_len); + if (!decode_buffer) { + *received = 0; + return N706A_TCP_ERROR; + } + + size_t decoded = 0; + for (size_t i = 0; i < hex_str_len && decoded < decode_len; i += 2) { + uint8_t high = (tcp_data[i] >= '0' && tcp_data[i] <= '9') ? + (tcp_data[i] - '0') : + ((tcp_data[i] >= 'A' && tcp_data[i] <= 'F') ? + (tcp_data[i] - 'A' + 10) : + (tcp_data[i] - 'a' + 10)); + uint8_t low = (tcp_data[i+1] >= '0' && tcp_data[i+1] <= '9') ? + (tcp_data[i+1] - '0') : + ((tcp_data[i+1] >= 'A' && tcp_data[i+1] <= 'F') ? + (tcp_data[i+1] - 'A' + 10) : + (tcp_data[i+1] - 'a' + 10)); + decode_buffer[decoded++] = (high << 4) | low; + } + + memcpy(buffer, decode_buffer, decoded); + free(decode_buffer); + *received = decoded; + PPP_DEBUG_VERBOSE("[TCP_RECV] Successfully extracted data from +TCPRECV URC: socket_id=%d (matched), hex_len=%u, binary_len=%u", + socket_id, (unsigned int)hex_str_len, (unsigned int)decoded); + return N706A_TCP_SUCCESS; + } else { + /* ASCII模式:直接复制 */ + size_t copy_len = (data_len < len) ? data_len : len; + memcpy(buffer, tcp_data, copy_len); + *received = copy_len; + PPP_DEBUG_VERBOSE("[TCP_RECV] Successfully extracted data from +TCPRECV URC (ASCII): socket_id=%d (matched), len=%u", + socket_id, (unsigned int)copy_len); + return N706A_TCP_SUCCESS; + } + } + } + + /* 如果没有找到 +TCPRECV URC,继续处理 +TCPREAD: 响应 */ + /* + * 根据AT指令手册,AT+TCPREAD的标准响应格式是: + * +TCPREAD: ,, + * 这是命令响应格式,与 +TCPRECV URC 格式不同。 + * 如果响应中包含 +TCPRECV URC,应该优先处理(上面的代码已处理)。 + * 如果响应中只包含 +TCPREAD: 格式,则在这里处理。 + */ + /* 查找 +TCPREAD: 或 +SSLTCPREAD: */ + const char *tcpread_start = strstr(response_str, "+TCPREAD:"); + if (!tcpread_start) { +#ifdef N706A_SSL_USE_AT_COMMAND + tcpread_start = strstr(response_str, "+SSLTCPREAD:"); + if (!tcpread_start) { +#endif + /* 没有找到 +TCPREAD: 响应,返回超时状态,减少轮询频率 */ + *received = 0; + return N706A_TCP_ETIMEDOUT; /* 返回超时,让上层知道需要等待 */ +#ifdef N706A_SSL_USE_AT_COMMAND + } +#endif + } + + /* 解析格式:+TCPREAD: ,, 或 +SSLTCPREAD: ,, */ + int socket_id; + size_t hex_length; + const char *hex_data_start; + + /* 跳过 "+TCPREAD: " 或 "+SSLTCPREAD: " */ + const char *p = tcpread_start; + if (strncmp(p, "+TCPREAD:", 9) == 0) { + p += 9; + } else if (strncmp(p, "+SSLTCPREAD:", 12) == 0) { + p += 12; + } else { + *received = 0; + return N706A_TCP_SUCCESS; + } + + /* 跳过空格 */ + while (*p == ' ' || *p == '\t') { + p++; + } + + /* 检查是否是 ERROR 响应(连接已关闭) */ + if (strncmp(p, "ERROR", 5) == 0) { + /* +TCPREAD: ERROR 表示连接已关闭 */ + PPP_DEBUG_ERROR("[TCP_RECV] AT+TCPREAD returned ERROR for socket %d, connection closed", sock->socket_id); + sock->state = N706A_TCP_STATE_CLOSED; + *received = 0; + return N706A_TCP_ENOTCONN; + } + + /* 解析socket_id */ + socket_id = 0; + while (*p >= '0' && *p <= '9') { + socket_id = socket_id * 10 + (*p - '0'); + p++; + } + + if (*p != ',') { + *received = 0; + return N706A_TCP_SUCCESS; + } + p++; // 跳过 ',' + + /* 解析hex_length */ + hex_length = 0; + while (*p >= '0' && *p <= '9') { + hex_length = hex_length * 10 + (*p - '0'); + p++; + } + + if (*p != ',') { + *received = 0; + return N706A_TCP_SUCCESS; + } + p++; // 跳过 ',' + + /* 检查是否是 +TCPREAD: 0,0(没有数据)的情况 */ + /* 这种情况下,应该返回ETIMEDOUT,让上层知道需要等待,而不是立即重试 */ + if (hex_length == 0) { + /* 没有数据,返回超时状态,让上层增加延迟再重试 */ + PPP_DEBUG_DEBUG("[TCP_RECV] AT+TCPREAD returned 0 bytes (no data), returning ETIMEDOUT to reduce polling"); + *received = 0; + return N706A_TCP_ETIMEDOUT; /* 返回超时,让上层知道需要等待 */ + } + + /* hex_data_start指向HEX数据开始位置 */ + hex_data_start = p; + + /* 查找数据结束位置(\r\n或字符串结束) */ + const char *hex_data_end = strstr(hex_data_start, "\r\n"); + if (!hex_data_end) { + hex_data_end = response_str + response.length; + } + + size_t hex_data_str_len = hex_data_end - hex_data_start; + + /* 检查是否使用HEX模式(根据AT+RECVMODE设置) */ + bool hex_mode = n706a_at_get_tcp_recv_hex_mode(); + bool is_hex_format = false; + + if (hex_mode) { + /* HEX模式:强制尝试HEX解码 */ + if (hex_data_str_len > 0 && hex_data_str_len % 2 == 0 && hex_length == hex_data_str_len) { + /* 检查是否所有字符都是十六进制字符 */ + const char *check_p = hex_data_start; + size_t check_count = (hex_data_str_len < 100) ? hex_data_str_len : 100; // 检查前100个字符 + bool all_hex = true; + size_t i; + for (i = 0; i < check_count; i++) { + if (!((*check_p >= '0' && *check_p <= '9') || + (*check_p >= 'A' && *check_p <= 'F') || + (*check_p >= 'a' && *check_p <= 'f'))) { + all_hex = false; + break; + } + check_p++; + } + if (all_hex) { + is_hex_format = true; + } else { + PPP_DEBUG_WARN("HEX mode enabled but data contains non-HEX characters"); + } + } else { + PPP_DEBUG_WARN("HEX mode enabled but data length mismatch (hex_data_str_len=%u, hex_length=%u, hex_data_str_len%%2=%u)", + (unsigned int)hex_data_str_len, (unsigned int)hex_length, (unsigned int)(hex_data_str_len % 2)); + } + } else { + /* ASCII模式:检查是否是HEX格式(自动检测) */ + if (hex_data_str_len > 0 && hex_data_str_len % 2 == 0) { + /* 简单检查:前几个字符是否都是十六进制字符 */ + const char *check_p = hex_data_start; + size_t check_count = (hex_data_str_len < 20) ? hex_data_str_len : 20; // 检查前20个字符 + bool all_hex = true; + size_t i; + for (i = 0; i < check_count; i++) { + if (!((*check_p >= '0' && *check_p <= '9') || + (*check_p >= 'A' && *check_p <= 'F') || + (*check_p >= 'a' && *check_p <= 'f'))) { + all_hex = false; + break; + } + check_p++; + } + if (all_hex && hex_length == hex_data_str_len) { + /* 如果AT+TCPREAD返回的len等于HEX字符串长度,说明是HEX格式 */ + is_hex_format = true; + } + } + } + + if (is_hex_format) { + /* HEX格式:需要解码 */ + size_t binary_len = hex_data_str_len / 2; + if (binary_len > len) { + binary_len = len; + } + + /* 解码HEX字符串 */ + uint8_t *decode_buffer = (uint8_t *)malloc(binary_len); + if (!decode_buffer) { + *received = 0; + return N706A_TCP_ERROR; + } + + size_t decoded_len = 0; + size_t i; + for (i = 0; i < hex_data_str_len && decoded_len < binary_len; i += 2) { + uint8_t high = (hex_data_start[i] >= '0' && hex_data_start[i] <= '9') ? + (hex_data_start[i] - '0') : + ((hex_data_start[i] >= 'A' && hex_data_start[i] <= 'F') ? + (hex_data_start[i] - 'A' + 10) : + (hex_data_start[i] - 'a' + 10)); + uint8_t low = (hex_data_start[i+1] >= '0' && hex_data_start[i+1] <= '9') ? + (hex_data_start[i+1] - '0') : + ((hex_data_start[i+1] >= 'A' && hex_data_start[i+1] <= 'F') ? + (hex_data_start[i+1] - 'A' + 10) : + (hex_data_start[i+1] - 'a' + 10)); + decode_buffer[decoded_len++] = (high << 4) | low; + } + + memcpy(buffer, decode_buffer, decoded_len); + free(decode_buffer); + *received = decoded_len; + PPP_DEBUG_DEBUG("TCPREAD HEX decoded: hex_len=%u -> binary_len=%u", + (unsigned int)hex_data_str_len, (unsigned int)decoded_len); + return N706A_TCP_SUCCESS; + } else { + /* ASCII格式:直接复制 */ + size_t copy_len = (hex_data_str_len < len) ? hex_data_str_len : len; + memcpy(buffer, hex_data_start, copy_len); + *received = copy_len; + PPP_DEBUG_DEBUG("TCPREAD ASCII: len=%u", (unsigned int)copy_len); + return N706A_TCP_SUCCESS; + } } - *sent = len; - return N706A_TCP_SUCCESS; + /* 如果没有找到 +TCPREAD: 响应,或者解析失败,返回超时状态 */ + /* 这样可以减少轮询频率,降低功耗 */ + *received = 0; + return N706A_TCP_ETIMEDOUT; /* 返回超时,让上层知道需要等待,而不是立即重试 */ } /** - * @brief 接收TCP数据 + * @brief 接收TCP数据(支持多次读取,确保读取完所有请求的数据) */ n706a_tcp_result_t n706a_tcp_recv(n706a_tcp_socket_t *sock, uint8_t *buffer, @@ -1006,26 +2319,96 @@ n706a_tcp_result_t n706a_tcp_recv(n706a_tcp_socket_t *sock, } /* 优先从URC缓存读取数据(如果有) */ - if (sock->urc_recv_buffer && sock->urc_recv_mutex && sock->urc_recv_length > 0) { + if (sock->urc_recv_buffer && sock->urc_recv_mutex) { osal_mutex *mutex = (osal_mutex *)sock->urc_recv_mutex; osal_mutex_lock(mutex); - size_t copy_len = (sock->urc_recv_length < len) ? sock->urc_recv_length : len; - memcpy(buffer, sock->urc_recv_buffer, copy_len); - - // 移动剩余数据到缓冲区开头 - if (copy_len < sock->urc_recv_length) { - memmove(sock->urc_recv_buffer, sock->urc_recv_buffer + copy_len, - sock->urc_recv_length - copy_len); + // 如果缓存有数据,直接读取 + if (sock->urc_recv_length > 0) { + size_t copy_len = (sock->urc_recv_length < len) ? sock->urc_recv_length : len; + memcpy(buffer, sock->urc_recv_buffer, copy_len); + + // 移动剩余数据到缓冲区开头 + if (copy_len < sock->urc_recv_length) { + memmove(sock->urc_recv_buffer, sock->urc_recv_buffer + copy_len, + sock->urc_recv_length - copy_len); + } + sock->urc_recv_length -= copy_len; + + osal_mutex_unlock(mutex); + + *received = copy_len; + + /* 详细调试:打印读取的数据内容(默认关闭,编译期移除) */ +#if PPP_DEBUG_VERBOSE_ENABLE + PPP_DEBUG_VERBOSE("[TCP_RECV] Read from URC buffer: socket=%d, len=%u, remaining=%u", + sock->socket_id, (unsigned int)copy_len, (unsigned int)sock->urc_recv_length); + if (copy_len > 0) { + size_t print_len = (copy_len > 32) ? 32 : copy_len; /* 只打印前32字节 */ + static char data_hex[128] = {0}; /* 使用静态缓冲区 */ + memset(data_hex, 0, sizeof(data_hex)); + for (size_t i = 0; i < print_len && i * 3 + 3 < sizeof(data_hex); i++) { + snprintf(data_hex + i * 3, 4, "%02X ", buffer[i]); + } + if (copy_len > 32) { + PPP_DEBUG_VERBOSE("[TCP_RECV] URC buffer data (first 32 bytes, HEX): %s... (total %u bytes)", + data_hex, (unsigned int)copy_len); + } else { + PPP_DEBUG_VERBOSE("[TCP_RECV] URC buffer data (HEX): %s", data_hex); + } + } +#endif + + return N706A_TCP_SUCCESS; } - sock->urc_recv_length -= copy_len; - osal_mutex_unlock(mutex); - - *received = copy_len; - PPP_DEBUG_DEBUG("Read from URC buffer: socket=%d, len=%u, remaining=%u", - sock->socket_id, (unsigned int)copy_len, (unsigned int)sock->urc_recv_length); - return N706A_TCP_SUCCESS; + // 缓存为空,如果有信号量,先等待URC数据 + // 优化:使用较长的超时时间(60秒),主要依赖URC机制,减少轮询 + // 如果timeout_ms小于60秒,使用timeout_ms;否则使用60秒(避免无限等待) + if (sock->urc_data_available_sem) { + osal_semaphore *sem = (osal_semaphore *)sock->urc_data_available_sem; + osal_mutex_unlock(mutex); // 释放mutex,避免死锁 + + // 等待信号量:使用较长的超时时间(60秒),完全依赖URC机制 + // 这样可以降低功耗和AT命令开销,适合小系统 + uint32_t urc_wait_timeout = (timeout_ms > 60000) ? 60000 : timeout_ms; + if (urc_wait_timeout < timeout_ms) { + PPP_DEBUG_DEBUG("[TCP_RECV] Using URC wait timeout %u ms (capped at 60s) instead of %u ms for socket %d", + urc_wait_timeout, timeout_ms, sock->socket_id); + } + int sem_ret = osal_sem_down_timeout(sem, urc_wait_timeout); + if (sem_ret == 0) { + // 信号量被触发,重新检查缓存 + osal_mutex_lock(mutex); + if (sock->urc_recv_length > 0) { + size_t copy_len = (sock->urc_recv_length < len) ? sock->urc_recv_length : len; + memcpy(buffer, sock->urc_recv_buffer, copy_len); + + // 移动剩余数据到缓冲区开头 + if (copy_len < sock->urc_recv_length) { + memmove(sock->urc_recv_buffer, sock->urc_recv_buffer + copy_len, + sock->urc_recv_length - copy_len); + } + sock->urc_recv_length -= copy_len; + + osal_mutex_unlock(mutex); + + *received = copy_len; + PPP_DEBUG_DEBUG("Read from URC buffer after wait: socket=%d, len=%u, remaining=%u", + sock->socket_id, (unsigned int)copy_len, (unsigned int)sock->urc_recv_length); + return N706A_TCP_SUCCESS; + } + osal_mutex_unlock(mutex); + } else { + // 等待超时,完全依赖URC机制,不再调用AT+TCPREAD + // 这样可以降低功耗和AT命令开销,适合小系统 + PPP_DEBUG_DEBUG("URC semaphore wait timeout, returning ETIMEDOUT (fully relying on URC mechanism): socket=%d", sock->socket_id); + *received = 0; + return N706A_TCP_ETIMEDOUT; /* 返回超时,让上层知道需要等待或重连 */ + } + } else { + osal_mutex_unlock(mutex); + } } #ifdef N706A_SSL_USE_MBEDTLS @@ -1046,46 +2429,75 @@ n706a_tcp_result_t n706a_tcp_recv(n706a_tcp_socket_t *sock, } #endif - /* AT指令模式或普通TCP */ - char at_cmd[64]; - n706a_at_response_t response; - -#ifdef N706A_SSL_USE_AT_COMMAND - if (sock->is_ssl) { - snprintf(at_cmd, sizeof(at_cmd), "AT+SSLTCPREAD=%d,%zu", - sock->socket_id, len); - } else -#endif - { - snprintf(at_cmd, sizeof(at_cmd), "AT+TCPREAD=%d,%zu", - sock->socket_id, len); - } - - n706a_at_error_t ret = n706a_at_send_command(at_cmd, NULL, timeout_ms, &response); - if (ret != N706A_AT_OK) { - return N706A_TCP_ETIMEDOUT; - } - - /* 解析响应,提取数据 */ - if (response.length > 0) { - /* 查找数据部分 */ - const char *data_start = strstr(response.response, "\r\n"); - if (data_start) { - data_start += 2; // 跳过\r\n - size_t data_len = response.length - (data_start - response.response); + /* AT指令模式或普通TCP:完全依赖URC机制,不再主动调用AT+TCPREAD */ + /* 优化:移除AT+TCPREAD轮询机制,完全依赖URC机制,降低功耗和AT命令开销 */ + /* 如果URC buffer为空且没有信号量,说明URC机制未初始化 */ + /* 这种情况可能发生在握手阶段,此时需要初始化URC buffer */ + if (!sock->urc_recv_buffer || !sock->urc_data_available_sem) { + PPP_DEBUG_WARN("[TCP_RECV] URC mechanism not initialized for socket %d, initializing now...", sock->socket_id); + + /* 尝试初始化URC buffer(使用默认大小) */ + n706a_tcp_result_t init_ret = n706a_tcp_init_urc_buffer(sock, 0); // 0表示使用默认大小 + if (init_ret != N706A_TCP_SUCCESS) { + PPP_DEBUG_ERROR("[TCP_RECV] Failed to initialize URC buffer for socket %d (ret=%d), cannot receive data", + sock->socket_id, init_ret); + *received = 0; + return N706A_TCP_ERROR; + } + + PPP_DEBUG_INFO("[TCP_RECV] URC buffer initialized successfully for socket %d, now waiting for URC data...", sock->socket_id); + + /* 初始化成功后,继续等待URC数据(重新检查buffer,然后等待信号量) */ + /* 注意:初始化后,sock->urc_data_available_sem应该已经设置,可以继续等待 */ + if (sock->urc_data_available_sem) { + osal_semaphore *sem = (osal_semaphore *)sock->urc_data_available_sem; - if (data_len > len) { - data_len = len; + /* 等待信号量:使用较长的超时时间(60秒),完全依赖URC机制 */ + uint32_t urc_wait_timeout = (timeout_ms > 60000) ? 60000 : timeout_ms; + if (urc_wait_timeout < timeout_ms) { + PPP_DEBUG_DEBUG("[TCP_RECV] Using URC wait timeout %u ms (capped at 60s) instead of %u ms for socket %d", + urc_wait_timeout, timeout_ms, sock->socket_id); + } + int sem_ret = osal_sem_down_timeout(sem, urc_wait_timeout); + if (sem_ret == 0) { + /* 信号量被触发,重新检查缓存 */ + osal_mutex *mutex = (osal_mutex *)sock->urc_recv_mutex; + if (mutex) { + osal_mutex_lock(mutex); + if (sock->urc_recv_length > 0) { + size_t copy_len = (sock->urc_recv_length < len) ? sock->urc_recv_length : len; + memcpy(buffer, sock->urc_recv_buffer, copy_len); + + /* 移动剩余数据到缓冲区开头 */ + if (copy_len < sock->urc_recv_length) { + memmove(sock->urc_recv_buffer, sock->urc_recv_buffer + copy_len, + sock->urc_recv_length - copy_len); + } + sock->urc_recv_length -= copy_len; + + osal_mutex_unlock(mutex); + + *received = copy_len; + PPP_DEBUG_DEBUG("[TCP_RECV] Read from URC buffer after initialization and wait: socket=%d, len=%u, remaining=%u", + sock->socket_id, (unsigned int)copy_len, (unsigned int)sock->urc_recv_length); + return N706A_TCP_SUCCESS; + } + osal_mutex_unlock(mutex); + } + } else { + /* 等待超时 */ + PPP_DEBUG_DEBUG("[TCP_RECV] URC semaphore wait timeout after initialization, returning ETIMEDOUT: socket=%d", sock->socket_id); + *received = 0; + return N706A_TCP_ETIMEDOUT; } - - memcpy(buffer, data_start, data_len); - *received = data_len; - return N706A_TCP_SUCCESS; } } + /* 如果到达这里,说明URC buffer为空且信号量等待超时 */ + /* 完全依赖URC机制,不再调用AT+TCPREAD */ + PPP_DEBUG_DEBUG("[TCP_RECV] URC buffer empty and semaphore timeout, returning ETIMEDOUT (fully relying on URC): socket=%d", sock->socket_id); *received = 0; - return N706A_TCP_SUCCESS; + return N706A_TCP_ETIMEDOUT; } /** @@ -1152,9 +2564,13 @@ n706a_tcp_result_t n706a_tcp_close(n706a_tcp_socket_t *sock) delay_count++; } } else { - PPP_DEBUG_ERROR("Socket %d close failed after %d attempts! Response: %s", - sock->socket_id, max_retries, - response.length > 0 ? response.response : "(empty)"); + PPP_DEBUG_ERROR("Socket %d close failed after %d attempts! Response: ", + sock->socket_id, max_retries); + if (response.length > 0) { + n706a_tcp_safe_print_response(&response, 256); + } else { + PPP_DEBUG_ERROR("(empty)"); + } /* 即使失败也标记为关闭,避免资源泄露,但记录错误 */ } } @@ -1164,7 +2580,26 @@ n706a_tcp_result_t n706a_tcp_close(n706a_tcp_socket_t *sock) sock->state = N706A_TCP_STATE_CLOSED; sock->is_ssl = false; - // 从全局socket表中移除 + // 清空累积缓冲区中的残留数据(避免遗留字符影响后续操作) + extern void n706a_at_clear_accumulate_buffer(void); + n706a_at_clear_accumulate_buffer(); + + // 从全局socket表中移除(延迟移除,避免异步URC丢失) + // 注意:先标记socket为关闭状态,但暂时不从表中移除,给异步URC一些时间到达 + // 延迟100ms后再从表中移除并释放URC buffer,避免异步URC(如+TCPRECV)丢失 + if (g_socket_table_initialized) { + osal_mutex_lock(&g_socket_table_mutex); + // 暂时不从表中移除,延迟释放 + // g_tcp_sockets[sock->socket_id] = NULL; + osal_mutex_unlock(&g_socket_table_mutex); + } + + // 延迟释放URC接收缓存,给异步URC一些时间到达(避免+TCPRECV URC丢失) + // 注意:这里使用简单的延迟,实际应用中可以考虑使用定时器或后台任务 + // 延迟500ms后释放URC buffer,给异步URC足够的时间到达 + osal_msleep(500); // 延迟500ms + + // 现在从全局socket表中移除 if (g_socket_table_initialized) { osal_mutex_lock(&g_socket_table_mutex); g_tcp_sockets[sock->socket_id] = NULL; @@ -1174,7 +2609,7 @@ n706a_tcp_result_t n706a_tcp_close(n706a_tcp_socket_t *sock) // 释放URC接收缓存 n706a_tcp_free_urc_buffer(sock); - PPP_DEBUG_INFO("Socket %d closed and URC buffer freed", sock->socket_id); + PPP_DEBUG_INFO("Socket %d closed and URC buffer freed (after 100ms delay)", sock->socket_id); return N706A_TCP_SUCCESS; } @@ -1336,17 +2771,15 @@ static int tcp_mbedtls_send_callback(void *ctx, const unsigned char *buf, size_t sock->socket_id, len); n706a_at_response_t response; - n706a_at_error_t ret = n706a_at_send_command(at_cmd, NULL, 5000, &response); + /* 发送 AT+TCPSEND 命令,期望收到 '>' 提示符 */ + n706a_at_error_t ret = n706a_at_send_command(at_cmd, ">", 5000, &response); if (ret != N706A_AT_OK) { - PPP_DEBUG_ERROR("Failed to send %s command (mbedtls)", at_cmd); + PPP_DEBUG_ERROR("Failed to send %s command (mbedtls, expected '>' prompt not received)", at_cmd); return MBEDTLS_ERR_NET_SEND_FAILED; } - /* 检查响应中是否包含 '>' 提示符(根据AT命令手册要求) */ - if (strstr((const char *)response.response, ">") == NULL) { - PPP_DEBUG_ERROR("Response does not contain '>' prompt (mbedtls): %s", response.response); - return MBEDTLS_ERR_NET_SEND_FAILED; - } + /* 验证成功,说明响应中已包含 '>' 提示符 */ + PPP_DEBUG_DEBUG("AT+TCPSEND command sent successfully (mbedtls), received '>' prompt"); /* 延迟50-100ms(根据AT命令手册要求,这里取75ms) */ PPP_DEBUG_DEBUG("Received '>' prompt (mbedtls), delaying 75ms before sending data..."); @@ -1592,3 +3025,31 @@ void n706a_tcp_on_urc_received(int socket_id, const uint8_t *data, size_t length } } +/** + * @brief +TCPCLOSE URC回调(由n706a_at_handler调用) + * @param socket_id Socket ID + * + * 注意:此函数由n706a_at_handler.c通过URC回调调用,不是应用层API + * 当服务器主动关闭连接时,会调用此函数更新socket状态 + */ +void n706a_tcp_on_tcpclose_urc(int socket_id) +{ + if (socket_id < 0 || socket_id >= N706A_MAX_SOCKETS) { + PPP_DEBUG_WARN("+TCPCLOSE URC: Invalid socket_id=%d", socket_id); + return; + } + + PPP_DEBUG_INFO("+TCPCLOSE URC received: socket=%d, updating socket state to CLOSED", socket_id); + + // 更新socket状态 + osal_mutex_lock(&g_socket_table_mutex); + n706a_tcp_socket_t *sock = g_tcp_sockets[socket_id]; + if (sock) { + sock->state = N706A_TCP_STATE_CLOSED; + PPP_DEBUG_INFO("Socket %d state updated to CLOSED (server closed connection)", socket_id); + } else { + PPP_DEBUG_WARN("+TCPCLOSE URC: Socket %d not found in socket table", socket_id); + } + osal_mutex_unlock(&g_socket_table_mutex); +} + diff --git a/code/sdk/zhihui/n706a_net/src/n706a_websocket.c b/code/sdk/zhihui/n706a_net/src/n706a_websocket.c index 1720bead0..24a9c343e 100644 --- a/code/sdk/zhihui/n706a_net/src/n706a_websocket.c +++ b/code/sdk/zhihui/n706a_net/src/n706a_websocket.c @@ -11,6 +11,12 @@ * - 分片传输 */ +/* 定义 _GNU_SOURCE 以使用 strcasestr(不区分大小写的字符串查找) */ +/* 注意:必须在包含 之前定义 */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + #include "n706a_websocket.h" #include "n706a_tcp_client.h" #include "ppp_debug.h" @@ -20,11 +26,28 @@ #include #include +/* 包含 WebSocket 解析器(使用 wrapper 头文件,实际实现需要链接 websocket_parser.c) */ +/* 注意:如果 websocket_parser.c 不在同一个编译单元,需要确保它被编译并链接 */ +/* 这里我们直接包含源文件以简化集成(如果项目允许) */ +/* 或者使用 wrapper 头文件,并在构建系统中确保 websocket_parser.c 被编译 */ +#ifdef N706A_WS_USE_PARSER_SOURCE + /* 直接包含源文件(如果项目允许) */ + #include "../../websocket/src/websocket_parser.c" +#else + /* 使用 wrapper 头文件(需要确保 websocket_parser.c 被编译并链接) */ + #include "../../websocket/include/websocket_parser_wrapper.h" +#endif + #ifdef __unix__ #include #include #elif defined(__rtthread__) #include +#else +/* 嵌入式平台(CMSIS OS) */ +#include "custom_utils.h" /* 用于 uapi_create_osThread_custom */ +#include "errcode.h" /* 用于 ERRCODE_SUCC */ +#include "cmsis_os2.h" /* 用于 osThreadFunc_t */ #endif /* WebSocket GUID(RFC 6455) */ @@ -41,16 +64,51 @@ static n706a_websocket_client_t *g_ws_clients[N706A_WS_MAX_CLIENTS] = {NULL}; /* ========== 私有函数声明 ========== */ -static int ws_perform_handshake(n706a_websocket_client_t *client); +static int ws_perform_handshake(n706a_websocket_client_t *client,const char* header_option); static int ws_send_frame(n706a_websocket_client_t *client, n706a_ws_opcode_t opcode, const uint8_t *data, size_t length, bool fin); +static void ws_send_close_if_needed(n706a_websocket_client_t *client, + n706a_ws_close_code_t code, + const char *reason); + +static void ws_send_close_if_needed(n706a_websocket_client_t *client, + n706a_ws_close_code_t code, + const char *reason) +{ + if (!client || client->state != N706A_WS_STATE_OPEN) { + return; + } + + uint8_t close_data[128]; + size_t close_len = 2; + close_data[0] = (code >> 8) & 0xFF; + close_data[1] = code & 0xFF; + + if (reason && reason[0] != '\0') { + size_t reason_len = strlen(reason); + if (reason_len > sizeof(close_data) - 2) { + reason_len = sizeof(close_data) - 2; + } + memcpy(close_data + 2, reason, reason_len); + close_len += reason_len; + } + + ws_send_frame(client, N706A_WS_OPCODE_CLOSE, close_data, close_len, true); +} static int ws_recv_frame(n706a_websocket_client_t *client); static void ws_generate_mask_key(uint8_t *mask_key); static void ws_mask_data(uint8_t *data, size_t len, const uint8_t *mask_key); +static void ws_recv_thread_func_impl(n706a_websocket_client_t *client); +#ifdef __unix__ static void *ws_recv_thread_func(void *arg); +#elif defined(__rtthread__) +static void ws_recv_thread_func(void *arg); +#else +static void ws_recv_thread_func(void *arg); +#endif static int ws_parse_url(const char *url, char *protocol, char *host, uint16_t *port, char *path); @@ -126,6 +184,18 @@ int n706a_websocket_constructor(n706a_ws_on_open_callback_t on_open, client->recv_buffer_len = 0; client->running = false; + /* 初始化 WebSocket 帧解析器 */ + client->parser = ws_parser_init(); + if (!client->parser) { + PPP_DEBUG_ERROR("[WebSocket] Failed to initialize parser for client %d", client_slot); + if (client->recv_buffer) { + free(client->recv_buffer); + } + free(client); + return -1; + } + client->fragment_opcode = N706A_WS_OPCODE_TEXT; /* 默认初始 opcode */ + g_ws_clients[client_slot] = client; return client_slot; @@ -174,6 +244,18 @@ int n706a_websocket_constructor_with_socket(int socket_id, return -1; } + /* 初始化 WebSocket 帧解析器 */ + client->parser = ws_parser_init(); + if (!client->parser) { + PPP_DEBUG_ERROR("[WebSocket] Failed to initialize parser for client %d", socket_id); + if (client->recv_buffer) { + free(client->recv_buffer); + } + free(client); + return -1; + } + client->fragment_opcode = N706A_WS_OPCODE_TEXT; /* 默认初始 opcode */ + g_ws_clients[socket_id] = client; return socket_id; @@ -182,7 +264,7 @@ int n706a_websocket_constructor_with_socket(int socket_id, /** * @brief 连接到WebSocket服务器 */ -int n706a_websocket_connect(int client_id, const char *url, const char *option) +int n706a_websocket_connect(int client_id, const char *url, const char *header_option) { if (client_id < 0 || client_id >= N706A_WS_MAX_CLIENTS) { return N706A_WS_ERROR_INVALID_PARAM; @@ -193,8 +275,51 @@ int n706a_websocket_connect(int client_id, const char *url, const char *option) return N706A_WS_ERROR_INVALID_PARAM; } + /* 如果状态不是 CLOSED,根据状态处理 */ if (client->state != N706A_WS_STATE_CLOSED) { - return N706A_WS_ERROR_ALREADY_OPEN; + if (client->state == N706A_WS_STATE_OPEN) { + /* 连接已经打开,直接返回成功 */ + PPP_DEBUG_INFO("[WebSocket] Client %d is already OPEN, reusing existing connection", client_id); + return N706A_WS_SUCCESS; + } else if (client->state == N706A_WS_STATE_CONNECTING) { + /* 正在连接中,等待连接完成(最多等待5秒) */ + PPP_DEBUG_INFO("[WebSocket] Client %d is CONNECTING, waiting for connection to complete...", client_id); + uint32_t wait_count = 0; + const uint32_t max_wait_count = 50; /* 5秒 = 50 * 100ms */ + while (client->state == N706A_WS_STATE_CONNECTING && wait_count < max_wait_count) { + osal_msleep(100); + wait_count++; + } + + if (client->state == N706A_WS_STATE_OPEN) { + PPP_DEBUG_INFO("[WebSocket] Client %d connection completed, state is now OPEN", client_id); + return N706A_WS_SUCCESS; + } else if (client->state == N706A_WS_STATE_CLOSED) { + /* 连接失败,状态变为 CLOSED,可以继续尝试连接 */ + PPP_DEBUG_WARN("[WebSocket] Client %d connection failed during wait, state is now CLOSED", client_id); + /* 继续执行连接逻辑 */ + } else { + /* 其他状态(CLOSING),需要关闭后重试 */ + PPP_DEBUG_WARN("[WebSocket] Client %d is in state %d after wait, closing first...", + client_id, client->state); + n706a_websocket_close(client_id); + osal_msleep(100); + } + } else { + /* CLOSING 或其他状态,先关闭连接 */ + PPP_DEBUG_WARN("[WebSocket] Client %d is not in CLOSED state (current state=%d), closing first...", + client_id, client->state); + n706a_websocket_close(client_id); + /* 等待一小段时间,确保关闭操作完成 */ + osal_msleep(100); + + /* 再次检查状态 */ + if (client->state != N706A_WS_STATE_CLOSED) { + PPP_DEBUG_ERROR("[WebSocket] Client %d still not in CLOSED state after close (state=%d)", + client_id, client->state); + return N706A_WS_ERROR_ALREADY_OPEN; + } + } } /* 解析URL */ @@ -230,7 +355,7 @@ int n706a_websocket_connect(int client_id, const char *url, const char *option) } /* WebSocket握手 */ - if (ws_perform_handshake(client) != 0) { + if (ws_perform_handshake(client,header_option) != 0) { n706a_tcp_close(&client->tcp_sock); client->state = N706A_WS_STATE_CLOSED; if (client->on_error) { @@ -243,12 +368,15 @@ int n706a_websocket_connect(int client_id, const char *url, const char *option) /* 启动接收线程 */ client->running = true; + PPP_DEBUG_INFO("[WebSocket] Starting receive thread for client %d", client_id); #ifdef __unix__ - if (pthread_create((pthread_t *)&client->recv_thread, NULL, ws_recv_thread_func, client) != 0) { - PPP_DEBUG_ERROR("Failed to create WebSocket receive thread for client %d", client_id); + int thread_ret = pthread_create((pthread_t *)&client->recv_thread, NULL, ws_recv_thread_func, client); + if (thread_ret != 0) { + PPP_DEBUG_ERROR("[WebSocket] Failed to create receive thread for client %d: pthread_create returned %d", client_id, thread_ret); client->running = false; return N706A_WS_ERROR_SEND_FAIL; } + PPP_DEBUG_INFO("[WebSocket] Receive thread created successfully for client %d (thread_id=%p)", client_id, (void *)client->recv_thread); #elif defined(__rtthread__) /* RT-Thread使用rt_thread_create */ rt_thread_t thread = rt_thread_create("ws_recv", @@ -266,8 +394,25 @@ int n706a_websocket_connect(int client_id, const char *url, const char *option) return N706A_WS_ERROR_SEND_FAIL; } #else - /* 其他平台暂不支持线程 */ - PPP_DEBUG_WARN("WebSocket receive thread not supported on this platform"); + /* 嵌入式平台(CMSIS OS)使用 uapi_create_osThread_custom_with_args 传递 client 参数 */ + app_task_definition_ext_t ws_recv_task_cfg = {0}; + ws_recv_task_cfg.attr.name = "ws_recv"; + ws_recv_task_cfg.attr.priority = 24; /* 优先级24(中等优先级) */ + ws_recv_task_cfg.attr.stack_size = 8192; /* 8KB栈(增加栈大小以避免栈溢出,因为调用链较深且可能有较大的栈缓冲区) */ + ws_recv_task_cfg.func = (osThreadFunc_t)ws_recv_thread_func; + ws_recv_task_cfg.use_mem = USE_EXTEND_MEM; /* 使用扩展内存(PSRAM)分配栈 */ + ws_recv_task_cfg.create_queue_handle = NULL; + ws_recv_task_cfg.inst_args = client; /* 传递 client 参数给线程函数 */ + + errcode_t thread_ret = uapi_create_osThread_custom_with_args(&ws_recv_task_cfg); + if (thread_ret != ERRCODE_SUCC) { + PPP_DEBUG_ERROR("[WebSocket] Failed to create receive thread for client %d: uapi_create_osThread_custom_with_args returned 0x%x", client_id, thread_ret); + client->running = false; + return N706A_WS_ERROR_SEND_FAIL; + } + + client->recv_thread = (void *)(uintptr_t)ws_recv_task_cfg.task_handle; + PPP_DEBUG_INFO("[WebSocket] Receive thread created successfully for client %d (task_handle=%p)", client_id, (void *)client->recv_thread); #endif /* 调用on_open回调 */ @@ -371,24 +516,7 @@ int n706a_websocket_close_with_reason(int client_id, } if (client->state == N706A_WS_STATE_OPEN) { - /* 发送关闭帧 */ - uint8_t close_data[128]; - size_t close_len = 2; - - close_data[0] = (close_code >> 8) & 0xFF; - close_data[1] = close_code & 0xFF; - - if (reason) { - size_t reason_len = strlen(reason); - if (reason_len > sizeof(close_data) - 2) { - reason_len = sizeof(close_data) - 2; - } - memcpy(close_data + 2, reason, reason_len); - close_len += reason_len; - } - - ws_send_frame(client, N706A_WS_OPCODE_CLOSE, close_data, close_len, true); - + ws_send_close_if_needed(client, close_code, reason); client->state = N706A_WS_STATE_CLOSING; } @@ -404,11 +532,23 @@ int n706a_websocket_close_with_reason(int client_id, rt_thread_delete((rt_thread_t)client->recv_thread); client->recv_thread = NULL; } +#else + /* 嵌入式平台(CMSIS OS)使用 osThreadTerminate */ + if (client->recv_thread) { + osThreadId_t thread_id = (osThreadId_t)(uintptr_t)client->recv_thread; + osThreadTerminate(thread_id); + client->recv_thread = NULL; + } #endif /* 关闭TCP连接 */ n706a_tcp_close(&client->tcp_sock); + /* 重置解析器 */ + if (client->parser) { + ws_parser_reset(client->parser); + } + client->state = N706A_WS_STATE_CLOSED; /* 调用on_close回调 */ @@ -436,6 +576,23 @@ n706a_ws_state_t n706a_websocket_get_state(int client_id) return client->state; } +/** + * @brief 获取WebSocket的底层TCP socket ID + */ +int n706a_websocket_get_socket_id(int client_id) +{ + if (client_id < 0 || client_id >= N706A_WS_MAX_CLIENTS) { + return -1; + } + + n706a_websocket_client_t *client = g_ws_clients[client_id]; + if (!client) { + return -1; + } + + return client->tcp_sock.socket_id; +} + /** * @brief 检查WebSocket是否已连接 */ @@ -464,6 +621,11 @@ int n706a_websocket_destructor(int client_id) } /* 释放资源 */ + if (client->parser) { + ws_parser_destroy(client->parser); + client->parser = NULL; + } + if (client->recv_buffer) { free(client->recv_buffer); } @@ -479,7 +641,7 @@ int n706a_websocket_destructor(int client_id) /** * @brief 执行WebSocket握手 */ -static int ws_perform_handshake(n706a_websocket_client_t *client) +static int ws_perform_handshake(n706a_websocket_client_t *client,const char* header_option) { /* 动态分配握手请求缓冲区 */ char *handshake_req = (char *)malloc(1024); @@ -494,18 +656,114 @@ static int ws_perform_handshake(n706a_websocket_client_t *client) strncpy(client->sec_key, sec_key, sizeof(client->sec_key) - 1); /* 构造握手请求 */ - int len = snprintf(handshake_req, 1024, - "GET %s HTTP/1.1\r\n" - "Host: %s\r\n" - "Upgrade: websocket\r\n" - "Connection: Upgrade\r\n" - "Sec-WebSocket-Key: %s\r\n" - "Sec-WebSocket-Version: 13\r\n" - "\r\n", - client->path, - client->host, - sec_key - ); + /* 注意:path应该包含查询参数(如果有),例如:/path?key=value */ + /* Host头需要包含端口号(与Python代码保持一致) */ + /* + "GET /%s?%s HTTP/1.1\r\n" + "Host: %s:%d\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Key: %s\r\n" + "Sec-WebSocket-Version: 13\r\n" + "Origin: http://%s:%d\r\n" + "%s\r\n" + "\r\n", + */ + /* HTTP请求必须以 \r\n\r\n 结尾(两个 \r\n),第一个结束最后一行,第二个表示请求头结束 */ + int len = 0; + if (header_option != NULL) { //header_option 自带\r\n 注意不能重复添加\r\n,结尾只能又两个\r\n + len = snprintf(handshake_req, 1024, + "GET %s HTTP/1.1\r\n" + "Host: %s:%d\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Key: %s\r\n" + "Sec-WebSocket-Version: 13\r\n" + "Origin: http://%s:%d\r\n" + "%s" + "\r\n", + client->path, client->host, client->port, sec_key, client->host, client->port, header_option); + } else { + len = snprintf(handshake_req, 1024, + "GET %s HTTP/1.1\r\n" + "Host: %s:%d\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Key: %s\r\n" + "Sec-WebSocket-Version: 13\r\n" + "Origin: http://%s:%d\r\n" + "\r\n", + client->path, + client->host, + client->port, + sec_key, + client->host, + client->port + ); + } + /* 调试:打印握手请求(完整ASCII码,\r\n可见) */ + PPP_DEBUG_INFO("[WebSocket] Sending handshake request: %d bytes", len); + + /* 详细调试:打印握手请求的详细内容(默认关闭,编译期移除) */ +#if PPP_DEBUG_VERBOSE_ENABLE + PPP_DEBUG_VERBOSE("[WebSocket] ========== Handshake Request (ASCII) =========="); + /* 打印ASCII码,\r\n显示为可见字符 */ + char *ascii_display = (char *)malloc(len * 4 + 1); // 每个字节最多4个字符(\r\n等) + if (ascii_display) { + int ascii_idx = 0; + for (int i = 0; i < len; i++) { + uint8_t c = (uint8_t)handshake_req[i]; + if (c == '\r') { + ascii_display[ascii_idx++] = '\\'; + ascii_display[ascii_idx++] = 'r'; + } else if (c == '\n') { + ascii_display[ascii_idx++] = '\\'; + ascii_display[ascii_idx++] = 'n'; + } else if (c >= 32 && c < 127) { + ascii_display[ascii_idx++] = c; + } else { + // 其他不可打印字符,显示为\xXX格式 + ascii_display[ascii_idx++] = '\\'; + ascii_display[ascii_idx++] = 'x'; + char hex[3]; + snprintf(hex, sizeof(hex), "%02X", c); + ascii_display[ascii_idx++] = hex[0]; + ascii_display[ascii_idx++] = hex[1]; + } + } + ascii_display[ascii_idx] = '\0'; + PPP_DEBUG_VERBOSE("[WebSocket] %s", ascii_display); + free(ascii_display); + } + + /* 打印HEX格式(每16字节一行) */ + PPP_DEBUG_VERBOSE("[WebSocket] ========== Handshake Request (HEX) =========="); + for (int i = 0; i < len; i += 16) { + char hex_line[128] = {0}; + char ascii_line[32] = {0}; + int hex_idx = 0; + int ascii_idx = 0; + + for (int j = 0; j < 16 && (i + j) < len; j++) { + uint8_t c = (uint8_t)handshake_req[i + j]; + snprintf(hex_line + hex_idx, 4, "%02X ", c); + hex_idx += 3; + + if (c >= 32 && c < 127) { + ascii_line[ascii_idx++] = c; + } else { + ascii_line[ascii_idx++] = '.'; + } + } + ascii_line[ascii_idx] = '\0'; + PPP_DEBUG_VERBOSE("[WebSocket] [%04d] %-48s | %s", i, hex_line, ascii_line); + } + + /* 打印原始字符串(用于对比) */ + PPP_DEBUG_VERBOSE("[WebSocket] ========== Handshake Request (Raw String) =========="); + PPP_DEBUG_VERBOSE("[WebSocket] %s", handshake_req); + PPP_DEBUG_VERBOSE("[WebSocket] ===================================================="); +#endif /* 发送握手请求 */ size_t sent; @@ -517,6 +775,10 @@ static int ws_perform_handshake(n706a_websocket_client_t *client) free(handshake_req); if (ret != N706A_TCP_SUCCESS) { + if (ret == N706A_TCP_ENOTCONN) { + PPP_DEBUG_ERROR("[WebSocket] Socket closed during handshake send"); + client->state = N706A_WS_STATE_CLOSED; + } return -1; } @@ -534,22 +796,110 @@ static int ws_perform_handshake(n706a_websocket_client_t *client) 10000); if (ret != N706A_TCP_SUCCESS || received == 0) { + PPP_DEBUG_ERROR("[WebSocket] Failed to receive handshake response: ret=%d, received=%u", + ret, (unsigned int)received); free(response); return -1; } response[received] = '\0'; - /* 验证响应 */ + /* 关键信息:握手响应接收成功 */ + PPP_DEBUG_INFO("[WebSocket] Handshake response received: %u bytes", (unsigned int)received); + + /* 详细调试:打印响应的详细内容(默认关闭,编译期移除) */ +#if PPP_DEBUG_VERBOSE_ENABLE + PPP_DEBUG_VERBOSE("[WebSocket] ========== Handshake Response (ASCII) =========="); + /* 打印ASCII码,\r\n显示为可见字符 */ + char *ascii_display_resp = (char *)malloc(received * 4 + 1); // 每个字节最多4个字符(\r\n等) + if (ascii_display_resp) { + int ascii_idx = 0; + for (size_t i = 0; i < received; i++) { + uint8_t c = response[i]; + if (c == '\r') { + ascii_display_resp[ascii_idx++] = '\\'; + ascii_display_resp[ascii_idx++] = 'r'; + } else if (c == '\n') { + ascii_display_resp[ascii_idx++] = '\\'; + ascii_display_resp[ascii_idx++] = 'n'; + } else if (c >= 32 && c < 127) { + ascii_display_resp[ascii_idx++] = c; + } else { + // 其他不可打印字符,显示为\xXX格式 + ascii_display_resp[ascii_idx++] = '\\'; + ascii_display_resp[ascii_idx++] = 'x'; + char hex[3]; + snprintf(hex, sizeof(hex), "%02X", c); + ascii_display_resp[ascii_idx++] = hex[0]; + ascii_display_resp[ascii_idx++] = hex[1]; + } + } + ascii_display_resp[ascii_idx] = '\0'; + PPP_DEBUG_VERBOSE("[WebSocket] %s", ascii_display_resp); + free(ascii_display_resp); + } + + /* 打印HEX格式(每16字节一行,使用静态缓冲区避免栈溢出) */ + PPP_DEBUG_VERBOSE("[WebSocket] ========== Handshake Response (HEX) =========="); + static char hex_line[128] = {0}; /* 使用静态缓冲区:16字节 * 3字符/字节 = 48字节,留有余量 */ + static char ascii_line[32] = {0}; /* 使用静态缓冲区:16字节 + 余量 */ + for (size_t i = 0; i < received; i += 16) { + memset(hex_line, 0, sizeof(hex_line)); + memset(ascii_line, 0, sizeof(ascii_line)); + int hex_idx = 0; + int ascii_idx = 0; + + for (int j = 0; j < 16 && (i + j) < received && hex_idx + 3 < sizeof(hex_line) && ascii_idx < sizeof(ascii_line) - 1; j++) { + uint8_t c = response[i + j]; + snprintf(hex_line + hex_idx, 4, "%02X ", c); + hex_idx += 3; + + if (c >= 32 && c < 127) { + ascii_line[ascii_idx++] = c; + } else { + ascii_line[ascii_idx++] = '.'; + } + } + ascii_line[ascii_idx] = '\0'; + PPP_DEBUG_VERBOSE("[WebSocket] [%04zu] %-48s | %s", i, hex_line, ascii_line); + } + + /* 打印原始字符串(用于对比) */ + PPP_DEBUG_VERBOSE("[WebSocket] ========== Handshake Response (Raw String) =========="); + PPP_DEBUG_VERBOSE("[WebSocket] %s", (char *)response); + PPP_DEBUG_VERBOSE("[WebSocket] ===================================================="); +#endif + + /* 检查是否是HTTP错误响应 */ + if (strstr((char *)response, "HTTP/1.1 400") || + strstr((char *)response, "HTTP/1.1 404") || + strstr((char *)response, "HTTP/1.1 500")) { + PPP_DEBUG_ERROR("[WebSocket] Server returned error response"); + free(response); + return -1; + } +/* +HTTP/1.1 101 +Connection: upgrade +Upgrade: websocket +Sec-WebSocket-Accept: i1xQGZhF6iELYH+tPNPGRUz0bYU= +Date: Wed, 19 Nov 2025 07:10:54 GMT +X-Kong-Upstream-Latency: 2 +X-Kong-Proxy-Latency: 0 +Via: kong/2.5.0 +*/ + /* 验证响应(使用不区分大小写的查找,因为 HTTP 头部字段名不区分大小写) */ if (!strstr((char *)response, "HTTP/1.1 101") || - !strstr((char *)response, "Upgrade: websocket") || - !strstr((char *)response, "Connection: Upgrade")) { + !strcasestr((char *)response, "Upgrade: websocket") || + !strcasestr((char *)response, "Connection: Upgrade") + ) { + PPP_DEBUG_ERROR("[WebSocket] Invalid handshake response format"); free(response); return -1; } - /* 验证Sec-WebSocket-Accept */ - const char *accept_start = strstr((char *)response, "Sec-WebSocket-Accept:"); + /* 验证Sec-WebSocket-Accept(使用不区分大小写的查找) */ + const char *accept_start = strcasestr((char *)response, "Sec-WebSocket-Accept:"); if (accept_start) { accept_start += 21; // 跳过"Sec-WebSocket-Accept:" while (*accept_start == ' ') accept_start++; @@ -602,137 +952,444 @@ static int ws_send_frame(n706a_websocket_client_t *client, memcpy(frame + frame_len, mask_key, 4); frame_len += 4; - /* 发送帧头 */ - size_t sent; - n706a_tcp_send(&client->tcp_sock, frame, frame_len, &sent); - - /* 发送payload(掩码后) */ + /* 关键信息:发送帧的基本信息 */ + PPP_DEBUG_INFO("[WebSocket] Sending frame: opcode=0x%02X (%s), payload_len=%u bytes", + opcode, + (opcode == N706A_WS_OPCODE_TEXT) ? "TEXT" : + (opcode == N706A_WS_OPCODE_BINARY) ? "BINARY" : + (opcode == N706A_WS_OPCODE_CLOSE) ? "CLOSE" : + (opcode == N706A_WS_OPCODE_PING) ? "PING" : + (opcode == N706A_WS_OPCODE_PONG) ? "PONG" : "UNKNOWN", + (unsigned int)length); + + /* 详细调试:打印帧头的详细内容(默认关闭,编译期移除) */ +#if PPP_DEBUG_VERBOSE_ENABLE + PPP_DEBUG_VERBOSE("[WebSocket] Frame details: fin=%d, frame_header_len=%u", + fin ? 1 : 0, (unsigned int)frame_len); + + /* 打印帧头HEX内容(使用静态缓冲区避免栈溢出) */ + static char frame_header_hex[64] = {0}; /* 使用静态缓冲区:14字节 * 3字符/字节 = 42字节 */ + memset(frame_header_hex, 0, sizeof(frame_header_hex)); + for (size_t i = 0; i < frame_len && i * 3 + 3 < sizeof(frame_header_hex); i++) { + snprintf(frame_header_hex + i * 3, 4, "%02X ", frame[i]); + } + PPP_DEBUG_VERBOSE("[WebSocket] Frame header (HEX): %s", frame_header_hex); + PPP_DEBUG_VERBOSE("[WebSocket] Mask key: %02X %02X %02X %02X", + mask_key[0], mask_key[1], mask_key[2], mask_key[3]); + + /* 打印payload内容(如果有,使用静态缓冲区避免栈溢出) */ if (data && length > 0) { - uint8_t *masked_data = (uint8_t *)malloc(length); - if (masked_data) { - memcpy(masked_data, data, length); - ws_mask_data(masked_data, length, mask_key); - - n706a_tcp_send(&client->tcp_sock, masked_data, length, &sent); - - free(masked_data); + size_t print_len = (length > 32) ? 32 : length; // 只打印前32字节以节省栈空间 + static char payload_hex[128] = {0}; /* 使用静态缓冲区:32字节 * 3字符/字节 = 96字节 */ + memset(payload_hex, 0, sizeof(payload_hex)); + for (size_t i = 0; i < print_len && i * 3 + 3 < sizeof(payload_hex); i++) { + snprintf(payload_hex + i * 3, 4, "%02X ", data[i]); + } + if (length > 32) { + PPP_DEBUG_VERBOSE("[WebSocket] Payload (first 32 bytes, HEX): %s... (total %u bytes)", + payload_hex, (unsigned int)length); + } else { + PPP_DEBUG_VERBOSE("[WebSocket] Payload (HEX): %s", payload_hex); } + + /* 如果是TEXT帧,也打印ASCII内容(可打印字符,使用静态缓冲区避免栈溢出) */ + if (opcode == N706A_WS_OPCODE_TEXT) { + static char payload_ascii[64] = {0}; /* 使用静态缓冲区:32字节 + 余量 */ + memset(payload_ascii, 0, sizeof(payload_ascii)); + size_t ascii_len = (length < sizeof(payload_ascii) - 1) ? length : sizeof(payload_ascii) - 1; + for (size_t i = 0; i < ascii_len && i < print_len; i++) { + payload_ascii[i] = (data[i] >= 32 && data[i] < 127) ? data[i] : '.'; + } + payload_ascii[ascii_len] = '\0'; + PPP_DEBUG_VERBOSE("[WebSocket] Payload (ASCII): %s", payload_ascii); + } + } +#endif + + size_t total_len = frame_len + length; + uint8_t *send_buffer = (uint8_t *)malloc(total_len); + if (!send_buffer) { + PPP_DEBUG_ERROR("[WebSocket] Failed to allocate frame buffer (%u bytes)", (unsigned int)total_len); + return N706A_WS_ERROR_SEND_FAIL; + } + + memcpy(send_buffer, frame, frame_len); + if (length > 0 && data) { + memcpy(send_buffer + frame_len, data, length); + ws_mask_data(send_buffer + frame_len, length, mask_key); + } + + size_t sent = 0; + n706a_tcp_result_t ret = n706a_tcp_send(&client->tcp_sock, send_buffer, total_len, &sent); + free(send_buffer); + + if (ret != N706A_TCP_SUCCESS || sent != total_len) { + if (ret == N706A_TCP_ENOTCONN) { + PPP_DEBUG_ERROR("[WebSocket] Socket closed during frame send, closing WebSocket connection"); + client->state = N706A_WS_STATE_CLOSING; + if (client->on_error) { + client->on_error(client->client_id, + N706A_WS_ERROR_SEND_FAIL, + "Socket closed during send"); + } + } else { + PPP_DEBUG_ERROR("[WebSocket] Failed to send frame: ret=%d, sent=%u/%u", + ret, (unsigned int)sent, (unsigned int)total_len); + } + return N706A_WS_ERROR_SEND_FAIL; } return N706A_WS_SUCCESS; } /** - * @brief 接收WebSocket帧 + * @brief 接收WebSocket帧(使用 websocket_parser 进行流式解析) */ static int ws_recv_frame(n706a_websocket_client_t *client) { - uint8_t header[2]; - size_t received; + if (!client || !client->parser) { + return -1; + } + + /* 在接收前检查socket状态,如果已关闭则立即返回 */ + if (client->tcp_sock.state == N706A_TCP_STATE_CLOSED) { + PPP_DEBUG_WARN("[WebSocket] Socket already CLOSED before recv, updating WebSocket state to CLOSING"); + client->state = N706A_WS_STATE_CLOSING; + return -1; + } - /* 接收帧头前2字节 */ + /* 从 TCP 接收数据到缓冲区 */ + /* 优化:使用较长的超时时间(60秒),完全依赖URC机制等待数据,不再主动轮询 */ + /* 这样可以降低功耗和AT命令开销,适合小系统 */ + /* TCP接收函数会阻塞在URC信号量上,直到数据到达或超时 */ + size_t received = 0; + PPP_DEBUG_VERBOSE("[WebSocket] Calling n706a_tcp_recv: buffer_size=%u, timeout=60000ms (fully relying on URC)", + (unsigned int)client->recv_buffer_size); n706a_tcp_result_t ret = n706a_tcp_recv(&client->tcp_sock, - header, - 2, + client->recv_buffer, + client->recv_buffer_size, &received, - 5000); + 60000); /* 60秒超时,完全依赖URC机制 */ - if (ret != N706A_TCP_SUCCESS || received < 2) { + PPP_DEBUG_VERBOSE("[WebSocket] n706a_tcp_recv returned: ret=%d, received=%u bytes", ret, (unsigned int)received); + + /* 接收后再次检查socket状态(可能在接收过程中被关闭) */ + if (client->tcp_sock.state == N706A_TCP_STATE_CLOSED) { + PPP_DEBUG_WARN("[WebSocket] Socket CLOSED during recv, updating WebSocket state to CLOSING"); + client->state = N706A_WS_STATE_CLOSING; return -1; } - bool fin = (header[0] & WS_FIN_BIT) != 0; - n706a_ws_opcode_t opcode = (n706a_ws_opcode_t)(header[0] & WS_OPCODE_MASK); - bool masked = (header[1] & WS_MASK_BIT) != 0; - uint64_t payload_len = header[1] & WS_PAYLOAD_MASK; - - /* 扩展payload长度 */ - if (payload_len == 126) { - uint8_t len_buf[2]; - n706a_tcp_recv(&client->tcp_sock, len_buf, 2, &received, 5000); - payload_len = (len_buf[0] << 8) | len_buf[1]; - } else if (payload_len == 127) { - uint8_t len_buf[8]; - n706a_tcp_recv(&client->tcp_sock, len_buf, 8, &received, 5000); - payload_len = 0; - for (int i = 0; i < 8; i++) { - payload_len = (payload_len << 8) | len_buf[i]; - } + if (ret == N706A_TCP_ETIMEDOUT) { + /* 超时:没有数据到达,这是正常的,继续等待 */ + /* 注意:超时不应该断开连接,应该继续循环等待 */ + PPP_DEBUG_DEBUG("[WebSocket] TCP recv timeout (no data), continuing to wait"); + return 0; /* 返回 0 表示没有错误,只是没有数据 */ } - /* 接收masking key(如果有) */ - uint8_t mask_key[4] = {0}; - if (masked) { - n706a_tcp_recv(&client->tcp_sock, mask_key, 4, &received, 5000); - if (received < 4) { - return -1; - } + if (ret == N706A_TCP_ENOTCONN) { + /* Socket已关闭,这是真正的错误 */ + PPP_DEBUG_ERROR("[WebSocket] Socket closed during recv (ENOTCONN), updating WebSocket state to CLOSING"); + client->state = N706A_WS_STATE_CLOSING; + return -1; } - /* 接收payload */ - if (payload_len > 0) { - uint8_t *payload = (uint8_t *)malloc(payload_len + 1); - if (!payload) { - return -1; - } + if (ret != N706A_TCP_SUCCESS) { + /* 其他错误 */ + PPP_DEBUG_ERROR("[WebSocket] TCP recv error: ret=%d", ret); + return -1; + } + + if (received == 0) { + /* 接收到0字节,可能是连接关闭或暂时没有数据 */ + /* 注意:如果socket已关闭,n706a_tcp_recv应该返回ENOTCONN,而不是SUCCESS with received=0 */ + /* 这里收到0字节可能是正常的(暂时没有数据),继续等待 */ + PPP_DEBUG_DEBUG("[WebSocket] TCP recv returned 0 bytes, continuing to wait"); + return 0; /* 返回 0 表示没有错误,只是没有数据 */ + } + + /* 详细调试:打印接收到的原始数据(默认关闭,编译期移除) */ +#if PPP_DEBUG_VERBOSE_ENABLE + PPP_DEBUG_VERBOSE("[WebSocket] Received TCP data: %u bytes", (unsigned int)received); + /* 注意:使用静态缓冲区避免栈溢出,只打印前32字节 */ + size_t print_len = (received > 32) ? 32 : received; /* 只打印前32字节以节省栈空间 */ + static char recv_hex[128] = {0}; /* 使用静态缓冲区:32字节 * 3字符/字节 = 96字节,留有余量 */ + memset(recv_hex, 0, sizeof(recv_hex)); + for (size_t i = 0; i < print_len && i * 3 + 3 < sizeof(recv_hex); i++) { + snprintf(recv_hex + i * 3, 4, "%02X ", client->recv_buffer[i]); + } + if (received > 32) { + PPP_DEBUG_VERBOSE("[WebSocket] Received TCP data (first 32 bytes, HEX): %s... (total %u bytes)", + recv_hex, (unsigned int)received); + } else { + PPP_DEBUG_VERBOSE("[WebSocket] Received TCP data (HEX): %s", recv_hex); + } +#endif + + /* 使用 websocket_parser 进行流式解析 */ + /* 注意:ws_parse_stream 返回处理的字节数(>=0),参数无效时返回 -1 */ + PPP_DEBUG_VERBOSE("[WebSocket] Calling ws_parse_stream: received=%u bytes", (unsigned int)received); + int processed = ws_parse_stream(client->parser, client->recv_buffer, received); + PPP_DEBUG_VERBOSE("[WebSocket] ws_parse_stream returned: processed=%d bytes", processed); + + if (processed < 0) { + /* 参数无效 */ + PPP_DEBUG_ERROR("[WebSocket] Invalid parameters for ws_parse_stream"); + return -1; + } + + /* 检查解析器状态 */ + int parser_state = ws_parser_get_state(client->parser); + bool has_error = ws_parser_has_error(client->parser); + PPP_DEBUG_VERBOSE("[WebSocket] Parser state: %d, has_error=%d", parser_state, has_error ? 1 : 0); + + /* 检查解析器是否处于错误状态 */ + if (has_error) { + PPP_DEBUG_ERROR("[WebSocket] Frame parsing error for client %d", client->client_id); + ws_parser_reset(client->parser); + return -1; + } + + /* 检查是否解析完成一个完整的帧 */ + if (parser_state == WS_STATE_COMPLETE) { + PPP_DEBUG_VERBOSE("[WebSocket] ✅ Frame parsing complete! Extracting frame data..."); + const ws_header_t *header = ws_get_header(client->parser); + const uint8_t *payload = ws_get_payload(client->parser); + size_t payload_len = ws_get_payload_length(client->parser); - n706a_tcp_recv(&client->tcp_sock, payload, payload_len, &received, 10000); - if (received < payload_len) { - free(payload); + PPP_DEBUG_VERBOSE("[WebSocket] Extracted frame: header=%p, payload=%p, payload_len=%u", + (void *)header, (void *)payload, (unsigned int)payload_len); + + if (!header) { + PPP_DEBUG_ERROR("[WebSocket] Failed to get frame header"); + ws_parser_reset(client->parser); return -1; } - payload[payload_len] = '\0'; - /* 如果数据被掩码,需要解掩码 */ - if (masked) { - ws_mask_data(payload, payload_len, mask_key); + PPP_DEBUG_VERBOSE("[WebSocket] Frame header: FIN=%d, RSV1=%d, RSV2=%d, RSV3=%d, opcode=0x%02X, mask=%d, payload_len=%u", + header->fin ? 1 : 0, + header->rsv1 ? 1 : 0, + header->rsv2 ? 1 : 0, + header->rsv3 ? 1 : 0, + header->opcode, + header->mask ? 1 : 0, + (unsigned int)payload_len); + + /* 映射 websocket_parser 的 opcode 到 n706a_ws_opcode_t */ + n706a_ws_opcode_t opcode; + switch (header->opcode) { + case WS_CONTINUATION: + opcode = N706A_WS_OPCODE_CONTINUATION; + break; + case WS_TEXT: + opcode = N706A_WS_OPCODE_TEXT; + break; + case WS_BINARY: + opcode = N706A_WS_OPCODE_BINARY; + break; + case WS_CLOSE: + opcode = N706A_WS_OPCODE_CLOSE; + break; + case WS_PING: + opcode = N706A_WS_OPCODE_PING; + break; + case WS_PONG: + opcode = N706A_WS_OPCODE_PONG; + break; + default: + PPP_DEBUG_ERROR("[WebSocket] Unknown opcode: 0x%02X", header->opcode); + ws_parser_reset(client->parser); + return -1; } /* 处理不同类型的帧 */ switch (opcode) { + case N706A_WS_OPCODE_CONTINUATION: + /* CONTINUATION 帧:继续之前的分片消息 */ + PPP_DEBUG_VERBOSE("[WebSocket] Received CONTINUATION frame: payload_len=%u, fin=%d", + (unsigned int)payload_len, header->fin ? 1 : 0); + if (client->on_message && payload) { + PPP_DEBUG_VERBOSE("[WebSocket] Calling on_message callback for CONTINUATION: client_id=%d, payload_len=%u", + client->client_id, (unsigned int)payload_len); + /* 对于 TEXT 消息,确保数据以 \0 结尾(使用 recv_buffer 作为临时缓冲区) */ + const char *message_data = (const char *)payload; + if (client->fragment_opcode == N706A_WS_OPCODE_TEXT && payload_len > 0) { + /* 检查 recv_buffer 是否有足够空间(至少需要 payload_len + 1) */ + if (payload_len + 1 <= client->recv_buffer_size) { + memcpy(client->recv_buffer, payload, payload_len); + client->recv_buffer[payload_len] = '\0'; + message_data = (const char *)client->recv_buffer; + } else { + PPP_DEBUG_WARN("[WebSocket] Payload too large for null-termination buffer (%u > %u), passing as-is", + (unsigned int)payload_len, (unsigned int)client->recv_buffer_size); + } + } + /* 使用之前保存的 fragment_opcode */ + client->on_message(client->client_id, + message_data, + payload_len, + header->fin ? true : false, + client->fragment_opcode == N706A_WS_OPCODE_BINARY); + PPP_DEBUG_VERBOSE("[WebSocket] on_message callback for CONTINUATION returned"); + } + /* 如果这是最后一帧(FIN=1),重置 fragment_opcode */ + if (header->fin) { + client->fragment_opcode = N706A_WS_OPCODE_TEXT; /* 重置为默认值 */ + } + break; + case N706A_WS_OPCODE_TEXT: case N706A_WS_OPCODE_BINARY: - if (client->on_message) { + /* 保存初始 opcode,用于后续的 CONTINUATION 帧 */ + client->fragment_opcode = opcode; + + /* 关键信息:接收到的消息 */ + PPP_DEBUG_INFO("[WebSocket] Received %s message: payload_len=%u bytes", + opcode == N706A_WS_OPCODE_TEXT ? "TEXT" : "BINARY", + (unsigned int)payload_len); + /* 详细调试:打印消息内容(默认关闭,编译期移除) */ +#if PPP_DEBUG_VERBOSE_ENABLE + if (payload && payload_len > 0) { + /* 注意:使用静态缓冲区避免栈溢出,只打印前32字节 */ + size_t print_len = (payload_len > 32) ? 32 : payload_len; /* 只打印前32字节 */ + static char msg_hex[128] = {0}; /* 使用静态缓冲区:32字节 * 3字符/字节 = 96字节 */ + static char msg_ascii[64] = {0}; /* 使用静态缓冲区:32字节 + 余量 */ + memset(msg_hex, 0, sizeof(msg_hex)); + memset(msg_ascii, 0, sizeof(msg_ascii)); + + for (size_t i = 0; i < print_len && i * 3 + 3 < sizeof(msg_hex) && i < sizeof(msg_ascii) - 1; i++) { + snprintf(msg_hex + i * 3, 4, "%02X ", payload[i]); + msg_ascii[i] = (payload[i] >= 32 && payload[i] < 127) ? payload[i] : '.'; + } + msg_ascii[print_len < sizeof(msg_ascii) - 1 ? print_len : sizeof(msg_ascii) - 1] = '\0'; + + if (payload_len > 32) { + PPP_DEBUG_VERBOSE("[WebSocket] Message content (first 32 bytes): HEX=%s... ASCII=%s...", + msg_hex, msg_ascii); + } else { + PPP_DEBUG_VERBOSE("[WebSocket] Message content: HEX=%s ASCII=%s", + msg_hex, msg_ascii); + } + } +#endif + + if (client->on_message && payload) { + PPP_DEBUG_VERBOSE("[WebSocket] Calling on_message callback: client_id=%d, payload_len=%u, fin=%d, is_binary=%d", + client->client_id, (unsigned int)payload_len, header->fin ? 1 : 0, + opcode == N706A_WS_OPCODE_BINARY ? 1 : 0); + /* 对于 TEXT 消息,确保数据以 \0 结尾(使用 recv_buffer 作为临时缓冲区) */ + const char *message_data = (const char *)payload; + if (opcode == N706A_WS_OPCODE_TEXT && payload_len > 0) { + /* 检查 recv_buffer 是否有足够空间(至少需要 payload_len + 1) */ + if (payload_len + 1 <= client->recv_buffer_size) { + memcpy(client->recv_buffer, payload, payload_len); + client->recv_buffer[payload_len] = '\0'; + message_data = (const char *)client->recv_buffer; + PPP_DEBUG_VERBOSE("[WebSocket] TEXT message null-terminated in recv_buffer (%u bytes)", + (unsigned int)payload_len); + } else { + PPP_DEBUG_WARN("[WebSocket] Payload too large for null-termination buffer (%u > %u), passing as-is", + (unsigned int)payload_len, (unsigned int)client->recv_buffer_size); + } + } client->on_message(client->client_id, - (char *)payload, + message_data, payload_len, - fin, + header->fin ? true : false, opcode == N706A_WS_OPCODE_BINARY); + PPP_DEBUG_VERBOSE("[WebSocket] on_message callback returned"); + } else { + if (!client->on_message) { + PPP_DEBUG_WARN("[WebSocket] on_message callback is NULL, message not delivered!"); + } + if (!payload) { + PPP_DEBUG_WARN("[WebSocket] payload is NULL, message not delivered!"); + } + } + /* 如果这是最后一帧(FIN=1),重置 fragment_opcode */ + if (header->fin) { + client->fragment_opcode = N706A_WS_OPCODE_TEXT; /* 重置为默认值 */ } break; case N706A_WS_OPCODE_PING: - /* 自动回复Pong */ - ws_send_frame(client, N706A_WS_OPCODE_PONG, payload, payload_len, true); + /* 自动回复 Pong */ + if (payload) { + ws_send_frame(client, N706A_WS_OPCODE_PONG, payload, payload_len, true); + } else { + ws_send_frame(client, N706A_WS_OPCODE_PONG, NULL, 0, true); + } break; case N706A_WS_OPCODE_PONG: - /* 忽略Pong */ + /* 忽略 Pong(心跳响应) */ + PPP_DEBUG_VERBOSE("[WebSocket] Received Pong frame"); break; case N706A_WS_OPCODE_CLOSE: /* 服务器关闭连接 */ + PPP_DEBUG_INFO("[WebSocket] Received CLOSE frame from server, closing connection"); client->state = N706A_WS_STATE_CLOSING; uint16_t close_code = N706A_WS_CLOSE_NO_STATUS; - if (payload_len >= 2) { + if (payload && payload_len >= 2) { close_code = (payload[0] << 8) | payload[1]; + PPP_DEBUG_INFO("[WebSocket] Close code: %u", close_code); + } + + /* 根据 RFC 6455,收到 CLOSE 帧后应该发送 CLOSE 帧响应 */ + /* 注意:这里不直接调用 n706a_websocket_close,因为会关闭 TCP 连接 */ + /* 而是发送 CLOSE 帧响应,然后让接收线程退出时关闭 TCP 连接 */ + if (payload && payload_len >= 2) { + /* 回显相同的关闭码 */ + uint8_t close_response[128]; + close_response[0] = (close_code >> 8) & 0xFF; + close_response[1] = close_code & 0xFF; + size_t close_response_len = 2; + + /* 如果有原因,也回显 */ + if (payload_len > 2) { + size_t reason_len = payload_len - 2; + if (reason_len < sizeof(close_response) - 2) { + memcpy(close_response + 2, payload + 2, reason_len); + close_response_len += reason_len; + } + } + + ws_send_frame(client, N706A_WS_OPCODE_CLOSE, close_response, close_response_len, true); + } else { + /* 没有关闭码,发送空的 CLOSE 帧 */ + ws_send_frame(client, N706A_WS_OPCODE_CLOSE, NULL, 0, true); } if (client->on_close) { client->on_close(client->client_id, close_code, - payload_len > 2 ? (char *)(payload + 2) : NULL); + (payload && payload_len > 2) ? (char *)(payload + 2) : NULL); } break; default: + PPP_DEBUG_WARN("[WebSocket] Unhandled opcode: 0x%02X", opcode); break; } - free(payload); + /* 重置解析器,准备解析下一帧 */ + ws_parser_reset(client->parser); + + PPP_DEBUG_VERBOSE("[WebSocket] ✅ Frame processing completed successfully, ready for next frame"); + return 0; /* 成功解析一帧 */ + } else if (parser_state == WS_STATE_ERROR) { + /* 解析错误 */ + PPP_DEBUG_ERROR("[WebSocket] ❌ Parser in error state, resetting parser"); + ws_parser_reset(client->parser); + return -1; + } else { + /* 数据不完整,需要继续接收 */ + /* 注意:这里返回 0 表示没有错误,但帧还未完成 */ + /* 下次调用时会继续解析 */ + PPP_DEBUG_VERBOSE("[WebSocket] Frame incomplete (state=%d), need more data, continuing...", parser_state); + return 0; } - - return 0; } /** @@ -740,7 +1397,8 @@ static int ws_recv_frame(n706a_websocket_client_t *client) */ static void ws_generate_mask_key(uint8_t *mask_key) { - for (int i = 0; i < 4; i++) { + int i; + for (i = 0; i < 4; i++) { mask_key[i] = rand() & 0xFF; } } @@ -757,6 +1415,9 @@ static void ws_mask_data(uint8_t *data, size_t len, const uint8_t *mask_key) /** * @brief 解析WebSocket URL + * + * 解析格式:ws://host:port/path?query#fragment 或 wss://host:port/path?query#fragment + * path 会包含完整的路径、query参数和fragment(如果存在) */ static int ws_parse_url(const char *url, char *protocol, char *host, uint16_t *port, char *path) @@ -780,34 +1441,79 @@ static int ws_parse_url(const char *url, char *protocol, char *host, return -1; } - /* 解析host */ + /* 解析host、port和path */ + /* 查找路径开始位置(第一个 '/') */ const char *path_start = strchr(p, '/'); + /* 查找端口号开始位置(第一个 ':',但必须在路径之前) */ const char *port_start = strchr(p, ':'); + /* 如果找到端口号,且端口号在路径之前(或没有路径) */ if (port_start && (!path_start || port_start < path_start)) { + /* 提取host(从当前位置到端口号) */ size_t host_len = port_start - p; + if (host_len >= 128) { + host_len = 127; // host缓冲区大小限制 + } strncpy(host, p, host_len); host[host_len] = '\0'; - *port = atoi(port_start + 1); + /* 解析端口号(从 ':' 后开始,到路径或字符串结束) */ + const char *port_end = path_start ? path_start : (p + strlen(p)); + size_t port_str_len = port_end - (port_start + 1); + if (port_str_len > 0 && port_str_len < 10) { + char port_str[16] = {0}; + strncpy(port_str, port_start + 1, port_str_len); + port_str[port_str_len] = '\0'; + *port = (uint16_t)atoi(port_str); + } + /* 提取path(包含query参数和fragment,如果有) */ if (path_start) { - strcpy(path, path_start); + /* 计算path长度(包括query和fragment) */ + size_t path_len = strlen(path_start); + if (path_len >= N706A_WS_MAX_URL_LEN) { + path_len = N706A_WS_MAX_URL_LEN - 1; // path缓冲区大小限制(N706A_WS_MAX_URL_LEN字节,包括'\0') + PPP_DEBUG_WARN("[WebSocket] Path too long, truncated to %u bytes", (unsigned int)path_len); + } + strncpy(path, path_start, path_len); + path[path_len] = '\0'; } else { strcpy(path, "/"); } } else { + /* 没有端口号,或路径在端口号之前(异常情况) */ if (path_start) { + /* 提取host(从当前位置到路径) */ size_t host_len = path_start - p; + if (host_len >= 128) { + host_len = 127; // host缓冲区大小限制 + } strncpy(host, p, host_len); host[host_len] = '\0'; - strcpy(path, path_start); + + /* 提取path(包含query参数和fragment,如果有) */ + size_t path_len = strlen(path_start); + if (path_len >= N706A_WS_MAX_URL_LEN) { + path_len = N706A_WS_MAX_URL_LEN - 1; // path缓冲区大小限制(N706A_WS_MAX_URL_LEN字节,包括'\0') + PPP_DEBUG_WARN("[WebSocket] Path too long, truncated to %u bytes", (unsigned int)path_len); + } + strncpy(path, path_start, path_len); + path[path_len] = '\0'; } else { - strcpy(host, p); + /* 没有路径,只有host */ + size_t host_len = strlen(p); + if (host_len >= 128) { + host_len = 127; + } + strncpy(host, p, host_len); + host[host_len] = '\0'; strcpy(path, "/"); } } + PPP_DEBUG_VERBOSE("[WebSocket] Parsed URL: protocol=%s, host=%s, port=%u, path=%s", + protocol, host, *port, path); + return 0; } @@ -985,24 +1691,38 @@ static void sha1_simple(const unsigned char *input, size_t len, unsigned char ou } /** - * @brief WebSocket接收线程函数 + * @brief WebSocket接收线程函数(内部实现) * * 持续接收WebSocket帧并调用回调函数通知应用层 */ -static void *ws_recv_thread_func(void *arg) +static void ws_recv_thread_func_impl(n706a_websocket_client_t *client) { - n706a_websocket_client_t *client = (n706a_websocket_client_t *)arg; if (!client) { - return NULL; + return; } - PPP_DEBUG_INFO("WebSocket receive thread started for client %d", client->client_id); + PPP_DEBUG_INFO("[WebSocket] ✅ Receive thread started for client %d", client->client_id); + uint32_t loop_count = 0; while (client->running && client->state == N706A_WS_STATE_OPEN) { + loop_count++; +#if PPP_DEBUG_VERBOSE_ENABLE + if (loop_count % 100 == 0) { + /* 每100次循环打印一次,确认线程在运行(详细调试,编译期移除) */ + PPP_DEBUG_VERBOSE("[WebSocket] Receive thread loop #%u: client_id=%d, state=%d, running=%d", + loop_count, client->client_id, client->state, client->running ? 1 : 0); + } +#endif + /* 尝试接收一帧数据 */ + PPP_DEBUG_VERBOSE("[WebSocket] Receive thread loop #%u: calling ws_recv_frame...", loop_count); int ret = ws_recv_frame(client); + PPP_DEBUG_VERBOSE("[WebSocket] Receive thread loop #%u: ws_recv_frame returned %d", loop_count, ret); if (ret != 0) { + if (client->state == N706A_WS_STATE_OPEN) { + ws_send_close_if_needed(client, N706A_WS_CLOSE_ABNORMAL, "Receive frame failed"); + } /* 接收失败,可能是连接断开或超时 */ if (client->running) { PPP_DEBUG_WARN("WebSocket receive frame failed for client %d (ret=%d)", @@ -1021,28 +1741,42 @@ static void *ws_recv_thread_func(void *arg) } } - /* 如果状态变为CLOSING,退出循环 */ + /* 如果状态变为CLOSING,退出循环并关闭连接 */ if (client->state == N706A_WS_STATE_CLOSING || client->state == N706A_WS_STATE_CLOSED) { break; } - /* 短暂延迟,避免CPU占用过高 */ + /* 优化:移除延迟,让线程在TCP接收函数中完全阻塞 + * TCP接收函数会阻塞在URC信号量上,直到数据到达或超时 + * 这样可以达到最低功耗和CPU占用,适合小系统 + * + * 如果ret==0(成功但没有数据),说明TCP接收超时返回 + * 此时不需要延迟,直接再次调用TCP接收,让它继续阻塞等待 + * + * 如果ret!=0(错误),说明连接可能断开,短暂延迟后重试 + */ + if (ret != 0) { + /* 有错误,短暂延迟后重试(避免快速重试导致CPU占用过高) */ #ifdef __unix__ - usleep(1000); // 1ms + usleep(1000); // 1ms #elif defined(__rtthread__) - rt_thread_mdelay(1); + rt_thread_mdelay(1); #else - /* 其他平台:简单延迟循环 */ - volatile int i; - for (i = 0; i < 1000; i++); + osDelay(1); /* 1ms延迟 */ #endif + } + /* ret==0时不需要延迟,直接继续循环,让TCP接收函数阻塞等待URC */ } PPP_DEBUG_INFO("WebSocket receive thread stopped for client %d", client->client_id); - /* 如果是因为接收失败而退出,关闭连接 */ - if (client->state == N706A_WS_STATE_OPEN || client->state == N706A_WS_STATE_CLOSING) { + /* 如果是因为接收失败或发送失败而退出,关闭连接 */ + if (client->state == N706A_WS_STATE_OPEN || + client->state == N706A_WS_STATE_CLOSING) { + /* 关闭 TCP 连接 */ + n706a_tcp_close(&client->tcp_sock); + client->state = N706A_WS_STATE_CLOSED; /* 调用关闭回调 */ @@ -1052,9 +1786,34 @@ static void *ws_recv_thread_func(void *arg) "Receive thread exited"); } } - +} + +#ifdef __unix__ +/** + * @brief WebSocket接收线程函数(POSIX线程包装) + */ +static void *ws_recv_thread_func(void *arg) +{ + ws_recv_thread_func_impl((n706a_websocket_client_t *)arg); return NULL; } +#elif defined(__rtthread__) +/** + * @brief WebSocket接收线程函数(RT-Thread包装) + */ +static void ws_recv_thread_func(void *arg) +{ + ws_recv_thread_func_impl((n706a_websocket_client_t *)arg); +} +#else +/** + * @brief WebSocket接收线程函数(CMSIS OS包装) + */ +static void ws_recv_thread_func(void *arg) +{ + ws_recv_thread_func_impl((n706a_websocket_client_t *)arg); +} +#endif /** * @brief Base64编码 @@ -1084,7 +1843,8 @@ static void base64_encode(const unsigned char *input, size_t len, char *output) } if (i) { - for (size_t k = i; k < 3; k++) { + size_t k; + for (k = i; k < 3; k++) { char_array_3[k] = '\0'; } @@ -1092,7 +1852,7 @@ static void base64_encode(const unsigned char *input, size_t len, char *output) char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); - for (size_t k = 0; k < i + 1; k++) { + for (k = 0; k < i + 1; k++) { output[j++] = base64_table[char_array_4[k]]; } diff --git a/code/sdk/zhihui/n706a_net/src/ppp_interrupt.c b/code/sdk/zhihui/n706a_net/src/ppp_interrupt.c index b7150ef2c..3a78bce3e 100644 --- a/code/sdk/zhihui/n706a_net/src/ppp_interrupt.c +++ b/code/sdk/zhihui/n706a_net/src/ppp_interrupt.c @@ -235,11 +235,11 @@ static void ppp_irq_process_thread(void *arg) // 只打印前16字节 PPP_DEBUG_PRINT(" Data: "); - for (uint32_t i = 0; i < msg.length && i < 16; i++) { + for (uint32_t i = 0; i < msg.length && i < 32; i++) { PPP_DEBUG_PRINT("%02X ", buffer->data[i]); } - if (msg.length > 16) { - PPP_DEBUG_PRINT("... (%u more bytes)", msg.length - 16); + if (msg.length > 32) { + PPP_DEBUG_PRINT("... (%u more bytes)", msg.length - 32); } PPP_DEBUG_PRINT("\n"); diff --git a/code/sdk/zhihui/n706a_net/src/ppp_power_ctl.c b/code/sdk/zhihui/n706a_net/src/ppp_power_ctl.c index 67169b612..d144d791b 100644 --- a/code/sdk/zhihui/n706a_net/src/ppp_power_ctl.c +++ b/code/sdk/zhihui/n706a_net/src/ppp_power_ctl.c @@ -346,7 +346,7 @@ void ppp_api_wakeup_cat1(uint8_t wakeup) uint8_t wake_value = g_ppp_cat1_ctl_pins.wakemodem_pin_en_value; if (wake_pin == PIN_NONE) { - PPP_DEBUG_PRINT("PPP: wakeup_cat1 wake_pin is none\n"); + PPP_DEBUG_CRITICAL("PPP: ⚠️ wakeup_cat1 wake_pin is none, wakeup=%d\n", wakeup); return; } @@ -354,17 +354,19 @@ void ppp_api_wakeup_cat1(uint8_t wakeup) if (wakeup) { if (wake_value) { uapi_gpio_set_val(wake_pin, GPIO_LEVEL_HIGH); + PPP_DEBUG_CRITICAL("PPP: 🔔 Wakeup Cat1: pin=%d, level=HIGH (wake_value=1)\n", wake_pin); } else { uapi_gpio_set_val(wake_pin, GPIO_LEVEL_LOW); + PPP_DEBUG_CRITICAL("PPP: 🔔 Wakeup Cat1: pin=%d, level=LOW (wake_value=0)\n", wake_pin); } - PPP_DEBUG_PRINT("PPP: Wakeup Cat1 pin %d\n", wake_pin); } else { if (wake_value) { uapi_gpio_set_val(wake_pin, GPIO_LEVEL_LOW); + PPP_DEBUG_CRITICAL("PPP: 🔕 Release Wakeup Cat1: pin=%d, level=LOW (wake_value=1)\n", wake_pin); } else { uapi_gpio_set_val(wake_pin, GPIO_LEVEL_HIGH); + PPP_DEBUG_CRITICAL("PPP: 🔕 Release Wakeup Cat1: pin=%d, level=HIGH (wake_value=0)\n", wake_pin); } - PPP_DEBUG_PRINT("PPP: Release Wakeup Cat1 pin %d\n", wake_pin); } } @@ -395,57 +397,24 @@ errcode_t ppp_register_ctl_pin(ppp_ctl_pin_4g_t *ctl_pin) } /** - * @brief PPP模块Cat1唤醒中断回调函数(带去抖处理) + * @brief PPP模块Cat1唤醒中断回调函数(无防抖,收到就打印) */ void ppp_ring_isr_callback(pin_t pin, uintptr_t param) { (void)param; - // ⚠️ 去抖处理:防止信号抖动导致频繁中断 - static uint64_t last_interrupt_time = 0; - static gpio_level_t last_level = GPIO_LEVEL_LOW; - static uint32_t same_level_count = 0; - - uint64_t current_time = uapi_tcxo_get_ms(); // 获取当前时间(毫秒) + // 直接读取电平并打印,不做防抖处理 gpio_level_t val = uapi_gpio_get_val(pin); - // 去抖时间窗口:50ms(可根据实际情况调整) - #define RING_DEBOUNCE_TIME_MS 50 - - // 检查是否在去抖时间内 - if ((current_time - last_interrupt_time) < RING_DEBOUNCE_TIME_MS) { - // 在去抖时间内,检查电平是否一致 - if (val == last_level) { - same_level_count++; - // 如果同一电平连续触发超过5次,打印警告 - if (same_level_count == 5) { - PPP_DEBUG_WARN("PPP: ⚠️ Ring pin %d signal bouncing (level=%d, count=%u)\n", - pin, val, same_level_count); - } - } else { - same_level_count = 1; - last_level = val; - } - - // 清除中断标志,但不处理 - // uapi_gpio_clear_interrupt(pin); - // return; - } - - // 去抖时间已过,处理中断 - last_interrupt_time = current_time; - last_level = val; - same_level_count = 1; - if (val == GPIO_LEVEL_HIGH) { - PPP_DEBUG_WARN("PPP: Cat1 ring interrupt on pin %d → HIGH \n", pin); + PPP_DEBUG_WARN("PPP: Cat1 ring interrupt on pin %d → HIGH\n", pin); cat1_api_add_cat1_vote(); -} else { + cat1_api_wakeup_cat1(true); + } else { PPP_DEBUG_WARN("PPP: Cat1 ring interrupt on pin %d → LOW\n", pin); //idle后 remove } uapi_gpio_clear_interrupt(pin); - } /** diff --git a/code/sdk/zhihui/n706a_net/src/ppp_serial.c b/code/sdk/zhihui/n706a_net/src/ppp_serial.c index 4d3049854..142d38469 100644 --- a/code/sdk/zhihui/n706a_net/src/ppp_serial.c +++ b/code/sdk/zhihui/n706a_net/src/ppp_serial.c @@ -164,6 +164,7 @@ ppp_error_t ppp_serial_init(const ppp_serial_config_t *config) // - 中断模式(SUPPORT_PPP_SERVICE_INTERRUPT):uart_porting.c中已完整初始化UART, // 这里只需要设置波特率 + errcode_t ret; #if defined(SUPPORT_PPP_SERVICE_POLLING) // 轮询模式:完整初始化UART(注意:DMA已在chip_init.c:152-153初始化,不要重复调用) PPP_DEBUG_WARN("Polling mode: Initializing UART for the first time"); @@ -173,13 +174,13 @@ ppp_error_t ppp_serial_init(const ppp_serial_config_t *config) g_serial_ctx.extra_attr.rx_dma_enable = UART_DMA_RX_ENABLE; g_serial_ctx.extra_attr.rx_int_threshold = 0; PPP_DEBUG_WARN("Polling mode with DMA: TX DMA and RX DMA both enabled"); - errcode_t ret = uapi_uart_init(g_serial_ctx.uart_bus, + ret = uapi_uart_init(g_serial_ctx.uart_bus, &g_serial_ctx.pin_config, &g_serial_ctx.uart_attr, &g_serial_ctx.extra_attr, &g_serial_ctx.buffer_config); #else - errcode_t ret = uapi_uart_init(g_serial_ctx.uart_bus, + ret = uapi_uart_init(g_serial_ctx.uart_bus, &g_serial_ctx.pin_config, &g_serial_ctx.uart_attr, NULL, @@ -188,7 +189,7 @@ ppp_error_t ppp_serial_init(const ppp_serial_config_t *config) #elif defined(SUPPORT_PPP_SERVICE_INTERRUPT) || defined(SUPPORT_N706A_NET) // 中断模式/N706A_NET模式:UART已在uart_porting.c:uart_config_init()中初始化,只设置波特率 PPP_DEBUG_WARN("Interrupt/N706A_NET mode: UART already initialized, updating baudrate"); - errcode_t ret = uapi_uart_set_attr(g_serial_ctx.uart_bus, &g_serial_ctx.uart_attr); + ret = uapi_uart_set_attr(g_serial_ctx.uart_bus, &g_serial_ctx.uart_attr); #else #error "SUPPORT_PPP_SERVICE_POLLING, SUPPORT_PPP_SERVICE_INTERRUPT, or SUPPORT_N706A_NET must be defined" #endif @@ -281,7 +282,7 @@ void check_cat1_uart_wakeup(void) /* 如需清空缓冲区,请使用 ppp_serial_flush_rx() */ /** - * @brief 发送数据 - 两种模式都直接通过UART发送 + * @brief 发送数据 - n706a_net 只使用 AT 模式,直接通过 UART 发送 */ ppp_error_t ppp_serial_write(const uint8_t *data, uint32_t length) { @@ -293,81 +294,35 @@ ppp_error_t ppp_serial_write(const uint8_t *data, uint32_t length) PPP_DEBUG_DEBUG("=== PPP Serial Write Start ==="); #if defined(PPP_LOW_POWER_MODE) && (PPP_LOW_POWER_MODE == 1) + PPP_DEBUG_CRITICAL("PPP Serial Write: Waking up CAT1 module before sending data..."); ppp_api_wakeup_cat1(true); osal_msleep(1); + PPP_DEBUG_CRITICAL("PPP Serial Write: CAT1 wakeup completed, proceeding to send data"); + #else + PPP_DEBUG_CRITICAL("PPP Serial Write: PPP_LOW_POWER_MODE is disabled, skipping wakeup"); #endif - PPP_DEBUG_DEBUG("Write request: %u bytes in mode %d", length, g_serial_ctx.current_mode); + PPP_DEBUG_DEBUG("Write request: %u bytes (AT mode)", length); PPP_DEBUG_DEBUG("UART Bus: %u", g_serial_ctx.uart_bus); + /* n706a_net 只使用 AT 模式,直接使用 UART 发送 */ + PPP_DEBUG_DEBUG("Sending %u bytes via UART (AT mode)", length); + + PPP_DEBUG_DEBUG("Calling uapi_uart_write(bus=%u, data=%p, len=%u, timeout=1000)...", + g_serial_ctx.uart_bus, data, length, 1000); + + int32_t write_result = uapi_uart_write(g_serial_ctx.uart_bus, data, length, 1000); + PPP_DEBUG_DEBUG("uapi_uart_write returned: %d (expected: %u)", write_result, length); + ppp_error_t result = PPP_ERR_OK; - int32_t write_result = 0; - switch (g_serial_ctx.current_mode) { - case PPP_SERIAL_MODE_AT: - /* AT命令模式:直接使用UART发送 */ - PPP_DEBUG_DEBUG("Sending %u bytes via UART (AT mode)", length); - // PPP_DEBUG_HEXDUMP(data, length); - - PPP_DEBUG_DEBUG("Calling uapi_uart_write(bus=%u, data=%p, len=%u, timeout=1000)...", - g_serial_ctx.uart_bus, data, length, 1000); - - write_result = uapi_uart_write(g_serial_ctx.uart_bus, data, length, 1000); - PPP_DEBUG_DEBUG("uapi_uart_write returned: %d (expected: %u)", write_result, length); - - if (write_result != (int32_t)length) { - PPP_DEBUG_ERROR("UART write failed in AT mode: returned %d, expected %u", write_result, length); - PPP_DEBUG_ERROR("Possible causes: UART not initialized, hardware error, or timeout"); - g_serial_ctx.tx_errors++; - result = PPP_ERR_UART; - } else { - g_serial_ctx.tx_bytes += length; - PPP_DEBUG_INFO("Sent %u bytes successfully via UART (total tx: %u bytes)", - length, g_serial_ctx.tx_bytes); - } - break; - - case PPP_SERIAL_MODE_PPP_DATA: - /* PPP数据模式:直接使用UART发送(与AT模式一致) */ - PPP_DEBUG_INFO("Sending %u bytes via UART (PPP data mode)", length); - - /* 检查UART状态 */ - PPP_DEBUG_DEBUG("UART Bus: %u, Mode: PPP_DATA, TX Bytes: %u, TX Errors: %u", - g_serial_ctx.uart_bus, g_serial_ctx.tx_bytes, g_serial_ctx.tx_errors); - - /* 打印要发送的数据 */ - PPP_DEBUG_PRINT("PPP TX Data: "); - for (uint32_t i = 0; i < length && i < 16; i++) { - PPP_DEBUG_PRINT("%02X ", data[i]); - } - if (length > 16) { - PPP_DEBUG_PRINT("... (total %u bytes)", length); - } - PPP_DEBUG_PRINT("\n"); - - PPP_DEBUG_DEBUG("Calling uapi_uart_write(bus=%u, data=%p, len=%u, timeout=%d)...", - g_serial_ctx.uart_bus, data, length, PPP_OPTIMIZED_UART_WRITE_TIMEOUT); - - /* 优化UART写超时:使用配置的优化值以提高PPP协商速度 */ - write_result = uapi_uart_write(g_serial_ctx.uart_bus, data, length, PPP_OPTIMIZED_UART_WRITE_TIMEOUT); - PPP_DEBUG_INFO("uapi_uart_write returned: %d (expected: %u)", write_result, length); - - if (write_result != (int32_t)length) { - PPP_DEBUG_ERROR("UART write failed in PPP data mode: returned %d, expected %u", write_result, length); - PPP_DEBUG_ERROR("UART状态: bus=%u, 总发送字节=%u, 总错误=%u", - g_serial_ctx.uart_bus, g_serial_ctx.tx_bytes, g_serial_ctx.tx_errors); - PPP_DEBUG_ERROR("可能原因: UART未初始化、硬件错误、超时或缓冲区满"); - g_serial_ctx.tx_errors++; - result = PPP_ERR_UART; - } else { - g_serial_ctx.tx_bytes += length; - PPP_DEBUG_INFO("Sent %u bytes successfully via UART (PPP mode, total tx: %u bytes)", - length, g_serial_ctx.tx_bytes); - } - break; - - default: - PPP_DEBUG_ERROR("Invalid serial mode: %d", g_serial_ctx.current_mode); - result = PPP_ERR_INVALID_PARAM; - break; + if (write_result != (int32_t)length) { + PPP_DEBUG_ERROR("UART write failed: returned %d, expected %u", write_result, length); + PPP_DEBUG_ERROR("Possible causes: UART not initialized, hardware error, or timeout"); + g_serial_ctx.tx_errors++; + result = PPP_ERR_UART; + } else { + g_serial_ctx.tx_bytes += length; + PPP_DEBUG_INFO("Sent %u bytes successfully via UART (total tx: %u bytes)", + length, g_serial_ctx.tx_bytes); } PPP_DEBUG_INFO("Write result: %s (error code: %d)", @@ -382,7 +337,7 @@ ppp_error_t ppp_serial_write(const uint8_t *data, uint32_t length) } /** - * @brief 读取数据 - AT模式直接从UART读取,PPP模式从软件缓冲区读取 + * @brief 读取数据 - n706a_net 只使用 AT 模式,直接从 UART 读取 */ ppp_error_t ppp_serial_read(uint8_t *buffer, uint32_t buffer_size, uint32_t *bytes_read) { @@ -397,135 +352,27 @@ ppp_error_t ppp_serial_read(uint8_t *buffer, uint32_t buffer_size, uint32_t *byt *bytes_read = 0; ppp_error_t result = PPP_ERR_OK; - switch (g_serial_ctx.current_mode) { - case PPP_SERIAL_MODE_AT: - /* AT命令模式:直接从UART读取 */ - { - - #if defined(CONFIG_UART_SUPPORT_DMA) - uart_write_dma_config_t dma_cfg = { - .src_width = UART_DMA_WIDTH, - .dest_width = UART_DMA_WIDTH, - .burst_length = UART_DMA_BURST_LENGTH, - .priority = UART_DMA_PRIORITY - }; - int32_t read_result = uapi_uart_read_by_dma(g_serial_ctx.uart_bus, buffer, buffer_size,&dma_cfg); - - #else - int32_t read_result = uapi_uart_read(g_serial_ctx.uart_bus, buffer, buffer_size, 100); - #endif - if (read_result > 0) { - *bytes_read = (uint32_t)read_result; - g_serial_ctx.rx_bytes += *bytes_read; - PPP_DEBUG_DEBUG("Read %u bytes via UART (AT mode)", *bytes_read); - } else if (read_result < 0) { - PPP_DEBUG_ERROR("UART read error: %d", read_result); - g_serial_ctx.rx_errors++; - result = PPP_ERR_UART; - } - } - break; - - case PPP_SERIAL_MODE_PPP_DATA: - /* PPP数据模式:使用优化的直接UART读取,平衡响应性和完整性 */ - { - /* 使用类似SLIP的高效读取策略: - * 1. 首次尝试非阻塞读取(1ms超时) - * 2. 如果有数据,继续快速读取直到无数据 - * 3. 这样可以批量读取连续到达的数据,提高效率 - */ - uint32_t total_read = 0; - const uint32_t MAX_BATCH_READ = 8; // 最多批量读取8次 - uint32_t batch_count = 0; - - while (batch_count < MAX_BATCH_READ && total_read < buffer_size) { - uint32_t remaining = buffer_size - total_read; - uint8_t *current_buffer = buffer + total_read; - - // 使用短超时进行读取,类似sio_tryread的1ms超时 - - #if defined(CONFIG_UART_SUPPORT_DMA) - uart_write_dma_config_t dma_cfg = { - .src_width = UART_DMA_WIDTH, - .dest_width = UART_DMA_WIDTH, - .burst_length = UART_DMA_BURST_LENGTH, - .priority = UART_DMA_PRIORITY - }; - int32_t read_result = uapi_uart_read_by_dma(g_serial_ctx.uart_bus, current_buffer, - remaining,&dma_cfg); - - #else - int32_t read_result = uapi_uart_read(g_serial_ctx.uart_bus, current_buffer, - remaining, batch_count == 0 ? 10 : 1); - #endif - if (read_result > 0) { - total_read += (uint32_t)read_result; - g_serial_ctx.rx_bytes += (uint32_t)read_result; - batch_count++; - - PPP_DEBUG_DEBUG("PPP batch read %u: got %d bytes (total: %u)", - batch_count, read_result, total_read); - - // 如果读取的数据少于请求的数据,说明没有更多数据了 - if ((uint32_t)read_result < remaining) { - break; - } - } else if (read_result == 0) { - // 超时,无更多数据 - break; - } else { - // 读取错误 - if (total_read == 0) { - PPP_DEBUG_ERROR("PPP UART Read: Error %d", read_result); - result = PPP_ERR_UART; - break; - } - break; - } - } - - *bytes_read = total_read; - - if (total_read > 0) { - PPP_DEBUG_DEBUG("PPP optimized read: %u bytes in %u batches", total_read, batch_count); - result = PPP_ERR_OK; - break; - } else { - result = PPP_ERR_OK; - break; // 无数据但无错误 - } - } - break; -#if 0 - case PPP_SERIAL_MODE_PPP_DATA: - /* PPP数据模式:从软件环形缓冲区读取 */ - osal_mutex_lock(&g_serial_ctx.rx_mutex); - - uint32_t available = ppp_serial_buffer_data_count(); - if (available == 0) { - osal_mutex_unlock(&g_serial_ctx.rx_mutex); - return PPP_ERR_OK; // 没有数据可读 - } - - uint32_t to_read = (available < buffer_size) ? available : buffer_size; - - for (uint32_t i = 0; i < to_read; i++) { - buffer[i] = g_serial_ctx.rx_buffer[g_serial_ctx.rx_tail]; - g_serial_ctx.rx_tail = (g_serial_ctx.rx_tail + 1) % g_serial_ctx.rx_buffer_size; - g_serial_ctx.rx_count--; - } - - *bytes_read = to_read; - osal_mutex_unlock(&g_serial_ctx.rx_mutex); - - PPP_DEBUG_DEBUG("Read %u bytes from software buffer (PPP data mode)", to_read); - // PPP_DEBUG_HEXDUMP(buffer, to_read); - break; -#endif - default: - PPP_DEBUG_ERROR("Invalid serial mode: %d", g_serial_ctx.current_mode); - result = PPP_ERR_INVALID_PARAM; - break; + /* n706a_net 只使用 AT 模式,直接从 UART 读取 */ + #if defined(CONFIG_UART_SUPPORT_DMA) + uart_write_dma_config_t dma_cfg = { + .src_width = UART_DMA_WIDTH, + .dest_width = UART_DMA_WIDTH, + .burst_length = UART_DMA_BURST_LENGTH, + .priority = UART_DMA_PRIORITY + }; + int32_t read_result = uapi_uart_read_by_dma(g_serial_ctx.uart_bus, buffer, buffer_size, &dma_cfg); + #else + int32_t read_result = uapi_uart_read(g_serial_ctx.uart_bus, buffer, buffer_size, 100); + #endif + + if (read_result > 0) { + *bytes_read = (uint32_t)read_result; + g_serial_ctx.rx_bytes += *bytes_read; + PPP_DEBUG_DEBUG("Read %u bytes via UART (AT mode)", *bytes_read); + } else if (read_result < 0) { + PPP_DEBUG_ERROR("UART read error: %d", read_result); + g_serial_ctx.rx_errors++; + result = PPP_ERR_UART; } #if defined(PPP_LOW_POWER_MODE) && (PPP_LOW_POWER_MODE == 1) @@ -536,7 +383,7 @@ ppp_error_t ppp_serial_read(uint8_t *buffer, uint32_t buffer_size, uint32_t *byt } /** - * @brief PPP快速尝试读取 - 类似SLIP的sio_tryread,使用1ms超时 + * @brief 快速尝试读取 - n706a_net 只使用 AT 模式,使用短超时读取 * @param buffer 接收缓冲区 * @param buffer_size 缓冲区大小 * @param bytes_read 实际读取字节数 @@ -550,90 +397,20 @@ ppp_error_t ppp_serial_tryread(uint8_t *buffer, uint32_t buffer_size, uint32_t * *bytes_read = 0; - if (g_serial_ctx.current_mode == PPP_SERIAL_MODE_PPP_DATA) { - /* PPP数据模式:混合策略(先轮询检测,有数据时用 DMA) */ - #if defined(CONFIG_UART_SUPPORT_DMA) - // ⚠️ 核心问题:uapi_uart_read_by_dma 会阻塞 1 秒等待数据,不适合无数据时的轮询 - // 解决方案:先用普通读取(timeout=0)快速检测,有数据时再用 DMA 批量读取 - - // 第 1 步:非阻塞检测是否有数据(立即返回) - uint8_t probe_buf[8]; - int32_t probe_result = uapi_uart_read(g_serial_ctx.uart_bus, probe_buf, sizeof(probe_buf), 0); - - int32_t read_result = 0; - - if (probe_result > 0) { - // 有数据!先复制探测数据,再用 DMA 读取剩余数据 - static uint32_t dma_call_count = 0; - static uint32_t dma_success_count = 0; - - // 复制探测数据 - if ((uint32_t)probe_result <= buffer_size) { - memcpy(buffer, probe_buf, probe_result); - read_result = probe_result; - } - - // 如果 buffer 还有空间,尝试用 DMA 批量读取剩余数据 - if ((uint32_t)probe_result < buffer_size) { - uint32_t remaining = buffer_size - probe_result; - // DMA 读取策略:既然已确认有数据,就充分利用 DMA 批量传输能力 - // 只对足够大的块使用 DMA(>=64字节),小块数据用 DMA 开销反而更大 - uint32_t dma_size = (remaining >= 64) ? remaining : 0; - - if (dma_size > 0) { - uart_write_dma_config_t dma_cfg = { - .src_width = UART_DMA_WIDTH, - .dest_width = UART_DMA_WIDTH, - .burst_length = UART_DMA_BURST_LENGTH, - .priority = UART_DMA_PRIORITY - }; - - dma_call_count++; - if (dma_call_count == 1) { - PPP_DEBUG_WARN("=== Hybrid Mode: Poll + DMA ==="); - PPP_DEBUG_WARN("Poll detects data (instant), DMA reads bulk (8-64 bytes)"); - } - - int32_t dma_result = uapi_uart_read_by_dma(g_serial_ctx.uart_bus, - buffer + probe_result, - dma_size, &dma_cfg); - - if (dma_result > 0) { - read_result += dma_result; - dma_success_count++; - if (dma_success_count == 1 || dma_success_count % 50 == 0) { - PPP_DEBUG_WARN("DMA Success #%u: poll %d + DMA %d = %d bytes", - dma_success_count, probe_result, dma_result, read_result); - } - } else if (dma_result < 0 && dma_call_count <= 5) { - PPP_DEBUG_ERROR("DMA failed (%d), but poll got %d bytes", - dma_result, probe_result); - } - } - } - } else { - // 没有数据,立即返回(无阻塞) - read_result = 0; - } - - #else - int32_t read_result = uapi_uart_read(g_serial_ctx.uart_bus, buffer, buffer_size, 0); - #endif - if (read_result > 0) { - *bytes_read = (uint32_t)read_result; - g_serial_ctx.rx_bytes += *bytes_read; - return PPP_ERR_OK; - } else if (read_result == 0) { - /* 超时,无数据可读 */ - return PPP_ERR_OK; - } else { - /* UART读取错误 */ - PPP_DEBUG_ERROR("PPP UART tryread error: %d", read_result); - return PPP_ERR_UART; - } + /* n706a_net 只使用 AT 模式,使用短超时(0ms)快速读取 */ + int32_t read_result = uapi_uart_read(g_serial_ctx.uart_bus, buffer, buffer_size, 0); + + if (read_result > 0) { + *bytes_read = (uint32_t)read_result; + g_serial_ctx.rx_bytes += *bytes_read; + return PPP_ERR_OK; + } else if (read_result == 0) { + /* 超时,无数据可读 */ + return PPP_ERR_OK; } else { - /* AT模式:使用标准读取 */ - return ppp_serial_read(buffer, buffer_size, bytes_read); + /* UART读取错误 */ + PPP_DEBUG_ERROR("UART tryread error: %d", read_result); + return PPP_ERR_UART; } } diff --git "a/code/sdk/zhihui/ppp/doc/Neoway_\345\212\237\350\203\275\346\250\241\345\235\227TCP\345\272\224\347\224\250\346\214\207\345\215\227_V1.5.md" "b/code/sdk/zhihui/ppp/doc/Neoway_\345\212\237\350\203\275\346\250\241\345\235\227TCP\345\272\224\347\224\250\346\214\207\345\215\227_V1.5.md" new file mode 100644 index 000000000..91041c3aa --- /dev/null +++ "b/code/sdk/zhihui/ppp/doc/Neoway_\345\212\237\350\203\275\346\250\241\345\235\227TCP\345\272\224\347\224\250\346\214\207\345\215\227_V1.5.md" @@ -0,0 +1,6892 @@ +# TCP应用指南 + +## 功能 + +## 模组 + +### TCP 应用指南 + +###### 版本 1.5 日期 2025-04-24 + +功能模组TCP 应用指南 + +版权声明 + +深圳市有方科技股份有限公司版权所有 +ii + +版权声明 + +版权所有 © 深圳市有方科技股份有限公司 2025。深圳市有方科技股份有限公司保留所有权利。 + +未经深圳市有方科技股份有限公司书面同意,任何单位和个人不得擅自摘抄、复制本文档内容的部分或全部, +并不得以任何形式传播。 + +是深圳市有方科技股份有限公司所有商标。 + +本文档中出现的其他商标,由商标所有者所有。 + +说明 + +本文档对应产品为功能模组。 + +本文档的使用对象为系统工程师,开发工程师及测试工程师。 + +本设计指南为客户产品设计提供支持,客户须按照本文中的规范和参数进行产品设计和调试。如因客户操作 +不当造成的人身伤害和财产损失,有方概不承担责任。 + +由于产品版本升级或其它原因,本文档内容会在不预先通知的情况下进行必要的更新。 + +除非另有约定,本文档中的所有陈述、信息和建议不构成任何明示或暗示的担保。 + +深圳市有方科技股份有限公司为客户提供全方位的技术支持,任何垂询请直接联系您的客户经理或发送邮件 +至以下邮箱: + +Sales@neoway.com + +Support@neoway.com + +公司网址:http://www.neoway.com + +功能模组TCP 应用指南 + +目录 + +深圳市有方科技股份有限公司版权所有 +iii + +#### 目 录 + +###### 关于本文档 ....................................................................................................... v + +范围 ................................................................................................................................................... v + +读者对象 ............................................................................................................................................ v + +修订记录 ............................................................................................................................................ v + +符号约定 ............................................................................................................................................ v +1 概述 .............................................................................................................. 6 + +1.1 TCP 协议 ..................................................................................................................................... 6 + +1.2 TCP 传输模式 .............................................................................................................................. 6 +2 模块初始化流程 ........................................................................................... 8 + +2.1 标准指令初始化流程 ................................................................................................................... 9 + +2.2 国网指令初始化流程 ................................................................................................................. 10 +3 标准指令TCP 应用 .................................................................................... 11 + +3.1 TCP 非透传客户端 ..................................................................................................................... 12 + +3.1.1 异常处理 .......................................................................................................................... 13 + +3.1.2 示例.................................................................................................................................. 16 + +3.2 TCP 非透传服务器端 ................................................................................................................. 18 + +3.2.1 异常处理 .......................................................................................................................... 19 + +3.2.2 示例.................................................................................................................................. 20 + +3.3 TCP 透传客户端 ........................................................................................................................ 22 + +3.3.1 异常处理 .......................................................................................................................... 23 + +3.3.2 示例.................................................................................................................................. 25 +4 国网指令TCP 应用 .................................................................................... 27 + +4.1 TCP 非透传客户端 ..................................................................................................................... 27 + +4.1.1 异常处理 .......................................................................................................................... 29 + +4.1.2 示例.................................................................................................................................. 32 + +4.2 TCP 非透传服务器端 ................................................................................................................. 34 + +4.2.1 异常处理 .......................................................................................................................... 35 + +4.2.2 示例.................................................................................................................................. 37 + +4.3 TCP 透传客户端 ........................................................................................................................ 38 + +4.3.1 异常处理 .......................................................................................................................... 40 + +4.3.2 示例.................................................................................................................................. 42 +5 外部协议栈TCP 应用 ................................................................................ 45 + +5.1 异常处理 ................................................................................................................................... 45 + +5.2 示例 .......................................................................................................................................... 46 + +功能模组TCP 应用指南 + +目录 + +深圳市有方科技股份有限公司版权所有 +iv + +###### A 关键AT 命令 .............................................................................................. 47 + +A.1 标准AT 指令 ............................................................................................................................ 47 + +A.1.1 AT+CGDCONT–设置PDP 格式 ...................................................................................... 47 + +A.1.2 AT+XGAUTH–用户认证 .................................................................................................. 48 + +A.1.3 AT+XIIC–建立PPP 链接 ................................................................................................. 49 + +A.1.4 AT+TCPSETUP–建立TCP 连接 ..................................................................................... 50 + +A.1.5 AT+TCPSEND–发送TCP 数据 ....................................................................................... 51 + +A.1.6 AT+IPSTATUS–查询TCP/UDP 链路状态 ....................................................................... 53 + +A.1.7 AT+TCPACK–查询TCP 链路发送数据状态 .................................................................... 54 + +A.1.8 AT+TCPLISTEN–设置服务器侦听功能............................................................................ 55 + +A.1.9 AT+CLOSELISTEN–关闭侦听链接 ................................................................................. 56 + +A.1.10 AT+CLOSECLIENT–关闭主站链接 ............................................................................... 57 + +A.1.11 +TCPRECV(S)–接收到主站的数据 ............................................................................... 58 + +A.1.12 AT+TCPREADS–读取到主站的数据 ............................................................................. 58 + +A.1.13 AT+TCPSENDS–发送给主站的数据 ............................................................................. 59 + +A.1.14 AT+CLIENTSTATUS–查询主站链路的状态 ................................................................... 60 + +A.1.15 AT+TCPACKS–查询TCP 服务器发送数据状态 ............................................................ 61 + +A.1.16 AT+TCPTRANS–建立TCP 透传链接 ............................................................................ 62 + +A.1.17 AT+TRANSCLOSE–关闭透传方式链接......................................................................... 63 + +A.1.18 ATD*99#–GPRS ........................................................................................................... 64 + +A.2 国网AT 指令 ............................................................................................................................ 65 + +A.2.1 AT$MYNETCON–设置网络连接初始化参数 .................................................................... 65 + +A.2.2 AT$MYNETACT–激活/去激活网络连接 ........................................................................... 66 + +A.2.3 AT$MYNETSRV–设置非透明传输服务参数 .................................................................... 67 + +A.2.4 AT$MYNETOPEN–开启服务........................................................................................... 68 + +A.2.5 AT$MYNETREAD–读取数据 ........................................................................................... 69 + +A.2.6 AT$MYNETWRITE–发送数据 ......................................................................................... 69 + +A.2.7 AT$MYNETCLOSE–关闭连接......................................................................................... 70 + +A.2.8 AT$MYNETACK–查询TCPACK 信息.............................................................................. 71 + +A.2.9 AT$MYNETACCEPT–接受侦听请求 ............................................................................... 71 + +A.2.10 AT$MYNETCREATE–开启透明传输服务 ...................................................................... 72 + +A.2.11 $MYURCREAD–数据到来主动上报 .............................................................................. 74 + +A.2.12 $MYURCCLOSE–链路断开主动上报 ............................................................................ 74 + +A.2.13 $MYURCACT–网络连接状态主动上报.......................................................................... 75 + +A.2.14 $MYURCCLIENT–客户端连接主动上报 ........................................................................ 76 + +功能模组TCP 应用指南 + +关于本文档 + +深圳市有方科技股份有限公司版权所有 +v + +#### 关于本文档 + +##### 范围 + +本文档对应产品为功能模组。 + +##### 读者对象 + +本文档的使用对象为系统工程师,开发工程师及测试工程师。 + +##### 修订记录 + +版本 +日期 +变更 +作者 + +1.0 +2018-11 +初始版本 +Zhang Song + +1.1 +2020-01 + +⚫ +删除各TCP 连接中的模块初始化部分。 + +⚫ +新增第2 ç«  模块初始化流程。 +Zhang Song + +1.2 +2020-09 +优化初始化流程图 +Zhang Song + +1.3 +2022-02 +优化各流程图样式 +Zhang Song + +1.4 +2022-06 +修改初始化流程图 +Zhang Song + +1.5 +2025-04 +优化第2 章模块初始化流程图 +Zhang Song + +##### 符号约定 + +符号 +含义 + +危险或警告,用户必须遵从的规则,否则会造成模块或客户设备不可逆的故障损坏, + +甚至可能造成人员身体伤害。 + +注意,警示用户使用模块时应该特别注意的地方,如不遵从,模块或客户设备可能 + +出现故障。 + +说明或提示,提供模块使用的意见或建议。 + +功能模组TCP 应用指南 + +第1 章概述 + +深圳市有方科技股份有限公司版权所有 +6 + +#### 1 概述 + +本文主要介绍如何使用有方科技股份有限公司功能模块的TCP 功能。 + +##### 1.1 TCP 协议 + +TCP(Transmission Control Protocol 传输控制协议)协议是一种面向连接的、可靠地、基于字节 + +流的运输层协议。相比UDP 协议,TCP 具有更高的可靠性。对于数据传输有高可靠性要求的应用场景, + +建议选用此协议。 + +用户使用功能模块TCP 功能时,可以用模块内置的TCP 协议栈(内部协议栈)也可以使用用户主 + +控自带的协议栈(外部协议栈)。二者的区别在于:模块内部协议栈已经将PPP 协议、TCP 协议封装成 + +了对应的AT 指令,主控 只需要操作对应的AT 指令就可以完成拨号、建立和关闭TCP 连接、收发数 + +据等操作。外部协议栈主要依赖于主控自己的协议栈,需要主控处理PPP 协议、TCP 协议具体的数据 + +帧和数据内容。 + +为适应不同客户使用场景和行业要求,有方功能模块内置的TCP 协议栈具有丰富的AT 指令,主要 + +分为两类:国网指令和标准指令。国网指令是国家电网在376.3 规约中定义的AT 指令,主要用于电力 + +行业;标准指令是我司自定义格式的AT 指令。详见各模块对应的AT 指令集。 + +##### 1.2 TCP 传输模式 + +功能模块支持TCP 客户端和TCP 服务端功能。其中,TCP 客户端功能具有两种模式: + +⚫ +透明传输模式 + +建立TCP 连接后,主控从端口(USB、UART)发送给模块的字符(包括AT 命令)都将被当作 + +TCP 数据发送到网络,模块从网络侧接收到的TCP 数据将会直接从串口上报给主控,不带任何提 + +示回码。 + +主控可向模块发送“+++”退出透明传输模式,查询信号强度,网络状态等;在TCP 连接未断开的 + +情况下,可以向模块发送“ATO”返回透明传输模式。 + +⚫ +非透明传输模式 + +建立TCP 连接后,主控每发送一次数据都需要调用发送数据的AT 命令,模块从网络侧接收到的 + +TCP 数据将会直接从串口上报给主控,但在TCP 数据内容前会带有对应的提示数据头 + ++TCPRECV:,,。 + +功能模组TCP 应用指南 + +第1 章概述 + +深圳市有方科技股份有限公司版权所有 +7 + +本文按照标准指令集、国网指令集的顺序介绍模块内置协议栈TCP 功能的使用方式,主要包括相 + +关AT 命令的介绍、TCP 客户端和服务器端的使用流程、异常的排查与处理等内容。最后,我们会简单 + +介绍外部协议栈的使用方式。 + +功能模组TCP 应用指南 + +第2 章模块初始化流程 + +深圳市有方科技股份有限公司版权所有 +8 + +#### 2 模块初始化流程 + +请确保模块在建立TCP 连接前已执行过该初始化步骤。 + +⚫ +发送AT 命令后,必须等待收到回码才能发送下一条AT 命令。 + +⚫ +发送AT 命令后,若未返回任何回码,则继续发送,1s/次,最多3 次,若发送3 次后,仍未返 + +回任何回码,则重启模块。 + +⚫ +上述流程中设置的APN、用户名、密码、鉴权方式等参数,仅用于举例说明。实际应用过程 + +中,需结合所使用的SIM 卡的签约信息,设置正确的APN、用户名、密码、鉴权方式等参 + +数。 + +⚫ +针对单模块使用双卡或者三卡的情况,在设置注册网络参数指令后(APN、用户名密码、鉴权 + +方式),需要增加指令(例如AT+COPS=2;AT+COPS=0,具体指令在设计流程时可与有方 + +技术支持沟通),让模块注销网络并重新注册网络。 + +⚫ +重启模块请参照《Neoway_标准模块_开关机流程操作指南》进行。24 小时内过多的重启可能 + +会导致FLASH 及SIM 卡不可逆的损坏。 + +⚫ +若客户产品需要使用模组2G SMS、CSD 功能,需要确保模组注册上CS 域。所以初始化流 + +程中,主控必须执行AT+CREG?查询模组CS 域的注册情况。若客户产品未使用模组2G + +SMS、CSD 功能,则主控可以不执行AT+CREG?查询模组CS 域的注册情况。 + +⚫ +若客户产品需要使用模组分组数据业务功能,需要确保模组附着上PS 域。所以初始化流程 + +中,主控必须执行AT+CGATT?查询模组PS 域的附着情况。若客户产品未使用模组分组数据 + +业务功能,则主控可以不执行AT+CGATT?查询模组PS 域的附着情况。在查询模组PS 域附 + +着情况前,可以按需使用AT+CGREG?、AT+CEREG?查询PS 域的注册情况: + +- +2G/3G 模组或者4G 模组2G/3G 模式下,使用AT+CGREG?查询PS 域(GPRS 网络) + +的注册情况。 + +- +4G 模组4G 模式下,使用AT+CGREG?查询PS 域(EPS 网络)的注册情况。 + +功能模组TCP 应用指南 + +第2 章模块初始化流程 + +深圳市有方科技股份有限公司版权所有 +9 + +##### 2.1 标准指令初始化流程 + +延时 + +"OK"? + +1. AT + +2. AT+CGSN + +3. AT+GMR + +Yes + +4. AT+CPIN? + ++CPIN: READY ? + +5. AT+CIMI + +8. AT+CSQ + ++CSQ: , + and 0 signal 31 ? + +12. AT+CGATT? + +"+CGATT: 1"? + +No + +No + +13. AT$MYSYSINFO + +查询<40次? + +Yes + +No + +查询<15次? + +查询<30次? +手动附着 + +查询<10次? + +模块初始化完成 + +No + +No + +Yes + +No + +2G模块延时3s,4G模块延时10s。该过程中模块处于 +初始化阶段,可能出现未响应或响应出错的情况。 + +- 4.检查SIM卡状态 +- 若返回"+CPIN: READY",则可进入下一步。 +- 若未返回任何回码或者返回其他错误回码,则继续 + +查询,1s/次,最多15次。若查询15次后仍未返回 +"+CPIN: READY",则重启模块。 + +模块重启 +模块开机 + +6. AT+CGDCONT=1,"IP","CMNET" + +"OK"? + +7. AT+XGAUTH=1,1,"gsm","1234" + +"OK"? + +No + +查询<2次? + +拨号<3次? + +重新设置<3次? + +No + +Yes + +No +No +Yes + +No +Yes +No + +Yes + +No +Yes +No + +1. 检查串口是否已通 +- 若返回"OK",则可进入下一步。 +- 若未返回任何回码或者返回其他错误回码,则继续尝 + +试,1s/次,最少10次。若尝试10次后,仍未返回 +"OK ",则重启模块。 + +Yes + +Yes + +- 5.获取SIM卡IMSI +- 若返回"OK",则可进入下一步。 +- 若未返回任何回码或者返回其他错误回码,则继续查 + +询,最多2次。若查询2次后仍未返回"OK",则进入下 +一步。 + +6. 设置APN +- 若返回"OK",则可进入下一步。 +- 若未收到任何回码或收到其他错误回码,则继续设 + +置,1s/次,最多3次。若重新设置3次后,仍未若返 +回"OK ",则重启模块。 + +7. 设置身份认证参数 +- 若返回"OK",则可进入下一步。 +- 若未返回任何回码或者返回其他错误回码,则继续 + +若收到其他错误回码设置,1s/次,最多3次。若重 +新设置3次后,仍未若返回"OK ",则重启模块。 + +8.查询信号强度 +• 若返回"+CSQ: ,"且0 signal 31则有信 + +号。建议signal 12时,再进入下一步。极端恶劣网 +络环境下,只要有信号强度即可进入下一步。 +• 若未返回任何回码、返回的回码包含"+CSQ: 99,"、 + +或者返回其他错误回码,则继续查询,1s/次,最多40 +次。若查询40次后仍无信号,则重启模块。 + +12. 查询网络附着情况 +- 若返回"+CGATT: 1",则进入下一步 +- 若返回"+CGATT: 0",或其他错误回码,则继续查 + +询,1s/次,最多30次。若查询30次后,仍未返回 +"+CGATT: 1",则手动进行网络附着。最长等待2分 +钟返回回码。 +• 手动进行网络附着后,仍未返回"+CGATT: 1",则 + +重启模块。 + +13.查询当前网络制式 + +2. 获取模块IMEI + +3. 获取模块固件版本 + +"OK"? + +Yes + +Yes + +Yes + +Yes + +Yes + +10. AT+CGREG? + ++CGREG: 0,1 or + ++CGREG: 0,5? +查询<40次? + +Yes + +No + +Yes + +9. AT+CREG? + ++CREG: 0,1 or + ++CREG: 0,5? +查询<40次? + +Yes + +No + +Yes + +No + +No + +11. AT+CEREG? + ++CEREG: 0,1 or + ++CEREG: 0,5? +查询<40次? + +Yes + +No + +Yes + +No + +No + +9. 查询GSM网络CS域注册情况 +- 若回码包含"+CREG: 0,1"或者"+CREG: 0,5"字段,则进 + +入下一步。 +• 若未返回任何回码或返回其他错误回码,则继续查 + +询,1s/次,最多40次。若查询40次后,回码仍不包 +含"+CREG: 0,1"或者"+CREG: 0,5"字段:针对需要使用 +2G短信/CSD功能的场景,需要确保模组注册上CS +域,可重启模块。针对不需要使用2G短信/CSD功能 +的场景,可进入下一步。 + +10. 查询GPRS网络注册情况 +- 若回码包含"+CGREG: 0,1"或者"+CGREG: 0,5"字段, + +则进入下一步。 +• 若未返回任何回码或返回其他错误回码,则继续查 + +询,1s/次,最多40次。若查询40次后,回码仍不包 +含"+CGREG: 0,1"或者"+CGREG: 0,5"字段,可进入下 +一步。 + +11. 查询EPS网络注册情况 +- 若回码包含"+CEREG: 0,1"或者"+CEREG: 0,5"字段,则 + +进入下一步。 +• 若未返回任何回码或返回其他错误回码,则继续查 + +询,1s/次,最多40次。若查询40次后,回码仍不包 +含"+CEREG: 0,1"或者"+CEREG: 0,5"字段,可进入下一 +æ­¥ + +功能模组TCP 应用指南 + +第2 章模块初始化流程 + +深圳市有方科技股份有限公司版权所有 +10 + +##### 2.2 国网指令初始化流程 + +Yes + +延时 + +"OK"? + +1. AT + +2. AT+CGSN + +3. AT+GMR + +Yes + +4. AT+CPIN? + ++CPIN: READY ? + +5. AT+CIMI + +"OK"? + +9. AT+CSQ + ++CSQ: , + and 0 signal 31 ? + +11. AT+CGREG? + ++CGREG: 0,1 or + ++CGREG: 0,5? + ++CGATT: 1? + +13. AT+CGATT? + +Yes + +No + +14. AT$MYSYSINFO + +查询<40次? + +查询<40次? + +Yes + +查询<15次? + +查询<30次? + +``` +AT+CGATT=1 +``` + +手动附着 + +查询<10次? + +模块初始化完成 + +No +Yes + +No +No + +2G模块延时3s,4G模块延时10s。该过程中模块处于 +初始化阶段,可能出现未响应或响应出错的情况。 + +4. 检查SIM卡状态 +- 若返回"+CPIN: READY",则可进入下一步 +- 若未返回任何回码或者返回其他错误回码,则继续查 + +询,1s/次,最多15次。若查询15次后仍未返回 +"+CPIN: READY",则重启模块。 + +模块重启 +模块开机 + +6. AT$MYNETCON=0,"APN","CMNET" + +"OK"? + +7. AT$MYNETCON=0,"USERPWD","CARD,CARD" + +"OK"? + +No + +查询<2次? + +拨号<3次? + +重新设置<3次? + +No + +Yes + +No + +No +No +Yes + +No + +1. 检查串口是否已通 +- 若返回"OK",则可进入下一步。 +- 若未返回任何回码或者返回其他错误回码,则继续尝 + +试,1s/次,最少10次。若尝试10次后,仍未返回 +"OK ",则重启模块。 + +Yes + +Yes + +Yes + +Yes + +- 5.获取SIM卡IMSI +- 若返回"OK",则可进入下一步。 +- 若未返回任何回码或返回其他错误回码,则继续查 + +询,最多2次。若查询2次后仍未返回"OK",则进入下 +一步。 + +6. 设置APN +- 若返回"OK",则可进入下一步。 +- 若未返回任何回码或返回其他错误回码,则继续设 + +置,1s/次,最多3次。若重新设置3次后,仍未若 +返回"OK ",则重启模块。 + +7. 设置身份认证参数 +- 若返回"OK",则可进入下一步。 +- 若未返回任何回码或返回其他错误回码,则继续设 + +置,1s/次,最多3次。若重新设置3次后,仍未若返 +回"OK ",则重启模块。 + +8. AT$MYNETCON=0,"AUTH",1 + +OK? + +9. 查询信号强度 +- 若返回"+CSQ: ,"且0 signal 31则有信 + +号。建议signal 12时,再进入下一步。极端恶劣网 +络环境下,只要有信号强度即可进入下一步。 +• 若未返回任何回码、回码包含"+CSQ: 99,"、返回其 + +他错误回码,则继续查询,1s/次,最多40次。若查 +询40次后仍无信号,则重启模块。 + +Yes + +2. 获取模块IMEI + +3. 获取模块固件版本 + +Yes + +Yes + +Yes + +No +Yes +No + +查询<40次? +No +No +Yes + +No +No +Yes + +No + +Yes + +8. 设置身份认证参数 +- 若返回"OK",则可进入下一步。 +- 若未返回任何回码或返回其他错误回码,则继续设 + +置,1s/次,最多3次。若重新设置3次后,仍未若返 +回"OK ",则重启模块。 + +13. 查询PS域附着情况 +- 若返回"+CGATT: 1",则进入下一步 +- 若未返回任何回码、返回"+CGATT: 0"、返回其他 + +错误回码,则继续查询,1s/次,最多30次。若查 +询30次后,仍未返回"+CGATT: 1",则手动进行网 +络附着。最长等待2分钟返回回码。 +• 手动进行网络附着后,仍未返回"+CGATT: 1",则 + +重启模块。 + +14.查询当前网络制式 + +10. 查询GSM网络CS域注册情况 +- 若回码包含"+CREG: 0,1"或者"+CREG: 0,5"字段,则 + +进入下一步。 +• 若未返回任何回码或返回其他错误回码,则继续查 + +询,1s/次,最多40次。若查询40次后,回码仍不 +包含"+CREG: 0,1"或者"+CREG: 0,5"字段:针对需要 +使用2G短信/CSD功能的场景,需要确保模组注册上 +CS域,可重启模块。针对不需要使用2G短信/CSD功 +能的场景,可进入下一步。 + +11. 查询GPRS网络注册情况 +- 若回码包含"+CGREG: 0,1"或者"+CGREG: 0,5"字段, + +则进入下一步。 +• 若未返回任何回码或返回其他错误回码,则继续查 + +询,1s/次,最多40次。若查询40次后,回码仍不 +包含"+CGREG: 0,1"或者"+CGREG: 0,5"字段,可进入 +下一步。 + +10. AT+CREG? + ++CREG: 0,1 or + ++CREG: 0,5? +查询<40次? + +Yes + +No + +Yes + +No + +No + +12. AT+CEREG? + ++CEREG: 0,1 or + ++CEREG: 0,5? +查询<40次? + +Yes + +No + +Yes + +12. 查询EPS网络注册情况 +- 若回码包含"+CEREG: 0,1"或者"+CEREG: 0,5"字段, + +则进入下一步。 +• 若未返回任何回码或返回其他错误回码,则继续查 + +询,1s/次,最多40次。若查询40次后,回码仍不 +包含"+CEREG: 0,1"或者"+CEREG: 0,5"字段,可进入 +下一步 +No + +No + +功能模组TCP 应用指南 +第3 章标准指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +11 + +#### 3 标准指令TCP 应用 + +本章主要介绍如何使用有方标准指令实现TCP 功能(包括TCP 非透传客户端、TCP 非透传服务器 + +端、TCP 透传客户端功能)。内容包括对应功能相关的AT 指令、AT 流程、异常处理以及简单示例。 + +连接注意事项: + +⚫ +发送AT 命令后必须等待收到回码后,才能发送下一条AT 命令。 + +⚫ +在建立PPP 连接后,主动去附着前,只要收到"GPRS DISCONNECTION",则返回重新激活PPP 连 + +接。 + +⚫ +在建立TCP 连接后,主动断开TCP 连接前,只要收到"+TCPCLOSE: X,Link Closed",则返回重新 + +建立TCP 连接。 + +⚫ +重启模块请参照《Neoway_标准模块_开关机流程操作指南》进行。24 小时内过多的重启可能会导致 + +FLASH 及SIM 卡不可逆的损坏。 + +功能模组TCP 应用指南 +第3 章标准指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +12 + +##### 3.1 TCP 非透传客户端 + +本节主要实现TCP 非透传客户端功能相关的AT 指令、AT 流程、异常处理以及简单示例。在进行 + +PPP 拨号前,请确保模块已经过初始化(具体初始化流程参考第2 章)。 + +图 3-1 连接流程 + +设置PPP拨号参数 + +"OK"? + +1. AT+XIIC=1 + +"+XIIC: 1"? + +2. AT+XIIC? + +"+TCPCLOSE: X,OK" or "+TCPCLOSE: + +ERROR"? + +3. AT+TCPCLOSE=X + +Yes + +4. AT+TCPSETUP=X,, + +"+TCPSETUP: X,OK"? + +5. AT+IPSTATUS=X + +"+IPSTATUS: +X,CONNECT,TCP,4096"? + +9. AT+CGATT=0 + +"OK" or "ERROR"? + +No + +No + +Yes +重连<3次? + +查询<3次? + +关机 + +No + +No + +Yes + +模块重启 +模块初始化流程 + +6. AT+TCPSEND=X, + +7. AT+TCPACK=X + +"+TCPACK: +X,,"? + +No + +查询<5次? + +重发<3次? + +查询<5次? + +No + +Yes + +No + +No +No +Yes + +No +Yes +No + +1. 进行PPP拨号 +- 若返回"OK",则可进入下一步。 +- 若未收到任何回码或收到其他错误回码,则继续拨号,1s/次,最 + +多3次。若重新拨号3次后,仍未若返回"OK ",则重启模块。 + +Yes + +Yes + +Yes + +4. 建立TCP连接 +- 该指令等待返回"+TCPSETUP: X,OK"的超时时间为30s +- 若返回"+TCPSETUP: X,OK",则可进入下一步。 +- 若出现以下情况,则继续重连,最多3次,若3次仍无法建立TCP连 + +接,则重启模块。 + -- 收到回码:"+TCPSETUP: X,FAIL"/"+TCPSETUP: ERROR" + -- 收到其他错误回码或者未收到任何回码 + +5. 查询链路状态 +- 若返回"+IPSTATUS: X,CONNECT,TCP,4096",则进行下一步。 +- 若回码包含"+IPSTATUS: X,DISCONNECT",则返回重新建立TCP连接。 +- 若回码包含"+IPSTATUS: X,CONNECT",Buffer大小不为4096,则循环 + +查询链路状态,1s/次,最多5次,若查询5次后,Buffer大小不为 +4096,则返回关闭当前连接,重新建立TCP连接。 + +6. 发送数据 +无论是否返回 > ,都延时50-100ms,再将数据发送到模块串口, +等待数据发送成功的回码 +TCPSEND:X, 。 +• 若返回 +TCPSEND:X, ,则进入下一步。 +• 若返回 +TCPSEND: SOCKET ID OPEN FAILED ,则返回重新建立TCP + +连接。 +• 若返回其他错误回码,则继续执行该命令重发数据,最多3次。若 + +重发3次仍然失败,则返回重新建立TCP连接。 + +8. 关闭Socket +- 若返回"+TCPCLOSE: X,OK""+TCPCLOSE: ERROR"则进入下一步,等待 + +返回的超时时间为30s。 +• 若超时未收到回码或收到其他错误回码,则继续关闭,最多3次, + +若重复关闭3次后,仍未收到"+TCPCLOSE: X,OK"或者"+TCPCLOSE: +ERROR"则重启模块。 + +9. 去附着网络 +- 若返回"OK"或者" ERROR",则进入下一步,等返回的超时时间为 + +15s。 +• 若超时未收到回码或者收到其他错误回码,则继续执行,最多3 + +次,若重复关闭3次后,仍未收到"OK"或者" ERROR"则重启模块。 + +"+TCPCLOSE: X,OK" or "+TCPCLOSE: + +ERROR"? + +7. 查询数据发送情况 +- 若返回"+TCPACK: X,,"且与 + +数值相等,则可进入下一步:继续发送数据或者接收 +数据。 +• 若返回"+TCPACK: 0,DISCONNECT",则返回重新建立TCP连接。 +• 若返回"+TCPACK: X,,"且若大 + +于数值,则循环查询循环查询1s/次,最多5次。若查 +询5次,回码中与数值仍不相等,则返回 +关闭当前连接,重新建立连接,以释放缓冲区。 +• 若收到其他错误回码、超时时间到未收到任何回码,则重启模块。 + +"+TCPSEND: X,"? + +8. AT+TCPCLOSE=X + +查询<30次? + +关闭<3次? + +关闭<3次? + +去附着<3次? + +Yes + +No + +Yes + +No + +No +Yes + +Yes + +No +No + +2. 查询PPP连接建立情况 +- 若回码中包含"+XIIC: 1 ",则可进入下一步。 +- 若回码未包含"+XIIC: 1",则循环查询,1s/次,最多30次。若30次 + +后仍未返回"+XIIC: 1",则重新拨号,最多重拨3次,若重新拨号3次 +后,仍未获取到IP,则重启模块。 + +3. 关闭待用Socket +- 该指令等待返回"+TCPCLOSE: X,OK""+TCPCLOSE: ERROR"的超时时间 + +为30s。 +• 若超时未收到回码或收到其他错误回码,则继续关闭,最多3次, + +若重复关闭3次后,仍未收到"+TCPCLOSE: X,OK"或者"+TCPCLOSE: +ERROR"则重启模块。 + +Yes + +Yes + +No + +No + +Yes + +Yes + +Yes + +功能模组TCP 应用指南 +第3 章标准指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +13 + +###### 3.1.1 异常处理 + +###### 模块附着PS 域失败 + +1. +检查模块串口通信是否正常。若串口不通,可参考以下方式排查解决: + +− +确保模块已经开机。若模块不开机需要检查电源设计和开机时序 + +− +确保串口电平变换正常。若电平变换不正常需要检查串口电路设计和器件选型 + +− +确保软件上串口配置正常,软件处理是否有误。比如自适应逻辑、是否带回车换行发送AT + +2. +检查模块是否已经识别到SIM 卡。若模块无法识别SIM 卡,可参考以下方式排查解决(涉及到的 + +AT 命令,请参考功能 AT 指令集): + +− +确保SIM 卡是否插好、SIM 卡本身完好无损、与卡座接触是否良好 + +− +确保SIM 卡座相关电路无设计问题,是否有虚假焊等 + +− +确保SIM 卡没有被锁卡 + +若通过以上排查模块仍无法识别到SIM 卡,可以联系运营商处理或者更换SIM 卡解决。 + +3. +查询当前模块的接收信号是否满足通信条件。我们建议信号强度值为12(-83dbm)再进行通信。 + +若现场信号弱,可通过以下方式排查: + +− +确保天线接好、天线本身完好无损(断路、短路)。 + +− +确认模块注册的网络制式(AT$MYSYSINFO)等信息,若为现场网络环境问题,则联系运营 + +商增强网络覆盖,或者更换其他运营商SIM 卡解决。 + +4. +若现场信号良好,模块能识别到SIM 卡且SIM 卡未欠费,但模块不能附着上PS 域,则需要联系 + +运营商确定SIM 卡配置: + +− +SIM 卡是否开通数据业务 + +− +SIM 卡配置鉴权参数是否正常 + +− +SIM 卡是否有绑定设备 + +− +SIM 卡数据业务相关的APN、用户名、密码、鉴权方式是否正常 + +###### 模块激活PDP 失败 + +若模块正常注册上PS 域,但是无法激活PPP 连接。可以参考以下方式排查解决(涉及到的AT 命 + +令,请参考功能 AT 指令集): + +⚫ +确保SIM 未欠费。 + +功能模组TCP 应用指南 +第3 章标准指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +14 + +⚫ +使用AT+CGDCONT?/AT+XGAUTH?查询APN、用户名、密码,确保这些参数与运营商提供的 + +一致。 + +若以上参数都没有问题,模块仍无法激活PPP 连接,请联系当地运营商协助检查SIM 卡和后台服 + +务器参数配置。 + +###### 模块建立TCP 非透传客户端连接失败 + +若模块正常获取到IP,但是建立TCP 非透传客户端连接失败。可以参考以下方式排查解决(涉及 + +到的AT 命令,请参考功能 AT 指令集): + +1. +确认服务器IP 和端口都已配置好,端口已经建立服务器端侦听,IP 能被客户端PING 通。 + +可以通过用其他客户端设备来连接该服务器IP 和端口确认。 + +2. +确认模块获取到的IP 是否能访问服务器。 + +可通过发送AT+PING=给模块,操作模块PING 服务器来确认。 + +− +若模块能PING 通服务器,则需要检查服务器软件和端口配置; + +− +若模块不能PING 通服务器,则需要检查APN、用户名、密码设置是否正确,SIM 卡配置是 + +否正确。 + +3. +确认SIM 卡配置是否有问题。 + +可以直接联系运营商确认SIM 卡是否可以访问目的IP 或者让运营商直接跟踪后台消息排查原因。 + +###### 模块发送TCP 数据失败 + +若模块正常建立TCP 非透传客户端连接,但是发送TCP 数据失败。可以参考以下方式排查解决 + +(涉及到的AT 命令,请参考功能 AT 指令集): + +1. +确认是否是服务器问题。 + +可通过更换服务器对比确认是否是服务器配置问题。 + +2. +确认SIM 卡是否也有欠费。 + +可联系运营商确认。 + +3. +确认是否是长时间没有发送数据导致的假连接。 + +可通过缩短心跳周期或者是提高数据发送频率来确认。 + +若缩短心跳周期或者是提高数据发送频率后,数据发送正常则可能是假连接引起的问题。 + +4. +确认TCP 连接是否还存在。 + +功能模组TCP 应用指南 +第3 章标准指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +15 + +可通过”AT+IPSTATUS=”命令的返回值来确认。 + +− +若返回值中含有“CONNECT”字符串,则表示TCP 连接存在; + +− +若返回“DISCONNECT” 字符串,则表示TCP 连接不存在。 + +5. +确认终端(或主控)是否已经将数据发送到模块。 + +可以通过模块串口与终端(或主控)串口之间交互的AT 命令和数据内容来确认。 + +为此,我司建议主控具备保存或打印模块与终端串口交互log 功能。只要AT+TCPSEND 命令返 + +回“+TCPSEND: ,”就表示主控数据已经发送到模块。 + +6. +确认模块底层协议栈是否收到服务器端TCP 协议层的ACK。 + +可通过”AT+TCPACK=”命令的返回值来确认。 + +− +若返回值中< data_sent >字段与字段数值相同,则表示数据已经发送到服务器 + +且服务器已经确认。 + +− +若返回值中< data_sent >字段数值大于字段数值,则有可能是数据没有发送到 + +服务器,或者是服务器接收有延时还未收到数据,需要在服务器和模块同时抓取TCP 层数据 + +来排查,可以联系我司FAE 跟进排查。 + +###### 模块接收TCP 数据失败 + +若模块正常建立TCP 非透传客户端连接,但是接收TCP 数据失败。可以参考以下方式排查解决 + +(涉及到的AT 命令,请参考功能 AT 指令集): + +1. +确认是否是服务器问题。 + +可通过更换服务器对比确认是否是服务器配置问题。 + +2. +确认SIM 卡是否也有欠费。 + +可联系运营商确认。 + +3. +确认TCP 连接是否还存在。 + +可通过”AT+IPSTATUS=”命令的返回值来确认。 + +− +若返回值中含有“CONNECT”字符串,则表示TCP 连接存在。 + +− +若返回“DISCONNECT” 字符串,则表示TCP 连接不存在。 + +4. +确认是否是长时间没有发送数据导致的假连接。 + +可通过缩短心跳周期或者是提高数据发送频率来确认。 + +− +若缩短心跳周期或者是提高数据发送频率后,数据接收正常则可能是假连接引起的问题。 + +功能模组TCP 应用指南 +第3 章标准指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +16 + +− +若缩短心跳或提高数据发送频率仍无法确认接收不到数据的原因,需要在服务器和模块同时 + +抓取TCP 层数据来排查,可以联系我司FAE 跟进排查。 + +###### 3.1.2 示例 + +1. +设置APN、用户名、密码,检查并确保模块已成功注册上PS 域 + +AT +//确认模块能正常响应AT 命令 +OK +AT+CGSN ++CGSN: "865324037375316" +OK +AT+CGMR ++CGMR: N720_EAB0CM_BZ_V008 +OK +AT+CPIN? + ++CPIN: READY +//确认模块能识别到SIM 卡 +OK + +AT+CIMI +460016657520885 +OK + +AT+CGDCONT=1,"IP","UNINET" +//设置APN +OK +AT+XGAUTH=1,1, "CARD","CARD" +//设置用户名、密码、鉴权方式 + +OK + +AT+CSQ ++CSQ: 31, 99 +//确认信号良好 +OK +AT+CREG=2 +OK +AT+CREG? ++CREG: 2,1,"FFFE","61C7E47",7 //确认模块注册上CS 域 +OK +AT+CREG=0 +OK + +AT+CGATT? ++CGATT: 1 +//确认模块附着上PS 域 +OK +AT$MYSYSINFO +$MYSYSINFO: 4,02 +//确认模块注册的网络制式 +OK + +2. +建立PPP 连接 + +At+XIIC=1 +//激活PPP 连接 +OK +AT+XIIC? +//查询模块本地IP ++XIIC: 1,10.211.164.210 +OK + +3. +建立TCP 非透传客户端连接并收发数据 + +AT+TCPCLOSE=0 + + +//主动关闭待用SocketID ++TCPCLOSE: ERROR + +AT+TCPSETUP=0,183.239.240.40,5002 +OK + +功能模组TCP 应用指南 +第3 章标准指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +17 + ++TCPSETUP: 0,OK +AT+IPSTATUS=0 + + + + //确认TCP 连接是否正常 ++IPSTATUS: 0,CONNECT,TCP,4096 +AT+TCPACK=0 + + + + //确认数据发送情况 ++TCPACK: 0,0,0 +AT+TCPSEND=0,10 +> +OK + ++TCPSEND: 0,10 ++TCPRECV: 0,6,Neoway +AT+IPSTATUS=0 ++IPSTATUS: 0,CONNECT,TCP,4096 +AT+TCPACK=0 + + + + +//确认数据发送情况 ++TCPACK: 0,10,10 +AT+TCPSEND=0,10 + + + + +//发送数据 +> + +OK + ++TCPSEND: 0,10 ++TCPRECV: 0,6,Neoway +AT+TCPCLOSE=0 + ++TCPCLOSE: 0,OK +AT+CGATT=0 +GPRS DISCONNECTION + +OK + +功能模组TCP 应用指南 +第3 章标准指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +18 + +##### 3.2 TCP 非透传服务器端 + +本节主要实现TCP 非透传服务端功能相关的AT 指令、AT 流程、异常处理以及简单示例。在进行 + +PPP 拨号前,请确保模块已经过初始化(具体初始化流程参考第2 章)。 + +图 3-2 连接流程 + +Yes + +设置PPP拨号参数 + +"OK"? + +1. AT+XIIC=1 + +"+XIIC: 1"? + +2. AT+XIIC? + +"ERROR" or "+CLOSELISTEN: 0,local + +link closed" ? + +3. AT+CLOSELISTEN + +Yes + +4. AT+TCPLISTEN= + +" +TCPLISTEN: 0,OK" ? + +5.AT+CLIENTSTATUS=Y + +"+CLIENTSTATUS: +Y,CONNECT,TCP,61440" ? + +9. AT+CGATT=0 + +"OK" or "ERROR"? + +No + +No + +Yes +重连<3次? + +查询<3次? + +关机 + +No + +No + +Yes + +模块重启 +模块初始化流程 + +6. AT+TCPSENDS=Y, + +7. AT+TCPACKS=Y + +"+TCPACKS: +Y,," + +No + +查询<5次? + +重发<3次? + +查询<5次? + +No + +Yes + +No + +No +No +Yes + +No +Yes +No + +1. 进行PPP拨号 +- 若返回"OK",则可进入下一步。 +- 若未收到任何回码或收到其他错误回码,则继续拨号,1s/次,最多 + +3次。若重新拨号3次后,仍未若返回"OK ",则重启模块。 + +Yes + +Yes + +Yes + +Yes + +4. 建立服务器端侦听 +- 若返回"+TCPLISTEN: 0,OK"或者"Listening...",则可进入下一步。 +- 若出现以下情况,则继续建立侦听,最多3次。 +- 收到回码:"ERROR"/"+TCPLISTEN:bind error"。 +- 收到其他错误回码或者未收到任何回码。 +- 若重新建立3次侦听,仍未返回"+TCPLISTEN: 0,OK"或者"Listening..." + +则重启模块。 + +等待接收客户端连接请求 +需要设置等待客户端连接的超时时间,若超时时间到,没有收到任 +何客户端连接请求,则主动重新拨号建立侦听。 + +5. 查询链路状态 +- 若返回"+CLIENTSTATUS: Y,CONNECT,TCP,61440",则进行下一步。 +- 若回码包含"+CLIENTSTATUS: Y,DISCONNECT",则客户端连接未建立或者已经断 + +开,等待客户端重新建立TCP连接。 +• 若回码包含"+CLIENTSTATUS: Y,CONNECT",Buffer大小不为61440,则循环查询链 + +路状态,最多5次。若查询5次后,Buffer大小不为61440,则关闭当前连接, +等待客户端重新建立TCP连接。 +• 若未返回任何回码或者返回其他错误回码,则继续查询1s/次,最多3次。若查 + +询3次后,仍无回码或者返回其他错误回码,则重启模块。 + +7. 查询数据发送情况 +- 若返回"+TCPACK: Y,,"且与 + +数值相等,则可进入下一步:继续发送数据或者接收 +数据。 +• 若返回"+TCPACK: Y,DISCONNECT",则返回重新建立TCP连接。 +• 若返回"+TCPACK: Y,,"且若大 + +于数值,则继续查询,1s/次,最多5次。若查询5次, +回码中与数值仍不相等,则返回关闭当 +前连接,重新建立连接,以释放缓冲区。 +• 若收到其他错误回码、超时时间到未收到任何回码,则重启模块。 + +9.去附着网络 +若返回"OK"或者" ERROR",则进入下一步,等返回的超时时间为15s。 +若超时未收到回码或者收到其他错误回码,则继续执行,最多3次, +若重复关闭3次后,仍未收到"OK"或者" ERROR"则重启模块。 + ++TCPRECV(S): Y,, + +"ERROR" or "+CLOSELISTEN: 0,local + +link closed" ? + +6. 发送数据 +- 若返回">",则延时50-100ms,将数据发送到模块串口,等待数据发 + +- 送成功的回码"+TCPSENDS: Y,",超时时间为30s。 +- 若返回"+TCPSENDS: Y,",则进入下一步。 +- 若返回"+TCPSENDS:SOCKET ID NOT ACTIVE",则等待客户端连接请 + +求。 +• 若收到其他错误回码、超时时间到未收到任何回码,则继续执行该 + +命令重发数据,最多3次。若重发3次仍然失败,则重启模块。 + +"+TCPSENDS: Y," ? + +8. AT+CLOSELISTEN + +查询<30次? + +重新建立侦听<3次? + +关闭<3次? + +去附着<3次? + +Yes + +No + +Yes + +No + +No +Yes + +Yes +No +No + +2. PPP连接建立情况 +- 若回码中包含"+XIIC: 1 ",则可进入下一步。 +- 若回码未包含"+XIIC: 1",则循环查询,1s/次,最多30次。若30次 + +后仍未返回"+XIIC: 1",则重新拨号,最多重拨3次,若重新拨号3次 +后,仍未获取到IP,则重启模块。 + +3. 关闭服务器侦听 +- 该指令等待返回回码的超时时间为20s。 +- 若返回"+CLOSELISTEN: 0,local link closed"或者"ERROR",则可进入下 + +- 一步。 +- 若超时未收到回码或收到其他错误回码,则继续关闭,最多3次。 +- 若重复关闭3次后,仍未收到"+CLOSELISTEN: 0,local link closed"或者 + +"ERROR"则重启模块。 + +Connect AcceptSocket=Y, +ClientAddr=119.123.77.133,ClientPort=8000 + +8. 关闭服务器侦听 +- 该指令等待返回回码的超时时间为20s。若返回"+CLOSELISTEN: + +- 0,local link closed"或者"ERROR",则可进入下一步。 +- 若超时未收到回码或收到其他错误回码,则继续关闭,最多3次。 +- 若重复关闭3次后,仍未收到"+CLOSELISTEN: 0,local link closed"或者 + +"ERROR"则重启模块。 + +No + +No + +Yes + +Yes + +Yes + +Yes + +功能模组TCP 应用指南 +第3 章标准指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +19 + +###### 3.2.1 异常处理 + +###### 模块附着PS 域失败 + +1. +检查模块串口通信是否正常。若串口不通,可参考以下方式排查解决: + +− +确保模块已经开机:若模块不开机需要检查电源设计和开机时序 + +− +确保串口电平变换正常:若电平变换不正常需要检查串口电路设计和器件选型 + +− +确保软件上串口配置正常,软件处理是否有误。比如自适应逻辑、是否带回车换行发送AT + +2. +检查模块是否已经识别到SIM 卡。若模块无法识别SIM 卡,可参考以下方式排查解决(涉及到的 + +AT 命令,请参考功能 AT 指令集): + +− +确保SIM 卡是否插好、SIM 卡本身完好无损、与卡座接触是否良好 + +− +确保SIM 卡座相关电路无设计问题,是否有虚假焊等 + +− +确保SIM 卡没有被锁卡 + +若通过以上排查模块仍无法识别到SIM 卡,可以联系运营商处理或者更换SIM 卡解决。 + +3. +查询当前模块的接收信号是否满足通信条件。 + +我们建议信号强度值为12(-83dbm)再进行通信。若现场信号弱,可通过以下方式排查: + +− +确保天线接好、天线本身完好无损(断路、短路) + +− +确认模块注册的网络制式(AT$MYSYSINFO)等信息,若为现场网络环境问题,则联系运营 + +商增强网络覆盖,或者更换其他运营商SIM 卡解决。 + +4. +若现场信号良好,模块能识别到SIM 卡且SIM 卡未欠费,但模块不能附着上PS 域,则需要联系 + +运营商确定SIM 卡配置: + +− +SIM 卡是否开通数据业务 + +− +SIM 卡配置鉴权参数是否正常 + +− +SIM 卡是否有绑定设备 + +− +SIM 卡数据业务相关的APN、用户名、密码、鉴权方式是否正常 + +###### 模块激活PDP 失败 + +若模块正常注册上PS 域,但是无法激活PPP 连接。可以参考以下方式排查解决(涉及到的AT 命 + +令,请参考功能 AT 指令集): + +功能模组TCP 应用指南 +第3 章标准指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +20 + +⚫ +确保SIM 未欠费。 + +⚫ +使用AT+CGDCONT?/AT+XGAUTH?查询APN、用户名、密码,确保这些参数与运营商提供的 + +一致。 + +若以上参数都没有问题,模块仍无法激活PPP 连接,请联系当地运营商协助检查SIM 卡和后台服 + +务器参数配置。 + +###### 客户端无法连接服务器 + +若模块正常建立服务器端侦听后,客户端无法连接上服务器。可以参考以下方式排查解决(涉及到 + +的AT 命令,请参考功能 AT 指令集): + +1. +确认服务器IP 和端口都已配置好,端口已经建立服务器端侦听,IP 能被客户端PING 通。 + +可以通过用其他客户端设备来连接该服务器IP 和端口确认。 + +2. +确认模块获取到的IP 与客户端的IP 是否在同一个网段内。 + +可通过在PC 上PING 模块来确认。 + +− +若PC 客户端能PING 通服务器,则需要检查服务器软件和端口配置。 + +− +若PC 客户端不能PING 通服务器,则需要检查APN、用户名、密码设置是否正确,SIM 卡 + +配置是否正确。 + +3. +确认SIM 卡配置是否有问题。 + +可以直接联系运营商客户端是否可以访问SIM 卡的IP 或者让运营商直接跟踪后台消息排查原因。 + +###### 3.2.2 示例 + +1. +设置APN、用户名、密码,检查并确保模块已成功注册上PS 域 + +AT +//确认模块能正常响应AT 命令 +OK + +AT+CGSN ++CGSN: "865324037375316" +OK + +AT+CGMR ++CGMR: N720_EAB0CM_BZ_V008 +OK +AT+CPIN? ++CPIN: READY +//确认模块能识别到SIM 卡 +OK +AT+CIMI +460016657520885 +OK + +AT+CGDCONT=1,"IP","UNINET" +//设置APN +OK +AT+XGAUTH=1,1, "CARD","CARD" +//设置用户名、密码、鉴权方式 + +功能模组TCP 应用指南 +第3 章标准指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +21 + +OK +AT+CSQ ++CSQ: 31, 99 +//确认信号良好 +OK +AT+CREG=2 +OK +AT+CREG? ++CREG: 2,1,"FFFE","61C7E47",7 //确认模块注册上CS 域 + +OK +AT+CREG=0 +OK +AT+CGATT? ++CGATT: 1 +//确认模块附着上PS 域 +OK +AT$MYSYSINFO +$MYSYSINFO: 4,02 +//确认模块注册的网络制式 +OK + +2. +建立PPP 连接 + +AT+XIIC=1 +//激活PPP 连接 +OK + +AT+XIIC? +//查询模块本地IP ++XIIC: 1,10.211.164.210 +OK + +3. +建立TCP 非透传服务器端连接并收发数据 + +AT+CLOSELISTEN + + + + //主动关闭待用SocketID +ERROR +AT+TCPLISTEN=6800 + + +//建立侦听 + ++TCPLISTEN:0,OK +Connect AcceptSocket=1,ClientAddr=119.123.77.133 //客户端连接建立 +AT+CLIENTSTATUS=1 + + + + //确认TCP 连接是否正常 ++IPSTATUS: 1,CONNECT,TCP,1024 + +``` +AT+TCPSENDS=1,10 +``` + +> +OK + ++TCPSENDS: 1,10 +AT+TCPACKS=1 + + + + //确认数据发送情况 + ++TCPACKS: 1,10,10 ++TCPRECV(S): 1,6,Neoway +AT+ CLOSELISTEN + + + + + +//关闭侦听 + ++CLOSELISTEN: 0,local link closed + +AT+CGATT=0 +GPRS DISCONNECTION +OK + +功能模组TCP 应用指南 +第3 章标准指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +22 + +##### 3.3 TCP 透传客户端 + +本节主要实现TCP 透传客户端功能相关的AT 指令、AT 流程、异常处理以及简单示例。在进行PPP + +拨号前,请确保模块已经过初始化(具体初始化流程参考第2 章)。 + +图 3-3 连接流程 + +设置PPP拨号参数 + +"OK"? + +1. AT+XIIC=1 + +"+XIIC: 1"? + +2. AT+XIIC? + +"+TRANSCLOSE: 0,OK" or " ERROR"? + +3. AT+TRANSCLOSE + +Yes + +4. AT+TCPTRANS=, + +"+TCPTRANS: OK"? + +收发数据 + +9. AT+CGATT=0 + +"OK" or "ERROR"? + +No + +Yes +重连<3次? + +查询<3次? + +关机 + +No + +No + +Yes + +模块重启 +模块初始化流程 + +6. AT+TCPACK + +"+TCPACK: +X,,"? + +No + +退出透传<3次? + +查询<5次? + +Yes + +No + +1. 进行PPP拨号 +- 若返回"OK",则可进入下一步。 +- 若未收到任何回码或收到其他错误回码,则继续拨号,1s/次,最多 + +3次。若重新拨号3次后,仍未若返回"OK ",则重启模块。 + +Yes + +Yes + +Yes + +4. 建立TCP连接 +- 该指令等待返回"+TCPTRANS: OK"的超时时间为30s +- 若返回"+TCPSETUP: X,OK",则可进入下一步。 +- 若出现以下情况,则继续重连,最多3次,若3次仍无法建立TCP连 + +接,则重启模块。 + -- 收到回码:"+TCPSETUP: X,FAIL"/"+TCPSETUP: ERROR" + -- 收到其他错误回码或者未收到任何回码 + +5. 退出透传 +- 若返回"OK",则进入下一步。 +- 若返回其他错误回码、或者未返回任何回码,则继续重发,最多3 + +次。若重发3次仍未收到"OK",则重启模块。 +• "+++"必须以字符串的形式发送到串口,且"+++"不必带回车发送 + +8. 关闭Socket +- 若返回"+TCPCLOSE: X,OK""+TCPCLOSE: ERROR"则进入下一步,等待 + +返回的超时时间为30s。 +• 若超时未收到回码或收到其他错误回码,则继续关闭,最多3次, + +若重复关闭3次后,仍未收到"+TCPCLOSE: X,OK"或者"+TCPCLOSE: +ERROR"则重启模块。 + +9. 去附着网络 +- 若返回"OK"或者" ERROR",则进入下一步,等返回的超时时间为 + +15s。 +• 若超时未收到回码或者收到其他错误回码,则继续执行,最多3 + +次,若重复关闭3次后,仍未收到"OK"或者" ERROR"则重启模块。 + +7. ATO + +"+TRANSCLOSE: 0,OK" + +or "ERROR"? + +6. 查询数据发送情况 +- 若返回"+TCPACK: X,,"且与 + +数值相等,则可进入下一步:继续发送数据或者接收 +数据。 +• 若返回"+TCPACK: 0,DISCONNECT",则返回重新建立TCP连接。 +• 若返回"+TCPACK: X,,"且若大 + +于数值,则循环查询循环查询1s/次,最多5次。若查 +询5次,回码中与数值仍不相等,则返回 +关闭当前连接,重新建立连接,以释放缓冲区。 +• 若收到其他错误回码、超时时间到未收到任何回码,则重启模 + +块。 + +"OK"? + +8. AT+TRANSCLOSE + +查询<30次? + +关闭<3次? + +关闭<3次? + +去附着<3次? + +Yes + +No + +Yes + +No + +2. 查询PPP连接建立情况 +- 若回码中包含"+XIIC: 1 ",则可进入下一步。 +- 若回码未包含"+XIIC: 1",则循环查询,1s/次,最多30次。若30次 + +后仍未返回"+XIIC: 1",则重新拨号,最多重拨3次,若重新拨号3次 +后,仍未获取到IP,则重启模块。 + +3. 关闭连接 +- 该指令等待返回"+TRANSCLOSE: 0,OK""ERROR"的超时时间为30s。 +- 若超时未收到回码或收到其他错误回码,则继续关闭,最多3次。 +- 若重复关闭3次后,仍未收到"+TRANSCLOSE: 0,OK"或"ERROR"则重 + +启模块。 + ++++ + +"CONNECT" ? +返回透传<3次? + +Yes + +No + +Yes + +Yes + +Yes + +No + +No + +No + +7. 返回透传模式 +- 若返回"CONNECT",则进入下一步:继续发送数据。 +- 若返回其他错误回码、或者未返回任何回码,则继续重发,最多3 + +次。若重发3次仍未收到"OK",则重启模块。 + +Yes + +Yes + +Yes + +Yes + +Yes + +No + +No + +No + +No + +No + +No + +No + +功能模组TCP 应用指南 +第3 章标准指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +23 + +###### 3.3.1 异常处理 + +###### 模块附着PS 域失败 + +1. +使用AT+CPIN?检查模块是否已经识别到SIM 卡。若模块无法识别SIM 卡,可参考以下方式排查 + +解决(涉及到的AT 命令,请参考功能 AT 指令集): + +− +确保SIM 卡是否插好; + +− +确保SIM 卡本身完好无损; + +− +确保SIM 卡没有被锁卡; + +− +确保SIM 卡卡座完好,不存在假焊; + +− +擦拭SIM 卡及卡座的触点(该触点发生氧化、脏污也会产生读不到SIM 卡的现象),确保 + +SIM 卡与模块卡座接触良好。 + +若通过以上排查模块仍无法识别到SIM 卡,可以更换SIM 卡解决。 + +2. +检查天线是否完好,使用AT+CSQ 命令查询当前模块的接收信号是否满足通信条件。 + +我们建议信号强度值为15(-83dbm)再进行通信。 + +3. +若现场信号良好,模块能识别到SIM 卡且SIM 卡未欠费,但模块不能附着上PS 域,可通过 + +``` +AT+CGATT=1 命令,主动附着PS 域。 +``` + +###### 模块激活PDP 失败 + +若模块正常注册上PS 域,但是无法激活PPP 连接。可以参考以下方式排查解决(涉及到的AT 命 + +令,请参考功能 AT 指令集): + +⚫ +确保SIM 卡未欠费。 + +⚫ +使用AT+CGDCONT?/AT+NETAPN?查询APN、用户名、密码,确保这些参数与运营商提供的 + +一致。 + +若以上参数都没有问题,模块仍无法激活PPP 连接,请联系当地运营商协助检查SIM 卡和后台服 + +务器参数配置。 + +###### 模块建立TCP 透传客户端连接失败 + +若模块正常获取到IP,但是建立TCP 非透传客户端连接。可以参考以下方式排查解决(涉及到的 + +AT 命令,请参考功能 AT 指令集): + +功能模组TCP 应用指南 +第3 章标准指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +24 + +1. +确认服务器IP 和端口都已配置好,端口已经建立服务器端侦听,IP 能被客户端PING 通。 + +可以通过用其他客户端设备来连接该服务器IP 和端口确认。 + +2. +确认模块获取到的IP 是否能访问服务器。 + +可通过发送AT+PING=给模块,操作模块PINGf 服务器来确认。 + +− +若模块能PING 通服务器,则需要检查服务器软件和端口配置。 + +− +若模块不能PING 通服务器,则需要检查APN、用户名、密码设置是否正确,SIM 卡配置是 + +否正确。 + +3. +确认SIM 卡配置是否有问题。 + +可以直接联系运营商确认SIM 卡是否可以访问目的IP 或者让运营商直接跟踪后台消息排查原因。 + +###### 模块发送TCP 数据失败 + +若模块正常建立TCP 非透传客户端连接,但是发送TCP 数据失败。可以参考以下方式排查解决 + +(涉及到的AT 命令,请参考功能 AT 指令集): + +1. +确认是否是服务器问题。 + +可通过更换服务器对比确认是否是服务器配置问题。 + +2. +确认SIM 卡是否也有欠费。 + +可联系运营商确认。 + +3. +确认是否是长时间没有发送数据导致的假连接。 + +可通过缩短心跳周期或者是提高数据发送频率来确认。 + +若缩短心跳周期或者是提高数据发送频率后,数据发送正常则可能是假连接引起的问题。 + +4. +确认TCP 连接是否还存在。 + +可通过”AT+IPSTATUS=”命令的返回值来确认。 + +− +若返回值中含有“CONNECT”字符串,则表示TCP 连接存在。 + +− +若返回“DISCONNECT” 字符串,则表示TCP 连接不存在。 + +5. +确认终端(或主控)是否已经将数据发送到模块。 + +可以通过模块串口与终端(或主控)串口之间交互的AT 命令和数据内容来确认。 + +为此,我司建议主控具备保存或打印模块与终端串口交互log 功能。只要AT+TCPSEND 命令返 + +回“+TCPSEND: ,”就表示主控数据已经发送到模块。 + +6. +确认模块底层协议栈是否收到服务器端TCP 协议层的ACK。 + +功能模组TCP 应用指南 +第3 章标准指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +25 + +可通过”AT+TCPACK=”命令的返回值来确认。 + +− +若返回值中< data_sent >字段与字段数值相同,则表示数据已经发送到服务器 + +且服务器已经确认。 + +− +若返回值中< data_sent >字段数值大于字段数值,则有可能是数据没有发送到 + +服务器,或者是服务器接收有延时还未收到数据,需要在服务器和模块同时抓取TCP 层数据 + +来排查,可以联系我司FAE 跟进排查。 + +###### 模块接收TCP 数据失败 + +若模块正常建立TCP 非透传客户端连接,但是接收TCP 数据失败。可以参考以下方式排查解决 + +(涉及到的AT 命令,请参考功能 AT 指令集): + +1. +确认是否是服务器问题。 + +可通过更换服务器对比确认是否是服务器配置问题。 + +2. +确认SIM 卡是否也有欠费。 + +可联系运营商确认。 + +3. +确认TCP 连接是否还存在。 + +可通过”AT+IPSTATUS=”命令的返回值来确认。 + +− +若返回值中含有“CONNECT”字符串,则表示TCP 连接存在。 + +− +若返回“DISCONNECT” 字符串,则表示TCP 连接不存在。 + +4. +确认是否是长时间没有发送数据导致的假连接。 + +可通过缩短心跳周期或者是提高数据发送频率来确认。 + +− +若缩短心跳周期或者是提高数据发送频率后,数据接收正常则可能是假连接引起的问题。 + +− +若缩短心跳或提高数据发送频率仍无法确认接收不到数据的原因,需要在服务器和模块同时 + +抓取TCP 层数据来排查,可以联系我司FAE 跟进排查。 + +###### 3.3.2 示例 + +1. +设置APN、用户名、密码,检查并确保模块已成功注册上PS 域 + +AT +//确认模块能正常响应AT 命令 +OK +AT+CGSN ++CGSN: "865324037375316" + +OK + +``` +AT+CGMR +``` + ++CGMR: N720_EAB0CM_BZ_V008 +OK + +功能模组TCP 应用指南 +第3 章标准指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +26 + +AT+CPIN? ++CPIN: READY +//确认模块能识别到SIM 卡 +OK +AT+CIMI +460016657520885 +OK +AT+CGDCONT=1,"IP","UNINET" +//设置APN +OK + +AT+XGAUTH=1,1, "CARD","CARD" +//设置用户名、密码、鉴权方式 +OK +AT+CSQ ++CSQ: 31, 99 +//确认信号良好 +OK +AT+CREG=2 +OK +AT+CREG? ++CREG: 2,1,"FFFE","61C7E47",7 //确认模块注册上CS 域 + +OK +AT+CREG=0 + +OK +AT+CGATT? ++CGATT: 1 +//确认模块附着上PS 域 + +OK +AT$MYSYSINFO +$MYSYSINFO: 4,02 +//确认模块注册的网络制式 + +OK + +2. +建立PPP 连接 + +At+XIIC=1 +//激活PPP 连接 +OK +AT+XIIC? +//查询模块本地IP ++XIIC: 1,10.211.164.210 +OK + +3. +建立TCP 非透传服务器端连接并收发数据 + +``` +AT+TRANSCLOSE +ERROR +AT+TCPTRANS=183.239.240.40,5009 //建立TCP 透传连接 +``` + +OK ++TCPTRANS: OK + +//收发数据,发送+++(不带回车换行)退出透传 +OK +AT+IPSTATUS +//查询链路状态 + ++IPSTATUS: CONNECT,TCP,4096 + +ATO +//切换回透传模式 +CONNECT + + + + +//收发数据,发送+++(不带回车换行)退出透传 +OK +AT+TRANSCLOSE +//关闭透传连接 ++TRANSCLOSE: 0,OK +AT+CGATT=0 +GPRS DISCONNECTION +OK + +功能模组TCP 应用指南 +第4 章国网指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +27 + +#### 4 国网指令TCP 应用 + +本章主要介绍如何使用有方标准指令实现TCP 功能(包括TCP 非透传客户端、TCP 非透传服务器 + +端、TCP 透传客户端功能) 。内容包括对应功能相关的AT 指令、AT 流程、异常处理以及简单示例。 + +连接注意事项: + +⚫ +发送AT 命令后必须等待收到回码后,才能发送下一条AT 命令。 + +⚫ +在建立PPP 连接后,主动去附着前,只要收到"$MYURCACT: 0,0",则返回重新激活PPP 连接。 + +⚫ +在建立TCP 连接后,主动断开TCP 连接前,只要收到"$MYURCCLOSE: X",则返回重新建立TCP + +连接。 + +##### 4.1 TCP 非透传客户端 + +本节主要实现TCP 非透传客户端功能相关的AT 指令、AT 流程、异常处理以及简单示例。在进行 + +PPP 拨号前,请确保模块已经过初始化(具体初始化流程参考第2 章)。 + +功能模组TCP 应用指南 +第4 章国网指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +28 + +图 4-1 连接流程 + +设置PPP拨号参数 + +"$MYURCACT: 0,1"? + +1. AT$MYNETACT=0,1 + +"$MYNETACT: 0,1"? + +2. AT$MYNETACT? + +"$MYNETCLOSE: X" + +or "ERROR: 913"? + +3. AT$MYNETCLOSE=X + +Yes + +4. AT$MYNETSRV=0,X,0,0,"IP:Port" + +"OK"? + +5. AT$MYNETOPEN=X + +" $MYNETOPEN: X,2000" or" + +ERROR: 912" ? + +9. AT+CGATT=0 + +"OK" or "ERROR"? + +No + +Yes +重设<3次? + +拨号<3次? + +关机 + +No + +No + +Yes + +模块重启 +模块初始化流程 + +6. AT$MYNETWRITE=X, + +"$MYNETWRITE: X," ? + +输入要发送的数据 + +"OK"? + +No + +重新开启服务<3次? + +重发<3次? + +查询<5次? + +No + +Yes + +No + +No +No +Yes + +No +Yes +No + +1. PPP拨号 + +• 若返回"$MYURCACT: 0,1"或"ERROR: 902",则进入下一步。该指令等待返 + +回"$MYURCACT: 0,1"或"ERROR: 902"的超时时间为30s。 +• 若未收到任何回码或收到其他错误回码,则继续拨号,最多3次。若重 + +新拨号3次后,仍未若返回"OK ",则重启模块。 + +Yes + +Yes + +Yes + +Yes + +4. 设置非透明传输服务参数 +- 若返回"OK",则进入下一步。 +- 若未收到回码或收到其他错误回码,则继续设置,最多3次。若重复设 + +ç½®3次后,仍未收到"OK",则重启模块。 +注:信道号必须与已激活的信道号一致,第三个参数必须设置为0。 + +5. 开启服务(建立TCP连接) +- 若返回"$MYNETOPEN: 0,2000"或" ERROR: 912" ,则进入下一步。该指令等 + +- 待返回"$MYNETOPEN: 0,2000"或" ERROR: 912"的超时时间为30s。 +- 若返回"ERROR: 901"则重新设置参数进行PPP拨号。 +- 若超时未收到回码或收到其他错误回码,则继续开启,最多3次。若重 + +复开启3次后,仍未收到"$MYNETOPEN: 0,2000"或" ERROR: 912"则重启模 +块。 + +6. 发送数据 +- 若返回"$MYNETWRITE: X,",则延时50-100ms,将数据发送到模块串 + +- 口,等待数据发送成功的回码"OK"。若返回"OK",则进入下一步。 +- 若返回"ERROR: 901",则返回重新进行PPP拨号。 +- 若返回"ERROR: 913",则重新建立TCP连接。 +- 若返回其他错误回码、或者未返回任何回码,则继续重发,最多3次。 + +若重发3次仍未收到"OK",则重启模块。 + +8. 读取数据 +- 若返回"$MYNETREAD: X,0",则可进入下一步:继续发送数据或者接收 + +- 数据。若回码中""字段不为0,则继续读取。 +- 若返回"ERROR: 901",则重新PPP拨号。 +- 若返回"ERROR: 913",则重建TCP连接。 +- 若返回其他错误回码、或者未返回任何回码,则继续读取,最多3次。 + +若重读3次仍返回其他错误回码、或者未返回任何回码,则重启模块。 + +9. 去附着网络 +- 若返回"OK"或者" ERROR",则进入下一步,等返回的超时时间为15s。 +- 若超时未收到回码或者收到其他错误回码,则继续执行,最多3次,若 + +重复关闭3次后,仍未收到"OK"或者" ERROR"则重启模块。 + +7.AT$MYNETACK=X + +"$MYNETREAD: X,0" ? + +7. 查询数据发送情况 +- 若返回"$MYNETACK: X,,"且 + +值为0,则进入下一步:继续发送数据或者接收数 +据。 +• 若返回"ERROR: 901",则重新PPP拨号。 +• 若返回"ERROR: 913",则重新建立TCP连接。 +• 若回码中""字段不为0,则继续查询,1s/次,最多5 + +次。若查询5次,""字段不为0,则重新建立TCP连 +接。 +• 若返回其他错误回码、或者未返回任何回码,则继续重发,最多3次。 + +若重发3次仍返回其他错误回码、或者未返回任何回码,则重启模块。 + +8. AT$MYNETREAD=X, + +关闭<3次? + +读取<3次? + +去附着<3次? + +Yes + +No + +No +Yes + +Yes + +No + +2. 查询PPP连接建立情况 + +- 若回码中包含"$MYNETACT: 0,1"字段,则进入下一步。 +- 若回码中包含"$MYNETACT: 0,0"则重新设置参数进行PPP拨号。 +- 若返回其他错误回码,则循环查询,1s/次,最多3次。若查询3次后仍 + +未返回"$MYNETACT: 0,1",则重新设置参数进行PPP拨号。 +• 若未返回任何回码,则继续查询,1s/次,最多3次。若查询继续3次 + +后,仍未返回任何回码,则重启模块。 + +3. 关闭待用Socket +- 若返回"$MYNETCLOSE: X"或"ERROR: 913",则进入下一步。该指令等待返 + +- 回"$MYNETCLOSE: X"或"ERROR: 913"的超时时间为30s。 +- 若返回"ERROR: 901"则重新设置参数进行PPP拨号。 +- 若超时未收到回码或收到其他错误回码,则继续关闭,最多3次。若重 + +复关闭3次后,仍未收到"$MYNETCLOSE: X"或"ERROR: 913"则重启模块。 + +"$MYNETACK: X,0" ? + +$MYURCREAD: X + +No +查询<3次? + +Yes + +No + +查询<5次? + +Yes +No + +Yes + +Yes + +Yes + +Yes + +Yes + +No + +No + +No + +No + +功能模组TCP 应用指南 +第4 章国网指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +29 + +###### 4.1.1 异常处理 + +###### 模块附着PS 域失败 + +1. +检查模块串口通信是否正常。 + +若串口不通,可参考以下方式排查解决: + +− +确保模块已经开机:若模块不开机需要检查电源设计和开机时序 + +− +确保串口电平变换正常:若电平变换不正常需要检查串口电路设计和器件选型 + +− +确保软件上串口配置正常,软件处理是否有误。比如自适应逻辑、是否带回车换行发送AT + +2. +检查模块是否已经识别到SIM 卡。 + +若模块无法识别SIM 卡,可参考以下方式排查解决(涉及到的AT 命令,请参考功能 AT 指令 + +集): + +− +确保SIM 卡是否插好、SIM 卡本身完好无损、与卡座接触是否良好 + +− +确保SIM 卡座相关电路无设计问题,是否有虚假焊等 + +− +确保SIM 卡没有被锁卡 + +若通过以上排查模块仍无法识别到SIM 卡,可以联系运营商处理或者更换SIM 卡解决。 + +3. +查询当前模块的接收信号是否满足通信条件。 + +我们建议信号强度值为12(-83dbm)再进行通信。若现场信号弱,可通过以下方式排查: + +− +确保天线接好、天线本身完好无损(断路、短路) + +− +确认模块注册的网络制式(AT$MYSYSINFO)等信息,若为现场网络环境问题,则联系运营 + +商增强网络覆盖,或者更换其他运营商SIM 卡解决。 + +4. +若现场信号良好,模块能识别到SIM 卡且SIM 卡未欠费,但模块不能附着上PS 域,则需要联系 + +运营商确定SIM 卡配置: + +− +SIM 卡是否开通数据业务 + +− +SIM 卡配置鉴权参数是否正常 + +− +SIM 卡是否有绑定设备 + +− +SIM 卡数据业务相关的APN、用户名、密码、鉴权方式是否正常 + +功能模组TCP 应用指南 +第4 章国网指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +30 + +###### 模块激活PDP 失败 + +若模块正常注册上PS 域,但是无法激活PPP 连接。可以参考以下方式排查解决(涉及到的AT 命 + +令,请参考功能 AT 指令集): + +⚫ +确保SIM 未欠费。 + +⚫ +使用AT$MYNETCON?查询APN、用户名、密码,确保这些参数与运营商提供的一致。 + +若以上参数都没有问题,模块仍无法激活PPP 连接,请联系当地运营商协助检查SIM 卡和后台服 + +务器参数配置。 + +###### 模块建立TCP 非透传客户端连接失败 + +若模块正常获取到IP,但是建立TCP 非透传客户端连接。可以参考以下方式排查解决(涉及到的 + +AT 命令,请参考功能 AT 指令集): + +1. +确认服务器IP 和端口都已配置好,端口已经建立服务器端侦听,IP 能被客户端PING 通。 + +可以通过用其他客户端设备来连接该服务器IP 和端口确认。 + +2. +确认模块获取到的IP 是否能访问服务器。 + +可通过发送AT+PING=给模块,操作模块PING 服务器来确认。 + +若模块能PING 通服务器,则需要检查服务器软件和端口配置。 + +若模块不能PING 通服务器,则需要检查APN、用户名、密码设置是否正确,SIM 卡配置是否正 + +确。 + +3. +确认SIM 卡配置是否有问题。 + +可以直接联系运营商确认SIM 卡是否可以访问目的IP 或者让运营商直接跟踪后台消息排查原因。 + +###### 模块发送TCP 数据失败 + +若模块正常建立TCP 非透传客户端连接,但是发送TCP 数据失败。可以参考以下方式排查解决 + +(涉及到的AT 命令,请参考功能 AT 指令集): + +1. +确认是否是服务器问题。 + +可通过更换服务器对比确认是否是服务器配置问题。 + +2. +确认SIM 卡是否也有欠费。 + +可联系运营商确认。 + +3. +确认是否是长时间没有发送数据导致的假连接。 + +功能模组TCP 应用指南 +第4 章国网指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +31 + +可通过缩短心跳周期或者是提高数据发送频率来确认。 + +若缩短心跳周期或者是提高数据发送频率后,数据发送正常则可能是假连接引起的问题。 + +4. +确认TCP 连接是否还存在。 + +可通过”AT$MYNETOPEN?”命令的返回值来确认。 + +若返回值中含有“$MYNETOPEN:”字符串,则表示TCP 连接存在;若返回字段只有OK,则表示 + +TCP 连接不存在。 + +5. +确认终端(或主控)是否已经将数据发送到模块。 + +可以通过模块串口与终端(或主控)串口之间交互的AT 命令和数据内容来确认。 + +为此,我司建议主控具备保存或打印模块与终端串口交互log 功能。只要AT$MYNETWRITE 命 + +令返回“OK”就表示主控数据已经发送到模块。 + +6. +确认模块底层协议栈是否收到服务器端TCP 协议层的ACK。 + +可通过”AT$MYNETACK =”命令的返回值来确认。 + +若返回值中字段数值为0,则表示数据已经发送到服务器且服务器已经确 + +认。 + +若返回值中字段数值不为0,则有可能是数据没有发送到服务器,或者是服 + +务器接收有延时还未收到数据,需要在服务器和模块同时抓取TCP 层数据来排查,可以联系我司 + +FAE 跟进排查。 + +###### 模块接收TCP 数据失败 + +若模块正常建立TCP 非透传客户端连接,但是接收TCP 数据失败。可以参考以下方式排查解决 + +(涉及到的AT 命令,请参考功能 AT 指令集): + +1. +确认是否是服务器问题。 + +可通过更换服务器对比确认是否是服务器配置问题。 + +2. +确认SIM 卡是否也有欠费。 + +可联系运营商确认。 + +3. +确认TCP 连接是否还存在。 + +可通过”AT$MYNETOPEN?”命令的返回值来确认。 + +若返回值中含有“$MYNETOPEN:”字符串,则表示TCP 连接存在。 + +若返回字段只有OK,则表示TCP 连接不存在。 + +4. +确认是否是长时间没有发送数据导致的假连接。 + +可通过缩短心跳周期或者是提高数据发送频率来确认。 + +功能模组TCP 应用指南 +第4 章国网指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +32 + +若缩短心跳周期或者是提高数据发送频率后,数据接收正常则可能是假连接引起的问题。 + +若缩短心跳或提高数据发送频率仍无法确认接收不到数据的原因,需要在服务器和模块同时抓取 + +TCP 层数据来排查,可以联系我司FAE 跟进排查。 + +###### 4.1.2 示例 + +1. +设置APN、用户名、密码,检查并确保模块已成功注册上PS 域 + +AT +//确认模块能正常响应AT 命令 +OK +AT+CGSN ++CGSN: "865324037375316" +OK +AT+CGMR ++CGMR: N720_DAC0CM_DL_V004 + +OK +AT+CPIN? ++CPIN: READY +//确认模块能识别到SIM 卡 + +OK +AT+CIMI +460016657520885 + +OK +AT$MYNETCON=0,"APN","CMNET" + + +//设置APN +OK + +AT$MYNETCON=0,"USERPWD","CARD,CARD" + +//设置用户名和密码 +OK +AT$MYNETCON=0,"AUTH",1 + + + +//设置鉴权方式 +OK +AT+CSQ ++CSQ: 31, 99 +//确认信号良好 +OK +AT+CREG=2 +OK +AT+CREG? + ++CREG: 2,1,"FFFE","61C7E47",7 //确认模块注册上CS 域 + +OK +AT+CREG=0 +OK +AT+CGATT? ++CGATT: 1 +//确认模块附着上PS 域 +OK +AT$MYSYSINFO + +$MYSYSINFO: 4,02 +//确认模块注册的网络制式 + +OK + +2. +激活网络连接 + +AT$MYNETURC=1 +OK +AT$MYNETACT=0,1 + + + + +//激活PPP 连接 +OK +$MYURCACT: 0,1,"10.51.228.13" +AT$MYNETACT? + + + + +//查询PPP 连接是否正常 + +$MYNETACT: 0,1,"10.51.228.13" +OK + +功能模组TCP 应用指南 +第4 章国网指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +33 + +3. +建立TCP 非透传客户端连接并收发数据 + +AT$MYNETCLOSE=0 +ERROR: 913 +AT$MYNETSRV=0,0,0,0,"183.239.24.40:5002" //使用域名或者IP 均可以建立连接 +OK +AT$MYNETOPEN=0 + + + +//建立TCP 连接 + +$MYNETOPEN: 0,2000 +OK +AT$MYNETWRITE=0,6 + + +//发送数据 + +$MYNETWRITE: 0,6 +OK +AT$MYNETREAD=0,1 +$MYNETREAD: 0,0 +OK +$MYURCREAD: 0 + + + +//接收到服务器发送的数据 + +AT$MYNETREAD=0,2048 + + +//读取数据 +$MYNETREAD: 0,10 +1234567890 +OK + +AT$MYNETCLOSE=0 +$MYNETCLOSE: 0 +OK + +AT+CGATT=0 +OK + +$MYURCACT: 0,0 + +功能模组TCP 应用指南 +第4 章国网指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +34 + +##### 4.2 TCP 非透传服务器端 + +本节主要实现TCP 非透传服务端功能相关的AT 指令、AT 流程、异常处理以及简单示例。在进行 + +PPP 拨号前,请确保模块已经过初始化(具体初始化流程参考第2 章)。 + +图 4-2 连接流程 + +设置PPP拨号参数 + +"$MYURCACT: 0,1"? + +1. AT$MYNETACT=0,1 + +"$MYNETCLOSE: X" + +or "ERROR: 913"? + +3. AT$MYNETCLOSE=X + +"OK" ? + +4. AT$MYNETSRV=0,X,1,0,"127.0.0.1:Port" + +Yes + +5. AT$MYNETOPEN=X + +"$MYNETOPEN: X" or" ERROR: 912" ? + +6.AT$MYNETACCEPT=Y,0,0 + +"OK" ? + +$MYURCREAD: Y + +"OK" or "ERROR"? + +No + +No + +Yes + +重新开启<3次? + +查询<3次? + +关机 + +No + +No + +Yes + +模块重启 +模块初始化流程 + +7. AT$MYNETWRITE=Y, + +"$MYNETWRITE: Y," ? + +输入要发送的数据 + +"OK"? + +No + +去附着<3次? + +重发<3次? + +重发<3次? + +Yes + +No + +No +No +Yes + +No +Yes +No + +1. 进行PPP拨号 +- 若返回"OK",则可进入下一步。 +- 若未收到任何回码或收到其他错误回码,则继续拨号,1s/次,最多 + +3次。若重新拨号3次后,仍未若返回"OK ",则重启模块。 + +Yes + +Yes + +Yes + +Yes + +4. 设置非透明传输服务参数 +- 若返回"OK",则进入下一步; +- 若未收到回码或收到其他错误回码,则继续设置,最多3次。若重 + +复设置3次后,仍未收到"OK",则重启模块。 +注:信道号必须与已激活的信道号一致,第三个参数必须设置为1; + +5. 开启服务器服务 + +• 若返回"$MYNETOPEN: X"或" ERROR: 912" ,则进入下一步。该指令等 + +- 待返回"$MYNETOPEN: X"或" ERROR: 912"的超时时间为30s。 +- 若返回"ERROR: 901"则重新设置参数进行PPP拨号。 +- 若超时未收到回码或收到其他错误回码,则继续开启,最多3次。 + +若重复开启3次后,仍未收到"$MYNETOPEN: X"或" ERROR: 912"则 +重启模块。 + +等待接收客户端连接请求 +需要设置等待客户端连接的超时时间,若超时时间到,没有收到任何 + +客户端连接请求,则主动重新拨号建立侦听。 + +7. 发送数据 +- 若返回"$MYNETWRITE: Y,",则延时50-100ms,将数据发送到 + +模块串口,等待数据发送成功的回码"OK"。若返回"OK",则进入下 +一步。 +• 若返回"ERROR: 901",则返回重新进行PPP拨号。 +• 若返回"ERROR: 913",则等待客户端连接。 +• 若返回其他错误回码、或者未返回任何回码,则继续重发,最多3 + +次。若重发3次仍未收到"OK",则重启模块。 + +11. 去附着网络 +- 若返回"OK"或者" ERROR",则进入下一步,等返回的超时时间为 + +15s。 +• 若超时未收到回码或者收到其他错误回码,则继续执行,最多3 + +次,若重复关闭3次后,仍未收到"OK"或者" ERROR"则重启模块。 + +8. AT$MYNETACK=Y + +"$MYNETACK: Y,0" ? + +- 6.接受客户端的连接请求 +- 若返回"OK"则进入下一步; +- 若超时未收到回码或者收到其他错误回码,则继续去附着,最多3 + +次。若重复关闭3次后,仍未收到"OK"则重启模块。 + +关闭<3次? + +设置<3次? + +查询<3次? + +去附着<3次? + +Yes + +No + +Yes + +No + +No +Yes + +Yes + +No +No + +2. 查询PPP连接建立情况 +- 若回码中包含"+XIIC: 1 ",则可进入下一步。 +- 若回码未包含"+XIIC: 1",则循环查询,1s/次,最多30次。若30次 + +后仍未返回"+XIIC: 1",则重新拨号,最多重拨3次,若重新拨号3次 +后,仍未获取到IP,则重启模块。 + +3. 关闭侦听 +- 若返回"$MYNETCLOSE: X"或"ERROR: 913",则进入下一步。该指令等 + +- 待返回"$MYNETCLOSE: X"或"ERROR: 913"的超时时间为30s。 +- 若返回"ERROR: 901"则重新设置参数进行PPP拨号。 +- 若超时未收到回码或收到其他错误回码,则继续关闭,最多3次。 + +若重复关闭3次后,仍未收到"$MYNETCLOSE: X"或"ERROR: 913"则重 +启模块。 + +$MYURCCLIENT: Y,"10.91.215.190",63342 + +8. 查询数据发送情况 +- 若回码包含"$MYNETACK: Y,0,",则进入下一步:继续发送或者接收 + +- 数据。 +- 若返回"ERROR: 901",则返回重新PPP拨号。 +- 若返回"ERROR: 913",则等待客户端连接。 +- 若回码中""字段不为0,则继续查询,1s/次,最 + +多5次;若查询5次,""字段不为0,则返回关闭 +当前连接,重新建立连接。 +• 若返回其他错误回码、或者未返回任何回码,则继续重发,最多3 + +次。若重发3次仍返回其他错误回码、或者未返回任何回码,则重 +启模块。 + +9. AT$MYNETREAD=Y, + +"$MYNETREAD: Y,0" ? + +10. AT$MYNETCLOSE=X + +"$MYNETCLOSE: X" + +or "ERROR: 913"? + +11. AT+CGATT=0 + +读取<3次? + +Yes + +No + +No + +关闭侦听<3次? + +"$MYNETCLOSE: X" + +or "ERROR: 913"? + +2. AT$MYNETACT? + +查询<3次? + +Yes + +Yes + +Yes + +Yes + +Yes + +Yes + +9. 读取数据 +- 若返回"$MYNETREAD: Y,0",则可进入下一步:继续发送或者接收数 + +据。若回码中""字段不为0,则继续读取。 +• 若返回"ERROR: 901",则重新PPP拨号;返回"ERROR: 913",则等待客 + +户端连接。 +• 若返回其他错误回码、或者未返回任何回码,则继续读取,最多3 + +次。若重读3次仍返回其他错误回码、或者未返回任何回码,则重 +启模块。 + +10. 关闭侦听 +- 若返回"$MYNETCLOSE: X"或"ERROR: 913",则进入下一步。该指令等 + +待返回"$MYNETCLOSE: X"或"ERROR: 913"的超时时间为30s。 +• 若超时未收到回码或收到其他错误回码,则继续关闭,最多3次。 + +若重复关闭3次后,仍未收到"$MYNETCLOSE: X"或"ERROR: 913"则重 +启模块。 + +No + +No + +No + +No +No +Yes + +No + +Yes + +No + +Yes + +功能模组TCP 应用指南 +第4 章国网指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +35 + +###### 4.2.1 异常处理 + +###### 模块附着PS 域失败 + +1. +检查模块串口通信是否正常。若串口不通,可参考以下方式排查解决: + +− +确保模块已经开机:若模块不开机需要检查电源设计和开机时序 + +− +确保串口电平变换正常:若电平变换不正常需要检查串口电路设计和器件选型 + +− +确保软件上串口配置正常,软件处理是否有误。比如自适应逻辑、是否带回车换行发送AT + +2. +检查模块是否已经识别到SIM 卡。若模块无法识别SIM 卡,可参考以下方式排查解决(涉及到的 + +AT 命令,请参考功能 AT 指令集): + +− +确保SIM 卡是否插好、SIM 卡本身完好无损、与卡座接触是否良好 + +− +确保SIM 卡座相关电路无设计问题,是否有虚假焊等 + +− +确保SIM 卡没有被锁卡 + +若通过以上排查模块仍无法识别到SIM 卡,可以联系运营商处理或者更换SIM 卡解决。 + +3. +查询当前模块的接收信号是否满足通信条件。我们建议信号强度值为12(-83dbm)再进行通信。 + +若现场信号弱,可通过以下方式排查: + +− +确保天线接好、天线本身完好无损(断路、短路) + +− +确认模块注册的网络制式(AT$MYSYSINFO)等信息。 + +若为现场网络环境问题,则联系运营商增强网络覆盖,或者更换其他运营商SIM 卡解决。 + +4. +若现场信号良好,模块能识别到SIM 卡且SIM 卡未欠费,但模块不能附着上PS 域,则需要联系 + +运营商确定SIM 卡配置: + +− +SIM 卡是否开通数据业务 + +− +SIM 卡配置鉴权参数是否正常 + +− +SIM 卡是否有绑定设备 + +− +SIM 卡数据业务相关的APN、用户名、密码、鉴权方式是否正常 + +###### 模块激活PDP 失败 + +若模块正常注册上PS 域,但是无法激活PPP 连接。可以参考以下方式排查解决(涉及到的AT 命 + +令,请参考功能 AT 指令集): + +功能模组TCP 应用指南 +第4 章国网指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +36 + +⚫ +确保SIM 未欠费. + +⚫ +使用AT+$MYNETCON?查询APN、用户名、密码,确保这些参数与运营商提供的一致。 + +若以上参数都没有问题,模块仍无法激活PPP 连接,请联系当地运营商协助检查SIM 卡和后台服 + +务器参数配置。 + +###### 客户端无法连接服务器 + +若模块正常建立服务器端侦听后,客户端无法连接上服务器。可以参考以下方式排查解决(涉及到 + +的AT 命令,请参考功能 AT 指令集): + +1. +确认服务器IP 和端口都已配置好,端口已经建立服务器端侦听,IP 能被客户端PING 通。 + +可以通过用其他客户端设备来连接该服务器IP 和端口确认。 + +2. +确认模块获取到的IP 与客户端的IP 是否在同一个网段内。 + +可通过在PC 上PING 模块来确认。 + +− +若PC 客户端能PING 通服务器,则需要检查服务器软件和端口配置。 + +− +若PC 客户端不能PING 通服务器,则需要检查APN、用户名、密码设置是否正确,SIM 卡 + +配置是否正确。 + +3. +确认SIM 卡配置是否有问题。 + +可以直接联系运营商客户端是否可以访问SIM 卡的IP 或者让运营商直接跟踪后台消息排查原因。 + +###### 模块发送TCP 数据失败 + +若模块正常建立TCP 非透传客户端连接,但是发送TCP 数据失败。可以参考以下方式排查解决 + +(涉及到的AT 命令,请参考功能 AT 指令集): + +1. +确认是否是服务器问题。 + +可通过更换服务器对比确认是否是服务器配置问题。 + +2. +确认SIM 卡是否也有欠费。 + +可联系运营商确认。 + +3. +确认是否是长时间没有发送数据导致的假连接。 + +可通过缩短心跳周期或者是提高数据发送频率来确认。 + +若缩短心跳周期或者是提高数据发送频率后,数据发送正常则可能是假连接引起的问题。 + +4. +确认TCP 连接是否还存在。 + +功能模组TCP 应用指南 +第4 章国网指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +37 + +可通过”AT$MYNETOPEN?”命令的返回值来确认. + +− +若返回值中含有“$MYNETOPEN:”字符串,则表示TCP 连接存在。 + +− +若返回字段只有OK,则表示TCP 连接不存在。 + +5. +确认终端(或主控)是否已经将数据发送到模块。 + +可以通过模块串口与终端(或主控)串口之间交互的AT 命令和数据内容来确认。 + +为此,我司建议主控具备保存或打印模块与终端串口交互log 功能。只要AT$MYNETWRITE 命 + +令返回“OK”就表示主控数据已经发送到模块。 + +6. +确认模块底层协议栈是否收到服务器端TCP 协议层的ACK。 + +可通过”AT$MYNETACK =”命令的返回值来确认。 + +− +若返回值中字段数值为0,则表示数据已经发送到服务器且服务器已经 + +确认。 + +− +若返回值中字段数值不为0,则有可能是数据没有发送到服务器,或者 + +是服务器接收有延时还未收到数据,需要在服务器和模块同时抓取TCP 层数据来排查,可以 + +联系我司FAE 跟进排查。 + +###### 4.2.2 示例 + +1. +设置APN、用户名、密码,检查并确保模块已成功注册上PS 域 + +AT +//确认模块能正常响应AT 命令 +OK +AT+CGSN ++CGSN: "865324037375316" +OK + +AT+CGMR ++CGMR: N720_DAC0CM_DL_V004 +OK +AT+CPIN? ++CPIN: READY +//确认模块能识别到SIM 卡 +OK +AT+CIMI + +460016657520885 +OK + +AT$MYNETCON=0,"APN","CMNET" + + +//设置APN +OK +AT$MYNETCON=0,"USERPWD","CARD,CARD" + +//设置用户名和密码 +OK +AT$MYNETCON=0,"AUTH",1 + + + +//设置鉴权方式 +OK +AT+CSQ ++CSQ: 31, 99 +//确认信号良好 +OK +AT+CREG=2 + +OK +AT+CREG? ++CREG: 2,1,"FFFE","61C7E47",7 //确认模块注册上CS 域 + +功能模组TCP 应用指南 +第4 章国网指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +38 + +OK +AT+CREG=0 +OK +AT+CGATT? ++CGATT: 1 +//确认模块附着上PS 域 +OK +AT$MYSYSINFO +$MYSYSINFO: 4,02 +//确认模块注册的网络制式 + +OK + +2. +激活网络连接 + +AT$MYNETURC=1 +OK +AT$MYNETACT=0,1 + + + + +//激活PPP 连接 +OK +$MYURCACT: 0,1,"10.51.228.13" + +AT$MYNETACT? + + + + +//查询PPP 连接是否正常 +$MYNETACT: 0,1,"10.51.228.13" +OK + +3. +建立TCP 非透传服务器端侦听并与客户端建立连接、收发数据 + +AT$MYNETCLOSE=0 +ERROR: 913 +AT$MYNETSRV=0,0,1,0,"127.0.0.1:6800" + +OK + +AT$MYNETOPEN=0 +$MYNETOPEN: 0 +OK +$MYURCCLIENT: 1,"112.97.54.136",14759 +AT$MYNETACCEPT=1,0,0 +OK +AT$MYNETWRITE=1,10 +$MYNETWRITE: 0,10 + 输入要发送的数据 + +OK + +$MYURCREAD: 1 +AT$MYNETREAD=1,2048 +$MYNETREAD: 1,6 + +Neoway +OK + +AT$MYNETACK=1 +$MYNETACK: 1,0,10240 +OK + +AT$MYNETCLOSE=X + +OK +$MYURCCLOSE: 1 +$MYURCCLOSE: 0 +AT+CGATT=0 +OK +$MYURCACT: 0,0 + +##### 4.3 TCP 透传客户端 + +本节主要实现TCP 透传客户端功能相关的AT 指令、AT 流程、异常处理以及简单示例。在进行PPP + +功能模组TCP 应用指南 +第4 章国网指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +39 + +拨号前,请确保模块已经过初始化(具体初始化流程参考第2 章)。 + +图 4-3 连接流程 + +设置PPP拨号参数 + +"$MYURCACT: 0,1"? + +1. AT$MYNETACT=0,1 + +"$MYNETACT: 0,1"? + +2. AT$MYNETACT? + +"$MYNETCLOSE: X" + +or "ERROR: 913"? + +3. AT$MYNETCLOSE=X + +Yes + +4. AT$MYNETCREATE=0,0,0,"IP",Port + +"CONNECT"? + +收发数据 + +9. AT+CGATT=0 + +"OK" or "ERROR"? + +No + +Yes +重设<3次? + +拨号<3次? + +关机 + +No + +No + +Yes + +模块重启 +模块初始化流程 + +5. +++ + +"OK" ? + +6. AT$MYNETACK=X + +"$MYNETACK: X,0"? + +No + +重发<3次? + +查询<3次? + +返回透传<3次? + +Yes +No + +No +Yes + +No +Yes + +1. PPP拨号 + +• 若返回"$MYURCACT: 0,1"或"ERROR: 902",则进入下一步。该指令等待返 + +回"$MYURCACT: 0,1"或"ERROR: 902"的超时时间为30s。 +• 若未收到任何回码或收到其他错误回码,则继续拨号,最多3次。若重 + +新拨号3次后,仍未若返回"OK ",则重启模块。 + +Yes + +Yes + +4. 建立透明TCP客户端连接 +- 若返回"CONNECT"或" ERROR: 912" ,则进入下一步。该指令等待返回 + +- "CONNECT"或" ERROR: 912"的超时时间为30s。 +- 若返回"ERROR: 901"则重新设置参数进行PPP拨号。 +- 若超时未收到回码或收到其他错误回码,则继续开启,最多3次。若重 + +复开启3次后,仍未收到"CONNECT"或" ERROR: 912"则重启模块。 + +5. 退出透传 +- 若返回"OK",则进入下一步; +- 若返回其他错误回码、或者未返回任何回码,则继续重发,最多3次。若 + +重发3次仍未收到"OK",则重启模块。 +• 注:"+++"必须以字符串的形式发送到串口,且"+++"不必带回车发送 + +8. 关闭待用Socket +- 若返回"$MYNETCLOSE: X"或"ERROR: 913",则进入下一步。该指令等待返 + +- 回"$MYNETCLOSE: X"或"ERROR: 913"的超时时间为30s。 +- 若返回"ERROR: 901"则重新设置参数进行PPP拨号。 +- 若超时未收到回码或收到其他错误回码,则继续关闭,最多3次。若重 + +复关闭3次后,仍未收到"$MYNETCLOSE: X"或"ERROR: 913"则重启模块。 + +9. 去附着网络 +- 若返回"OK"或者" ERROR",则进入下一步,等返回的超时时间为15s。 +- 若超时未收到回码或者收到其他错误回码,则继续执行,最多3次,若 + +重复关闭3次后,仍未收到"OK"或者" ERROR"则重启模块。 + +7.ATO + +"$MYNETCLOSE: X" + +or "ERROR: 913"? + +7. 返回透传 + +- 若返回"CONNECT",则进入下一步:继续发送数据。 +- 若返回其他错误回码、或者未返回任何回码,则继续重发,最多3次。 + +若重发3次仍未收到"OK",则重启模块。 + +关闭<3次? + +关闭<3次? + +Yes + +No + +No +Yes + +2. 查询PPP连接建立情况 + +- 若回码中包含"$MYNETACT: 0,1"字段,则进入下一步。 +- 若回码中包含"$MYNETACT: 0,0"则重新设置参数进行PPP拨号。 +- 若返回其他错误回码,则循环查询,1s/次,最多3次。若查询3次后仍 + +未返回"$MYNETACT: 0,1",则重新设置参数进行PPP拨号。 +• 若未返回任何回码,则继续查询,1s/次,最多3次。若查询继续3次 + +后,仍未返回任何回码,则重启模块。 + +3. 关闭待用Socket +- 若返回"$MYNETCLOSE: X"或"ERROR: 913",则进入下一步。该指令等待返 + +- 回"$MYNETCLOSE: X"或"ERROR: 913"的超时时间为30s。 +- 若返回"ERROR: 901"则重新设置参数进行PPP拨号。 +- 若超时未收到回码或收到其他错误回码,则继续关闭,最多3次。若重 + +复关闭3次后,仍未收到"$MYNETCLOSE: X"或"ERROR: 913"则重启模块。 + +"CONNECT" ? + +8. AT$MYNETCLOSE=X + +No +查询<3次? + +Yes + +No + +关闭<3次? + +Yes +No + +Yes + +Yes + +Yes + +Yes + +6. 查询数据发送情况 +- 若返回"$MYNETACK: X,0,",则进入下一步:继续发送或者接收数据; +- 若返回"ERROR: 901",则返回重新PPP拨号; +- 若返回"ERROR: 913",则重新建立TCP连接; +- 若回码中""字段不为0,则继续查询,1s/次,最多5 + +次。若查询5次后,""字段不为0,则返回关闭当前连 +接,重新建立连接; +• 若返回其他错误回码、或者未返回任何回码,则继续重发,最多3次。 + +若重发3次仍返回其他错误回码、或者未返回任何回码,则重启模块。 + +No + +No + +No + +No + +No + +No + +功能模组TCP 应用指南 +第4 章国网指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +40 + +###### 4.3.1 异常处理 + +###### 模块附着PS 域失败 + +1. +检查模块串口通信是否正常。若串口不通,可参考以下方式排查解决: + +− +确保模块已经开机:若模块不开机需要检查电源设计和开机时序 + +− +确保串口电平变换正常:若电平变换不正常需要检查串口电路设计和器件选型 + +− +确保软件上串口配置正常,软件处理是否有误。比如自适应逻辑、是否带回车换行发送AT + +2. +检查模块是否已经识别到SIM 卡。若模块无法识别SIM 卡,可参考以下方式排查解决(涉及到的 + +AT 命令,请参考功能 AT 指令集): + +− +确保SIM 卡是否插好、SIM 卡本身完好无损、与卡座接触是否良好 + +− +确保SIM 卡座相关电路无设计问题,是否有虚假焊等 + +− +确保SIM 卡没有被锁卡 + +若通过以上排查模块仍无法识别到SIM 卡,可以联系运营商处理或者更换SIM 卡解决。 + +3. +查询当前模块的接收信号是否满足通信条件。我们建议信号强度值为12(-83dbm)再进行通信。 + +若现场信号弱,可通过以下方式排查: + +− +确保天线接好、天线本身完好无损(断路、短路) + +− +确认模块注册的网络制式(AT$MYSYSINFO)等信息。 + +若为现场网络环境问题,则联系运营商增强网络覆盖,或者更换其他运营商SIM 卡解决。 + +4. +若现场信号良好,模块能识别到SIM 卡且SIM 卡未欠费,但模块不能附着上PS 域,则需要联系 + +运营商确定SIM 卡配置: + +− +SIM 卡是否开通数据业务 + +− +SIM 卡配置鉴权参数是否正常 + +− +SIM 卡是否有绑定设备 + +− +SIM 卡数据业务相关的APN、用户名、密码、鉴权方式是否正常 + +###### 模块激活PDP 失败 + +若模块正常注册上PS 域,但是无法激活PPP 连接。可以参考以下方式排查解决(涉及到的AT 命 + +令,请参考功能 AT 指令集): + +功能模组TCP 应用指南 +第4 章国网指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +41 + +⚫ +确保SIM 未欠费。 + +⚫ +使用AT+$MYNETCON?查询APN、用户名、密码,确保这些参数与运营商提供的一致。 + +若以上参数都没有问题,模块仍无法激活PPP 连接,请联系当地运营商协助检查SIM 卡和后台服 + +务器参数配置。 + +###### 模块建立TCP 透传客户端连接失败 + +若模块正常获取到IP,但是建立TCP 非透传客户端连接。可以参考以下方式排查解决(涉及到的 + +AT 命令,请参考功能 AT 指令集): + +1. +确认服务器IP 和端口都已配置好,端口已经建立服务器端侦听,IP 能被客户端PING 通。 + +可以通过用其他客户端设备来连接该服务器IP 和端口确认。 + +2. +确认模块获取到的IP 是否能访问服务器。 + +可通过发送AT+PING=给模块,操作模块PING 服务器来确认。 + +若模块能PING 通服务器,则需要检查服务器软件和端口配置。 + +若模块不能PING 通服务器,则需要检查APN、用户名、密码设置是否正确,SIM 卡配置是否正 + +确。 + +3. +确认SIM 卡配置是否有问题。 + +可以直接联系运营商确认SIM 卡是否可以访问目的IP 或者让运营商直接跟踪后台消息排查原因。 + +###### 模块发送TCP 数据失败 + +若模块正常建立TCP 非透传客户端连接,但是发送TCP 数据失败。可以参考以下方式排查解决 + +(涉及到的AT 命令,请参考功能 AT 指令集): + +1. +确认是否是服务器问题。 + +可通过更换服务器对比确认是否是服务器配置问题。 + +2. +确认SIM 卡是否也有欠费。 + +可联系运营商确认。 + +3. +确认是否是长时间没有发送数据导致的假连接。 + +可通过缩短心跳周期或者是提高数据发送频率来确认。 + +若缩短心跳周期或者是提高数据发送频率后,数据发送正常则可能是假连接引起的问题。 + +4. +确认模块底层协议栈是否收到服务器端TCP 协议层的ACK。 + +功能模组TCP 应用指南 +第4 章国网指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +42 + +可通过”AT$MYNETACK =”命令的返回值来确认。 + +− +若返回值中字段数值为0,则表示数据已经发送到服务器且服务器已经 + +确认。 + +− +若返回值中字段数值不为0,则有可能是数据没有发送到服务器,或者 + +是服务器接收有延时还未收到数据,需要在服务器和模块同时抓取TCP 层数据来排查,可以 + +联系我司FAE 跟进排查。 + +###### 模块接收TCP 数据失败 + +若模块正常建立TCP 非透传客户端连接,但是接收TCP 数据失败。可以参考以下方式排查解决 + +(涉及到的AT 命令,请参考功能 AT 指令集): + +1. +确认是否是服务器问题。 + +可通过更换服务器对比确认是否是服务器配置问题。 + +2. +确认SIM 卡是否也有欠费。 + +可联系运营商确认。 + +3. +确认TCP 连接是否还存在。 + +可通过”AT$MYNETOPEN?”命令的返回值来确认。 + +− +若返回值中含有“$MYNETOPEN:”字符串,则表示TCP 连接存在。 + +− +若返回字段只有OK,则表示TCP 连接不存在。 + +4. +确认是否是长时间没有发送数据导致的假连接。 + +可通过缩短心跳周期或者是提高数据发送频率来确认。 + +− +若缩短心跳周期或者是提高数据发送频率后,数据接收正常则可能是假连接引起的问题。 + +− +若缩短心跳或提高数据发送频率仍无法确认接收不到数据的原因,需要在服务器和模块同时 + +抓取TCP 层数据来排查,可以联系我司FAE 跟进排查。 + +###### 4.3.2 示例 + +1. +检查并确保模块已成功注册上PS 域 + +AT +//确认模块能正常响应AT 命令 +OK +AT+CGSN + ++CGSN: "865324037375316" +OK + +AT+CGMR ++CGMR: N720_DAC0CM_DL_V004 +OK + +功能模组TCP 应用指南 +第4 章国网指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +43 + +AT+CPIN? ++CPIN: READY +//确认模块能识别到SIM 卡 +OK +AT+CIMI +460016657520885 +OK +AT$MYNETCON=0,"APN","CMNET" + + +//设置APN +OK + +AT$MYNETCON=0,"USERPWD","CARD,CARD" + +//设置用户名和密码 +OK +AT$MYNETCON=0,"AUTH",1 + + + +//设置鉴权方式 +OK +AT+CSQ ++CSQ: 31, 99 +//确认信号良好 +OK +AT+CREG=2 +OK + +AT+CREG? ++CREG: 2,1,"FFFE","61C7E47",7 //确认模块注册上CS 域 + +OK +AT+CREG=0 +OK + +AT+CGATT? ++CGATT: 1 +//确认模块附着上PS 域 +OK + +AT$MYSYSINFO + +$MYSYSINFO: 4,02 +//确认模块注册的网络制式 +OK + +2. +激活网络连接 + +AT$MYNETURC=1 +OK +AT$MYNETACT=0,1 + + + + +//激活PPP 连接 +OK +$MYURCACT: 0,1,"10.51.228.13" + +AT$MYNETACT? + + + + +//查询PPP 连接是否正常 +$MYNETACT: 0,1,"10.51.228.13" +OK + +3. +建立TCP 非透传客户端连接并收发数据 + +AT$MYNETCLOSE=0 + +ERROR: 913 + +AT$MYNETCREATE=0,0,0,"183.239.240.40",5008 + +CONNECT + + +//收发数据后,发送+++数据模式 +OK +AT$MYNETACK=0 +$MYNETACK: 0,0,2047 +OK +ATO + + + + + +//切换回数据模式 +CONNECT + +OK +AT$MYNETCLOSE=0 + + + +//关闭链路 + +功能模组TCP 应用指南 +第4 章国网指令TCP 应用 + +深圳市有方科技股份有限公司版权所有 +44 + +$MYNETCLOSE: 0 +OK +AT+CGATT=0 +OK +$MYURCACT: 0,0 + +功能模组TCP 应用指南 +第5 章外部协议栈TCP 应用 + +深圳市有方科技股份有限公司版权所有 +45 + +#### 5 外部协议栈TCP 应用 + +图 5-1 连接流程 + +模块初始化流程 + +"CONNECT"? + +ATD*99# + +进入外部协议栈成功 + +进入外部协议栈 +• 若返回 CONNECT ,则模块进入外部协议栈模式;该指令 + +等待返回"CONNECT"的超时时间为30s。 +• 若返回 NO CARRIER ,则继续执行ATD*99#,最多5次。若 + +执行5次后仍未成功,则返回执行模块初始化流程查询网络 +注册情况。 +• 若超时未收到任何回码或者收到其他错误回码,则继续执行 + +ATD*99#,最多3次。若执行3次后仍未成功,则重启模块。 +Yes + +No + +返回"NO CARRIER + +继续执行" + +其它情况 +重启模块 + +##### 5.1 异常处理 + +###### 模块附着PS 域失败 + +使用AT+CPIN?检查模块是否已经识别到SIM 卡。若模块无法识别SIM 卡,可参考以下方式排查 + +解决(涉及到的AT 命令,请参考功能 AT 指令集): + +− +确保SIM 卡是否插好; + +− +确保SIM 卡本身完好无损; + +− +确保SIM 卡没有被锁卡; + +− +确保SIM 卡卡座完好,不存在假焊; + +− +擦拭SIM 卡及卡座的触点(该触点发生氧化、脏污也会产生读不到SIM 卡的现象),确保SI + +卡与模块卡座接触良好。 + +若通过以上排查模块仍无法识别到SIM 卡,可以更换SIM 卡解决。 + +1. +检查天线是否完好,使用AT+CSQ 命令查询当前模块的接收信号是否满足通信条件。 + +我们建议信号强度值为15(-83dbm)再进行通信。 + +2. +若现场信号良好,模块能识别到SIM 卡且SIM 卡未欠费,但模块不能附着上PS 域,可通过 + +``` +AT+CGATT=1 命令,主动附着PS 域。 +``` + +功能模组TCP 应用指南 +第5 章外部协议栈TCP 应用 + +深圳市有方科技股份有限公司版权所有 +46 + +##### 5.2 示例 + +1. +设置APN、用户名、密码,检查并确保模块已成功注册上PS 域 + +AT +OK + +AT+CGSN ++CGSN: "865324037375316" +OK +AT+CGMR ++CGMR: N720_EAB0CM_BZ_V008 +OK +AT+CPIN? ++CPIN: READY +OK +AT+CIMI + +460016657520885 +OK +AT+CGDCONT=1,"IP","UNINET" +//设置APN +OK +AT+XGAUTH=1,1, "CARD","CARD" +//设置用户名、密码、鉴权方式 +OK +AT+CSQ ++CSQ: 31, 99 +OK + +AT+CREG=2 +OK +AT+CREG? ++CREG: 2,1,"FFFE","61C7E47",7 +OK +AT+CREG=0 +OK +AT+CGATT? ++CGATT: 1 +OK + +AT$MYSYSINFO +$MYSYSINFO: 4,02 +OK + +2. +进入外部协议栈 + +ATD*99# + //进入外部协议栈 +CONNECT + +功能模组TCP 应用指南 + +A 关键AT 命令 + +深圳市有方科技股份有限公司版权所有 +47 + +#### A 关键AT 命令 + +##### A.1 标准AT 指令 + +###### A.1.1 AT+CGDCONT–设置PDP 格式 + +设置GPRS 的PDP(Packet Data Protocol,分组数据协议)格式。目前仅支持设置一个APN。 + +###### 命令格式 + +类型 +命令 +响应格式 + +设置 + +AT+CGDCONT=[,[,[ +,[,[,[, +[,…[,]]]]]]]] + +OK +Or +ERROR + +查询 +AT+CGDCONT? + ++CGDCONT: +,,,,,, ,…, + +OK + +测试 +AT+CGDCONT=? + ++CGDCONT: value range +of , value range of , +value range of , value range of +, +value +range +of +, value range of , +value range of ,…, value range +of pdN +OK + +###### 参数 + + +(PDP Context Identifier)一个数字参数,指定一个PDP 上下文定义,这个参数是 + +当地的TE-MT 接口并且被应用到其他PDP 上下文相关的命令当中,使用查询命 + +令可以查询到允许的值(最小值为1) + + +(Packet Data Protocol type)字符串参数,用于指定分组数据协议的类型 + +“IP”网络协议(Internet Protocol)(IETFSTD 5) + +功能模组TCP 应用指南 + +A 关键AT 命令 + +深圳市有方科技股份有限公司版权所有 +48 + +“PPP”点对点协议(Point to Point Protocol)(IETF STD 51) + +“IPv6”网络协议V6 + + +(Access Point Name)字符串形式,是一个逻辑名称,用来选择GGSN 或者外部 + +分组数据网。 + + +字符串形式,用来在地址空间中区分MT。 + +如果不写这个参数,则在PDP 的启动过程当中由TE 提供这个值。 + +如果TE 提供失败,就请求动态地址,即使在PDP 的启动过程当中分配了地址, + +在使用这条指令查询的时候仍然会返回空。 + + +数字参数用来控制PDP 数据压缩(仅适用于SNDCP) + +0 - off (缺省情况下默认值) + + +数字参数用来控制PDP 头部压缩0 - off (缺省情况下默认值) + +, … 0 到N,字符串类型,意义与有关 + +###### 示例 + +AT+CGDCONT=1,"IP","CMNET" +OK +设置PDP 格式,PDP 类型为IP,APN 名称为CMNET + +AT+CGDCONT? ++CGDCONT: 1,"IP","CMNET","0.0.0.0",0,0,0,0 + +OK + +查询当前PDP 格式 + +AT+CGDCONT=? ++CGDCONT: (1-24),"IP",,,(0-2),(0-4),(0-1),(0-1) ++CGDCONT: (1-24),"PPP",,,(0-2),(0-4),(0-1),(0-1) ++CGDCONT: (1-24),"IPV6",,,(0-2),(0-4),(0-1),(0-1) ++CGDCONT: (1-24),"IPV4V6",,,(0-2),(0-4),(0-1),(0-1) +OK + +查询设置PDP 格式的取值范围 + +###### A.1.2 AT+XGAUTH–用户认证 + +PDP 认证。 + +该指令要放在AT+CGDCONT 这条指令后面。目前在专网中各个地方逐渐增加了用户身份认证需 + +求,使用内部协议栈,需要使用到这条指令,因此,请在代码流程上加上这条指令。 + +###### 命令格式 + +类型 +命令 +响应格式 + +设置 +AT+XGAUTH=,[,, +] + +OK +Or +ERROR + +测试 +AT+XGAUTH=? + ++XGAUTH: +(list +of +supported +),(value +range +of),(length +of +),(length of ) + +功能模组TCP 应用指南 + +A 关键AT 命令 + +深圳市有方科技股份有限公司版权所有 +49 + +OK + +###### 参数 + + +(PDP Context Identifier)一个数字参数,指定一个PDP 上下文定义 + +对应+CGDCONT 中的。 + + +鉴权类型,默认为1 + +0:NONE + +1:PAP + +2:CHAP + +3:PAP or CHAP + +鉴权类型为非NONE 时,需带和参数 + + +用户名 + + +密码 + +###### 示例 + +AT+XGAUTH=1,1,”gsm”,”1234” +OK +设置第一个PDP 认证 + +AT+XGAUTH=? ++XGAUTH: (1),(0-3),32,32 +OK + +查询参数值范围 + +###### A.1.3 AT+XIIC–建立PPP 链接 + +建立PPP 连接,获取IP 地址。 + +在使用AT+XIIC=1 建立PPP 连接之前,确保模块已经注册上网络。可使用AT+CEREG?来判断, + +如果返回+CEREG: 0,1 或+CEREG: 0,5,都表明已注册上网络。 + +###### 命令格式 + +类型 +命令 +响应格式 + +执行 +AT+XIIC= + +OK +Or +ERROR + +查询 +AT+XIIC? ++XIIC: , +OK + +功能模组TCP 应用指南 + +A 关键AT 命令 + +深圳市有方科技股份有限公司版权所有 +50 + +###### 响应时间 + +最大响应时间300ms。 + +###### 参数 + + +0:去激活PPP 连接 + +1:激活PPP 连接 + + +IP 地址 + +###### 示例 + +AT+XIIC=1 +OK +建立第一个ppp 连接 + +AT+XIIC? ++XIIC: 1,10.107.216.162 +OK + +第一个PPP 链路建立成功,IP 地址是10.107.216.162。 + +(1 前面有4 个空格) + +AT+XIIC? ++XIIC: 0,0.0.0.0 +OK + +PPP 链路还未å»ºç«‹æˆåŠŸï¼Œ + +(0 前面有4 个空格) + +###### A.1.4 AT+TCPSETUP–建立TCP 连接 + +建立TCP 连接。 + +请先使用AT+XIIC=1 建立PPP 链接。 + +###### 命令格式 + +类型 +命令 +响应格式 + +执行 +AT+TCPSETUP=,,[,] + +OK ++TCPSETUP:, +Or +ERROR +Or ++TCPSETUP:,ERROR1 +Or ++TCPSETUP:ERRPR + +###### 响应时间 + +响应超时时间为60 秒。 + +功能模组TCP 应用指南 + +A 关键AT 命令 + +深圳市有方科技股份有限公司版权所有 +51 + +###### 参数 + + +链路编号,只能为0~5 + + +目的IP 地址,必须是形如xx.xx.xx.xx 的输入,或者形如www.china.com(域名) + + +目的端口号,必须是十进制的ASCII 码 + + +模式选择,范围0-1 + +0:接收到的TCP、UDP 数据直接从串口输出(默认) + +1:接收到的TCP、UDP 数据缓存起来,输出来数据提示,需要外部MCU 主动发命 + +令读取数据 + + +结果码 + +OK 建立成功 + +FAIL 建立失败 + +LINK ALREADY OPENED 连接已存在 + +###### 示例 + +AT+TCPSETUP=0,220.199.66.56,6800 +OK + ++TCPSETUP: 0,OK + +在链路0 上建立到 +220.199.66.56,6800 的连接,成功 + +AT+TCPSETUP=0,neowayjsr.oicp.net,60010 +OK + ++TCPSETUP: 0,OK + +在链路0 上建立到 +neowayjsr.oicp.net,60010 的连接,成功 + ++TCPCLOSE: 0,Link Closed +链路被动断开后返回的回码 +AT+TCPSETUP=1,192.168.20.6,7000 +OK + ++TCPSETUP: 1,FAIL + +在链路1 上建立到192.168.20.6,7000 的连接,失 +败;可能是服务器未开通,IP 地址或端口不正确,或 +者是SIM 卡欠费,等等 + +AT+TCPSETUP=0,neowayjsr.oicp.net,60010 +OK + ++TCPSETUP:0,ERROR1 + +当前链路0 的TCP/UDP 链接已存在 + +AT+TCPSETUP=6,192.168.20.6,7000 +ERROR +AT 指令格式错误,链路号错误 + +AT+TCPSETUP=0.58.60.184.213.10012 +ERROR +AT 指令格式错误,符号错误 + +``` +AT+TCPSET=0,58.60.184.213,10012 +``` + +ERROR +AT 指令格式错误,指令不完整 + +###### A.1.5 AT+TCPSEND–发送TCP 数据 + +发送TCP 数据的命令,支持buffer 模式与命令模式,支持ASCII 与hex 发送。 + +这条命令发送完毕后,会接收到大于号">",这时候请延迟50ms-100ms,然后发送数据。 + +在发送TCP 数据之前,必须确保TCP 链路已经建立;建议在发送数据之前,先使用AT+IPSTATUS + +查看可用的buffer 大小。 + +功能模组TCP 应用指南 + +A 关键AT 命令 + +深圳市有方科技股份有限公司版权所有 +52 + +命令设置参数不保存,每次发送需要设置模式。 + +###### 命令格式 + +类型 +命令 +响应格式 + +执行 +AT+TCPSEND=, +[[,][,mode]] + +> +OK ++TCPSEND: , +Or +> ++TCPSEND: ERROR +Or +> ++TCPSEND: +,OPERATION +EXPIRED +Or ++TCPSEND: + +###### 响应时间 + +最大响应时间300ms。 + +###### 参数 + + +链路编号,只能为0~5,且该链路已建立了TCP 连接 + + +要发送的数据长度,以字节为单位,取值范围: + +buffer 模式发送HEX 数据:1~4096; + +buffer 模式发送ASCII 数据:1~4096; + +命令模式发送HEX 数据:1~512; + +命令模式发送ASCII 数据:1~1024; + + +命令模式下发送的数据,支持转义模式,content 长度范围0~1024 + +Content 内容的逗号个数必须小于15 个,若需要传输大量逗号的数据,建议使用 + +buffer 模式。 + + +HEX 发送启用开关 + +0:ASCII 模式发送,支持转义字符(默认) + +1:HEX 模式发送 + + +SOCKET ID OPEN FAILED +DATA LENGTH ERROR + +功能模组TCP 应用指南 + +A 关键AT 命令 + +深圳市有方科技股份有限公司版权所有 +53 + +###### 示例 + +AT+TCPSEND=0,1 +> +OK + ++TCPSEND: 0,1 + +在链路0 上发送1 字节的数据,成 +功 + +AT+TCPSEND=0,1024,,1 +> +OK + ++TCPSEND: 0,1024 + +Buffer 模式,HEX 模式,发送1024 长度数据,发送成功 + +AT+TCPSEND=0,27,"\"3~!@#$%\\^&*()_+;'332\",\"32\"" +OK + ++TCPSEND: 0,27 + +命令模式,转义模式,发送数据,发送成功 + +``` +AT+TCPSEND=0,3,”313233”,1 +``` + +OK + ++TCPSEND: 0,3 + +命令模式,HEX 模式,发送数据,发送成功 + +AT+TCPSEND=0,10 +> + ++TCPSEND:0,OPERATION EXPIRED + +输入发送命令出现“>”后,不输入数据,30 秒后提示超时 + +AT+TCPSEND=0,1 ++TCPSEND: SOCKET ID OPEN FAILED + +在链路0 上发送1 字节的数据,该 +链路尚未建立,发送失败 + +###### A.1.6 AT+IPSTATUS–查询TCP/UDP 链路状态 + +非透传下TCP/UDP 链路状态。由于UDP 的特点,该指令仅能查询是否已经通过指令建立链接, + +并不代表链接的真实情况。 + +###### 命令格式 + +类型 +命令 +响应格式 + +执行 +AT+IPSTATUS= + ++IPSTATUS: +,[,, +] +Or +ERROR + +###### 响应时间 + +最大响应时间300ms。 + +###### 参数 + + +链路编号,范围 0~5 + +功能模组TCP 应用指南 + +A 关键AT 命令 + +深圳市有方科技股份有限公司版权所有 +54 + + +该链路的状态,取值: + +CONNECT +DISCONNECT +CONNECTING +DISCONNECTING + +链路类型,取值为 TCP 或者 UDP + + +模块内部可用的send buffer 的大小,十进制ASCII 码表示,单位为字节 + +UDP 链路上不支持查询参数 + +###### 示例 + +``` +AT+IPSTATUS=0 +``` + ++IPSTATUS: 0,CONNECT,TCP,4096 +链路0,已建TCP 连接,可用buffer 为4096 字节 + +AT+IPSTATUS=0 ++IPSTATUS: 0,CONNECT,UDP,0 +链路0,已建UDP 连接 + +AT+IPSTATUS=1 ++IPSTATUS: 1,DISCONNECT +链路1,未建立任何“TCP/UDP”连接 + +AT+IPSTATU +ERROR +AT 指令格式错误,指令不完整 + +``` +AT+IPSTATUS=6 +``` + +ERROR +指令链路编号错误 + +###### A.1.7 AT+TCPACK–查询TCP 链路发送数据状态 + +查询TCP 链路发送成功的数据大小、接收方成功接收该链路的数据大小。 + +###### 命令格式 + +类型 +命令 +响应格式 + +执行 +AT+TCPACK= + ++TCPACK: +,, +Or +ERROR +Or ++TCPACK: ,DISCONNECT +Or ++TCPACK: NO TCP LINK + +###### 响应时间 + +最大响应时间300ms。 + +功能模组TCP 应用指南 + +A 关键AT 命令 + +深圳市有方科技股份有限公司版权所有 +55 + +###### 参数 + + +链路编号,只能为0~5 + + +该链路发送成功的链路数据大小,无符号64 位整型数,十进制ASCII 码表示,单位 + +为字节。 + + +接收方成功接收的链路数据大小,无符号64 位整型数,十进制ASCII 码表示,单位 + +为字节。 + +###### 示例 + +AT+TCPACK=0 ++TCPACK: 0,20,20 +链路0,发送成功20 个字节数据,接收方成功接收20 个字节数据 + +``` +AT+TCPACK=0 +``` + ++TCPACK: 0,128,120 +链路0,发送成功128 个字节数据,接收方成功接收120 个字节数据 + +AT+TCPACK=1 ++TCPACK: 1,DISCONNECT +链路1,未建立任何连接 + +``` +AT+TCPACK=2 +``` + ++TCPACK: NO TCP LINK +链路2 建立的是UDP 连接 + +AT+TCPACK=6 +ERROR +指令链路编号错误 + +###### A.1.8 AT+TCPLISTEN–设置服务器侦听功能 + +设置服务器侦听功能。 + +使用联通卡或者移动的专网卡可以进行调试使用,移动的公网卡不能作为服务器调试; + +###### 命令格式 + +类型 +命令 +响应格式 + +设置 +AT+TCPLISTEN= + ++TCPLISTEN:,OK +Or ++TCPLISTEN:bind error +Or +Listening... +Or +ERROR + +查询 +AT+TCPLISTEN? + ++TCPLISTEN:listening status +Or ++TCPLISTEN:not listening + +功能模组TCP 应用指南 + +A 关键AT 命令 + +深圳市有方科技股份有限公司版权所有 +56 + +###### 响应时间 + +最大响应时间300ms。 + +###### 参数 + + +端口号 + + +socket 号 + +###### 示例 + +AT+TCPLISTEN=6800 ++TCPLISTEN: 0,OK + +侦听端口号6800 +服务器侦听开始启动 +AT+TCPLISTEN=6800 ++TCPLISTEN: bind error + +侦听端口号6800 +绑定失败 +AT+TCPLISTEN=6800 + +Listening... + +如果已经设置了侦听,再设置的话,会提示 + +Listening... +AT+TCPLISTEN=0 +ERROR +侦听非法端口号 + +AT+TCPLISTEN? ++TCPLISTEN: listening status +查询侦听状态,表示当前处于侦听 + +AT+TCPLISTEN? ++TCPLISTEN: not listening +查询侦听状态,表示当前没有侦听 + +Connect +AcceptSocket=1,ClientAddr=119.123.77.133,ClientPort=8000 + +收到主站连接请求,其AcceptSocket 是主站跟模块 +建立的socket,119.123.77.133 是主站的IP 地 +址,8000 是主站的端口号 + +###### A.1.9 AT+CLOSELISTEN–关闭侦听链接 + +关闭侦听链接。 + +###### 命令格式 + +类型 +命令 +响应格式 + +执行 +AT+CLOSELISTEN + ++CLOSELISTEN:,local link closed +Or ++CLOSELISTEN:Transparent local + +主动上报 ++CLOSELISTEN:,local link closed + +###### 响应时间 + +最大响应时间300ms。 + +功能模组TCP 应用指南 + +A 关键AT 命令 + +深圳市有方科技股份有限公司版权所有 +57 + +###### 参数 + +无。 + +###### 示例 + ++CLOSELISTEN: 0,local link closed +主站关闭链接或网络异常时,会主动上报该回码 +AT+CLOSELISTEN ++CLOSELISTEN: 0,local link closed +如果有主站链接,会同时把主站链接关闭 + +AT+CLOSELISTEN ++CLOSELISTEN:Transparent local +linkclosed + +使用透传方式时,会接收到这些信息 + +###### A.1.10 AT+CLOSECLIENT–关闭主站链接 + +关闭主站链接。 + +###### 命令格式 + +类型 +命令 +响应格式 + +执行 +AT+CLOSECLIENT[=] + ++CLOSECLIENT: +,remote +link closed +Or +ERROR +Or ++CLOSECLIENT: All remote link +closed + +###### 响应时间 + +最大响应时间300ms。 + +###### 参数 + + +socket 号 + +###### 示例 + +AT+CLOSECLIENT ++CLOSECLIENT: 1,remote link closed + ++CLOSECLIENT: 2,remote link closed + +不带参数,多个关闭 + +功能模组TCP 应用指南 + +A 关键AT 命令 + +深圳市有方科技股份有限公司版权所有 +58 + +AT+CLOSECLIENT=1 ++CLOSECLIENT: 1,remote link closed +带参数,单个关闭 + +AT+CLOSECLIENT=1 +ERROR +1 链路不存在远程客户端 + +AT+CLOSECLIENT ++CLOSECLIENT: All remote link closed +所有远程客户端已关闭 + +###### A.1.11 +TCPRECV(S)–接收到主站的数据 + +接收到主站的数据。 + +跟客户端模式的接收格式不同,多了符号“(S)”;跟客户端的参数有所区别,请注意。 + +###### 命令格式 + +类型 +命令 + +主动上报 ++TCPRECV(S): ,, + +###### 响应时间 + +无。 + +###### 参数 + + +链路编号,只能为0~5 + + +接收到的数据长度 + + +接收到的数据。尾部追加0x0d 0x0a。用户可根据参数来判断结尾 + +###### 示例 + ++TCPRECV(S): 1,10,1234567899 +链路1 接收到主站发过来的10 个字节的数据,接收字符格式为字符类型 + +###### A.1.12 AT+TCPREADS–读取到主站的数据 + +读取到主站的数据 + +###### 命令格式 + +类型 +命令 +响应格式 + +执行 +AT+TCPREADS=, ++TCPREADS: ,, + +功能模组TCP 应用指南 + +A 关键AT 命令 + +深圳市有方科技股份有限公司版权所有 +59 + +OK +Or +ERROR + +###### 响应时间 + +最大响应时间300ms。 + +###### 参数 + + +链路编号,只能为0~5 + + +本次允许读取的最大数据长度,范围1-2048 字节 + + +读取到的数据长度 + + +读取到的数据 + +###### 示例 + ++TCPRECV(S): 1 +AT+TCPREADS=1,100 ++TCPREADS: 1,10,1234567890 +OK + +RECVMODE=0 +链路1 接收到主站发过来的10 个字节的数据 + +###### A.1.13 AT+TCPSENDS–发送给主站的数据 + +发送给主站的数据。 + +在发送TCP 数据之前,必须确保TCP 链路已经建立。 + +###### 命令格式 + +类型 +命令 +响应格式 + +执行 +AT+TCPSENDS=[ +,] + +> +OK ++TCPSENDS:, +Or +> ++TCPSENDS: +Or +> ++TCPSENDS: +, +OPERATION +EXPIRED + +功能模组TCP 应用指南 + +A 关键AT 命令 + +深圳市有方科技股份有限公司版权所有 +60 + +Or ++TCPSENDS: is not link + +###### 响应时间 + +出现“>”后,不输入数据,60s 后提示超时。 + +###### 参数 + + +侦听到的AcceptSocket 值,即主站跟模块的建立的socket ,参考 + +``` +AT+TCPLISTEN 指令的说明 +``` + + +要发送的数据长度,以字节为单位,取值范围1~2000 + + +ERROR +Buffer not enough,439 + +###### 示例 + +AT+TCPSENDS=0,10 +> +OK + ++TCPSENDS: 0,10 + +在sokcet 0 上发送10 字节的数据(如:1234567890 或 +31323334353637383930),发送成功 + +AT+TCPSENDS=0,536 +> ++TCPSENDS: Buffer not enough,439 + +在sokcet 0 上发送536 字节的数据(如:1234567890…),内部buffer +不足,发送失败 + +``` +AT+TCPSENDS=0 +``` + +> + +OK + ++TCPSENDS: 0,21 + +在链路 0 上发送 21 字节的数据(如:012345678901234567890 或 + +303132333435363738393031323334353637383930),发送成功 +(不带数据长度时以Ctrl+Z 为结束标志,最长不能超过2000) + +AT+TCPSENDS=0,1024 +> ++TCPSENDS: ERROR + +服务器模块发送TCP 数据塞包 + +AT+TCPSENDS=0,10 ++TCPSENDS: 0 is not link +服务器模块未建立链路0 的连接 + +``` +AT+TCPSENDS=0,5 +``` + +> ++TCPSENDS: 0,OPERATION EXPIRED + +输入发送命令出现“>”后,不输入数据,30s 后提示超时 + +###### A.1.14 AT+CLIENTSTATUS–查询主站链路的状态 + +查询主站链路的状态 + +功能模组TCP 应用指南 + +A 关键AT 命令 + +深圳市有方科技股份有限公司版权所有 +61 + +###### 命令格式 + +类型 +命令 +响应格式 + +执行 +AT+CLIENTSTATUS= + ++CLIENTSTATUS: ,, +, +Or +ERROR + +###### 响应时间 + +最大响应时间300ms。 + +###### 参数 + + +侦听到的AcceptSocket 值,即主站跟模块的建立的socket ,参考 + +``` +AT+TCPLISTEN 指令的说明 +``` + + +该链路的状态,取值为CONNECT 或者DISCONNECT + + +链路类型,取值为TCP 或者INVALID + +INVALID 表示该链路不是侦听到的TCP 连接,可能为TCP/UDP 客户端或者 + +服务器侦听链路。 + + +模块内部可用的send buffer 的大小,十进制ASCII 码表示,单位为字节 + +###### 示例 + +AT+CLIENTSTATUS=0 ++CLIENTSTATUS: 0,CONNECT,TCP,61440 + +主站socket 0,已建立TCP 连接,可用buffer 为 +61440 字节 +AT+CLIENTSTATUS=4 ++CLIENTSTATUS: 4,DISCONNECT + +Socket 4,没有建立连接 + +AT+CLIENTSTATUS=1 ++CLIENTSTATUS: 1,CONNECT,INVALID + +Socket 1 作为服务器侦听,链路类型返回INVALID + +###### A.1.15 AT+TCPACKS–查询TCP 服务器发送数据状态 + +查询 TCP 服务器链路发送成功的数据大小、接收方成功接收该链路的数据大小。 + +###### 命令格式 + +类型 +命令 +响应格式 + +执行 +AT+TCPACKS= +TCPACKS: + +,, + +功能模组TCP 应用指南 + +A 关键AT 命令 + +深圳市有方科技股份有限公司版权所有 +62 + +Or ++TCPACKS: ,DISCONNECT + +###### 响应时间 + +最大响应时间300ms。 + +###### 参数 + + +socket 链路编号,只能为0~5 + + +该链路发送成功的链路数据大小,无符号64 位整型数,十进制ASCII 码表示,单位 + +为字节。。 + + +接收方成功接收的链路数据大小,无符号64 位整型数,十进制ASCII 码表示,单位 + +为字节。 + +###### 示例 + +AT+TCPACKS=0 ++TCPACKS: 0,20,20 + +链路0,发送成功20 个字节数据,接收方成功接收20 个字节数据 + +AT+TCPACKS=0 ++TCPACKS:0,20,20 + +链路0,发送成功128 个字节数据,接收方成功接收120 个字节数据 + +AT+TCPACKS=1 ++TCPACKS:1,DISCONNECT + +链路1,未建立任何连接 + +AT+TCPACK=6 +ERROR + +指令链路编号错误 + +###### A.1.16 AT+TCPTRANS–建立TCP 透传链接 + +建立TCP 透传链接的命令。 + +建立TCP 透传链接成功后,向服务器发送数据,串口不显示发送的数据; + +使用“+++”指令(不带回车换行)切换到命令模式;“ATO”指令切换到数据模式; + +建议透传方式一次最多收发4096 字节数据。 + +###### 命令格式 + +类型 +命令 +响应格式 + +执行 +AT+TCPTRANS=, +[,] + +[OK] ++TCPTRANS: +Or + +功能模组TCP 应用指南 + +A 关键AT 命令 + +深圳市有方科技股份有限公司版权所有 +63 + +ERROR + +###### 响应时间 + +最大响应时间60s。 + +###### 参数 + + +目的IP 地址,必须是形如xx.xx.xx.xx 的输入,或者形如www.china.com(域名) + + +目的端口号,必须是十进制的ASCII 码 + + +OK +FAIL +ERROR + +###### 示例 + +``` +AT+TCPTRANS=220.199.66.56,6800 +``` + +OK + ++TCPTRANS:OK + +建立TCP 透传链接,成功 + +AT+TCPTRANS=neowayjsr.oicp.net,60010 +OK + ++TCPTRANS: OK + +用域名建立TCP 透传链接,成功 + +AT+TCPTRANS=220.199.66.56, ++TCPTRANS: ERROR +AT 指令格式错误 + +``` +AT+TCPTRANS=220.199.66.56,6800 +``` + +OK + ++TCPTRANS: FAIL + +建立TCP 透传链接,失败 + +AT+TCPTRANS=220.199.66.56,6800 +ERROR + +若已建立透传(TCP、UDP、TCP 服务器)链接,再发送该 +指令会返回:ERROR + +###### A.1.17 AT+TRANSCLOSE–关闭透传方式链接 + +关闭透明传输方式链接。 + +###### 命令格式 + +类型 +命令 +响应格式 + +执行 +AT+TRANSCLOSE + ++TRANSCLOSE: ,OK +Or +ERROR + +功能模组TCP 应用指南 + +A 关键AT 命令 + +深圳市有方科技股份有限公司版权所有 +64 + +主动上报 ++TCPTRANS: Link Closed ++UDPTRANS: Link Closed + +###### 响应时间 + +最大响应时间300ms。 + +###### 参数 + +无。 + +###### 示例 + +AT+TRANSCLOSE ++TRANSCLOSE: 0,OK +主动关闭TCP 透传方式链接,成功 + +AT+TRANSCLOSE +ERROR +未建立TCP/UDP 透传方式链接,失败 + +``` +AT+TRANSCLOSE +``` + ++TRANSCLOSE: 1,OK +主动关闭UDP 透传方式链接,成功 + ++TCPTRANS: Link Closed +被动关闭TCP 透传方式链接 ++UDPTRANS: Link Closed +被动关闭UDP 透传方式链接 + +###### A.1.18 ATD*99#–GPRS + +使用外部协议栈,进行GPRS 拨号连接 + +进行拨号之前一定要确保CREG 已经注册成功,并且设置了APN。 + +###### 命令格式 + +类型 +命令 +响应格式 + +执行 +ATD*99# +CONNECT + +###### 参数 + +无 + +###### 示例 + +ATD*99# +开始拨号连接 + +功能模组TCP 应用指南 + +A 关键AT 命令 + +深圳市有方科技股份有限公司版权所有 +65 + +CONNECT +拨号成功的返回值 + +##### A.2 国网AT 指令 + +###### A.2.1 AT$MYNETCON–设置网络连接初始化参数 + +该命令实现网络连接前的各种参数设置,是使用TCPIP 功能前必须完成的一步操作。 + +###### 命令格式 + +类型 +命令 +响应格式 + +设置 +AT$MYNETCON=, +, + +OK +ERROR: + +查询 +AT$MYNETCON? + +$MYNETCON: ,, + +OK + +###### 参数 + + +通道号,0-5。 + +, + +与对应的取值如下: + +USERPWD:用户名和密码,格式为"user,passwd" + +APN:字符串类型 + +CFGT:每包发送时等待的时间,1-65535,单位ms,默认值100,用于透明数据传 + +输模式 + +CFGP:数据包被传送的门限值,1-1460,默认值为1024,用于透明数据传输模式 + +AUTH:鉴权类型,默认为1 + +0:NONE + +1:PAP + +2:CHAP + +3:PAP or CHAP + +###### 示例 + +AT$MYNETCON=0,"USERPWD","user,pwd" +OK + +AT$MYNETCON? + +$MYNETCON: 0,"USERPWD","" +$MYNETCON: 0,"APN","" + +功能模组TCP 应用指南 + +A 关键AT 命令 + +深圳市有方科技股份有限公司版权所有 +66 + +$MYNETCON: 0,"CFGT",1000 +$MYNETCON: 0,"CFGP",1024 +$MYNETCON: 0,"AUTH",0 +OK + +###### A.2.2 AT$MYNETACT–激活/去激活网络连接 + +实现GPRS 网络的连接和断开,是使用TCP/IP 功能前必须完成的一步操作。 + +命令执行成功,获取到IP 会主动上报本地IP。 + +###### 命令格式 + +类型 +命令 +响应格式 + +执行 +AT$MYNETACT=, + +OK +Or +ERROR: + +查询 +AT$MYNETACT? +$MYNETACT: ,, +OK + +测试 +AT$MYNETACT=? +$MYNETACT: , +OK + +###### 参数 + + +0:去激活网络连接 + +1:激活网络连接 + + +通道号(0-5) + + +0:网络未激活 + +1:网络已激活 + + +IP 地址;在网络已激活的情况下,显示本机IP + +###### 示例 + +AT$MYNETACT=0,1 +OK +$MYURCACT: 0,1,"10.92.220.73" +AT$MYNETACT=0,1 +OK +$MYURCACT: 0,1,"10.67.163.14" + +$MYURCACT: + +0,1,"24e0:bf:d206:6ed:1:2:fd94:4982" + +激活网络连接 +执行成功 +主动上报获取的IP +双栈激活 + +AT$MYNETACT? +$MYNETACT: 0,1," 10.92.220.73" +OK + +查询本地IP + +功能模组TCP 应用指南 + +A 关键AT 命令 + +深圳市有方科技股份有限公司版权所有 +67 + +AT$MYNETACT=0,1 +OK +$MYURCACT: 0,1,"10.67.163.14" +AT$MYNETACT=0,1 +ERROR: 902 + +重复激活提示ERROR + +###### A.2.3 AT$MYNETSRV–设置非透明传输服务参数 + +设置TCP/IP 的服务参数。 + +###### 命令格式 + +类型 +命令 +响应格式 + +执行 + +AT$MYNETSRV=,< +SocketID>,,,[,] + +OK +Or +ERROR: + +查询 +AT$MYNETSRV? + +$MYNETSRV: ,, +,, +OK + +测试 +AT$MYNETSRV=? +OK + +###### 参数 + + +整数类型,0-5,通道号,每个通道2K Bytes 的发送缓冲区和10K Bytes 的接收缓 + +冲区 + + +整数类型,0-14,本命令使用的链接号 + + +网络类型 + +0:TCP Client + +1:TCP Server,本模式下,最多可建立4 个侦听 + +2:UDP + + +非透明数据传输模式下数据显示方式 + +0:HEX(默认) + +1:TEXT + + + + + +address 所对应的值,如:59.40.29.34:4988。address 中的IP 地址为127.0.0.1 + +的时候,表示使用该SocketID 建立侦听服务。 + +域名解析方式 + +0:IPv4(默认) + +1:IPv6 + + +错误代码 + +功能模组TCP 应用指南 + +A 关键AT 命令 + +深圳市有方科技股份有限公司版权所有 +68 + +###### 示例 + +AT$MYNETSRV=0,0,0,0,"59.40.29.34:4988" +OK + +通道0 设置TCP Client 服务参数 +(远端服务器IP 地址为59.40.29.34,端口号为4988) + +###### A.2.4 AT$MYNETOPEN–开启服务 + +打开TCP/UDP 连接。 + +###### 命令格式 + +类型 +命令 +响应格式 + +执行 +AT$MYNETOPEN= + +$MYNETOPEN: +[,] +OK +Or +ERROR: + +查询 +AT$MYNETOPEN? + +$MYNETOPEN:,,,,,,,, +OK + +测试 +AT$MYNETOPEN=? +OK + +###### 参数 + + +整数类型,0-14,本命令使用的链接号。 + + +整数类型,TCP 连接打开时候,client 和server 协商成功的MSS 值。 + + +字符类型,本地IP 地址。 + + +本地端口(每个通道可能不一样)。 + + +网关 + + +首选域名解析服务器 + + +备用域名解析服务器 + + +TCP Client/TCP Server/UDP + +0:TCP Client + +1:TCP Server + +2:UDP + + +需要连接的IP 地址 + + +需要连接的端口号 + +功能模组TCP 应用指南 + +A 关键AT 命令 + +深圳市有方科技股份有限公司版权所有 +69 + +###### 示例 + +AT$MYNETOPEN=0 +$MYNETOPEN: 0,2000 +OK + +打开0 号socket。 + +AT$MYNETOPEN? +$MYNETOPEN:0,10.92.220.73,5000,0,dns1,dns2,0,59.40.29.34,4988 + +OK + +OPEN 的socket 查询,查询到0 号 +socket 为打开状态。 + +AT$MYNETSRV=5,1,0,0,"58.60.184.213:10188" +OK +AT$MYNETOPEN=2 +ERROR: 913 + +打开失败。 + +###### A.2.5 AT$MYNETREAD–读取数据 + +用于读取接收到的数据。 + +###### 命令格式 + +类型 +命令 +响应格式 + +执行 +AT$MYNETREAD=, + + +$MYNETREAD: , + +OK + +测试 +AT$MYNETREAD=? +OK + +###### 参数 + + +整数类型,0-14,本命令使用的链接号。 + + +需要读取的最大数据长度,1-2048。 + + +实际读取到的数据长度,0-2048。 + + +数据,当data_length=0 时,无显示。 + +###### 示例 + +AT$MYNETREAD=0,2048 +$MYNETREAD:0,0 +OK + +从0 号socket 读取2048 个数据 +没有数据在缓冲区或数据已经全部读取完毕 + +AT$MYNETREAD=0,2048 +$MYNETREAD: 0,10 +1234567890 +OK + +从0 号socket 读取2048 个数据 +实际读到10 个数据 +数据内容为1234567890 + +###### A.2.6 AT$MYNETWRITE–发送数据 + +用于发送数据。 + +功能模组TCP 应用指南 + +A 关键AT 命令 + +深圳市有方科技股份有限公司版权所有 +70 + +UDP 链路,建议客户每次发送数据不大于1472 字节,可降低丢包概率。 + +###### 命令格式 + +类型 +命令 +响应格式 + +执行 +AT$MYNETWRITE=, + + +$MYNETWRITE: , + +OK +Or +ERROR: + +测试 +AT$MYNETWRITE=? +OK + +###### 参数 + + +整数类型,0-14,本命令使用的链接号。 + + +要发送的数据长度,1-2048。 + + +本次发送的数据内容。 + + +错误代码。 + +###### 示例 + +AT$MYNETWRITE=0,10 +$MYNETWRITE: 0,10 +1234567890 + +OK + +0 号socket 发送10 个数据 +表示可以输入要发送数据 +写入要发送数据1234567890(输入的数据不会回显在串口上) + +命令执行成功 + +###### A.2.7 AT$MYNETCLOSE–关闭连接 + +用于关闭一条socket 连接。 + +###### 命令格式 + +类型 +命令 +响应格式 + +执行 +AT$MYNETCLOSE= + +$MYNETCLOSE: +OK +ERROR: + +查询 +AT$MYNETCLOSE? +OK + +测试 +AT$MYNETCLOSE=? +OK + +功能模组TCP 应用指南 + +A 关键AT 命令 + +深圳市有方科技股份有限公司版权所有 +71 + +###### 参数 + + +整数类型,0-14,需要断开的socket 链接号。 + +###### 示例 + +AT$MYNETCLOSE=0 +$MYNETCLOSE: 0 +OK + +关闭0 号socket。 + +###### A.2.8 AT$MYNETACK–查询TCPACK 信息 + +用于查询终端已发送但未被对端确认的TCP 数据数量,模块内置协议栈剩余缓冲区的大小。 + +###### 命令格式 + +类型 +命令 +响应格式 + +执行 +AT$MYNETACK= + +$MYNETACK:, +, +OK + +###### 参数 + + +整数类型,0-14,需要查询的链接号。 + + +数据发送但未收到对方确认信息的数量,最大值为2^32-1。 + + +模块内置协议栈剩余缓冲区的大小,0-10240。 + +###### 示例 + +AT$MYNETACK=0 +$MYNETACK: 0,0,10240 +OK + +查询0 号socket 发送数据的ACK +发送了的未被对端确认ACK 的数量为0 +模块内置协议栈剩余缓冲区为10240 + +###### A.2.9 AT$MYNETACCEPT–接受侦听请求 + +该命令是用于当通信模块工作在TCP 服务器模式下,接收客户端的侦听请求。 + +在透明数据传输模式下,如果要关闭Server 的连接,首先要+++退出数据模式,进入命令模式,然 + +后运行AT$MYNETCLOSE=SocketID,这样将一起关闭Server 侦听的SocketID 和这个SocketID 下建 + +立的其他链接。 + +功能模组TCP 应用指南 + +A 关键AT 命令 + +深圳市有方科技股份有限公司版权所有 +72 + +###### 命令格式 + +类型 +命令 +响应格式 + +执行 +AT$MYNETACCEPT=, +, + +=0: + +OK +=1: + +CONNECT +Or +ERROR: + +查询 +AT$MYNETACCEPT? +OK + +测试 +AT$MYNETACCEPT=? +OK + +###### 参数 + + +0-14,新接入的socket id,由主动上报$MYURCCLIENT 提供。 + + +是否接受侦听。 + +0:接受 + +1:拒绝 + + +侦听成功后模块的传输模式。 + +0:非透明数据传输模式 + +1:透明数据传输模式 + +###### 示例 + +AT$MYNETSRV=0,0,1,0,"127.0.0.1:5000" +OK +AT$MYNETOPEN=0 +$MYNETOPEN: 0 +OK + +$MYURCCLIENT: 1,"10.90.72.235",51614 + +AT$MYNETACCEPT=1,0,0 +OK + +设置0 号socket 侦听本地端口5000。 + +打开0 号socket 开启服务器模式。 + + + +客户端1 连接上报 + +接受客户端1 连接 + +$MYURCCLIENT: 2,"10.90.72.235",55469 +AT$MYNETACCEPT=2,1,0 +OK + +客户端2 连接上报 +拒绝客户端2 连接 + +###### A.2.10 AT$MYNETCREATE–开启透明传输服务 + +开启通信模块透明数据传输服务。 + +TCP/UDP Client 模式下,执行本条命令后通信模块开始连接服务器,连接服务器成功后,通信模块 + +进入透明数据传输模式。 + +功能模组TCP 应用指南 + +A 关键AT 命令 + +深圳市有方科技股份有限公司版权所有 +73 + +TCPServer 模式下,执行本条命令,模块返回OK,仅代表通信模块建立侦听socket 成功,但模块 + +不立即进入数据模式,仍旧在命令模式;只有在客户端连接成功以后,通信模块才进入透明数据传输模 + +式。 + +透明数据传输模式的挂断,必须首先使用“+++”切入命令模式后,方可执行AT$MYNETCLOSE + +命令。+++要以字符串的形式输入才能退出透传模式,单个+输入,倘若+与+之间相差50ms,+会当做 + +数据发送出去。 + +###### 命令格式 + +类型 +命令 +响应格式 + +执行 + +AT$MYNETCREATE=,,< +SocketID>,,[,][,] + +=1: + +OK + +=0 或2: + +CONNECT +Or +ERROR: + +查询 +AT$MYNETCREATE? +OK + +测试 +AT$MYNETCREATE=? +OK + +###### 参数 + + +通道号0-5 + + +TCP Client/TCP Server/UDP,0-2 + +0:TCPClient + +1:TCPServer + +2:UDP + + +本次的链接号,整数类型,0-14 + + +需要连接的IP + + +客户端模式则为需要连接的端口,服务器模式则为侦听端口 + + + + +可缺省 + +域名解析方式,可缺省 + +0:IPv4(默认) + +1:IPv6 + +###### 示例 + +AT$MYNETCREATE=0,0,0,"59.40.29.34",4988,3000 +CONNECT + +建立TCP 透明数据传输通道 +目的IP 和PORT:"59.40.29.34",4988 + +功能模组TCP 应用指南 + +A 关键AT 命令 + +深圳市有方科技股份有限公司版权所有 +74 + +OK + +AT$MYNETCREATE=0,0,0,"2400:da00:2::29",4988,3000 +CONNECT + +OK + +CONNECT 表示可以开始透明传输 +输入+++ +模块返回OK 表示回到命令模式 + +建立TCPIPV6 透明数据传输通道 + +###### A.2.11 $MYURCREAD–数据到来主动上报 + +该主动上报命令表达模块接收到了数据。 + +每收到一包数据,上报一次。 + +###### 命令格式 + +类型 +命令 + +主动上报 +$MYURCREAD: + +###### 参数 + + +整数类型,0-14,本次上报的链接号。 + +###### 示例 + +AT$MYNETSRV=0,0,0,0,"59.40.29.34:4988" + +OK +AT$MYNETOPEN=0 + +$MYNETOPEN: 0,2000 +OK + +$MYURCREAD: 0 +AT$MYNETREAD=0,2048 +$MYNETREAD: 0,10 +0123456789 + +OK + +0 号socket 设置为TCP 客户端 + +打开0 号socket 进行TCP 链接 + +0 号socket 数据到来主动上报 +按最大长度2048 读取 +0 号socket 读取到10 个字节 +数据内容为0123456789 + +###### A.2.12 $MYURCCLOSE–链路断开主动上报 + +该主动上报命令表达当前SocketID 对应的链接已断开 + +上报了本命令后,SocketID 对应的缓冲区内的收发数据会被清空。 + +功能模组TCP 应用指南 + +A 关键AT 命令 + +深圳市有方科技股份有限公司版权所有 +75 + +###### 命令格式 + +类型 +命令 + +主动上报 +$MYURCCLOSE: + +###### 参数 + + +整数类型,0-14,本次上报的链接号。 + +###### 示例 + +AT$MYNETSRV=0,0,0,0,"59.40.29.34:4988" +OK +AT$MYNETOPEN=0 +$MYNETOPEN: 0,2000 +OK + +$MYURCCLOSE: 0 + +0 号socket 设置为TCP 客户端 + +打开0 号socket 进行TCP 链接 + +主动上报:0 号socket 断开 + +###### A.2.13 $MYURCACT–网络连接状态主动上报 + +该主动上报命令表达网络连接建立或者断开 + +###### 命令格式 + +类型 +命令 + +主动上报 +$MYURCACT: ,, + +###### 参数 + + +使用的Channel 通道 + + +网络连接状态 + +0:网络连接断开 + +1:网络连接建立 + + +本机IP 地址 + +###### 示例 + +AT$MYNETACT=0,1 +OK + +激活开网络连接 +网络连接状态上报 + +功能模组TCP 应用指南 + +A 关键AT 命令 + +深圳市有方科技股份有限公司版权所有 +76 + +$MYURCACT: 0,1,"10.91.102.62" +网络激活成功,获取的本地IP 为"10.91.102.62" +AT$MYNETACT=0,1 +OK +$MYURCACT: 0,1,"10.67.163.14" + +激活开网络连接 +网络连接状态上报 +网络激活成功,获取的本地IP 为"10.91.102.62" + +###### A.2.14 $MYURCCLIENT–客户端连接主动上报 + +当开启侦听服务后,客户端连接上报。 + +该上报表示有客户端申请连接。 + +###### 命令格式 + +类型 +命令 + +主动上报 +$MYURCCLIENT: ,, + +###### 参数 + + +新Client socket id + + +客户端IP 地址 + + +客户端端口号 + +###### 示例 + +AT$MYNETSRV=0,0,1,0,"127.0.0.1:5000" + +OK + +AT$MYNETOPEN=0 +$MYNETOPEN: 0 +OK + +$MYURCCLIENT: 1,"10.90.72.235",51614 +AT$MYNETACCEPT=1,0,0 +OK + +0 号socket 设置为TCP server,侦听端口为5000 + +打开0 号socket 开始侦听 + +客户端连接主动上报 diff --git "a/code/sdk/zhihui/ppp/doc/Neoway_\345\212\237\350\203\275\346\250\241\345\235\227TCP\345\272\224\347\224\250\346\214\207\345\215\227_V1.5.pdf" "b/code/sdk/zhihui/ppp/doc/Neoway_\345\212\237\350\203\275\346\250\241\345\235\227TCP\345\272\224\347\224\250\346\214\207\345\215\227_V1.5.pdf" new file mode 100644 index 000000000..ef0683a0a Binary files /dev/null and "b/code/sdk/zhihui/ppp/doc/Neoway_\345\212\237\350\203\275\346\250\241\345\235\227TCP\345\272\224\347\224\250\346\214\207\345\215\227_V1.5.pdf" differ diff --git a/code/sdk/zhihui/ppp/include/ppp_config.h b/code/sdk/zhihui/ppp/include/ppp_config.h index 2f9836193..4f8891df2 100644 --- a/code/sdk/zhihui/ppp/include/ppp_config.h +++ b/code/sdk/zhihui/ppp/include/ppp_config.h @@ -83,10 +83,9 @@ extern "C" { /* 调试配置 */ #define PPP_DEBUG_ENABLE 1 // 启用调试输出 -// #define PPP_DEBUG_LEVEL PPP_DEBUG_LEVEL_DEBUG // 默认只显示警告和错误 (1=ERROR, 2=WARN, 3=INFO, 4=DEBUG) +#define PPP_DEBUG_LEVEL PPP_DEBUG_LEVEL_DEBUG // 默认只显示警告和错误 (1=ERROR, 2=WARN, 3=INFO, 4=DEBUG) // #define PPP_DEBUG_LEVEL PPP_DEBUG_LEVEL_INFO // 默认只显示警告和错误 (1=ERROR, 2=WARN, 3=INFO, 4=DEBUG) -#define PPP_DEBUG_LEVEL PPP_DEBUG_LEVEL_WARN // 默认只显示警告和错误 (1=ERROR, 2=WARN, 3=INFO, 4=DEBUG) - +// #define PPP_DEBUG_LEVEL PPP_DEBUG_LEVEL_WARN // 默认只显示警告和错误 (1=ERROR, 2=WARN, 3=INFO, 4=DEBUG) /* * 调试级别说明: * PPP_DEBUG_LEVEL_ERROR (1) - 只显示错误信息 diff --git a/code/sdk/zhihui/ppp/src/ppp_api.c b/code/sdk/zhihui/ppp/src/ppp_api.c index 4c14cceff..709679c4b 100644 --- a/code/sdk/zhihui/ppp/src/ppp_api.c +++ b/code/sdk/zhihui/ppp/src/ppp_api.c @@ -2957,15 +2957,17 @@ static errcode_t ppp_parse_cclk_and_convert_to_timestamp(const char *response, * @param timezone_quarters 时区(quarters,15分钟为单位) * @return ERRCODE_SUCC-成功,其他-失败 * - * 时间设置逻辑说明: + * 时间设置逻辑说明(参照 cat1_porting.c 的 sync_time_utc 实现): * - 输入:UTC时间戳 + 时区偏移 - * - 设置:本地时间 = UTC时间 + 时区偏移 + * - 设置:tv.tv_sec 设置为本地时间戳(UTC + 时区偏移),tz_minuteswest 设置时区偏移 + * - 系统时间直接设置为本地时间,tz_minuteswest 用于时区信息记录 * * 示例(UTC+8): - * - UTC时间:02:59:43 + * - UTC时间:15:06:28 * - 时区:+32 quarters = +8 hours - * - 本地时间:02:59:43 + 8小时 = 10:59:43 - * - settimeofday 会将系统时间设置为 10:59:43 + * - tv.tv_sec:本地时间戳(UTC时间戳 + 8小时 = 对应23:06:28) + * - tz_minuteswest:+480分钟 + * - 系统显示:23:06:28 */ static errcode_t ppp_set_system_time(uint32_t utc_timestamp, int timezone_quarters) { @@ -2981,12 +2983,12 @@ static errcode_t ppp_set_system_time(uint32_t utc_timestamp, int timezone_quarte return ERRCODE_FAIL; } - // 准备时间结构(参照 cat1_porting.c 的实现) + // 准备时间结构(参照 cat1_porting.c 的 sync_time_utc 实现) struct timeval tv = {0}; struct timezone tzone = {0}; - // 设置本地时间 = UTC时间 + 时区偏移 - // settimeofday 的 tv_sec 应该设置为本地时间戳 + // settimeofday 的 tv_sec 设置为本地时间戳(UTC + 时区偏移) + // 这样系统时间就是本地时间,tz_minuteswest 用于时区信息记录 tv.tv_sec = utc_timestamp + timezone_quarters * MINUTES_PER_QUARTER * 60; tv.tv_usec = 0; @@ -2996,9 +2998,9 @@ static errcode_t ppp_set_system_time(uint32_t utc_timestamp, int timezone_quarte PPP_DEBUG_INFO("Setting system time:"); PPP_DEBUG_INFO(" UTC timestamp: %u", utc_timestamp); + PPP_DEBUG_INFO(" Local timestamp: %ld (UTC + %d hours)", tv.tv_sec, timezone_quarters / 4); PPP_DEBUG_INFO(" Timezone: %+d minutes (%+d quarters)", tzone.tz_minuteswest, timezone_quarters); - PPP_DEBUG_INFO(" Local timestamp: %ld", tv.tv_sec); // 调用系统API设置时间 int ret = settimeofday(&tv, &tzone); diff --git a/code/sdk/zhihui/ppp/src/ppp_at_handler.c b/code/sdk/zhihui/ppp/src/ppp_at_handler.c index cb663ade1..147398b60 100644 --- a/code/sdk/zhihui/ppp/src/ppp_at_handler.c +++ b/code/sdk/zhihui/ppp/src/ppp_at_handler.c @@ -932,21 +932,21 @@ static bool ppp_at_parse_response(const char *response, const char *expected) return false; } - - // 检查期望响应 ,优先检测期望响应 - if (expected && strstr(response, expected)) { - return true; + // 优先检查ABORT条件(ERROR、BUSY、NO CARRIER),即使响应中包含期望的字符串,如果有ERROR也应该返回失败 + if (strstr(response, "ERROR") != NULL) { + PPP_DEBUG_ERROR("AT command response contains ERROR: %s", response); + return false; } - - // 检查ABORT条件 (对应chat文件中的ABORT指令) - if (strstr(response, "BUSY") || - strstr(response, "ERROR") || - strstr(response, "NO CARRIER")) { + + if (strstr(response, "BUSY") != NULL || strstr(response, "NO CARRIER") != NULL) { PPP_DEBUG_ERROR("AT command aborted: %s", response); return false; } - + // 检查期望响应 + if (expected && strstr(response, expected)) { + return true; + } return false; } diff --git a/code/sdk/zhihui/utils/custom_utils.c b/code/sdk/zhihui/utils/custom_utils.c index b567a58dc..19e738685 100644 --- a/code/sdk/zhihui/utils/custom_utils.c +++ b/code/sdk/zhihui/utils/custom_utils.c @@ -26,6 +26,7 @@ #include "tcxo.h" #include #include "pm_veto.h" +#include "pm.h" #include // #include "config/include/ConfigModelC.h" #include "upg_porting.h" @@ -4607,6 +4608,28 @@ void uapi_cat1_add_cat1_vote(void) #endif } +void uapi_net_app_remove_net_app_vote(void) +{ + uint8_t cnt = pm_veto_get_id_cnt(PM_ID_NET_APP); + if (cnt == 1) { + uapi_pm_remove_sleep_veto(PM_ID_NET_APP); + } else { + // WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP,"NET_APP has delete pm_id_net_app vote\r\n"); + } + // WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP,"\r\nNET_APP SLEEP SUCCESS\r\n"); +} + +void uapi_net_app_add_net_app_vote(void) +{ + uint8_t cnt = pm_veto_get_id_cnt(PM_ID_NET_APP); + if (cnt == 0) { + uapi_pm_add_sleep_veto(PM_ID_NET_APP); + } else { + // WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP,"NET_APP has got pm_id_net_app vote\r\n"); + } + // WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP,"\r\nNET_APP WAKEUP SUCCESS\r\n"); +} + void uapi_get_lcd_id(uint32_t *id) { lcd_drv_common *lcd_drv_comm_api = NULL; diff --git a/code/sdk/zhihui/utils/custom_utils.h b/code/sdk/zhihui/utils/custom_utils.h index 1fe7b6115..6fa7f4784 100644 --- a/code/sdk/zhihui/utils/custom_utils.h +++ b/code/sdk/zhihui/utils/custom_utils.h @@ -470,6 +470,9 @@ uint8_t calculate_lrc(uint8_t *data,uint16_t len,uint8_t _lrcValue); void uapi_cat1_remove_cat1_vote(void); void uapi_cat1_add_cat1_vote(void); +void uapi_net_app_remove_net_app_vote(void); +void uapi_net_app_add_net_app_vote(void); + void uapi_get_lcd_id(uint32_t *id); void uapi_lcd_backlight_on(void); diff --git a/code/sdk/zhihui/websocket/websockets_py/src/websocket_console_server.py b/code/sdk/zhihui/websocket/websockets_py/src/websocket_console_server.py new file mode 100644 index 000000000..2fb78dc53 --- /dev/null +++ b/code/sdk/zhihui/websocket/websockets_py/src/websocket_console_server.py @@ -0,0 +1,446 @@ +#!/usr/bin/env python3 +""" +WebSocket控制台服务器 +支持: +- 正常接收和回复 ping/pong 指令(自动处理) +- 打印收到的所有数据 +- 通过控制台发送消息给指定的连接的客户端 +""" + +import asyncio +import logging +import argparse +import time +import sys +from typing import Dict, Set, Optional +import json + +# 导入 websockets +try: + import websockets + from websockets.asyncio.server import serve + from websockets.exceptions import ConnectionClosedOK, ConnectionClosedError +except ImportError as e: + print(f"错误: 无法导入 websockets 模块: {e}") + print("请确保已安装 websockets: pip install websockets") + sys.exit(1) + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + + +class WebSocketConsoleServer: + def __init__(self, host: str, port: int): + self.host = host + self.port = port + self.clients: Dict[int, websockets.WebSocketServerProtocol] = {} # client_id -> websocket + self.client_info: Dict[int, dict] = {} # client_id -> info dict + self.message_queue = asyncio.Queue() # 用于控制台输入的消息队列 + self.running = True + + def get_client_id(self, websocket) -> int: + """获取客户端ID""" + return id(websocket) + + def get_client_address(self, websocket) -> tuple: + """安全获取客户端地址""" + remote_address = getattr(websocket, 'remote_address', None) + if isinstance(remote_address, tuple) and len(remote_address) >= 2: + return remote_address[0], remote_address[1] + return 'unknown', -1 + + async def handle_client(self, websocket): + """处理客户端连接""" + # 从 websocket 对象获取 path(兼容不同版本的 websockets 库) + path = getattr(websocket, 'path', '') + + client_id = self.get_client_id(websocket) + client_ip, client_port = self.get_client_address(websocket) + + # 保存客户端信息 + self.clients[client_id] = websocket + self.client_info[client_id] = { + 'id': client_id, + 'ip': client_ip, + 'port': client_port, + 'path': path, + 'connect_time': time.time(), + 'messages_received': 0, + 'messages_sent': 0 + } + + logger.info(f"[客户端连接] ID={client_id}, 地址={client_ip}:{client_port}, 路径={path}") + logger.info(f"[客户端列表] 当前连接数: {len(self.clients)}") + + # 发送欢迎消息 + try: + welcome_msg = json.dumps({ + "type": "welcome", + "message": "连接成功", + "client_id": client_id, + "server_time": time.strftime("%Y-%m-%d %H:%M:%S") + }, ensure_ascii=False) + await websocket.send(welcome_msg) + self.client_info[client_id]['messages_sent'] += 1 + logger.info(f"[发送] 客户端 {client_id}: 欢迎消息") + except Exception as e: + logger.error(f"发送欢迎消息失败: {e}") + + try: + # 处理消息循环 + async for message in websocket: + await self.process_message(websocket, message, client_id) + + except ConnectionClosedOK: + logger.info(f"[客户端断开] ID={client_id} 正常关闭") + except ConnectionClosedError as e: + logger.warning(f"[客户端断开] ID={client_id} 异常关闭: {e}") + except (asyncio.CancelledError, AssertionError) as e: + # keepalive 相关的取消和断言错误(连接可能已断开) + logger.info(f"[客户端断开] ID={client_id} 任务被取消或 keepalive 超时: {type(e).__name__}") + except Exception as e: + # 记录其他异常,但不中断清理流程 + logger.error(f"[错误] 处理客户端 {client_id} 消息时出错: {type(e).__name__}: {e}") + finally: + # 清理客户端 + if client_id in self.clients: + del self.clients[client_id] + if client_id in self.client_info: + info = self.client_info[client_id] + duration = time.time() - info['connect_time'] + logger.info(f"[客户端统计] ID={client_id}, 连接时长={duration:.2f}秒, " + f"接收={info['messages_received']}, 发送={info['messages_sent']}") + del self.client_info[client_id] + logger.info(f"[客户端列表] 当前连接数: {len(self.clients)}") + + async def process_message(self, websocket, message, client_id: int): + """处理接收到的消息""" + self.client_info[client_id]['messages_received'] += 1 + + # 打印收到的所有数据 + if isinstance(message, bytes): + # 二进制消息 + hex_str = message.hex() + ascii_str = ''.join(chr(b) if 32 <= b < 127 else '.' for b in message[:64]) + logger.info(f"[接收-二进制] 客户端 {client_id}: {len(message)} bytes") + logger.info(f" HEX: {hex_str[:128]}{'...' if len(hex_str) > 128 else ''}") + logger.info(f" ASCII预览: {ascii_str}") + + # 尝试解析为文本(如果是UTF-8编码) + try: + text = message.decode('utf-8') + logger.info(f" UTF-8文本: {text[:200]}{'...' if len(text) > 200 else ''}") + except UnicodeDecodeError: + pass + else: + # 文本消息 + logger.info(f"[接收-文本] 客户端 {client_id}: {message}") + + # 尝试解析为JSON + try: + data = json.loads(message) + logger.info(f" JSON解析: {json.dumps(data, ensure_ascii=False, indent=2)}") + except (json.JSONDecodeError, TypeError): + pass + + async def console_input_handler(self): + """处理控制台输入""" + loop = asyncio.get_event_loop() + + print("\n" + "="*60) + print("WebSocket 控制台服务器") + print("="*60) + print("可用命令:") + print(" list - 列出所有连接的客户端") + print(" send - 向指定客户端发送文本消息") + print(" sendall - 向所有客户端发送文本消息") + print(" close - 关闭指定客户端连接") + print(" stats - 显示统计信息") + print(" help - 显示帮助信息") + print(" quit/exit - 退出服务器") + print("="*60 + "\n") + + while self.running: + try: + # 使用 asyncio 读取 stdin + line = await loop.run_in_executor(None, sys.stdin.readline) + if not line: + break + + line = line.strip() + if not line: + continue + + # 处理命令 + await self.handle_console_command(line) + + except EOFError: + break + except Exception as e: + logger.error(f"处理控制台输入时出错: {e}") + + async def handle_console_command(self, command: str): + """处理控制台命令""" + parts = command.split(None, 2) + cmd = parts[0].lower() if parts else "" + + if cmd == "list": + await self.cmd_list_clients() + elif cmd == "send" and len(parts) >= 3: + client_id = int(parts[1]) + message = parts[2] + await self.cmd_send_to_client(client_id, message) + elif cmd == "sendall" and len(parts) >= 2: + message = parts[1] + await self.cmd_send_to_all(message) + elif cmd == "close" and len(parts) >= 2: + client_id = int(parts[1]) + await self.cmd_close_client(client_id) + elif cmd == "stats": + await self.cmd_show_stats() + elif cmd in ["help", "h", "?"]: + self.print_help() + elif cmd in ["quit", "exit", "q"]: + logger.info("收到退出命令,正在关闭服务器...") + self.running = False + # 关闭所有连接 + for client_id, websocket in list(self.clients.items()): + try: + await websocket.close() + except Exception as e: + logger.error(f"关闭客户端 {client_id} 时出错: {e}") + else: + logger.warning(f"未知命令: {command}") + logger.info("输入 'help' 查看可用命令") + + async def cmd_list_clients(self): + """列出所有客户端""" + if not self.clients: + logger.info("[客户端列表] 当前没有连接的客户端") + return + + logger.info(f"[客户端列表] 当前连接数: {len(self.clients)}") + for client_id, info in self.client_info.items(): + duration = time.time() - info['connect_time'] + logger.info(f" ID={client_id}, 地址={info['ip']}:{info['port']}, " + f"路径={info['path']}, 连接时长={duration:.2f}秒, " + f"接收={info['messages_received']}, 发送={info['messages_sent']}") + + async def cmd_send_to_client(self, client_id: int, message: str): + """向指定客户端发送消息""" + if client_id not in self.clients: + logger.error(f"[发送失败] 客户端 {client_id} 不存在") + await self.cmd_list_clients() + return + + websocket = self.clients[client_id] + + # 检查连接状态(兼容不同版本的 websockets 库) + try: + if getattr(websocket, 'closed', False): + logger.error(f"[发送失败] 客户端 {client_id} 连接已关闭") + if client_id in self.clients: + del self.clients[client_id] + if client_id in self.client_info: + del self.client_info[client_id] + return + except Exception: + pass # 如果属性不存在,继续尝试发送 + + try: + await websocket.send(message) + self.client_info[client_id]['messages_sent'] += 1 + logger.info(f"[发送成功] 客户端 {client_id}: {message}") + except (ConnectionClosedOK, ConnectionClosedError) as e: + logger.warning(f"[发送失败] 客户端 {client_id} 连接已关闭: {e}") + # 清理客户端 + if client_id in self.clients: + del self.clients[client_id] + if client_id in self.client_info: + del self.client_info[client_id] + except (asyncio.CancelledError, AssertionError) as e: + # 忽略 keepalive 相关的取消和断言错误(连接可能已断开) + logger.warning(f"[发送失败] 客户端 {client_id} keepalive 错误(连接可能已断开): {type(e).__name__}") + # 清理客户端 + if client_id in self.clients: + del self.clients[client_id] + if client_id in self.client_info: + del self.client_info[client_id] + except Exception as e: + logger.error(f"[发送失败] 客户端 {client_id}: {type(e).__name__}: {e}") + # 如果是连接相关错误,清理客户端 + if "connection" in str(e).lower() or "closed" in str(e).lower(): + if client_id in self.clients: + del self.clients[client_id] + if client_id in self.client_info: + del self.client_info[client_id] + + async def cmd_send_to_all(self, message: str): + """向所有客户端发送消息""" + if not self.clients: + logger.warning("[发送失败] 当前没有连接的客户端") + return + + success_count = 0 + failed_count = 0 + clients_to_remove = [] + + for client_id, websocket in list(self.clients.items()): + # 检查连接状态(兼容不同版本的 websockets 库) + try: + if getattr(websocket, 'closed', False): + logger.warning(f"[发送跳过] 客户端 {client_id} 连接已关闭") + clients_to_remove.append(client_id) + failed_count += 1 + continue + except Exception: + pass # 如果属性不存在,继续尝试发送 + + try: + await websocket.send(message) + self.client_info[client_id]['messages_sent'] += 1 + success_count += 1 + except (ConnectionClosedOK, ConnectionClosedError) as e: + logger.warning(f"[发送失败] 客户端 {client_id} 连接已关闭: {e}") + clients_to_remove.append(client_id) + failed_count += 1 + except (asyncio.CancelledError, AssertionError) as e: + # 忽略 keepalive 相关的取消和断言错误(连接可能已断开) + logger.warning(f"[发送失败] 客户端 {client_id} keepalive 错误(连接可能已断开): {type(e).__name__}") + clients_to_remove.append(client_id) + failed_count += 1 + except Exception as e: + logger.error(f"[发送失败] 客户端 {client_id}: {type(e).__name__}: {e}") + # 如果是连接相关错误,标记为需要移除 + if "connection" in str(e).lower() or "closed" in str(e).lower(): + clients_to_remove.append(client_id) + failed_count += 1 + + # 清理已断开的客户端 + for client_id in clients_to_remove: + if client_id in self.clients: + del self.clients[client_id] + if client_id in self.client_info: + del self.client_info[client_id] + + logger.info(f"[发送完成] 成功={success_count}, 失败={failed_count}, 消息={message}") + + async def cmd_close_client(self, client_id: int): + """关闭指定客户端连接""" + if client_id not in self.clients: + logger.error(f"[关闭失败] 客户端 {client_id} 不存在") + await self.cmd_list_clients() + return + + websocket = self.clients[client_id] + try: + await websocket.close() + logger.info(f"[关闭成功] 客户端 {client_id}") + except Exception as e: + logger.error(f"[关闭失败] 客户端 {client_id}: {e}") + + async def cmd_show_stats(self): + """显示统计信息""" + total_received = sum(info['messages_received'] for info in self.client_info.values()) + total_sent = sum(info['messages_sent'] for info in self.client_info.values()) + + logger.info("=== 服务器统计信息 ===") + logger.info(f"当前连接数: {len(self.clients)}") + logger.info(f"总接收消息数: {total_received}") + logger.info(f"总发送消息数: {total_sent}") + logger.info("====================") + + def print_help(self): + """打印帮助信息""" + print("\n可用命令:") + print(" list - 列出所有连接的客户端") + print(" send - 向指定客户端发送文本消息") + print(" sendall - 向所有客户端发送文本消息") + print(" close - 关闭指定客户端连接") + print(" stats - 显示统计信息") + print(" help - 显示帮助信息") + print(" quit/exit - 退出服务器") + print() + + async def start_server(self): + """启动WebSocket服务器""" + logger.info(f"启动WebSocket控制台服务器: ws://{self.host}:{self.port}") + + # 启动服务器 + # 注意:ping_interval 和 ping_timeout 设置 + # - ping_interval: 发送 ping 的间隔(秒),None 表示禁用 + # - ping_timeout: 等待 pong 的超时时间(秒),None 表示不超时 + # 如果客户端响应慢,可以增加 ping_timeout 或设置为 None + async with serve( + self.handle_client, + self.host, + self.port, + ping_interval=30, # 每30秒发送ping + ping_timeout=20, # ping超时20秒(增加超时时间,避免网络延迟导致误判) + close_timeout=10 # 关闭超时10秒 + ) as server: + logger.info("WebSocket服务器启动成功") + logger.info("注意: ping/pong 由 websockets 库自动处理") + logger.info("输入 'help' 查看可用命令\n") + + # 启动控制台输入处理任务 + console_task = asyncio.create_task(self.console_input_handler()) + + # 等待服务器关闭或控制台退出 + try: + await asyncio.gather( + server.wait_closed(), + console_task, + return_exceptions=True + ) + except KeyboardInterrupt: + logger.info("收到中断信号,正在关闭服务器...") + self.running = False + finally: + # 关闭所有连接 + for client_id, websocket in list(self.clients.items()): + try: + await websocket.close() + except Exception: + pass + console_task.cancel() + try: + await console_task + except asyncio.CancelledError: + pass + + +async def main(): + """主函数""" + parser = argparse.ArgumentParser(description='WebSocket控制台服务器') + parser.add_argument('--host', default='0.0.0.0', help='服务器地址 (默认: 0.0.0.0)') + parser.add_argument('--port', type=int, default=8910, help='服务器端口 (默认: 8910)') + parser.add_argument('--verbose', '-v', action='store_true', help='详细日志输出') + + args = parser.parse_args() + + if args.verbose: + logging.getLogger().setLevel(logging.DEBUG) + + # 创建服务器实例 + server = WebSocketConsoleServer(args.host, args.port) + + # 启动服务器 + try: + await server.start_server() + except Exception as e: + logger.error(f"服务器运行出错: {e}") + finally: + logger.info("服务器已关闭") + + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + print("\n服务器已停止") + diff --git a/code/sdk/zhihui/zhihui_net/include/zhihui_net.h b/code/sdk/zhihui/zhihui_net/include/zhihui_net.h index eced4554b..7fbf74e86 100644 --- a/code/sdk/zhihui/zhihui_net/include/zhihui_net.h +++ b/code/sdk/zhihui/zhihui_net/include/zhihui_net.h @@ -13,8 +13,7 @@ * - 未定义 SUPPORT_N706A_NET → 使用 OpenHarmony 模式(PPP拨号 + lwIP) * * SSL实现(仅影响N706A内部): - * - N706A_SSL_USE_MBEDTLS → n706a_net 内部使用 mbedtls 实现SSL - * - N706A_SSL_USE_AT_COMMAND → n706a_net 内部使用 AT 指令实现SSL + * - N706A_SSL_USE_AT_COMMAND → n706a_net 内部使用 AT 指令实现SSL(由N706A模块内置SSL处理) * * @version 5.1.0 * @date 2025-10-31 diff --git a/code/sdk/zhihui/zhihui_net/include/zhihui_net_http.h b/code/sdk/zhihui/zhihui_net/include/zhihui_net_http.h index 316078905..9c48340e5 100644 --- a/code/sdk/zhihui/zhihui_net/include/zhihui_net_http.h +++ b/code/sdk/zhihui/zhihui_net/include/zhihui_net_http.h @@ -87,6 +87,159 @@ void ZhihuiHttpClientSetCustomHeader(HttpClient *client, char *header); */ int ZhihuiHttpClientGetRequest(HttpClient *client, const char *url, HttpClientData *clientData); +/* ========== HTTPS 证书和 SSL 配置接口(可选,高级接口)========== */ + +/** + * @brief 初始化HTTPS证书(仅N706A模式需要,可选接口) + * @return 0成功,非0失败 + * @note + * - 在OpenHarmony模式下,此函数不做任何操作,直接返回成功 + * - 默认情况下,HTTPS 连接时会自动初始化证书,无需手动调用此函数 + * - 此接口仅用于需要手动控制证书初始化的高级场景 + */ +int ZhihuiHttpClientInitCertificate(void); + +/** + * @brief 配置HTTPS SSL参数(仅N706A模式需要,可选接口) + * @param ssl_version SSL版本:0=SSL3.0, 1=TLS1.0, 2=TLS1.1, 3=TLS1.2, 4=TLS1.0/1.1/1.2, 5=TLS1.3 + * @param auth_mode 认证模式:0=不认证, 1=单向认证, 2=双向认证 + * @param ca_cert CA证书文件名(NULL表示使用默认证书) + * @param client_cert 客户端证书文件名(双向认证需要,单向认证为NULL) + * @param client_key 客户端密钥文件名(双向认证需要,单向认证为NULL) + * @return 0成功,非0失败 + * @note + * - 在OpenHarmony模式下,此函数不做任何操作,直接返回成功 + * - 默认情况下,HTTPS 连接时会自动配置 SSL 参数(TLS1.2,单向认证),无需手动调用此函数 + * - 此接口仅用于需要自定义 SSL 配置的高级场景(如双向认证、不同 SSL 版本等) + */ +int ZhihuiHttpClientConfigHttps(int ssl_version, + int auth_mode, + const char *ca_cert, + const char *client_cert, + const char *client_key); + +/* ========== 高级 HTTP 客户端接口(类似 n706a_http_client 的接口)========== */ + +/** + * @brief HTTP客户端句柄(不透明类型,内部实现相关) + */ +typedef void* ZhihuiHttpClientHandle; + +/** + * @brief 初始化HTTP客户端(高级接口) + * @param handle 输出:HTTP客户端句柄 + * @return 0成功,非0失败 + */ +int ZhihuiHttpClientInitAdvanced(ZhihuiHttpClientHandle *handle); + +/** + * @brief 设置HTTP/HTTPS请求URL(高级接口) + * @param handle HTTP客户端句柄 + * @param url 完整URL(如"http://example.com/path"或"https://example.com/path") + * @param keepalive 是否启用长连接,0=关闭,1=启用 + * @return 0成功,非0失败 + */ +int ZhihuiHttpClientSetUrl(ZhihuiHttpClientHandle handle, const char *url, int keepalive); + +/** + * @brief 建立HTTP/HTTPS连接(高级接口) + * @param handle HTTP客户端句柄 + * @return 0成功,非0失败 + */ +int ZhihuiHttpClientSetupConnection(ZhihuiHttpClientHandle handle); + +/** + * @brief 设置HTTP请求头(高级接口) + * @param handle HTTP客户端句柄 + * @param header 请求头字符串(如"Content-Type: application/json\r\n") + * @return 0成功,非0失败 + */ +int ZhihuiHttpClientSetHeader(ZhihuiHttpClientHandle handle, const char *header); + +/** + * @brief 发送HTTP GET请求(高级接口) + * @param handle HTTP客户端句柄 + * @param path 请求路径(如"/api/data",NULL表示使用URL中设置的路径) + * @param clientData 响应数据(responseBuf和responseBufLen必须已设置) + * @return 0成功,非0失败 + */ +int ZhihuiHttpClientGet(ZhihuiHttpClientHandle handle, const char *path, HttpClientData *clientData); + +/** + * @brief 发送HTTP POST请求(高级接口) + * @param handle HTTP客户端句柄 + * @param path 请求路径(如"/api/data",NULL表示使用URL中设置的路径) + * @param content_type 内容类型(如"application/json") + * @param body 请求体数据 + * @param body_len 请求体长度 + * @param clientData 响应数据(responseBuf和responseBufLen必须已设置) + * @return 0成功,非0失败 + */ +int ZhihuiHttpClientPost(ZhihuiHttpClientHandle handle, + const char *path, + const char *content_type, + const void *body, + size_t body_len, + HttpClientData *clientData); + +/** + * @brief 获取HTTP响应码(高级接口) + * @param handle HTTP客户端句柄 + * @return HTTP响应码(如200, 404等),-1表示失败 + */ +int ZhihuiHttpClientGetResponseCode(ZhihuiHttpClientHandle handle); + +/** + * @brief HTTP流式接收数据回调函数类型 + * @param user_data 用户数据指针(由应用层传入) + * @param data 响应体数据块指针 + * @param length 数据块长度 + * @param is_finished 是否是最后一个数据块(true=接收完成,false=还有更多数据) + * @return 0表示成功,非0表示错误(会导致接收中断) + * + * @note 此回调在URC到达时立即调用,应用层可以在此回调中直接写入文件 + * 回调函数应该尽快返回,避免阻塞后续数据接收 + */ +typedef int (*ZhihuiHttpStreamCallback)(void *user_data, + const uint8_t *data, + size_t length, + bool is_finished); + +/** + * @brief 设置HTTP流式接收回调(用于大文件下载等场景) + * @param handle HTTP客户端句柄 + * @param callback 流式接收回调函数(NULL表示禁用流式模式) + * @param user_data 回调函数的用户数据指针 + * @return 0成功,非0失败 + * + * @note 启用流式模式后,响应体数据会通过回调函数立即传递给应用层 + * 应用层可以在回调中直接写入文件,无需等待所有数据接收完成 + * 流式模式与传统的累积模式互斥,启用流式模式后,responseBuf将不再累积数据 + */ +int ZhihuiHttpClientSetStreamCallback(ZhihuiHttpClientHandle handle, + ZhihuiHttpStreamCallback callback, + void *user_data); + +/** + * @brief 流式接收HTTP响应数据(高级接口,用于文件下载等场景) + * @param handle HTTP客户端句柄 + * @param clientData 响应数据(responseBuf和responseBufLen必须已设置) + * @return HTTP_SUCCESS成功,HTTP_EAGAIN还有更多数据,其他值表示错误 + * @note 此接口用于在发送GET/POST请求后,循环接收响应数据块 + * 需要配合 isMore 字段判断是否还有更多数据 + * @note 如果启用了流式模式(通过ZhihuiHttpClientSetStreamCallback), + * 数据会通过回调函数传递,此函数只用于等待响应头解析完成 + * @note 此函数与旧接口同名但参数不同,为避免冲突,使用不同的函数名 + */ +int ZhihuiHttpClientRecvResponseAdvanced(ZhihuiHttpClientHandle handle, HttpClientData *clientData); + +/** + * @brief 关闭HTTP客户端(高级接口) + * @param handle HTTP客户端句柄 + * @return 0成功,非0失败 + */ +int ZhihuiHttpClientClose(ZhihuiHttpClientHandle handle); + #ifdef __cplusplus } #endif diff --git a/code/sdk/zhihui/zhihui_net/src/zhihui_net_http.c b/code/sdk/zhihui/zhihui_net/src/zhihui_net_http.c index 8487b1f39..1f6bd3892 100644 --- a/code/sdk/zhihui/zhihui_net/src/zhihui_net_http.c +++ b/code/sdk/zhihui/zhihui_net/src/zhihui_net_http.c @@ -141,7 +141,7 @@ int ZhihuiHttpClientSend(HttpClient *client, const char *url, int method, HttpCl return -1; } - n706a_httpc_result_t ret = n706a_http_client_init(ppp_client, -1); /* 自动分配socket_id,避免与WebSocket冲突 */ + n706a_httpc_result_t ret = n706a_http_client_init(ppp_client); /* 自动分配socket_id,避免与WebSocket冲突 */ if (ret != N706A_HTTP_SUCCESS) { free(ppp_client); return -1; @@ -274,3 +274,332 @@ int ZhihuiHttpClientGetRequest(HttpClient *client, const char *url, HttpClientDa } } +/* ========== HTTPS 证书和 SSL 配置接口实现 ========== */ + +int ZhihuiHttpClientInitCertificate(void) +{ + if (ZhihuiNetGetImplementation() == ZHIHUI_NET_IMPL_OPENHARMONY) { + /* OpenHarmony 模式:不需要证书初始化,直接返回成功 */ + return 0; + } else { + /* N706A 模式:调用 n706a_http_client_init_certificate */ + n706a_httpc_result_t ret = n706a_http_client_init_certificate(); + return (ret == N706A_HTTP_SUCCESS) ? 0 : -1; + } +} + +int ZhihuiHttpClientConfigHttps(int ssl_version, + int auth_mode, + const char *ca_cert, + const char *client_cert, + const char *client_key) +{ + if (ZhihuiNetGetImplementation() == ZHIHUI_NET_IMPL_OPENHARMONY) { + /* OpenHarmony 模式:不需要SSL配置,直接返回成功 */ + return 0; + } else { + /* N706A 模式:调用 n706a_http_client_config_https */ + n706a_httpc_result_t ret = n706a_http_client_config_https( + ssl_version, + auth_mode, + ca_cert, + client_cert, + client_key + ); + return (ret == N706A_HTTP_SUCCESS) ? 0 : -1; + } +} + +/* ========== 高级 HTTP 客户端接口实现 ========== */ + +/* N706A 模式下的客户端句柄结构 */ +typedef struct { + n706a_http_client_t *n706a_client; +} zhihui_http_client_handle_t; + +int ZhihuiHttpClientInitAdvanced(ZhihuiHttpClientHandle *handle) +{ + if (!handle) { + return -1; + } + + if (ZhihuiNetGetImplementation() == ZHIHUI_NET_IMPL_OPENHARMONY) { + /* OpenHarmony 模式:暂不支持高级接口,返回错误 */ + printf("[ZhihuiNet][HTTP] Advanced HTTP client interface not supported in OpenHarmony mode\n"); + return -1; + } else { + /* N706A 模式:创建客户端句柄 */ + zhihui_http_client_handle_t *h = (zhihui_http_client_handle_t *)malloc(sizeof(zhihui_http_client_handle_t)); + if (!h) { + return -1; + } + + h->n706a_client = (n706a_http_client_t *)malloc(sizeof(n706a_http_client_t)); + if (!h->n706a_client) { + free(h); + return -1; + } + + memset(h->n706a_client, 0, sizeof(n706a_http_client_t)); + + n706a_httpc_result_t ret = n706a_http_client_init(h->n706a_client); + if (ret != N706A_HTTP_SUCCESS) { + free(h->n706a_client); + free(h); + return -1; + } + + *handle = (ZhihuiHttpClientHandle)h; + return 0; + } +} + +int ZhihuiHttpClientSetUrl(ZhihuiHttpClientHandle handle, const char *url, int keepalive) +{ + if (!handle || !url) { + return -1; + } + + if (ZhihuiNetGetImplementation() == ZHIHUI_NET_IMPL_OPENHARMONY) { + /* OpenHarmony 模式:暂不支持高级接口 */ + return -1; + } else { + /* N706A 模式 */ + zhihui_http_client_handle_t *h = (zhihui_http_client_handle_t *)handle; + n706a_httpc_result_t ret = n706a_http_client_set_url(h->n706a_client, url, keepalive); + return (ret == N706A_HTTP_SUCCESS) ? 0 : -1; + } +} + +int ZhihuiHttpClientSetupConnection(ZhihuiHttpClientHandle handle) +{ + if (!handle) { + return -1; + } + + if (ZhihuiNetGetImplementation() == ZHIHUI_NET_IMPL_OPENHARMONY) { + /* OpenHarmony 模式:暂不支持高级接口 */ + return -1; + } else { + /* N706A 模式 */ + zhihui_http_client_handle_t *h = (zhihui_http_client_handle_t *)handle; + n706a_httpc_result_t ret = n706a_http_client_setup_connection(h->n706a_client); + return (ret == N706A_HTTP_SUCCESS) ? 0 : -1; + } +} + +int ZhihuiHttpClientSetHeader(ZhihuiHttpClientHandle handle, const char *header) +{ + if (!handle || !header) { + return -1; + } + + if (ZhihuiNetGetImplementation() == ZHIHUI_NET_IMPL_OPENHARMONY) { + /* OpenHarmony 模式:暂不支持高级接口 */ + return -1; + } else { + /* N706A 模式 */ + zhihui_http_client_handle_t *h = (zhihui_http_client_handle_t *)handle; + n706a_httpc_result_t ret = n706a_http_client_set_header(h->n706a_client, header); + return (ret == N706A_HTTP_SUCCESS) ? 0 : -1; + } +} + +int ZhihuiHttpClientGet(ZhihuiHttpClientHandle handle, const char *path, HttpClientData *clientData) +{ + if (!handle || !clientData) { + return -1; + } + + if (ZhihuiNetGetImplementation() == ZHIHUI_NET_IMPL_OPENHARMONY) { + /* OpenHarmony 模式:暂不支持高级接口 */ + return -1; + } else { + /* N706A 模式 */ + zhihui_http_client_handle_t *h = (zhihui_http_client_handle_t *)handle; + + /* 准备 n706a_http_client_data_t 结构 */ + n706a_http_client_data_t n706a_data = {0}; + n706a_data.responseBuf = clientData->responseBuf; + n706a_data.responseBufLen = clientData->responseBufLen; + n706a_data.headerBuf = clientData->headerBuf; + n706a_data.headerBufLen = clientData->headerBufLen; + + n706a_httpc_result_t ret = n706a_http_client_get(h->n706a_client, path, &n706a_data); + + /* 更新 clientData(注意:n706a_http_client_data_t 和 HttpClientData 字段名可能不同) */ + if (clientData) { + clientData->responseContentLen = n706a_data.responseContentLen; + clientData->contentBlockLen = n706a_data.responseContentLen; + clientData->isMore = n706a_data.isMore; + /* 注意:responseBufLen 在 n706a 中可能被更新为实际接收的长度 */ + if (n706a_data.responseBufLen > 0 && n706a_data.responseBufLen != clientData->responseBufLen) { + clientData->responseBufLen = n706a_data.responseBufLen; + } + } + + return (ret == N706A_HTTP_SUCCESS) ? 0 : -1; + } +} + +int ZhihuiHttpClientPost(ZhihuiHttpClientHandle handle, + const char *path, + const char *content_type, + const void *body, + size_t body_len, + HttpClientData *clientData) +{ + if (!handle || !clientData) { + return -1; + } + + if (ZhihuiNetGetImplementation() == ZHIHUI_NET_IMPL_OPENHARMONY) { + /* OpenHarmony 模式:暂不支持高级接口 */ + return -1; + } else { + /* N706A 模式 */ + zhihui_http_client_handle_t *h = (zhihui_http_client_handle_t *)handle; + + /* 准备 n706a_http_client_data_t 结构 */ + n706a_http_client_data_t n706a_data = {0}; + n706a_data.responseBuf = clientData->responseBuf; + n706a_data.responseBufLen = clientData->responseBufLen; + n706a_data.headerBuf = clientData->headerBuf; + n706a_data.headerBufLen = clientData->headerBufLen; + + n706a_httpc_result_t ret = n706a_http_client_post(h->n706a_client, + path, + content_type, + (const char *)body, + body_len, + &n706a_data); + + /* 更新 clientData */ + if (clientData) { + clientData->responseContentLen = n706a_data.responseContentLen; + clientData->contentBlockLen = n706a_data.responseContentLen; + clientData->isMore = n706a_data.isMore; + /* 注意:responseBufLen 在 n706a 中可能被更新为实际接收的长度 */ + if (n706a_data.responseBufLen > 0 && n706a_data.responseBufLen != clientData->responseBufLen) { + clientData->responseBufLen = n706a_data.responseBufLen; + } + } + + return (ret == N706A_HTTP_SUCCESS) ? 0 : -1; + } +} + +int ZhihuiHttpClientGetResponseCode(ZhihuiHttpClientHandle handle) +{ + if (!handle) { + return -1; + } + + if (ZhihuiNetGetImplementation() == ZHIHUI_NET_IMPL_OPENHARMONY) { + /* OpenHarmony 模式:暂不支持高级接口 */ + return -1; + } else { + /* N706A 模式 */ + zhihui_http_client_handle_t *h = (zhihui_http_client_handle_t *)handle; + return n706a_http_client_get_response_code(h->n706a_client); + } +} + +int ZhihuiHttpClientSetStreamCallback(ZhihuiHttpClientHandle handle, + ZhihuiHttpStreamCallback callback, + void *user_data) +{ + if (!handle) { + return -1; + } + + if (ZhihuiNetGetImplementation() == ZHIHUI_NET_IMPL_OPENHARMONY) { + /* OpenHarmony 模式:暂不支持流式接收 */ + return -1; + } else { + /* N706A 模式 */ + zhihui_http_client_handle_t *h = (zhihui_http_client_handle_t *)handle; + + /* 将回调函数类型转换为 n706a_http_stream_callback_t */ + n706a_http_stream_callback_t n706a_callback = (n706a_http_stream_callback_t)callback; + + n706a_httpc_result_t ret = n706a_http_client_set_stream_callback(h->n706a_client, + n706a_callback, + user_data); + return (ret == N706A_HTTP_SUCCESS) ? 0 : -1; + } +} + +int ZhihuiHttpClientRecvResponseAdvanced(ZhihuiHttpClientHandle handle, HttpClientData *clientData) +{ + if (!handle || !clientData) { + return -1; + } + + if (ZhihuiNetGetImplementation() == ZHIHUI_NET_IMPL_OPENHARMONY) { + /* OpenHarmony 模式:暂不支持高级接口 */ + return -1; + } else { + /* N706A 模式 */ + zhihui_http_client_handle_t *h = (zhihui_http_client_handle_t *)handle; + + /* 准备 n706a_http_client_data_t 结构 */ + n706a_http_client_data_t n706a_data = {0}; + n706a_data.responseBuf = clientData->responseBuf; + n706a_data.responseBufLen = clientData->responseBufLen; + n706a_data.headerBuf = clientData->headerBuf; + n706a_data.headerBufLen = clientData->headerBufLen; + n706a_data.isMore = clientData->isMore; + n706a_data.responseContentLen = clientData->responseContentLen; + + /* 如果启用了流式模式,使用流式接收接口 */ + n706a_httpc_result_t ret; + if (h->n706a_client && + ((n706a_http_client_t *)h->n706a_client)->stream_mode_enabled) { + /* 流式模式:等待后续URC数据到达 */ + ret = n706a_http_client_recv_response_stream(h->n706a_client, &n706a_data, 5000); + } else { + /* 传统模式:调用 http_receive_response(但它是静态函数,无法直接调用) */ + /* 实际上,传统模式下,数据已经在 http_receive_response 中处理了 */ + /* 这里只需要返回状态 */ + ret = (n706a_data.isMore) ? N706A_HTTP_EAGAIN : N706A_HTTP_SUCCESS; + } + + /* 更新 clientData */ + clientData->isMore = n706a_data.isMore; + clientData->responseContentLen = n706a_data.responseContentLen; + clientData->contentBlockLen = n706a_data.contentBlockLen; + + if (ret == N706A_HTTP_EAGAIN) { + return HTTP_EAGAIN; /* 还有更多数据 */ + } else if (ret == N706A_HTTP_SUCCESS) { + return HTTP_SUCCESS; /* 接收完成 */ + } else { + return -1; /* 错误 */ + } + } +} + +int ZhihuiHttpClientClose(ZhihuiHttpClientHandle handle) +{ + if (!handle) { + return -1; + } + + if (ZhihuiNetGetImplementation() == ZHIHUI_NET_IMPL_OPENHARMONY) { + /* OpenHarmony 模式:暂不支持高级接口 */ + return -1; + } else { + /* N706A 模式 */ + zhihui_http_client_handle_t *h = (zhihui_http_client_handle_t *)handle; + + if (h->n706a_client) { + n706a_http_client_close(h->n706a_client); + free(h->n706a_client); + } + + free(h); + return 0; + } +} + diff --git a/code/sdk/zhihui/zhihui_net/src/zhihui_net_websocket.c b/code/sdk/zhihui/zhihui_net/src/zhihui_net_websocket.c index 174c11c50..484d48abd 100644 --- a/code/sdk/zhihui/zhihui_net/src/zhihui_net_websocket.c +++ b/code/sdk/zhihui/zhihui_net/src/zhihui_net_websocket.c @@ -17,9 +17,12 @@ #include #include #include "soc_osal.h" +#include "custom_utils.h" /* 用于 app_task_definition_t, USE_EXTEND_MEM */ +#include "thread_init.h" /* 确保包含 app_task_definition_t, USE_EXTEND_MEM, osPriorityNormal, osThreadFunc_t */ /* 包含 N706A 网络层的头文件 */ #include "n706a_websocket.h" +#include "n706a_tcp_client.h" /* 外部函数:获取当前实现类型 */ extern zhihui_net_impl_type_t ZhihuiNetGetImplementation(void); @@ -31,7 +34,7 @@ typedef struct { bool initialized; bool running; int client_id; - osal_task *worker_thread; + uint32_t worker_thread; /* 使用 uint32_t 以配合 uapi_create_osThread_custom */ /* 保存的回调 */ ws_call_backs_t callbacks; @@ -40,6 +43,7 @@ typedef struct { bool stop_requested; bool reconnect_requested; bool sleep_mode; + bool is_connected; /* 连接状态标志 */ } n706a_ws_adapter_t; @@ -54,6 +58,9 @@ static void n706a_on_open_callback(int client_id) { printf("[ZhihuiNet][N706A][WS] Connection opened (client_id=%d)\n", client_id); + /* 标记为已连接(异步确认) */ + g_adapter.is_connected = true; + if (g_adapter.callbacks.event_callback) { g_adapter.callbacks.event_callback(WS_EVENT_CONNECTED); } @@ -83,6 +90,10 @@ static void n706a_on_message_callback(int client_id, const char *data, size_t le if (is_finished) { /* 完整消息 */ if (g_adapter.callbacks.message_callback) { + /* 注意:data 应该已经是以 \0 结尾的字符串 + * (由 n706a_websocket.c 在解析层保证,使用 recv_buffer 作为临时缓冲区) + * 这里直接传递即可,不需要再次创建副本 + */ g_adapter.callbacks.message_callback(data, 0); } } else { @@ -102,6 +113,9 @@ static void n706a_on_error_callback(int client_id, int error_code, const char *e printf("[ZhihuiNet][N706A][WS] Error occurred: code=%d, msg=%s\n", error_code, error_msg ? error_msg : "unknown"); + /* 标记为未连接,worker线程会在下次循环时尝试重连 */ + g_adapter.is_connected = false; + if (g_adapter.callbacks.event_callback) { g_adapter.callbacks.event_callback(WS_EVENT_DISCONNECTED); } @@ -115,6 +129,9 @@ static void n706a_on_close_callback(int client_id, int close_code, const char *r printf("[ZhihuiNet][N706A][WS] Connection closed: code=%d, reason=%s\n", close_code, reason ? reason : "unknown"); + /* 标记为未连接,worker线程会在下次循环时尝试重连 */ + g_adapter.is_connected = false; + if (g_adapter.callbacks.event_callback) { g_adapter.callbacks.event_callback(WS_EVENT_DISCONNECTED); } @@ -127,19 +144,18 @@ static void* ws_worker_thread(void *arg) { printf("[ZhihuiNet][N706A][WS] Worker thread started\n"); - /* 获取URL */ - char url[512] = {0}; - char header[1024] = {0}; + /* 动态分配大缓冲区,避免栈溢出 */ + char *url = (char *)malloc(512); + char *header = (char *)malloc(1024); - if (g_adapter.callbacks.url_callback) { - g_adapter.callbacks.url_callback(url, sizeof(url), header, sizeof(header)); - } else { - printf("[ZhihuiNet][N706A][WS] ERROR: No URL callback provided\n"); + if (!url || !header) { + printf("[ZhihuiNet][N706A][WS] ERROR: Failed to allocate memory for url/header\n"); + if (url) free(url); + if (header) free(header); + g_adapter.running = false; return NULL; } - printf("[ZhihuiNet][N706A][WS] Connecting to: %s\n", url); - /* 创建WebSocket客户端 */ g_adapter.client_id = n706a_websocket_constructor( n706a_on_open_callback, @@ -150,39 +166,158 @@ static void* ws_worker_thread(void *arg) if (g_adapter.client_id < 0) { printf("[ZhihuiNet][N706A][WS] ERROR: Failed to create WebSocket client\n"); + free(url); + free(header); g_adapter.running = false; return NULL; } printf("[ZhihuiNet][N706A][WS] WebSocket client created (id=%d)\n", g_adapter.client_id); - /* 连接到服务器 */ - if (g_adapter.callbacks.event_callback) { - g_adapter.callbacks.event_callback(WS_EVENT_CONNECTING); - } - - int ret = n706a_websocket_connect(g_adapter.client_id, url, - strlen(header) > 0 ? header : NULL); - if (ret != N706A_WS_SUCCESS) { - printf("[ZhihuiNet][N706A][WS] ERROR: Failed to connect (ret=%d)\n", ret); - n706a_websocket_close(g_adapter.client_id); - g_adapter.running = false; - return NULL; - } - /* 主循环 */ uint32_t interval = g_adapter.callbacks.main_thread_interval_ms; if (interval == 0) { interval = 1000; // 默认1秒 } + g_adapter.is_connected = false; + + /* 重试相关变量 */ + uint32_t retry_count = 0; + uint32_t retry_delay_ms = 1000; /* 初始重试延迟:1秒 */ + const uint32_t max_retry_delay_ms = 30000; /* 最大重试延迟:30秒 */ + const uint32_t max_retry_count = 10; /* 最大重试次数(超过后重置延迟) */ + while (g_adapter.running && !g_adapter.stop_requested) { - /* 处理心跳 */ - if (g_adapter.callbacks.heartbeat_callback) { + /* 检查连接状态 */ + if (!g_adapter.is_connected) { + /* 未连接状态:先检查token是否存在 */ + memset(url, 0, 512); + memset(header, 0, 1024); + + if (g_adapter.callbacks.url_callback) { + g_adapter.callbacks.url_callback(url, 512, header, 1024); + } else { + printf("[ZhihuiNet][N706A][WS] ERROR: No URL callback provided\n"); + osal_msleep(interval); + continue; + } + + /* 检查URL中是否包含有效的token(检查deviceToken参数是否为空) */ + bool has_valid_token = false; + const char *token_param = strstr(url, "deviceToken="); + if (token_param) { + token_param += strlen("deviceToken="); + const char *token_end = strchr(token_param, '&'); + if (token_end) { + size_t token_len = token_end - token_param; + has_valid_token = (token_len > 0); + } else { + /* token在URL末尾 */ + has_valid_token = (strlen(token_param) > 0); + } + } + + if (!has_valid_token) { + /* Token不存在,sleep后继续循环,不进行网络连接 */ + printf("[ZhihuiNet][N706A][WS] Token not available, waiting...\n"); + osal_msleep(interval); + continue; + } + + /* Token存在,尝试连接 */ + printf("[ZhihuiNet][N706A][WS] Connecting to: %s\n", url); + + if (g_adapter.callbacks.event_callback) { + g_adapter.callbacks.event_callback(WS_EVENT_CONNECTING); + } + + int ret = n706a_websocket_connect(g_adapter.client_id, url, + strlen(header) > 0 ? header : NULL); + + if (ret == N706A_WS_SUCCESS) { + g_adapter.is_connected = true; + retry_count = 0; /* 连接成功,重置重试计数 */ + retry_delay_ms = 1000; /* 重置重试延迟 */ + printf("[ZhihuiNet][N706A][WS] Connected successfully\n"); + } else { + /* 连接失败,根据错误类型处理 */ + if (ret == N706A_WS_ERROR_ALREADY_OPEN) { + /* 这个错误不应该出现,因为 n706a_websocket_connect 已经处理了 OPEN 状态 */ + /* 如果仍然出现,说明状态异常,强制关闭连接 */ + printf("[ZhihuiNet][N706A][WS] Unexpected ALREADY_OPEN error (ret=%d), closing connection...\n", ret); + n706a_websocket_close(g_adapter.client_id); + g_adapter.is_connected = false; + osal_msleep(200); /* 等待关闭完成 */ + } else { + printf("[ZhihuiNet][N706A][WS] Failed to connect (ret=%d), will retry after %u ms (retry #%u)\n", + ret, retry_delay_ms, retry_count + 1); + } + + /* 增加重试计数 */ + retry_count++; + + /* 使用指数退避策略 */ + if (retry_count >= max_retry_count) { + /* 达到最大重试次数,重置计数和延迟 */ + retry_count = 0; + retry_delay_ms = 1000; + } else { + /* 指数退避:每次重试延迟翻倍,但不超过最大值 */ + retry_delay_ms *= 2; + if (retry_delay_ms > max_retry_delay_ms) { + retry_delay_ms = max_retry_delay_ms; + } + } + + /* 等待重试延迟 */ + osal_msleep(retry_delay_ms); + continue; + } + } + + + + /* 处理心跳(仅在已连接时) */ + if (g_adapter.is_connected && g_adapter.callbacks.heartbeat_callback) { int need_heartbeat = g_adapter.callbacks.heartbeat_callback(); if (need_heartbeat) { // 发送心跳(可以是Ping帧或自定义消息) - n706a_websocket_send_ping(g_adapter.client_id, NULL, 0); + int ret = n706a_websocket_send_ping(g_adapter.client_id, NULL, 0); + if (ret != N706A_WS_SUCCESS) { + /* 发送失败,检查TCP连接状态 */ + printf("[ZhihuiNet][N706A][WS] Ping failed (ret=%d), checking TCP connection state...\n", ret); + + /* 获取WebSocket客户端的socket_id并检查TCP连接状态 */ + int socket_id = n706a_websocket_get_socket_id(g_adapter.client_id); + if (socket_id >= 0) { + /* 查询TCP连接状态 */ + bool is_connected = false; + bool is_ssl = false; + n706a_tcp_result_t query_ret = n706a_tcp_query_socket_status(socket_id, &is_connected, &is_ssl); + + if (query_ret == N706A_TCP_SUCCESS) { + if (is_connected) { + /* 连接仍然有效,可能是发送失败但连接未断开 */ + printf("[ZhihuiNet][N706A][WS] Ping failed but TCP connection is still active, will retry\n"); + /* 不设置 g_adapter.is_connected = false,继续使用当前连接 */ + } else { + /* 连接已断开 */ + printf("[ZhihuiNet][N706A][WS] Ping failed and TCP connection is disconnected\n"); + g_adapter.is_connected = false; + } + } else { + /* 查询失败,保守处理:判定为连接断开 */ + printf("[ZhihuiNet][N706A][WS] Ping failed and failed to query socket status, assuming disconnected\n"); + g_adapter.is_connected = false; + } + } else { + /* 无法获取socket_id,判定为连接断开 */ + printf("[ZhihuiNet][N706A][WS] Ping failed and cannot get socket ID, assuming disconnected\n"); + g_adapter.is_connected = false; + } + continue; + } } } @@ -190,15 +325,11 @@ static void* ws_worker_thread(void *arg) if (g_adapter.reconnect_requested) { printf("[ZhihuiNet][N706A][WS] Reconnecting...\n"); n706a_websocket_close(g_adapter.client_id); + g_adapter.is_connected = false; osal_msleep(1000); - - /* 重新连接 */ - ret = n706a_websocket_connect(g_adapter.client_id, url, - strlen(header) > 0 ? header : NULL); - if (ret == N706A_WS_SUCCESS) { - printf("[ZhihuiNet][N706A][WS] Reconnection successful\n"); - } g_adapter.reconnect_requested = false; + /* 继续循环,让连接逻辑处理重连 */ + continue; } /* 休眠模式处理 */ @@ -212,7 +343,11 @@ static void* ws_worker_thread(void *arg) /* 清理 */ printf("[ZhihuiNet][N706A][WS] Worker thread exiting\n"); - n706a_websocket_close(g_adapter.client_id); + if (g_adapter.client_id >= 0) { + n706a_websocket_close(g_adapter.client_id); + } + free(url); + free(header); g_adapter.running = false; return NULL; @@ -247,23 +382,28 @@ int ZhihuiWsStart(const ws_call_backs_t *callbacks) g_adapter.sleep_mode = false; g_adapter.client_id = -1; - /* 创建worker线程 */ - osal_kthread_lock(); - g_adapter.worker_thread = osal_kthread_create( - (osal_kthread_handler)ws_worker_thread, - NULL, - "ws_worker", - 8192 - ); - osal_kthread_unlock(); + /* 创建worker线程 - 使用 uapi_create_osThread_custom 在 PSRAM 中分配栈 */ + /* ✅ 优化:通过 USE_EXTEND_MEM 将线程栈分配到 PSRAM,节省珍贵的 DTCM */ + app_task_definition_t ws_worker_task_cfg = {0}; + ws_worker_task_cfg.attr.name = "ws_worker"; + ws_worker_task_cfg.attr.priority = osPriorityNormal; + ws_worker_task_cfg.attr.stack_size = 16384; // 16KB栈(增加栈大小以避免在HTTP和WebSocket同时运行时的栈溢出) + ws_worker_task_cfg.func = (osThreadFunc_t)ws_worker_thread; + ws_worker_task_cfg.task_handle = &g_adapter.worker_thread; + ws_worker_task_cfg.use_mem = USE_EXTEND_MEM; // ⭐ 关键:使用 PSRAM 分配栈 + ws_worker_task_cfg.create_queue_handle = NULL; - if (!g_adapter.worker_thread) { - printf("[ZhihuiNet][N706A][WS] ERROR: Failed to create worker thread\n"); + errcode_t ret = uapi_create_osThread_custom(&ws_worker_task_cfg); + if (ret != ERRCODE_SUCC) { + printf("[ZhihuiNet][N706A][WS] ERROR: Failed to create worker thread: 0x%x\n", ret); g_adapter.initialized = false; g_adapter.running = false; return -1; } + printf("[ZhihuiNet][N706A][WS] Worker thread created with PSRAM stack (handle=0x%X)\n", + g_adapter.worker_thread); + printf("[ZhihuiNet][N706A][WS] Service started successfully\n"); return 0; }