/* * ESP8266 WiFi模块驱动(支持HTTP加密传输) * 版本:v2.0 - 加密方案C * 修改: * 1. 添加HTTP POST请求支持 * 2. 添加加密数据传输 * 3. 保留原有TCP兼容接口 */ #include "esp8266.h" #include "Serial.h" #include "Delay.h" #include "Debug.h" #include "Crypto.h" #include #include #include // ← 添加atoi函数声明 // 外部配置(在main.c中定义) extern const char* WIFI_SSID; extern const char* WIFI_PASSWORD; extern const char* HTTP_SERVER_IP; extern const char* HTTP_SERVER_PORT; extern const char* HTTP_API_TOKEN; extern uint8_t g_server_connected; /* --- 私有辅助函数 --- */ static void ESP8266_SendCmd(const char* cmd) { Serial_SendString(cmd); Serial_SendString("\r\n"); } static uint8_t ESP8266_WaitForAck(const char* ack, uint32_t timeout) { uint32_t time_start = Get_Time_ms(); uint32_t time_elapsed; Serial_ClearRxBuffer(); DEBUG_PRINT("[ESP8266] Waiting for: %s (timeout: %dms)\r\n", ack, timeout); while(1) { time_elapsed = Get_Time_ms() - time_start; if (time_elapsed > timeout) { DEBUG_PRINT("[ESP8266] Timeout after %dms\r\n", time_elapsed); break; } char* response = Serial_GetRxBuffer(); if (response && strstr(response, ack) != NULL) { DEBUG_OK("[ESP8266] Received expected response"); return ESP8266_SUCCESS; } Delay_ms(10); } DEBUG_PRINT("[ESP8266] Failed! Response: %s\r\n", Serial_GetRxBuffer()); return ESP8266_FAIL; } static uint8_t IsLeapYear(uint16_t year) { return ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) ? 1 : 0; } static uint8_t DaysInMonth(uint16_t year, uint8_t month) { static const uint8_t days_table[12] = {31,28,31,30,31,30,31,31,30,31,30,31}; if (month == 2 && IsLeapYear(year)) return 29; return days_table[month - 1]; } static void EpochToDateTimeUTC8(uint32_t epoch_seconds, ESP8266_DateTime* datetime) { // 转为北京时间(UTC+8) uint32_t total = epoch_seconds + 8U * 3600U; uint32_t days = total / 86400U; uint32_t sec_of_day = total % 86400U; datetime->hour = sec_of_day / 3600U; datetime->minute = (sec_of_day % 3600U) / 60U; datetime->second = sec_of_day % 60U; datetime->year = 1970; while (1) { uint16_t ydays = IsLeapYear(datetime->year) ? 366 : 365; if (days < ydays) break; days -= ydays; datetime->year++; } datetime->month = 1; while (1) { uint8_t mdays = DaysInMonth(datetime->year, datetime->month); if (days < mdays) break; days -= mdays; datetime->month++; } datetime->day = (uint8_t)(days + 1U); } static uint8_t ESP8266_CopyDecryptedBody(char* response, char* body_start, uint16_t body_len, char* response_buffer, uint16_t buffer_size) { uint8_t encrypted = 0; char saved_char; uint32_t plain_len; if (strstr(response, "X-Encrypted: AES128-CBC") != NULL) { encrypted = 1; } if (!encrypted) { if (body_len >= buffer_size) { body_len = buffer_size - 1; } strncpy(response_buffer, body_start, body_len); response_buffer[body_len] = '\0'; return ESP8266_SUCCESS; } saved_char = body_start[body_len]; body_start[body_len] = '\0'; plain_len = Crypto_DecryptJSON(body_start, response_buffer, buffer_size); body_start[body_len] = saved_char; if (plain_len == 0) { DEBUG_ERROR("[ESP8266] Failed to decrypt encrypted HTTP response"); return ESP8266_FAIL; } return ESP8266_SUCCESS; } /* --- 公共接口函数实现 --- */ uint8_t ESP8266_Setup(void) { ESP8266_SendCmd("+++"); Delay_ms(500); ESP8266_SendCmd("AT"); if (!ESP8266_WaitForAck("OK", 1000)) return ESP8266_FAIL; ESP8266_SendCmd("AT+CWMODE=1"); if (!ESP8266_WaitForAck("OK", 2000)) return ESP8266_FAIL; // 先关闭可能存在的连接(避免 "link is builded" 错误) ESP8266_SendCmd("AT+CIPCLOSE"); Delay_ms(500); // 等待连接关闭 // 设置单连接模式 ESP8266_SendCmd("AT+CIPMUX=0"); ESP8266_WaitForAck("OK", 1000); return ESP8266_SUCCESS; } uint8_t ESP8266_ConnectWiFi(const char* ssid, const char* password) { char cmd_buffer[128]; sprintf(cmd_buffer, "AT+CWJAP=\"%s\",\"%s\"", ssid, password); ESP8266_SendCmd(cmd_buffer); return ESP8266_WaitForAck("WIFI GOT IP", 15000); } /** * @brief 连接到HTTP服务器(新增) */ uint8_t ESP8266_ConnectHTTPServer(const char* ip, const char* port) { char cmd_buffer[100]; uint32_t start_time; DEBUG_PRINT("[ESP8266] Connecting to HTTP server %s:%s\r\n", ip, port); // 先断开之前的连接 ESP8266_SendCmd("AT+CIPCLOSE"); Delay_ms(500); // 建立TCP连接 sprintf(cmd_buffer, "AT+CIPSTART=\"TCP\",\"%s\",%s", ip, port); Serial_ClearRxBuffer(); ESP8266_SendCmd(cmd_buffer); // 等待连接成功 start_time = Get_Time_ms(); while(Get_Time_ms() - start_time < 10000) { char* response = Serial_GetRxBuffer(); if (strstr(response, "CONNECT") != NULL || strstr(response, "ALREADY") != NULL) { DEBUG_OK("[ESP8266] HTTP server connected"); Delay_ms(500); return ESP8266_SUCCESS; } if (strstr(response, "ERROR") != NULL || strstr(response, "CLOSED") != NULL) { DEBUG_ERROR("[ESP8266] Connection failed"); break; } Delay_ms(100); } DEBUG_ERROR("[ESP8266] Connection timeout"); return ESP8266_FAIL; } /** * @brief 发送加密数据(HTTP POST请求) */ uint8_t ESP8266_SendEncryptedData(const char* encrypted_data) { char http_request[1024]; char cmd_buffer[32]; char response_body[192]; uint32_t content_length = strlen(encrypted_data); char* response; uint8_t send_ok = 0; if (ESP8266_CheckConnection() != ESP8266_SUCCESS) { DEBUG_INFO("[ESP8266] HTTP connection not ready, reconnecting..."); if (ESP8266_ConnectHTTPServer(HTTP_SERVER_IP, HTTP_SERVER_PORT) != ESP8266_SUCCESS) { DEBUG_ERROR("[ESP8266] Reconnect failed before POST"); return ESP8266_FAIL; } g_server_connected = 1; } // 构建HTTP POST请求 sprintf(http_request, "POST /api/v1/attendance/checkins HTTP/1.1\r\n" "Host: %s:%s\r\n" "Content-Type: application/octet-stream\r\n" "X-Api-Token: %s\r\n" "X-Encrypted: AES128-CBC\r\n" "X-Encrypted-Response: true\r\n" "Content-Length: %u\r\n" "Connection: close\r\n" "\r\n" "%s", HTTP_SERVER_IP, HTTP_SERVER_PORT, HTTP_API_TOKEN, content_length, encrypted_data ); uint32_t request_len = strlen(http_request); DEBUG_PRINT("[ESP8266] Sending HTTP POST (%u bytes)\r\n", request_len); // 发送数据长度命令 sprintf(cmd_buffer, "AT+CIPSEND=%u", request_len); Serial_ClearRxBuffer(); ESP8266_SendCmd(cmd_buffer); // 等待 ">" 提示符 uint32_t start_time = Get_Time_ms(); while(Get_Time_ms() - start_time < 2000) { if (strstr(Serial_GetRxBuffer(), ">") != NULL) { DEBUG_INFO("[ESP8266] Ready to send"); // 发送HTTP请求 Serial_SendString(http_request); break; } Delay_ms(10); } if (Get_Time_ms() - start_time >= 2000 && strstr(Serial_GetRxBuffer(), ">") == NULL) { DEBUG_ERROR("[ESP8266] No '>' prompt"); return ESP8266_FAIL; } start_time = Get_Time_ms(); while(Get_Time_ms() - start_time < 8000) { response = Serial_GetRxBuffer(); if (strstr(response, "SEND FAIL") != NULL || strstr(response, "ERROR") != NULL) { DEBUG_ERROR("[ESP8266] POST send failed"); return ESP8266_FAIL; } if (strstr(response, "SEND OK") != NULL) { send_ok = 1; } if (send_ok && strstr(response, "HTTP/1.1 200") != NULL) { char* ipd_marker = strstr(response, "+IPD,"); if (ipd_marker != NULL) { char* data_start = strchr(ipd_marker, ':'); if (data_start != NULL) { response = data_start + 1; } } char* body_start = strstr(response, "\r\n\r\n"); if (body_start != NULL) { uint16_t body_len; body_start += 4; body_len = strlen(body_start); char* cl_str = strstr(response, "Content-Length:"); if (cl_str != NULL) { uint32_t post_content_length; cl_str += 15; while (*cl_str == ' ') cl_str++; post_content_length = (uint32_t)atoi(cl_str); if (post_content_length > 0 && body_len > post_content_length) { body_len = (uint16_t)post_content_length; } } char* closed_marker = strstr(body_start, "\r\nCLOSED"); if (closed_marker != NULL) { *closed_marker = '\0'; body_len = strlen(body_start); } closed_marker = strstr(body_start, "CLOSED"); if (closed_marker != NULL) { *closed_marker = '\0'; body_len = strlen(body_start); } if (ESP8266_CopyDecryptedBody(response, body_start, body_len, response_body, sizeof(response_body)) == ESP8266_SUCCESS && strstr(response_body, "\"success\":true") != NULL) { DEBUG_OK("[ESP8266] POST accepted by server"); return ESP8266_SUCCESS; } } } if (strstr(response, "HTTP/1.1 401") != NULL || strstr(response, "HTTP/1.1 400") != NULL || strstr(response, "HTTP/1.1 413") != NULL || strstr(response, "HTTP/1.1 500") != NULL || strstr(response, "\"success\":false") != NULL) { DEBUG_PRINT("[ESP8266] POST rejected: %s\r\n", response); return ESP8266_FAIL; } Delay_ms(10); } DEBUG_PRINT("[ESP8266] POST response timeout: %s\r\n", Serial_GetRxBuffer()); return ESP8266_FAIL; } uint8_t ESP8266_GetNetworkTime(ESP8266_DateTime* datetime) { char response_buffer[256]; char* epoch_ptr; uint32_t epoch_seconds = 0; uint8_t digit_count = 0; DEBUG_INFO("[ESP8266] Getting network time..."); // 优先直接请求业务服务器时间接口 // 期望响应: {"epoch":1700000000,"server_time":"2026-02-12 12:34:56"} if (ESP8266_HttpGet("/api/v1/system/time", response_buffer, sizeof(response_buffer)) != ESP8266_SUCCESS) { DEBUG_ERROR("[ESP8266] /api/v1/system/time request failed, try reconnect..."); if (ESP8266_ConnectHTTPServer(HTTP_SERVER_IP, HTTP_SERVER_PORT) != ESP8266_SUCCESS) { DEBUG_ERROR("[ESP8266] Reconnect failed when getting time"); return ESP8266_FAIL; } g_server_connected = 1; if (ESP8266_HttpGet("/api/v1/system/time", response_buffer, sizeof(response_buffer)) != ESP8266_SUCCESS) { DEBUG_ERROR("[ESP8266] /api/v1/system/time request failed after reconnect"); return ESP8266_FAIL; } } epoch_ptr = strstr(response_buffer, "\"epoch\":"); if (epoch_ptr == NULL) { DEBUG_ERROR("[ESP8266] /api/v1/system/time response missing epoch"); DEBUG_PRINT("[ESP8266] Response: %s\r\n", response_buffer); return ESP8266_FAIL; } epoch_ptr += 8; // 跳过 "epoch": while (*epoch_ptr == ' ' || *epoch_ptr == '\"') epoch_ptr++; while (*epoch_ptr >= '0' && *epoch_ptr <= '9' && digit_count < 10) { epoch_seconds = epoch_seconds * 10U + (uint32_t)(*epoch_ptr - '0'); epoch_ptr++; digit_count++; } if (digit_count < 9) { DEBUG_ERROR("[ESP8266] Invalid epoch value in /api/v1/system/time response"); DEBUG_PRINT("[ESP8266] Response: %s\r\n", response_buffer); return ESP8266_FAIL; } EpochToDateTimeUTC8(epoch_seconds, datetime); DEBUG_PRINT("[ESP8266] Time(from server): %04d-%02d-%02d %02d:%02d:%02d\r\n", datetime->year, datetime->month, datetime->day, datetime->hour, datetime->minute, datetime->second); return ESP8266_SUCCESS; } uint8_t ESP8266_CheckConnection(void) { char* response; Serial_ClearRxBuffer(); ESP8266_SendCmd("AT+CIPSTATUS"); Delay_ms(500); response = Serial_GetRxBuffer(); if (strstr(response, "STATUS:3") != NULL) { DEBUG_INFO("[ESP8266] Connection OK (STATUS:3)"); return ESP8266_SUCCESS; } if (strstr(response, "STATUS:4") != NULL) { DEBUG_ERROR("[ESP8266] Connection closed (STATUS:4)"); return ESP8266_FAIL; } if (strstr(response, "STATUS:5") != NULL) { DEBUG_ERROR("[ESP8266] WiFi disconnected (STATUS:5)"); return ESP8266_FAIL; } if (strstr(response, "STATUS:2") != NULL) { DEBUG_ERROR("[ESP8266] Not connected to server (STATUS:2)"); return ESP8266_FAIL; } DEBUG_ERROR("[ESP8266] Unknown connection status"); return ESP8266_FAIL; } /** * @brief 发送HTTP GET请求并接收响应 */ uint8_t ESP8266_HttpGet(const char* path, char* response_buffer, uint16_t buffer_size) { char http_request[320]; char cmd_buffer[32]; uint8_t request_sent = 0; DEBUG_PRINT("[ESP8266] HTTP GET: %s\r\n", path); if (response_buffer != NULL && buffer_size > 0) { response_buffer[0] = '\0'; } if (ESP8266_CheckConnection() != ESP8266_SUCCESS) { DEBUG_INFO("[ESP8266] HTTP connection not ready, reconnecting..."); if (ESP8266_ConnectHTTPServer(HTTP_SERVER_IP, HTTP_SERVER_PORT) != ESP8266_SUCCESS) { DEBUG_ERROR("[ESP8266] Reconnect failed before GET"); return ESP8266_FAIL; } g_server_connected = 1; } // 构建HTTP GET请求 sprintf(http_request, "GET %s HTTP/1.1\r\n" "Host: %s:%s\r\n" "X-Api-Token: %s\r\n" "X-Encrypted-Response: true\r\n" "Connection: close\r\n" "\r\n", path, HTTP_SERVER_IP, HTTP_SERVER_PORT, HTTP_API_TOKEN ); uint32_t request_len = strlen(http_request); // 发送数据长度命令 sprintf(cmd_buffer, "AT+CIPSEND=%u", request_len); Serial_ClearRxBuffer(); ESP8266_SendCmd(cmd_buffer); // 等待 ">" 提示符 uint32_t start_time = Get_Time_ms(); while(Get_Time_ms() - start_time < 2000) { if (strstr(Serial_GetRxBuffer(), ">") != NULL) { DEBUG_INFO("[ESP8266] Ready to send GET request"); // 发送HTTP GET请求 Serial_SendString(http_request); request_sent = 1; break; } Delay_ms(10); } if (!request_sent) { DEBUG_ERROR("[ESP8266] No '>' prompt for GET request"); return ESP8266_FAIL; } // 等待接收完整响应(最多8秒) start_time = Get_Time_ms(); uint8_t found_header = 0; uint32_t content_length = 0; while(Get_Time_ms() - start_time < 8000) { char* response = Serial_GetRxBuffer(); // 1. 处理+IPD前缀(ESP8266格式:+IPD,长度:数据) char* ipd_marker = strstr(response, "+IPD,"); if (ipd_marker != NULL) { // 跳过 "+IPD," 找到实际数据 char* data_start = strchr(ipd_marker, ':'); if (data_start != NULL) { data_start++; // 跳过 ':' response = data_start; // 从这里开始才是HTTP响应 } } // 2. 查找HTTP响应体(JSON数据在\r\n\r\n之后) char* body_start = strstr(response, "\r\n\r\n"); if (body_start != NULL) { body_start += 4; // 跳过 \r\n\r\n // 3. 如果还没提取Content-Length,尝试提取 if (!found_header) { char* cl_str = strstr(response, "Content-Length:"); if (cl_str != NULL) { cl_str += 15; // 跳过 "Content-Length:" while (*cl_str == ' ') cl_str++; content_length = atoi(cl_str); DEBUG_PRINT("[ESP8266] Content-Length: %u\r\n", content_length); found_header = 1; } } // 4. 检查是否接收完整(比较实际长度和Content-Length) uint16_t body_len = strlen(body_start); // 移除末尾的 "CLOSED" "OK" 等ESP8266附加信息 char* closed_marker = strstr(body_start, "\r\nCLOSED"); if (closed_marker != NULL) { *closed_marker = '\0'; body_len = strlen(body_start); } // 同时处理没有\r\n的情况 closed_marker = strstr(body_start, "CLOSED"); if (closed_marker != NULL) { *closed_marker = '\0'; body_len = strlen(body_start); } // 移除末尾的OK char* ok_marker = strstr(body_start, "\r\nOK"); if (ok_marker != NULL) { *ok_marker = '\0'; body_len = strlen(body_start); } // 5. 如果服务端给了Content-Length,必须收完整;否则才使用超时兜底 if ((found_header && body_len >= content_length) || (!found_header && Get_Time_ms() - start_time > 3000 && body_len > 0)) { if (found_header && body_len > content_length) { body_len = (uint16_t)content_length; } if (ESP8266_CopyDecryptedBody(response, body_start, body_len, response_buffer, buffer_size) != ESP8266_SUCCESS) { return ESP8266_FAIL; } DEBUG_PRINT("[ESP8266] Received response (%d bytes)\r\n", strlen(response_buffer)); DEBUG_PRINT("[ESP8266] Response: %s\r\n", response_buffer); return ESP8266_SUCCESS; } } Delay_ms(100); } DEBUG_ERROR("[ESP8266] No valid HTTP response or timeout"); return ESP8266_FAIL; } /** * @brief 查询学生在指定时段是否已签到(服务器端查重) * @param student_id 学号 * @param slot_start 时段开始时间(格式"HH:MM") * @param slot_end 时段结束时间(格式"HH:MM") * @retval 1 表示已签到(重复), 0 表示未签到 */ uint8_t ESP8266_CheckDuplicate(const char* student_id, const char* slot_start, const char* slot_end) { char path[128]; char response_buffer[256]; // 构建查询路径 sprintf(path, "/api/v1/attendance/duplicates?student_id=%s&slot_start=%s&slot_end=%s", student_id, slot_start, slot_end); DEBUG_PRINT("[ESP8266] Checking duplicate: %s\r\n", path); // 先检查连接状态,如果断开则重连 if (ESP8266_CheckConnection() != ESP8266_SUCCESS) { DEBUG_INFO("[ESP8266] Connection lost, reconnecting..."); extern const char* HTTP_SERVER_IP; extern const char* HTTP_SERVER_PORT; if (ESP8266_ConnectHTTPServer(HTTP_SERVER_IP, HTTP_SERVER_PORT) != ESP8266_SUCCESS) { DEBUG_ERROR("[ESP8266] Reconnect failed"); return 0; // 连接失败,默认允许签到 } Delay_ms(200); // 等待连接稳定 } // 发送HTTP GET请求 if (ESP8266_HttpGet(path, response_buffer, sizeof(response_buffer)) == ESP8266_SUCCESS) { // 解析响应,查找 "duplicate":true if (strstr(response_buffer, "\"duplicate\":true") != NULL) { DEBUG_INFO("[ESP8266] Duplicate found on server!"); return 1; // 已签到 } else { DEBUG_INFO("[ESP8266] No duplicate found"); return 0; // 未签到 } } DEBUG_ERROR("[ESP8266] Failed to check duplicate, assuming not duplicate"); return 0; // 查询失败时默认为未签到,允许本地签到 } /** * @brief 下载今日已签到学生列表(开机时调用) * @param slot_start 时段开始时间(格式"HH:MM") * @param slot_end 时段结束时间(格式"HH:MM") * @param student_ids 输出:学号数组(逗号分隔的字符串) * @param buffer_size 缓冲区大小 * @retval 成功下载的学号数量 */ uint8_t ESP8266_DownloadTodayRecords(const char* slot_start, const char* slot_end, char* student_ids, uint16_t buffer_size) { char path[128]; char response_buffer[512]; // 构建查询路径 sprintf(path, "/api/v1/attendance/today?slot_start=%s&slot_end=%s", slot_start, slot_end); DEBUG_PRINT("[ESP8266] Downloading today's records: %s\r\n", path); // 先检查连接状态,如果断开则重连 if (ESP8266_CheckConnection() != ESP8266_SUCCESS) { DEBUG_INFO("[ESP8266] Connection lost, reconnecting before download..."); extern const char* HTTP_SERVER_IP; extern const char* HTTP_SERVER_PORT; if (ESP8266_ConnectHTTPServer(HTTP_SERVER_IP, HTTP_SERVER_PORT) != ESP8266_SUCCESS) { DEBUG_ERROR("[ESP8266] Reconnect failed"); return 0; } Delay_ms(300); // 等待连接稳定 } // 发送HTTP GET请求 if (ESP8266_HttpGet(path, response_buffer, sizeof(response_buffer)) != ESP8266_SUCCESS) { DEBUG_ERROR("[ESP8266] Failed to download today's records"); return 0; } // 解析响应,提取学号列表 // 格式: {"date":"2026-02-07","records":["1","2","3"]} char* records_start = strstr(response_buffer, "\"records\":["); if (records_start == NULL) { DEBUG_ERROR("[ESP8266] Invalid response format"); return 0; } records_start += 11; // 跳过 "records":[ char* records_end = strchr(records_start, ']'); if (records_end == NULL) { DEBUG_ERROR("[ESP8266] Invalid response format"); return 0; } // 复制学号列表到输出缓冲区 uint16_t len = records_end - records_start; if (len >= buffer_size) len = buffer_size - 1; strncpy(student_ids, records_start, len); student_ids[len] = '\0'; // 统计学号数量(空列表返回0) uint8_t count = 0; if (len > 2) { // 至少有一个学号 "x" count = 1; for (uint16_t i = 0; i < len; i++) { if (student_ids[i] == ',') count++; } } DEBUG_PRINT("[ESP8266] Downloaded %d records: %s\r\n", count, student_ids); return count; }