初始化

This commit is contained in:
2026-05-01 11:13:45 +08:00
commit bb1d10615e
99 changed files with 48378 additions and 0 deletions
+695
View File
@@ -0,0 +1,695 @@
#include "Attendance.h"
#include "Delay.h"
#include "Debug.h"
#include "esp8266.h" // ✨ 添加ESP8266头文件
#include <string.h>
#include <stdlib.h> // ← 添加atoi函数声明
// 配置参数
#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 uint8_t cached_id_count = 0;
// 默认时间段配置(在服务器配置下载失败时使用)
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
};
#define DEFAULT_TIME_SLOT_COUNT 2
// 当前时间(由外部更新,如果有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; // 总考勤人次
/**
* @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));
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));
DEBUG_INFO("[Attendance] Cache cleared");
}
/**
* @brief 添加学号到缓存
*/
void Attendance_AddToCache(const char* student_id)
{
if (cached_id_count >= MAX_CACHED_IDS) {
DEBUG_ERROR("[Attendance] Cache full, cannot add more IDs");
return;
}
// 检查是否已存在
for (uint8_t i = 0; i < cached_id_count; i++) {
if (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_id_count++;
DEBUG_PRINT("[Attendance] Added to cache: %s (total: %d)\r\n", student_id, cached_id_count);
}
/**
* @brief 在本地缓存中查重
* @retval 1=在缓存中找到(重复), 0=未找到
*/
uint8_t Attendance_IsDuplicateInCache(const char* student_id)
{
for (uint8_t i = 0; i < cached_id_count; i++) {
if (strcmp(cached_student_ids[i], student_id) == 0) {
DEBUG_PRINT("[Attendance] Found in cache: %s\r\n", student_id);
return 1;
}
}
return 0;
}
/**
* @brief 从服务器加载今日已签到记录到缓存
* @retval 加载的记录数量
*/
uint8_t Attendance_LoadTodayRecordsFromServer(void)
{
char slot_start[8];
char slot_end[8];
char records_buffer[512];
// 获取当前时段时间
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();
// 从服务器下载
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 != '\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)
{
current_hour = hour;
current_minute = minute;
}
/**
* @brief 获取当前所处的考勤时段
* @retval 时段编号 (0-N),如果不在任何时段内则返回 -1
*/
int8_t Attendance_GetCurrentTimeSlot(void)
{
for (uint8_t i = 0; i < time_slot_count; i++)
{
// 跳过禁用的时间段
if (!time_slots[i].enabled) {
continue;
}
uint16_t current_minutes = current_hour * 60 + current_minute;
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 获取当前时段的开始和结束时间
* @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 (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;
}