#include "Attendance.h" #include "Delay.h" #include "Debug.h" #include "esp8266.h" // ✨ 添加ESP8266头文件 #include #include // ← 添加atoi函数声明 extern uint8_t g_server_connected; // 配置参数 #define MAX_ATTENDANCE_RECORDS 100 // 最大记录数量 #define MIN_CONFIDENCE_THRESHOLD 0.75 // 最小置信度阈值 #define MAX_TIME_SLOT_COUNT 10 // 最大时间段数量 #define MAX_CACHED_IDS 50 // 最大缓存学号数量(从服务器下载) // 时间段配置(动态从服务器更新) static TimeSlot time_slots[MAX_TIME_SLOT_COUNT]; static uint8_t time_slot_count = 0; // 当前有效时间段数量 // 今日已签到学号缓存(从服务器下载) static char cached_student_ids[MAX_CACHED_IDS][20]; static int8_t cached_time_slots[MAX_CACHED_IDS]; static uint8_t cached_id_count = 0; static int8_t active_cache_slot = -1; // 默认时间段配置(在服务器配置下载失败时使用) static const TimeSlot default_time_slots[] = { {1, 8, 0, 9, 30, "Morning Check-in"}, // 上午签到 08:00-09:30 {1, 12, 0, 13, 0, "Noon Check-out"}, // 中午签退 12:00-13:00 {1, 14, 0, 15, 0, "Afternoon Check-in"}, // 下午签到 14:00-15:00 {1, 17, 0, 18, 30, "Evening Check-out"}, // 晚上签退 17:00-18:30 }; #define DEFAULT_TIME_SLOT_COUNT 4 // 当前时间(由外部更新,如果有RTC则从RTC读取) static uint8_t current_hour = 8; static uint8_t current_minute = 30; // 内部存储 static AttendanceRecord attendance_records[MAX_ATTENDANCE_RECORDS]; static uint16_t record_count = 0; static uint32_t total_attendance_count = 0; // 总考勤人次 static int8_t Attendance_FindTimeSlot(uint8_t hour, uint8_t minute) { uint16_t current_minutes = hour * 60 + minute; for (uint8_t i = 0; i < time_slot_count; i++) { if (!time_slots[i].enabled) { continue; } uint16_t start_minutes = time_slots[i].start_hour * 60 + time_slots[i].start_minute; uint16_t end_minutes = time_slots[i].end_hour * 60 + time_slots[i].end_minute; if (current_minutes >= start_minutes && current_minutes <= end_minutes) { return i; } } return -1; } /** * @brief 初始化考勤模块 */ void Attendance_Init(void) { record_count = 0; total_attendance_count = 0; current_hour = 8; current_minute = 0; memset(attendance_records, 0, sizeof(attendance_records)); // 初始化为默认时间段配置 memcpy(time_slots, default_time_slots, sizeof(default_time_slots)); time_slot_count = DEFAULT_TIME_SLOT_COUNT; // 清空缓存 cached_id_count = 0; memset(cached_student_ids, 0, sizeof(cached_student_ids)); memset(cached_time_slots, 0xFF, sizeof(cached_time_slots)); active_cache_slot = -1; DEBUG_INFO("[Attendance] Initialized with time slot mode"); } /** * @brief 清空今日签到缓存 */ void Attendance_ClearCache(void) { cached_id_count = 0; memset(cached_student_ids, 0, sizeof(cached_student_ids)); memset(cached_time_slots, 0xFF, sizeof(cached_time_slots)); active_cache_slot = Attendance_GetCurrentTimeSlot(); DEBUG_INFO("[Attendance] Cache cleared"); } /** * @brief 添加学号到缓存 */ void Attendance_AddToCache(const char* student_id) { int8_t current_slot = Attendance_GetCurrentTimeSlot(); if (current_slot < 0) { DEBUG_INFO("[Attendance] Not in any slot, skip cache add"); return; } if (cached_id_count >= MAX_CACHED_IDS) { DEBUG_ERROR("[Attendance] Cache full, cannot add more IDs"); return; } if (active_cache_slot != current_slot) { Attendance_ClearCache(); active_cache_slot = current_slot; } // 检查是否已存在 for (uint8_t i = 0; i < cached_id_count; i++) { if (cached_time_slots[i] == current_slot && strcmp(cached_student_ids[i], student_id) == 0) { return; // 已存在,不重复添加 } } strncpy(cached_student_ids[cached_id_count], student_id, 19); cached_student_ids[cached_id_count][19] = '\0'; cached_time_slots[cached_id_count] = current_slot; cached_id_count++; DEBUG_PRINT("[Attendance] Added to cache: %s slot=%d (total: %d)\r\n", student_id, current_slot, cached_id_count); } /** * @brief 在本地缓存中查重 * @retval 1=在缓存中找到(重复), 0=未找到 */ uint8_t Attendance_IsDuplicateInCache(const char* student_id) { int8_t current_slot = Attendance_GetCurrentTimeSlot(); if (current_slot < 0) { return 0; } if (active_cache_slot != current_slot) { Attendance_ClearCache(); active_cache_slot = current_slot; return 0; } for (uint8_t i = 0; i < cached_id_count; i++) { if (cached_time_slots[i] == current_slot && strcmp(cached_student_ids[i], student_id) == 0) { DEBUG_PRINT("[Attendance] Found in cache: %s slot=%d\r\n", student_id, current_slot); return 1; } } return 0; } /** * @brief 从服务器加载今日已签到记录到缓存 * @retval 加载的记录数量 */ uint8_t Attendance_LoadTodayRecordsFromServer(void) { char slot_start[8]; char slot_end[8]; char records_buffer[512]; int8_t current_slot = Attendance_GetCurrentTimeSlot(); if (current_slot < 0) { Attendance_ClearCache(); DEBUG_INFO("[Attendance] Not in time slot, skip loading all-day cache"); return 0; } // 获取当前时段时间 Attendance_GetCurrentSlotTime(slot_start, slot_end); DEBUG_PRINT("[Attendance] Loading today's records for slot %s-%s\r\n", slot_start, slot_end); // 清空旧缓存 Attendance_ClearCache(); active_cache_slot = current_slot; // 从服务器下载 uint8_t count = ESP8266_DownloadTodayRecords(slot_start, slot_end, records_buffer, sizeof(records_buffer)); if (count == 0) { DEBUG_INFO("[Attendance] No records to load or download failed"); return 0; } // 解析学号列表 (格式: "1","2","3") char* ptr = records_buffer; char student_id[20]; uint8_t loaded = 0; while (*ptr != '\0' && loaded < MAX_CACHED_IDS) { // 跳过引号和逗号 while (*ptr == '"' || *ptr == ',' || *ptr == ' ') ptr++; if (*ptr == '\0') break; // 提取学号 char* id_start = ptr; while (*ptr != '"' && *ptr != ',' && *ptr != '\0') ptr++; uint8_t len = ptr - id_start; if (len > 0 && len < 20) { strncpy(student_id, id_start, len); student_id[len] = '\0'; Attendance_AddToCache(student_id); loaded++; } } DEBUG_PRINT("[Attendance] Loaded %d records from server\r\n", loaded); return loaded; } /** * @brief 更新时间段配置(从服务器下载后调用) * @param slots 新的时间段配置数组 * @param count 时间段数量 */ void Attendance_UpdateTimeSlots(TimeSlot* slots, uint8_t count) { if (count > MAX_TIME_SLOT_COUNT) { DEBUG_PRINT("[Attendance] Too many time slots: %d (max: %d)\r\n", count, MAX_TIME_SLOT_COUNT); return; } // 更新时间段配置 memcpy(time_slots, slots, count * sizeof(TimeSlot)); time_slot_count = count; DEBUG_PRINT("[Attendance] Time slots updated: %d slots loaded\r\n", count); // 调试:打印所有时间段 for (uint8_t i = 0; i < time_slot_count; i++) { DEBUG_PRINT("[Attendance] Slot %d: %s (%02d:%02d-%02d:%02d) %s\r\n", i, time_slots[i].name, time_slots[i].start_hour, time_slots[i].start_minute, time_slots[i].end_hour, time_slots[i].end_minute, time_slots[i].enabled ? "ENABLED" : "DISABLED"); } } /** * @brief 从JSON字符串解析时间段配置 * @param json_str JSON字符串(格式:{"time_slots":[{"enabled":true,"start_time":"08:00",...}]}) * @retval 成功解析的时间段数量 */ uint8_t Attendance_ParseTimeSlotsJSON(const char* json_str) { TimeSlot temp_slots[MAX_TIME_SLOT_COUNT]; uint8_t slot_count = 0; DEBUG_INFO("[Attendance] Parsing time slots JSON..."); DEBUG_PRINT("[Attendance] JSON: %s\r\n", json_str); // 查找 "time_slots" 数组 const char* ptr = strstr(json_str, "\"time_slots\""); if (ptr == NULL) { DEBUG_ERROR("[Attendance] Invalid JSON: no 'time_slots' field found"); return 0; } // 查找数组开始 '[' ptr = strchr(ptr, '['); if (ptr == NULL) { DEBUG_ERROR("[Attendance] Invalid JSON: no '[' found"); return 0; } ptr++; // 跳过 '[' while (*ptr == ' ' || *ptr == '\n' || *ptr == '\r' || *ptr == '\t') ptr++; if (*ptr == ']') { DEBUG_INFO("[Attendance] Empty time slot config, keeping current slots"); return time_slot_count; } // 解析每个时间段对象 while (*ptr != '\0' && *ptr != ']' && slot_count < MAX_TIME_SLOT_COUNT) { // 跳过空格、换行、逗号 while (*ptr == ' ' || *ptr == ',' || *ptr == '\n' || *ptr == '\r' || *ptr == '\t') ptr++; // 查找对象开始 if (*ptr != '{') break; ptr++; // 跳过 '{' // 初始化当前时间段 temp_slots[slot_count].enabled = 1; temp_slots[slot_count].start_hour = 0; temp_slots[slot_count].start_minute = 0; temp_slots[slot_count].end_hour = 0; temp_slots[slot_count].end_minute = 0; strcpy(temp_slots[slot_count].name, "Unknown"); // 解析对象字段 while (*ptr != '\0' && *ptr != '}') { // 跳过空格、换行、逗号 while (*ptr == ' ' || *ptr == ',' || *ptr == '\n' || *ptr == '\r' || *ptr == '\t') ptr++; // 跳过引号 if (*ptr == '"') ptr++; // 解析字段名 if (strncmp(ptr, "enabled", 7) == 0) { ptr = strchr(ptr, ':'); if (ptr) { ptr++; while (*ptr == ' ') ptr++; // 处理 true/false 或 1/0 temp_slots[slot_count].enabled = (*ptr == 't' || *ptr == '1') ? 1 : 0; } } else if (strncmp(ptr, "start_time", 10) == 0) { // 格式: "start_time": "08:30" ptr = strchr(ptr, ':'); if (ptr) { ptr++; while (*ptr == ' ' || *ptr == '"') ptr++; // 解析 "HH:MM" temp_slots[slot_count].start_hour = (ptr[0] - '0') * 10 + (ptr[1] - '0'); temp_slots[slot_count].start_minute = (ptr[3] - '0') * 10 + (ptr[4] - '0'); } } else if (strncmp(ptr, "end_time", 8) == 0) { // 格式: "end_time": "09:30" ptr = strchr(ptr, ':'); if (ptr) { ptr++; while (*ptr == ' ' || *ptr == '"') ptr++; // 解析 "HH:MM" temp_slots[slot_count].end_hour = (ptr[0] - '0') * 10 + (ptr[1] - '0'); temp_slots[slot_count].end_minute = (ptr[3] - '0') * 10 + (ptr[4] - '0'); } } else if (strncmp(ptr, "name", 4) == 0) { ptr = strchr(ptr, ':'); if (ptr) { ptr++; while (*ptr == ' ' || *ptr == '"') ptr++; // 提取名称(直到遇到引号) const char* name_start = ptr; while (*ptr != '"' && *ptr != '\0') ptr++; int len = ptr - name_start; if (len > 19) len = 19; strncpy(temp_slots[slot_count].name, name_start, len); temp_slots[slot_count].name[len] = '\0'; } } // 移动到下一个字段或对象结束 while (*ptr != '\0' && *ptr != ',' && *ptr != '}') ptr++; if (*ptr == ',') ptr++; } if (*ptr == '}') ptr++; // 跳过 '}' DEBUG_PRINT("[Attendance] Parsed slot %d: %s (%02d:%02d-%02d:%02d) %s\r\n", slot_count, temp_slots[slot_count].name, temp_slots[slot_count].start_hour, temp_slots[slot_count].start_minute, temp_slots[slot_count].end_hour, temp_slots[slot_count].end_minute, temp_slots[slot_count].enabled ? "ENABLED" : "DISABLED"); slot_count++; } DEBUG_PRINT("[Attendance] Parsed %d time slots\r\n", slot_count); // 应用解析结果 if (slot_count > 0) { Attendance_UpdateTimeSlots(temp_slots, slot_count); return slot_count; } DEBUG_ERROR("[Attendance] Failed to parse any time slots"); return 0; } /** * @brief 设置当前时间(由外部定期调用更新) * @param hour 小时 (0-23) * @param minute 分钟 (0-59) */ void Attendance_SetCurrentTime(uint8_t hour, uint8_t minute) { int8_t old_slot = Attendance_FindTimeSlot(current_hour, current_minute); int8_t new_slot = Attendance_FindTimeSlot(hour, minute); current_hour = hour; current_minute = minute; if (old_slot != new_slot) { Attendance_ClearCache(); active_cache_slot = new_slot; DEBUG_PRINT("[Attendance] Slot changed: %d -> %d, cache reset\r\n", old_slot, new_slot); } } /** * @brief 获取当前所处的考勤时段 * @retval 时段编号 (0-N),如果不在任何时段内则返回 -1 */ int8_t Attendance_GetCurrentTimeSlot(void) { return Attendance_FindTimeSlot(current_hour, current_minute); } /** * @brief 获取当前时段的开始和结束时间 * @param start_time 输出:开始时间(格式"HH:MM") * @param end_time 输出:结束时间(格式"HH:MM") */ void Attendance_GetCurrentSlotTime(char* start_time, char* end_time) { int8_t current_slot = Attendance_GetCurrentTimeSlot(); if (current_slot >= 0 && current_slot < time_slot_count) { sprintf(start_time, "%02d:%02d", time_slots[current_slot].start_hour, time_slots[current_slot].start_minute); sprintf(end_time, "%02d:%02d", time_slots[current_slot].end_hour, time_slots[current_slot].end_minute); } else { // 不在任何时段,返回全天 strcpy(start_time, "00:00"); strcpy(end_time, "23:59"); } } /** * @brief 检查学生是否在服务器端已签到(服务器端查重) * @param student_id 学生学号 * @retval 1=服务器显示已签到(重复), 0=未签到或查询失败 */ uint8_t Attendance_IsDuplicateOnServer(const char* student_id) { char slot_start[8]; char slot_end[8]; // 获取当前时段时间 Attendance_GetCurrentSlotTime(slot_start, slot_end); DEBUG_PRINT("[Attendance] Checking server for duplicate: %s (%s-%s)\r\n", student_id, slot_start, slot_end); // 调用ESP8266查询服务器(这个过程可能耗时,蜂鸣器状态由调用者控制) return ESP8266_CheckDuplicate(student_id, slot_start, slot_end); } /** * @brief 检查是否在当前时段重复打卡(本地+服务器) * @param student_id 学生学号 * @retval 1=重复, 0=不重复 */ uint8_t Attendance_IsDuplicate(const char* student_id) { int8_t current_slot = Attendance_GetCurrentTimeSlot(); if (current_slot < 0) { return 0; // 不在考勤时段,不算重复 } // 遍历现有记录,检查是否在当前时段已打卡 for (uint16_t i = 0; i < record_count; i++) { // 如果找到相同学号且在相同时段 if (strcmp(attendance_records[i].student_id, student_id) == 0 && attendance_records[i].time_slot == current_slot) { return 1; // 本时段已打卡,重复 } } return 0; // 不重复 } /** * @brief 处理人脸识别结果并记录考勤 * @param result 人脸识别结果 * @retval 考勤状态 */ AttendanceStatus Attendance_ProcessFaceResult(FaceRecognitionResult* result) { // 1. 检查识别是否成功 if (result->success == 0) { return ATTENDANCE_UNKNOWN; } // 2. 检查置信度 if (result->confidence < MIN_CONFIDENCE_THRESHOLD) { return ATTENDANCE_LOW_CONFIDENCE; } // 3. 检查是否在考勤时段内 int8_t current_slot = Attendance_GetCurrentTimeSlot(); if (current_slot < 0) { DEBUG_PRINT("[Attendance] Not in time slot (Current: %02d:%02d)\r\n", current_hour, current_minute); return ATTENDANCE_OUT_OF_TIME; } DEBUG_PRINT("[Attendance] Current slot: %s (%02d:%02d)\r\n", time_slots[current_slot].name, current_hour, current_minute); // 4. 检查是否在当前时段重复打卡(本地记录检查) if (Attendance_IsDuplicate(result->student_id)) { DEBUG_PRINT("[Attendance] Local duplicate in slot: %s\r\n", time_slots[current_slot].name); return ATTENDANCE_DUPLICATE; } // 5. 检查缓存中是否已签到(从服务器下载的今日记录) if (Attendance_IsDuplicateInCache(result->student_id)) { DEBUG_PRINT("[Attendance] Cache duplicate in slot: %s\r\n", time_slots[current_slot].name); return ATTENDANCE_DUPLICATE; } // 6. 服务器在线时补一次服务端查重,避免多设备或缓存过期造成重复签到 if (g_server_connected && Attendance_IsDuplicateOnServer(result->student_id)) { DEBUG_PRINT("[Attendance] Server duplicate in slot: %s\r\n", time_slots[current_slot].name); Attendance_AddToCache(result->student_id); return ATTENDANCE_DUPLICATE; } // 7. 添加新记录 if (record_count < MAX_ATTENDANCE_RECORDS) { // 复制学号和姓名 strncpy(attendance_records[record_count].student_id, result->student_id, 19); attendance_records[record_count].student_id[19] = '\0'; strncpy(attendance_records[record_count].student_name, result->student_name, 31); attendance_records[record_count].student_name[31] = '\0'; // 记录置信度 attendance_records[record_count].confidence = result->confidence; // 记录时间戳和时段 // 简化:使用系统运行时间戳(服务器会用接收时间替换) attendance_records[record_count].timestamp = Get_Time_ms() / 1000; attendance_records[record_count].time_slot = current_slot; // 初始化同步状态 attendance_records[record_count].sync_status = SYNC_PENDING; attendance_records[record_count].retry_count = 0; attendance_records[record_count].last_retry_time = 0; record_count++; total_attendance_count++; // 添加到缓存,防止重复签到 Attendance_AddToCache(result->student_id); DEBUG_PRINT("[Attendance] Success! Slot=%s, Total=%d, Status=PENDING\r\n", time_slots[current_slot].name, total_attendance_count); return ATTENDANCE_SUCCESS; } else { // 记录已满,覆盖最早的记录 memmove(&attendance_records[0], &attendance_records[1], sizeof(AttendanceRecord) * (MAX_ATTENDANCE_RECORDS - 1)); record_count = MAX_ATTENDANCE_RECORDS - 1; // 在最后位置添加新记录 strncpy(attendance_records[record_count].student_id, result->student_id, 19); attendance_records[record_count].student_id[19] = '\0'; strncpy(attendance_records[record_count].student_name, result->student_name, 31); attendance_records[record_count].student_name[31] = '\0'; attendance_records[record_count].confidence = result->confidence; // 简化:使用系统运行时间戳(服务器会用接收时间替换) attendance_records[record_count].timestamp = Get_Time_ms() / 1000; attendance_records[record_count].time_slot = current_slot; attendance_records[record_count].sync_status = SYNC_PENDING; attendance_records[record_count].retry_count = 0; attendance_records[record_count].last_retry_time = 0; record_count++; total_attendance_count++; return ATTENDANCE_SUCCESS; } } /** * @brief 清空所有考勤记录 */ void Attendance_ClearRecords(void) { record_count = 0; memset(attendance_records, 0, sizeof(attendance_records)); // 注意:不清空 total_attendance_count,保留历史统计 } /** * @brief 获取累计考勤总数 */ uint16_t Attendance_GetTotalCount(void) { return total_attendance_count; } /** * @brief 获取当前缓存中的记录数 */ uint16_t Attendance_GetActiveCount(void) { return record_count; } /** * @brief 获取待同步记录的数量(PENDING或FAILED状态) * @retval 待同步记录数 */ uint16_t Attendance_GetPendingCount(void) { uint16_t count = 0; for (uint16_t i = 0; i < record_count; i++) { if (attendance_records[i].sync_status == SYNC_PENDING || attendance_records[i].sync_status == SYNC_FAILED) { count++; } } return count; } /** * @brief 获取下一个待同步的记录(PENDING或FAILED状态) * @param index 输出参数,返回记录的索引 * @retval 记录指针,如果没有待同步记录则返回NULL */ AttendanceRecord* Attendance_GetNextPendingRecord(uint16_t* index) { uint32_t current_time = Get_Time_ms(); // 优先处理PENDING状态的记录 for (uint16_t i = 0; i < record_count; i++) { if (attendance_records[i].sync_status == SYNC_PENDING) { if (index != NULL) { *index = i; } return &attendance_records[i]; } } // 然后处理FAILED状态的记录(需要等待重试间隔) for (uint16_t i = 0; i < record_count; i++) { if (attendance_records[i].sync_status == SYNC_FAILED) { // 指数退避算法:重试间隔 = 2^retry_count * 1000ms (最大30秒) uint32_t retry_interval = (1 << attendance_records[i].retry_count) * 1000; if (retry_interval > 30000) retry_interval = 30000; // 检查是否到达重试时间 if (current_time - attendance_records[i].last_retry_time >= retry_interval) { if (index != NULL) { *index = i; } return &attendance_records[i]; } } } return NULL; // 没有待同步记录 } /** * @brief 更新记录的同步状态 * @param index 记录索引 * @param status 新的同步状态 */ void Attendance_MarkSyncStatus(uint16_t index, SyncStatus status) { if (index < record_count) { attendance_records[index].sync_status = status; if (status == SYNC_FAILED || status == SYNC_SYNCING) { attendance_records[index].last_retry_time = Get_Time_ms(); } const char* status_str[] = {"PENDING", "SYNCING", "SYNCED", "FAILED"}; DEBUG_PRINT("[Attendance] Record %d status: %s (ID=%s)\r\n", index, status_str[status], attendance_records[index].student_id); } } /** * @brief 增加记录的重试计数 * @param index 记录索引 */ void Attendance_IncrementRetry(uint16_t index) { if (index < record_count) { attendance_records[index].retry_count++; DEBUG_PRINT("[Attendance] Record %d retry count: %d\r\n", index, attendance_records[index].retry_count); } } /** * @brief 将所有FAILED记录重置为PENDING(用于网络恢复后) */ void Attendance_ResetFailedRecords(void) { uint16_t reset_count = 0; for (uint16_t i = 0; i < record_count; i++) { if (attendance_records[i].sync_status == SYNC_FAILED) { attendance_records[i].sync_status = SYNC_PENDING; attendance_records[i].retry_count = 0; reset_count++; } } if (reset_count > 0) { DEBUG_PRINT("[Attendance] Reset %d failed records to pending\r\n", reset_count); } } /** * @brief 按索引获取记录 * @param index 记录索引 * @retval 记录指针,如果索引无效则返回NULL */ AttendanceRecord* Attendance_GetRecordByIndex(uint16_t index) { if (index < record_count) { return &attendance_records[index]; } return NULL; }