#endif
//最低视频帧率
- #define LOW_FREQUENCE 25
+ #define LOW_FREQUENCE 18
//最高视频帧率
- #define HIGH_FREQUENCE 60
+ #define HIGH_FREQUENCE 30
namespace OHOS
{
enum OTA_STATUS
\r
LOSCFG_BASE_IPC_QUEUE_LIMIT = 50\r
LOSCFG_BASE_CORE_SWTMR_LIMIT = 50\r
-LOSCFG_BASE_IPC_SEM_LIMIT = 30\r
+LOSCFG_BASE_IPC_SEM_LIMIT = 50\r
LOSCFG_PLATFORM_HWI_LIMIT = 96\r
configHWI_MAX_VECTOR_CNT = 152\r
configQUEUE_REGISTRY_SIZE = 8\r
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
"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
#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
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
}
${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
)
# 公共头文件路径(供其他模块引用)
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"
" \"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"
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;
}
#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 */
/* 获取设备token: 优先读本地缓存,判断过期,否则刷新 */
char *uapi_ln_get_device_token(void)
{
- // return "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJXWVNaIiwiZGV2aWNlTW9kZWwiOiJhYm15Ym94XzEiLCJkZXZpY2VWZXJzaW9uIjoiMS4wLjAiLCJkZXZpY2VJZCI6IkRFVklDRV9QQ19URVNUXzAwMSIsImV4cCI6MTc2MTQ2MjQ5Mn0.Oeyu8BDopDTvtAcdNpzz46U2w1UwzEzqcvPwD3Dp08Y";
- /* 若内存已有且非占位,直接返回 */
+ // 若内存已有且非占位,直接返回
if (g_device_token && strcmp(g_device_token, "<null>") != 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])
{
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, "<null>") != 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, "<null>") != 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;
}
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)
{
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 ))
{
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)
{
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 ))
{
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;
}
return ret;
}
+#endif
/* ============================ WebSocket 业务消息 上层处理 ============================ */
void ln_ws_process_message(const ws_data_msg_t *data_msg, ws_command_t type)
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;
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)
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;
}
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)
}
}
-end:
- ZhihuiHttpClientUnprepare(&clientData);
- ZhihuiHttpClientClse(&client);
- free(header_buf2);
+ free(header_buf);
+ free(clientData.responseBuf);
+ ZhihuiHttpClientClose(http_client);
return ERRCODE_SUCC;
}
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)
{
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);
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;
}
cJSON_Delete(root);
free(json_text);
- ZhihuiHttpClientUnprepare(&clientData);
- ZhihuiHttpClientClse(&client);
free(header_buf);
+ free(clientData.responseBuf);
+ ZhihuiHttpClientClose(http_client);
return ERRCODE_FAIL;
}
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;
}
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
/* 全局变量 */
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);
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; // 文件存在且大小匹配(如果知道大小)
}
/* 批量下载表情并上报结果 */
}
}
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);
// 跳过已缓存的文件
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;
}
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)
pkg->emojiCount, downloaded_count, failed_count, remaining_count);
}
+report_status:
WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "上报表情下载状态");
// 下载完成后上报状态
}
}
+/* 表情下载任务包装函数(在独立线程中执行) */
+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 */
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;
}
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);
// 使用说明:
// - 该接口用于设备向服务器同步表情下载/删除状态
// - 服务器会根据上报的信息更新小程序端的表情状态显示
// - 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)
{
// {
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);
free(response_buf);
}
-
free(body);
- ZhihuiHttpClientUnprepare(&clientData);
free(header_buf);
+ free(clientData.responseBuf);
+ ZhihuiHttpClientClose(http_client);
return ERRCODE_SUCC;
}
/* 7.2 上报设备所有表情组状态 */
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)
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;
}
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) +
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;
}
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;
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)
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;
}
#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)
{
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;
}
#include "mbedtls/md5.h"
#include <sys/stat.h>
#include <time.h>
+/* 智慧网络统一接口(已包含 HTTP 接口) */
+#include "zhihui_net.h"
static void ln_to_hex_lower(const uint8_t *in, size_t in_len, char *out)
{
/* 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 */
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);
#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;
}
{
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;
}
{
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 <null>");
g_device_token = strdup("<null>");
}
+
/* 落盘保存 */
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;
}
} 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;
// 打印函数
// 日志等级字符串数组
(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);
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);
}
{
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");
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
--- /dev/null
+/**
+ * @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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+/* 流式下载上下文结构(前向声明) */
+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;
+}
+
--- /dev/null
+/**
+ * @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 */
+
--- /dev/null
+#!/usr/bin/env python3\r
+# -*- coding: utf-8 -*-\r
+"""\r
+基于AT指令的标准Socket接口实现\r
+提供类似Python标准socket.socket的接口,支持TCP和TCP+SSL\r
+"""\r
+\r
+import socket as std_socket\r
+import time\r
+from n706a_tcp_client import N706ATCPClient\r
+from n706a_at_handler import ATCommandHandler\r
+\r
+\r
+class ATSocket:\r
+ """\r
+ 基于AT指令的标准Socket接口\r
+ 提供类似Python标准socket.socket的接口,支持TCP和TCP+SSL\r
+ """\r
+ \r
+ # Socket类型常量(兼容标准socket)\r
+ AF_INET = std_socket.AF_INET\r
+ SOCK_STREAM = std_socket.SOCK_STREAM\r
+ \r
+ def __init__(self, family=AF_INET, type=SOCK_STREAM, at_handler=None, socket_id=0):\r
+ """\r
+ 创建AT Socket对象\r
+ \r
+ Args:\r
+ family: 地址族(目前仅支持AF_INET)\r
+ type: Socket类型(目前仅支持SOCK_STREAM/TCP)\r
+ at_handler: ATCommandHandler实例(如果为None,需要后续调用set_at_handler设置)\r
+ socket_id: Socket ID(0-5),默认0\r
+ """\r
+ if family != self.AF_INET:\r
+ raise ValueError(f"不支持的地址族: {family},目前仅支持AF_INET")\r
+ if type != self.SOCK_STREAM:\r
+ raise ValueError(f"不支持的Socket类型: {type},目前仅支持SOCK_STREAM")\r
+ \r
+ self.family = family\r
+ self.type = type\r
+ self.at_handler = at_handler\r
+ self.socket_id = socket_id\r
+ self._tcp_client = None\r
+ self._timeout = None\r
+ self._connected = False\r
+ self._use_ssl = False\r
+ self._ssl_config = None\r
+ \r
+ # 如果提供了at_handler,立即创建TCP客户端\r
+ if at_handler:\r
+ self._tcp_client = N706ATCPClient(at_handler, socket_id=socket_id)\r
+ \r
+ def set_at_handler(self, at_handler):\r
+ """\r
+ 设置AT指令处理器(如果初始化时未提供)\r
+ \r
+ Args:\r
+ at_handler: ATCommandHandler实例\r
+ """\r
+ self.at_handler = at_handler\r
+ if not self._tcp_client:\r
+ self._tcp_client = N706ATCPClient(at_handler, socket_id=self.socket_id)\r
+ \r
+ def settimeout(self, timeout):\r
+ """\r
+ 设置socket超时时间\r
+ \r
+ Args:\r
+ timeout: 超时时间(秒),None表示阻塞模式\r
+ """\r
+ self._timeout = timeout\r
+ \r
+ def gettimeout(self):\r
+ """\r
+ 获取socket超时时间\r
+ \r
+ Returns:\r
+ float或None: 超时时间(秒),None表示阻塞模式\r
+ """\r
+ return self._timeout\r
+ \r
+ def configure_ssl(self, ssl_version=3, auth_mode=0, ca_cert=None, client_cert=None, client_key=None):\r
+ """\r
+ 配置SSL/TLS参数(用于TCP+SSL连接)\r
+ \r
+ Args:\r
+ ssl_version: SSL版本,0=SSL3.0, 1=TLS1.0, 2=TLS1.1, 3=TLS1.2, 4=TLS1.3\r
+ auth_mode: 认证模式,0=不认证, 1=单向认证, 2=双向认证\r
+ ca_cert: CA证书文件名(单向/双向认证需要)\r
+ client_cert: 客户端证书文件名(双向认证需要)\r
+ client_key: 客户端密钥文件名(双向认证需要)\r
+ \r
+ Returns:\r
+ bool: 是否配置成功\r
+ """\r
+ if not self._tcp_client:\r
+ raise RuntimeError("AT handler未设置,无法配置SSL")\r
+ \r
+ self._use_ssl = True\r
+ self._ssl_config = {\r
+ 'ssl_version': ssl_version,\r
+ 'auth_mode': auth_mode,\r
+ 'ca_cert': ca_cert,\r
+ 'client_cert': client_cert,\r
+ 'client_key': client_key\r
+ }\r
+ \r
+ return self._tcp_client.configure_ssl(\r
+ ssl_version=ssl_version,\r
+ auth_mode=auth_mode,\r
+ ca_cert=ca_cert,\r
+ client_cert=client_cert,\r
+ client_key=client_key\r
+ )\r
+ \r
+ def connect(self, address):\r
+ """\r
+ 连接到服务器\r
+ \r
+ Args:\r
+ address: (host, port) 元组,host可以是IP地址或域名\r
+ \r
+ Raises:\r
+ OSError: 连接失败时抛出\r
+ """\r
+ if not self._tcp_client:\r
+ raise RuntimeError("AT handler未设置,无法连接")\r
+ \r
+ if len(address) != 2:\r
+ raise ValueError("address必须是(host, port)元组")\r
+ \r
+ host, port = address\r
+ \r
+ # 配置SSL(如果需要)\r
+ if self._use_ssl and self._ssl_config:\r
+ if not self._tcp_client.configure_ssl(**self._ssl_config):\r
+ raise OSError("SSL配置失败")\r
+ \r
+ # 建立连接\r
+ if not self._tcp_client.connect(\r
+ server_ip=host,\r
+ server_port=port,\r
+ use_ssl=self._use_ssl,\r
+ transparent_mode=False # 非透传模式\r
+ ):\r
+ raise OSError("连接失败")\r
+ \r
+ self._connected = True\r
+ \r
+ def send(self, data, flags=0):\r
+ """\r
+ 发送数据\r
+ \r
+ Args:\r
+ data: 要发送的数据(bytes)\r
+ flags: 标志位(目前未使用,为兼容性保留)\r
+ \r
+ Returns:\r
+ int: 实际发送的字节数\r
+ \r
+ Raises:\r
+ OSError: 发送失败时抛出\r
+ """\r
+ if not self._connected:\r
+ raise OSError("Socket未连接")\r
+ \r
+ if not isinstance(data, bytes):\r
+ raise TypeError("data必须是bytes类型")\r
+ \r
+ if self._tcp_client.send(data):\r
+ return len(data)\r
+ else:\r
+ raise OSError("数据发送失败")\r
+ \r
+ def recv(self, bufsize, flags=0):\r
+ """\r
+ 接收数据\r
+ \r
+ Args:\r
+ bufsize: 最大接收字节数\r
+ flags: 标志位(目前未使用,为兼容性保留)\r
+ \r
+ Returns:\r
+ bytes: 接收到的数据,如果连接关闭返回空bytes\r
+ \r
+ Raises:\r
+ OSError: 接收失败时抛出\r
+ socket.timeout: 超时时抛出\r
+ """\r
+ if not self._connected:\r
+ raise OSError("Socket未连接")\r
+ \r
+ timeout = self._timeout if self._timeout is not None else 30\r
+ \r
+ try:\r
+ data = self._tcp_client.receive(max_length=min(bufsize, 2048), timeout=timeout)\r
+ if data is None:\r
+ # 超时或连接关闭\r
+ if self._timeout is not None:\r
+ raise std_socket.timeout("接收超时")\r
+ return b""\r
+ return data\r
+ except Exception as e:\r
+ if isinstance(e, std_socket.timeout):\r
+ raise\r
+ raise OSError(f"接收数据失败: {e}")\r
+ \r
+ def recv_into(self, buffer, nbytes=0, flags=0):\r
+ """\r
+ 接收数据到缓冲区\r
+ \r
+ Args:\r
+ buffer: 接收缓冲区(可写字节对象)\r
+ nbytes: 最大接收字节数(0表示使用buffer长度)\r
+ flags: 标志位(目前未使用,为兼容性保留)\r
+ \r
+ Returns:\r
+ int: 实际接收的字节数\r
+ """\r
+ if nbytes == 0:\r
+ nbytes = len(buffer)\r
+ \r
+ data = self.recv(nbytes, flags)\r
+ n = len(data)\r
+ buffer[:n] = data\r
+ return n\r
+ \r
+ def close(self):\r
+ """\r
+ 关闭socket连接\r
+ """\r
+ if self._connected and self._tcp_client:\r
+ self._tcp_client.close()\r
+ self._connected = False\r
+ \r
+ def shutdown(self, how):\r
+ """\r
+ 关闭socket的发送/接收功能\r
+ \r
+ Args:\r
+ how: SHUT_RD, SHUT_WR, 或 SHUT_RDWR\r
+ """\r
+ # AT指令不支持部分关闭,直接关闭连接\r
+ self.close()\r
+ \r
+ def getpeername(self):\r
+ """\r
+ 获取远程地址\r
+ \r
+ Returns:\r
+ tuple: (host, port) 元组\r
+ """\r
+ if not self._connected or not self._tcp_client:\r
+ raise OSError("Socket未连接")\r
+ \r
+ return (self._tcp_client.server_ip, self._tcp_client.server_port)\r
+ \r
+ def getsockname(self):\r
+ """\r
+ 获取本地地址(AT指令不支持,返回默认值)\r
+ \r
+ Returns:\r
+ tuple: (host, port) 元组\r
+ """\r
+ # AT指令无法获取本地地址,返回默认值\r
+ return ("0.0.0.0", 0)\r
+ \r
+ def fileno(self):\r
+ """\r
+ 获取文件描述符(AT Socket不支持,抛出异常)\r
+ \r
+ Raises:\r
+ OSError: AT Socket不支持文件描述符\r
+ """\r
+ raise OSError("AT Socket不支持文件描述符")\r
+ \r
+ def setblocking(self, flag):\r
+ """\r
+ 设置阻塞/非阻塞模式\r
+ \r
+ Args:\r
+ flag: True表示阻塞模式,False表示非阻塞模式\r
+ """\r
+ if flag:\r
+ self._timeout = None\r
+ else:\r
+ # 非阻塞模式使用很小的超时时间\r
+ self._timeout = 0.0\r
+ \r
+ def getblocking(self):\r
+ """\r
+ 获取阻塞/非阻塞模式\r
+ \r
+ Returns:\r
+ bool: True表示阻塞模式,False表示非阻塞模式\r
+ """\r
+ return self._timeout is None\r
+ \r
+ def makefile(self, mode='r', buffering=-1, encoding=None, errors=None, newline=None):\r
+ """\r
+ 创建文件对象(AT Socket不支持,抛出异常)\r
+ \r
+ Raises:\r
+ OSError: AT Socket不支持makefile\r
+ """\r
+ raise OSError("AT Socket不支持makefile")\r
+ \r
+ def __enter__(self):\r
+ """上下文管理器入口"""\r
+ return self\r
+ \r
+ def __exit__(self, exc_type, exc_val, exc_tb):\r
+ """上下文管理器出口"""\r
+ self.close()\r
+ return False\r
+ \r
+ def __del__(self):\r
+ """析构函数"""\r
+ try:\r
+ self.close()\r
+ except:\r
+ pass\r
+\r
+\r
+# 兼容标准socket模块的函数\r
+def create_connection(address, timeout=None, source_address=None, at_handler=None, socket_id=0, use_ssl=False, ssl_config=None):\r
+ """\r
+ 创建TCP连接(兼容标准socket.create_connection)\r
+ \r
+ Args:\r
+ address: (host, port) 元组\r
+ timeout: 超时时间(秒)\r
+ source_address: 源地址(AT Socket不支持,忽略)\r
+ at_handler: ATCommandHandler实例(必需)\r
+ socket_id: Socket ID(0-5),默认0\r
+ use_ssl: 是否使用SSL/TLS,默认False\r
+ ssl_config: SSL配置字典,包含ssl_version, auth_mode, ca_cert等\r
+ \r
+ Returns:\r
+ ATSocket: 已连接的AT Socket对象\r
+ \r
+ Raises:\r
+ OSError: 连接失败时抛出\r
+ """\r
+ if at_handler is None:\r
+ raise ValueError("at_handler参数是必需的")\r
+ \r
+ sock = ATSocket(at_handler=at_handler, socket_id=socket_id)\r
+ \r
+ if timeout is not None:\r
+ sock.settimeout(timeout)\r
+ \r
+ if use_ssl and ssl_config:\r
+ sock.configure_ssl(**ssl_config)\r
+ \r
+ sock.connect(address)\r
+ return sock\r
+\r
+\r
+def socket(family=ATSocket.AF_INET, type=ATSocket.SOCK_STREAM, proto=0, at_handler=None, socket_id=0):\r
+ """\r
+ 创建AT Socket对象(兼容标准socket.socket)\r
+ \r
+ Args:\r
+ family: 地址族(目前仅支持AF_INET)\r
+ type: Socket类型(目前仅支持SOCK_STREAM)\r
+ proto: 协议(未使用)\r
+ at_handler: ATCommandHandler实例(可选,可后续设置)\r
+ socket_id: Socket ID(0-5),默认0\r
+ \r
+ Returns:\r
+ ATSocket: AT Socket对象\r
+ """\r
+ return ATSocket(family=family, type=type, at_handler=at_handler, socket_id=socket_id)\r
+\r
SERVER_PORT = 8910
# AT模块串口配置(根据实际硬件配置修改)
-SERIAL_PORT = "COM19" # 修改为实际的串口设备
+SERIAL_PORT = "COM8" # 修改为实际的串口设备
SERIAL_BAUDRATE = 1000000
SERIAL_TIMEOUT = 10
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)
print(f"✅ {protocol_name}连接成功")
- # 被动接收服务器消息
+ # 交互式发送和接收
print("\n" + "=" * 50)
- print("【步骤6: 被动接收服务器消息】")
+ print("【步骤6: 交互式发送和接收数据】")
print("=" * 50)
- print(f"等待服务器发送消息(超时时间: {RECEIVE_TIMEOUT}秒)...")
- print("(按Ctrl+C中断)")
+ print("命令说明:")
+ print(" send <hex_string> - 发送HEX数据(例如: send 48656C6C6F)")
+ print(" send_ascii <text> - 发送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)
#!/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
OUTPUT_FILENAME = "downloaded_file"
# AT模块串口配置(根据实际硬件配置修改)
-SERIAL_PORT = "COM19" # 修改为实际的串口设备
+SERIAL_PORT = "COM8" # 修改为实际的串口设备
SERIAL_BAUDRATE = 1000000
SERIAL_TIMEOUT = 10
# 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):
"""
- 使ç\94¨ATæ\8c\87令é\80\9aè¿\87HTTP/HTTPSä¸\8bè½½文件
+ ä¸\8bè½½å\8d\95个文件
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)
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â\9c\85 æ\89\80æ\9c\89æ\96\87ä»¶ä¸\8bè½½å®\8cæ\88\90ï¼\81")
else:
- print("\n❌ 文件下载失败!")
+ print("\n❌ 部分文件下载失败!")
exit(1)
SIGN_SECRET = "vDdXVXgPUEUM6_piUjz5AEhMvYTSFzQdPaN-ogKnSus="
# AT模块串口配置(根据实际硬件配置修改)
-SERIAL_PORT = "COM19" # 修改为实际的串口设备
+SERIAL_PORT = "COM8" # 修改为实际的串口设备
SERIAL_BAUDRATE = 1000000
SERIAL_TIMEOUT = 10
--- /dev/null
+#!/usr/bin/env python3\r
+# -*- coding: utf-8 -*-\r
+"""\r
+通过TCP实现WebSocket协议\r
+包括握手、收数据协议解析、发数据协议封包\r
+"""\r
+\r
+import time\r
+import json\r
+import base64\r
+import hashlib\r
+import struct\r
+import random\r
+import string\r
+import os\r
+import urllib.parse\r
+from n706a_at_handler import ATCommandHandler\r
+from n706a_tcp_client import N706ATCPClient\r
+\r
+# 配置参数\r
+SERIAL_PORT = "COM8" # 修改为实际的串口设备\r
+SERIAL_BAUDRATE = 1000000\r
+SERIAL_TIMEOUT = 10\r
+\r
+# WebSocket服务器配置\r
+WS_URL = "ws://110.42.224.129:8910/websocket"\r
+\r
+\r
+class WebSocketFrame:\r
+ """WebSocket帧处理类"""\r
+ \r
+ # Opcode定义\r
+ OPCODE_CONTINUATION = 0x0\r
+ OPCODE_TEXT = 0x1\r
+ OPCODE_BINARY = 0x2\r
+ OPCODE_CLOSE = 0x8\r
+ OPCODE_PING = 0x9\r
+ OPCODE_PONG = 0xA\r
+ \r
+ @staticmethod\r
+ def encode_frame(payload: bytes, opcode: int = OPCODE_TEXT, fin: bool = True, mask: bool = True) -> bytes:\r
+ """\r
+ 编码WebSocket帧(客户端发送必须使用掩码)\r
+ \r
+ Args:\r
+ payload: 负载数据\r
+ opcode: 操作码\r
+ fin: 是否为最后一帧\r
+ mask: 是否使用掩码(客户端必须为True)\r
+ \r
+ Returns:\r
+ bytes: 编码后的帧数据\r
+ """\r
+ payload_len = len(payload)\r
+ \r
+ # 第一个字节:FIN(1) + RSV(3) + Opcode(4)\r
+ first_byte = (0x80 if fin else 0x00) | (opcode & 0x0F)\r
+ \r
+ # 第二个字节:MASK(1) + Payload length(7)\r
+ if payload_len < 126:\r
+ second_byte = (0x80 if mask else 0x00) | payload_len\r
+ header_len = 2\r
+ elif payload_len < 65536:\r
+ second_byte = (0x80 if mask else 0x00) | 126\r
+ header_len = 4\r
+ else:\r
+ second_byte = (0x80 if mask else 0x00) | 127\r
+ header_len = 10\r
+ \r
+ # 构建帧头\r
+ frame = bytearray([first_byte, second_byte])\r
+ \r
+ # 添加扩展长度字段\r
+ if payload_len >= 126 and payload_len < 65536:\r
+ frame.extend(struct.pack('!H', payload_len))\r
+ elif payload_len >= 65536:\r
+ frame.extend(struct.pack('!Q', payload_len))\r
+ \r
+ # 添加掩码(客户端必须使用)\r
+ if mask:\r
+ mask_key = os.urandom(4)\r
+ frame.extend(mask_key)\r
+ # 对负载进行掩码处理\r
+ masked_payload = bytearray(payload)\r
+ for i in range(len(masked_payload)):\r
+ masked_payload[i] ^= mask_key[i % 4]\r
+ frame.extend(masked_payload)\r
+ else:\r
+ frame.extend(payload)\r
+ \r
+ return bytes(frame)\r
+ \r
+ @staticmethod\r
+ def decode_frame(data: bytes) -> tuple:\r
+ """\r
+ 解码WebSocket帧(服务器发送的数据没有掩码)\r
+ \r
+ Args:\r
+ data: 帧数据\r
+ \r
+ Returns:\r
+ tuple: (opcode, payload, fin, remaining_data) 或 (None, None, None, data) 如果数据不完整\r
+ """\r
+ if len(data) < 2:\r
+ return None, None, None, data\r
+ \r
+ first_byte = data[0]\r
+ second_byte = data[1]\r
+ \r
+ fin = (first_byte & 0x80) != 0\r
+ opcode = first_byte & 0x0F\r
+ masked = (second_byte & 0x80) != 0\r
+ payload_len = second_byte & 0x7F\r
+ \r
+ header_len = 2\r
+ \r
+ # 解析扩展长度字段\r
+ if payload_len == 126:\r
+ if len(data) < 4:\r
+ return None, None, None, data\r
+ payload_len = struct.unpack('!H', data[2:4])[0]\r
+ header_len = 4\r
+ elif payload_len == 127:\r
+ if len(data) < 10:\r
+ return None, None, None, data\r
+ payload_len = struct.unpack('!Q', data[2:10])[0]\r
+ header_len = 10\r
+ \r
+ # 解析掩码(服务器发送的数据不应该有掩码,但为了兼容性处理)\r
+ mask_key = None\r
+ if masked:\r
+ if len(data) < header_len + 4:\r
+ return None, None, None, data\r
+ mask_key = data[header_len:header_len + 4]\r
+ header_len += 4\r
+ \r
+ # 检查是否有足够的负载数据\r
+ if len(data) < header_len + payload_len:\r
+ return None, None, None, data\r
+ \r
+ # 提取负载\r
+ payload = data[header_len:header_len + payload_len]\r
+ \r
+ # 去除掩码(如果存在)\r
+ if masked and mask_key:\r
+ payload = bytearray(payload)\r
+ for i in range(len(payload)):\r
+ payload[i] ^= mask_key[i % 4]\r
+ payload = bytes(payload)\r
+ \r
+ remaining_data = data[header_len + payload_len:]\r
+ \r
+ return opcode, payload, fin, remaining_data\r
+\r
+\r
+def websocket_handshake(tcp_client: N706ATCPClient, url: str, headers: dict = None) -> bool:\r
+ """\r
+ 执行WebSocket握手\r
+ \r
+ Args:\r
+ tcp_client: TCP客户端实例\r
+ url: WebSocket URL\r
+ headers: 额外的HTTP头\r
+ \r
+ Returns:\r
+ bool: 握手是否成功\r
+ """\r
+ # 解析URL\r
+ parsed = urllib.parse.urlparse(url)\r
+ host = parsed.hostname\r
+ port = parsed.port or (443 if parsed.scheme == 'wss' else 80)\r
+ path = parsed.path or '/'\r
+ if parsed.query:\r
+ path += '?' + parsed.query\r
+ \r
+ # 生成WebSocket Key\r
+ key = base64.b64encode(\r
+ ''.join(random.choices(string.ascii_letters + string.digits, k=16)).encode()\r
+ ).decode()\r
+ \r
+ # 构建握手请求\r
+ request_lines = [\r
+ f"GET {path} HTTP/1.1",\r
+ f"Host: {host}:{port}",\r
+ "Upgrade: websocket",\r
+ "Connection: Upgrade",\r
+ f"Sec-WebSocket-Key: {key}",\r
+ "Sec-WebSocket-Version: 13",\r
+ ]\r
+ \r
+ # 添加额外头部\r
+ if headers:\r
+ for name, value in headers.items():\r
+ request_lines.append(f"{name}: {value}")\r
+ \r
+ request_lines.append("") # 空行\r
+ request_lines.append("") # 空行\r
+ \r
+ request = "\r\n".join(request_lines)\r
+ request_bytes = request.encode('utf-8')\r
+ \r
+ print(f"\n【WebSocket握手请求】")\r
+ print(request)\r
+ print("=" * 60)\r
+ \r
+ # 发送握手请求\r
+ print(f"\n【发送握手请求数据】")\r
+ print(f"数据长度: {len(request_bytes)} 字节")\r
+ print(f"HEX格式: {request_bytes.hex().upper()}")\r
+ print(f"ASCII格式:")\r
+ print(request)\r
+ print("=" * 60)\r
+ \r
+ if not tcp_client.send(request_bytes):\r
+ print("❌ 发送握手请求失败")\r
+ return False\r
+ \r
+ print("✅ 握手请求已发送")\r
+ \r
+ # 接收握手响应\r
+ print("\n等待握手响应...")\r
+ response = b""\r
+ start_time = time.time()\r
+ timeout = 30\r
+ \r
+ while time.time() - start_time < timeout:\r
+ # 尝试接收数据\r
+ data = tcp_client.wait_for_data(timeout=5)\r
+ if data:\r
+ print(f"\n【接收握手响应数据片段】")\r
+ print(f"数据长度: {len(data)} 字节")\r
+ print(f"HEX格式: {data.hex().upper()}")\r
+ try:\r
+ print(f"ASCII格式: {data.decode('utf-8', errors='replace')}")\r
+ except:\r
+ print(f"ASCII格式: (无法解码)")\r
+ print("-" * 60)\r
+ response += data\r
+ # 检查是否收到完整的HTTP响应(以\r\n\r\n结尾)\r
+ if b"\r\n\r\n" in response:\r
+ break\r
+ else:\r
+ # 如果没有数据,尝试主动读取\r
+ data = tcp_client.receive(max_length=4096, timeout=1)\r
+ if data:\r
+ print(f"\n【接收握手响应数据片段(主动读取)】")\r
+ print(f"数据长度: {len(data)} 字节")\r
+ print(f"HEX格式: {data.hex().upper()}")\r
+ try:\r
+ print(f"ASCII格式: {data.decode('utf-8', errors='replace')}")\r
+ except:\r
+ print(f"ASCII格式: (无法解码)")\r
+ print("-" * 60)\r
+ response += data\r
+ if b"\r\n\r\n" in response:\r
+ break\r
+ \r
+ if not response:\r
+ print("❌ 未收到握手响应")\r
+ return False\r
+ \r
+ # 解析响应\r
+ print(f"\n【WebSocket握手响应(完整数据)】")\r
+ print(f"总长度: {len(response)} 字节")\r
+ print(f"HEX格式: {response.hex().upper()}")\r
+ print(f"ASCII格式:")\r
+ response_text = response.decode('utf-8', errors='ignore')\r
+ print(response_text)\r
+ print("=" * 60)\r
+ \r
+ lines = response_text.split('\r\n')\r
+ \r
+ # 检查状态行\r
+ if not lines[0].startswith('HTTP/1.1 101'):\r
+ print(f"❌ 握手失败,状态行: {lines[0]}")\r
+ return False\r
+ \r
+ # 检查Upgrade和Connection头\r
+ response_headers = {}\r
+ for line in lines[1:]:\r
+ if ':' in line:\r
+ name, value = line.split(':', 1)\r
+ response_headers[name.strip().lower()] = value.strip()\r
+ \r
+ if response_headers.get('upgrade', '').lower() != 'websocket':\r
+ print("❌ 握手失败,缺少Upgrade头")\r
+ return False\r
+ \r
+ if 'upgrade' not in response_headers.get('connection', '').lower():\r
+ print("❌ 握手失败,Connection头不正确")\r
+ return False\r
+ \r
+ # 验证Sec-WebSocket-Accept\r
+ expected_accept = base64.b64encode(\r
+ hashlib.sha1((key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode()).digest()\r
+ ).decode()\r
+ \r
+ actual_accept = response_headers.get('sec-websocket-accept', '')\r
+ if actual_accept != expected_accept:\r
+ print(f"❌ 握手失败,Sec-WebSocket-Accept不匹配")\r
+ print(f"期望: {expected_accept}")\r
+ print(f"实际: {actual_accept}")\r
+ return False\r
+ \r
+ print("✅ WebSocket握手成功")\r
+ return True\r
+\r
+\r
+def test_websocket_via_tcp():\r
+ """通过TCP实现WebSocket协议测试"""\r
+ print("=" * 60)\r
+ print("通过TCP实现WebSocket协议")\r
+ print("=" * 60)\r
+ \r
+ at_handler = ATCommandHandler(SERIAL_PORT, SERIAL_BAUDRATE, SERIAL_TIMEOUT)\r
+ \r
+ try:\r
+ # 连接串口\r
+ if not at_handler.connect():\r
+ print("❌ 无法连接串口")\r
+ return False\r
+ \r
+ # 测试AT连接\r
+ success, response = at_handler.send_at_command("AT", timeout=3)\r
+ if not success:\r
+ print("❌ AT模块无响应")\r
+ return False\r
+ print("✅ AT模块连接正常")\r
+ \r
+ # 检查PPP拨号状态\r
+ is_ppp_connected, ppp_ip = at_handler.check_ppp_status()\r
+ if not is_ppp_connected:\r
+ print("建立PPP连接...")\r
+ success, ip = at_handler.establish_ppp_connection()\r
+ if not success:\r
+ print("❌ PPP连接失败")\r
+ return False\r
+ print(f"✅ PPP连接成功,IP: {ip}")\r
+ else:\r
+ print(f"✅ PPP已连接,IP: {ppp_ip}")\r
+ \r
+ # 解析WebSocket URL\r
+ parsed = urllib.parse.urlparse(WS_URL)\r
+ host = parsed.hostname\r
+ port = parsed.port or (443 if parsed.scheme == 'wss' else 80)\r
+ \r
+ print(f"\n【步骤1: 建立TCP连接】")\r
+ print(f"服务器: {host}:{port}")\r
+ print("=" * 60)\r
+ \r
+ # 在连接前,先关闭可能存在的旧连接\r
+ print("检查并关闭可能存在的旧连接...")\r
+ socket_id = 0\r
+ \r
+ # 尝试关闭所有可能的socket连接\r
+ for close_socket_id in [socket_id]:\r
+ # 尝试关闭普通TCP连接\r
+ success, response = at_handler.send_at_command(f"AT+TCPCLOSE={close_socket_id}", timeout=3)\r
+ if success:\r
+ print(f"✅ Socket {close_socket_id} 已关闭(TCP)")\r
+ else:\r
+ print(f"⚠️ Socket {close_socket_id} TCP关闭命令响应: {response}")\r
+ \r
+ # 尝试关闭SSL TCP连接\r
+ success, response = at_handler.send_at_command(f"AT+SSLTCPCLOSE={close_socket_id}", timeout=3)\r
+ if success:\r
+ print(f"✅ Socket {close_socket_id} 已关闭(SSL TCP)")\r
+ else:\r
+ print(f"⚠️ Socket {close_socket_id} SSL TCP关闭命令响应: {response}")\r
+ \r
+ # 等待模块清理socket状态\r
+ print("等待模块清理socket状态...")\r
+ time.sleep(0.5)\r
+ \r
+ # 再次确认PPP连接状态\r
+ print("\n检查PPP连接状态...")\r
+ is_ppp_connected, ppp_ip = at_handler.check_ppp_status()\r
+ if not is_ppp_connected:\r
+ print("⚠️ PPP连接状态异常,尝试重新建立...")\r
+ success, ip = at_handler.establish_ppp_connection()\r
+ if not success:\r
+ print("❌ PPP连接失败,无法继续TCP连接")\r
+ return False\r
+ print(f"✅ PPP连接已重新建立,IP: {ip}")\r
+ time.sleep(1) # 等待PPP稳定\r
+ else:\r
+ print(f"✅ PPP连接正常,IP: {ppp_ip}")\r
+ \r
+ # 检查网络注册状态\r
+ print("\n检查网络注册状态...")\r
+ if at_handler.check_network_registration():\r
+ print("✅ 网络已注册")\r
+ else:\r
+ print("⚠️ 网络未注册,可能影响连接")\r
+ \r
+ # 测试DNS解析(如果使用域名)\r
+ if host and not host.replace('.', '').isdigit():\r
+ print(f"\n测试DNS解析: {host}...")\r
+ # 可以尝试ping或DNS查询,这里先跳过\r
+ print("⚠️ 使用域名,确保DNS配置正确")\r
+ \r
+ # 创建TCP客户端\r
+ tcp_client = N706ATCPClient(at_handler, socket_id=socket_id)\r
+ \r
+ # 建立TCP连接\r
+ print(f"\n尝试建立TCP连接到 {host}:{port}...")\r
+ if not tcp_client.connect(host, port, use_ssl=False, transparent_mode=False):\r
+ print("❌ TCP连接失败")\r
+ print("\n可能的原因:")\r
+ print(" 1. 服务器地址或端口不正确")\r
+ print(" 2. 网络连接问题(检查PPP连接状态)")\r
+ print(" 3. Socket ID被占用(尝试使用其他Socket ID)")\r
+ print(" 4. 服务器不可达或防火墙阻止")\r
+ return False\r
+ \r
+ print("✅ TCP连接成功")\r
+ \r
+ # WebSocket握手\r
+ print(f"\n【步骤2: WebSocket握手】")\r
+ print("=" * 60)\r
+ \r
+ # 额外的HTTP头(可选)\r
+ headers = {\r
+ "User-Agent": "AT-WebSocket-Client/1.0"\r
+ }\r
+ \r
+ if not websocket_handshake(tcp_client, WS_URL, headers):\r
+ print("❌ WebSocket握手失败")\r
+ tcp_client.close()\r
+ return False\r
+ \r
+ # 发送和接收消息\r
+ print(f"\n【步骤3: 发送和接收消息】")\r
+ print("=" * 60)\r
+ \r
+ # 接收缓冲区\r
+ recv_buffer = b""\r
+ \r
+ # 发送文本消息\r
+ print("\n发送文本消息...")\r
+ test_text = json.dumps({\r
+ "type": "test",\r
+ "message": "Hello from AT WebSocket via TCP!",\r
+ "timestamp": int(time.time())\r
+ })\r
+ text_payload = test_text.encode('utf-8')\r
+ text_frame = WebSocketFrame.encode_frame(text_payload, opcode=WebSocketFrame.OPCODE_TEXT)\r
+ \r
+ print(f"\n【发送文本消息帧】")\r
+ print(f"原始文本: {test_text}")\r
+ print(f"负载长度: {len(text_payload)} 字节")\r
+ print(f"帧长度: {len(text_frame)} 字节")\r
+ print(f"帧HEX: {text_frame.hex().upper()}")\r
+ print(f"帧前16字节HEX: {text_frame[:16].hex().upper()}...")\r
+ print("-" * 60)\r
+ \r
+ if tcp_client.send(text_frame):\r
+ print(f"✅ 文本消息发送成功")\r
+ else:\r
+ print("❌ 文本消息发送失败")\r
+ \r
+ # 发送二进制消息\r
+ print("\n发送二进制消息...")\r
+ test_binary = b"Binary test data: " + bytes(range(10))\r
+ binary_frame = WebSocketFrame.encode_frame(test_binary, opcode=WebSocketFrame.OPCODE_BINARY)\r
+ \r
+ print(f"\n【发送二进制消息帧】")\r
+ print(f"原始数据: {test_binary}")\r
+ print(f"负载长度: {len(test_binary)} 字节")\r
+ print(f"负载HEX: {test_binary.hex().upper()}")\r
+ print(f"帧长度: {len(binary_frame)} 字节")\r
+ print(f"帧HEX: {binary_frame.hex().upper()}")\r
+ print(f"帧前16字节HEX: {binary_frame[:16].hex().upper()}...")\r
+ print("-" * 60)\r
+ \r
+ if tcp_client.send(binary_frame):\r
+ print(f"✅ 二进制消息发送成功")\r
+ else:\r
+ print("❌ 二进制消息发送失败")\r
+ \r
+ # 发送Ping\r
+ print("\n发送Ping...")\r
+ ping_payload = b"ping"\r
+ ping_frame = WebSocketFrame.encode_frame(ping_payload, opcode=WebSocketFrame.OPCODE_PING)\r
+ \r
+ print(f"\n【发送Ping帧】")\r
+ print(f"负载: {ping_payload}")\r
+ print(f"负载长度: {len(ping_payload)} 字节")\r
+ print(f"帧长度: {len(ping_frame)} 字节")\r
+ print(f"帧HEX: {ping_frame.hex().upper()}")\r
+ print("-" * 60)\r
+ \r
+ if tcp_client.send(ping_frame):\r
+ print("✅ Ping发送成功")\r
+ else:\r
+ print("❌ Ping发送失败")\r
+ \r
+ # 接收消息(最多等待30秒)\r
+ print("\n等待接收消息(最多30秒)...")\r
+ start_time = time.time()\r
+ message_count = 0\r
+ \r
+ while time.time() - start_time < 30:\r
+ # 尝试从缓冲区解析帧\r
+ if recv_buffer:\r
+ opcode, payload, fin, remaining = WebSocketFrame.decode_frame(recv_buffer)\r
+ if payload is not None:\r
+ # 打印解析出的帧信息\r
+ opcode_names = {\r
+ WebSocketFrame.OPCODE_TEXT: "TEXT",\r
+ WebSocketFrame.OPCODE_BINARY: "BINARY",\r
+ WebSocketFrame.OPCODE_CLOSE: "CLOSE",\r
+ WebSocketFrame.OPCODE_PING: "PING",\r
+ WebSocketFrame.OPCODE_PONG: "PONG",\r
+ WebSocketFrame.OPCODE_CONTINUATION: "CONTINUATION"\r
+ }\r
+ opcode_name = opcode_names.get(opcode, f"UNKNOWN({opcode})")\r
+ \r
+ print(f"\n【解析WebSocket帧】")\r
+ print(f"Opcode: {opcode_name} (0x{opcode:02X})")\r
+ print(f"FIN: {fin}")\r
+ print(f"负载长度: {len(payload)} 字节")\r
+ print(f"负载HEX: {payload.hex().upper()}")\r
+ if len(payload) > 100:\r
+ print(f"负载HEX(前100字节): {payload[:100].hex().upper()}...")\r
+ print("-" * 60)\r
+ \r
+ recv_buffer = remaining\r
+ \r
+ # 处理控制帧\r
+ if opcode == WebSocketFrame.OPCODE_CLOSE:\r
+ print("🔌 收到关闭帧")\r
+ if len(payload) >= 2:\r
+ close_code = struct.unpack('!H', payload[:2])[0]\r
+ close_reason = payload[2:].decode('utf-8', errors='ignore') if len(payload) > 2 else ""\r
+ print(f" 关闭码: {close_code}, 原因: {close_reason}")\r
+ break\r
+ elif opcode == WebSocketFrame.OPCODE_PING:\r
+ print("📨 收到Ping,发送Pong...")\r
+ pong_frame = WebSocketFrame.encode_frame(payload, opcode=WebSocketFrame.OPCODE_PONG)\r
+ print(f"【发送Pong帧】")\r
+ print(f"帧HEX: {pong_frame.hex().upper()}")\r
+ print("-" * 60)\r
+ tcp_client.send(pong_frame)\r
+ elif opcode == WebSocketFrame.OPCODE_PONG:\r
+ print("📨 收到Pong响应")\r
+ try:\r
+ pong_text = payload.decode('utf-8', errors='replace')\r
+ print(f" Pong数据: {pong_text}")\r
+ except:\r
+ print(f" Pong数据HEX: {payload.hex().upper()}")\r
+ elif opcode == WebSocketFrame.OPCODE_TEXT:\r
+ message_count += 1\r
+ try:\r
+ text = payload.decode('utf-8')\r
+ print(f"📨 收到文本消息 #{message_count}: {text}")\r
+ except:\r
+ print(f"📨 收到文本消息 #{message_count} (无法解码为UTF-8)")\r
+ print(f" 原始HEX: {payload.hex().upper()}")\r
+ elif opcode == WebSocketFrame.OPCODE_BINARY:\r
+ message_count += 1\r
+ print(f"📨 收到二进制消息 #{message_count}, 长度: {len(payload)} 字节")\r
+ print(f" 完整HEX: {payload.hex().upper()}")\r
+ if len(payload) > 100:\r
+ print(f" HEX(前100字节): {payload[:100].hex().upper()}...")\r
+ continue\r
+ \r
+ # 接收新数据\r
+ data = tcp_client.wait_for_data(timeout=5)\r
+ if data:\r
+ print(f"\n【接收原始数据】")\r
+ print(f"数据长度: {len(data)} 字节")\r
+ print(f"HEX格式: {data.hex().upper()}")\r
+ if len(data) > 100:\r
+ print(f"HEX格式(前100字节): {data[:100].hex().upper()}...")\r
+ try:\r
+ ascii_str = data.decode('utf-8', errors='replace')\r
+ if all(32 <= ord(c) < 127 or c in '\n\r\t' for c in ascii_str[:100]):\r
+ print(f"ASCII格式: {ascii_str[:200]}")\r
+ else:\r
+ print(f"ASCII格式: (包含非打印字符)")\r
+ except:\r
+ print(f"ASCII格式: (无法解码为UTF-8)")\r
+ print("-" * 60)\r
+ \r
+ recv_buffer += data\r
+ print(f"📥 缓冲区总长度: {len(recv_buffer)} 字节")\r
+ else:\r
+ # 超时,继续等待\r
+ continue\r
+ \r
+ if message_count == 0:\r
+ print("⚠️ 未收到任何消息")\r
+ \r
+ # 发送关闭帧\r
+ print("\n【步骤4: 关闭连接】")\r
+ print("=" * 60)\r
+ close_payload = struct.pack('!H', 1000) + b"Normal closure"\r
+ close_frame = WebSocketFrame.encode_frame(close_payload, opcode=WebSocketFrame.OPCODE_CLOSE)\r
+ \r
+ print(f"\n【发送关闭帧】")\r
+ print(f"关闭码: 1000")\r
+ print(f"关闭原因: Normal closure")\r
+ print(f"负载长度: {len(close_payload)} 字节")\r
+ print(f"帧长度: {len(close_frame)} 字节")\r
+ print(f"帧HEX: {close_frame.hex().upper()}")\r
+ print("-" * 60)\r
+ \r
+ tcp_client.send(close_frame)\r
+ print("✅ 关闭帧已发送")\r
+ \r
+ # 关闭TCP连接\r
+ tcp_client.close()\r
+ print("✅ TCP连接已关闭")\r
+ \r
+ return True\r
+ \r
+ except Exception as e:\r
+ print(f"❌ 测试异常: {e}")\r
+ import traceback\r
+ traceback.print_exc()\r
+ return False\r
+ finally:\r
+ at_handler.disconnect()\r
+\r
+\r
+def main():\r
+ """主函数"""\r
+ print("=" * 60)\r
+ print("通过TCP实现WebSocket协议测试")\r
+ print(f"WebSocket URL: {WS_URL}")\r
+ print("=" * 60)\r
+ \r
+ # 执行测试\r
+ success = test_websocket_via_tcp()\r
+ \r
+ # 总结\r
+ print("\n" + "=" * 60)\r
+ print("测试总结")\r
+ print("=" * 60)\r
+ print(f"WebSocket测试: {'✅ 通过' if success else '❌ 失败'}")\r
+ print("=" * 60)\r
+\r
+\r
+if __name__ == "__main__":\r
+ main()\r
+\r
--- /dev/null
+#!/usr/bin/env python3\r
+# -*- coding: utf-8 -*-\r
+"""\r
+基于AT Socket的WebSocket客户端实现\r
+使用标准socket接口实现WebSocket协议\r
+"""\r
+\r
+import base64\r
+import hashlib\r
+import struct\r
+import time\r
+import urllib.parse\r
+from typing import Optional, Callable, Dict, Any\r
+from at_socket import ATSocket, create_connection\r
+from n706a_at_handler import ATCommandHandler\r
+\r
+\r
+class WebSocketFrame:\r
+ """WebSocket帧处理类"""\r
+ \r
+ # Opcode定义\r
+ OPCODE_CONTINUATION = 0x0\r
+ OPCODE_TEXT = 0x1\r
+ OPCODE_BINARY = 0x2\r
+ OPCODE_CLOSE = 0x8\r
+ OPCODE_PING = 0x9\r
+ OPCODE_PONG = 0xA\r
+ \r
+ @staticmethod\r
+ def encode_frame(payload: bytes, opcode: int = OPCODE_TEXT, fin: bool = True, mask: bool = True) -> bytes:\r
+ """\r
+ 编码WebSocket帧\r
+ \r
+ Args:\r
+ payload: 负载数据\r
+ opcode: 操作码\r
+ fin: 是否为最后一帧\r
+ mask: 是否使用掩码(客户端必须使用)\r
+ \r
+ Returns:\r
+ bytes: 编码后的帧数据\r
+ """\r
+ payload_len = len(payload)\r
+ \r
+ # 第一个字节:FIN(1) + RSV(3) + Opcode(4)\r
+ first_byte = (0x80 if fin else 0x00) | (opcode & 0x0F)\r
+ \r
+ # 第二个字节:MASK(1) + Payload length(7)\r
+ if payload_len < 126:\r
+ second_byte = (0x80 if mask else 0x00) | payload_len\r
+ header_len = 2\r
+ elif payload_len < 65536:\r
+ second_byte = (0x80 if mask else 0x00) | 126\r
+ header_len = 4\r
+ else:\r
+ second_byte = (0x80 if mask else 0x00) | 127\r
+ header_len = 10\r
+ \r
+ # 构建帧头\r
+ frame = bytearray([first_byte, second_byte])\r
+ \r
+ # 添加扩展长度字段\r
+ if payload_len >= 126 and payload_len < 65536:\r
+ frame.extend(struct.pack('!H', payload_len))\r
+ elif payload_len >= 65536:\r
+ frame.extend(struct.pack('!Q', payload_len))\r
+ \r
+ # 添加掩码(客户端必须使用)\r
+ if mask:\r
+ import os\r
+ mask_key = os.urandom(4)\r
+ frame.extend(mask_key)\r
+ # 对负载进行掩码处理\r
+ masked_payload = bytearray(payload)\r
+ for i in range(len(masked_payload)):\r
+ masked_payload[i] ^= mask_key[i % 4]\r
+ frame.extend(masked_payload)\r
+ else:\r
+ frame.extend(payload)\r
+ \r
+ return bytes(frame)\r
+ \r
+ @staticmethod\r
+ def decode_frame(data: bytes) -> tuple:\r
+ """\r
+ 解码WebSocket帧\r
+ \r
+ Args:\r
+ data: 帧数据\r
+ \r
+ Returns:\r
+ tuple: (opcode, payload, fin, remaining_data)\r
+ """\r
+ if len(data) < 2:\r
+ return None, None, None, data\r
+ \r
+ first_byte = data[0]\r
+ second_byte = data[1]\r
+ \r
+ fin = (first_byte & 0x80) != 0\r
+ opcode = first_byte & 0x0F\r
+ masked = (second_byte & 0x80) != 0\r
+ payload_len = second_byte & 0x7F\r
+ \r
+ header_len = 2\r
+ \r
+ # 解析扩展长度字段\r
+ if payload_len == 126:\r
+ if len(data) < 4:\r
+ return None, None, None, data\r
+ payload_len = struct.unpack('!H', data[2:4])[0]\r
+ header_len = 4\r
+ elif payload_len == 127:\r
+ if len(data) < 10:\r
+ return None, None, None, data\r
+ payload_len = struct.unpack('!Q', data[2:10])[0]\r
+ header_len = 10\r
+ \r
+ # 解析掩码\r
+ mask_key = None\r
+ if masked:\r
+ if len(data) < header_len + 4:\r
+ return None, None, None, data\r
+ mask_key = data[header_len:header_len + 4]\r
+ header_len += 4\r
+ \r
+ # 检查是否有足够的负载数据\r
+ if len(data) < header_len + payload_len:\r
+ return None, None, None, data\r
+ \r
+ # 提取负载\r
+ payload = data[header_len:header_len + payload_len]\r
+ \r
+ # 去除掩码(服务器发送的数据不应该有掩码,但为了兼容性处理)\r
+ if masked and mask_key:\r
+ payload = bytearray(payload)\r
+ for i in range(len(payload)):\r
+ payload[i] ^= mask_key[i % 4]\r
+ payload = bytes(payload)\r
+ \r
+ remaining_data = data[header_len + payload_len:]\r
+ \r
+ return opcode, payload, fin, remaining_data\r
+\r
+\r
+class ATWebSocketClient:\r
+ """基于AT Socket的WebSocket客户端"""\r
+ \r
+ def __init__(self, at_handler: ATCommandHandler, socket_id: int = 0):\r
+ """\r
+ 初始化WebSocket客户端\r
+ \r
+ Args:\r
+ at_handler: ATCommandHandler实例\r
+ socket_id: Socket ID(0-5),默认0\r
+ """\r
+ self.at_handler = at_handler\r
+ self.socket_id = socket_id\r
+ self.sock: Optional[ATSocket] = None\r
+ self.connected = False\r
+ self.url = None\r
+ self.host = None\r
+ self.port = None\r
+ self.path = None\r
+ self.use_ssl = False\r
+ self.ssl_config = None\r
+ \r
+ # 回调函数\r
+ self.on_error: Optional[Callable[[Exception], None]] = None\r
+ self.on_close: Optional[Callable[[int, str], None]] = None\r
+ self.on_open: Optional[Callable[[], None]] = None\r
+ \r
+ # 接收缓冲区\r
+ self._recv_buffer = b""\r
+ \r
+ def set_ssl_config(self, ssl_version=3, auth_mode=0, ca_cert=None, client_cert=None, client_key=None):\r
+ """\r
+ 设置SSL配置(用于wss连接)\r
+ \r
+ Args:\r
+ ssl_version: SSL版本,0=SSL3.0, 1=TLS1.0, 2=TLS1.1, 3=TLS1.2, 4=TLS1.3\r
+ auth_mode: 认证模式,0=不认证, 1=单向认证, 2=双向认证\r
+ ca_cert: CA证书文件名(单向/双向认证需要)\r
+ client_cert: 客户端证书文件名(双向认证需要)\r
+ client_key: 客户端密钥文件名(双向认证需要)\r
+ """\r
+ self.ssl_config = {\r
+ 'ssl_version': ssl_version,\r
+ 'auth_mode': auth_mode,\r
+ 'ca_cert': ca_cert,\r
+ 'client_cert': client_cert,\r
+ 'client_key': client_key\r
+ }\r
+ \r
+ def connect(self, url: str, headers: Optional[Dict[str, str]] = None, timeout: Optional[float] = None) -> bool:\r
+ """\r
+ 连接到WebSocket服务器\r
+ \r
+ Args:\r
+ url: WebSocket URL(ws://或wss://)\r
+ headers: 额外的HTTP头\r
+ timeout: 连接超时时间(秒)\r
+ \r
+ Returns:\r
+ bool: 是否连接成功\r
+ """\r
+ try:\r
+ # 解析URL\r
+ parsed = urllib.parse.urlparse(url)\r
+ \r
+ if parsed.scheme not in ('ws', 'wss'):\r
+ raise ValueError(f"不支持的协议: {parsed.scheme},仅支持ws和wss")\r
+ \r
+ self.url = url\r
+ self.use_ssl = (parsed.scheme == 'wss')\r
+ self.host = parsed.hostname\r
+ self.port = parsed.port or (443 if self.use_ssl else 80)\r
+ self.path = parsed.path or '/'\r
+ if parsed.query:\r
+ self.path += '?' + parsed.query\r
+ \r
+ # 创建socket连接\r
+ if self.use_ssl:\r
+ if not self.ssl_config:\r
+ # 默认SSL配置\r
+ self.ssl_config = {\r
+ 'ssl_version': 3, # TLS1.2\r
+ 'auth_mode': 0,\r
+ 'ca_cert': None,\r
+ 'client_cert': None,\r
+ 'client_key': None\r
+ }\r
+ \r
+ self.sock = create_connection(\r
+ address=(self.host, self.port),\r
+ timeout=timeout,\r
+ at_handler=self.at_handler,\r
+ socket_id=self.socket_id,\r
+ use_ssl=True,\r
+ ssl_config=self.ssl_config\r
+ )\r
+ else:\r
+ self.sock = create_connection(\r
+ address=(self.host, self.port),\r
+ timeout=timeout,\r
+ at_handler=self.at_handler,\r
+ socket_id=self.socket_id,\r
+ use_ssl=False\r
+ )\r
+ \r
+ # 执行WebSocket握手\r
+ if not self._handshake(headers):\r
+ self.sock.close()\r
+ self.sock = None\r
+ return False\r
+ \r
+ self.connected = True\r
+ \r
+ # 调用on_open回调\r
+ if self.on_open:\r
+ try:\r
+ self.on_open()\r
+ except Exception as e:\r
+ if self.on_error:\r
+ self.on_error(e)\r
+ \r
+ return True\r
+ \r
+ except Exception as e:\r
+ if self.on_error:\r
+ self.on_error(e)\r
+ if self.sock:\r
+ self.sock.close()\r
+ self.sock = None\r
+ return False\r
+ \r
+ def _handshake(self, extra_headers: Optional[Dict[str, str]] = None) -> bool:\r
+ """\r
+ 执行WebSocket握手\r
+ \r
+ Args:\r
+ extra_headers: 额外的HTTP头\r
+ \r
+ Returns:\r
+ bool: 握手是否成功\r
+ """\r
+ # 生成WebSocket Key\r
+ import random\r
+ import string\r
+ key = base64.b64encode(\r
+ ''.join(random.choices(string.ascii_letters + string.digits, k=16)).encode()\r
+ ).decode()\r
+ \r
+ # 构建握手请求\r
+ request_lines = [\r
+ f"GET {self.path} HTTP/1.1",\r
+ f"Host: {self.host}:{self.port}",\r
+ "Upgrade: websocket",\r
+ "Connection: Upgrade",\r
+ f"Sec-WebSocket-Key: {key}",\r
+ "Sec-WebSocket-Version: 13",\r
+ ]\r
+ \r
+ # 添加额外头部\r
+ if extra_headers:\r
+ for name, value in extra_headers.items():\r
+ request_lines.append(f"{name}: {value}")\r
+ \r
+ request_lines.append("") # 空行\r
+ request_lines.append("") # 空行\r
+ \r
+ request = "\r\n".join(request_lines)\r
+ request_bytes = request.encode('utf-8')\r
+ \r
+ # 发送握手请求\r
+ try:\r
+ self.sock.send(request_bytes)\r
+ except Exception as e:\r
+ print(f"发送握手请求失败: {e}")\r
+ return False\r
+ \r
+ # 接收握手响应\r
+ response = b""\r
+ timeout = self.sock.gettimeout() or 30\r
+ start_time = time.time()\r
+ \r
+ while time.time() - start_time < timeout:\r
+ try:\r
+ chunk = self.sock.recv(4096)\r
+ if not chunk:\r
+ break\r
+ response += chunk\r
+ \r
+ # 检查是否收到完整的HTTP响应(以\r\n\r\n结尾)\r
+ if b"\r\n\r\n" in response:\r
+ break\r
+ except Exception as e:\r
+ print(f"接收握手响应失败: {e}")\r
+ return False\r
+ \r
+ if not response:\r
+ print("未收到握手响应")\r
+ return False\r
+ \r
+ # 解析响应\r
+ response_text = response.decode('utf-8', errors='ignore')\r
+ lines = response_text.split('\r\n')\r
+ \r
+ # 检查状态行\r
+ if not lines[0].startswith('HTTP/1.1 101'):\r
+ print(f"握手失败,状态行: {lines[0]}")\r
+ return False\r
+ \r
+ # 检查Upgrade和Connection头\r
+ headers = {}\r
+ for line in lines[1:]:\r
+ if ':' in line:\r
+ name, value = line.split(':', 1)\r
+ headers[name.strip().lower()] = value.strip()\r
+ \r
+ if headers.get('upgrade', '').lower() != 'websocket':\r
+ print("握手失败,缺少Upgrade头")\r
+ return False\r
+ \r
+ if 'upgrade' not in headers.get('connection', '').lower():\r
+ print("握手失败,Connection头不正确")\r
+ return False\r
+ \r
+ # 验证Sec-WebSocket-Accept\r
+ expected_accept = base64.b64encode(\r
+ hashlib.sha1((key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode()).digest()\r
+ ).decode()\r
+ \r
+ actual_accept = headers.get('sec-websocket-accept', '')\r
+ if actual_accept != expected_accept:\r
+ print(f"握手失败,Sec-WebSocket-Accept不匹配")\r
+ print(f"期望: {expected_accept}")\r
+ print(f"实际: {actual_accept}")\r
+ return False\r
+ \r
+ print("✅ WebSocket握手成功")\r
+ return True\r
+ \r
+ def send(self, data: bytes, opcode: int = WebSocketFrame.OPCODE_BINARY) -> bool:\r
+ """\r
+ 发送WebSocket消息\r
+ \r
+ Args:\r
+ data: 要发送的数据(bytes)\r
+ opcode: 操作码(OPCODE_TEXT或OPCODE_BINARY)\r
+ \r
+ Returns:\r
+ bool: 是否发送成功\r
+ """\r
+ if not self.connected or not self.sock:\r
+ return False\r
+ \r
+ try:\r
+ frame = WebSocketFrame.encode_frame(data, opcode=opcode, fin=True, mask=True)\r
+ self.sock.send(frame)\r
+ return True\r
+ except Exception as e:\r
+ if self.on_error:\r
+ self.on_error(e)\r
+ return False\r
+ \r
+ def send_text(self, text: str) -> bool:\r
+ """\r
+ 发送文本消息\r
+ \r
+ Args:\r
+ text: 要发送的文本\r
+ \r
+ Returns:\r
+ bool: 是否发送成功\r
+ """\r
+ return self.send(text.encode('utf-8'), opcode=WebSocketFrame.OPCODE_TEXT)\r
+ \r
+ def send_binary(self, data: bytes) -> bool:\r
+ """\r
+ 发送二进制消息\r
+ \r
+ Args:\r
+ data: 要发送的二进制数据\r
+ \r
+ Returns:\r
+ bool: 是否发送成功\r
+ """\r
+ return self.send(data, opcode=WebSocketFrame.OPCODE_BINARY)\r
+ \r
+ def ping(self, payload: bytes = b"") -> bool:\r
+ """\r
+ 发送Ping帧\r
+ \r
+ Args:\r
+ payload: Ping负载(可选)\r
+ \r
+ Returns:\r
+ bool: 是否发送成功\r
+ """\r
+ return self.send(payload, opcode=WebSocketFrame.OPCODE_PING)\r
+ \r
+ def pong(self, payload: bytes = b"") -> bool:\r
+ """\r
+ 发送Pong帧\r
+ \r
+ Args:\r
+ payload: Pong负载(可选)\r
+ \r
+ Returns:\r
+ bool: 是否发送成功\r
+ """\r
+ return self.send(payload, opcode=WebSocketFrame.OPCODE_PONG)\r
+ \r
+ def recv(self, timeout: Optional[float] = None) -> Optional[bytes]:\r
+ """\r
+ 接收WebSocket消息(阻塞模式)\r
+ \r
+ Args:\r
+ timeout: 超时时间(秒)\r
+ \r
+ Returns:\r
+ bytes: 接收到的消息,如果超时或连接关闭返回None\r
+ """\r
+ if not self.connected or not self.sock:\r
+ return None\r
+ \r
+ original_timeout = self.sock.gettimeout()\r
+ try:\r
+ if timeout is not None:\r
+ self.sock.settimeout(timeout)\r
+ \r
+ # 处理缓冲区中的数据\r
+ if self._recv_buffer:\r
+ opcode, payload, fin, remaining = WebSocketFrame.decode_frame(self._recv_buffer)\r
+ if payload is not None:\r
+ self._recv_buffer = remaining\r
+ return payload\r
+ \r
+ # 接收新数据\r
+ while True:\r
+ data = self.sock.recv(4096)\r
+ if not data:\r
+ return None\r
+ \r
+ self._recv_buffer += data\r
+ \r
+ # 尝试解码帧\r
+ opcode, payload, fin, remaining = WebSocketFrame.decode_frame(self._recv_buffer)\r
+ \r
+ if payload is not None:\r
+ self._recv_buffer = remaining\r
+ \r
+ # 处理控制帧\r
+ if opcode == WebSocketFrame.OPCODE_CLOSE:\r
+ self.connected = False\r
+ return None\r
+ elif opcode == WebSocketFrame.OPCODE_PING:\r
+ # 自动回复Pong\r
+ self.pong(payload)\r
+ continue\r
+ elif opcode == WebSocketFrame.OPCODE_PONG:\r
+ # 忽略Pong\r
+ continue\r
+ else:\r
+ return payload\r
+ else:\r
+ # 数据不完整,继续接收\r
+ continue\r
+ \r
+ except Exception as e:\r
+ if self.on_error:\r
+ self.on_error(e)\r
+ return None\r
+ finally:\r
+ if timeout is not None:\r
+ self.sock.settimeout(original_timeout)\r
+ \r
+ def recv_text(self, timeout: Optional[float] = None) -> Optional[str]:\r
+ """\r
+ 接收文本消息\r
+ \r
+ Args:\r
+ timeout: 超时时间(秒)\r
+ \r
+ Returns:\r
+ str: 接收到的文本,如果超时或连接关闭返回None\r
+ """\r
+ data = self.recv(timeout)\r
+ if data is None:\r
+ return None\r
+ return data.decode('utf-8')\r
+ \r
+ def close(self, code: int = 1000, reason: str = "Normal closure"):\r
+ """\r
+ 关闭WebSocket连接\r
+ \r
+ Args:\r
+ code: 关闭码\r
+ reason: 关闭原因\r
+ """\r
+ if not self.connected:\r
+ return\r
+ \r
+ try:\r
+ # 发送关闭帧\r
+ close_payload = struct.pack('!H', code) + reason.encode('utf-8')\r
+ self.send(close_payload, opcode=WebSocketFrame.OPCODE_CLOSE)\r
+ except:\r
+ pass\r
+ \r
+ self.connected = False\r
+ \r
+ if self.sock:\r
+ self.sock.close()\r
+ self.sock = None\r
+ \r
+ # 调用on_close回调\r
+ if self.on_close:\r
+ try:\r
+ self.on_close(code, reason)\r
+ except Exception as e:\r
+ if self.on_error:\r
+ self.on_error(e)\r
+\r
+++ /dev/null
-#!/usr/bin/env python3\r
-# -*- coding: utf-8 -*-\r
-"""\r
-使用AT指令通过HTTPS下载MP4文件\r
-参考 get_device_token_at.py 实现\r
-"""\r
-\r
-import serial\r
-import time\r
-import re\r
-import urllib.parse\r
-import os\r
-from get_device_token_at import ATCommandHandler, HTTPSATClient, HTTPATClient\r
-\r
-# -------------------------- 1. 配置参数 --------------------------\r
-# 目标文件URL\r
-MP4_URL = "https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/RoundEyes/Blue/Angry.mp4"\r
-# MP4_URL = "http://snicker.com.cn:8089/LEINIAO/blue/angry.zip"\r
-\r
-# 保存路径\r
-OUTPUT_DIR = "downloads"\r
-OUTPUT_FILENAME = "Angry.mp4"\r
-\r
-# AT模块串口配置(根据实际硬件配置修改)\r
-SERIAL_PORT = "COM19" # 修改为实际的串口设备\r
-SERIAL_BAUDRATE = 2000000\r
-SERIAL_TIMEOUT = 10\r
-\r
-# 认证模式配置\r
-AUTH_MODE = 1 # 0=无需认证, 1=单向认证, 2=双向认证\r
-# CA_CERT_PATH = None # 例如: "certs/ca.crt"\r
-CA_CERT_PATH = "certs/ca_chain.pem"\r
-\r
-# -------------------------- 2. 下载MP4文件函数 --------------------------\r
-def download_mp4_file(url, output_path, auth_mode=0, ca_cert_path=None):\r
- """\r
- 使用AT指令通过HTTP/HTTPS下载MP4文件\r
- \r
- Args:\r
- url: MP4文件的HTTP/HTTPS URL\r
- output_path: 保存路径\r
- auth_mode: 认证模式,0=无需认证, 1=单向认证, 2=双向认证(仅HTTPS需要)\r
- ca_cert_path: CA证书路径(HTTPS单向认证时需要)\r
- """\r
- # 解析URL,判断协议类型\r
- parsed_url = urllib.parse.urlparse(url)\r
- is_https = parsed_url.scheme.lower() == 'https'\r
- is_http = parsed_url.scheme.lower() == 'http'\r
- \r
- if not (is_http or is_https):\r
- raise ValueError(f"不支持的URL协议: {parsed_url.scheme},仅支持 http:// 或 https://")\r
- \r
- protocol_name = "HTTPS" if is_https else "HTTP"\r
- print(f"检测到协议类型: {protocol_name}")\r
- # 初始化AT指令处理器\r
- at_handler = ATCommandHandler(SERIAL_PORT, SERIAL_BAUDRATE, SERIAL_TIMEOUT)\r
- \r
- try:\r
- # 连接串口\r
- if not at_handler.connect():\r
- print("❌ 无法连接串口,请检查串口配置")\r
- return False\r
- \r
- # 测试AT连接\r
- success, response = at_handler.send_at_command("AT", timeout=3)\r
- if not success:\r
- print("❌ AT模块无响应,请检查连接")\r
- return False\r
- print("✅ AT模块连接正常")\r
- \r
- # 检查PPP拨号状态\r
- print("\n" + "=" * 50)\r
- print("【步骤1: 检查PPP拨号状态】")\r
- print("=" * 50)\r
- is_ppp_connected, ppp_ip = at_handler.check_ppp_status()\r
- \r
- if is_ppp_connected:\r
- print(f"✅ PPP已连接,IP地址: {ppp_ip}")\r
- else:\r
- print(f"❌ PPP未连接,IP地址: {ppp_ip or '未知'}")\r
- print("\n尝试建立PPP连接...")\r
- \r
- apn_name = None\r
- success, ip = at_handler.establish_ppp_connection(apn_name=apn_name)\r
- \r
- if not success:\r
- print(f"❌ PPP连接建立失败,无法继续{protocol_name}访问")\r
- return False\r
- \r
- ppp_ip = ip\r
- \r
- # 根据协议类型初始化客户端\r
- if is_https:\r
- # 初始化HTTPS客户端\r
- http_client = HTTPSATClient(at_handler)\r
- \r
- # 处理证书(如果需要)\r
- ca_cert_name = None\r
- if auth_mode == 1 and ca_cert_path:\r
- print("\n" + "=" * 50)\r
- print("【步骤2: 处理证书】")\r
- print("=" * 50)\r
- \r
- ca_cert_name = os.path.basename(ca_cert_path)\r
- \r
- # 检查证书是否已存在\r
- if http_client.certificate_exists(ca_cert_name):\r
- print(f"✅ 证书 {ca_cert_name} 已存在,跳过添加")\r
- else:\r
- print(f"证书 {ca_cert_name} 不存在,开始添加...")\r
- from get_device_token_at import load_certificate_file\r
- \r
- ca_cert_data = load_certificate_file(ca_cert_path)\r
- if not ca_cert_data:\r
- print("❌ 无法加载CA证书,终止流程")\r
- return False\r
- \r
- if not http_client.add_certificate(ca_cert_name, ca_cert_data):\r
- print("❌ CA证书添加失败,终止流程")\r
- return False\r
- \r
- if not http_client.check_certificate(ca_cert_name):\r
- print("❌ CA证书确认失败,终止流程")\r
- return False\r
- \r
- # ========== 开始计时:HTTPS访问耗时 ==========\r
- access_start_time = time.time()\r
- \r
- # 配置HTTPS参数\r
- print("\n" + "=" * 50)\r
- print("【步骤3: 配置HTTPS参数】")\r
- print("=" * 50)\r
- if not http_client.configure_https(\r
- ssl_version=3, # TLS1.2\r
- auth_mode=auth_mode,\r
- ca_cert=ca_cert_name\r
- ):\r
- print("❌ HTTPS配置失败,终止流程")\r
- return False\r
- \r
- # 确认HTTPS配置\r
- print("\n确认HTTPS配置...")\r
- config = http_client.verify_https_config()\r
- if config:\r
- print("✅ HTTPS配置确认成功")\r
- else:\r
- print("⚠️ HTTPS配置确认失败,但继续执行")\r
- \r
- # 设置URL\r
- print("\n" + "=" * 50)\r
- print("【步骤4: 设置HTTPS请求参数】")\r
- print("=" * 50)\r
- print(f"设置请求URL: {url}")\r
- http_client.set_url(url)\r
- \r
- # 建立HTTPS连接\r
- print("\n" + "=" * 50)\r
- print("【步骤5: 建立HTTPS连接】")\r
- print("=" * 50)\r
- http_client.setup_connection()\r
- print("✅ HTTPS连接建立成功")\r
- \r
- # 发送GET请求\r
- print("\n" + "=" * 50)\r
- print("【步骤6: 发送HTTPS GET请求】")\r
- print("=" * 50)\r
- \r
- # 使用标准GET模式 (mode=0)\r
- success, response = at_handler.send_at_command("AT+HTTPSACTION=0", timeout=5)\r
- if not success:\r
- raise Exception(f"发送HTTPS请求失败: {response}")\r
- \r
- # 等待响应数据\r
- print("等待HTTPS响应...")\r
- recv_indicator = b"+HTTPSRECV:"\r
- else:\r
- # 初始化HTTP客户端\r
- http_client = HTTPATClient(at_handler)\r
- \r
- # ========== 开始计时:HTTP访问耗时 ==========\r
- access_start_time = time.time()\r
- \r
- # 设置URL\r
- print("\n" + "=" * 50)\r
- print("【步骤3: 设置HTTP请求参数】")\r
- print("=" * 50)\r
- print(f"设置请求URL: {url}")\r
- http_client.set_url(url)\r
- \r
- # 建立HTTP连接\r
- print("\n" + "=" * 50)\r
- print("【步骤4: 建立HTTP连接】")\r
- print("=" * 50)\r
- http_client.setup_connection()\r
- print("✅ HTTP连接建立成功")\r
- \r
- # 发送GET请求\r
- print("\n" + "=" * 50)\r
- print("【步骤5: 发送HTTP GET请求】")\r
- print("=" * 50)\r
- \r
- # 使用标准GET模式 (mode=0)\r
- success, response = at_handler.send_at_command("AT+HTTPACTION=0", timeout=5)\r
- if not success:\r
- raise Exception(f"发送HTTP请求失败: {response}")\r
- \r
- # 等待响应数据\r
- print("等待HTTP响应...")\r
- recv_indicator = b"+HTTPRECV:"\r
- \r
- # 重要:直接读取二进制数据,不使用wait_for_unsolicited(它会解码为文本,可能丢失二进制数据)\r
- # 手动等待+HTTPRECV:/+HTTPSRECV:提示符,然后立即开始读取二进制数据\r
- response_data = b""\r
- start_time = time.time()\r
- last_data_time = time.time()\r
- header_received = False\r
- content_length = None\r
- body_start_pos = None\r
- recv_found = False\r
- \r
- # 等待+HTTPRECV:/+HTTPSRECV:提示符\r
- indicator_name = protocol_name + "RECV"\r
- print(f"等待+{indicator_name}:提示符...")\r
- while time.time() - start_time < 60:\r
- if at_handler.serial_conn.in_waiting > 0:\r
- # 读取数据(二进制模式)\r
- chunk = at_handler.serial_conn.read(at_handler.serial_conn.in_waiting)\r
- if chunk:\r
- response_data += chunk\r
- last_data_time = time.time()\r
- \r
- # 检查是否包含+HTTPRECV:/+HTTPSRECV:(可能在字节串中)\r
- if recv_indicator in response_data:\r
- recv_found = True\r
- print(f"✅ 收到+{indicator_name}:提示符")\r
- # 继续读取,查找HTTP响应头\r
- break\r
- time.sleep(0.1)\r
- \r
- if not recv_found:\r
- # 尝试查找HTTP响应头(可能+HTTPRECV:/+HTTPSRECV:已经被读取)\r
- if b"HTTP/" in response_data:\r
- recv_found = True\r
- print("✅ 检测到HTTP响应数据")\r
- \r
- if not recv_found:\r
- raise Exception(f"未收到+{indicator_name}:提示符或HTTP响应")\r
- \r
- # 查找HTTP响应头开始位置(可能在+HTTPSRECV:之后)\r
- http_start = response_data.find(b"HTTP/")\r
- if http_start == -1:\r
- # 如果没有找到HTTP/,继续读取直到找到\r
- print("继续读取数据以查找HTTP响应头...")\r
- while time.time() - start_time < 10:\r
- if at_handler.serial_conn.in_waiting > 0:\r
- chunk = at_handler.serial_conn.read(at_handler.serial_conn.in_waiting)\r
- if chunk:\r
- response_data += chunk\r
- http_start = response_data.find(b"HTTP/")\r
- if http_start != -1:\r
- break\r
- time.sleep(0.1)\r
- \r
- if http_start == -1:\r
- raise Exception("未找到HTTP响应头")\r
- \r
- # 从HTTP响应头开始位置处理数据\r
- # 跳过+HTTPSRECV:前缀(如果存在)\r
- if http_start > 0:\r
- print(f"跳过+HTTPSRECV:前缀 ({http_start} 字节)")\r
- response_data = response_data[http_start:]\r
- \r
- # 查找HTTP响应头结束位置(\r\n\r\n)\r
- print("读取响应头...")\r
- header_timeout = 10\r
- header_start = time.time()\r
- \r
- while time.time() - header_start < header_timeout:\r
- if at_handler.serial_conn.in_waiting > 0:\r
- chunk = at_handler.serial_conn.read(at_handler.serial_conn.in_waiting)\r
- if chunk:\r
- response_data += chunk\r
- \r
- # 查找HTTP响应头结束位置(\r\n\r\n)\r
- header_end = response_data.find(b"\r\n\r\n")\r
- if header_end != -1:\r
- header_received = True\r
- body_start_pos = header_end + 4\r
- \r
- # 解析响应头\r
- header_text = response_data[:header_end].decode('utf-8', errors='ignore')\r
- print("\nHTTP响应头:")\r
- print(header_text)\r
- \r
- # 提取Content-Length\r
- content_length_match = re.search(r'Content-Length:\s*(\d+)', header_text, re.IGNORECASE)\r
- if content_length_match:\r
- content_length = int(content_length_match.group(1))\r
- print(f"\n文件大小: {content_length} 字节 ({content_length/1024/1024:.2f} MB)")\r
- else:\r
- print("⚠️ 未找到Content-Length,将接收所有数据直到连接关闭")\r
- \r
- # 检查状态码\r
- status_match = re.search(r'HTTP/\d\.\d\s+(\d+)', header_text)\r
- if status_match:\r
- status_code = int(status_match.group(1))\r
- if status_code != 200:\r
- raise Exception(f"HTTP请求失败,状态码: {status_code}")\r
- print(f"HTTP状态码: {status_code} ✅")\r
- \r
- # 检查文件数据开始位置的前几个字节(用于调试)\r
- if len(response_data) > body_start_pos:\r
- preview = response_data[body_start_pos:body_start_pos+20]\r
- print(f"\n文件数据前20字节(hex): {preview.hex()}")\r
- print(f"文件数据前20字节(repr): {repr(preview)}")\r
- \r
- break\r
- time.sleep(0.1)\r
- \r
- if not header_received:\r
- raise Exception("未收到完整的HTTP响应头")\r
- \r
- # 继续接收文件数据\r
- print("\n开始接收文件数据...")\r
- last_progress_time = time.time() # 上次打印进度的时间\r
- progress_interval = 5 # 每5秒打印一次进度\r
- \r
- while time.time() - start_time < 600: # 最多等待10分钟\r
- if at_handler.serial_conn.in_waiting > 0:\r
- chunk = at_handler.serial_conn.read(at_handler.serial_conn.in_waiting)\r
- if chunk:\r
- response_data += chunk\r
- last_data_time = time.time()\r
- \r
- # 如果知道文件大小,显示进度\r
- if content_length:\r
- body_data = response_data[body_start_pos:]\r
- received = len(body_data)\r
- if received >= content_length:\r
- print(f"\n✅ 文件接收完成: {received} / {content_length} 字节")\r
- break\r
- else:\r
- # 每5秒打印一次进度\r
- current_time = time.time()\r
- if current_time - last_progress_time >= progress_interval:\r
- progress = (received / content_length) * 100\r
- print(f"接收进度: {progress:.1f}% ({received}/{content_length} 字节, {received/1024/1024:.2f} MB)")\r
- last_progress_time = current_time\r
- else:\r
- # 不知道文件大小,每5秒显示已接收的数据量\r
- current_time = time.time()\r
- if current_time - last_progress_time >= progress_interval:\r
- body_data = response_data[body_start_pos:]\r
- received = len(body_data)\r
- print(f"已接收: {received} 字节 ({received/1024/1024:.2f} MB)")\r
- last_progress_time = current_time\r
- else:\r
- # 如果超过10秒没有新数据,检查是否接收完成\r
- if time.time() - last_data_time > 10:\r
- if content_length:\r
- body_data = response_data[body_start_pos:]\r
- received = len(body_data)\r
- if received >= content_length:\r
- print(f"\n✅ 文件接收完成: {received} / {content_length} 字节")\r
- break\r
- else:\r
- print(f"\n⚠️ 接收超时,已接收: {received} / {content_length} 字节")\r
- break\r
- else:\r
- # 没有Content-Length,如果10秒没有新数据,认为接收完成\r
- print("\n响应数据接收完成(超时)")\r
- break\r
- time.sleep(0.1)\r
- \r
- # 提取文件数据(跳过HTTP响应头)\r
- file_data = response_data[body_start_pos:]\r
- \r
- # 验证文件数据(MP4文件格式)\r
- # MP4文件结构:Box size (4字节) + Box type (4字节,通常是"ftyp")\r
- # 所以文件应该以 0x00 0x00 0x00 0x1C 开头(或类似的大小值),然后是 "ftyp"\r
- print(f"\n文件数据前16字节(hex): {file_data[:16].hex()}")\r
- print(f"文件数据前16字节(repr): {repr(file_data[:16])}")\r
- \r
- # MP4文件应该以Box size开头(4字节大端整数),然后是"ftyp"\r
- # 检查前4字节是否为有效的Box size(通常不会太大,比如小于100MB)\r
- if len(file_data) >= 8:\r
- box_size_bytes = file_data[:4]\r
- box_type = file_data[4:8]\r
- \r
- # 计算Box size(大端序)\r
- box_size = int.from_bytes(box_size_bytes, byteorder='big')\r
- \r
- print(f"检测到的Box size: {box_size} (hex: {box_size_bytes.hex()})")\r
- print(f"检测到的Box type: {box_type} (hex: {box_type.hex()})")\r
- \r
- # 验证是否为有效的MP4文件\r
- if box_type == b"ftyp":\r
- print("✅ 检测到有效的MP4文件格式(ftyp box)")\r
- elif box_size > 0 and box_size < 100 * 1024 * 1024: # Box size在合理范围内\r
- print(f"⚠️ Box size看起来合理,但Box type不是'ftyp',可能是其他格式")\r
- else:\r
- # 如果前4字节不是有效的Box size,尝试查找ftyp\r
- ftyp_pos = file_data.find(b"ftyp")\r
- if ftyp_pos != -1 and ftyp_pos < 100:\r
- print(f"⚠️ 检测到文件数据偏移,ftyp在位置 {ftyp_pos}")\r
- print(f" 丢失了前 {ftyp_pos} 字节数据")\r
- print(f" 建议检查数据接收过程,确保没有丢失数据")\r
- # 不自动调整,保留原始数据\r
- # file_data = file_data[ftyp_pos:]\r
- \r
- # 保存文件\r
- print("\n" + "=" * 50)\r
- print("【步骤7: 保存文件】")\r
- print("=" * 50)\r
- \r
- # 创建输出目录\r
- os.makedirs(os.path.dirname(output_path), exist_ok=True)\r
- \r
- # 写入文件\r
- with open(output_path, 'wb') as f:\r
- f.write(file_data)\r
- \r
- file_size = len(file_data)\r
- print(f"✅ 文件保存成功: {output_path}")\r
- print(f" 文件大小: {file_size} 字节 ({file_size/1024/1024:.2f} MB)")\r
- \r
- # 关闭连接\r
- print("\n" + "=" * 50)\r
- print(f"【步骤8: 关闭{protocol_name}连接】")\r
- print("=" * 50)\r
- try:\r
- http_client.close_connection()\r
- print(f"✅ {protocol_name}连接已关闭")\r
- except Exception as e:\r
- print(f"警告: 关闭{protocol_name}连接时出错: {str(e)}")\r
- \r
- # ========== 结束计时:HTTP/HTTPS访问耗时 ==========\r
- access_end_time = time.time()\r
- access_duration = access_end_time - access_start_time\r
- \r
- print("\n" + "=" * 50)\r
- print(f"【{protocol_name}访问耗时统计】")\r
- print("=" * 50)\r
- print(f"{protocol_name}访问总耗时: {access_duration:.3f} 秒 ({access_duration*1000:.1f} 毫秒)")\r
- print(f"下载速度: {file_size/1024/1024/access_duration:.2f} MB/s")\r
- print("\n耗时包含以下步骤:")\r
- if is_https:\r
- print(" - HTTPS配置和验证")\r
- print(" - URL和端口设置")\r
- print(f" - {protocol_name}连接建立")\r
- print(" - HTTP请求发送")\r
- print(" - 文件数据接收")\r
- print(" - 连接关闭")\r
- print("=" * 50)\r
- \r
- return True\r
- \r
- except Exception as e:\r
- print("\n" + "=" * 50)\r
- print("【错误处理】")\r
- print("=" * 50)\r
- print(f"❌ 下载异常: {str(e)}")\r
- import traceback\r
- traceback.print_exc()\r
- return False\r
- finally:\r
- # 断开串口连接\r
- print("\n断开串口连接...")\r
- at_handler.disconnect()\r
-\r
-# -------------------------- 3. 主程序 --------------------------\r
-if __name__ == "__main__":\r
- print("=" * 50)\r
- print("HTTPS MP4文件下载工具(AT指令版本)")\r
- print("=" * 50)\r
- print(f"目标URL: {MP4_URL}")\r
- \r
- # 构建输出路径\r
- output_path = os.path.join(OUTPUT_DIR, OUTPUT_FILENAME)\r
- print(f"保存路径: {output_path}")\r
- print("=" * 50)\r
- \r
- # 执行下载\r
- success = download_mp4_file(\r
- url=MP4_URL,\r
- output_path=output_path,\r
- auth_mode=AUTH_MODE,\r
- ca_cert_path=CA_CERT_PATH\r
- )\r
- \r
- if success:\r
- print("\n✅ MP4文件下载完成!")\r
- else:\r
- print("\n❌ MP4文件下载失败!")\r
- exit(1)\r
-\r
+++ /dev/null
-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>,<ip>
- # 注意:手册说明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: <n>,<stat>
- # 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=<file_name>,<length>
- 2. 立即返回 CONNECT(不是OK)
- 3. 发送证书数据
- 4. 返回 +CERTADD: <length>,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: <length>,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: <file_name>,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: <file_name>,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: <sslversion>,<authmode>,<cacert>,<clientcert>,<clientkey>
- # 例如: +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<CRLF>
- http_request = f"GET {path} HTTP/1.1\r\n"
- # Host头:Host: hostname<CRLF>
- http_request += f"Host: {parsed.netloc}\r\n"
-
- # 添加自定义头(每行格式:Header-Name: value<CRLF>)
- for key, value in headers.items():
- http_request += f"{key}: {value}\r\n"
-
- # Connection头
- http_request += "Connection: close\r\n"
- # 请求头和请求体之间的空行(即使没有请求体也需要)<CRLF><CRLF>
- http_request += "\r\n"
-
- # 计算请求长度(字节数,用于AT+HTTPSACTION=99,<length>)
- request_length = len(http_request.encode('utf-8'))
-
- # 步骤1:直接发送AT+HTTPSACTION=99,<length>命令
- # 注意:根据手册,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协议,使用<CRLF>作为行结束符
- 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
- )
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,<length>命令
self.at.serial_conn.reset_input_buffer()
# 发送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响应
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,<length>命令
+ 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)
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
print(f"❌ TCP连接失败: {response}")
return False
- # 等待连接结果(+TCPSETUP: <n>,OK 或 +TCPSETUP: <n>,FAIL)
- tcpsetup_pattern = re.compile(r'\+TCPSETUP:\s*(\d+),(OK|FAIL|ERROR)')
+ # 等待连接结果(+TCPSETUP: <n>,OK 或 +TCPSETUP: <n>,FAIL 或 +TCPSETUP: <n>,ERROR[数字])
+ tcpsetup_pattern = re.compile(r'\+TCPSETUP:\s*(\d+),(OK|FAIL|ERROR\d*)')
match = tcpsetup_pattern.search(response)
if match:
status = match.group(2)
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)
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)
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=<n>,<binary_length>,,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: <n>,<length>)
+ # 注意:根据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: <n>,<length>
+ 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=<n>,<data_length>
+ # 注意:根据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("等待'>'提示符...")
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: <n>,OK 或 +SSLTCPSEND: <n>,FAIL)
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}")
def receive(self, max_length=2048, timeout=30):
"""
- 接收TCP/TCP+SSL数据(非透传模式)
+ æ\8e¥æ\94¶TCP/TCP+SSLæ\95°æ\8d®ï¼\88é\9d\9eé\80\8fä¼ æ¨¡å¼\8fï¼\8cHEX模å¼\8fï¼\89
Args:
max_length: 最大接收长度(1-2048字节),默认2048
timeout: 超时时间(秒),默认30
Returns:
- bytes: 接收到的数据,如果失败返回None
+ bytes: 接收到的二进制数据,如果失败返回None
"""
if not self.is_connected:
print("❌ 连接未建立,无法接收数据")
print(f"❌ 读取数据失败: {response}")
return None
- # 解析响应
+ # 解析响应(HEX模式)
if self.is_ssl:
- # +SSLTCPREAD: <n>,<len>,<data>
+ # +SSLTCPREAD: <n>,<hex_len>,<hex_data>
pattern = re.compile(r'\+SSLTCPREAD:\s*(\d+),(\d+),(.+)')
else:
- # +TCPREAD: <n>,<len>,<data>
+ # +TCPREAD: <n>,<hex_len>,<hex_data>
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()
- # å°\9dè¯\95è§£æ\9e\90æ\95°æ\8d®ï¼\88å\8f¯è\83½æ\98¯ASCIIæ\88\96HEXï¼\89
+ # å°\86HEXå\97符串转æ\8d¢ä¸ºäº\8cè¿\9bå\88¶æ\95°æ\8d®
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):
"""
- 被动等待接收数据(透传或非透传模式都支持)
+ 被å\8a¨ç\89å¾\85æ\8e¥æ\94¶æ\95°æ\8d®ï¼\88é\80\8fä¼ æ\88\96é\9d\9eé\80\8fä¼ æ¨¡å¼\8fé\83½æ\94¯æ\8c\81ï¼\8cHEX模å¼\8fï¼\89
Args:
timeout: 超时时间(秒),默认30
Returns:
- bytes: 接收到的数据,如果超时返回None
+ bytes: 接收到的二进制数据,如果超时返回None
"""
if not self.is_connected:
print("❌ 连接未建立,无法接收数据")
print(f"等待数据超时: {timeout}秒")
return None
- # 解析接收到的数据
+ # 解析接收到的数据(HEX模式)
if self.is_ssl:
- # +SSLTCPRECV: <n>,<len>,<data>
+ # +SSLTCPRECV: <n>,<hex_len>,<hex_data>
recv_pattern = re.compile(r'\+SSLTCPRECV:\s*(\d+),(\d+),(.+)')
else:
- # +TCPRECV: <n>,<len>,<data> 或 +TCPRECV: <n>
+ # +TCPRECV: <n>,<hex_len>,<hex_data> 或 +TCPRECV: <n>
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()
- # å°\9dè¯\95è§£æ\9e\90数据
+ # å°\86HEXå\97符串转æ\8d¢ä¸ºäº\8cè¿\9bå\88¶数据
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: <n>,<STATUS>[,<TYPE>,<send-buffer-size>]
+ # 示例: +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: <n>,<data_sent>,<acked_recv>
+ # 或: +TCPACK: <n>,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连接
--- /dev/null
+#!/usr/bin/env python3\r
+# -*- coding: utf-8 -*-\r
+"""\r
+TCP测试客户端(标准Python socket版本)\r
+使用标准Python socket接口连接TCP服务器\r
+支持交互式发送和接收数据\r
+"""\r
+\r
+import socket\r
+import time\r
+import threading\r
+import sys\r
+\r
+# -------------------------- 1. 配置参数 --------------------------\r
+# 服务器配置\r
+SERVER_HOST = "snicker.com.cn"\r
+SERVER_PORT = 8910\r
+\r
+# 接收超时时间(秒)\r
+RECEIVE_TIMEOUT = 30\r
+\r
+# -------------------------- 2. TCP客户端类 --------------------------\r
+class TCPTestClient:\r
+ """标准Python socket TCP客户端"""\r
+ \r
+ def __init__(self, host, port):\r
+ """\r
+ 初始化TCP客户端\r
+ \r
+ Args:\r
+ host: 服务器地址\r
+ port: 服务器端口\r
+ """\r
+ self.host = host\r
+ self.port = port\r
+ self.socket = None\r
+ self.connected = False\r
+ self.receive_thread = None\r
+ self.running = False\r
+ self.received_data_queue = []\r
+ self.data_lock = threading.Lock()\r
+ \r
+ def connect(self):\r
+ """\r
+ 连接到服务器\r
+ \r
+ Returns:\r
+ bool: 是否连接成功\r
+ """\r
+ try:\r
+ self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\r
+ self.socket.settimeout(5.0) # 连接超时5秒\r
+ \r
+ print(f"正在连接服务器: {self.host}:{self.port}...")\r
+ self.socket.connect((self.host, self.port))\r
+ \r
+ # 连接成功后,设置接收超时\r
+ self.socket.settimeout(RECEIVE_TIMEOUT)\r
+ self.connected = True\r
+ \r
+ # 启动接收线程\r
+ self.running = True\r
+ self.receive_thread = threading.Thread(target=self._receive_loop, daemon=True)\r
+ self.receive_thread.start()\r
+ \r
+ print(f"✅ TCP连接成功: {self.host}:{self.port}")\r
+ return True\r
+ \r
+ except socket.timeout:\r
+ print(f"❌ 连接超时: {self.host}:{self.port}")\r
+ return False\r
+ except socket.error as e:\r
+ print(f"❌ 连接失败: {e}")\r
+ return False\r
+ except Exception as e:\r
+ print(f"❌ 连接异常: {e}")\r
+ return False\r
+ \r
+ def _receive_loop(self):\r
+ """接收数据循环(后台线程)"""\r
+ while self.running and self.connected:\r
+ try:\r
+ data = self.socket.recv(4096)\r
+ if not data:\r
+ # 服务器关闭连接\r
+ print("\n⚠️ 服务器关闭了连接")\r
+ self.connected = False\r
+ break\r
+ \r
+ # 将接收到的数据放入队列\r
+ with self.data_lock:\r
+ self.received_data_queue.append(data)\r
+ \r
+ except socket.timeout:\r
+ # 超时是正常的,继续等待\r
+ continue\r
+ except socket.error as e:\r
+ if self.running:\r
+ print(f"\n❌ 接收数据时出错: {e}")\r
+ self.connected = False\r
+ break\r
+ except Exception as e:\r
+ if self.running:\r
+ print(f"\n❌ 接收数据异常: {e}")\r
+ self.connected = False\r
+ break\r
+ \r
+ def send(self, data):\r
+ """\r
+ 发送数据\r
+ \r
+ Args:\r
+ data: 要发送的数据(bytes)\r
+ \r
+ Returns:\r
+ bool: 是否发送成功\r
+ """\r
+ if not self.connected:\r
+ print("❌ 连接未建立,无法发送数据")\r
+ return False\r
+ \r
+ try:\r
+ self.socket.sendall(data)\r
+ return True\r
+ except socket.error as e:\r
+ print(f"❌ 发送数据失败: {e}")\r
+ self.connected = False\r
+ return False\r
+ except Exception as e:\r
+ print(f"❌ 发送数据异常: {e}")\r
+ return False\r
+ \r
+ def receive(self, timeout=None):\r
+ """\r
+ 主动接收数据(从队列中获取)\r
+ \r
+ Args:\r
+ timeout: 超时时间(秒),None表示立即返回\r
+ \r
+ Returns:\r
+ bytes: 接收到的数据,如果没有数据返回None\r
+ """\r
+ if timeout is None:\r
+ timeout = 0\r
+ \r
+ start_time = time.time()\r
+ while time.time() - start_time < timeout:\r
+ with self.data_lock:\r
+ if self.received_data_queue:\r
+ return self.received_data_queue.pop(0)\r
+ time.sleep(0.1)\r
+ \r
+ # 检查是否有数据\r
+ with self.data_lock:\r
+ if self.received_data_queue:\r
+ return self.received_data_queue.pop(0)\r
+ \r
+ return None\r
+ \r
+ def wait_for_data(self, timeout=30):\r
+ """\r
+ 被动等待接收数据\r
+ \r
+ Args:\r
+ timeout: 超时时间(秒)\r
+ \r
+ Returns:\r
+ bytes: 接收到的数据,超时返回None\r
+ """\r
+ start_time = time.time()\r
+ while time.time() - start_time < timeout:\r
+ with self.data_lock:\r
+ if self.received_data_queue:\r
+ return self.received_data_queue.pop(0)\r
+ \r
+ if not self.connected:\r
+ return None\r
+ \r
+ time.sleep(0.1)\r
+ \r
+ return None\r
+ \r
+ def close(self):\r
+ """关闭连接"""\r
+ self.running = False\r
+ self.connected = False\r
+ \r
+ if self.receive_thread and self.receive_thread.is_alive():\r
+ # 等待接收线程结束\r
+ self.receive_thread.join(timeout=1.0)\r
+ \r
+ if self.socket:\r
+ try:\r
+ self.socket.close()\r
+ except:\r
+ pass\r
+ \r
+ print("✅ TCP连接已关闭")\r
+\r
+# -------------------------- 3. 连接测试服务器函数 --------------------------\r
+def connect_test_server():\r
+ """连接测试服务器并交互式发送和接收消息"""\r
+ \r
+ # 初始化TCP客户端\r
+ print("\n" + "=" * 50)\r
+ print("【步骤1: 初始化TCP客户端】")\r
+ print("=" * 50)\r
+ tcp_client = TCPTestClient(SERVER_HOST, SERVER_PORT)\r
+ \r
+ try:\r
+ # 建立TCP连接\r
+ print("\n" + "=" * 50)\r
+ print("【步骤2: 建立TCP连接】")\r
+ print("=" * 50)\r
+ print(f"连接服务器: {SERVER_HOST}:{SERVER_PORT}")\r
+ \r
+ if not tcp_client.connect():\r
+ print(f"❌ TCP连接失败")\r
+ return False\r
+ \r
+ print(f"✅ TCP连接成功")\r
+ \r
+ # 交互式发送和接收\r
+ print("\n" + "=" * 50)\r
+ print("【步骤3: 交互式发送和接收数据】")\r
+ print("=" * 50)\r
+ print("命令说明:")\r
+ print(" send <hex_string> - 发送HEX数据(例如: send 48656C6C6F)")\r
+ print(" send_ascii <text> - 发送ASCII文本(例如: send_ascii Hello)")\r
+ print(" recv - 主动接收数据")\r
+ print(" wait - 切换到被动等待模式(自动接收)")\r
+ print(" interactive - 切换到交互模式(手动输入命令)")\r
+ print(" quit - 退出程序")\r
+ print("=" * 50)\r
+ \r
+ message_count = 0\r
+ start_time = time.time()\r
+ wait_mode = False # 默认交互模式,方便用户输入命令\r
+ \r
+ def print_received_data(data, msg_num, elapsed):\r
+ """打印接收到的数据"""\r
+ print(f"\n【接收消息 #{msg_num}】 (连接后 {elapsed:.1f}秒)")\r
+ print(f"数据长度: {len(data)} 字节")\r
+ \r
+ # 打印首字节信息\r
+ if len(data) > 0:\r
+ first_byte = data[0]\r
+ print(f"首字节: 0x{first_byte:02X} ({first_byte:3d})")\r
+ \r
+ # 打印完整数据 - HEX格式\r
+ hex_str = data.hex().upper()\r
+ if len(hex_str) > 200:\r
+ print(f"HEX格式(前200字符): {hex_str[:200]}...")\r
+ print(f"HEX格式(完整,共{len(hex_str)}字符):")\r
+ # 每行显示64个字符(32字节)\r
+ for i in range(0, len(hex_str), 64):\r
+ chunk = hex_str[i:i+64]\r
+ offset = i // 2\r
+ print(f" {offset:06X}: {chunk}")\r
+ else:\r
+ print(f"HEX格式: {hex_str}")\r
+ \r
+ # 打印完整数据 - ASCII格式\r
+ try:\r
+ ascii_str = data.decode('utf-8', errors='replace')\r
+ # 检查是否包含可打印字符\r
+ if all(32 <= ord(c) < 127 or c in '\n\r\t' for c in ascii_str):\r
+ print(f"ASCII格式: {repr(ascii_str)}")\r
+ else:\r
+ # 包含非打印字符,显示为repr格式\r
+ print(f"ASCII格式(含非打印字符): {repr(ascii_str)}")\r
+ # 同时显示可打印部分\r
+ printable = ''.join(c if 32 <= ord(c) < 127 or c in '\n\r\t' else '.' for c in ascii_str)\r
+ if printable.strip():\r
+ print(f"可打印部分: {printable}")\r
+ except:\r
+ print(f"ASCII格式: (无法解码为UTF-8)")\r
+ \r
+ # 打印数据的前几个字节(十六进制和十进制)\r
+ if len(data) > 0:\r
+ preview_bytes = min(16, len(data))\r
+ hex_preview = ' '.join(f'{b:02X}' for b in data[:preview_bytes])\r
+ dec_preview = ' '.join(f'{b:3d}' for b in data[:preview_bytes])\r
+ print(f"前{preview_bytes}字节 HEX: {hex_preview}")\r
+ print(f"前{preview_bytes}字节 DEC: {dec_preview}")\r
+ \r
+ print("-" * 50)\r
+ \r
+ try:\r
+ while True:\r
+ if wait_mode:\r
+ # 被动等待接收数据\r
+ data = tcp_client.wait_for_data(timeout=RECEIVE_TIMEOUT)\r
+ \r
+ if data:\r
+ message_count += 1\r
+ elapsed_time = time.time() - start_time\r
+ print_received_data(data, message_count, elapsed_time)\r
+ else:\r
+ if not tcp_client.connected:\r
+ print("\n⚠️ 连接已断开")\r
+ break\r
+ print(f"\n⚠️ 接收超时({RECEIVE_TIMEOUT}秒),继续等待...")\r
+ print("提示: 按 Ctrl+C 中断,然后输入 'interactive' 切换到交互模式")\r
+ print("-" * 50)\r
+ else:\r
+ # 交互模式:等待用户输入\r
+ try:\r
+ user_input = input("\n请输入命令 (send/send_ascii/recv/wait/interactive/quit): ").strip()\r
+ \r
+ if not user_input:\r
+ continue\r
+ \r
+ if user_input.lower() == 'quit':\r
+ break\r
+ elif user_input.lower() == 'recv':\r
+ # 主动接收数据\r
+ data = tcp_client.receive(timeout=0.1)\r
+ if data:\r
+ message_count += 1\r
+ elapsed_time = time.time() - start_time\r
+ print_received_data(data, message_count, elapsed_time)\r
+ else:\r
+ print("❌ 未接收到数据")\r
+ elif user_input.lower() == 'wait':\r
+ wait_mode = True\r
+ print("✅ 切换到被动等待模式(自动接收数据)")\r
+ print("提示: 按 Ctrl+C 中断,然后输入 'interactive' 切换回交互模式")\r
+ elif user_input.lower() == 'interactive':\r
+ wait_mode = False\r
+ print("✅ 切换到交互模式(手动输入命令)")\r
+ elif user_input.startswith('send '):\r
+ # 发送HEX数据\r
+ hex_str = user_input[5:].strip().replace(' ', '').upper()\r
+ if not hex_str:\r
+ print("❌ 错误: send 命令需要HEX字符串参数")\r
+ print(" 示例: send 48656C6C6F")\r
+ continue\r
+ try:\r
+ send_data = bytes.fromhex(hex_str)\r
+ print(f"发送HEX数据: {hex_str} (二进制长度: {len(send_data)} 字节)")\r
+ if tcp_client.send(send_data):\r
+ print("✅ 数据发送成功")\r
+ else:\r
+ print("❌ 数据发送失败")\r
+ except ValueError as e:\r
+ print(f"❌ HEX字符串格式错误: {e}")\r
+ print(" 提示: HEX字符串只能包含 0-9, A-F 字符")\r
+ elif user_input.startswith('send_ascii '):\r
+ # 发送ASCII文本\r
+ ascii_text = user_input[11:].strip()\r
+ if not ascii_text:\r
+ print("❌ 错误: send_ascii 命令需要文本参数")\r
+ print(" 示例: send_ascii Hello World")\r
+ continue\r
+ send_data = ascii_text.encode('utf-8')\r
+ hex_str = send_data.hex().upper()\r
+ print(f"发送ASCII文本: {ascii_text}")\r
+ print(f" -> 二进制: {send_data}")\r
+ print(f" -> HEX字符串: {hex_str}")\r
+ if tcp_client.send(send_data):\r
+ print("✅ 数据发送成功")\r
+ else:\r
+ print("❌ 数据发送失败")\r
+ else:\r
+ print("❌ 未知命令")\r
+ print(" 可用命令: send, send_ascii, recv, wait, interactive, quit")\r
+ except EOFError:\r
+ # 如果没有交互式输入,切换到被动等待模式\r
+ wait_mode = True\r
+ print("\n⚠️ 检测到非交互式环境,切换到被动等待模式...")\r
+ except KeyboardInterrupt:\r
+ # 用户按 Ctrl+C,切换到交互模式\r
+ wait_mode = False\r
+ print("\n\n⚠️ 用户中断,切换到交互模式")\r
+ print("提示: 输入 'wait' 可切换回被动等待模式")\r
+ \r
+ except KeyboardInterrupt:\r
+ print("\n\n用户中断")\r
+ \r
+ # 关闭连接\r
+ print("\n" + "=" * 50)\r
+ print("【步骤4: 关闭TCP连接】")\r
+ print("=" * 50)\r
+ tcp_client.close()\r
+ \r
+ # 统计信息\r
+ total_time = time.time() - start_time\r
+ print("\n" + "=" * 50)\r
+ print("【连接统计】")\r
+ print("=" * 50)\r
+ print(f"连接持续时间: {total_time:.1f} 秒")\r
+ print(f"接收消息数量: {message_count}")\r
+ if message_count > 0:\r
+ print(f"平均消息间隔: {total_time/message_count:.1f} 秒")\r
+ print("=" * 50)\r
+ \r
+ return True\r
+ \r
+ except Exception as e:\r
+ print("\n" + "=" * 50)\r
+ print("【错误处理】")\r
+ print("=" * 50)\r
+ print(f"❌ 连接异常: {str(e)}")\r
+ import traceback\r
+ traceback.print_exc()\r
+ return False\r
+\r
+# -------------------------- 4. 主程序 --------------------------\r
+if __name__ == "__main__":\r
+ print("=" * 50)\r
+ print("TCP测试客户端(标准Python socket版本)")\r
+ print("=" * 50)\r
+ print(f"服务器地址: {SERVER_HOST}:{SERVER_PORT}")\r
+ print("=" * 50)\r
+ \r
+ # 执行连接\r
+ success = connect_test_server()\r
+ \r
+ if success:\r
+ print("\n✅ 连接测试完成!")\r
+ else:\r
+ print("\n❌ 连接测试失败!")\r
+ sys.exit(1)\r
+\r
\r
# 显示接收到的数据\r
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')\r
+ packet_size = len(data)\r
+ first_byte = data[0] if data else 0\r
+ \r
print(f"\n[{timestamp}] 📥 收到数据 [客户端ID: {client_id}, {address[0]}:{address[1]}]")\r
- print(f" 数据长度: {len(data)} 字节")\r
+ print(f"数据长度: {packet_size} 字节, 首字节: 0x{first_byte:02X} ({first_byte:3d})")\r
+ \r
+ # 打印完整数据 - HEX格式\r
+ hex_str = data.hex().upper()\r
+ if len(hex_str) > 200:\r
+ print(f"HEX格式(前200字符): {hex_str[:200]}...")\r
+ print(f"HEX格式(完整,共{len(hex_str)}字符):")\r
+ # 每行显示64个字符(32字节)\r
+ for i in range(0, len(hex_str), 64):\r
+ chunk = hex_str[i:i+64]\r
+ offset = i // 2\r
+ print(f" {offset:06X}: {chunk}")\r
+ else:\r
+ print(f"HEX格式: {hex_str}")\r
\r
- # 尝试以文本和十六进制显示\r
+ # 打印完整数据 - ASCII格式\r
try:\r
- text = data.decode('utf-8', errors='ignore')\r
- if text.isprintable() or '\n' in text or '\r' in text:\r
- print(f" 文本内容:")\r
- for line in text.splitlines():\r
- print(f" {line}")\r
+ ascii_str = data.decode('utf-8', errors='replace')\r
+ # 检查是否包含可打印字符\r
+ if all(32 <= ord(c) < 127 or c in '\n\r\t' for c in ascii_str):\r
+ print(f"ASCII格式: {repr(ascii_str)}")\r
else:\r
- print(f" 十六进制: {data.hex()}")\r
+ # 包含非打印字符,显示为repr格式\r
+ print(f"ASCII格式(含非打印字符): {repr(ascii_str)}")\r
+ # 同时显示可打印部分\r
+ printable = ''.join(c if 32 <= ord(c) < 127 or c in '\n\r\t' else '.' for c in ascii_str)\r
+ if printable.strip():\r
+ print(f"可打印部分: {printable}")\r
except:\r
- print(f" 十六进制: {data.hex()}")\r
+ print(f"ASCII格式: (无法解码为UTF-8)")\r
\r
- # æ\98¾ç¤ºå\89\8d64å\97è\8a\82ç\9a\84ASCII表示\r
- if len(data) <= 64:\r
- ascii_repr = ''.join(chr(b) if 32 <= b < 127 else '.' for b in data)\r
- print(f" ASCII: {ascii_repr}")\r
- else:\r
- ascii_repr = ''.join(chr(b) if 32 <= b < 127 else '.' for b in data[:64])\r
- print(f" ASCII (前64字节): {ascii_repr}...")\r
+ # æ\89\93å\8d°æ\95°æ\8d®ç\9a\84å\89\8då\87 个å\97è\8a\82ï¼\88å\8d\81å\85è¿\9bå\88¶å\92\8cå\8d\81è¿\9bå\88¶ï¼\89\r
+ if packet_size > 0:\r
+ preview_bytes = min(16, packet_size)\r
+ hex_preview = ' '.join(f'{b:02X}' for b in data[:preview_bytes])\r
+ dec_preview = ' '.join(f'{b:3d}' for b in data[:preview_bytes])\r
+ print(f"前{preview_bytes}字节 HEX: {hex_preview}")\r
+ print(f"前{preview_bytes}字节 DEC: {dec_preview}")\r
\r
- print()\r
+ print("-" * 60)\r
\r
except socket.timeout:\r
continue\r
--- /dev/null
+# 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脚本一致。
+
+
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
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");
*/
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
/**
* @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
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
-#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,<length> 的 <length> 最大长度为2048字节
+ * - AT+HTTPSACTION=99,<length> 的 <length> 最大长度为2048字节
+ *
+ * 注意:HTTP请求(包括头部和body)必须作为一个完整的报文发送,不能分片。
+ * 如果请求超过此限制,应该返回错误或使用其他方式发送(如TCP直接发送)。
+ */
+#define N706A_HTTP_MAX_REQUEST_LEN 2048 /**< HTTP发送请求最大长度(字节) */
+
/**
* @brief N706A HTTP请求类型
*/
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;
/**
/**
* @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请求
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客户端结构指针
*/
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);
+
/* ========== 辅助函数 ========== */
/**
*/
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变量值
*
*/
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模块波特率(在所有模式下)
*
*
* SSL实现方式:
* - N706A_SSL_USE_AT_COMMAND: 使用AT+SSLTCP*指令(默认,推荐)
- * - N706A_SSL_USE_MBEDTLS: 使用mbedtls库(需要额外库支持)
*/
#ifndef PPP_N706A_TCP_CLIENT_H
/**
* 编译时选择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
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;
/**
/**
* @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,
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指针
*/
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指针
*/
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
#include <stddef.h>
#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
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 */
bool running; /**< 接收线程运行标志 */
void *recv_thread; /**< 接收线程句柄 */
+ /* WebSocket 帧解析器 */
+ ws_parser_t *parser; /**< WebSocket 帧解析器实例 */
+ n706a_ws_opcode_t fragment_opcode; /**< 分片消息的初始 opcode(用于 CONTINUATION 帧) */
+
} n706a_websocket_client_t;
/**
*/
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
#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}
/* 调试配置 */
#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状态定义 */
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, ...)
#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 */
#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 <string.h>
#include <stdio.h>
#include <stdlib.h> // For malloc and free
+#include <stdint.h> // For UINT32_MAX
/* ========== AT响应异步缓冲区(由ppp_interrupt.c写入) ========== */
#define N706A_AT_RESPONSE_RING_BUFFER_SIZE 4096 // 4KB环形缓冲区
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;
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;
/* 网络状态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,
/* 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 数据指针
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: <socket_id>,<data_len>,<data>\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可以立即重用
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;
+ }
}
}
}
// 立即回调应用层(在中断上下文或处理线程中)
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)");
}
}
}
}
/* ========== 类别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: <len>,<data> 或 +HTTPSRECV: <len>,<data> */
+ /* 注意: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: <len>,<data> */
+ 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: ");
}
/**
- * @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 的响应格式是:<CR><LF>> <CR><content>
+ // 其中 "> " 后面的空格是提示符的一部分,必须保留
+ 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: <params>,<data_len>,<data>\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: <socket_id>,<data_len>,<data>\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: <data_len>,<data>
+ 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: <socket_id>,<sent_bytes>\r\n 或 +TCPSEND: <socket_id>,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: <socket_id> 或 +TCPCLOSE: <socket_id>,<reason>
+ // 格式:+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: <socket_id>,<sent_bytes> 格式,至少需要找到逗号后的数字
+ 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=<n>,<len>\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: <rxlev>,<ber>,<rscp>,<ecno>,<rsrq>,<rsrp>
*/
-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: <rxlev>,<ber>,<rscp>,<ecno>,<rsrq>,<rsrp>
+ 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: <socket_id>,<binary_len>,<ascii_data>\r\n
+ * 例如:+TCPRECV: 0,10,1234567890\r\n (10字节ASCII数据)
+ * 注意:<length>是二进制数据长度(10)
+ *
+ * URC格式(HEX模式):+TCPRECV: <socket_id>,<binary_len>,<hex_data>\r\n
+ * 例如:+TCPRECV: 0,3,313233\r\n (表示3字节二进制数据:0x31, 0x32, 0x33,HEX字符串长度为6)
+ * 注意:<length>是二进制数据长度(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: <socket_id>
+ * +TCPCLOSE: <socket_id>,Link Closed
+ * +TCPCLOSE: ERROR(错误格式,socket_id设为-1)
+ * 或:+SSLTCPCLOSE: <socket_id>
+ */
+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: <socket_id>,<sent_bytes>
+ * 或:+SSLTCPEND: <socket_id>,<sent_bytes>
*/
-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: <socket_id>,OK 或 +TCPSETUP: <socket_id>,FAIL
+ * 或:+SSLTCPSETUP: <socket_id>,OK 或 +SSLTCPSETUP: <socket_id>,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中声明的数据长度(从 <len> 字段解析,二进制长度)
+ * @param actual_len 输出:实际可用的数据长度(从数据开始位置到URC结束位置)
+ * @param data 输出:数据指针(指向 urc_data 内部)
+ * @return true=解析成功,false=失败
*
- * 该函数从ppp_interrupt.c填充的环形缓冲区中异步读取AT响应数据
- * 提供同步的函数接口,但底层是异步的高速串口接收
+ * URC格式(两种可能,由 AT+HTTPPARA=recvmode 决定):
+ * 1. recvmode=1:+HTTPRECV: <len>,<data> 或 +HTTPSRECV: <len>,<data> (带长度字段)
+ * 2. recvmode=0(默认):+HTTPRECV: <data> 或 +HTTPSRECV: <data> (无长度字段,直接是数据)
+ *
+ * 例如:
+ * +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中声明的 <len> 字段,表示当前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: <len>,<data> */
+ 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: <len>,<data> */
+ 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中声明的 <len> 作为 declared_len(这是最准确的) */
+ /* 根据用户确认:HTTPRECV 不支持分片,<length> 字段就是当前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: <data> (无长度字段) */
+ 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 è§£æ\9e\90AT+CESQå\93\8dåº\94
- * 参考N706A AT命令手册: +CESQ: <rxlev>,<ber>,<rscp>,<ecno>,<rsrq>,<rsrp>
+ * @brief è\8e·å\8f\96累积ç¼\93å\86²å\8cºé\95¿åº¦ï¼\88ç\94¨äº\8eè°\83è¯\95ï¼\89
+ * @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: <rxlev>,<ber>,<rscp>,<ecno>,<rsrq>,<rsrp>
- 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: <socket_id>,<data_len>,<data>\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;
+
+ /* 解析格式:<socket_id>,<data_len>,<data> */
+ 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;
+
+ /* 解析格式:<socket_id>,<data_len>,<data> */
+ 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: <socket_id>,<data_len>,<data> 或 +SSLTCPRECV: <socket_id>,<data_len>,<data> */
+ 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: <socket_id>,OK 或 +TCPSETUP: <socket_id>,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: <socket_id> 或 +SSLTCPCLOSE: <socket_id> */
+ 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: <socket_id>,<sent_bytes> 或 +SSLTCPEND: <socket_id>,<sent_bytes> */
+ 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: <socket_id>,<binary_len>,<ascii_data>\r\n
- * 例如:+TCPRECV: 0,10,1234567890\r\n (10字节ASCII数据)
- * 注意:<length>是二进制数据长度(10)
- *
- * URC格式(HEX模式):+TCPRECV: <socket_id>,<hex_str_len>,<hex_data>\r\n
- * 例如:+TCPRECV: 0,6,313233\r\n (表示3字节二进制数据:0x31, 0x32, 0x33)
- * 注意:<length>是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: <socket_id>
- * +TCPCLOSE: <socket_id>,Link Closed
- * +TCPCLOSE: ERROR(错误格式,socket_id设为-1)
- * 或:+SSLTCPCLOSE: <socket_id>
+ * @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: <socket_id>,<sent_bytes>
- * 或:+SSLTCPEND: <socket_id>,<sent_bytes>
- */
-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: <socket_id>,OK 或 +TCPSETUP: <socket_id>,FAIL
- * 或:+SSLTCPSETUP: <socket_id>,OK 或 +SSLTCPSETUP: <socket_id>,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
/**
* @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 <stdio.h>
#include <string.h>
#include <stdlib.h>
+/* ========== 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,
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,
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请求
*/
const char *path,
n706a_http_client_data_t *client_data)
{
- if (!client || !path || !client_data) {
+ if (!client || !client_data) {
return N706A_HTTP_EARG;
}
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,<length> 或 AT+HTTPACTION=99,<length> */
+ 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);
}
/**
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,<length> 或 AT+HTTPSACTION=99,<length> */
+ 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,
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,<length> 或 AT+HTTPSACTION=99,<length> */
+ 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,
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,<length> 或 AT+HTTPSACTION=99,<length> */
+ 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,
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,<length> 或 AT+HTTPSACTION=99,<length> */
+ 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: <len>,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;
}
/**
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;
}
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;
}
return false;
}
- return client->connected && n706a_tcp_is_connected(&client->tcp_sock);
+ return client->connected;
}
/**
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请求
*/
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);
}
/**
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;
- /* æ\9f¥æ\89¾å¤´å°¾å\88\86ç\95\8c */
- const char *header_end = strstr(response, "\r\n\r\n");
+ /* æ\9f¥æ\89¾å\93\8dåº\94头ç»\93æ\9d\9fä½\8dç½® */
+ 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编码:格式为 <chunk_size_hex>\r\n<chunk_data>\r\n<chunk_size_hex>\r\n<chunk_data>\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 的 <length> 是该 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);
}
#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 <string.h>
#include <stdlib.h>
+#include <time.h>
+#include <sys/time.h>
+
+// 时区相关宏定义(参照 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;
.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电源控制引脚配置
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 = {
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);
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接收回调
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");
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");
*
* 功能说明:
* - 执行 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)
{
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 ========== */
/**
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;
}
}
// 根据网络注册状态设置网络类型
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;
}
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模块波特率(在所有模式下)
*
#include <stdlib.h>
/* ========== 全局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;
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);
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);
/* ========== 公共函数实现 ========== */
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);
#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: <socket_id>,<status>[,<type>[,<send-buffer-size>]] */
+ /* 示例:+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: <socket_id>," */
+ 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 成功,其他值失败
*/
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;
+ }
}
/* 即使关闭命令失败,也返回成功(模块端可能已经关闭) */
}
/**
- * @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;
}
/**
{
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);
}
/**
}
}
- /* 额外检查:即使状态是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状态(避免状态混乱导致连接失败) */
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响应 ========== */
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();
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可以使用 */
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响应 ========== */
}
/**
- * @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=<n>,<data_length>
+ * 注意:根据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=<n>,<binary_len>,,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=<n>,<binary_len> */
+ 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: <n>,<length> 或 +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)
#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;
#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=<n>,<binary_len>,,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=<n>,<binary_len>,,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: <n>,<len>,<data>
+ * 但实际模块可能在响应中包含 +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: <n>,<len>,<data>
+ * 这是命令响应格式,与 +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: <n>,<hex_len>,<hex_data> 或 +SSLTCPREAD: <n>,<hex_len>,<hex_data> */
+ 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,
}
/* 优先从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
}
#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;
}
/**
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)");
+ }
/* 即使失败也标记为关闭,避免资源泄露,但记录错误 */
}
}
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;
// 释放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;
}
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...");
}
}
+/**
+ * @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);
+}
+
* - 分片传输
*/
+/* 定义 _GNU_SOURCE 以使用 strcasestr(不区分大小写的字符串查找) */
+/* 注意:必须在包含 <string.h> 之前定义 */
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+
#include "n706a_websocket.h"
#include "n706a_tcp_client.h"
#include "ppp_debug.h"
#include <time.h>
#include <stdint.h>
+/* 包含 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 <pthread.h>
#include <unistd.h>
#elif defined(__rtthread__)
#include <rtthread.h>
+#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) */
/* ========== 私有函数声明 ========== */
-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);
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;
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;
/**
* @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;
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 */
}
/* 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) {
/* 启动接收线程 */
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",
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回调 */
}
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;
}
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回调 */
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是否已连接
*/
}
/* 释放资源 */
+ if (client->parser) {
+ ws_parser_destroy(client->parser);
+ client->parser = NULL;
+ }
+
if (client->recv_buffer) {
free(client->recv_buffer);
}
/**
* @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);
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;
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;
}
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++;
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;
}
/**
*/
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;
}
}
/**
* @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)
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;
}
}
/**
- * @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)",
}
}
- /* 如果状态变为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;
/* 调用关闭回调 */
"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编码
}
if (i) {
- for (size_t k = i; k < 3; k++) {
+ size_t k;
+ for (k = i; k < 3; k++) {
char_array_3[k] = '\0';
}
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]];
}
// 只打印前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");
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;
}
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);
}
}
}
/**
- * @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);
-
}
/**
// - 中断模式(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");
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,
#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
/* 如需清空缓冲区,请使用 ppp_serial_flush_rx() */
/**
- * @brief 发送数据 - 两种模式都直接通过UART发送
+ * @brief 发送数据 - n706a_net 只使用 AT 模式,直接通过 UART 发送
*/
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)",
}
/**
- * @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)
{
*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)
}
/**
- * @brief PPP快速尝试读取 - 类似SLIP的sio_tryread,使用1ms超时
+ * @brief 快速尝试读取 - n706a_net 只使用 AT 模式,使用短超时读取
* @param buffer 接收缓冲区
* @param buffer_size 缓冲区大小
* @param bytes_read 实际读取字节数
*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;
}
}
--- /dev/null
+# TCP应用指南\r
+\r
+## 功能\r
+\r
+## 模组\r
+\r
+### TCP 应用指南\r
+\r
+###### 版本 1.5 日期 2025-04-24\r
+\r
+功能模组TCP 应用指南\r
+\r
+版权声明\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+ii\r
+\r
+版权声明\r
+\r
+版权所有 © 深圳市有方科技股份有限公司 2025。深圳市有方科技股份有限公司保留所有权利。\r
+\r
+未经深圳市有方科技股份有限公司书面同意,任何单位和个人不得擅自摘抄、复制本文档内容的部分或全部,\r
+并不得以任何形式传播。\r
+\r
+是深圳市有方科技股份有限公司所有商标。\r
+\r
+本文档中出现的其他商标,由商标所有者所有。\r
+\r
+说明\r
+\r
+本文档对应产品为功能模组。\r
+\r
+本文档的使用对象为系统工程师,开发工程师及测试工程师。\r
+\r
+本设计指南为客户产品设计提供支持,客户须按照本文中的规范和参数进行产品设计和调试。如因客户操作\r
+不当造成的人身伤害和财产损失,有方概不承担责任。\r
+\r
+由于产品版本升级或其它原因,本文档内容会在不预先通知的情况下进行必要的更新。\r
+\r
+除非另有约定,本文档中的所有陈述、信息和建议不构成任何明示或暗示的担保。\r
+\r
+深圳市有方科技股份有限公司为客户提供全方位的技术支持,任何垂询请直接联系您的客户经理或发送邮件\r
+至以下邮箱:\r
+\r
+Sales@neoway.com\r
+\r
+Support@neoway.com\r
+\r
+公司网址:http://www.neoway.com\r
+\r
+功能模组TCP 应用指南\r
+\r
+目录\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+iii\r
+\r
+#### 目 录\r
+\r
+###### 关于本文档 ....................................................................................................... v\r
+\r
+范围 ................................................................................................................................................... v\r
+\r
+读者对象 ............................................................................................................................................ v\r
+\r
+修订记录 ............................................................................................................................................ v\r
+\r
+符号约定 ............................................................................................................................................ v \r
+1 概述 .............................................................................................................. 6\r
+\r
+1.1 TCP 协议 ..................................................................................................................................... 6\r
+\r
+1.2 TCP 传输模式 .............................................................................................................................. 6 \r
+2 模块初始化流程 ........................................................................................... 8\r
+\r
+2.1 标准指令初始化流程 ................................................................................................................... 9\r
+\r
+2.2 国网指令初始化流程 ................................................................................................................. 10 \r
+3 标准指令TCP 应用 .................................................................................... 11\r
+\r
+3.1 TCP 非透传客户端 ..................................................................................................................... 12\r
+\r
+3.1.1 异常处理 .......................................................................................................................... 13\r
+\r
+3.1.2 示例.................................................................................................................................. 16\r
+\r
+3.2 TCP 非透传服务器端 ................................................................................................................. 18\r
+\r
+3.2.1 异常处理 .......................................................................................................................... 19\r
+\r
+3.2.2 示例.................................................................................................................................. 20\r
+\r
+3.3 TCP 透传客户端 ........................................................................................................................ 22\r
+\r
+3.3.1 异常处理 .......................................................................................................................... 23\r
+\r
+3.3.2 示例.................................................................................................................................. 25 \r
+4 国网指令TCP 应用 .................................................................................... 27\r
+\r
+4.1 TCP 非透传客户端 ..................................................................................................................... 27\r
+\r
+4.1.1 异常处理 .......................................................................................................................... 29\r
+\r
+4.1.2 示例.................................................................................................................................. 32\r
+\r
+4.2 TCP 非透传服务器端 ................................................................................................................. 34\r
+\r
+4.2.1 异常处理 .......................................................................................................................... 35\r
+\r
+4.2.2 示例.................................................................................................................................. 37\r
+\r
+4.3 TCP 透传客户端 ........................................................................................................................ 38\r
+\r
+4.3.1 异常处理 .......................................................................................................................... 40\r
+\r
+4.3.2 示例.................................................................................................................................. 42 \r
+5 外部协议栈TCP 应用 ................................................................................ 45\r
+\r
+5.1 异常处理 ................................................................................................................................... 45\r
+\r
+5.2 示例 .......................................................................................................................................... 46\r
+\r
+功能模组TCP 应用指南\r
+\r
+目录\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+iv\r
+\r
+###### A 关键AT 命令 .............................................................................................. 47\r
+\r
+A.1 标准AT 指令 ............................................................................................................................ 47\r
+\r
+A.1.1 AT+CGDCONT–设置PDP 格式 ...................................................................................... 47\r
+\r
+A.1.2 AT+XGAUTH–用户认证 .................................................................................................. 48\r
+\r
+A.1.3 AT+XIIC–建立PPP 链接 ................................................................................................. 49\r
+\r
+A.1.4 AT+TCPSETUP–建立TCP 连接 ..................................................................................... 50\r
+\r
+A.1.5 AT+TCPSEND–发送TCP 数据 ....................................................................................... 51\r
+\r
+A.1.6 AT+IPSTATUS–查询TCP/UDP 链路状态 ....................................................................... 53\r
+\r
+A.1.7 AT+TCPACK–查询TCP 链路发送数据状态 .................................................................... 54\r
+\r
+A.1.8 AT+TCPLISTEN–设置服务器侦听功能............................................................................ 55\r
+\r
+A.1.9 AT+CLOSELISTEN–关闭侦听链接 ................................................................................. 56\r
+\r
+A.1.10 AT+CLOSECLIENT–关闭主站链接 ............................................................................... 57\r
+\r
+A.1.11 +TCPRECV(S)–接收到主站的数据 ............................................................................... 58\r
+\r
+A.1.12 AT+TCPREADS–读取到主站的数据 ............................................................................. 58\r
+\r
+A.1.13 AT+TCPSENDS–发送给主站的数据 ............................................................................. 59\r
+\r
+A.1.14 AT+CLIENTSTATUS–查询主站链路的状态 ................................................................... 60\r
+\r
+A.1.15 AT+TCPACKS–查询TCP 服务器发送数据状态 ............................................................ 61\r
+\r
+A.1.16 AT+TCPTRANS–建立TCP 透传链接 ............................................................................ 62\r
+\r
+A.1.17 AT+TRANSCLOSE–关闭透传方式链接......................................................................... 63\r
+\r
+A.1.18 ATD*99#–GPRS ........................................................................................................... 64\r
+\r
+A.2 国网AT 指令 ............................................................................................................................ 65\r
+\r
+A.2.1 AT$MYNETCON–设置网络连接初始化参数 .................................................................... 65\r
+\r
+A.2.2 AT$MYNETACT–激活/去激活网络连接 ........................................................................... 66\r
+\r
+A.2.3 AT$MYNETSRV–设置非透明传输服务参数 .................................................................... 67\r
+\r
+A.2.4 AT$MYNETOPEN–开启服务........................................................................................... 68\r
+\r
+A.2.5 AT$MYNETREAD–读取数据 ........................................................................................... 69\r
+\r
+A.2.6 AT$MYNETWRITE–发送数据 ......................................................................................... 69\r
+\r
+A.2.7 AT$MYNETCLOSE–关闭连接......................................................................................... 70\r
+\r
+A.2.8 AT$MYNETACK–查询TCPACK 信息.............................................................................. 71\r
+\r
+A.2.9 AT$MYNETACCEPT–接受侦听请求 ............................................................................... 71\r
+\r
+A.2.10 AT$MYNETCREATE–开启透明传输服务 ...................................................................... 72\r
+\r
+A.2.11 $MYURCREAD–数据到来主动上报 .............................................................................. 74\r
+\r
+A.2.12 $MYURCCLOSE–链路断开主动上报 ............................................................................ 74\r
+\r
+A.2.13 $MYURCACT–网络连接状态主动上报.......................................................................... 75\r
+\r
+A.2.14 $MYURCCLIENT–客户端连接主动上报 ........................................................................ 76\r
+\r
+功能模组TCP 应用指南\r
+\r
+关于本文档\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+v\r
+\r
+#### 关于本文档\r
+\r
+##### 范围\r
+\r
+本文档对应产品为功能模组。\r
+\r
+##### 读者对象\r
+\r
+本文档的使用对象为系统工程师,开发工程师及测试工程师。\r
+\r
+##### 修订记录\r
+\r
+版本 \r
+日期 \r
+变更 \r
+作者\r
+\r
+1.0 \r
+2018-11 \r
+初始版本 \r
+Zhang Song\r
+\r
+1.1 \r
+2020-01\r
+\r
+⚫ \r
+删除各TCP 连接中的模块初始化部分。\r
+\r
+⚫ \r
+新增第2 章 模块初始化流程。 \r
+Zhang Song\r
+\r
+1.2 \r
+2020-09 \r
+优化初始化流程图 \r
+Zhang Song\r
+\r
+1.3 \r
+2022-02 \r
+优化各流程图样式 \r
+Zhang Song\r
+\r
+1.4 \r
+2022-06 \r
+修改初始化流程图 \r
+Zhang Song\r
+\r
+1.5 \r
+2025-04 \r
+优化第2 章模块初始化流程图 \r
+Zhang Song\r
+\r
+##### 符号约定\r
+\r
+符号 \r
+含义\r
+\r
+危险或警告,用户必须遵从的规则,否则会造成模块或客户设备不可逆的故障损坏,\r
+\r
+甚至可能造成人员身体伤害。\r
+\r
+注意,警示用户使用模块时应该特别注意的地方,如不遵从,模块或客户设备可能\r
+\r
+出现故障。\r
+\r
+说明或提示,提供模块使用的意见或建议。\r
+\r
+功能模组TCP 应用指南\r
+\r
+第1 章概述\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+6\r
+\r
+#### 1 概述\r
+\r
+本文主要介绍如何使用有方科技股份有限公司功能模块的TCP 功能。\r
+\r
+##### 1.1 TCP 协议\r
+\r
+TCP(Transmission Control Protocol 传输控制协议)协议是一种面向连接的、可靠地、基于字节\r
+\r
+流的运输层协议。相比UDP 协议,TCP 具有更高的可靠性。对于数据传输有高可靠性要求的应用场景,\r
+\r
+建议选用此协议。\r
+\r
+用户使用功能模块TCP 功能时,可以用模块内置的TCP 协议栈(内部协议栈)也可以使用用户主\r
+\r
+控自带的协议栈(外部协议栈)。二者的区别在于:模块内部协议栈已经将PPP 协议、TCP 协议封装成\r
+\r
+了对应的AT 指令,主控 只需要操作对应的AT 指令就可以完成拨号、建立和关闭TCP 连接、收发数\r
+\r
+据等操作。外部协议栈主要依赖于主控自己的协议栈,需要主控处理PPP 协议、TCP 协议具体的数据\r
+\r
+帧和数据内容。\r
+\r
+为适应不同客户使用场景和行业要求,有方功能模块内置的TCP 协议栈具有丰富的AT 指令,主要\r
+\r
+分为两类:国网指令和标准指令。国网指令是国家电网在376.3 规约中定义的AT 指令,主要用于电力\r
+\r
+行业;标准指令是我司自定义格式的AT 指令。详见各模块对应的AT 指令集。\r
+\r
+##### 1.2 TCP 传输模式\r
+\r
+功能模块支持TCP 客户端和TCP 服务端功能。其中,TCP 客户端功能具有两种模式:\r
+\r
+⚫ \r
+透明传输模式\r
+\r
+建立TCP 连接后,主控从端口(USB、UART)发送给模块的字符(包括AT 命令)都将被当作\r
+\r
+TCP 数据发送到网络,模块从网络侧接收到的TCP 数据将会直接从串口上报给主控,不带任何提\r
+\r
+示回码。\r
+\r
+主控可向模块发送“+++”退出透明传输模式,查询信号强度,网络状态等;在TCP 连接未断开的\r
+\r
+情况下,可以向模块发送“ATO”返回透明传输模式。\r
+\r
+⚫ \r
+非透明传输模式\r
+\r
+建立TCP 连接后,主控每发送一次数据都需要调用发送数据的AT 命令,模块从网络侧接收到的\r
+\r
+TCP 数据将会直接从串口上报给主控,但在TCP 数据内容前会带有对应的提示数据头\r
+\r
++TCPRECV:<SocketID>,<data_length>,<data>。\r
+\r
+功能模组TCP 应用指南\r
+\r
+第1 章概述\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+7\r
+\r
+本文按照标准指令集、国网指令集的顺序介绍模块内置协议栈TCP 功能的使用方式,主要包括相\r
+\r
+关AT 命令的介绍、TCP 客户端和服务器端的使用流程、异常的排查与处理等内容。最后,我们会简单\r
+\r
+介绍外部协议栈的使用方式。\r
+\r
+功能模组TCP 应用指南\r
+\r
+第2 章模块初始化流程\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+8\r
+\r
+#### 2 模块初始化流程\r
+\r
+请确保模块在建立TCP 连接前已执行过该初始化步骤。\r
+\r
+⚫ \r
+发送AT 命令后,必须等待收到回码才能发送下一条AT 命令。\r
+\r
+⚫ \r
+发送AT 命令后,若未返回任何回码,则继续发送,1s/次,最多3 次,若发送3 次后,仍未返\r
+\r
+回任何回码,则重启模块。\r
+\r
+⚫ \r
+上述流程中设置的APN、用户名、密码、鉴权方式等参数,仅用于举例说明。实际应用过程\r
+\r
+中,需结合所使用的SIM 卡的签约信息,设置正确的APN、用户名、密码、鉴权方式等参\r
+\r
+数。\r
+\r
+⚫ \r
+针对单模块使用双卡或者三卡的情况,在设置注册网络参数指令后(APN、用户名密码、鉴权\r
+\r
+方式),需要增加指令(例如AT+COPS=2;AT+COPS=0,具体指令在设计流程时可与有方\r
+\r
+技术支持沟通),让模块注销网络并重新注册网络。\r
+\r
+⚫ \r
+重启模块请参照《Neoway_标准模块_开关机流程操作指南》进行。24 小时内过多的重启可能\r
+\r
+会导致FLASH 及SIM 卡不可逆的损坏。\r
+\r
+⚫ \r
+若客户产品需要使用模组2G SMS、CSD 功能,需要确保模组注册上CS 域。所以初始化流\r
+\r
+程中,主控必须执行AT+CREG?查询模组CS 域的注册情况。若客户产品未使用模组2G\r
+\r
+SMS、CSD 功能,则主控可以不执行AT+CREG?查询模组CS 域的注册情况。\r
+\r
+⚫ \r
+若客户产品需要使用模组分组数据业务功能,需要确保模组附着上PS 域。所以初始化流程\r
+\r
+中,主控必须执行AT+CGATT?查询模组PS 域的附着情况。若客户产品未使用模组分组数据\r
+\r
+业务功能,则主控可以不执行AT+CGATT?查询模组PS 域的附着情况。在查询模组PS 域附\r
+\r
+着情况前,可以按需使用AT+CGREG?、AT+CEREG?查询PS 域的注册情况:\r
+\r
+- \r
+2G/3G 模组或者4G 模组2G/3G 模式下,使用AT+CGREG?查询PS 域(GPRS 网络)\r
+\r
+的注册情况。\r
+\r
+- \r
+4G 模组4G 模式下,使用AT+CGREG?查询PS 域(EPS 网络)的注册情况。\r
+\r
+功能模组TCP 应用指南\r
+\r
+第2 章模块初始化流程\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+9\r
+\r
+##### 2.1 标准指令初始化流程\r
+\r
+延时\r
+\r
+"OK"?\r
+\r
+1. AT\r
+\r
+2. AT+CGSN\r
+\r
+3. AT+GMR\r
+\r
+Yes\r
+\r
+4. AT+CPIN?\r
+\r
++CPIN: READY ?\r
+\r
+5. AT+CIMI\r
+\r
+8. AT+CSQ\r
+\r
++CSQ: <signal>,<ber>\r
+ and 0 signal 31 ?\r
+\r
+12. AT+CGATT?\r
+\r
+"+CGATT: 1"?\r
+\r
+No\r
+\r
+No\r
+\r
+13. AT$MYSYSINFO\r
+\r
+查询<40次?\r
+\r
+Yes\r
+\r
+No\r
+\r
+查询<15次?\r
+\r
+查询<30次?\r
+手动附着\r
+\r
+查询<10次?\r
+\r
+模块初始化完成\r
+\r
+No\r
+\r
+No\r
+\r
+Yes\r
+\r
+No\r
+\r
+2G模块延时3s,4G模块延时10s。该过程中模块处于\r
+初始化阶段,可能出现未响应或响应出错的情况。\r
+\r
+- 4.检查SIM卡状态\r
+- 若返回"+CPIN: READY",则可进入下一步。\r
+- 若未返回任何回码或者返回其他错误回码,则继续\r
+\r
+查询,1s/次,最多15次。若查询15次后仍未返回\r
+"+CPIN: READY",则重启模块。\r
+\r
+模块重启\r
+模块开机\r
+\r
+6. AT+CGDCONT=1,"IP","CMNET"\r
+\r
+"OK"?\r
+\r
+7. AT+XGAUTH=1,1,"gsm","1234"\r
+\r
+"OK"?\r
+\r
+No\r
+\r
+查询<2次?\r
+\r
+拨号<3次?\r
+\r
+重新设置<3次?\r
+\r
+No\r
+\r
+Yes\r
+\r
+No\r
+No\r
+Yes\r
+\r
+No\r
+Yes\r
+No\r
+\r
+Yes\r
+\r
+No\r
+Yes\r
+No\r
+\r
+1. 检查串口是否已通\r
+- 若返回"OK",则可进入下一步。\r
+- 若未返回任何回码或者返回其他错误回码,则继续尝\r
+\r
+试,1s/次,最少10次。若尝试10次后,仍未返回\r
+"OK ",则重启模块。\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+- 5.获取SIM卡IMSI\r
+- 若返回"OK",则可进入下一步。\r
+- 若未返回任何回码或者返回其他错误回码,则继续查\r
+\r
+询,最多2次。若查询2次后仍未返回"OK",则进入下\r
+一步。\r
+\r
+6. 设置APN\r
+- 若返回"OK",则可进入下一步。\r
+- 若未收到任何回码或收到其他错误回码,则继续设\r
+\r
+置,1s/次,最多3次。若重新设置3次后,仍未若返\r
+回"OK ",则重启模块。\r
+\r
+7. 设置身份认证参数\r
+- 若返回"OK",则可进入下一步。\r
+- 若未返回任何回码或者返回其他错误回码,则继续\r
+\r
+若收到其他错误回码设置,1s/次,最多3次。若重\r
+新设置3次后,仍未若返回"OK ",则重启模块。\r
+\r
+8.查询信号强度\r
+• 若返回"+CSQ: <signal>,<ber>"且0 signal 31则有信\r
+\r
+号。建议signal 12时,再进入下一步。极端恶劣网\r
+络环境下,只要有信号强度即可进入下一步。\r
+• 若未返回任何回码、返回的回码包含"+CSQ: 99,"、\r
+\r
+或者返回其他错误回码,则继续查询,1s/次,最多40\r
+次。若查询40次后仍无信号,则重启模块。\r
+\r
+12. 查询网络附着情况\r
+- 若返回"+CGATT: 1",则进入下一步\r
+- 若返回"+CGATT: 0",或其他错误回码,则继续查\r
+\r
+询,1s/次,最多30次。若查询30次后,仍未返回\r
+"+CGATT: 1",则手动进行网络附着。最长等待2分\r
+钟返回回码。\r
+• 手动进行网络附着后,仍未返回"+CGATT: 1",则\r
+\r
+重启模块。\r
+\r
+13.查询当前网络制式\r
+\r
+2. 获取模块IMEI\r
+\r
+3. 获取模块固件版本\r
+\r
+"OK"?\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+10. AT+CGREG?\r
+\r
++CGREG: 0,1 or\r
+\r
++CGREG: 0,5?\r
+查询<40次?\r
+\r
+Yes\r
+\r
+No\r
+\r
+Yes\r
+\r
+9. AT+CREG?\r
+\r
++CREG: 0,1 or\r
+\r
++CREG: 0,5?\r
+查询<40次?\r
+\r
+Yes\r
+\r
+No\r
+\r
+Yes\r
+\r
+No\r
+\r
+No\r
+\r
+11. AT+CEREG?\r
+\r
++CEREG: 0,1 or\r
+\r
++CEREG: 0,5?\r
+查询<40次?\r
+\r
+Yes\r
+\r
+No\r
+\r
+Yes\r
+\r
+No\r
+\r
+No\r
+\r
+9. 查询GSM网络CS域注册情况\r
+- 若回码包含"+CREG: 0,1"或者"+CREG: 0,5"字段,则进\r
+\r
+入下一步。\r
+• 若未返回任何回码或返回其他错误回码,则继续查\r
+\r
+询,1s/次,最多40次。若查询40次后,回码仍不包\r
+含"+CREG: 0,1"或者"+CREG: 0,5"字段:针对需要使用\r
+2G短信/CSD功能的场景,需要确保模组注册上CS\r
+域,可重启模块。针对不需要使用2G短信/CSD功能\r
+的场景,可进入下一步。\r
+\r
+10. 查询GPRS网络注册情况\r
+- 若回码包含"+CGREG: 0,1"或者"+CGREG: 0,5"字段,\r
+\r
+则进入下一步。\r
+• 若未返回任何回码或返回其他错误回码,则继续查\r
+\r
+询,1s/次,最多40次。若查询40次后,回码仍不包\r
+含"+CGREG: 0,1"或者"+CGREG: 0,5"字段,可进入下\r
+一步。\r
+\r
+11. 查询EPS网络注册情况\r
+- 若回码包含"+CEREG: 0,1"或者"+CEREG: 0,5"字段,则\r
+\r
+进入下一步。\r
+• 若未返回任何回码或返回其他错误回码,则继续查\r
+\r
+询,1s/次,最多40次。若查询40次后,回码仍不包\r
+含"+CEREG: 0,1"或者"+CEREG: 0,5"字段,可进入下一\r
+步\r
+\r
+功能模组TCP 应用指南\r
+\r
+第2 章模块初始化流程\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+10\r
+\r
+##### 2.2 国网指令初始化流程\r
+\r
+Yes\r
+\r
+延时\r
+\r
+"OK"?\r
+\r
+1. AT\r
+\r
+2. AT+CGSN\r
+\r
+3. AT+GMR\r
+\r
+Yes\r
+\r
+4. AT+CPIN?\r
+\r
++CPIN: READY ?\r
+\r
+5. AT+CIMI\r
+\r
+"OK"?\r
+\r
+9. AT+CSQ\r
+\r
++CSQ: <signal>,<ber>\r
+ and 0 signal 31 ?\r
+\r
+11. AT+CGREG?\r
+\r
++CGREG: 0,1 or\r
+\r
++CGREG: 0,5?\r
+\r
++CGATT: 1?\r
+\r
+13. AT+CGATT?\r
+\r
+Yes\r
+\r
+No\r
+\r
+14. AT$MYSYSINFO\r
+\r
+查询<40次?\r
+\r
+查询<40次?\r
+\r
+Yes\r
+\r
+查询<15次?\r
+\r
+查询<30次?\r
+\r
+```\r
+AT+CGATT=1\r
+```\r
+\r
+手动附着\r
+\r
+查询<10次?\r
+\r
+模块初始化完成\r
+\r
+No\r
+Yes\r
+\r
+No\r
+No\r
+\r
+2G模块延时3s,4G模块延时10s。该过程中模块处于\r
+初始化阶段,可能出现未响应或响应出错的情况。\r
+\r
+4. 检查SIM卡状态\r
+- 若返回"+CPIN: READY",则可进入下一步\r
+- 若未返回任何回码或者返回其他错误回码,则继续查\r
+\r
+询,1s/次,最多15次。若查询15次后仍未返回\r
+"+CPIN: READY",则重启模块。\r
+\r
+模块重启\r
+模块开机\r
+\r
+6. AT$MYNETCON=0,"APN","CMNET"\r
+\r
+"OK"?\r
+\r
+7. AT$MYNETCON=0,"USERPWD","CARD,CARD"\r
+\r
+"OK"?\r
+\r
+No\r
+\r
+查询<2次?\r
+\r
+拨号<3次?\r
+\r
+重新设置<3次?\r
+\r
+No\r
+\r
+Yes\r
+\r
+No\r
+\r
+No\r
+No\r
+Yes\r
+\r
+No\r
+\r
+1. 检查串口是否已通\r
+- 若返回"OK",则可进入下一步。\r
+- 若未返回任何回码或者返回其他错误回码,则继续尝\r
+\r
+试,1s/次,最少10次。若尝试10次后,仍未返回\r
+"OK ",则重启模块。\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+- 5.获取SIM卡IMSI\r
+- 若返回"OK",则可进入下一步。\r
+- 若未返回任何回码或返回其他错误回码,则继续查\r
+\r
+询,最多2次。若查询2次后仍未返回"OK",则进入下\r
+一步。\r
+\r
+6. 设置APN\r
+- 若返回"OK",则可进入下一步。\r
+- 若未返回任何回码或返回其他错误回码,则继续设\r
+\r
+置,1s/次,最多3次。若重新设置3次后,仍未若\r
+返回"OK ",则重启模块。\r
+\r
+7. 设置身份认证参数\r
+- 若返回"OK",则可进入下一步。\r
+- 若未返回任何回码或返回其他错误回码,则继续设\r
+\r
+置,1s/次,最多3次。若重新设置3次后,仍未若返\r
+回"OK ",则重启模块。\r
+\r
+8. AT$MYNETCON=0,"AUTH",1\r
+\r
+OK?\r
+\r
+9. 查询信号强度\r
+- 若返回"+CSQ: <signal>,<ber>"且0 signal 31则有信\r
+\r
+号。建议signal 12时,再进入下一步。极端恶劣网\r
+络环境下,只要有信号强度即可进入下一步。\r
+• 若未返回任何回码、回码包含"+CSQ: 99,"、返回其\r
+\r
+他错误回码,则继续查询,1s/次,最多40次。若查\r
+询40次后仍无信号,则重启模块。\r
+\r
+Yes\r
+\r
+2. 获取模块IMEI\r
+\r
+3. 获取模块固件版本\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+No\r
+Yes\r
+No\r
+\r
+查询<40次?\r
+No\r
+No\r
+Yes\r
+\r
+No\r
+No\r
+Yes\r
+\r
+No\r
+\r
+Yes\r
+\r
+8. 设置身份认证参数\r
+- 若返回"OK",则可进入下一步。\r
+- 若未返回任何回码或返回其他错误回码,则继续设\r
+\r
+置,1s/次,最多3次。若重新设置3次后,仍未若返\r
+回"OK ",则重启模块。\r
+\r
+13. 查询PS域附着情况\r
+- 若返回"+CGATT: 1",则进入下一步\r
+- 若未返回任何回码、返回"+CGATT: 0"、返回其他\r
+\r
+错误回码,则继续查询,1s/次,最多30次。若查\r
+询30次后,仍未返回"+CGATT: 1",则手动进行网\r
+络附着。最长等待2分钟返回回码。\r
+• 手动进行网络附着后,仍未返回"+CGATT: 1",则\r
+\r
+重启模块。\r
+\r
+14.查询当前网络制式\r
+\r
+10. 查询GSM网络CS域注册情况\r
+- 若回码包含"+CREG: 0,1"或者"+CREG: 0,5"字段,则\r
+\r
+进入下一步。\r
+• 若未返回任何回码或返回其他错误回码,则继续查\r
+\r
+询,1s/次,最多40次。若查询40次后,回码仍不\r
+包含"+CREG: 0,1"或者"+CREG: 0,5"字段:针对需要\r
+使用2G短信/CSD功能的场景,需要确保模组注册上\r
+CS域,可重启模块。针对不需要使用2G短信/CSD功\r
+能的场景,可进入下一步。\r
+\r
+11. 查询GPRS网络注册情况\r
+- 若回码包含"+CGREG: 0,1"或者"+CGREG: 0,5"字段,\r
+\r
+则进入下一步。\r
+• 若未返回任何回码或返回其他错误回码,则继续查\r
+\r
+询,1s/次,最多40次。若查询40次后,回码仍不\r
+包含"+CGREG: 0,1"或者"+CGREG: 0,5"字段,可进入\r
+下一步。\r
+\r
+10. AT+CREG?\r
+\r
++CREG: 0,1 or\r
+\r
++CREG: 0,5?\r
+查询<40次?\r
+\r
+Yes\r
+\r
+No\r
+\r
+Yes\r
+\r
+No\r
+\r
+No\r
+\r
+12. AT+CEREG?\r
+\r
++CEREG: 0,1 or\r
+\r
++CEREG: 0,5?\r
+查询<40次?\r
+\r
+Yes\r
+\r
+No\r
+\r
+Yes\r
+\r
+12. 查询EPS网络注册情况\r
+- 若回码包含"+CEREG: 0,1"或者"+CEREG: 0,5"字段,\r
+\r
+则进入下一步。\r
+• 若未返回任何回码或返回其他错误回码,则继续查\r
+\r
+询,1s/次,最多40次。若查询40次后,回码仍不\r
+包含"+CEREG: 0,1"或者"+CEREG: 0,5"字段,可进入\r
+下一步\r
+No\r
+\r
+No\r
+\r
+功能模组TCP 应用指南 \r
+第3 章标准指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+11\r
+\r
+#### 3 标准指令TCP 应用\r
+\r
+本章主要介绍如何使用有方标准指令实现TCP 功能(包括TCP 非透传客户端、TCP 非透传服务器\r
+\r
+端、TCP 透传客户端功能)。内容包括对应功能相关的AT 指令、AT 流程、异常处理以及简单示例。\r
+\r
+连接注意事项:\r
+\r
+⚫ \r
+发送AT 命令后必须等待收到回码后,才能发送下一条AT 命令。\r
+\r
+⚫ \r
+在建立PPP 连接后,主动去附着前,只要收到"GPRS DISCONNECTION",则返回重新激活PPP 连\r
+\r
+接。\r
+\r
+⚫ \r
+在建立TCP 连接后,主动断开TCP 连接前,只要收到"+TCPCLOSE: X,Link Closed",则返回重新\r
+\r
+建立TCP 连接。\r
+\r
+⚫ \r
+重启模块请参照《Neoway_标准模块_开关机流程操作指南》进行。24 小时内过多的重启可能会导致\r
+\r
+FLASH 及SIM 卡不可逆的损坏。\r
+\r
+功能模组TCP 应用指南 \r
+第3 章标准指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+12\r
+\r
+##### 3.1 TCP 非透传客户端\r
+\r
+本节主要实现TCP 非透传客户端功能相关的AT 指令、AT 流程、异常处理以及简单示例。在进行\r
+\r
+PPP 拨号前,请确保模块已经过初始化(具体初始化流程参考第2 章)。\r
+\r
+图 3-1 连接流程\r
+\r
+设置PPP拨号参数\r
+\r
+"OK"?\r
+\r
+1. AT+XIIC=1\r
+\r
+"+XIIC: 1"?\r
+\r
+2. AT+XIIC?\r
+\r
+"+TCPCLOSE: X,OK" or "+TCPCLOSE:\r
+\r
+ERROR"?\r
+\r
+3. AT+TCPCLOSE=X\r
+\r
+Yes\r
+\r
+4. AT+TCPSETUP=X,<IP/域名>,<Port>\r
+\r
+"+TCPSETUP: X,OK"?\r
+\r
+5. AT+IPSTATUS=X\r
+\r
+"+IPSTATUS: \r
+X,CONNECT,TCP,4096"?\r
+\r
+9. AT+CGATT=0\r
+\r
+"OK" or "ERROR"?\r
+\r
+No\r
+\r
+No\r
+\r
+Yes\r
+重连<3次?\r
+\r
+查询<3次?\r
+\r
+关机\r
+\r
+No\r
+\r
+No\r
+\r
+Yes\r
+\r
+模块重启\r
+模块初始化流程\r
+\r
+6. AT+TCPSEND=X,<Length>\r
+\r
+7. AT+TCPACK=X\r
+\r
+"+TCPACK: \r
+X,<Data_send>,<Acked_data>"?\r
+\r
+No\r
+\r
+查询<5次?\r
+\r
+重发<3次?\r
+\r
+查询<5次?\r
+\r
+No\r
+\r
+Yes\r
+\r
+No\r
+\r
+No\r
+No\r
+Yes\r
+\r
+No\r
+Yes\r
+No\r
+\r
+1. 进行PPP拨号\r
+- 若返回"OK",则可进入下一步。\r
+- 若未收到任何回码或收到其他错误回码,则继续拨号,1s/次,最\r
+\r
+多3次。若重新拨号3次后,仍未若返回"OK ",则重启模块。\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+4. 建立TCP连接\r
+- 该指令等待返回"+TCPSETUP: X,OK"的超时时间为30s\r
+- 若返回"+TCPSETUP: X,OK",则可进入下一步。\r
+- 若出现以下情况,则继续重连,最多3次,若3次仍无法建立TCP连\r
+\r
+接,则重启模块。\r
+ -- 收到回码:"+TCPSETUP: X,FAIL"/"+TCPSETUP: ERROR"\r
+ -- 收到其他错误回码或者未收到任何回码\r
+\r
+5. 查询链路状态\r
+- 若返回"+IPSTATUS: X,CONNECT,TCP,4096",则进行下一步。\r
+- 若回码包含"+IPSTATUS: X,DISCONNECT",则返回重新建立TCP连接。\r
+- 若回码包含"+IPSTATUS: X,CONNECT",Buffer大小不为4096,则循环\r
+\r
+查询链路状态,1s/次,最多5次,若查询5次后,Buffer大小不为\r
+4096,则返回关闭当前连接,重新建立TCP连接。\r
+\r
+6. 发送数据\r
+无论是否返回 > ,都延时50-100ms,再将数据发送到模块串口, \r
+等待数据发送成功的回码 +TCPSEND:X,<Length> 。\r
+• 若返回 +TCPSEND:X,<Length> ,则进入下一步。\r
+• 若返回 +TCPSEND: SOCKET ID OPEN FAILED ,则返回重新建立TCP\r
+\r
+连接。\r
+• 若返回其他错误回码,则继续执行该命令重发数据,最多3次。若\r
+\r
+重发3次仍然失败,则返回重新建立TCP连接。\r
+\r
+8. 关闭Socket\r
+- 若返回"+TCPCLOSE: X,OK""+TCPCLOSE: ERROR"则进入下一步,等待\r
+\r
+返回的超时时间为30s。\r
+• 若超时未收到回码或收到其他错误回码,则继续关闭,最多3次,\r
+\r
+若重复关闭3次后,仍未收到"+TCPCLOSE: X,OK"或者"+TCPCLOSE: \r
+ERROR"则重启模块。\r
+\r
+9. 去附着网络\r
+- 若返回"OK"或者" ERROR",则进入下一步,等返回的超时时间为\r
+\r
+15s。\r
+• 若超时未收到回码或者收到其他错误回码,则继续执行,最多3\r
+\r
+次,若重复关闭3次后,仍未收到"OK"或者" ERROR"则重启模块。\r
+\r
+"+TCPCLOSE: X,OK" or "+TCPCLOSE:\r
+\r
+ERROR"?\r
+\r
+7. 查询数据发送情况\r
+- 若返回"+TCPACK: X,<Data_send>,<Acked_data>"且<Data_send>与\r
+\r
+<Acked_data>数值相等,则可进入下一步:继续发送数据或者接收\r
+数据。\r
+• 若返回"+TCPACK: 0,DISCONNECT",则返回重新建立TCP连接。\r
+• 若返回"+TCPACK: X,<Data_send>,<Acked_data>"且若<Data_send>大\r
+\r
+于<Acked_data>数值,则循环查询循环查询1s/次,最多5次。若查\r
+询5次,回码中<Data_send>与<Acked_data>数值仍不相等,则返回\r
+关闭当前连接,重新建立连接,以释放缓冲区。\r
+• 若收到其他错误回码、超时时间到未收到任何回码,则重启模块。\r
+\r
+"+TCPSEND: X,<Length>"?\r
+\r
+8. AT+TCPCLOSE=X\r
+\r
+查询<30次?\r
+\r
+关闭<3次?\r
+\r
+关闭<3次?\r
+\r
+去附着<3次?\r
+\r
+Yes\r
+\r
+No\r
+\r
+Yes\r
+\r
+No\r
+\r
+No\r
+Yes\r
+\r
+Yes\r
+\r
+No\r
+No\r
+\r
+2. 查询PPP连接建立情况\r
+- 若回码中包含"+XIIC: 1 ",则可进入下一步。\r
+- 若回码未包含"+XIIC: 1",则循环查询,1s/次,最多30次。若30次\r
+\r
+后仍未返回"+XIIC: 1",则重新拨号,最多重拨3次,若重新拨号3次\r
+后,仍未获取到IP,则重启模块。\r
+\r
+3. 关闭待用Socket\r
+- 该指令等待返回"+TCPCLOSE: X,OK""+TCPCLOSE: ERROR"的超时时间\r
+\r
+为30s。\r
+• 若超时未收到回码或收到其他错误回码,则继续关闭,最多3次,\r
+\r
+若重复关闭3次后,仍未收到"+TCPCLOSE: X,OK"或者"+TCPCLOSE: \r
+ERROR"则重启模块。\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+No\r
+\r
+No\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+功能模组TCP 应用指南 \r
+第3 章标准指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+13\r
+\r
+###### 3.1.1 异常处理\r
+\r
+###### 模块附着PS 域失败\r
+\r
+1. \r
+检查模块串口通信是否正常。若串口不通,可参考以下方式排查解决:\r
+\r
+− \r
+确保模块已经开机。若模块不开机需要检查电源设计和开机时序\r
+\r
+− \r
+确保串口电平变换正常。若电平变换不正常需要检查串口电路设计和器件选型\r
+\r
+− \r
+确保软件上串口配置正常,软件处理是否有误。比如自适应逻辑、是否带回车换行发送AT\r
+\r
+2. \r
+检查模块是否已经识别到SIM 卡。若模块无法识别SIM 卡,可参考以下方式排查解决(涉及到的\r
+\r
+AT 命令,请参考功能 AT 指令集):\r
+\r
+− \r
+确保SIM 卡是否插好、SIM 卡本身完好无损、与卡座接触是否良好\r
+\r
+− \r
+确保SIM 卡座相关电路无设计问题,是否有虚假焊等\r
+\r
+− \r
+确保SIM 卡没有被锁卡\r
+\r
+若通过以上排查模块仍无法识别到SIM 卡,可以联系运营商处理或者更换SIM 卡解决。\r
+\r
+3. \r
+查询当前模块的接收信号是否满足通信条件。我们建议信号强度值为12(-83dbm)再进行通信。\r
+\r
+若现场信号弱,可通过以下方式排查:\r
+\r
+− \r
+确保天线接好、天线本身完好无损(断路、短路)。\r
+\r
+− \r
+确认模块注册的网络制式(AT$MYSYSINFO)等信息,若为现场网络环境问题,则联系运营\r
+\r
+商增强网络覆盖,或者更换其他运营商SIM 卡解决。\r
+\r
+4. \r
+若现场信号良好,模块能识别到SIM 卡且SIM 卡未欠费,但模块不能附着上PS 域,则需要联系\r
+\r
+运营商确定SIM 卡配置:\r
+\r
+− \r
+SIM 卡是否开通数据业务\r
+\r
+− \r
+SIM 卡配置鉴权参数是否正常\r
+\r
+− \r
+SIM 卡是否有绑定设备\r
+\r
+− \r
+SIM 卡数据业务相关的APN、用户名、密码、鉴权方式是否正常\r
+\r
+###### 模块激活PDP 失败\r
+\r
+若模块正常注册上PS 域,但是无法激活PPP 连接。可以参考以下方式排查解决(涉及到的AT 命\r
+\r
+令,请参考功能 AT 指令集):\r
+\r
+⚫ \r
+确保SIM 未欠费。\r
+\r
+功能模组TCP 应用指南 \r
+第3 章标准指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+14\r
+\r
+⚫ \r
+使用AT+CGDCONT?/AT+XGAUTH?查询APN、用户名、密码,确保这些参数与运营商提供的\r
+\r
+一致。\r
+\r
+若以上参数都没有问题,模块仍无法激活PPP 连接,请联系当地运营商协助检查SIM 卡和后台服\r
+\r
+务器参数配置。\r
+\r
+###### 模块建立TCP 非透传客户端连接失败\r
+\r
+若模块正常获取到IP,但是建立TCP 非透传客户端连接失败。可以参考以下方式排查解决(涉及\r
+\r
+到的AT 命令,请参考功能 AT 指令集):\r
+\r
+1. \r
+确认服务器IP 和端口都已配置好,端口已经建立服务器端侦听,IP 能被客户端PING 通。\r
+\r
+可以通过用其他客户端设备来连接该服务器IP 和端口确认。\r
+\r
+2. \r
+确认模块获取到的IP 是否能访问服务器。\r
+\r
+可通过发送AT+PING=<IP/域名>给模块,操作模块PING 服务器来确认。\r
+\r
+− \r
+若模块能PING 通服务器,则需要检查服务器软件和端口配置;\r
+\r
+− \r
+若模块不能PING 通服务器,则需要检查APN、用户名、密码设置是否正确,SIM 卡配置是\r
+\r
+否正确。\r
+\r
+3. \r
+确认SIM 卡配置是否有问题。\r
+\r
+可以直接联系运营商确认SIM 卡是否可以访问目的IP 或者让运营商直接跟踪后台消息排查原因。\r
+\r
+###### 模块发送TCP 数据失败\r
+\r
+若模块正常建立TCP 非透传客户端连接,但是发送TCP 数据失败。可以参考以下方式排查解决\r
+\r
+(涉及到的AT 命令,请参考功能 AT 指令集):\r
+\r
+1. \r
+确认是否是服务器问题。\r
+\r
+可通过更换服务器对比确认是否是服务器配置问题。\r
+\r
+2. \r
+确认SIM 卡是否也有欠费。\r
+\r
+可联系运营商确认。\r
+\r
+3. \r
+确认是否是长时间没有发送数据导致的假连接。\r
+\r
+可通过缩短心跳周期或者是提高数据发送频率来确认。\r
+\r
+若缩短心跳周期或者是提高数据发送频率后,数据发送正常则可能是假连接引起的问题。\r
+\r
+4. \r
+确认TCP 连接是否还存在。\r
+\r
+功能模组TCP 应用指南 \r
+第3 章标准指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+15\r
+\r
+可通过”AT+IPSTATUS=<SocketID>”命令的返回值来确认。\r
+\r
+− \r
+若返回值中含有“CONNECT”字符串,则表示TCP 连接存在;\r
+\r
+− \r
+若返回“DISCONNECT” 字符串,则表示TCP 连接不存在。\r
+\r
+5. \r
+确认终端(或主控)是否已经将数据发送到模块。\r
+\r
+可以通过模块串口与终端(或主控)串口之间交互的AT 命令和数据内容来确认。\r
+\r
+为此,我司建议主控具备保存或打印模块与终端串口交互log 功能。只要AT+TCPSEND 命令返\r
+\r
+回“+TCPSEND: <SocketID>,<DataLength>”就表示主控数据已经发送到模块。\r
+\r
+6. \r
+确认模块底层协议栈是否收到服务器端TCP 协议层的ACK。\r
+\r
+可通过”AT+TCPACK=<SocketID>”命令的返回值来确认。\r
+\r
+− \r
+若返回值中< data_sent >字段与<acked_recv>字段数值相同,则表示数据已经发送到服务器\r
+\r
+且服务器已经确认。\r
+\r
+− \r
+若返回值中< data_sent >字段数值大于<acked_recv>字段数值,则有可能是数据没有发送到\r
+\r
+服务器,或者是服务器接收有延时还未收到数据,需要在服务器和模块同时抓取TCP 层数据\r
+\r
+来排查,可以联系我司FAE 跟进排查。\r
+\r
+###### 模块接收TCP 数据失败\r
+\r
+若模块正常建立TCP 非透传客户端连接,但是接收TCP 数据失败。可以参考以下方式排查解决\r
+\r
+(涉及到的AT 命令,请参考功能 AT 指令集):\r
+\r
+1. \r
+确认是否是服务器问题。\r
+\r
+可通过更换服务器对比确认是否是服务器配置问题。\r
+\r
+2. \r
+确认SIM 卡是否也有欠费。\r
+\r
+可联系运营商确认。\r
+\r
+3. \r
+确认TCP 连接是否还存在。\r
+\r
+可通过”AT+IPSTATUS=<SocketID>”命令的返回值来确认。\r
+\r
+− \r
+若返回值中含有“CONNECT”字符串,则表示TCP 连接存在。\r
+\r
+− \r
+若返回“DISCONNECT” 字符串,则表示TCP 连接不存在。\r
+\r
+4. \r
+确认是否是长时间没有发送数据导致的假连接。\r
+\r
+可通过缩短心跳周期或者是提高数据发送频率来确认。\r
+\r
+− \r
+若缩短心跳周期或者是提高数据发送频率后,数据接收正常则可能是假连接引起的问题。\r
+\r
+功能模组TCP 应用指南 \r
+第3 章标准指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+16\r
+\r
+− \r
+若缩短心跳或提高数据发送频率仍无法确认接收不到数据的原因,需要在服务器和模块同时\r
+\r
+抓取TCP 层数据来排查,可以联系我司FAE 跟进排查。\r
+\r
+###### 3.1.2 示例\r
+\r
+1. \r
+设置APN、用户名、密码,检查并确保模块已成功注册上PS 域\r
+\r
+AT \r
+//确认模块能正常响应AT 命令 \r
+OK \r
+AT+CGSN \r
++CGSN: "865324037375316" \r
+OK \r
+AT+CGMR \r
++CGMR: N720_EAB0CM_BZ_V008 \r
+OK \r
+AT+CPIN?\r
+\r
++CPIN: READY \r
+//确认模块能识别到SIM 卡 \r
+OK\r
+\r
+AT+CIMI \r
+460016657520885 \r
+OK\r
+\r
+AT+CGDCONT=1,"IP","UNINET" \r
+//设置APN \r
+OK \r
+AT+XGAUTH=1,1, "CARD","CARD" \r
+//设置用户名、密码、鉴权方式\r
+\r
+OK\r
+\r
+AT+CSQ \r
++CSQ: 31, 99 \r
+//确认信号良好 \r
+OK \r
+AT+CREG=2 \r
+OK \r
+AT+CREG? \r
++CREG: 2,1,"FFFE","61C7E47",7 //确认模块注册上CS 域 \r
+OK \r
+AT+CREG=0 \r
+OK\r
+\r
+AT+CGATT? \r
++CGATT: 1 \r
+//确认模块附着上PS 域 \r
+OK \r
+AT$MYSYSINFO \r
+$MYSYSINFO: 4,02 \r
+//确认模块注册的网络制式 \r
+OK\r
+\r
+2. \r
+建立PPP 连接\r
+\r
+At+XIIC=1 \r
+//激活PPP 连接 \r
+OK \r
+AT+XIIC? \r
+//查询模块本地IP \r
++XIIC: 1,10.211.164.210 \r
+OK\r
+\r
+3. \r
+建立TCP 非透传客户端连接并收发数据\r
+\r
+AT+TCPCLOSE=0 \r
+ \r
+ \r
+//主动关闭待用SocketID \r
++TCPCLOSE: ERROR\r
+\r
+AT+TCPSETUP=0,183.239.240.40,5002 \r
+OK\r
+\r
+功能模组TCP 应用指南 \r
+第3 章标准指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+17\r
+\r
++TCPSETUP: 0,OK \r
+AT+IPSTATUS=0 \r
+ \r
+ \r
+ \r
+ //确认TCP 连接是否正常 \r
++IPSTATUS: 0,CONNECT,TCP,4096 \r
+AT+TCPACK=0 \r
+ \r
+ \r
+ \r
+ //确认数据发送情况 \r
++TCPACK: 0,0,0 \r
+AT+TCPSEND=0,10 \r
+> \r
+OK\r
+\r
++TCPSEND: 0,10 \r
++TCPRECV: 0,6,Neoway \r
+AT+IPSTATUS=0 \r
++IPSTATUS: 0,CONNECT,TCP,4096 \r
+AT+TCPACK=0 \r
+ \r
+ \r
+ \r
+ \r
+//确认数据发送情况 \r
++TCPACK: 0,10,10 \r
+AT+TCPSEND=0,10 \r
+ \r
+ \r
+ \r
+ \r
+//发送数据 \r
+>\r
+\r
+OK\r
+\r
++TCPSEND: 0,10 \r
++TCPRECV: 0,6,Neoway \r
+AT+TCPCLOSE=0\r
+\r
++TCPCLOSE: 0,OK \r
+AT+CGATT=0 \r
+GPRS DISCONNECTION\r
+\r
+OK\r
+\r
+功能模组TCP 应用指南 \r
+第3 章标准指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+18\r
+\r
+##### 3.2 TCP 非透传服务器端\r
+\r
+本节主要实现TCP 非透传服务端功能相关的AT 指令、AT 流程、异常处理以及简单示例。在进行\r
+\r
+PPP 拨号前,请确保模块已经过初始化(具体初始化流程参考第2 章)。\r
+\r
+图 3-2 连接流程\r
+\r
+Yes\r
+\r
+设置PPP拨号参数\r
+\r
+"OK"?\r
+\r
+1. AT+XIIC=1\r
+\r
+"+XIIC: 1"?\r
+\r
+2. AT+XIIC?\r
+\r
+"ERROR" or "+CLOSELISTEN: 0,local\r
+\r
+link closed" ?\r
+\r
+3. AT+CLOSELISTEN\r
+\r
+Yes\r
+\r
+4. AT+TCPLISTEN=<Port>\r
+\r
+" +TCPLISTEN: 0,OK" ?\r
+\r
+5.AT+CLIENTSTATUS=Y\r
+\r
+"+CLIENTSTATUS: \r
+Y,CONNECT,TCP,61440" ?\r
+\r
+9. AT+CGATT=0\r
+\r
+"OK" or "ERROR"?\r
+\r
+No\r
+\r
+No\r
+\r
+Yes\r
+重连<3次?\r
+\r
+查询<3次?\r
+\r
+关机\r
+\r
+No\r
+\r
+No\r
+\r
+Yes\r
+\r
+模块重启\r
+模块初始化流程\r
+\r
+6. AT+TCPSENDS=Y,<Length>\r
+\r
+7. AT+TCPACKS=Y\r
+\r
+"+TCPACKS: \r
+Y,<Data_send>,<Acked_data>"\r
+\r
+No\r
+\r
+查询<5次?\r
+\r
+重发<3次?\r
+\r
+查询<5次?\r
+\r
+No\r
+\r
+Yes\r
+\r
+No\r
+\r
+No\r
+No\r
+Yes\r
+\r
+No\r
+Yes\r
+No\r
+\r
+1. 进行PPP拨号\r
+- 若返回"OK",则可进入下一步。\r
+- 若未收到任何回码或收到其他错误回码,则继续拨号,1s/次,最多\r
+\r
+3次。若重新拨号3次后,仍未若返回"OK ",则重启模块。\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+4. 建立服务器端侦听\r
+- 若返回"+TCPLISTEN: 0,OK"或者"Listening...",则可进入下一步。\r
+- 若出现以下情况,则继续建立侦听,最多3次。\r
+- 收到回码:"ERROR"/"+TCPLISTEN:bind error"。\r
+- 收到其他错误回码或者未收到任何回码。\r
+- 若重新建立3次侦听,仍未返回"+TCPLISTEN: 0,OK"或者"Listening..."\r
+\r
+则重启模块。\r
+\r
+等待接收客户端连接请求\r
+需要设置等待客户端连接的超时时间,若超时时间到,没有收到任\r
+何客户端连接请求,则主动重新拨号建立侦听。\r
+\r
+5. 查询链路状态\r
+- 若返回"+CLIENTSTATUS: Y,CONNECT,TCP,61440",则进行下一步。\r
+- 若回码包含"+CLIENTSTATUS: Y,DISCONNECT",则客户端连接未建立或者已经断\r
+\r
+开,等待客户端重新建立TCP连接。\r
+• 若回码包含"+CLIENTSTATUS: Y,CONNECT",Buffer大小不为61440,则循环查询链\r
+\r
+路状态,最多5次。若查询5次后,Buffer大小不为61440,则关闭当前连接,\r
+等待客户端重新建立TCP连接。\r
+• 若未返回任何回码或者返回其他错误回码,则继续查询1s/次,最多3次。若查\r
+\r
+询3次后,仍无回码或者返回其他错误回码,则重启模块。\r
+\r
+7. 查询数据发送情况\r
+- 若返回"+TCPACK: Y,<Data_send>,<Acked_data>"且<Data_send>与\r
+\r
+<Acked_data>数值相等,则可进入下一步:继续发送数据或者接收\r
+数据。\r
+• 若返回"+TCPACK: Y,DISCONNECT",则返回重新建立TCP连接。\r
+• 若返回"+TCPACK: Y,<Data_send>,<Acked_data>"且若<Data_send>大\r
+\r
+于<Acked_data>数值,则继续查询,1s/次,最多5次。若查询5次,\r
+回码中<Data_send>与<Acked_data>数值仍不相等,则返回关闭当\r
+前连接,重新建立连接,以释放缓冲区。\r
+• 若收到其他错误回码、超时时间到未收到任何回码,则重启模块。\r
+\r
+9.去附着网络\r
+若返回"OK"或者" ERROR",则进入下一步,等返回的超时时间为15s。\r
+若超时未收到回码或者收到其他错误回码,则继续执行,最多3次,\r
+若重复关闭3次后,仍未收到"OK"或者" ERROR"则重启模块。\r
+\r
++TCPRECV(S): Y,<length>,<data>\r
+\r
+"ERROR" or "+CLOSELISTEN: 0,local\r
+\r
+link closed" ?\r
+\r
+6. 发送数据\r
+- 若返回">",则延时50-100ms,将数据发送到模块串口,等待数据发\r
+\r
+- 送成功的回码"+TCPSENDS: Y,<Length>",超时时间为30s。\r
+- 若返回"+TCPSENDS: Y,<Length>",则进入下一步。\r
+- 若返回"+TCPSENDS:SOCKET ID NOT ACTIVE",则等待客户端连接请\r
+\r
+求。\r
+• 若收到其他错误回码、超时时间到未收到任何回码,则继续执行该\r
+\r
+命令重发数据,最多3次。若重发3次仍然失败,则重启模块。\r
+\r
+"+TCPSENDS: Y,<Length>" ?\r
+\r
+8. AT+CLOSELISTEN\r
+\r
+查询<30次?\r
+\r
+重新建立侦听<3次?\r
+\r
+关闭<3次?\r
+\r
+去附着<3次?\r
+\r
+Yes\r
+\r
+No\r
+\r
+Yes\r
+\r
+No\r
+\r
+No\r
+Yes\r
+\r
+Yes\r
+No\r
+No\r
+\r
+2. PPP连接建立情况\r
+- 若回码中包含"+XIIC: 1 ",则可进入下一步。\r
+- 若回码未包含"+XIIC: 1",则循环查询,1s/次,最多30次。若30次\r
+\r
+后仍未返回"+XIIC: 1",则重新拨号,最多重拨3次,若重新拨号3次\r
+后,仍未获取到IP,则重启模块。\r
+\r
+3. 关闭服务器侦听\r
+- 该指令等待返回回码的超时时间为20s。\r
+- 若返回"+CLOSELISTEN: 0,local link closed"或者"ERROR",则可进入下\r
+\r
+- 一步。\r
+- 若超时未收到回码或收到其他错误回码,则继续关闭,最多3次。\r
+- 若重复关闭3次后,仍未收到"+CLOSELISTEN: 0,local link closed"或者\r
+\r
+"ERROR"则重启模块。\r
+\r
+Connect AcceptSocket=Y,\r
+ClientAddr=119.123.77.133,ClientPort=8000\r
+\r
+8. 关闭服务器侦听\r
+- 该指令等待返回回码的超时时间为20s。若返回"+CLOSELISTEN:\r
+\r
+- 0,local link closed"或者"ERROR",则可进入下一步。\r
+- 若超时未收到回码或收到其他错误回码,则继续关闭,最多3次。\r
+- 若重复关闭3次后,仍未收到"+CLOSELISTEN: 0,local link closed"或者\r
+\r
+"ERROR"则重启模块。\r
+\r
+No\r
+\r
+No\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+功能模组TCP 应用指南 \r
+第3 章标准指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+19\r
+\r
+###### 3.2.1 异常处理\r
+\r
+###### 模块附着PS 域失败\r
+\r
+1. \r
+检查模块串口通信是否正常。若串口不通,可参考以下方式排查解决:\r
+\r
+− \r
+确保模块已经开机:若模块不开机需要检查电源设计和开机时序\r
+\r
+− \r
+确保串口电平变换正常:若电平变换不正常需要检查串口电路设计和器件选型\r
+\r
+− \r
+确保软件上串口配置正常,软件处理是否有误。比如自适应逻辑、是否带回车换行发送AT\r
+\r
+2. \r
+检查模块是否已经识别到SIM 卡。若模块无法识别SIM 卡,可参考以下方式排查解决(涉及到的\r
+\r
+AT 命令,请参考功能 AT 指令集):\r
+\r
+− \r
+确保SIM 卡是否插好、SIM 卡本身完好无损、与卡座接触是否良好\r
+\r
+− \r
+确保SIM 卡座相关电路无设计问题,是否有虚假焊等\r
+\r
+− \r
+确保SIM 卡没有被锁卡\r
+\r
+若通过以上排查模块仍无法识别到SIM 卡,可以联系运营商处理或者更换SIM 卡解决。\r
+\r
+3. \r
+查询当前模块的接收信号是否满足通信条件。\r
+\r
+我们建议信号强度值为12(-83dbm)再进行通信。若现场信号弱,可通过以下方式排查:\r
+\r
+− \r
+确保天线接好、天线本身完好无损(断路、短路)\r
+\r
+− \r
+确认模块注册的网络制式(AT$MYSYSINFO)等信息,若为现场网络环境问题,则联系运营\r
+\r
+商增强网络覆盖,或者更换其他运营商SIM 卡解决。\r
+\r
+4. \r
+若现场信号良好,模块能识别到SIM 卡且SIM 卡未欠费,但模块不能附着上PS 域,则需要联系\r
+\r
+运营商确定SIM 卡配置:\r
+\r
+− \r
+SIM 卡是否开通数据业务\r
+\r
+− \r
+SIM 卡配置鉴权参数是否正常\r
+\r
+− \r
+SIM 卡是否有绑定设备\r
+\r
+− \r
+SIM 卡数据业务相关的APN、用户名、密码、鉴权方式是否正常\r
+\r
+###### 模块激活PDP 失败\r
+\r
+若模块正常注册上PS 域,但是无法激活PPP 连接。可以参考以下方式排查解决(涉及到的AT 命\r
+\r
+令,请参考功能 AT 指令集):\r
+\r
+功能模组TCP 应用指南 \r
+第3 章标准指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+20\r
+\r
+⚫ \r
+确保SIM 未欠费。\r
+\r
+⚫ \r
+使用AT+CGDCONT?/AT+XGAUTH?查询APN、用户名、密码,确保这些参数与运营商提供的\r
+\r
+一致。\r
+\r
+若以上参数都没有问题,模块仍无法激活PPP 连接,请联系当地运营商协助检查SIM 卡和后台服\r
+\r
+务器参数配置。\r
+\r
+###### 客户端无法连接服务器\r
+\r
+若模块正常建立服务器端侦听后,客户端无法连接上服务器。可以参考以下方式排查解决(涉及到\r
+\r
+的AT 命令,请参考功能 AT 指令集):\r
+\r
+1. \r
+确认服务器IP 和端口都已配置好,端口已经建立服务器端侦听,IP 能被客户端PING 通。\r
+\r
+可以通过用其他客户端设备来连接该服务器IP 和端口确认。\r
+\r
+2. \r
+确认模块获取到的IP 与客户端的IP 是否在同一个网段内。\r
+\r
+可通过在PC 上PING 模块来确认。\r
+\r
+− \r
+若PC 客户端能PING 通服务器,则需要检查服务器软件和端口配置。\r
+\r
+− \r
+若PC 客户端不能PING 通服务器,则需要检查APN、用户名、密码设置是否正确,SIM 卡\r
+\r
+配置是否正确。\r
+\r
+3. \r
+确认SIM 卡配置是否有问题。\r
+\r
+可以直接联系运营商客户端是否可以访问SIM 卡的IP 或者让运营商直接跟踪后台消息排查原因。\r
+\r
+###### 3.2.2 示例\r
+\r
+1. \r
+设置APN、用户名、密码,检查并确保模块已成功注册上PS 域\r
+\r
+AT \r
+//确认模块能正常响应AT 命令 \r
+OK\r
+\r
+AT+CGSN \r
++CGSN: "865324037375316" \r
+OK\r
+\r
+AT+CGMR \r
++CGMR: N720_EAB0CM_BZ_V008 \r
+OK \r
+AT+CPIN? \r
++CPIN: READY \r
+//确认模块能识别到SIM 卡 \r
+OK \r
+AT+CIMI \r
+460016657520885 \r
+OK\r
+\r
+AT+CGDCONT=1,"IP","UNINET" \r
+//设置APN \r
+OK \r
+AT+XGAUTH=1,1, "CARD","CARD" \r
+//设置用户名、密码、鉴权方式\r
+\r
+功能模组TCP 应用指南 \r
+第3 章标准指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+21\r
+\r
+OK \r
+AT+CSQ \r
++CSQ: 31, 99 \r
+//确认信号良好 \r
+OK \r
+AT+CREG=2 \r
+OK \r
+AT+CREG? \r
++CREG: 2,1,"FFFE","61C7E47",7 //确认模块注册上CS 域\r
+\r
+OK \r
+AT+CREG=0 \r
+OK \r
+AT+CGATT? \r
++CGATT: 1 \r
+//确认模块附着上PS 域 \r
+OK \r
+AT$MYSYSINFO \r
+$MYSYSINFO: 4,02 \r
+//确认模块注册的网络制式 \r
+OK\r
+\r
+2. \r
+建立PPP 连接\r
+\r
+AT+XIIC=1 \r
+//激活PPP 连接 \r
+OK\r
+\r
+AT+XIIC? \r
+//查询模块本地IP \r
++XIIC: 1,10.211.164.210 \r
+OK\r
+\r
+3. \r
+建立TCP 非透传服务器端连接并收发数据\r
+\r
+AT+CLOSELISTEN \r
+ \r
+ \r
+ \r
+ //主动关闭待用SocketID \r
+ERROR \r
+AT+TCPLISTEN=6800 \r
+ \r
+ \r
+//建立侦听 \r
+ \r
++TCPLISTEN:0,OK \r
+Connect AcceptSocket=1,ClientAddr=119.123.77.133 //客户端连接建立 \r
+AT+CLIENTSTATUS=1 \r
+ \r
+ \r
+ \r
+ //确认TCP 连接是否正常 \r
++IPSTATUS: 1,CONNECT,TCP,1024\r
+\r
+```\r
+AT+TCPSENDS=1,10\r
+```\r
+\r
+> \r
+OK\r
+\r
++TCPSENDS: 1,10 \r
+AT+TCPACKS=1 \r
+ \r
+ \r
+ \r
+ //确认数据发送情况\r
+\r
++TCPACKS: 1,10,10 \r
++TCPRECV(S): 1,6,Neoway \r
+AT+ CLOSELISTEN \r
+ \r
+ \r
+ \r
+ \r
+ \r
+//关闭侦听\r
+\r
++CLOSELISTEN: 0,local link closed\r
+\r
+AT+CGATT=0 \r
+GPRS DISCONNECTION \r
+OK\r
+\r
+功能模组TCP 应用指南 \r
+第3 章标准指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+22\r
+\r
+##### 3.3 TCP 透传客户端\r
+\r
+本节主要实现TCP 透传客户端功能相关的AT 指令、AT 流程、异常处理以及简单示例。在进行PPP\r
+\r
+拨号前,请确保模块已经过初始化(具体初始化流程参考第2 章)。\r
+\r
+图 3-3 连接流程\r
+\r
+设置PPP拨号参数\r
+\r
+"OK"?\r
+\r
+1. AT+XIIC=1\r
+\r
+"+XIIC: 1"?\r
+\r
+2. AT+XIIC?\r
+\r
+"+TRANSCLOSE: 0,OK" or " ERROR"?\r
+\r
+3. AT+TRANSCLOSE\r
+\r
+Yes\r
+\r
+4. AT+TCPTRANS=<IP/域名>,<Port>\r
+\r
+"+TCPTRANS: OK"?\r
+\r
+收发数据\r
+\r
+9. AT+CGATT=0\r
+\r
+"OK" or "ERROR"?\r
+\r
+No\r
+\r
+Yes\r
+重连<3次?\r
+\r
+查询<3次?\r
+\r
+关机\r
+\r
+No\r
+\r
+No\r
+\r
+Yes\r
+\r
+模块重启\r
+模块初始化流程\r
+\r
+6. AT+TCPACK\r
+\r
+"+TCPACK: \r
+X,<Data_send>,<Acked_data>"?\r
+\r
+No\r
+\r
+退出透传<3次?\r
+\r
+查询<5次?\r
+\r
+Yes\r
+\r
+No\r
+\r
+1. 进行PPP拨号\r
+- 若返回"OK",则可进入下一步。\r
+- 若未收到任何回码或收到其他错误回码,则继续拨号,1s/次,最多\r
+\r
+3次。若重新拨号3次后,仍未若返回"OK ",则重启模块。\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+4. 建立TCP连接\r
+- 该指令等待返回"+TCPTRANS: OK"的超时时间为30s\r
+- 若返回"+TCPSETUP: X,OK",则可进入下一步。\r
+- 若出现以下情况,则继续重连,最多3次,若3次仍无法建立TCP连\r
+\r
+接,则重启模块。\r
+ -- 收到回码:"+TCPSETUP: X,FAIL"/"+TCPSETUP: ERROR"\r
+ -- 收到其他错误回码或者未收到任何回码\r
+\r
+5. 退出透传\r
+- 若返回"OK",则进入下一步。\r
+- 若返回其他错误回码、或者未返回任何回码,则继续重发,最多3\r
+\r
+次。若重发3次仍未收到"OK",则重启模块。\r
+• "+++"必须以字符串的形式发送到串口,且"+++"不必带回车发送\r
+\r
+8. 关闭Socket\r
+- 若返回"+TCPCLOSE: X,OK""+TCPCLOSE: ERROR"则进入下一步,等待\r
+\r
+返回的超时时间为30s。\r
+• 若超时未收到回码或收到其他错误回码,则继续关闭,最多3次,\r
+\r
+若重复关闭3次后,仍未收到"+TCPCLOSE: X,OK"或者"+TCPCLOSE: \r
+ERROR"则重启模块。\r
+\r
+9. 去附着网络\r
+- 若返回"OK"或者" ERROR",则进入下一步,等返回的超时时间为\r
+\r
+15s。\r
+• 若超时未收到回码或者收到其他错误回码,则继续执行,最多3\r
+\r
+次,若重复关闭3次后,仍未收到"OK"或者" ERROR"则重启模块。\r
+\r
+7. ATO\r
+\r
+"+TRANSCLOSE: 0,OK"\r
+\r
+or "ERROR"?\r
+\r
+6. 查询数据发送情况\r
+- 若返回"+TCPACK: X,<Data_send>,<Acked_data>"且<Data_send>与\r
+\r
+<Acked_data>数值相等,则可进入下一步:继续发送数据或者接收\r
+数据。\r
+• 若返回"+TCPACK: 0,DISCONNECT",则返回重新建立TCP连接。\r
+• 若返回"+TCPACK: X,<Data_send>,<Acked_data>"且若<Data_send>大\r
+\r
+于<Acked_data>数值,则循环查询循环查询1s/次,最多5次。若查\r
+询5次,回码中<Data_send>与<Acked_data>数值仍不相等,则返回\r
+关闭当前连接,重新建立连接,以释放缓冲区。\r
+• 若收到其他错误回码、超时时间到未收到任何回码,则重启模\r
+\r
+块。\r
+\r
+"OK"?\r
+\r
+8. AT+TRANSCLOSE\r
+\r
+查询<30次?\r
+\r
+关闭<3次?\r
+\r
+关闭<3次?\r
+\r
+去附着<3次?\r
+\r
+Yes\r
+\r
+No\r
+\r
+Yes\r
+\r
+No\r
+\r
+2. 查询PPP连接建立情况\r
+- 若回码中包含"+XIIC: 1 ",则可进入下一步。\r
+- 若回码未包含"+XIIC: 1",则循环查询,1s/次,最多30次。若30次\r
+\r
+后仍未返回"+XIIC: 1",则重新拨号,最多重拨3次,若重新拨号3次\r
+后,仍未获取到IP,则重启模块。\r
+\r
+3. 关闭连接\r
+- 该指令等待返回"+TRANSCLOSE: 0,OK""ERROR"的超时时间为30s。\r
+- 若超时未收到回码或收到其他错误回码,则继续关闭,最多3次。\r
+- 若重复关闭3次后,仍未收到"+TRANSCLOSE: 0,OK"或"ERROR"则重\r
+\r
+启模块。\r
+\r
++++\r
+\r
+"CONNECT" ?\r
+返回透传<3次?\r
+\r
+Yes\r
+\r
+No\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+No\r
+\r
+No\r
+\r
+No\r
+\r
+7. 返回透传模式\r
+- 若返回"CONNECT",则进入下一步:继续发送数据。\r
+- 若返回其他错误回码、或者未返回任何回码,则继续重发,最多3\r
+\r
+次。若重发3次仍未收到"OK",则重启模块。\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+No\r
+\r
+No\r
+\r
+No\r
+\r
+No\r
+\r
+No\r
+\r
+No\r
+\r
+No\r
+\r
+功能模组TCP 应用指南 \r
+第3 章标准指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+23\r
+\r
+###### 3.3.1 异常处理\r
+\r
+###### 模块附着PS 域失败\r
+\r
+1. \r
+使用AT+CPIN?检查模块是否已经识别到SIM 卡。若模块无法识别SIM 卡,可参考以下方式排查\r
+\r
+解决(涉及到的AT 命令,请参考功能 AT 指令集):\r
+\r
+− \r
+确保SIM 卡是否插好;\r
+\r
+− \r
+确保SIM 卡本身完好无损;\r
+\r
+− \r
+确保SIM 卡没有被锁卡;\r
+\r
+− \r
+确保SIM 卡卡座完好,不存在假焊;\r
+\r
+− \r
+擦拭SIM 卡及卡座的触点(该触点发生氧化、脏污也会产生读不到SIM 卡的现象),确保\r
+\r
+SIM 卡与模块卡座接触良好。\r
+\r
+若通过以上排查模块仍无法识别到SIM 卡,可以更换SIM 卡解决。\r
+\r
+2. \r
+检查天线是否完好,使用AT+CSQ 命令查询当前模块的接收信号是否满足通信条件。\r
+\r
+我们建议信号强度值为15(-83dbm)再进行通信。\r
+\r
+3. \r
+若现场信号良好,模块能识别到SIM 卡且SIM 卡未欠费,但模块不能附着上PS 域,可通过\r
+\r
+```\r
+AT+CGATT=1 命令,主动附着PS 域。\r
+```\r
+\r
+###### 模块激活PDP 失败\r
+\r
+若模块正常注册上PS 域,但是无法激活PPP 连接。可以参考以下方式排查解决(涉及到的AT 命\r
+\r
+令,请参考功能 AT 指令集):\r
+\r
+⚫ \r
+确保SIM 卡未欠费。\r
+\r
+⚫ \r
+使用AT+CGDCONT?/AT+NETAPN?查询APN、用户名、密码,确保这些参数与运营商提供的\r
+\r
+一致。\r
+\r
+若以上参数都没有问题,模块仍无法激活PPP 连接,请联系当地运营商协助检查SIM 卡和后台服\r
+\r
+务器参数配置。\r
+\r
+###### 模块建立TCP 透传客户端连接失败\r
+\r
+若模块正常获取到IP,但是建立TCP 非透传客户端连接。可以参考以下方式排查解决(涉及到的\r
+\r
+AT 命令,请参考功能 AT 指令集):\r
+\r
+功能模组TCP 应用指南 \r
+第3 章标准指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+24\r
+\r
+1. \r
+确认服务器IP 和端口都已配置好,端口已经建立服务器端侦听,IP 能被客户端PING 通。\r
+\r
+可以通过用其他客户端设备来连接该服务器IP 和端口确认。\r
+\r
+2. \r
+确认模块获取到的IP 是否能访问服务器。\r
+\r
+可通过发送AT+PING=<IP/域名>给模块,操作模块PINGf 服务器来确认。\r
+\r
+− \r
+若模块能PING 通服务器,则需要检查服务器软件和端口配置。\r
+\r
+− \r
+若模块不能PING 通服务器,则需要检查APN、用户名、密码设置是否正确,SIM 卡配置是\r
+\r
+否正确。\r
+\r
+3. \r
+确认SIM 卡配置是否有问题。\r
+\r
+可以直接联系运营商确认SIM 卡是否可以访问目的IP 或者让运营商直接跟踪后台消息排查原因。\r
+\r
+###### 模块发送TCP 数据失败\r
+\r
+若模块正常建立TCP 非透传客户端连接,但是发送TCP 数据失败。可以参考以下方式排查解决\r
+\r
+(涉及到的AT 命令,请参考功能 AT 指令集):\r
+\r
+1. \r
+确认是否是服务器问题。\r
+\r
+可通过更换服务器对比确认是否是服务器配置问题。\r
+\r
+2. \r
+确认SIM 卡是否也有欠费。\r
+\r
+可联系运营商确认。\r
+\r
+3. \r
+确认是否是长时间没有发送数据导致的假连接。\r
+\r
+可通过缩短心跳周期或者是提高数据发送频率来确认。\r
+\r
+若缩短心跳周期或者是提高数据发送频率后,数据发送正常则可能是假连接引起的问题。\r
+\r
+4. \r
+确认TCP 连接是否还存在。\r
+\r
+可通过”AT+IPSTATUS=<SocketID>”命令的返回值来确认。\r
+\r
+− \r
+若返回值中含有“CONNECT”字符串,则表示TCP 连接存在。\r
+\r
+− \r
+若返回“DISCONNECT” 字符串,则表示TCP 连接不存在。\r
+\r
+5. \r
+确认终端(或主控)是否已经将数据发送到模块。\r
+\r
+可以通过模块串口与终端(或主控)串口之间交互的AT 命令和数据内容来确认。\r
+\r
+为此,我司建议主控具备保存或打印模块与终端串口交互log 功能。只要AT+TCPSEND 命令返\r
+\r
+回“+TCPSEND: <SocketID>,<DataLength>”就表示主控数据已经发送到模块。\r
+\r
+6. \r
+确认模块底层协议栈是否收到服务器端TCP 协议层的ACK。\r
+\r
+功能模组TCP 应用指南 \r
+第3 章标准指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+25\r
+\r
+可通过”AT+TCPACK=<SocketID>”命令的返回值来确认。\r
+\r
+− \r
+若返回值中< data_sent >字段与<acked_recv>字段数值相同,则表示数据已经发送到服务器\r
+\r
+且服务器已经确认。\r
+\r
+− \r
+若返回值中< data_sent >字段数值大于<acked_recv>字段数值,则有可能是数据没有发送到\r
+\r
+服务器,或者是服务器接收有延时还未收到数据,需要在服务器和模块同时抓取TCP 层数据\r
+\r
+来排查,可以联系我司FAE 跟进排查。\r
+\r
+###### 模块接收TCP 数据失败\r
+\r
+若模块正常建立TCP 非透传客户端连接,但是接收TCP 数据失败。可以参考以下方式排查解决\r
+\r
+(涉及到的AT 命令,请参考功能 AT 指令集):\r
+\r
+1. \r
+确认是否是服务器问题。\r
+\r
+可通过更换服务器对比确认是否是服务器配置问题。\r
+\r
+2. \r
+确认SIM 卡是否也有欠费。\r
+\r
+可联系运营商确认。\r
+\r
+3. \r
+确认TCP 连接是否还存在。\r
+\r
+可通过”AT+IPSTATUS=<SocketID>”命令的返回值来确认。\r
+\r
+− \r
+若返回值中含有“CONNECT”字符串,则表示TCP 连接存在。\r
+\r
+− \r
+若返回“DISCONNECT” 字符串,则表示TCP 连接不存在。\r
+\r
+4. \r
+确认是否是长时间没有发送数据导致的假连接。\r
+\r
+可通过缩短心跳周期或者是提高数据发送频率来确认。\r
+\r
+− \r
+若缩短心跳周期或者是提高数据发送频率后,数据接收正常则可能是假连接引起的问题。\r
+\r
+− \r
+若缩短心跳或提高数据发送频率仍无法确认接收不到数据的原因,需要在服务器和模块同时\r
+\r
+抓取TCP 层数据来排查,可以联系我司FAE 跟进排查。\r
+\r
+###### 3.3.2 示例\r
+\r
+1. \r
+设置APN、用户名、密码,检查并确保模块已成功注册上PS 域\r
+\r
+AT \r
+//确认模块能正常响应AT 命令 \r
+OK \r
+AT+CGSN \r
++CGSN: "865324037375316"\r
+\r
+OK\r
+\r
+```\r
+AT+CGMR\r
+```\r
+\r
++CGMR: N720_EAB0CM_BZ_V008 \r
+OK\r
+\r
+功能模组TCP 应用指南 \r
+第3 章标准指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+26\r
+\r
+AT+CPIN? \r
++CPIN: READY \r
+//确认模块能识别到SIM 卡 \r
+OK \r
+AT+CIMI \r
+460016657520885 \r
+OK \r
+AT+CGDCONT=1,"IP","UNINET" \r
+//设置APN \r
+OK\r
+\r
+AT+XGAUTH=1,1, "CARD","CARD" \r
+//设置用户名、密码、鉴权方式 \r
+OK \r
+AT+CSQ \r
++CSQ: 31, 99 \r
+//确认信号良好 \r
+OK \r
+AT+CREG=2 \r
+OK \r
+AT+CREG? \r
++CREG: 2,1,"FFFE","61C7E47",7 //确认模块注册上CS 域\r
+\r
+OK \r
+AT+CREG=0\r
+\r
+OK \r
+AT+CGATT? \r
++CGATT: 1 \r
+//确认模块附着上PS 域\r
+\r
+OK \r
+AT$MYSYSINFO \r
+$MYSYSINFO: 4,02 \r
+//确认模块注册的网络制式\r
+\r
+OK\r
+\r
+2. \r
+建立PPP 连接\r
+\r
+At+XIIC=1 \r
+//激活PPP 连接 \r
+OK \r
+AT+XIIC? \r
+//查询模块本地IP \r
++XIIC: 1,10.211.164.210 \r
+OK\r
+\r
+3. \r
+建立TCP 非透传服务器端连接并收发数据\r
+\r
+```\r
+AT+TRANSCLOSE \r
+ERROR \r
+AT+TCPTRANS=183.239.240.40,5009 //建立TCP 透传连接\r
+```\r
+\r
+OK \r
++TCPTRANS: OK\r
+\r
+//收发数据,发送+++(不带回车换行)退出透传 \r
+OK \r
+AT+IPSTATUS \r
+//查询链路状态\r
+\r
++IPSTATUS: CONNECT,TCP,4096\r
+\r
+ATO \r
+//切换回透传模式 \r
+CONNECT \r
+ \r
+ \r
+ \r
+ \r
+//收发数据,发送+++(不带回车换行)退出透传 \r
+OK \r
+AT+TRANSCLOSE \r
+//关闭透传连接 \r
++TRANSCLOSE: 0,OK \r
+AT+CGATT=0 \r
+GPRS DISCONNECTION \r
+OK\r
+\r
+功能模组TCP 应用指南 \r
+第4 章国网指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+27\r
+\r
+#### 4 国网指令TCP 应用\r
+\r
+本章主要介绍如何使用有方标准指令实现TCP 功能(包括TCP 非透传客户端、TCP 非透传服务器\r
+\r
+端、TCP 透传客户端功能) 。内容包括对应功能相关的AT 指令、AT 流程、异常处理以及简单示例。\r
+\r
+连接注意事项:\r
+\r
+⚫ \r
+发送AT 命令后必须等待收到回码后,才能发送下一条AT 命令。\r
+\r
+⚫ \r
+在建立PPP 连接后,主动去附着前,只要收到"$MYURCACT: 0,0",则返回重新激活PPP 连接。\r
+\r
+⚫ \r
+在建立TCP 连接后,主动断开TCP 连接前,只要收到"$MYURCCLOSE: X",则返回重新建立TCP\r
+\r
+连接。\r
+\r
+##### 4.1 TCP 非透传客户端\r
+\r
+本节主要实现TCP 非透传客户端功能相关的AT 指令、AT 流程、异常处理以及简单示例。在进行\r
+\r
+PPP 拨号前,请确保模块已经过初始化(具体初始化流程参考第2 章)。\r
+\r
+功能模组TCP 应用指南 \r
+第4 章国网指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+28\r
+\r
+图 4-1 连接流程\r
+\r
+设置PPP拨号参数\r
+\r
+"$MYURCACT: 0,1"?\r
+\r
+1. AT$MYNETACT=0,1\r
+\r
+"$MYNETACT: 0,1"?\r
+\r
+2. AT$MYNETACT?\r
+\r
+"$MYNETCLOSE: X"\r
+\r
+or "ERROR: 913"?\r
+\r
+3. AT$MYNETCLOSE=X\r
+\r
+Yes\r
+\r
+4. AT$MYNETSRV=0,X,0,0,"IP:Port"\r
+\r
+"OK"?\r
+\r
+5. AT$MYNETOPEN=X\r
+\r
+" $MYNETOPEN: X,2000" or"\r
+\r
+ERROR: 912" ?\r
+\r
+9. AT+CGATT=0\r
+\r
+"OK" or "ERROR"?\r
+\r
+No\r
+\r
+Yes\r
+重设<3次?\r
+\r
+拨号<3次?\r
+\r
+关机\r
+\r
+No\r
+\r
+No\r
+\r
+Yes\r
+\r
+模块重启\r
+模块初始化流程\r
+\r
+6. AT$MYNETWRITE=X,<Length>\r
+\r
+"$MYNETWRITE: X,<Length>" ?\r
+\r
+输入要发送的数据\r
+\r
+"OK"?\r
+\r
+No\r
+\r
+重新开启服务<3次?\r
+\r
+重发<3次?\r
+\r
+查询<5次?\r
+\r
+No\r
+\r
+Yes\r
+\r
+No\r
+\r
+No\r
+No\r
+Yes\r
+\r
+No\r
+Yes\r
+No\r
+\r
+1. PPP拨号\r
+\r
+• 若返回"$MYURCACT: 0,1"或"ERROR: 902",则进入下一步。该指令等待返\r
+\r
+回"$MYURCACT: 0,1"或"ERROR: 902"的超时时间为30s。\r
+• 若未收到任何回码或收到其他错误回码,则继续拨号,最多3次。若重\r
+\r
+新拨号3次后,仍未若返回"OK ",则重启模块。\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+4. 设置非透明传输服务参数\r
+- 若返回"OK",则进入下一步。\r
+- 若未收到回码或收到其他错误回码,则继续设置,最多3次。若重复设\r
+\r
+置3次后,仍未收到"OK",则重启模块。\r
+注:信道号必须与已激活的信道号一致,第三个参数必须设置为0。\r
+\r
+5. 开启服务(建立TCP连接)\r
+- 若返回"$MYNETOPEN: 0,2000"或" ERROR: 912" ,则进入下一步。该指令等\r
+\r
+- 待返回"$MYNETOPEN: 0,2000"或" ERROR: 912"的超时时间为30s。\r
+- 若返回"ERROR: 901"则重新设置参数进行PPP拨号。\r
+- 若超时未收到回码或收到其他错误回码,则继续开启,最多3次。若重\r
+\r
+复开启3次后,仍未收到"$MYNETOPEN: 0,2000"或" ERROR: 912"则重启模\r
+块。\r
+\r
+6. 发送数据\r
+- 若返回"$MYNETWRITE: X,<Length>",则延时50-100ms,将数据发送到模块串\r
+\r
+- 口,等待数据发送成功的回码"OK"。若返回"OK",则进入下一步。\r
+- 若返回"ERROR: 901",则返回重新进行PPP拨号。\r
+- 若返回"ERROR: 913",则重新建立TCP连接。\r
+- 若返回其他错误回码、或者未返回任何回码,则继续重发,最多3次。\r
+\r
+若重发3次仍未收到"OK",则重启模块。\r
+\r
+8. 读取数据\r
+- 若返回"$MYNETREAD: X,0",则可进入下一步:继续发送数据或者接收\r
+\r
+- 数据。若回码中"<data_Length>"字段不为0,则继续读取。\r
+- 若返回"ERROR: 901",则重新PPP拨号。\r
+- 若返回"ERROR: 913",则重建TCP连接。\r
+- 若返回其他错误回码、或者未返回任何回码,则继续读取,最多3次。\r
+\r
+若重读3次仍返回其他错误回码、或者未返回任何回码,则重启模块。\r
+\r
+9. 去附着网络\r
+- 若返回"OK"或者" ERROR",则进入下一步,等返回的超时时间为15s。\r
+- 若超时未收到回码或者收到其他错误回码,则继续执行,最多3次,若\r
+\r
+重复关闭3次后,仍未收到"OK"或者" ERROR"则重启模块。\r
+\r
+7.AT$MYNETACK=X\r
+\r
+"$MYNETREAD: X,0" ?\r
+\r
+7. 查询数据发送情况\r
+- 若返回"$MYNETACK: X,<unAcked_dataLen>,<rest_bufferLen>"且\r
+\r
+<unAcked_dataLen>值为0,则进入下一步:继续发送数据或者接收数\r
+据。\r
+• 若返回"ERROR: 901",则重新PPP拨号。\r
+• 若返回"ERROR: 913",则重新建立TCP连接。\r
+• 若回码中"<unAcked_dataLen>"字段不为0,则继续查询,1s/次,最多5\r
+\r
+次。若查询5次,"<unAcked_dataLen>"字段不为0,则重新建立TCP连\r
+接。\r
+• 若返回其他错误回码、或者未返回任何回码,则继续重发,最多3次。\r
+\r
+若重发3次仍返回其他错误回码、或者未返回任何回码,则重启模块。\r
+\r
+8. AT$MYNETREAD=X,<data_len>\r
+\r
+关闭<3次?\r
+\r
+读取<3次?\r
+\r
+去附着<3次?\r
+\r
+Yes\r
+\r
+No\r
+\r
+No\r
+Yes\r
+\r
+Yes\r
+\r
+No\r
+\r
+2. 查询PPP连接建立情况\r
+\r
+- 若回码中包含"$MYNETACT: 0,1"字段,则进入下一步。\r
+- 若回码中包含"$MYNETACT: 0,0"则重新设置参数进行PPP拨号。\r
+- 若返回其他错误回码,则循环查询,1s/次,最多3次。若查询3次后仍\r
+\r
+未返回"$MYNETACT: 0,1",则重新设置参数进行PPP拨号。\r
+• 若未返回任何回码,则继续查询,1s/次,最多3次。若查询继续3次\r
+\r
+后,仍未返回任何回码,则重启模块。\r
+\r
+3. 关闭待用Socket\r
+- 若返回"$MYNETCLOSE: X"或"ERROR: 913",则进入下一步。该指令等待返\r
+\r
+- 回"$MYNETCLOSE: X"或"ERROR: 913"的超时时间为30s。\r
+- 若返回"ERROR: 901"则重新设置参数进行PPP拨号。\r
+- 若超时未收到回码或收到其他错误回码,则继续关闭,最多3次。若重\r
+\r
+复关闭3次后,仍未收到"$MYNETCLOSE: X"或"ERROR: 913"则重启模块。\r
+\r
+"$MYNETACK: X,0" ?\r
+\r
+$MYURCREAD: X\r
+\r
+No\r
+查询<3次?\r
+\r
+Yes\r
+\r
+No\r
+\r
+查询<5次?\r
+\r
+Yes\r
+No\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+No\r
+\r
+No\r
+\r
+No\r
+\r
+No\r
+\r
+功能模组TCP 应用指南 \r
+第4 章国网指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+29\r
+\r
+###### 4.1.1 异常处理\r
+\r
+###### 模块附着PS 域失败\r
+\r
+1. \r
+检查模块串口通信是否正常。\r
+\r
+若串口不通,可参考以下方式排查解决:\r
+\r
+− \r
+确保模块已经开机:若模块不开机需要检查电源设计和开机时序\r
+\r
+− \r
+确保串口电平变换正常:若电平变换不正常需要检查串口电路设计和器件选型\r
+\r
+− \r
+确保软件上串口配置正常,软件处理是否有误。比如自适应逻辑、是否带回车换行发送AT\r
+\r
+2. \r
+检查模块是否已经识别到SIM 卡。\r
+\r
+若模块无法识别SIM 卡,可参考以下方式排查解决(涉及到的AT 命令,请参考功能 AT 指令\r
+\r
+集):\r
+\r
+− \r
+确保SIM 卡是否插好、SIM 卡本身完好无损、与卡座接触是否良好\r
+\r
+− \r
+确保SIM 卡座相关电路无设计问题,是否有虚假焊等\r
+\r
+− \r
+确保SIM 卡没有被锁卡\r
+\r
+若通过以上排查模块仍无法识别到SIM 卡,可以联系运营商处理或者更换SIM 卡解决。\r
+\r
+3. \r
+查询当前模块的接收信号是否满足通信条件。\r
+\r
+我们建议信号强度值为12(-83dbm)再进行通信。若现场信号弱,可通过以下方式排查:\r
+\r
+− \r
+确保天线接好、天线本身完好无损(断路、短路)\r
+\r
+− \r
+确认模块注册的网络制式(AT$MYSYSINFO)等信息,若为现场网络环境问题,则联系运营\r
+\r
+商增强网络覆盖,或者更换其他运营商SIM 卡解决。\r
+\r
+4. \r
+若现场信号良好,模块能识别到SIM 卡且SIM 卡未欠费,但模块不能附着上PS 域,则需要联系\r
+\r
+运营商确定SIM 卡配置:\r
+\r
+− \r
+SIM 卡是否开通数据业务\r
+\r
+− \r
+SIM 卡配置鉴权参数是否正常\r
+\r
+− \r
+SIM 卡是否有绑定设备\r
+\r
+− \r
+SIM 卡数据业务相关的APN、用户名、密码、鉴权方式是否正常\r
+\r
+功能模组TCP 应用指南 \r
+第4 章国网指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+30\r
+\r
+###### 模块激活PDP 失败\r
+\r
+若模块正常注册上PS 域,但是无法激活PPP 连接。可以参考以下方式排查解决(涉及到的AT 命\r
+\r
+令,请参考功能 AT 指令集):\r
+\r
+⚫ \r
+确保SIM 未欠费。\r
+\r
+⚫ \r
+使用AT$MYNETCON?查询APN、用户名、密码,确保这些参数与运营商提供的一致。\r
+\r
+若以上参数都没有问题,模块仍无法激活PPP 连接,请联系当地运营商协助检查SIM 卡和后台服\r
+\r
+务器参数配置。\r
+\r
+###### 模块建立TCP 非透传客户端连接失败\r
+\r
+若模块正常获取到IP,但是建立TCP 非透传客户端连接。可以参考以下方式排查解决(涉及到的\r
+\r
+AT 命令,请参考功能 AT 指令集):\r
+\r
+1. \r
+确认服务器IP 和端口都已配置好,端口已经建立服务器端侦听,IP 能被客户端PING 通。\r
+\r
+可以通过用其他客户端设备来连接该服务器IP 和端口确认。\r
+\r
+2. \r
+确认模块获取到的IP 是否能访问服务器。\r
+\r
+可通过发送AT+PING=<IP/域名>给模块,操作模块PING 服务器来确认。\r
+\r
+若模块能PING 通服务器,则需要检查服务器软件和端口配置。\r
+\r
+若模块不能PING 通服务器,则需要检查APN、用户名、密码设置是否正确,SIM 卡配置是否正\r
+\r
+确。\r
+\r
+3. \r
+确认SIM 卡配置是否有问题。\r
+\r
+可以直接联系运营商确认SIM 卡是否可以访问目的IP 或者让运营商直接跟踪后台消息排查原因。\r
+\r
+###### 模块发送TCP 数据失败\r
+\r
+若模块正常建立TCP 非透传客户端连接,但是发送TCP 数据失败。可以参考以下方式排查解决\r
+\r
+(涉及到的AT 命令,请参考功能 AT 指令集):\r
+\r
+1. \r
+确认是否是服务器问题。\r
+\r
+可通过更换服务器对比确认是否是服务器配置问题。\r
+\r
+2. \r
+确认SIM 卡是否也有欠费。\r
+\r
+可联系运营商确认。\r
+\r
+3. \r
+确认是否是长时间没有发送数据导致的假连接。\r
+\r
+功能模组TCP 应用指南 \r
+第4 章国网指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+31\r
+\r
+可通过缩短心跳周期或者是提高数据发送频率来确认。\r
+\r
+若缩短心跳周期或者是提高数据发送频率后,数据发送正常则可能是假连接引起的问题。\r
+\r
+4. \r
+确认TCP 连接是否还存在。\r
+\r
+可通过”AT$MYNETOPEN?”命令的返回值来确认。\r
+\r
+若返回值中含有“$MYNETOPEN:”字符串,则表示TCP 连接存在;若返回字段只有OK,则表示\r
+\r
+TCP 连接不存在。\r
+\r
+5. \r
+确认终端(或主控)是否已经将数据发送到模块。\r
+\r
+可以通过模块串口与终端(或主控)串口之间交互的AT 命令和数据内容来确认。\r
+\r
+为此,我司建议主控具备保存或打印模块与终端串口交互log 功能。只要AT$MYNETWRITE 命\r
+\r
+令返回“OK”就表示主控数据已经发送到模块。\r
+\r
+6. \r
+确认模块底层协议栈是否收到服务器端TCP 协议层的ACK。\r
+\r
+可通过”AT$MYNETACK =<SocketID>”命令的返回值来确认。\r
+\r
+若返回值中<unAcked_dataLen>字段数值为0,则表示数据已经发送到服务器且服务器已经确\r
+\r
+认。\r
+\r
+若返回值中<unAcked_dataLen>字段数值不为0,则有可能是数据没有发送到服务器,或者是服\r
+\r
+务器接收有延时还未收到数据,需要在服务器和模块同时抓取TCP 层数据来排查,可以联系我司\r
+\r
+FAE 跟进排查。\r
+\r
+###### 模块接收TCP 数据失败\r
+\r
+若模块正常建立TCP 非透传客户端连接,但是接收TCP 数据失败。可以参考以下方式排查解决\r
+\r
+(涉及到的AT 命令,请参考功能 AT 指令集):\r
+\r
+1. \r
+确认是否是服务器问题。\r
+\r
+可通过更换服务器对比确认是否是服务器配置问题。\r
+\r
+2. \r
+确认SIM 卡是否也有欠费。\r
+\r
+可联系运营商确认。\r
+\r
+3. \r
+确认TCP 连接是否还存在。\r
+\r
+可通过”AT$MYNETOPEN?”命令的返回值来确认。\r
+\r
+若返回值中含有“$MYNETOPEN:”字符串,则表示TCP 连接存在。\r
+\r
+若返回字段只有OK,则表示TCP 连接不存在。\r
+\r
+4. \r
+确认是否是长时间没有发送数据导致的假连接。\r
+\r
+可通过缩短心跳周期或者是提高数据发送频率来确认。\r
+\r
+功能模组TCP 应用指南 \r
+第4 章国网指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+32\r
+\r
+若缩短心跳周期或者是提高数据发送频率后,数据接收正常则可能是假连接引起的问题。\r
+\r
+若缩短心跳或提高数据发送频率仍无法确认接收不到数据的原因,需要在服务器和模块同时抓取\r
+\r
+TCP 层数据来排查,可以联系我司FAE 跟进排查。\r
+\r
+###### 4.1.2 示例\r
+\r
+1. \r
+设置APN、用户名、密码,检查并确保模块已成功注册上PS 域\r
+\r
+AT \r
+//确认模块能正常响应AT 命令 \r
+OK \r
+AT+CGSN \r
++CGSN: "865324037375316" \r
+OK \r
+AT+CGMR \r
++CGMR: N720_DAC0CM_DL_V004\r
+\r
+OK \r
+AT+CPIN? \r
++CPIN: READY \r
+//确认模块能识别到SIM 卡\r
+\r
+OK \r
+AT+CIMI \r
+460016657520885\r
+\r
+OK \r
+AT$MYNETCON=0,"APN","CMNET" \r
+ \r
+ \r
+//设置APN \r
+OK\r
+\r
+AT$MYNETCON=0,"USERPWD","CARD,CARD" \r
+ \r
+//设置用户名和密码 \r
+OK \r
+AT$MYNETCON=0,"AUTH",1 \r
+ \r
+ \r
+ \r
+//设置鉴权方式 \r
+OK \r
+AT+CSQ \r
++CSQ: 31, 99 \r
+//确认信号良好 \r
+OK \r
+AT+CREG=2 \r
+OK \r
+AT+CREG?\r
+\r
++CREG: 2,1,"FFFE","61C7E47",7 //确认模块注册上CS 域\r
+\r
+OK \r
+AT+CREG=0 \r
+OK \r
+AT+CGATT? \r
++CGATT: 1 \r
+//确认模块附着上PS 域 \r
+OK \r
+AT$MYSYSINFO\r
+\r
+$MYSYSINFO: 4,02 \r
+//确认模块注册的网络制式\r
+\r
+OK\r
+\r
+2. \r
+激活网络连接\r
+\r
+AT$MYNETURC=1 \r
+OK \r
+AT$MYNETACT=0,1 \r
+ \r
+ \r
+ \r
+ \r
+//激活PPP 连接 \r
+OK \r
+$MYURCACT: 0,1,"10.51.228.13" \r
+AT$MYNETACT? \r
+ \r
+ \r
+ \r
+ \r
+//查询PPP 连接是否正常\r
+\r
+$MYNETACT: 0,1,"10.51.228.13" \r
+OK\r
+\r
+功能模组TCP 应用指南 \r
+第4 章国网指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+33\r
+\r
+3. \r
+建立TCP 非透传客户端连接并收发数据\r
+\r
+AT$MYNETCLOSE=0 \r
+ERROR: 913 \r
+AT$MYNETSRV=0,0,0,0,"183.239.24.40:5002" //使用域名或者IP 均可以建立连接 \r
+OK \r
+AT$MYNETOPEN=0 \r
+ \r
+ \r
+ \r
+//建立TCP 连接\r
+\r
+$MYNETOPEN: 0,2000 \r
+OK \r
+AT$MYNETWRITE=0,6 \r
+ \r
+ \r
+//发送数据 \r
+ \r
+$MYNETWRITE: 0,6 \r
+OK \r
+AT$MYNETREAD=0,1 \r
+$MYNETREAD: 0,0 \r
+OK \r
+$MYURCREAD: 0 \r
+ \r
+ \r
+ \r
+//接收到服务器发送的数据\r
+\r
+AT$MYNETREAD=0,2048 \r
+ \r
+ \r
+//读取数据 \r
+$MYNETREAD: 0,10 \r
+1234567890 \r
+OK\r
+\r
+AT$MYNETCLOSE=0 \r
+$MYNETCLOSE: 0 \r
+OK\r
+\r
+AT+CGATT=0 \r
+OK\r
+\r
+$MYURCACT: 0,0\r
+\r
+功能模组TCP 应用指南 \r
+第4 章国网指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+34\r
+\r
+##### 4.2 TCP 非透传服务器端\r
+\r
+本节主要实现TCP 非透传服务端功能相关的AT 指令、AT 流程、异常处理以及简单示例。在进行\r
+\r
+PPP 拨号前,请确保模块已经过初始化(具体初始化流程参考第2 章)。\r
+\r
+图 4-2 连接流程\r
+\r
+设置PPP拨号参数\r
+\r
+"$MYURCACT: 0,1"?\r
+\r
+1. AT$MYNETACT=0,1\r
+\r
+"$MYNETCLOSE: X"\r
+\r
+or "ERROR: 913"?\r
+\r
+3. AT$MYNETCLOSE=X\r
+\r
+"OK" ?\r
+\r
+4. AT$MYNETSRV=0,X,1,0,"127.0.0.1:Port"\r
+\r
+Yes\r
+\r
+5. AT$MYNETOPEN=X\r
+\r
+"$MYNETOPEN: X" or" ERROR: 912" ?\r
+\r
+6.AT$MYNETACCEPT=Y,0,0\r
+\r
+"OK" ?\r
+\r
+$MYURCREAD: Y\r
+\r
+"OK" or "ERROR"?\r
+\r
+No\r
+\r
+No\r
+\r
+Yes\r
+\r
+重新开启<3次?\r
+\r
+查询<3次?\r
+\r
+关机\r
+\r
+No\r
+\r
+No\r
+\r
+Yes\r
+\r
+模块重启\r
+模块初始化流程\r
+\r
+7. AT$MYNETWRITE=Y,<Length>\r
+\r
+"$MYNETWRITE: Y,<Length>" ?\r
+\r
+输入要发送的数据\r
+\r
+"OK"?\r
+\r
+No\r
+\r
+去附着<3次?\r
+\r
+重发<3次?\r
+\r
+重发<3次?\r
+\r
+Yes\r
+\r
+No\r
+\r
+No\r
+No\r
+Yes\r
+\r
+No\r
+Yes\r
+No\r
+\r
+1. 进行PPP拨号\r
+- 若返回"OK",则可进入下一步。\r
+- 若未收到任何回码或收到其他错误回码,则继续拨号,1s/次,最多\r
+\r
+3次。若重新拨号3次后,仍未若返回"OK ",则重启模块。\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+4. 设置非透明传输服务参数\r
+- 若返回"OK",则进入下一步;\r
+- 若未收到回码或收到其他错误回码,则继续设置,最多3次。若重\r
+\r
+复设置3次后,仍未收到"OK",则重启模块。\r
+注:信道号必须与已激活的信道号一致,第三个参数必须设置为1;\r
+\r
+5. 开启服务器服务\r
+\r
+• 若返回"$MYNETOPEN: X"或" ERROR: 912" ,则进入下一步。该指令等\r
+\r
+- 待返回"$MYNETOPEN: X"或" ERROR: 912"的超时时间为30s。\r
+- 若返回"ERROR: 901"则重新设置参数进行PPP拨号。\r
+- 若超时未收到回码或收到其他错误回码,则继续开启,最多3次。\r
+\r
+若重复开启3次后,仍未收到"$MYNETOPEN: X"或" ERROR: 912"则\r
+重启模块。\r
+\r
+等待接收客户端连接请求\r
+需要设置等待客户端连接的超时时间,若超时时间到,没有收到任何\r
+\r
+客户端连接请求,则主动重新拨号建立侦听。\r
+\r
+7. 发送数据\r
+- 若返回"$MYNETWRITE: Y,<Length>",则延时50-100ms,将数据发送到\r
+\r
+模块串口,等待数据发送成功的回码"OK"。若返回"OK",则进入下\r
+一步。\r
+• 若返回"ERROR: 901",则返回重新进行PPP拨号。\r
+• 若返回"ERROR: 913",则等待客户端连接。\r
+• 若返回其他错误回码、或者未返回任何回码,则继续重发,最多3\r
+\r
+次。若重发3次仍未收到"OK",则重启模块。\r
+\r
+11. 去附着网络\r
+- 若返回"OK"或者" ERROR",则进入下一步,等返回的超时时间为\r
+\r
+15s。\r
+• 若超时未收到回码或者收到其他错误回码,则继续执行,最多3\r
+\r
+次,若重复关闭3次后,仍未收到"OK"或者" ERROR"则重启模块。\r
+\r
+8. AT$MYNETACK=Y\r
+\r
+"$MYNETACK: Y,0" ?\r
+\r
+- 6.接受客户端的连接请求\r
+- 若返回"OK"则进入下一步;\r
+- 若超时未收到回码或者收到其他错误回码,则继续去附着,最多3\r
+\r
+次。若重复关闭3次后,仍未收到"OK"则重启模块。\r
+\r
+关闭<3次?\r
+\r
+设置<3次?\r
+\r
+查询<3次?\r
+\r
+去附着<3次?\r
+\r
+Yes\r
+\r
+No\r
+\r
+Yes\r
+\r
+No\r
+\r
+No\r
+Yes\r
+\r
+Yes\r
+\r
+No\r
+No\r
+\r
+2. 查询PPP连接建立情况\r
+- 若回码中包含"+XIIC: 1 ",则可进入下一步。\r
+- 若回码未包含"+XIIC: 1",则循环查询,1s/次,最多30次。若30次\r
+\r
+后仍未返回"+XIIC: 1",则重新拨号,最多重拨3次,若重新拨号3次\r
+后,仍未获取到IP,则重启模块。\r
+\r
+3. 关闭侦听\r
+- 若返回"$MYNETCLOSE: X"或"ERROR: 913",则进入下一步。该指令等\r
+\r
+- 待返回"$MYNETCLOSE: X"或"ERROR: 913"的超时时间为30s。\r
+- 若返回"ERROR: 901"则重新设置参数进行PPP拨号。\r
+- 若超时未收到回码或收到其他错误回码,则继续关闭,最多3次。\r
+\r
+若重复关闭3次后,仍未收到"$MYNETCLOSE: X"或"ERROR: 913"则重\r
+启模块。\r
+\r
+$MYURCCLIENT: Y,"10.91.215.190",63342\r
+\r
+8. 查询数据发送情况\r
+- 若回码包含"$MYNETACK: Y,0,",则进入下一步:继续发送或者接收\r
+\r
+- 数据。\r
+- 若返回"ERROR: 901",则返回重新PPP拨号。\r
+- 若返回"ERROR: 913",则等待客户端连接。\r
+- 若回码中"<unAcked_dataLen>"字段不为0,则继续查询,1s/次,最\r
+\r
+多5次;若查询5次,"<unAcked_dataLen>"字段不为0,则返回关闭\r
+当前连接,重新建立连接。\r
+• 若返回其他错误回码、或者未返回任何回码,则继续重发,最多3\r
+\r
+次。若重发3次仍返回其他错误回码、或者未返回任何回码,则重\r
+启模块。\r
+\r
+9. AT$MYNETREAD=Y,<data_len>\r
+\r
+"$MYNETREAD: Y,0" ?\r
+\r
+10. AT$MYNETCLOSE=X\r
+\r
+"$MYNETCLOSE: X"\r
+\r
+or "ERROR: 913"?\r
+\r
+11. AT+CGATT=0\r
+\r
+读取<3次?\r
+\r
+Yes\r
+\r
+No\r
+\r
+No\r
+\r
+关闭侦听<3次?\r
+\r
+"$MYNETCLOSE: X"\r
+\r
+or "ERROR: 913"?\r
+\r
+2. AT$MYNETACT?\r
+\r
+查询<3次?\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+9. 读取数据\r
+- 若返回"$MYNETREAD: Y,0",则可进入下一步:继续发送或者接收数\r
+\r
+据。若回码中"<unAcked_dataLen>"字段不为0,则继续读取。\r
+• 若返回"ERROR: 901",则重新PPP拨号;返回"ERROR: 913",则等待客\r
+\r
+户端连接。\r
+• 若返回其他错误回码、或者未返回任何回码,则继续读取,最多3\r
+\r
+次。若重读3次仍返回其他错误回码、或者未返回任何回码,则重\r
+启模块。\r
+\r
+10. 关闭侦听\r
+- 若返回"$MYNETCLOSE: X"或"ERROR: 913",则进入下一步。该指令等\r
+\r
+待返回"$MYNETCLOSE: X"或"ERROR: 913"的超时时间为30s。\r
+• 若超时未收到回码或收到其他错误回码,则继续关闭,最多3次。\r
+\r
+若重复关闭3次后,仍未收到"$MYNETCLOSE: X"或"ERROR: 913"则重\r
+启模块。\r
+\r
+No\r
+\r
+No\r
+\r
+No\r
+\r
+No\r
+No\r
+Yes\r
+\r
+No\r
+\r
+Yes\r
+\r
+No\r
+\r
+Yes\r
+\r
+功能模组TCP 应用指南 \r
+第4 章国网指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+35\r
+\r
+###### 4.2.1 异常处理\r
+\r
+###### 模块附着PS 域失败\r
+\r
+1. \r
+检查模块串口通信是否正常。若串口不通,可参考以下方式排查解决:\r
+\r
+− \r
+确保模块已经开机:若模块不开机需要检查电源设计和开机时序\r
+\r
+− \r
+确保串口电平变换正常:若电平变换不正常需要检查串口电路设计和器件选型\r
+\r
+− \r
+确保软件上串口配置正常,软件处理是否有误。比如自适应逻辑、是否带回车换行发送AT\r
+\r
+2. \r
+检查模块是否已经识别到SIM 卡。若模块无法识别SIM 卡,可参考以下方式排查解决(涉及到的\r
+\r
+AT 命令,请参考功能 AT 指令集):\r
+\r
+− \r
+确保SIM 卡是否插好、SIM 卡本身完好无损、与卡座接触是否良好\r
+\r
+− \r
+确保SIM 卡座相关电路无设计问题,是否有虚假焊等\r
+\r
+− \r
+确保SIM 卡没有被锁卡\r
+\r
+若通过以上排查模块仍无法识别到SIM 卡,可以联系运营商处理或者更换SIM 卡解决。\r
+\r
+3. \r
+查询当前模块的接收信号是否满足通信条件。我们建议信号强度值为12(-83dbm)再进行通信。\r
+\r
+若现场信号弱,可通过以下方式排查:\r
+\r
+− \r
+确保天线接好、天线本身完好无损(断路、短路)\r
+\r
+− \r
+确认模块注册的网络制式(AT$MYSYSINFO)等信息。\r
+\r
+若为现场网络环境问题,则联系运营商增强网络覆盖,或者更换其他运营商SIM 卡解决。\r
+\r
+4. \r
+若现场信号良好,模块能识别到SIM 卡且SIM 卡未欠费,但模块不能附着上PS 域,则需要联系\r
+\r
+运营商确定SIM 卡配置:\r
+\r
+− \r
+SIM 卡是否开通数据业务\r
+\r
+− \r
+SIM 卡配置鉴权参数是否正常\r
+\r
+− \r
+SIM 卡是否有绑定设备\r
+\r
+− \r
+SIM 卡数据业务相关的APN、用户名、密码、鉴权方式是否正常\r
+\r
+###### 模块激活PDP 失败\r
+\r
+若模块正常注册上PS 域,但是无法激活PPP 连接。可以参考以下方式排查解决(涉及到的AT 命\r
+\r
+令,请参考功能 AT 指令集):\r
+\r
+功能模组TCP 应用指南 \r
+第4 章国网指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+36\r
+\r
+⚫ \r
+确保SIM 未欠费.\r
+\r
+⚫ \r
+使用AT+$MYNETCON?查询APN、用户名、密码,确保这些参数与运营商提供的一致。\r
+\r
+若以上参数都没有问题,模块仍无法激活PPP 连接,请联系当地运营商协助检查SIM 卡和后台服\r
+\r
+务器参数配置。\r
+\r
+###### 客户端无法连接服务器\r
+\r
+若模块正常建立服务器端侦听后,客户端无法连接上服务器。可以参考以下方式排查解决(涉及到\r
+\r
+的AT 命令,请参考功能 AT 指令集):\r
+\r
+1. \r
+确认服务器IP 和端口都已配置好,端口已经建立服务器端侦听,IP 能被客户端PING 通。\r
+\r
+可以通过用其他客户端设备来连接该服务器IP 和端口确认。\r
+\r
+2. \r
+确认模块获取到的IP 与客户端的IP 是否在同一个网段内。\r
+\r
+可通过在PC 上PING 模块来确认。\r
+\r
+− \r
+若PC 客户端能PING 通服务器,则需要检查服务器软件和端口配置。\r
+\r
+− \r
+若PC 客户端不能PING 通服务器,则需要检查APN、用户名、密码设置是否正确,SIM 卡\r
+\r
+配置是否正确。\r
+\r
+3. \r
+确认SIM 卡配置是否有问题。\r
+\r
+可以直接联系运营商客户端是否可以访问SIM 卡的IP 或者让运营商直接跟踪后台消息排查原因。\r
+\r
+###### 模块发送TCP 数据失败\r
+\r
+若模块正常建立TCP 非透传客户端连接,但是发送TCP 数据失败。可以参考以下方式排查解决\r
+\r
+(涉及到的AT 命令,请参考功能 AT 指令集):\r
+\r
+1. \r
+确认是否是服务器问题。\r
+\r
+可通过更换服务器对比确认是否是服务器配置问题。\r
+\r
+2. \r
+确认SIM 卡是否也有欠费。\r
+\r
+可联系运营商确认。\r
+\r
+3. \r
+确认是否是长时间没有发送数据导致的假连接。\r
+\r
+可通过缩短心跳周期或者是提高数据发送频率来确认。\r
+\r
+若缩短心跳周期或者是提高数据发送频率后,数据发送正常则可能是假连接引起的问题。\r
+\r
+4. \r
+确认TCP 连接是否还存在。\r
+\r
+功能模组TCP 应用指南 \r
+第4 章国网指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+37\r
+\r
+可通过”AT$MYNETOPEN?”命令的返回值来确认.\r
+\r
+− \r
+若返回值中含有“$MYNETOPEN:”字符串,则表示TCP 连接存在。\r
+\r
+− \r
+若返回字段只有OK,则表示TCP 连接不存在。\r
+\r
+5. \r
+确认终端(或主控)是否已经将数据发送到模块。\r
+\r
+可以通过模块串口与终端(或主控)串口之间交互的AT 命令和数据内容来确认。\r
+\r
+为此,我司建议主控具备保存或打印模块与终端串口交互log 功能。只要AT$MYNETWRITE 命\r
+\r
+令返回“OK”就表示主控数据已经发送到模块。\r
+\r
+6. \r
+确认模块底层协议栈是否收到服务器端TCP 协议层的ACK。\r
+\r
+可通过”AT$MYNETACK =<SocketID>”命令的返回值来确认。\r
+\r
+− \r
+若返回值中<unAcked_dataLen>字段数值为0,则表示数据已经发送到服务器且服务器已经\r
+\r
+确认。\r
+\r
+− \r
+若返回值中<unAcked_dataLen>字段数值不为0,则有可能是数据没有发送到服务器,或者\r
+\r
+是服务器接收有延时还未收到数据,需要在服务器和模块同时抓取TCP 层数据来排查,可以\r
+\r
+联系我司FAE 跟进排查。\r
+\r
+###### 4.2.2 示例\r
+\r
+1. \r
+设置APN、用户名、密码,检查并确保模块已成功注册上PS 域\r
+\r
+AT \r
+//确认模块能正常响应AT 命令 \r
+OK \r
+AT+CGSN \r
++CGSN: "865324037375316" \r
+OK\r
+\r
+AT+CGMR \r
++CGMR: N720_DAC0CM_DL_V004 \r
+OK \r
+AT+CPIN? \r
++CPIN: READY \r
+//确认模块能识别到SIM 卡 \r
+OK \r
+AT+CIMI\r
+\r
+460016657520885 \r
+OK\r
+\r
+AT$MYNETCON=0,"APN","CMNET" \r
+ \r
+ \r
+//设置APN \r
+OK \r
+AT$MYNETCON=0,"USERPWD","CARD,CARD" \r
+ \r
+//设置用户名和密码 \r
+OK \r
+AT$MYNETCON=0,"AUTH",1 \r
+ \r
+ \r
+ \r
+//设置鉴权方式 \r
+OK \r
+AT+CSQ \r
++CSQ: 31, 99 \r
+//确认信号良好 \r
+OK \r
+AT+CREG=2\r
+\r
+OK \r
+AT+CREG? \r
++CREG: 2,1,"FFFE","61C7E47",7 //确认模块注册上CS 域\r
+\r
+功能模组TCP 应用指南 \r
+第4 章国网指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+38\r
+\r
+OK \r
+AT+CREG=0 \r
+OK \r
+AT+CGATT? \r
++CGATT: 1 \r
+//确认模块附着上PS 域 \r
+OK \r
+AT$MYSYSINFO \r
+$MYSYSINFO: 4,02 \r
+//确认模块注册的网络制式\r
+\r
+OK\r
+\r
+2. \r
+激活网络连接\r
+\r
+AT$MYNETURC=1 \r
+OK \r
+AT$MYNETACT=0,1 \r
+ \r
+ \r
+ \r
+ \r
+//激活PPP 连接 \r
+OK \r
+$MYURCACT: 0,1,"10.51.228.13"\r
+\r
+AT$MYNETACT? \r
+ \r
+ \r
+ \r
+ \r
+//查询PPP 连接是否正常 \r
+$MYNETACT: 0,1,"10.51.228.13" \r
+OK\r
+\r
+3. \r
+建立TCP 非透传服务器端侦听并与客户端建立连接、收发数据\r
+\r
+AT$MYNETCLOSE=0 \r
+ERROR: 913 \r
+AT$MYNETSRV=0,0,1,0,"127.0.0.1:6800"\r
+\r
+OK\r
+\r
+AT$MYNETOPEN=0 \r
+$MYNETOPEN: 0 \r
+OK \r
+$MYURCCLIENT: 1,"112.97.54.136",14759 \r
+AT$MYNETACCEPT=1,0,0 \r
+OK \r
+AT$MYNETWRITE=1,10 \r
+$MYNETWRITE: 0,10 \r
+ 输入要发送的数据\r
+\r
+OK\r
+\r
+$MYURCREAD: 1 \r
+AT$MYNETREAD=1,2048 \r
+$MYNETREAD: 1,6\r
+\r
+Neoway \r
+OK\r
+\r
+AT$MYNETACK=1 \r
+$MYNETACK: 1,0,10240 \r
+OK\r
+\r
+AT$MYNETCLOSE=X\r
+\r
+OK \r
+$MYURCCLOSE: 1 \r
+$MYURCCLOSE: 0 \r
+AT+CGATT=0 \r
+OK \r
+$MYURCACT: 0,0\r
+\r
+##### 4.3 TCP 透传客户端\r
+\r
+本节主要实现TCP 透传客户端功能相关的AT 指令、AT 流程、异常处理以及简单示例。在进行PPP\r
+\r
+功能模组TCP 应用指南 \r
+第4 章国网指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+39\r
+\r
+拨号前,请确保模块已经过初始化(具体初始化流程参考第2 章)。\r
+\r
+图 4-3 连接流程\r
+\r
+设置PPP拨号参数\r
+\r
+"$MYURCACT: 0,1"?\r
+\r
+1. AT$MYNETACT=0,1\r
+\r
+"$MYNETACT: 0,1"?\r
+\r
+2. AT$MYNETACT?\r
+\r
+"$MYNETCLOSE: X"\r
+\r
+or "ERROR: 913"?\r
+\r
+3. AT$MYNETCLOSE=X\r
+\r
+Yes\r
+\r
+4. AT$MYNETCREATE=0,0,0,"IP",Port\r
+\r
+"CONNECT"?\r
+\r
+收发数据\r
+\r
+9. AT+CGATT=0\r
+\r
+"OK" or "ERROR"?\r
+\r
+No\r
+\r
+Yes\r
+重设<3次?\r
+\r
+拨号<3次?\r
+\r
+关机\r
+\r
+No\r
+\r
+No\r
+\r
+Yes\r
+\r
+模块重启\r
+模块初始化流程\r
+\r
+5. +++\r
+\r
+"OK" ?\r
+\r
+6. AT$MYNETACK=X\r
+\r
+"$MYNETACK: X,0"?\r
+\r
+No\r
+\r
+重发<3次?\r
+\r
+查询<3次?\r
+\r
+返回透传<3次?\r
+\r
+Yes\r
+No\r
+\r
+No\r
+Yes\r
+\r
+No\r
+Yes\r
+\r
+1. PPP拨号\r
+\r
+• 若返回"$MYURCACT: 0,1"或"ERROR: 902",则进入下一步。该指令等待返\r
+\r
+回"$MYURCACT: 0,1"或"ERROR: 902"的超时时间为30s。\r
+• 若未收到任何回码或收到其他错误回码,则继续拨号,最多3次。若重\r
+\r
+新拨号3次后,仍未若返回"OK ",则重启模块。\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+4. 建立透明TCP客户端连接\r
+- 若返回"CONNECT"或" ERROR: 912" ,则进入下一步。该指令等待返回\r
+\r
+- "CONNECT"或" ERROR: 912"的超时时间为30s。\r
+- 若返回"ERROR: 901"则重新设置参数进行PPP拨号。\r
+- 若超时未收到回码或收到其他错误回码,则继续开启,最多3次。若重\r
+\r
+复开启3次后,仍未收到"CONNECT"或" ERROR: 912"则重启模块。\r
+\r
+5. 退出透传\r
+- 若返回"OK",则进入下一步;\r
+- 若返回其他错误回码、或者未返回任何回码,则继续重发,最多3次。若\r
+\r
+重发3次仍未收到"OK",则重启模块。\r
+• 注:"+++"必须以字符串的形式发送到串口,且"+++"不必带回车发送\r
+\r
+8. 关闭待用Socket\r
+- 若返回"$MYNETCLOSE: X"或"ERROR: 913",则进入下一步。该指令等待返\r
+\r
+- 回"$MYNETCLOSE: X"或"ERROR: 913"的超时时间为30s。\r
+- 若返回"ERROR: 901"则重新设置参数进行PPP拨号。\r
+- 若超时未收到回码或收到其他错误回码,则继续关闭,最多3次。若重\r
+\r
+复关闭3次后,仍未收到"$MYNETCLOSE: X"或"ERROR: 913"则重启模块。\r
+\r
+9. 去附着网络\r
+- 若返回"OK"或者" ERROR",则进入下一步,等返回的超时时间为15s。\r
+- 若超时未收到回码或者收到其他错误回码,则继续执行,最多3次,若\r
+\r
+重复关闭3次后,仍未收到"OK"或者" ERROR"则重启模块。\r
+\r
+7.ATO\r
+\r
+"$MYNETCLOSE: X"\r
+\r
+or "ERROR: 913"?\r
+\r
+7. 返回透传\r
+\r
+- 若返回"CONNECT",则进入下一步:继续发送数据。\r
+- 若返回其他错误回码、或者未返回任何回码,则继续重发,最多3次。\r
+\r
+若重发3次仍未收到"OK",则重启模块。\r
+\r
+关闭<3次?\r
+\r
+关闭<3次?\r
+\r
+Yes\r
+\r
+No\r
+\r
+No\r
+Yes\r
+\r
+2. 查询PPP连接建立情况\r
+\r
+- 若回码中包含"$MYNETACT: 0,1"字段,则进入下一步。\r
+- 若回码中包含"$MYNETACT: 0,0"则重新设置参数进行PPP拨号。\r
+- 若返回其他错误回码,则循环查询,1s/次,最多3次。若查询3次后仍\r
+\r
+未返回"$MYNETACT: 0,1",则重新设置参数进行PPP拨号。\r
+• 若未返回任何回码,则继续查询,1s/次,最多3次。若查询继续3次\r
+\r
+后,仍未返回任何回码,则重启模块。\r
+\r
+3. 关闭待用Socket\r
+- 若返回"$MYNETCLOSE: X"或"ERROR: 913",则进入下一步。该指令等待返\r
+\r
+- 回"$MYNETCLOSE: X"或"ERROR: 913"的超时时间为30s。\r
+- 若返回"ERROR: 901"则重新设置参数进行PPP拨号。\r
+- 若超时未收到回码或收到其他错误回码,则继续关闭,最多3次。若重\r
+\r
+复关闭3次后,仍未收到"$MYNETCLOSE: X"或"ERROR: 913"则重启模块。\r
+\r
+"CONNECT" ?\r
+\r
+8. AT$MYNETCLOSE=X\r
+\r
+No\r
+查询<3次?\r
+\r
+Yes\r
+\r
+No\r
+\r
+关闭<3次?\r
+\r
+Yes\r
+No\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+Yes\r
+\r
+6. 查询数据发送情况\r
+- 若返回"$MYNETACK: X,0,",则进入下一步:继续发送或者接收数据;\r
+- 若返回"ERROR: 901",则返回重新PPP拨号;\r
+- 若返回"ERROR: 913",则重新建立TCP连接;\r
+- 若回码中"<unAcked_dataLen>"字段不为0,则继续查询,1s/次,最多5\r
+\r
+次。若查询5次后,"<unAcked_dataLen>"字段不为0,则返回关闭当前连\r
+接,重新建立连接;\r
+• 若返回其他错误回码、或者未返回任何回码,则继续重发,最多3次。\r
+\r
+若重发3次仍返回其他错误回码、或者未返回任何回码,则重启模块。\r
+\r
+No\r
+\r
+No\r
+\r
+No\r
+\r
+No\r
+\r
+No\r
+\r
+No\r
+\r
+功能模组TCP 应用指南 \r
+第4 章国网指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+40\r
+\r
+###### 4.3.1 异常处理\r
+\r
+###### 模块附着PS 域失败\r
+\r
+1. \r
+检查模块串口通信是否正常。若串口不通,可参考以下方式排查解决:\r
+\r
+− \r
+确保模块已经开机:若模块不开机需要检查电源设计和开机时序\r
+\r
+− \r
+确保串口电平变换正常:若电平变换不正常需要检查串口电路设计和器件选型\r
+\r
+− \r
+确保软件上串口配置正常,软件处理是否有误。比如自适应逻辑、是否带回车换行发送AT\r
+\r
+2. \r
+检查模块是否已经识别到SIM 卡。若模块无法识别SIM 卡,可参考以下方式排查解决(涉及到的\r
+\r
+AT 命令,请参考功能 AT 指令集):\r
+\r
+− \r
+确保SIM 卡是否插好、SIM 卡本身完好无损、与卡座接触是否良好\r
+\r
+− \r
+确保SIM 卡座相关电路无设计问题,是否有虚假焊等\r
+\r
+− \r
+确保SIM 卡没有被锁卡\r
+\r
+若通过以上排查模块仍无法识别到SIM 卡,可以联系运营商处理或者更换SIM 卡解决。\r
+\r
+3. \r
+查询当前模块的接收信号是否满足通信条件。我们建议信号强度值为12(-83dbm)再进行通信。\r
+\r
+若现场信号弱,可通过以下方式排查:\r
+\r
+− \r
+确保天线接好、天线本身完好无损(断路、短路)\r
+\r
+− \r
+确认模块注册的网络制式(AT$MYSYSINFO)等信息。\r
+\r
+若为现场网络环境问题,则联系运营商增强网络覆盖,或者更换其他运营商SIM 卡解决。\r
+\r
+4. \r
+若现场信号良好,模块能识别到SIM 卡且SIM 卡未欠费,但模块不能附着上PS 域,则需要联系\r
+\r
+运营商确定SIM 卡配置:\r
+\r
+− \r
+SIM 卡是否开通数据业务\r
+\r
+− \r
+SIM 卡配置鉴权参数是否正常\r
+\r
+− \r
+SIM 卡是否有绑定设备\r
+\r
+− \r
+SIM 卡数据业务相关的APN、用户名、密码、鉴权方式是否正常\r
+\r
+###### 模块激活PDP 失败\r
+\r
+若模块正常注册上PS 域,但是无法激活PPP 连接。可以参考以下方式排查解决(涉及到的AT 命\r
+\r
+令,请参考功能 AT 指令集):\r
+\r
+功能模组TCP 应用指南 \r
+第4 章国网指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+41\r
+\r
+⚫ \r
+确保SIM 未欠费。\r
+\r
+⚫ \r
+使用AT+$MYNETCON?查询APN、用户名、密码,确保这些参数与运营商提供的一致。\r
+\r
+若以上参数都没有问题,模块仍无法激活PPP 连接,请联系当地运营商协助检查SIM 卡和后台服\r
+\r
+务器参数配置。\r
+\r
+###### 模块建立TCP 透传客户端连接失败\r
+\r
+若模块正常获取到IP,但是建立TCP 非透传客户端连接。可以参考以下方式排查解决(涉及到的\r
+\r
+AT 命令,请参考功能 AT 指令集):\r
+\r
+1. \r
+确认服务器IP 和端口都已配置好,端口已经建立服务器端侦听,IP 能被客户端PING 通。\r
+\r
+可以通过用其他客户端设备来连接该服务器IP 和端口确认。\r
+\r
+2. \r
+确认模块获取到的IP 是否能访问服务器。\r
+\r
+可通过发送AT+PING=<IP/域名>给模块,操作模块PING 服务器来确认。\r
+\r
+若模块能PING 通服务器,则需要检查服务器软件和端口配置。\r
+\r
+若模块不能PING 通服务器,则需要检查APN、用户名、密码设置是否正确,SIM 卡配置是否正\r
+\r
+确。\r
+\r
+3. \r
+确认SIM 卡配置是否有问题。\r
+\r
+可以直接联系运营商确认SIM 卡是否可以访问目的IP 或者让运营商直接跟踪后台消息排查原因。\r
+\r
+###### 模块发送TCP 数据失败\r
+\r
+若模块正常建立TCP 非透传客户端连接,但是发送TCP 数据失败。可以参考以下方式排查解决\r
+\r
+(涉及到的AT 命令,请参考功能 AT 指令集):\r
+\r
+1. \r
+确认是否是服务器问题。\r
+\r
+可通过更换服务器对比确认是否是服务器配置问题。\r
+\r
+2. \r
+确认SIM 卡是否也有欠费。\r
+\r
+可联系运营商确认。\r
+\r
+3. \r
+确认是否是长时间没有发送数据导致的假连接。\r
+\r
+可通过缩短心跳周期或者是提高数据发送频率来确认。\r
+\r
+若缩短心跳周期或者是提高数据发送频率后,数据发送正常则可能是假连接引起的问题。\r
+\r
+4. \r
+确认模块底层协议栈是否收到服务器端TCP 协议层的ACK。\r
+\r
+功能模组TCP 应用指南 \r
+第4 章国网指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+42\r
+\r
+可通过”AT$MYNETACK =<SocketID>”命令的返回值来确认。\r
+\r
+− \r
+若返回值中<unAcked_dataLen>字段数值为0,则表示数据已经发送到服务器且服务器已经\r
+\r
+确认。\r
+\r
+− \r
+若返回值中<unAcked_dataLen>字段数值不为0,则有可能是数据没有发送到服务器,或者\r
+\r
+是服务器接收有延时还未收到数据,需要在服务器和模块同时抓取TCP 层数据来排查,可以\r
+\r
+联系我司FAE 跟进排查。\r
+\r
+###### 模块接收TCP 数据失败\r
+\r
+若模块正常建立TCP 非透传客户端连接,但是接收TCP 数据失败。可以参考以下方式排查解决\r
+\r
+(涉及到的AT 命令,请参考功能 AT 指令集):\r
+\r
+1. \r
+确认是否是服务器问题。\r
+\r
+可通过更换服务器对比确认是否是服务器配置问题。\r
+\r
+2. \r
+确认SIM 卡是否也有欠费。\r
+\r
+可联系运营商确认。\r
+\r
+3. \r
+确认TCP 连接是否还存在。\r
+\r
+可通过”AT$MYNETOPEN?”命令的返回值来确认。\r
+\r
+− \r
+若返回值中含有“$MYNETOPEN:”字符串,则表示TCP 连接存在。\r
+\r
+− \r
+若返回字段只有OK,则表示TCP 连接不存在。\r
+\r
+4. \r
+确认是否是长时间没有发送数据导致的假连接。\r
+\r
+可通过缩短心跳周期或者是提高数据发送频率来确认。\r
+\r
+− \r
+若缩短心跳周期或者是提高数据发送频率后,数据接收正常则可能是假连接引起的问题。\r
+\r
+− \r
+若缩短心跳或提高数据发送频率仍无法确认接收不到数据的原因,需要在服务器和模块同时\r
+\r
+抓取TCP 层数据来排查,可以联系我司FAE 跟进排查。\r
+\r
+###### 4.3.2 示例\r
+\r
+1. \r
+检查并确保模块已成功注册上PS 域\r
+\r
+AT \r
+//确认模块能正常响应AT 命令 \r
+OK \r
+AT+CGSN\r
+\r
++CGSN: "865324037375316" \r
+OK\r
+\r
+AT+CGMR \r
++CGMR: N720_DAC0CM_DL_V004 \r
+OK\r
+\r
+功能模组TCP 应用指南 \r
+第4 章国网指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+43\r
+\r
+AT+CPIN? \r
++CPIN: READY \r
+//确认模块能识别到SIM 卡 \r
+OK \r
+AT+CIMI \r
+460016657520885 \r
+OK \r
+AT$MYNETCON=0,"APN","CMNET" \r
+ \r
+ \r
+//设置APN \r
+OK\r
+\r
+AT$MYNETCON=0,"USERPWD","CARD,CARD" \r
+ \r
+//设置用户名和密码 \r
+OK \r
+AT$MYNETCON=0,"AUTH",1 \r
+ \r
+ \r
+ \r
+//设置鉴权方式 \r
+OK \r
+AT+CSQ \r
++CSQ: 31, 99 \r
+//确认信号良好 \r
+OK \r
+AT+CREG=2 \r
+OK\r
+\r
+AT+CREG? \r
++CREG: 2,1,"FFFE","61C7E47",7 //确认模块注册上CS 域\r
+\r
+OK \r
+AT+CREG=0 \r
+OK\r
+\r
+AT+CGATT? \r
++CGATT: 1 \r
+//确认模块附着上PS 域 \r
+OK\r
+\r
+AT$MYSYSINFO\r
+\r
+$MYSYSINFO: 4,02 \r
+//确认模块注册的网络制式 \r
+OK\r
+\r
+2. \r
+激活网络连接\r
+\r
+AT$MYNETURC=1 \r
+OK \r
+AT$MYNETACT=0,1 \r
+ \r
+ \r
+ \r
+ \r
+//激活PPP 连接 \r
+OK \r
+$MYURCACT: 0,1,"10.51.228.13"\r
+\r
+AT$MYNETACT? \r
+ \r
+ \r
+ \r
+ \r
+//查询PPP 连接是否正常 \r
+$MYNETACT: 0,1,"10.51.228.13" \r
+OK\r
+\r
+3. \r
+建立TCP 非透传客户端连接并收发数据\r
+\r
+AT$MYNETCLOSE=0 \r
+ \r
+ERROR: 913\r
+\r
+AT$MYNETCREATE=0,0,0,"183.239.240.40",5008\r
+\r
+CONNECT \r
+ \r
+ \r
+//收发数据后,发送+++数据模式 \r
+OK \r
+AT$MYNETACK=0 \r
+$MYNETACK: 0,0,2047 \r
+OK \r
+ATO \r
+ \r
+ \r
+ \r
+ \r
+ \r
+//切换回数据模式 \r
+CONNECT\r
+\r
+OK \r
+AT$MYNETCLOSE=0 \r
+ \r
+ \r
+ \r
+//关闭链路\r
+\r
+功能模组TCP 应用指南 \r
+第4 章国网指令TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+44\r
+\r
+$MYNETCLOSE: 0 \r
+OK \r
+AT+CGATT=0 \r
+OK \r
+$MYURCACT: 0,0\r
+\r
+功能模组TCP 应用指南 \r
+第5 章外部协议栈TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+45\r
+\r
+#### 5 外部协议栈TCP 应用\r
+\r
+图 5-1 连接流程\r
+\r
+模块初始化流程\r
+\r
+"CONNECT"?\r
+\r
+ATD*99#\r
+\r
+进入外部协议栈成功\r
+\r
+进入外部协议栈\r
+• 若返回 CONNECT ,则模块进入外部协议栈模式;该指令\r
+\r
+等待返回"CONNECT"的超时时间为30s。\r
+• 若返回 NO CARRIER ,则继续执行ATD*99#,最多5次。若\r
+\r
+执行5次后仍未成功,则返回执行模块初始化流程查询网络\r
+注册情况。\r
+• 若超时未收到任何回码或者收到其他错误回码,则继续执行\r
+\r
+ATD*99#,最多3次。若执行3次后仍未成功,则重启模块。\r
+Yes\r
+\r
+No\r
+\r
+返回"NO CARRIER\r
+\r
+继续执行"\r
+\r
+其它情况\r
+重启模块\r
+\r
+##### 5.1 异常处理\r
+\r
+###### 模块附着PS 域失败\r
+\r
+使用AT+CPIN?检查模块是否已经识别到SIM 卡。若模块无法识别SIM 卡,可参考以下方式排查\r
+\r
+解决(涉及到的AT 命令,请参考功能 AT 指令集):\r
+\r
+− \r
+确保SIM 卡是否插好;\r
+\r
+− \r
+确保SIM 卡本身完好无损;\r
+\r
+− \r
+确保SIM 卡没有被锁卡;\r
+\r
+− \r
+确保SIM 卡卡座完好,不存在假焊;\r
+\r
+− \r
+擦拭SIM 卡及卡座的触点(该触点发生氧化、脏污也会产生读不到SIM 卡的现象),确保SI\r
+\r
+卡与模块卡座接触良好。\r
+\r
+若通过以上排查模块仍无法识别到SIM 卡,可以更换SIM 卡解决。\r
+\r
+1. \r
+检查天线是否完好,使用AT+CSQ 命令查询当前模块的接收信号是否满足通信条件。\r
+\r
+我们建议信号强度值为15(-83dbm)再进行通信。\r
+\r
+2. \r
+若现场信号良好,模块能识别到SIM 卡且SIM 卡未欠费,但模块不能附着上PS 域,可通过\r
+\r
+```\r
+AT+CGATT=1 命令,主动附着PS 域。\r
+```\r
+\r
+功能模组TCP 应用指南 \r
+第5 章外部协议栈TCP 应用\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+46\r
+\r
+##### 5.2 示例\r
+\r
+1. \r
+设置APN、用户名、密码,检查并确保模块已成功注册上PS 域\r
+\r
+AT \r
+OK\r
+\r
+AT+CGSN \r
++CGSN: "865324037375316" \r
+OK \r
+AT+CGMR \r
++CGMR: N720_EAB0CM_BZ_V008 \r
+OK \r
+AT+CPIN? \r
++CPIN: READY \r
+OK \r
+AT+CIMI\r
+\r
+460016657520885 \r
+OK \r
+AT+CGDCONT=1,"IP","UNINET" \r
+//设置APN \r
+OK \r
+AT+XGAUTH=1,1, "CARD","CARD" \r
+//设置用户名、密码、鉴权方式 \r
+OK \r
+AT+CSQ \r
++CSQ: 31, 99 \r
+OK\r
+\r
+AT+CREG=2 \r
+OK \r
+AT+CREG? \r
++CREG: 2,1,"FFFE","61C7E47",7 \r
+OK \r
+AT+CREG=0 \r
+OK \r
+AT+CGATT? \r
++CGATT: 1 \r
+OK\r
+\r
+AT$MYSYSINFO \r
+$MYSYSINFO: 4,02 \r
+OK\r
+\r
+2. \r
+进入外部协议栈\r
+\r
+ATD*99# \r
+ //进入外部协议栈 \r
+CONNECT\r
+\r
+功能模组TCP 应用指南\r
+\r
+A 关键AT 命令\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+47\r
+\r
+#### A 关键AT 命令\r
+\r
+##### A.1 标准AT 指令\r
+\r
+###### A.1.1 AT+CGDCONT–设置PDP 格式\r
+\r
+设置GPRS 的PDP(Packet Data Protocol,分组数据协议)格式。目前仅支持设置一个APN。\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令 \r
+响应格式\r
+\r
+设置\r
+\r
+AT+CGDCONT=<cid>[,<PDP_type>[,<APN>[\r
+,<PDP_addr>[,<d_comp>[,<h_comp>[,<pd1> \r
+[,…[,<pdN>]]]]]]]]<CR>\r
+\r
+<CR><LF>OK<CR><LF> \r
+Or \r
+<CR><LF>ERROR<CR><LF>\r
+\r
+查询 \r
+AT+CGDCONT?<CR>\r
+\r
+<CR><LF>+CGDCONT: \r
+<cid>,<PDP_type>,<APN>,<PDP_ad\r
+dr>,<d_comp>,<h_comp>,<pd1> ,…,\r
+<pdN><CR><LF> \r
+<CR><LF>OK<CR><LF>\r
+\r
+测试 \r
+AT+CGDCONT=?<CR>\r
+\r
+<CR><LF>+CGDCONT: value range \r
+of <cid>, value range of <PDP_type>, \r
+value range of <APN>, value range of \r
+<PDP_addr>, \r
+value \r
+range \r
+of \r
+<d_comp>, value range of <h_comp>, \r
+value range of <pd1> ,…, value range \r
+of pdN \r
+<CR><LF>OK<CR><LF>\r
+\r
+###### 参数\r
+\r
+<cid> \r
+(PDP Context Identifier)一个数字参数,指定一个PDP 上下文定义,这个参数是\r
+\r
+当地的TE-MT 接口并且被应用到其他PDP 上下文相关的命令当中,使用查询命\r
+\r
+令可以查询到允许的值(最小值为1)\r
+\r
+<PDP_type> \r
+(Packet Data Protocol type)字符串参数,用于指定分组数据协议的类型\r
+\r
+“IP”网络协议(Internet Protocol)(IETFSTD 5)\r
+\r
+功能模组TCP 应用指南\r
+\r
+A 关键AT 命令\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+48\r
+\r
+“PPP”点对点协议(Point to Point Protocol)(IETF STD 51)\r
+\r
+“IPv6”网络协议V6\r
+\r
+<APN> \r
+(Access Point Name)字符串形式,是一个逻辑名称,用来选择GGSN 或者外部\r
+\r
+分组数据网。\r
+\r
+<PDP_address> \r
+字符串形式,用来在地址空间中区分MT。\r
+\r
+如果不写这个参数,则在PDP 的启动过程当中由TE 提供这个值。\r
+\r
+如果TE 提供失败,就请求动态地址,即使在PDP 的启动过程当中分配了地址,\r
+\r
+在使用这条指令查询的时候仍然会返回空。\r
+\r
+<d_comp> \r
+数字参数用来控制PDP 数据压缩(仅适用于SNDCP)\r
+\r
+0 - off (缺省情况下默认值)\r
+\r
+<h_comp> \r
+数字参数用来控制PDP 头部压缩0 - off (缺省情况下默认值)\r
+\r
+<pd1>, … <pdN> 0 到N,字符串类型,意义与<PDP_type>有关\r
+\r
+###### 示例\r
+\r
+AT+CGDCONT=1,"IP","CMNET" \r
+OK \r
+设置PDP 格式,PDP 类型为IP,APN 名称为CMNET\r
+\r
+AT+CGDCONT? \r
++CGDCONT: 1,"IP","CMNET","0.0.0.0",0,0,0,0\r
+\r
+OK\r
+\r
+查询当前PDP 格式\r
+\r
+AT+CGDCONT=? \r
++CGDCONT: (1-24),"IP",,,(0-2),(0-4),(0-1),(0-1) \r
++CGDCONT: (1-24),"PPP",,,(0-2),(0-4),(0-1),(0-1) \r
++CGDCONT: (1-24),"IPV6",,,(0-2),(0-4),(0-1),(0-1) \r
++CGDCONT: (1-24),"IPV4V6",,,(0-2),(0-4),(0-1),(0-1) \r
+OK\r
+\r
+查询设置PDP 格式的取值范围\r
+\r
+###### A.1.2 AT+XGAUTH–用户认证\r
+\r
+PDP 认证。\r
+\r
+该指令要放在AT+CGDCONT 这条指令后面。目前在专网中各个地方逐渐增加了用户身份认证需\r
+\r
+求,使用内部协议栈,需要使用到这条指令,因此,请在代码流程上加上这条指令。\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令 \r
+响应格式\r
+\r
+设置 \r
+AT+XGAUTH=<cid>,<auth>[,<name>,\r
+<pwd>]<CR>\r
+\r
+<CR><LF>OK<CR><LF> \r
+Or \r
+<CR><LF>ERROR<CR><LF>\r
+\r
+测试 \r
+AT+XGAUTH=?\r
+\r
+<CR><LF>+XGAUTH: \r
+(list \r
+of \r
+supported \r
+<cid>),(value \r
+range \r
+of<auth>),(length \r
+of \r
+<name>),(length of <pwd>)\r
+\r
+功能模组TCP 应用指南\r
+\r
+A 关键AT 命令\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+49\r
+\r
+<CR><LF>OK<CR><LF>\r
+\r
+###### 参数\r
+\r
+<cid> \r
+(PDP Context Identifier)一个数字参数,指定一个PDP 上下文定义\r
+\r
+<cid>对应+CGDCONT 中的<cid>。\r
+\r
+<auth> \r
+鉴权类型,默认为1\r
+\r
+0:NONE\r
+\r
+1:PAP\r
+\r
+2:CHAP\r
+\r
+3:PAP or CHAP\r
+\r
+鉴权类型为非NONE 时,需带<name>和<pwd>参数\r
+\r
+<name> \r
+用户名\r
+\r
+<pwd> \r
+密码\r
+\r
+###### 示例\r
+\r
+AT+XGAUTH=1,1,”gsm”,”1234” \r
+OK \r
+设置第一个PDP 认证\r
+\r
+AT+XGAUTH=? \r
++XGAUTH: (1),(0-3),32,32 \r
+OK\r
+\r
+查询参数值范围\r
+\r
+###### A.1.3 AT+XIIC–建立PPP 链接\r
+\r
+建立PPP 连接,获取IP 地址。\r
+\r
+在使用AT+XIIC=1 建立PPP 连接之前,确保模块已经注册上网络。可使用AT+CEREG?来判断,\r
+\r
+如果返回+CEREG: 0,1 或+CEREG: 0,5,都表明已注册上网络。\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令 \r
+响应格式\r
+\r
+执行 \r
+AT+XIIC=<act><CR>\r
+\r
+<CR><LF>OK<CR><LF> \r
+Or \r
+<CR><LF>ERROR<CR><LF>\r
+\r
+查询 \r
+AT+XIIC?<CR> \r
+<CR><LF>+XIIC: <act>,<ip><CR><LF> \r
+<CR><LF>OK<CR><LF>\r
+\r
+功能模组TCP 应用指南\r
+\r
+A 关键AT 命令\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+50\r
+\r
+###### 响应时间\r
+\r
+最大响应时间300ms。\r
+\r
+###### 参数\r
+\r
+<act> \r
+0:去激活PPP 连接\r
+\r
+1:激活PPP 连接\r
+\r
+<ip> \r
+IP 地址\r
+\r
+###### 示例\r
+\r
+AT+XIIC=1 \r
+OK \r
+建立第一个ppp 连接\r
+\r
+AT+XIIC? \r
++XIIC: 1,10.107.216.162 \r
+OK\r
+\r
+第一个PPP 链路建立成功,IP 地址是10.107.216.162。\r
+\r
+(1 前面有4 个空格)\r
+\r
+AT+XIIC? \r
++XIIC: 0,0.0.0.0 \r
+OK\r
+\r
+PPP 链路还未建立成功,\r
+\r
+(0 前面有4 个空格)\r
+\r
+###### A.1.4 AT+TCPSETUP–建立TCP 连接\r
+\r
+建立TCP 连接。\r
+\r
+请先使用AT+XIIC=1 建立PPP 链接。\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令 \r
+响应格式\r
+\r
+执行 \r
+AT+TCPSETUP=<n>,<ip>,<port\r
+>[,<recv_mode>]<CR>\r
+\r
+<CR><LF>OK<CR><LF> \r
+<CR><LF>+TCPSETUP:<n>,<result><CR><LF> \r
+Or \r
+<CR><LF>ERROR<CR><LF> \r
+Or \r
+<CR><LF>+TCPSETUP:<n>,ERROR1<CR><LF> \r
+Or \r
+<CR><LF>+TCPSETUP:ERRPR<CR><LF>\r
+\r
+###### 响应时间\r
+\r
+响应超时时间为60 秒。\r
+\r
+功能模组TCP 应用指南\r
+\r
+A 关键AT 命令\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+51\r
+\r
+###### 参数\r
+\r
+<n> \r
+链路编号,只能为0~5\r
+\r
+<ip> \r
+目的IP 地址,必须是形如xx.xx.xx.xx 的输入,或者形如www.china.com(域名)\r
+\r
+<port> \r
+目的端口号,必须是十进制的ASCII 码\r
+\r
+<recv_mode> \r
+模式选择,范围0-1\r
+\r
+0:接收到的TCP、UDP 数据直接从串口输出(默认)\r
+\r
+1:接收到的TCP、UDP 数据缓存起来,输出来数据提示,需要外部MCU 主动发命\r
+\r
+令读取数据\r
+\r
+<result> \r
+结果码\r
+\r
+OK 建立成功\r
+\r
+FAIL 建立失败\r
+\r
+LINK ALREADY OPENED 连接已存在\r
+\r
+###### 示例\r
+\r
+AT+TCPSETUP=0,220.199.66.56,6800 \r
+OK\r
+\r
++TCPSETUP: 0,OK\r
+\r
+在链路0 上建立到 \r
+220.199.66.56,6800 的连接,成功\r
+\r
+AT+TCPSETUP=0,neowayjsr.oicp.net,60010 \r
+OK \r
+ \r
++TCPSETUP: 0,OK\r
+\r
+在链路0 上建立到 \r
+neowayjsr.oicp.net,60010 的连接,成功\r
+\r
++TCPCLOSE: 0,Link Closed \r
+链路被动断开后返回的回码 \r
+AT+TCPSETUP=1,192.168.20.6,7000 \r
+OK \r
+ \r
++TCPSETUP: 1,FAIL\r
+\r
+在链路1 上建立到192.168.20.6,7000 的连接,失\r
+败;可能是服务器未开通,IP 地址或端口不正确,或\r
+者是SIM 卡欠费,等等\r
+\r
+AT+TCPSETUP=0,neowayjsr.oicp.net,60010 \r
+OK \r
+ \r
++TCPSETUP:0,ERROR1\r
+\r
+当前链路0 的TCP/UDP 链接已存在\r
+\r
+AT+TCPSETUP=6,192.168.20.6,7000 \r
+ERROR \r
+AT 指令格式错误,链路号错误\r
+\r
+AT+TCPSETUP=0.58.60.184.213.10012 \r
+ERROR \r
+AT 指令格式错误,符号错误\r
+\r
+```\r
+AT+TCPSET=0,58.60.184.213,10012\r
+```\r
+\r
+ERROR \r
+AT 指令格式错误,指令不完整\r
+\r
+###### A.1.5 AT+TCPSEND–发送TCP 数据\r
+\r
+发送TCP 数据的命令,支持buffer 模式与命令模式,支持ASCII 与hex 发送。\r
+\r
+这条命令发送完毕后,会接收到大于号">",这时候请延迟50ms-100ms,然后发送数据。\r
+\r
+在发送TCP 数据之前,必须确保TCP 链路已经建立;建议在发送数据之前,先使用AT+IPSTATUS\r
+\r
+查看可用的buffer 大小。\r
+\r
+功能模组TCP 应用指南\r
+\r
+A 关键AT 命令\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+52\r
+\r
+命令设置参数不保存,每次发送需要设置模式。\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令 \r
+响应格式\r
+\r
+执行 \r
+AT+TCPSEND=<n>,<length>\r
+[[,<content>][,mode]]<CR>\r
+\r
+<CR><LF>><content> \r
+<CR><LF>OK<CR><LF> \r
+<CR><LF>+TCPSEND: <n>,<length><CR><LF> \r
+Or \r
+<CR><LF>> \r
+<CR><LF>+TCPSEND: ERROR<CR><LF> \r
+Or \r
+<CR><LF>> \r
+<CR><LF>+TCPSEND: \r
+<n>,OPERATION \r
+EXPIRED<CR><LF> \r
+Or \r
+<CR><LF>+TCPSEND: <result><CR><LF>\r
+\r
+###### 响应时间\r
+\r
+最大响应时间300ms。\r
+\r
+###### 参数\r
+\r
+<n> \r
+链路编号,只能为0~5,且该链路已建立了TCP 连接\r
+\r
+<length> \r
+要发送的数据长度,以字节为单位,取值范围:\r
+\r
+buffer 模式发送HEX 数据:1~4096;\r
+\r
+buffer 模式发送ASCII 数据:1~4096;\r
+\r
+命令模式发送HEX 数据:1~512;\r
+\r
+命令模式发送ASCII 数据:1~1024;\r
+\r
+<content> \r
+命令模式下发送的数据,支持转义模式,content 长度范围0~1024\r
+\r
+Content 内容的逗号个数必须小于15 个,若需要传输大量逗号的数据,建议使用\r
+\r
+buffer 模式。\r
+\r
+<mode> \r
+HEX 发送启用开关\r
+\r
+0:ASCII 模式发送,支持转义字符(默认)\r
+\r
+1:HEX 模式发送\r
+\r
+<result> \r
+SOCKET ID OPEN FAILED \r
+DATA LENGTH ERROR\r
+\r
+功能模组TCP 应用指南\r
+\r
+A 关键AT 命令\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+53\r
+\r
+###### 示例\r
+\r
+AT+TCPSEND=0,1 \r
+> \r
+OK \r
+ \r
++TCPSEND: 0,1\r
+\r
+在链路0 上发送1 字节的数据,成 \r
+功\r
+\r
+AT+TCPSEND=0,1024,,1 \r
+> \r
+OK \r
+ \r
++TCPSEND: 0,1024\r
+\r
+Buffer 模式,HEX 模式,发送1024 长度数据,发送成功\r
+\r
+AT+TCPSEND=0,27,"\"3~!@#$%\\^&*()_+;'332\",\"32\"" \r
+OK \r
+ \r
++TCPSEND: 0,27\r
+\r
+命令模式,转义模式,发送数据,发送成功\r
+\r
+```\r
+AT+TCPSEND=0,3,”313233”,1\r
+```\r
+\r
+OK \r
+ \r
++TCPSEND: 0,3\r
+\r
+命令模式,HEX 模式,发送数据,发送成功\r
+\r
+AT+TCPSEND=0,10 \r
+>\r
+\r
++TCPSEND:0,OPERATION EXPIRED\r
+\r
+输入发送命令出现“>”后,不输入数据,30 秒后提示超时\r
+\r
+AT+TCPSEND=0,1 \r
++TCPSEND: SOCKET ID OPEN FAILED\r
+\r
+在链路0 上发送1 字节的数据,该 \r
+链路尚未建立,发送失败\r
+\r
+###### A.1.6 AT+IPSTATUS–查询TCP/UDP 链路状态\r
+\r
+非透传下TCP/UDP 链路状态。由于UDP 的特点,该指令仅能查询是否已经通过指令建立链接,\r
+\r
+并不代表链接的真实情况。\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令 \r
+响应格式\r
+\r
+执行 \r
+AT+IPSTATUS=<n><CR>\r
+\r
+<CR><LF>+IPSTATUS: \r
+<n>,<STATUS>[,<TYPE>, \r
+<send-buffer-size>] \r
+Or \r
+<CR><LF>ERROR<CR><LF>\r
+\r
+###### 响应时间\r
+\r
+最大响应时间300ms。\r
+\r
+###### 参数\r
+\r
+<n> \r
+链路编号,范围 0~5\r
+\r
+功能模组TCP 应用指南\r
+\r
+A 关键AT 命令\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+54\r
+\r
+<STATUS> \r
+该链路的状态,取值:\r
+\r
+CONNECT \r
+DISCONNECT \r
+CONNECTING \r
+DISCONNECTING \r
+<TYPE> \r
+链路类型,取值为 TCP 或者 UDP\r
+\r
+<send-buffer-size> \r
+模块内部可用的send buffer 的大小,十进制ASCII 码表示,单位为字节\r
+\r
+UDP 链路上不支持查询<send-buffer-size>参数\r
+\r
+###### 示例\r
+\r
+```\r
+AT+IPSTATUS=0\r
+```\r
+\r
++IPSTATUS: 0,CONNECT,TCP,4096 \r
+链路0,已建TCP 连接,可用buffer 为4096 字节\r
+\r
+AT+IPSTATUS=0 \r
++IPSTATUS: 0,CONNECT,UDP,0 \r
+链路0,已建UDP 连接\r
+\r
+AT+IPSTATUS=1 \r
++IPSTATUS: 1,DISCONNECT \r
+链路1,未建立任何“TCP/UDP”连接\r
+\r
+AT+IPSTATU \r
+ERROR \r
+AT 指令格式错误,指令不完整\r
+\r
+```\r
+AT+IPSTATUS=6\r
+```\r
+\r
+ERROR \r
+指令链路编号错误\r
+\r
+###### A.1.7 AT+TCPACK–查询TCP 链路发送数据状态\r
+\r
+查询TCP 链路发送成功的数据大小、接收方成功接收该链路的数据大小。\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令 \r
+响应格式\r
+\r
+执行 \r
+AT+TCPACK=<n><CR>\r
+\r
+<CR><LF>+TCPACK: \r
+<n>,<data_sent>,<acked_recv><CR><LF> \r
+Or \r
+<CR><LF>ERROR<CR><LF> \r
+Or \r
+<CR><LF>+TCPACK: <n>,DISCONNECT<CR><LF> \r
+Or \r
+<CR><LF>+TCPACK: NO TCP LINK<CR><LF>\r
+\r
+###### 响应时间\r
+\r
+最大响应时间300ms。\r
+\r
+功能模组TCP 应用指南\r
+\r
+A 关键AT 命令\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+55\r
+\r
+###### 参数\r
+\r
+<n> \r
+链路编号,只能为0~5\r
+\r
+<data_sent> \r
+该链路发送成功的链路数据大小,无符号64 位整型数,十进制ASCII 码表示,单位\r
+\r
+为字节。\r
+\r
+<acked_recv> \r
+接收方成功接收的链路数据大小,无符号64 位整型数,十进制ASCII 码表示,单位\r
+\r
+为字节。\r
+\r
+###### 示例\r
+\r
+AT+TCPACK=0 \r
++TCPACK: 0,20,20 \r
+链路0,发送成功20 个字节数据,接收方成功接收20 个字节数据\r
+\r
+```\r
+AT+TCPACK=0\r
+```\r
+\r
++TCPACK: 0,128,120 \r
+链路0,发送成功128 个字节数据,接收方成功接收120 个字节数据\r
+\r
+AT+TCPACK=1 \r
++TCPACK: 1,DISCONNECT \r
+链路1,未建立任何连接\r
+\r
+```\r
+AT+TCPACK=2\r
+```\r
+\r
++TCPACK: NO TCP LINK \r
+链路2 建立的是UDP 连接\r
+\r
+AT+TCPACK=6 \r
+ERROR \r
+指令链路编号错误\r
+\r
+###### A.1.8 AT+TCPLISTEN–设置服务器侦听功能\r
+\r
+设置服务器侦听功能。\r
+\r
+使用联通卡或者移动的专网卡可以进行调试使用,移动的公网卡不能作为服务器调试;\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令 \r
+响应格式\r
+\r
+设置 \r
+AT+TCPLISTEN=<port><CR>\r
+\r
+<CR><LF>+TCPLISTEN:<socket>,OK \r
+Or \r
+<CR><LF>+TCPLISTEN:bind error \r
+Or \r
+<CR><LF>Listening... <CR><LF> \r
+Or \r
+<CR><LF>ERROR<CR><LF>\r
+\r
+查询 \r
+AT+TCPLISTEN?<CR>\r
+\r
+<CR><LF>+TCPLISTEN:listening status<CR><LF> \r
+Or \r
+<CR><LF>+TCPLISTEN:not listening<CR><LF>\r
+\r
+功能模组TCP 应用指南\r
+\r
+A 关键AT 命令\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+56\r
+\r
+###### 响应时间\r
+\r
+最大响应时间300ms。\r
+\r
+###### 参数\r
+\r
+<port> \r
+端口号\r
+\r
+<socket> \r
+socket 号\r
+\r
+###### 示例\r
+\r
+AT+TCPLISTEN=6800 \r
++TCPLISTEN: 0,OK\r
+\r
+侦听端口号6800 \r
+服务器侦听开始启动 \r
+AT+TCPLISTEN=6800 \r
++TCPLISTEN: bind error\r
+\r
+侦听端口号6800 \r
+绑定失败 \r
+AT+TCPLISTEN=6800\r
+\r
+Listening...\r
+\r
+如果已经设置了侦听,再设置的话,会提示\r
+\r
+Listening... \r
+AT+TCPLISTEN=0 \r
+ERROR \r
+侦听非法端口号\r
+\r
+AT+TCPLISTEN? \r
++TCPLISTEN: listening status \r
+查询侦听状态,表示当前处于侦听\r
+\r
+AT+TCPLISTEN? \r
++TCPLISTEN: not listening \r
+查询侦听状态,表示当前没有侦听\r
+\r
+Connect \r
+AcceptSocket=1,ClientAddr=119.123.77.133,ClientPort=8000\r
+\r
+收到主站连接请求,其AcceptSocket 是主站跟模块\r
+建立的socket,119.123.77.133 是主站的IP 地\r
+址,8000 是主站的端口号\r
+\r
+###### A.1.9 AT+CLOSELISTEN–关闭侦听链接\r
+\r
+关闭侦听链接。\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令 \r
+响应格式\r
+\r
+执行 \r
+AT+CLOSELISTEN<CR>\r
+\r
+<CR><LF>+CLOSELISTEN:<Socket>,local link closed \r
+Or \r
+<CR><LF>+CLOSELISTEN:Transparent local<CR><LF>\r
+\r
+主动上报 \r
++CLOSELISTEN:<Socket>,local link closed\r
+\r
+###### 响应时间\r
+\r
+最大响应时间300ms。\r
+\r
+功能模组TCP 应用指南\r
+\r
+A 关键AT 命令\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+57\r
+\r
+###### 参数\r
+\r
+无。\r
+\r
+###### 示例\r
+\r
++CLOSELISTEN: 0,local link closed \r
+主站关闭链接或网络异常时,会主动上报该回码 \r
+AT+CLOSELISTEN \r
++CLOSELISTEN: 0,local link closed \r
+如果有主站链接,会同时把主站链接关闭\r
+\r
+AT+CLOSELISTEN \r
++CLOSELISTEN:Transparent local \r
+linkclosed\r
+\r
+使用透传方式时,会接收到这些信息\r
+\r
+###### A.1.10 AT+CLOSECLIENT–关闭主站链接\r
+\r
+关闭主站链接。\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令 \r
+响应格式\r
+\r
+执行 \r
+AT+CLOSECLIENT[=<socket>]<CR>\r
+\r
+<CR><LF>+CLOSECLIENT: \r
+<socket>,remote \r
+link closed<CR><LF> \r
+Or \r
+<CR><LF>ERROR<CR><LF> \r
+Or \r
+<CR><LF>+CLOSECLIENT: All remote link \r
+closed<CR><LF>\r
+\r
+###### 响应时间\r
+\r
+最大响应时间300ms。\r
+\r
+###### 参数\r
+\r
+<socket> \r
+socket 号\r
+\r
+###### 示例\r
+\r
+AT+CLOSECLIENT \r
++CLOSECLIENT: 1,remote link closed \r
+ \r
++CLOSECLIENT: 2,remote link closed\r
+\r
+不带参数,多个关闭\r
+\r
+功能模组TCP 应用指南\r
+\r
+A 关键AT 命令\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+58\r
+\r
+AT+CLOSECLIENT=1 \r
++CLOSECLIENT: 1,remote link closed \r
+带参数,单个关闭\r
+\r
+AT+CLOSECLIENT=1 \r
+ERROR \r
+1 链路不存在远程客户端\r
+\r
+AT+CLOSECLIENT \r
++CLOSECLIENT: All remote link closed \r
+所有远程客户端已关闭\r
+\r
+###### A.1.11 +TCPRECV(S)–接收到主站的数据\r
+\r
+接收到主站的数据。\r
+\r
+跟客户端模式的接收格式不同,多了符号“(S)”;跟客户端的参数有所区别,请注意。\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令\r
+\r
+主动上报 \r
++TCPRECV(S): <n>,<length>,<data><CR>\r
+\r
+###### 响应时间\r
+\r
+无。\r
+\r
+###### 参数\r
+\r
+<n> \r
+链路编号,只能为0~5\r
+\r
+<length> \r
+接收到的数据长度\r
+\r
+<data> \r
+接收到的数据。尾部追加0x0d 0x0a。用户可根据<length>参数来判断结尾\r
+\r
+###### 示例\r
+\r
++TCPRECV(S): 1,10,1234567899 \r
+链路1 接收到主站发过来的10 个字节的数据,接收字符格式为字符类型\r
+\r
+###### A.1.12 AT+TCPREADS–读取到主站的数据\r
+\r
+读取到主站的数据\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令 \r
+响应格式\r
+\r
+执行 \r
+AT+TCPREADS=<n>,<length><CR> \r
+<CR><LF>+TCPREADS: <n>,<len>,<data>\r
+\r
+功能模组TCP 应用指南\r
+\r
+A 关键AT 命令\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+59\r
+\r
+<CR><LF>OK<CR><LF> \r
+Or \r
+<CR><LF>ERROR<CR><LF>\r
+\r
+###### 响应时间\r
+\r
+最大响应时间300ms。\r
+\r
+###### 参数\r
+\r
+<n> \r
+链路编号,只能为0~5\r
+\r
+<length> \r
+本次允许读取的最大数据长度,范围1-2048 字节\r
+\r
+<len> \r
+读取到的数据长度\r
+\r
+<data> \r
+读取到的数据\r
+\r
+###### 示例\r
+\r
++TCPRECV(S): 1 \r
+AT+TCPREADS=1,100 \r
++TCPREADS: 1,10,1234567890 \r
+OK\r
+\r
+RECVMODE=0 \r
+链路1 接收到主站发过来的10 个字节的数据\r
+\r
+###### A.1.13 AT+TCPSENDS–发送给主站的数据\r
+\r
+发送给主站的数据。\r
+\r
+在发送TCP 数据之前,必须确保TCP 链路已经建立。\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令 \r
+响应格式\r
+\r
+执行 \r
+AT+TCPSENDS=<socket>[\r
+,<length>]<CR>\r
+\r
+<CR><LF>> \r
+<CR><LF>OK<CR><LF> \r
+<CR><LF>+TCPSENDS:<socket>,<length><CR><LF> \r
+Or \r
+<CR><LF>> \r
+<CR><LF>+TCPSENDS: <result><CR><LF> \r
+Or \r
+<CR><LF>> \r
+<CR><LF>+TCPSENDS: \r
+<socket>, \r
+OPERATION \r
+EXPIRED<CR><LF>\r
+\r
+功能模组TCP 应用指南\r
+\r
+A 关键AT 命令\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+60\r
+\r
+Or \r
+<CR><LF>+TCPSENDS: <socket> is not link<CR><LF>\r
+\r
+###### 响应时间\r
+\r
+出现“>”后,不输入数据,60s 后提示超时。\r
+\r
+###### 参数\r
+\r
+<socket> \r
+侦听到的AcceptSocket 值,即主站跟模块的建立的socket ,参考\r
+\r
+```\r
+AT+TCPLISTEN 指令的说明\r
+```\r
+\r
+<length> \r
+要发送的数据长度,以字节为单位,取值范围1~2000\r
+\r
+<result> \r
+ERROR \r
+Buffer not enough,439\r
+\r
+###### 示例\r
+\r
+AT+TCPSENDS=0,10 \r
+> \r
+OK \r
+ \r
++TCPSENDS: 0,10\r
+\r
+在sokcet 0 上发送10 字节的数据(如:1234567890 或\r
+31323334353637383930),发送成功\r
+\r
+AT+TCPSENDS=0,536 \r
+> \r
++TCPSENDS: Buffer not enough,439\r
+\r
+在sokcet 0 上发送536 字节的数据(如:1234567890…),内部buffer\r
+不足,发送失败\r
+\r
+```\r
+AT+TCPSENDS=0\r
+```\r
+\r
+>\r
+\r
+OK \r
+ \r
++TCPSENDS: 0,21\r
+\r
+在链路 0 上发送 21 字节的数据(如:012345678901234567890 或\r
+\r
+303132333435363738393031323334353637383930),发送成功 \r
+(不带数据长度时以Ctrl+Z 为结束标志,最长不能超过2000)\r
+\r
+AT+TCPSENDS=0,1024 \r
+> \r
++TCPSENDS: ERROR\r
+\r
+服务器模块发送TCP 数据塞包\r
+\r
+AT+TCPSENDS=0,10 \r
++TCPSENDS: 0 is not link \r
+服务器模块未建立链路0 的连接\r
+\r
+```\r
+AT+TCPSENDS=0,5\r
+```\r
+\r
+> \r
++TCPSENDS: 0,OPERATION EXPIRED\r
+\r
+输入发送命令出现“>”后,不输入数据,30s 后提示超时\r
+\r
+###### A.1.14 AT+CLIENTSTATUS–查询主站链路的状态\r
+\r
+查询主站链路的状态\r
+\r
+功能模组TCP 应用指南\r
+\r
+A 关键AT 命令\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+61\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令 \r
+响应格式\r
+\r
+执行 \r
+AT+CLIENTSTATUS=<socket><CR>\r
+\r
+<CR><LF>+CLIENTSTATUS: <socket>,<status>, \r
+<type>,<send-buffer-size><CR><LF> \r
+Or \r
+<CR><LF>ERROR<CR><LF>\r
+\r
+###### 响应时间\r
+\r
+最大响应时间300ms。\r
+\r
+###### 参数\r
+\r
+<socket> \r
+侦听到的AcceptSocket 值,即主站跟模块的建立的socket ,参考\r
+\r
+```\r
+AT+TCPLISTEN 指令的说明\r
+```\r
+\r
+<status> \r
+该链路的状态,取值为CONNECT 或者DISCONNECT\r
+\r
+<type> \r
+链路类型,取值为TCP 或者INVALID\r
+\r
+INVALID 表示该链路不是侦听到的TCP 连接,可能为TCP/UDP 客户端或者\r
+\r
+服务器侦听链路。\r
+\r
+<send-buffer-size> \r
+模块内部可用的send buffer 的大小,十进制ASCII 码表示,单位为字节\r
+\r
+###### 示例\r
+\r
+AT+CLIENTSTATUS=0 \r
++CLIENTSTATUS: 0,CONNECT,TCP,61440\r
+\r
+主站socket 0,已建立TCP 连接,可用buffer 为\r
+61440 字节 \r
+AT+CLIENTSTATUS=4 \r
++CLIENTSTATUS: 4,DISCONNECT\r
+\r
+Socket 4,没有建立连接\r
+\r
+AT+CLIENTSTATUS=1 \r
++CLIENTSTATUS: 1,CONNECT,INVALID\r
+\r
+Socket 1 作为服务器侦听,链路类型返回INVALID\r
+\r
+###### A.1.15 AT+TCPACKS–查询TCP 服务器发送数据状态\r
+\r
+查询 TCP 服务器链路发送成功的数据大小、接收方成功接收该链路的数据大小。\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令 \r
+响应格式\r
+\r
+执行 \r
+AT+TCPACKS=<socket><CR> <CR><LF>+TCPACKS:\r
+\r
+<socket>,<data_sent>,<acked_recv>\r
+\r
+功能模组TCP 应用指南\r
+\r
+A 关键AT 命令\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+62\r
+\r
+Or \r
+<CR><LF>+TCPACKS: <n>,DISCONNECT<CR><LF>\r
+\r
+###### 响应时间\r
+\r
+最大响应时间300ms。\r
+\r
+###### 参数\r
+\r
+<socket> \r
+socket 链路编号,只能为0~5\r
+\r
+<data_sent> \r
+该链路发送成功的链路数据大小,无符号64 位整型数,十进制ASCII 码表示,单位\r
+\r
+为字节。。\r
+\r
+<acked_recv> \r
+接收方成功接收的链路数据大小,无符号64 位整型数,十进制ASCII 码表示,单位\r
+\r
+为字节。\r
+\r
+###### 示例\r
+\r
+AT+TCPACKS=0 \r
++TCPACKS: 0,20,20\r
+\r
+链路0,发送成功20 个字节数据,接收方成功接收20 个字节数据\r
+\r
+AT+TCPACKS=0 \r
++TCPACKS:0,20,20\r
+\r
+链路0,发送成功128 个字节数据,接收方成功接收120 个字节数据\r
+\r
+AT+TCPACKS=1 \r
++TCPACKS:1,DISCONNECT\r
+\r
+链路1,未建立任何连接\r
+\r
+AT+TCPACK=6 \r
+ERROR\r
+\r
+指令链路编号错误\r
+\r
+###### A.1.16 AT+TCPTRANS–建立TCP 透传链接\r
+\r
+建立TCP 透传链接的命令。\r
+\r
+建立TCP 透传链接成功后,向服务器发送数据,串口不显示发送的数据;\r
+\r
+使用“+++”指令(不带回车换行)切换到命令模式;“ATO”指令切换到数据模式;\r
+\r
+建议透传方式一次最多收发4096 字节数据。\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令 \r
+响应格式\r
+\r
+执行 \r
+AT+TCPTRANS=<ip>,<port> \r
+[,<ip_type>]<CR>\r
+\r
+[<CR><LF>OK<CR><LF>] \r
+<CR><LF>+TCPTRANS: <result><CR><LF> \r
+Or\r
+\r
+功能模组TCP 应用指南\r
+\r
+A 关键AT 命令\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+63\r
+\r
+<CR><LF>ERROR<CR><LF>\r
+\r
+###### 响应时间\r
+\r
+最大响应时间60s。\r
+\r
+###### 参数\r
+\r
+<ip> \r
+目的IP 地址,必须是形如xx.xx.xx.xx 的输入,或者形如www.china.com(域名)\r
+\r
+<port> \r
+目的端口号,必须是十进制的ASCII 码\r
+\r
+<result> \r
+OK \r
+FAIL \r
+ERROR\r
+\r
+###### 示例\r
+\r
+```\r
+AT+TCPTRANS=220.199.66.56,6800\r
+```\r
+\r
+OK \r
+ \r
++TCPTRANS:OK\r
+\r
+建立TCP 透传链接,成功\r
+\r
+AT+TCPTRANS=neowayjsr.oicp.net,60010 \r
+OK \r
+ \r
++TCPTRANS: OK\r
+\r
+用域名建立TCP 透传链接,成功\r
+\r
+AT+TCPTRANS=220.199.66.56, \r
++TCPTRANS: ERROR \r
+AT 指令格式错误\r
+\r
+```\r
+AT+TCPTRANS=220.199.66.56,6800\r
+```\r
+\r
+OK \r
+ \r
++TCPTRANS: FAIL\r
+\r
+建立TCP 透传链接,失败\r
+\r
+AT+TCPTRANS=220.199.66.56,6800 \r
+ERROR\r
+\r
+若已建立透传(TCP、UDP、TCP 服务器)链接,再发送该\r
+指令会返回:ERROR\r
+\r
+###### A.1.17 AT+TRANSCLOSE–关闭透传方式链接\r
+\r
+关闭透明传输方式链接。\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令 \r
+响应格式\r
+\r
+执行 \r
+AT+TRANSCLOSE<CR>\r
+\r
+<CR><LF>+TRANSCLOSE: <n>,OK \r
+Or \r
+<CR><LF>ERROR<CR><LF>\r
+\r
+功能模组TCP 应用指南\r
+\r
+A 关键AT 命令\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+64\r
+\r
+主动上报 \r
++TCPTRANS: Link Closed \r
++UDPTRANS: Link Closed\r
+\r
+###### 响应时间\r
+\r
+最大响应时间300ms。\r
+\r
+###### 参数\r
+\r
+无。\r
+\r
+###### 示例\r
+\r
+AT+TRANSCLOSE \r
++TRANSCLOSE: 0,OK \r
+主动关闭TCP 透传方式链接,成功\r
+\r
+AT+TRANSCLOSE \r
+ERROR \r
+未建立TCP/UDP 透传方式链接,失败\r
+\r
+```\r
+AT+TRANSCLOSE\r
+```\r
+\r
++TRANSCLOSE: 1,OK \r
+主动关闭UDP 透传方式链接,成功\r
+\r
++TCPTRANS: Link Closed \r
+被动关闭TCP 透传方式链接 \r
++UDPTRANS: Link Closed \r
+被动关闭UDP 透传方式链接\r
+\r
+###### A.1.18 ATD*99#–GPRS\r
+\r
+使用外部协议栈,进行GPRS 拨号连接\r
+\r
+进行拨号之前一定要确保CREG 已经注册成功,并且设置了APN。\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令 \r
+响应格式\r
+\r
+执行 \r
+ATD*99#<CR> \r
+<CR><LF>CONNECT<CR><LF>\r
+\r
+###### 参数\r
+\r
+无\r
+\r
+###### 示例\r
+\r
+ATD*99# \r
+开始拨号连接\r
+\r
+功能模组TCP 应用指南\r
+\r
+A 关键AT 命令\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+65\r
+\r
+CONNECT \r
+拨号成功的返回值\r
+\r
+##### A.2 国网AT 指令\r
+\r
+###### A.2.1 AT$MYNETCON–设置网络连接初始化参数\r
+\r
+该命令实现网络连接前的各种参数设置,是使用TCPIP 功能前必须完成的一步操作。\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令 \r
+响应格式\r
+\r
+设置 \r
+AT$MYNETCON=<channel>,\r
+<type>,<type_name><CR>\r
+\r
+<CR><LF>OK<CR><LF> \r
+<CR><LF>ERROR: <err><CR><LF>\r
+\r
+查询 \r
+AT$MYNETCON?<CR>\r
+\r
+<CR><LF>$MYNETCON: <Channel>,<Type>, \r
+<type_name> \r
+<CR><LF>OK<CR><LF>\r
+\r
+###### 参数\r
+\r
+<channel> \r
+通道号,0-5。\r
+\r
+<type>,<type\r
+_name>\r
+\r
+<type>与<type_name>对应的取值如下:\r
+\r
+USERPWD:用户名和密码,格式为"user,passwd"\r
+\r
+APN:字符串类型\r
+\r
+CFGT:每包发送时等待的时间,1-65535,单位ms,默认值100,用于透明数据传\r
+\r
+输模式\r
+\r
+CFGP:数据包被传送的门限值,1-1460,默认值为1024,用于透明数据传输模式\r
+\r
+AUTH:鉴权类型,默认为1\r
+\r
+0:NONE\r
+\r
+1:PAP\r
+\r
+2:CHAP\r
+\r
+3:PAP or CHAP\r
+\r
+###### 示例\r
+\r
+AT$MYNETCON=0,"USERPWD","user,pwd" \r
+OK\r
+\r
+AT$MYNETCON?\r
+\r
+$MYNETCON: 0,"USERPWD","" \r
+$MYNETCON: 0,"APN",""\r
+\r
+功能模组TCP 应用指南\r
+\r
+A 关键AT 命令\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+66\r
+\r
+$MYNETCON: 0,"CFGT",1000 \r
+$MYNETCON: 0,"CFGP",1024 \r
+$MYNETCON: 0,"AUTH",0 \r
+OK\r
+\r
+###### A.2.2 AT$MYNETACT–激活/去激活网络连接\r
+\r
+实现GPRS 网络的连接和断开,是使用TCP/IP 功能前必须完成的一步操作。\r
+\r
+命令执行成功,获取到IP 会主动上报本地IP。\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令 \r
+响应格式\r
+\r
+执行 \r
+AT$MYNETACT=<channel>,<a\r
+ction><CR>\r
+\r
+<CR><LF>OK<CR><LF> \r
+Or \r
+<CR><LF>ERROR: <err><CR><LF>\r
+\r
+查询 \r
+AT$MYNETACT?<CR> \r
+<CR><LF>$MYNETACT: <channel>,<status>,<IP> \r
+<CR><LF>OK<CR><LF>\r
+\r
+测试 \r
+AT$MYNETACT=?<CR> \r
+<CR><LF>$MYNETACT: <status>,<Channel> \r
+<CR><LF>OK<CR><LF>\r
+\r
+###### 参数\r
+\r
+<action> \r
+0:去激活网络连接\r
+\r
+1:激活网络连接\r
+\r
+<channel> \r
+通道号(0-5)\r
+\r
+<status> \r
+0:网络未激活\r
+\r
+1:网络已激活\r
+\r
+<IP> \r
+IP 地址;在网络已激活的情况下,显示本机IP\r
+\r
+###### 示例\r
+\r
+AT$MYNETACT=0,1 \r
+OK \r
+$MYURCACT: 0,1,"10.92.220.73" \r
+AT$MYNETACT=0,1 \r
+OK \r
+$MYURCACT: 0,1,"10.67.163.14"\r
+\r
+$MYURCACT:\r
+\r
+0,1,"24e0:bf:d206:6ed:1:2:fd94:4982"\r
+\r
+激活网络连接 \r
+执行成功 \r
+主动上报获取的IP \r
+双栈激活\r
+\r
+AT$MYNETACT? \r
+$MYNETACT: 0,1," 10.92.220.73" \r
+OK\r
+\r
+查询本地IP\r
+\r
+功能模组TCP 应用指南\r
+\r
+A 关键AT 命令\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+67\r
+\r
+AT$MYNETACT=0,1 \r
+OK \r
+$MYURCACT: 0,1,"10.67.163.14" \r
+AT$MYNETACT=0,1 \r
+ERROR: 902\r
+\r
+重复激活提示ERROR\r
+\r
+###### A.2.3 AT$MYNETSRV–设置非透明传输服务参数\r
+\r
+设置TCP/IP 的服务参数。\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令 \r
+响应格式\r
+\r
+执行\r
+\r
+AT$MYNETSRV=<Channel>,<\r
+SocketID>,<nettype>,<viewMo\r
+de>,<ip:port>[,<ip_type>]<CR>\r
+\r
+<CR><LF>OK<CR><LF> \r
+Or \r
+<CR><LF>ERROR: <err><CR><LF>\r
+\r
+查询 \r
+AT$MYNETSRV?<CR>\r
+\r
+<CR><LF>$MYNETSRV: <Channel>,<SocketID>, \r
+<nettype>,<viewMode>,<ip:port> \r
+<CR><LF>OK<CR><LF>\r
+\r
+测试 \r
+AT$MYNETSRV=?<CR> \r
+<CR><LF>OK<CR><LF>\r
+\r
+###### 参数\r
+\r
+<Channel> \r
+整数类型,0-5,通道号,每个通道2K Bytes 的发送缓冲区和10K Bytes 的接收缓\r
+\r
+冲区\r
+\r
+<SocketID> \r
+整数类型,0-14,本命令使用的链接号\r
+\r
+<nettype> \r
+网络类型\r
+\r
+0:TCP Client\r
+\r
+1:TCP Server,本模式下,最多可建立4 个侦听\r
+\r
+2:UDP\r
+\r
+<viewMode> \r
+非透明数据传输模式下数据显示方式\r
+\r
+0:HEX(默认)\r
+\r
+1:TEXT\r
+\r
+<ip:port> \r
+ \r
+<ip_type>\r
+\r
+address 所对应的值,如:59.40.29.34:4988。address 中的IP 地址为127.0.0.1\r
+\r
+的时候,表示使用该SocketID 建立侦听服务。\r
+\r
+域名解析方式\r
+\r
+0:IPv4(默认)\r
+\r
+1:IPv6\r
+\r
+<err> \r
+错误代码\r
+\r
+功能模组TCP 应用指南\r
+\r
+A 关键AT 命令\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+68\r
+\r
+###### 示例\r
+\r
+AT$MYNETSRV=0,0,0,0,"59.40.29.34:4988" \r
+OK\r
+\r
+通道0 设置TCP Client 服务参数 \r
+(远端服务器IP 地址为59.40.29.34,端口号为4988)\r
+\r
+###### A.2.4 AT$MYNETOPEN–开启服务\r
+\r
+打开TCP/UDP 连接。\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令 \r
+响应格式\r
+\r
+执行 \r
+AT$MYNETOPEN=<SocketID><CR>\r
+\r
+<CR><LF>$MYNETOPEN: <SocketID> \r
+[,<Remote_MSS>] \r
+<CR><LF>OK<CR><LF> \r
+Or \r
+<CR><LF>ERROR: <err><CR><LF>\r
+\r
+查询 \r
+AT$MYNETOPEN?<CR>\r
+\r
+<CR><LF>$MYNETOPEN:<SocketID>,<LocalIP>,<l\r
+ocal_port>,<gate>,<DNS1>,<DNS2>,<type>,<dest_i\r
+p>,<dest_port> \r
+<CR><LF>OK<CR><LF>\r
+\r
+测试 \r
+AT$MYNETOPEN=?<CR> \r
+<CR><LF>OK<CR><LF>\r
+\r
+###### 参数\r
+\r
+<SocketID> \r
+整数类型,0-14,本命令使用的链接号。\r
+\r
+<Remote_MSS> \r
+整数类型,TCP 连接打开时候,client 和server 协商成功的MSS 值。\r
+\r
+<LocalIP> \r
+字符类型,本地IP 地址。\r
+\r
+<local_port> \r
+本地端口(每个通道可能不一样)。\r
+\r
+<gate> \r
+网关\r
+\r
+<DNS1> \r
+首选域名解析服务器\r
+\r
+<DNS2> \r
+备用域名解析服务器\r
+\r
+<type> \r
+TCP Client/TCP Server/UDP \r
+ \r
+0:TCP Client\r
+\r
+1:TCP Server\r
+\r
+2:UDP\r
+\r
+<dest_ip> \r
+需要连接的IP 地址\r
+\r
+<dest_port> \r
+需要连接的端口号\r
+\r
+功能模组TCP 应用指南\r
+\r
+A 关键AT 命令\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+69\r
+\r
+###### 示例\r
+\r
+AT$MYNETOPEN=0 \r
+$MYNETOPEN: 0,2000 \r
+OK\r
+\r
+打开0 号socket。\r
+\r
+AT$MYNETOPEN? \r
+$MYNETOPEN:0,10.92.220.73,5000,0,dns1,dns2,0,59.40.29.34,4988\r
+\r
+OK\r
+\r
+OPEN 的socket 查询,查询到0 号\r
+socket 为打开状态。\r
+\r
+AT$MYNETSRV=5,1,0,0,"58.60.184.213:10188" \r
+OK \r
+AT$MYNETOPEN=2 \r
+ERROR: 913\r
+\r
+打开失败。\r
+\r
+###### A.2.5 AT$MYNETREAD–读取数据\r
+\r
+用于读取接收到的数据。\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令 \r
+响应格式\r
+\r
+执行 \r
+AT$MYNETREAD=<SocketID>,\r
+<data_len><CR>\r
+\r
+<CR><LF>$MYNETREAD: <SocketID>,<data_length> \r
+<CR><LF><data> \r
+<CR><LF>OK<CR><LF>\r
+\r
+测试 \r
+AT$MYNETREAD=?<CR> \r
+<CR><LF>OK<CR><LF>\r
+\r
+###### 参数\r
+\r
+<SocketID> \r
+整数类型,0-14,本命令使用的链接号。\r
+\r
+<data_len> \r
+需要读取的最大数据长度,1-2048。\r
+\r
+<data_length> \r
+实际读取到的数据长度,0-2048。\r
+\r
+<data> \r
+数据,当data_length=0 时,无<data>显示。\r
+\r
+###### 示例\r
+\r
+AT$MYNETREAD=0,2048 \r
+$MYNETREAD:0,0 \r
+OK\r
+\r
+从0 号socket 读取2048 个数据 \r
+没有数据在缓冲区或数据已经全部读取完毕\r
+\r
+AT$MYNETREAD=0,2048 \r
+$MYNETREAD: 0,10 \r
+1234567890 \r
+OK\r
+\r
+从0 号socket 读取2048 个数据 \r
+实际读到10 个数据 \r
+数据内容为1234567890\r
+\r
+###### A.2.6 AT$MYNETWRITE–发送数据\r
+\r
+用于发送数据。\r
+\r
+功能模组TCP 应用指南\r
+\r
+A 关键AT 命令\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+70\r
+\r
+UDP 链路,建议客户每次发送数据不大于1472 字节,可降低丢包概率。\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令 \r
+响应格式\r
+\r
+执行 \r
+AT$MYNETWRITE=<SocketID>,\r
+<data_len><CR>\r
+\r
+<CR><LF>$MYNETWRITE: <SocketID>,<data_len> \r
+<CR><LF><input_data> \r
+<CR><LF>OK<CR><LF> \r
+Or \r
+<CR><LF>ERROR: <err><CR><LF>\r
+\r
+测试 \r
+AT$MYNETWRITE=?<CR> \r
+<CR><LF>OK<CR><LF>\r
+\r
+###### 参数\r
+\r
+<SocketID> \r
+整数类型,0-14,本命令使用的链接号。\r
+\r
+<data_len> \r
+要发送的数据长度,1-2048。\r
+\r
+<input_data> \r
+本次发送的数据内容。\r
+\r
+<err> \r
+错误代码。\r
+\r
+###### 示例\r
+\r
+AT$MYNETWRITE=0,10 \r
+$MYNETWRITE: 0,10 \r
+1234567890\r
+\r
+OK\r
+\r
+0 号socket 发送10 个数据 \r
+表示可以输入要发送数据 \r
+写入要发送数据1234567890(输入的数据不会回显在串口上)\r
+\r
+命令执行成功\r
+\r
+###### A.2.7 AT$MYNETCLOSE–关闭连接\r
+\r
+用于关闭一条socket 连接。\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令 \r
+响应格式\r
+\r
+执行 \r
+AT$MYNETCLOSE=<SocketID><CR>\r
+\r
+<CR><LF>$MYNETCLOSE: <SocketID> \r
+<CR><LF>OK<CR><LF> \r
+<CR><LF>ERROR: <err><CR><LF>\r
+\r
+查询 \r
+AT$MYNETCLOSE?<CR> \r
+<CR><LF>OK<CR><LF>\r
+\r
+测试 \r
+AT$MYNETCLOSE=?<CR> \r
+<CR><LF>OK<CR><LF>\r
+\r
+功能模组TCP 应用指南\r
+\r
+A 关键AT 命令\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+71\r
+\r
+###### 参数\r
+\r
+<SocketID> \r
+整数类型,0-14,需要断开的socket 链接号。\r
+\r
+###### 示例\r
+\r
+AT$MYNETCLOSE=0 \r
+$MYNETCLOSE: 0 \r
+OK\r
+\r
+关闭0 号socket。\r
+\r
+###### A.2.8 AT$MYNETACK–查询TCPACK 信息\r
+\r
+用于查询终端已发送但未被对端确认的TCP 数据数量,模块内置协议栈剩余缓冲区的大小。\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令 \r
+响应格式\r
+\r
+执行 \r
+AT$MYNETACK=<SocketID><CR>\r
+\r
+<CR><LF>$MYNETACK:<SocketID>, \r
+<unAcked_dataLen>,<rest_bufferLen> \r
+<CR><LF>OK<CR><LF>\r
+\r
+###### 参数\r
+\r
+<SocketID> \r
+整数类型,0-14,需要查询的链接号。\r
+\r
+<unAcked_dataLen> \r
+数据发送但未收到对方确认信息的数量,最大值为2^32-1。\r
+\r
+<rest_bufferLen> \r
+模块内置协议栈剩余缓冲区的大小,0-10240。\r
+\r
+###### 示例\r
+\r
+AT$MYNETACK=0 \r
+$MYNETACK: 0,0,10240 \r
+OK\r
+\r
+查询0 号socket 发送数据的ACK \r
+发送了的未被对端确认ACK 的数量为0 \r
+模块内置协议栈剩余缓冲区为10240\r
+\r
+###### A.2.9 AT$MYNETACCEPT–接受侦听请求\r
+\r
+该命令是用于当通信模块工作在TCP 服务器模式下,接收客户端的侦听请求。\r
+\r
+在透明数据传输模式下,如果要关闭Server 的连接,首先要+++退出数据模式,进入命令模式,然\r
+\r
+后运行AT$MYNETCLOSE=SocketID,这样将一起关闭Server 侦听的SocketID 和这个SocketID 下建\r
+\r
+立的其他链接。\r
+\r
+功能模组TCP 应用指南\r
+\r
+A 关键AT 命令\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+72\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令 \r
+响应格式\r
+\r
+执行 \r
+AT$MYNETACCEPT=<SocketID>,\r
+<action>,<transportMode><CR>\r
+\r
+<transportMode>=0:\r
+\r
+<CR><LF>OK<CR><LF> \r
+<transportMode>=1:\r
+\r
+<CR><LF>CONNECT<CR><LF> \r
+Or \r
+<CR><LF>ERROR: <err><CR><LF>\r
+\r
+查询 \r
+AT$MYNETACCEPT?<CR> \r
+<CR><LF>OK<CR><LF>\r
+\r
+测试 \r
+AT$MYNETACCEPT=?<CR> \r
+<CR><LF>OK<CR><LF>\r
+\r
+###### 参数\r
+\r
+<SocketID> \r
+0-14,新接入的socket id,由主动上报$MYURCCLIENT 提供。\r
+\r
+<action> \r
+是否接受侦听。\r
+\r
+0:接受\r
+\r
+1:拒绝\r
+\r
+<transportMode> \r
+侦听成功后模块的传输模式。\r
+\r
+0:非透明数据传输模式\r
+\r
+1:透明数据传输模式\r
+\r
+###### 示例\r
+\r
+AT$MYNETSRV=0,0,1,0,"127.0.0.1:5000" \r
+OK \r
+AT$MYNETOPEN=0 \r
+$MYNETOPEN: 0 \r
+OK \r
+ \r
+$MYURCCLIENT: 1,"10.90.72.235",51614\r
+\r
+AT$MYNETACCEPT=1,0,0 \r
+OK\r
+\r
+设置0 号socket 侦听本地端口5000。 \r
+ \r
+打开0 号socket 开启服务器模式。 \r
+ \r
+ \r
+ \r
+客户端1 连接上报\r
+\r
+接受客户端1 连接\r
+\r
+$MYURCCLIENT: 2,"10.90.72.235",55469 \r
+AT$MYNETACCEPT=2,1,0 \r
+OK\r
+\r
+客户端2 连接上报 \r
+拒绝客户端2 连接\r
+\r
+###### A.2.10 AT$MYNETCREATE–开启透明传输服务\r
+\r
+开启通信模块透明数据传输服务。\r
+\r
+TCP/UDP Client 模式下,执行本条命令后通信模块开始连接服务器,连接服务器成功后,通信模块\r
+\r
+进入透明数据传输模式。\r
+\r
+功能模组TCP 应用指南\r
+\r
+A 关键AT 命令\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+73\r
+\r
+TCPServer 模式下,执行本条命令,模块返回OK,仅代表通信模块建立侦听socket 成功,但模块\r
+\r
+不立即进入数据模式,仍旧在命令模式;只有在客户端连接成功以后,通信模块才进入透明数据传输模\r
+\r
+式。\r
+\r
+透明数据传输模式的挂断,必须首先使用“+++”切入命令模式后,方可执行AT$MYNETCLOSE\r
+\r
+命令。+++要以字符串的形式输入才能退出透传模式,单个+输入,倘若+与+之间相差50ms,+会当做\r
+\r
+数据发送出去。\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令 \r
+响应格式\r
+\r
+执行\r
+\r
+AT$MYNETCREATE=<channel>,<mode>,<\r
+SocketID>,<ip>,<port>[,<local_port>][,<ip_ty\r
+pe>]<CR>\r
+\r
+<mode>=1:\r
+\r
+<CR><LF>OK<CR><LF>\r
+\r
+<mode>=0 或2:\r
+\r
+<CR><LF>CONNECT<CR><LF> \r
+Or \r
+<CR><LF>ERROR: <err><CR><LF>\r
+\r
+查询 \r
+AT$MYNETCREATE?<CR> \r
+<CR><LF>OK<CR><LF>\r
+\r
+测试 \r
+AT$MYNETCREATE=?<CR> \r
+<CR><LF>OK<CR><LF>\r
+\r
+###### 参数\r
+\r
+<channel> \r
+通道号0-5\r
+\r
+<mode> \r
+TCP Client/TCP Server/UDP,0-2\r
+\r
+0:TCPClient\r
+\r
+1:TCPServer\r
+\r
+2:UDP\r
+\r
+<SocketID> \r
+本次的链接号,整数类型,0-14\r
+\r
+<ip> \r
+需要连接的IP\r
+\r
+<port> \r
+客户端模式则为需要连接的端口,服务器模式则为侦听端口\r
+\r
+<local_port> \r
+<ip_type>\r
+\r
+可缺省\r
+\r
+域名解析方式,可缺省\r
+\r
+0:IPv4(默认)\r
+\r
+1:IPv6\r
+\r
+###### 示例\r
+\r
+AT$MYNETCREATE=0,0,0,"59.40.29.34",4988,3000 \r
+CONNECT\r
+\r
+建立TCP 透明数据传输通道 \r
+目的IP 和PORT:"59.40.29.34",4988\r
+\r
+功能模组TCP 应用指南\r
+\r
+A 关键AT 命令\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+74\r
+\r
+OK \r
+ \r
+AT$MYNETCREATE=0,0,0,"2400:da00:2::29",4988,3000 \r
+CONNECT \r
+ \r
+OK\r
+\r
+CONNECT 表示可以开始透明传输 \r
+输入+++ \r
+模块返回OK 表示回到命令模式 \r
+ \r
+建立TCPIPV6 透明数据传输通道\r
+\r
+###### A.2.11 $MYURCREAD–数据到来主动上报\r
+\r
+该主动上报命令表达模块接收到了数据。\r
+\r
+每收到一包数据,上报一次。\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令\r
+\r
+主动上报 \r
+<CR><LF>$MYURCREAD: <SocketID><CR><LF>\r
+\r
+###### 参数\r
+\r
+<SocketID> \r
+整数类型,0-14,本次上报的链接号。\r
+\r
+###### 示例\r
+\r
+AT$MYNETSRV=0,0,0,0,"59.40.29.34:4988"\r
+\r
+OK \r
+AT$MYNETOPEN=0\r
+\r
+$MYNETOPEN: 0,2000 \r
+OK\r
+\r
+$MYURCREAD: 0 \r
+AT$MYNETREAD=0,2048 \r
+$MYNETREAD: 0,10 \r
+0123456789\r
+\r
+OK\r
+\r
+0 号socket 设置为TCP 客户端\r
+\r
+打开0 号socket 进行TCP 链接\r
+\r
+0 号socket 数据到来主动上报 \r
+按最大长度2048 读取 \r
+0 号socket 读取到10 个字节 \r
+数据内容为0123456789\r
+\r
+###### A.2.12 $MYURCCLOSE–链路断开主动上报\r
+\r
+该主动上报命令表达当前SocketID 对应的链接已断开\r
+\r
+上报了本命令后,SocketID 对应的缓冲区内的收发数据会被清空。\r
+\r
+功能模组TCP 应用指南\r
+\r
+A 关键AT 命令\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+75\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令\r
+\r
+主动上报 \r
+<CR><LF>$MYURCCLOSE: <SocketID><CR><LF>\r
+\r
+###### 参数\r
+\r
+<SocketID> \r
+整数类型,0-14,本次上报的链接号。\r
+\r
+###### 示例\r
+\r
+AT$MYNETSRV=0,0,0,0,"59.40.29.34:4988" \r
+OK \r
+AT$MYNETOPEN=0 \r
+$MYNETOPEN: 0,2000 \r
+OK\r
+\r
+$MYURCCLOSE: 0\r
+\r
+0 号socket 设置为TCP 客户端 \r
+ \r
+打开0 号socket 进行TCP 链接\r
+\r
+主动上报:0 号socket 断开\r
+\r
+###### A.2.13 $MYURCACT–网络连接状态主动上报\r
+\r
+该主动上报命令表达网络连接建立或者断开\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令\r
+\r
+主动上报 \r
+<CR><LF>$MYURCACT: <channel>,<type>,<IP><CR><LF>\r
+\r
+###### 参数\r
+\r
+<channel> \r
+使用的Channel 通道\r
+\r
+<type> \r
+网络连接状态\r
+\r
+0:网络连接断开\r
+\r
+1:网络连接建立\r
+\r
+<IP> \r
+本机IP 地址\r
+\r
+###### 示例\r
+\r
+AT$MYNETACT=0,1 \r
+OK\r
+\r
+激活开网络连接 \r
+网络连接状态上报\r
+\r
+功能模组TCP 应用指南\r
+\r
+A 关键AT 命令\r
+\r
+深圳市有方科技股份有限公司版权所有 \r
+76\r
+\r
+$MYURCACT: 0,1,"10.91.102.62" \r
+网络激活成功,获取的本地IP 为"10.91.102.62" \r
+AT$MYNETACT=0,1 \r
+OK \r
+$MYURCACT: 0,1,"10.67.163.14"\r
+\r
+激活开网络连接 \r
+网络连接状态上报 \r
+网络激活成功,获取的本地IP 为"10.91.102.62"\r
+\r
+###### A.2.14 $MYURCCLIENT–客户端连接主动上报\r
+\r
+当开启侦听服务后,客户端连接上报。\r
+\r
+该上报表示有客户端申请连接。\r
+\r
+###### 命令格式\r
+\r
+类型 \r
+命令\r
+\r
+主动上报 \r
+<CR><LF>$MYURCCLIENT: <SocketID>,<IP>,<port><CR><LF>\r
+\r
+###### 参数\r
+\r
+<SocketID> \r
+新Client socket id\r
+\r
+<IP> \r
+客户端IP 地址\r
+\r
+<port> \r
+客户端端口号\r
+\r
+###### 示例\r
+\r
+AT$MYNETSRV=0,0,1,0,"127.0.0.1:5000"\r
+\r
+OK\r
+\r
+AT$MYNETOPEN=0 \r
+$MYNETOPEN: 0 \r
+OK \r
+ \r
+$MYURCCLIENT: 1,"10.90.72.235",51614 \r
+AT$MYNETACCEPT=1,0,0 \r
+OK\r
+\r
+0 号socket 设置为TCP server,侦听端口为5000\r
+\r
+打开0 号socket 开始侦听\r
+\r
+客户端连接主动上报\r
/* 调试配置 */
#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) - 只显示错误信息
* @param timezone_quarters 时区(quarters,15分钟为单位)
* @return ERRCODE_SUCC-成功,其他-失败
*
- * 时间设置逻辑说明:
+ * æ\97¶é\97´è®¾ç½®é\80»è¾\91说æ\98\8eï¼\88å\8f\82ç\85§ cat1_porting.c ç\9a\84 sync_time_utc å®\9eç\8e°ï¼\89ï¼\9a
* - 输入: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)
{
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;
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);
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;
}
#include "tcxo.h"
#include <stdint.h>
#include "pm_veto.h"
+#include "pm.h"
#include <sys/stat.h>
// #include "config/include/ConfigModelC.h"
#include "upg_porting.h"
#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;
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);
--- /dev/null
+#!/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 <id> <msg> - 向指定客户端发送文本消息")
+ print(" sendall <msg> - 向所有客户端发送文本消息")
+ print(" close <id> - 关闭指定客户端连接")
+ 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 <id> <msg> - 向指定客户端发送文本消息")
+ print(" sendall <msg> - 向所有客户端发送文本消息")
+ print(" close <id> - 关闭指定客户端连接")
+ 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服务器已停止")
+
* - 未定义 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
*/
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
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;
}
}
+/* ========== 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;
+ }
+}
+
#include <string.h>
#include <stdlib.h>
#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);
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;
bool stop_requested;
bool reconnect_requested;
bool sleep_mode;
+ bool is_connected; /* 连接状态标志 */
} n706a_ws_adapter_t;
{
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);
}
if (is_finished) {
/* 完整消息 */
if (g_adapter.callbacks.message_callback) {
+ /* 注意:data 应该已经是以 \0 结尾的字符串
+ * (由 n706a_websocket.c 在解析层保证,使用 recv_buffer 作为临时缓冲区)
+ * 这里直接传递即可,不需要再次创建副本
+ */
g_adapter.callbacks.message_callback(data, 0);
}
} else {
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);
}
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);
}
{
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,
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;
+ }
}
}
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;
}
/* 休眠模式处理 */
/* 清理 */
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;
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;
}