# ==================================================================================== # K210 V11 (分时网络通信) # 功能更新: # - 继承V10所有功能 # - 【新功能】使用分时处理网络通信,每帧只做少量网络工作,避免长时间阻塞 # ==================================================================================== import sensor, image, lcd import KPU as kpu import time from Maix import FPIOA, GPIO, utils import gc from fpioa_manager import fm import utime from machine import UART import os try: import ubinascii except: ubinascii = None try: import ucryptolib as crypto_lib except: try: import cryptolib as crypto_lib except: crypto_lib = None # --- 增加堆内存大小 --- # K210有8MB SRAM,默认堆较小,增加到512KB用于图像压缩 utils.gc_heap_size(1024 * 1024) gc.collect() print("[BOOT] Heap size set to 1MB") # --- 配置参数(宏定义)--- SYNC_CHECK_INTERVAL_MS = 50000 # 检查同步请求的时间间隔(毫秒) NETWORK_FRAME_INTERVAL = 300 # 每N帧检查一次网络任务(约1秒) # --- 分时网络任务队列 --- photo_upload_queue = [] # 照片上传队列 [face_id, ...],JPEG先保存在SD卡 network_task_pending = False # 是否有网络任务待处理 frames_since_network = 0 # 距离上次网络操作的帧数 # --- 0. ESP8266 WiFi配置 --- WIFI_SSID = "yzfh" # 修改为你的WiFi名称 WIFI_PASSWORD = "yzfhroot" # 修改为你的WiFi密码 SERVER_IP = "27.106.113.205" # 修改为服务器IP地址 SERVER_PORT = 8082 # 服务器HTTP端口 API_TOKEN = "atk_nKfVWLhMjL0RGSfRll3R_sdJS6IrIjuV" # 必须与服务端 ATTENDANCE_API_TOKEN 一致 ESP_DEBUG = True # 打印ESP8266 AT响应,便于排查WiFi连接问题 ESP_BAUD_RATES = (115200, 9600, 74880, 57600, 38400) ESP_UART_BAUD = 115200 ESP_BOOT_WAIT_MS = 6000 # 等待ESP8266上电启动和自动连网 LOCAL_FACE_PHOTO_DIR = "/sd/face_photos" # 本地人脸照片缓存目录 AES_KEY = b"\x2b\x7e\x15\x16\x28\xae\xd2\xa6\xab\xf7\x15\x88\x09\xcf\x4f\x3c" AES_IV = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" def crypto_ready(): return crypto_lib is not None and ubinascii is not None def pkcs7_pad(data): pad_len = 16 - (len(data) % 16) return data + bytes([pad_len]) * pad_len def pkcs7_unpad(data): if not data: return b"" pad_len = data[-1] if pad_len < 1 or pad_len > 16 or pad_len > len(data): return data for b in data[-pad_len:]: if b != pad_len: return data return data[:-pad_len] def aes_cbc_crypt(data, encrypt=True): if not crypto_ready(): print("[CRYPTO] ucryptolib/ubinascii unavailable, refuse plaintext HTTP") return None try: cipher = crypto_lib.aes(AES_KEY, 2, AES_IV) return cipher.encrypt(data) if encrypt else cipher.decrypt(data) except Exception as e: print("[CRYPTO] AES failed:", e) return None def encrypt_payload_base64(data): if isinstance(data, str): data = data.encode("utf-8") encrypted = aes_cbc_crypt(pkcs7_pad(data), True) if encrypted is None: return None encoded = ubinascii.b2a_base64(encrypted).strip() try: return encoded.decode("utf-8") except: return str(encoded, "utf-8") def decrypt_payload_base64(data): if data is None: return None if isinstance(data, str): data = data.strip().encode("utf-8") try: encrypted = ubinascii.a2b_base64(data) except Exception as e: print("[CRYPTO] Base64 decode failed:", e) return None decrypted = aes_cbc_crypt(encrypted, False) if decrypted is None: return None try: return pkcs7_unpad(decrypted).decode("utf-8") except Exception as e: print("[CRYPTO] UTF-8 decode failed:", e) return None def extract_http_body(resp): if not resp: return "" marker = "\r\n\r\n" start = resp.find(marker) if start == -1: return "" body = resp[start + len(marker):] content_length = -1 cl_pos = resp.find("Content-Length:") if cl_pos != -1: cl_pos += len("Content-Length:") while cl_pos < len(resp) and resp[cl_pos] == " ": cl_pos += 1 cl_end = cl_pos while cl_end < len(resp) and resp[cl_end].isdigit(): cl_end += 1 try: content_length = int(resp[cl_pos:cl_end]) except: content_length = -1 if content_length >= 0 and len(body) >= content_length: return body[:content_length] for marker in ("\r\nCLOSED", "CLOSED", "\r\nOK"): pos = body.find(marker) if pos != -1: body = body[:pos] return body.strip() def http_body_complete(resp): if not resp or "\r\n\r\n" not in resp: return False body = resp.split("\r\n\r\n", 1)[1] cl_pos = resp.find("Content-Length:") if cl_pos == -1: return "CLOSED" in resp cl_pos += len("Content-Length:") while cl_pos < len(resp) and resp[cl_pos] == " ": cl_pos += 1 cl_end = cl_pos while cl_end < len(resp) and resp[cl_end].isdigit(): cl_end += 1 try: return len(body) >= int(resp[cl_pos:cl_end]) except: return "CLOSED" in resp def decode_http_json_body(resp): body = extract_http_body(resp) if not body: return None if "X-Encrypted: AES128-CBC" in resp: return decrypt_payload_base64(body) if body.startswith("{") or body.startswith("["): return body return decrypt_payload_base64(body) # 摄像头安装方向:当前实物画面上下左右都反,水平镜像+垂直翻转等价于旋转180度。 CAMERA_HMIRROR = 1 CAMERA_VFLIP = 1 # --- 1. UART通信初始化 --- # UART1: 连接STM32(原有功能) fm.register(24, fm.fpioa.UART1_TX, force=True) fm.register(25, fm.fpioa.UART1_RX, force=True) uart = UART(UART.UART1, 115200, 8, 0, 1, timeout=1000, read_buf_len=4096) # UART2: 连接新ESP8266(照片上传功能) fm.register(34, fm.fpioa.UART2_TX, force=True) fm.register(35, fm.fpioa.UART2_RX, force=True) uart2 = UART(UART.UART2, 115200, 8, 0, 1, timeout=2000, read_buf_len=4096) # --- ESP8266 WiFi通信函数(优化版 - 减少阻塞)--- def esp_reopen_uart(baud): """按指定波特率重新打开ESP8266串口""" global uart2, ESP_UART_BAUD ESP_UART_BAUD = baud try: uart2.deinit() except: pass uart2 = UART(UART.UART2, baud, 8, 0, 1, timeout=2000, read_buf_len=4096) utime.sleep_ms(100) def esp_clear_rx(): """清空ESP8266串口残留数据""" while uart2.any(): uart2.read() utime.sleep_ms(10) def esp_log_response(cmd, response): if ESP_DEBUG: text = response.replace('\r\n', ' ').strip() if response else "(空)" print("[ESP] {} => {}".format(cmd, text[:180])) def esp_send_cmd(cmd, timeout=1000, expected=None): """发送AT命令到ESP8266并等待响应""" esp_clear_rx() uart2.write(cmd + "\r\n") utime.sleep_ms(50) start = utime.ticks_ms() response = "" if expected is None: expected = ("OK", "ERROR", "FAIL", "ready") while utime.ticks_diff(utime.ticks_ms(), start) < timeout: if uart2.any(): data = uart2.read() if data: response += data.decode('utf-8', 'ignore') if any(token in response for token in expected): break utime.sleep_ms(5) esp_log_response(cmd, response) return response def esp_probe_uart(): """探测ESP8266实际AT波特率""" print("[ESP] Probing UART baud rate...") for baud in ESP_BAUD_RATES: print("[ESP] Try baud:", baud) esp_reopen_uart(baud) for _ in range(2): resp = esp_send_cmd("AT", 1500) if "OK" in resp: print("[ESP] UART baud detected:", baud) return True utime.sleep_ms(300) esp_reopen_uart(115200) print("[ESP] No AT response on tested baud rates") return False def esp_wait_boot(): """等待ESP8266启动完成,避免K210太早发送AT命令""" print("[ESP] Waiting for module boot...") start = utime.ticks_ms() boot_log = "" while utime.ticks_diff(utime.ticks_ms(), start) < ESP_BOOT_WAIT_MS: if uart2.any(): data = uart2.read() if data: boot_log += data.decode('utf-8', 'ignore') utime.sleep_ms(100) if ESP_DEBUG and boot_log: print("[ESP] Boot log:", boot_log.replace('\r\n', ' ')[:180]) esp_clear_rx() def esp_check_wifi_status(): """检查WiFi连接状态(快速检查)""" resp = esp_send_cmd("AT+CWJAP?", 1500) return WIFI_SSID in resp def esp_connect_wifi(): """连接WiFi(单次尝试)""" print("[ESP] Connecting to WiFi:", WIFI_SSID) resp = esp_send_cmd("ATE0", 1000) resp = esp_send_cmd("AT+CWMODE_CUR=1", 1500) if "OK" not in resp: resp = esp_send_cmd("AT+CWMODE=1", 1500) if "OK" not in resp and "no change" not in resp: print("[ESP] Set mode failed") return False if esp_check_wifi_status(): print("[ESP] WiFi already connected!") return True # 连接WiFi。部分路由器获取IP较慢,等待时间不能太短。 cmd = 'AT+CWJAP_CUR="{}","{}"'.format(WIFI_SSID, WIFI_PASSWORD) resp = esp_send_cmd(cmd, 20000, ("WIFI GOT IP", "OK", "FAIL", "ERROR")) if "OK" not in resp and "CONNECTED" not in resp and "WIFI GOT IP" not in resp: cmd = 'AT+CWJAP="{}","{}"'.format(WIFI_SSID, WIFI_PASSWORD) resp = esp_send_cmd(cmd, 20000, ("WIFI GOT IP", "OK", "FAIL", "ERROR")) if "OK" in resp or "CONNECTED" in resp or "WIFI GOT IP" in resp: utime.sleep_ms(1000) status = esp_send_cmd("AT+CWJAP?", 1500) ip = esp_send_cmd("AT+CIFSR", 1500) if WIFI_SSID not in status and "STAIP" not in ip: print("[ESP] WiFi command returned OK, but no station IP found") return False print("[ESP] WiFi connected!") return True print("[ESP] WiFi connect failed") return False def esp_init_wifi(max_retries=5): """初始化ESP8266并连接WiFi(减少重试次数)""" print("[ESP] Initializing...") esp_wait_boot() esp_clear_rx() # 测试AT响应 for i in range(max_retries): resp = esp_send_cmd("AT", 2500) if "OK" in resp: print("[ESP] AT OK") break print("[ESP] AT retry {}/{}".format(i+1, max_retries)) utime.sleep_ms(500) else: if not esp_probe_uart(): print("[ESP] ESP8266 not responding!") return False # 连接WiFi for i in range(max_retries): if esp_connect_wifi(): return True utime.sleep_ms(1000) return False def esp_ensure_wifi(): """确保WiFi已连接(快速检查)""" global wifi_connected if not wifi_connected: wifi_connected = esp_init_wifi(1) return wifi_connected return wifi_connected def ensure_local_photo_dir(): """确保SD卡本地照片目录存在""" try: os.listdir(LOCAL_FACE_PHOTO_DIR) return True except: try: os.mkdir(LOCAL_FACE_PHOTO_DIR) print("[PHOTO] Created local dir:", LOCAL_FACE_PHOTO_DIR) return True except Exception as e: print("[PHOTO] Create local dir failed:", e) return False def local_face_photo_path(face_id): return "{}/{}.jpg".format(LOCAL_FACE_PHOTO_DIR, face_id) def save_local_face_photo(face_id, jpeg_data): """先把人脸照片保存到SD卡,保证本地特征和照片一致""" if not ensure_local_photo_dir(): return False path = local_face_photo_path(face_id) try: with open(path, "wb") as f: f.write(jpeg_data) print("[PHOTO] Saved local photo:", path) return True except Exception as e: print("[PHOTO] Save local photo failed:", e) return False def load_local_face_photo(face_id): path = local_face_photo_path(face_id) try: with open(path, "rb") as f: return f.read() except Exception as e: print("[PHOTO] Read local photo failed {}: {}".format(path, e)) return None def remove_photo_from_queue(face_id): global photo_upload_queue face_id = str(face_id) photo_upload_queue = [x for x in photo_upload_queue if x != face_id] def delete_local_face_photo(face_id): face_id = str(face_id) remove_photo_from_queue(face_id) path = local_face_photo_path(face_id) try: os.remove(path) print("[PHOTO] Deleted local photo:", path) return True except: return False def delete_all_local_face_photos(): if not ensure_local_photo_dir(): return try: for name in os.listdir(LOCAL_FACE_PHOTO_DIR): if name.endswith(".jpg"): try: os.remove("{}/{}".format(LOCAL_FACE_PHOTO_DIR, name)) except: pass photo_upload_queue[:] = [] print("[PHOTO] Deleted all local face photos") except Exception as e: print("[PHOTO] Delete all local photos failed:", e) def esp_send_data(data, timeout=1500): """发送数据并等待SEND OK(优化超时)""" cmd = "AT+CIPSEND={}".format(len(data)) uart2.write(cmd + "\r\n") utime.sleep_ms(50) # 等待 > 提示符(缩短超时) start = utime.ticks_ms() resp = "" got_prompt = False while utime.ticks_diff(utime.ticks_ms(), start) < 1000: if uart2.any(): resp += uart2.read().decode('utf-8', 'ignore') if ">" in resp: got_prompt = True break if "ERROR" in resp or "link is not" in resp: return False utime.sleep_ms(5) if not got_prompt: return False # 发送数据 uart2.write(data) # 等待 SEND OK(缩短超时) start = utime.ticks_ms() resp = "" while utime.ticks_diff(utime.ticks_ms(), start) < timeout: if uart2.any(): resp += uart2.read().decode('utf-8', 'ignore') if "SEND OK" in resp: return True if "SEND FAIL" in resp or "ERROR" in resp: return False utime.sleep_ms(10) return False def esp_http_post_photo(face_id, jpeg_data, max_retries=2): """通过ESP8266发送HTTP POST请求上传照片""" # 直接使用face_id作为照片ID(不加时间戳) print("[ESP] Uploading photo for face ID:", face_id, "size:", len(jpeg_data)) for attempt in range(max_retries): # 确保WiFi连接 if not esp_ensure_wifi(): print("[ESP] No WiFi, retry {}/{}".format(attempt+1, max_retries)) utime.sleep_ms(2000) continue # 关闭之前的连接 esp_send_cmd("AT+CIPCLOSE", 500) utime.sleep_ms(100) # 设置单连接模式 esp_send_cmd("AT+CIPMUX=0", 500) # 建立TCP连接 cmd = 'AT+CIPSTART="TCP","{}",{}'.format(SERVER_IP, SERVER_PORT) resp = esp_send_cmd(cmd, 3000) if "OK" not in resp and "CONNECT" not in resp: print("[ESP] TCP failed, retry {}/{}".format(attempt+1, max_retries)) utime.sleep_ms(1000) continue print("[ESP] TCP connected, sending data...") # 使用face_id作为文件名 http_header = "POST /api/v1/faces/photos/{} HTTP/1.1\r\n".format(face_id) http_header += "Host: {}:{}\r\n".format(SERVER_IP, SERVER_PORT) http_header += "Content-Type: image/jpeg\r\n" http_header += "X-Api-Token: {}\r\n".format(API_TOKEN) http_header += "Content-Length: {}\r\n".format(len(jpeg_data)) http_header += "Connection: close\r\n\r\n" # 发送HTTP头 if not esp_send_data(http_header): print("[ESP] Send header failed") esp_send_cmd("AT+CIPCLOSE", 500) continue # 分块发送JPEG数据 CHUNK_SIZE = 1024 offset = 0 send_ok = True while offset < len(jpeg_data): chunk = jpeg_data[offset:offset + CHUNK_SIZE] if not esp_send_data(chunk): print("[ESP] Send chunk failed at", offset) send_ok = False break offset += CHUNK_SIZE print("[ESP] Sent {}/{}".format(min(offset, len(jpeg_data)), len(jpeg_data))) if not send_ok: esp_send_cmd("AT+CIPCLOSE", 500) utime.sleep_ms(500) continue # 等待服务器响应 print("[ESP] Waiting for response...") resp = "" start = utime.ticks_ms() while utime.ticks_diff(utime.ticks_ms(), start) < 3000: if uart2.any(): data = uart2.read() if data: resp += data.decode('utf-8', 'ignore') if "200" in resp or "success" in resp or "HTTP/1.1" in resp: print("[ESP] Got response!") break utime.sleep_ms(50) esp_send_cmd("AT+CIPCLOSE", 300) # 数据发送成功就算成功 if send_ok: print("[ESP] Photo uploaded successfully!") return True else: print("[ESP] Response:", resp[:100] if resp else "(empty)") print("[ESP] Retry {}/{}".format(attempt+1, max_retries)) utime.sleep_ms(1000) print("[ESP] Upload failed after {} retries".format(max_retries)) return False def capture_and_upload_face(face_id, img): """拍摄人脸照片并加入上传队列(非阻塞)""" try: # 先复制图像,避免修改原图 img_copy = img.copy() # 转换为RGB565格式(如果需要)后压缩为JPEG try: jpeg_img = img_copy.compress(quality=50) # 降低质量减小体积 jpeg_data = jpeg_img.to_bytes() except Exception as e1: print("[PHOTO] Compress method 1 failed:", e1) # 尝试另一种方式 try: jpeg_data = img_copy.to_bytes() # 直接尝试转换 except Exception as e2: print("[PHOTO] Compress method 2 failed:", e2) return False print("[PHOTO] JPEG size:", len(jpeg_data), "bytes") if len(jpeg_data) < 100: print("[PHOTO] JPEG too small, skip upload") return False # 先保存本地,再加入上传队列(非阻塞)。上传失败也不会丢照片。 if not save_local_face_photo(face_id, jpeg_data): return False queue_photo_upload(face_id) print("[PHOTO] Face {} saved locally and queued for upload".format(face_id)) return True except Exception as e: print("[PHOTO] Error:", e) return False # 初始化WiFi连接(启动时尝试连接) wifi_connected = False print("[BOOT] Starting ESP8266 WiFi init...") try: wifi_connected = esp_init_wifi() print("[BOOT] WiFi status:", "connected" if wifi_connected else "not connected") except Exception as e: print("[BOOT] ESP8266 init error:", e) # --- 人脸ID到真实学号的映射表 --- face_id_mapping = {} # 格式: {"1": "20210001", "2": "20210002"} MAPPING_FILE = "/sd/face_mapping.txt" # SD卡上的映射文件 SYNC_VERSION_FILE = "/sd/sync_version.txt" # 本地已应用的服务端同步版本 RECORD_PATH = "/sd/recordftr.txt" # 人脸特征记录文件路径 sync_last_version = 0 def load_local_mapping(): """从SD卡加载本地映射配置""" global face_id_mapping face_id_mapping.clear() try: with open(MAPPING_FILE, "r") as f: for line in f: line = line.strip() if '=' in line: parts = line.split('=') if len(parts) == 2: face_id_mapping[parts[0]] = parts[1] print("[LOCAL] Loaded {} mappings from SD".format(len(face_id_mapping))) except: print("[LOCAL] No local mapping file") def save_local_mapping(): """保存映射配置到SD卡""" try: with open(MAPPING_FILE, "w") as f: for k, v in face_id_mapping.items(): f.write("{}={}\n".format(k, v)) print("[LOCAL] Saved {} mappings to SD".format(len(face_id_mapping))) return True except Exception as e: print("[LOCAL] Save error:", e) return False def load_sync_version(): """从SD卡加载本地已同步版本""" global sync_last_version try: with open(SYNC_VERSION_FILE, "r") as f: value = f.read().strip() sync_last_version = int(value) if value else 0 except: sync_last_version = 0 print("[LOCAL] Sync version:", sync_last_version) def save_sync_version(version): """保存本地已同步版本""" global sync_last_version sync_last_version = version try: with open(SYNC_VERSION_FILE, "w") as f: f.write(str(version)) except Exception as e: print("[LOCAL] Save sync version error:", e) def update_face_record(face_id, new_student_id): """保存face_id到学号的映射,保持recordftr.txt中的原始face_id不变。""" global face_id_mapping, peoples face_id = str(face_id) new_student_id = str(new_student_id) if not local_face_id_exists(face_id): print("[UPDATE] Ignore mapping for missing local face_id {} -> {}".format(face_id, new_student_id)) return False face_id_mapping[face_id] = new_student_id save_local_mapping() found = False for p in peoples: if p.name == "ID:{}".format(face_id) or p.name == face_id: found = True break if found: print("[UPDATE] Mapping saved: face_id {} -> student_id {}".format(face_id, new_student_id)) else: print("[UPDATE] Mapping saved, local face feature not found yet: {}".format(face_id)) return True def esp_http_get(url_path, timeout=8000): """发送HTTP GET请求并返回响应内容""" if not wifi_connected: print("[HTTP_GET] WiFi未连接") return None print("[HTTP_GET] 关闭旧连接...") esp_send_cmd("AT+CIPCLOSE", 500) utime.sleep_ms(300) esp_send_cmd("AT+CIPMUX=0", 500) utime.sleep_ms(100) # 建立TCP连接 print("[HTTP_GET] 建立TCP连接: {}:{}".format(SERVER_IP, SERVER_PORT)) cmd = 'AT+CIPSTART="TCP","{}",{}'.format(SERVER_IP, SERVER_PORT) resp = esp_send_cmd(cmd, 5000) print("[HTTP_GET] TCP响应: {}".format(resp.replace('\r\n', ' ')[:80] if resp else "(空)")) if "OK" not in resp and "CONNECT" not in resp: print("[HTTP_GET] TCP连接失败") return None print("[HTTP_GET] TCP连接成功") utime.sleep_ms(200) # 构建HTTP GET请求 http_request = "GET {} HTTP/1.1\r\nHost: {}:{}\r\nX-Api-Token: {}\r\nX-Encrypted-Response: true\r\nConnection: close\r\n\r\n".format( url_path, SERVER_IP, SERVER_PORT, API_TOKEN) # 使用AT+CIPSEND发送 print("[HTTP_GET] 准备发送 {} bytes".format(len(http_request))) cmd = "AT+CIPSEND={}".format(len(http_request)) uart2.write(cmd + "\r\n") utime.sleep_ms(100) # 等待 > 提示符 start = utime.ticks_ms() prompt_resp = "" while utime.ticks_diff(utime.ticks_ms(), start) < 2000: if uart2.any(): data = uart2.read() if data: prompt_resp += data.decode('utf-8', 'ignore') if ">" in prompt_resp: break utime.sleep_ms(10) if ">" not in prompt_resp: print("[HTTP_GET] 未收到>提示符: {}".format(prompt_resp[:50])) esp_send_cmd("AT+CIPCLOSE", 300) return None print("[HTTP_GET] 收到>提示符,发送请求...") # 发送HTTP请求 uart2.write(http_request) # 等待SEND OK start = utime.ticks_ms() send_resp = "" while utime.ticks_diff(utime.ticks_ms(), start) < 3000: if uart2.any(): data = uart2.read() if data: send_resp += data.decode('utf-8', 'ignore') if "SEND OK" in send_resp: print("[HTTP_GET] SEND OK") break utime.sleep_ms(10) if "SEND OK" not in send_resp: print("[HTTP_GET] 发送失败: {}".format(send_resp[:50])) esp_send_cmd("AT+CIPCLOSE", 300) return None # 等待服务器响应(关键:要等足够长的时间) print("[HTTP_GET] 等待服务器响应...") resp = send_resp # 继续累积数据 start = utime.ticks_ms() while utime.ticks_diff(utime.ticks_ms(), start) < timeout: if uart2.any(): data = uart2.read() if data: try: resp += data.decode('utf-8', 'ignore') print("[HTTP_GET] 累积数据: {} bytes".format(len(resp))) except: pass # 加密响应是Base64文本,按Content-Length或连接关闭判断完整性。 if http_body_complete(resp): if "CLOSED" in resp: print("[HTTP_GET] 连接已关闭,数据完整") break # 继续等一下看有没有更多数据 utime.sleep_ms(500) if uart2.any(): extra = uart2.read() if extra: resp += extra.decode('utf-8', 'ignore') if "CLOSED" in resp or utime.ticks_diff(utime.ticks_ms(), start) > 3000: break utime.sleep_ms(30) esp_send_cmd("AT+CIPCLOSE", 300) print("[HTTP_GET] 原始响应长度: {}".format(len(resp))) if len(resp) > 0: print("[HTTP_GET] 响应片段: {}".format(resp[:300].replace('\r\n', '\\n'))) result = decode_http_json_body(resp) if result: print("[HTTP_GET] 解密JSON成功: {}".format(result[:100])) return result print("[HTTP_GET] 未找到可用JSON") return None def extract_face_id(face_name): face_name = str(face_name) if face_name.startswith("ID:"): return face_name.split(":", 1)[1] return face_name def local_face_id_exists(face_id): face_id = str(face_id) for p in peoples: if extract_face_id(p.name) == face_id: return True return False def get_mapped_student_id(face_name): face_id = extract_face_id(face_name) return face_id_mapping.get(face_id, "") def get_display_name(face_name): """显示学号映射;未绑定时显示原始face_id。""" mapped = get_mapped_student_id(face_name) return mapped if mapped else face_name # 加载本地映射配置 load_local_mapping() load_sync_version() # 启动时同步将在所有函数定义后执行(见文件末尾) # --- 定时同步配置 --- last_sync_time = utime.ticks_ms() sync_in_progress = False # 同步状态标志 sync_step = 0 # 同步步骤(分步执行避免长时间阻塞) # --- 远程删除人脸功能 --- def delete_face_by_id(face_id): """根据人脸ID删除本地人脸数据""" global peoples target_patterns = ["ID:{}".format(face_id)] print("[DELETE] Looking for patterns:", target_patterns) deleted_index = -1 for i, p in enumerate(peoples): for pattern in target_patterns: if p.name == pattern: deleted_index = i break if deleted_index >= 0: break if deleted_index >= 0: deleted_name = peoples[deleted_index].name peoples.pop(deleted_index) try: write_face_record() print("[DELETE] 删除人脸成功: {}".format(deleted_name)) # 同时删除本地照片,避免下次启动补传已删除的人脸照片 delete_local_face_photo(face_id) if deleted_name.startswith("ID:"): delete_local_face_photo(deleted_name.split(":", 1)[1]) else: delete_local_face_photo(deleted_name) for pattern in target_patterns: if pattern.startswith("ID:"): delete_local_face_photo(pattern.split(":", 1)[1]) else: delete_local_face_photo(pattern) # 同时删除映射 if face_id in face_id_mapping: del face_id_mapping[face_id] save_local_mapping() # 重新加载 load_faces() return True except Exception as e: print("[DELETE] 重写文件失败:", e) load_faces() return False else: print("[DELETE] 未找到人脸ID: {}".format(face_id)) return False def esp_http_post_json(url_path, json_data, timeout=2000): """发送HTTP POST请求(优化版)""" if not wifi_connected: return None encrypted_data = encrypt_payload_base64(json_data) if encrypted_data is None: return None esp_send_cmd("AT+CIPCLOSE", 300) utime.sleep_ms(50) esp_send_cmd("AT+CIPMUX=0", 300) cmd = 'AT+CIPSTART="TCP","{}",{}'.format(SERVER_IP, SERVER_PORT) resp = esp_send_cmd(cmd, 2000) if "OK" not in resp and "CONNECT" not in resp: return None http_request = "POST {} HTTP/1.1\r\nHost: {}:{}\r\nContent-Type: application/octet-stream\r\nX-Api-Token: {}\r\nX-Encrypted: AES128-CBC\r\nX-Encrypted-Response: true\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}".format( url_path, SERVER_IP, SERVER_PORT, API_TOKEN, len(encrypted_data), encrypted_data) if not esp_send_data(http_request, 1500): esp_send_cmd("AT+CIPCLOSE", 300) return None resp = "" start = utime.ticks_ms() while utime.ticks_diff(utime.ticks_ms(), start) < timeout: if uart2.any(): data = uart2.read() if data: resp += data.decode('utf-8', 'ignore') if http_body_complete(resp): break utime.sleep_ms(10) esp_send_cmd("AT+CIPCLOSE", 300) return decode_http_json_body(resp) def json_get_int(text, key, default=0): marker = '"{}":'.format(key) start = text.find(marker) if start == -1: return default start += len(marker) while start < len(text) and text[start] == ' ': start += 1 end = start while end < len(text) and text[end].isdigit(): end += 1 try: return int(text[start:end]) except: return default def extract_json_block(text, key, open_ch, close_ch): marker = '"{}":'.format(key) pos = text.find(marker) if pos == -1: return "" start = text.find(open_ch, pos + len(marker)) if start == -1: return "" depth = 0 for i in range(start, len(text)): ch = text[i] if ch == open_ch: depth += 1 elif ch == close_ch: depth -= 1 if depth == 0: return text[start:i + 1] return "" def parse_string_array_block(arr_str): values = [] pos = 0 while pos < len(arr_str): start = arr_str.find('"', pos) if start == -1: break end = arr_str.find('"', start + 1) if end == -1: break values.append(arr_str[start + 1:end]) pos = end + 1 return values def parse_delete_tasks(tasks_str): tasks = [] pos = 0 while pos < len(tasks_str): obj_start = tasks_str.find('{', pos) if obj_start == -1: break obj_end = tasks_str.find('}', obj_start) if obj_end == -1: break obj = tasks_str[obj_start:obj_end + 1] task_id = json_get_string(obj, "task_id", "") face_id = json_get_string(obj, "face_id", "") if face_id: tasks.append({"task_id": task_id, "face_id": face_id}) pos = obj_end + 1 return tasks def json_get_string(text, key, default=""): marker = '"{}":"'.format(key) start = text.find(marker) if start == -1: return default start += len(marker) end = text.find('"', start) if end == -1: return default return text[start:end] def parse_photo_ids(response): """解析服务端 /api/v1/faces/photos 返回的照片ID列表""" ids = {} pos = 0 marker = '"id":"' while pos < len(response): start = response.find(marker, pos) if start == -1: break start += len(marker) end = response.find('"', start) if end == -1: break photo_id = response[start:end] if photo_id: ids[photo_id] = 1 pos = end + 1 return ids def parse_mapping_object(obj_str): mappings = {} if len(obj_str) < 2: return mappings inner = obj_str[1:-1].strip() if not inner: return mappings parts = inner.split(',') for part in parts: if ':' not in part: continue kv = part.split(':', 1) key = kv[0].strip().strip('"') value = kv[1].strip().strip('"') if key and value: mappings[key] = value return mappings def apply_face_profiles(server_mappings): global face_id_mapping print("[SYNC_STATE] 服务器映射数量: {}".format(len(server_mappings))) updated_count = 0 valid_mappings = {} for face_id, student_id in face_id_mapping.items(): if local_face_id_exists(face_id) and student_id: valid_mappings[face_id] = student_id for face_id, student_id in server_mappings.items(): if not local_face_id_exists(face_id): print("[SYNC_STATE] 跳过本地不存在的face_id映射: ID:{} -> {}".format(face_id, student_id)) continue if student_id == "__PHOTO_ONLY__": print("[SYNC_STATE] 服务端仅有照片占位,保留本地映射: ID:{}".format(face_id)) continue if not student_id: if face_id in valid_mappings: del valid_mappings[face_id] updated_count += 1 print("[SYNC_STATE] 解除本地face_id绑定: ID:{}".format(face_id)) continue print("[SYNC_STATE] 映射: ID:{} -> {}".format(face_id, student_id)) if update_face_record(face_id, student_id): updated_count += 1 valid_mappings[face_id] = student_id face_id_mapping = valid_mappings save_local_mapping() print("[SYNC_STATE] 映射应用完成,更新 {} 条".format(updated_count)) def sync_unified_state(): """按服务端统一版本号同步K210所需状态""" global sync_last_version if not wifi_connected: print("[SYNC_STATE] WiFi未连接,跳过") return False path = "/api/v1/sync/state?client=k210&last_version={}".format(sync_last_version) print("[SYNC_STATE] GET {}".format(path)) response = esp_http_get(path, 8000) if response is None: print("[SYNC_STATE] 服务器无响应") return False server_version = json_get_int(response, "version", sync_last_version) changed = ('"changed":true' in response) print("[SYNC_STATE] local={}, server={}, changed={}".format(sync_last_version, server_version, changed)) if not changed: save_sync_version(server_version) return True profiles_arr = extract_json_block(response, "face_profiles", '[', ']') server_mappings = {} server_face_ids = {} pos = 0 while profiles_arr and pos < len(profiles_arr): obj_start = profiles_arr.find('{', pos) if obj_start == -1: break obj_end = profiles_arr.find('}', obj_start) if obj_end == -1: break obj = profiles_arr[obj_start:obj_end + 1] face_id = json_get_string(obj, "face_id", "") student_id = json_get_string(obj, "student_id", "") status = json_get_string(obj, "status", "") if face_id: server_face_ids[face_id] = 1 if status == "photo_only" and not student_id: student_id = "__PHOTO_ONLY__" server_mappings[face_id] = student_id pos = obj_end + 1 tasks_arr = extract_json_block(response, "delete_tasks", '[', ']') delete_tasks = parse_delete_tasks(tasks_arr) pending_arr = extract_json_block(response, "pending_delete", '[', ']') pending_delete = parse_string_array_block(pending_arr) if not delete_tasks: delete_tasks = [{"task_id": "", "face_id": face_id} for face_id in pending_delete] deleted_ids = [] deleted_task_ids = [] delete_failed = False print("[SYNC_STATE] 待删除任务: {}".format(delete_tasks)) for task in delete_tasks: face_id = task["face_id"] if face_id in server_face_ids: print("[SYNC_STATE] 忽略旧删除任务,服务端仍有人脸记录: {}".format(face_id)) deleted_ids.append(face_id) if task["task_id"]: deleted_task_ids.append(task["task_id"]) continue if not local_face_id_exists(face_id): print("[SYNC_STATE] 删除任务face_id本地不存在,跳过但ACK: {}".format(face_id)) deleted_ids.append(face_id) if task["task_id"]: deleted_task_ids.append(task["task_id"]) continue if delete_face_by_id(face_id): deleted_ids.append(face_id) if task["task_id"]: deleted_task_ids.append(task["task_id"]) else: delete_failed = True if deleted_ids: deleted_json = ",".join(['"{}"'.format(x) for x in deleted_ids]) task_json = ",".join(['"{}"'.format(x) for x in deleted_task_ids]) ack_body = '{{"client":"k210","version":{},"deleted":[{}],"task_ids":[{}]}}'.format(server_version, deleted_json, task_json) ack_resp = esp_http_post_json("/api/v1/sync/ack", ack_body, 3000) print("[SYNC_STATE] ACK响应:", ack_resp[:120] if ack_resp else "(空)") ack_compact = ack_resp.replace(" ", "").replace("\r", "").replace("\n", "") if ack_resp else "" ack_ok = '"success":true' in ack_compact if not ack_ok: delete_failed = True print("[SYNC_STATE] 删除ACK失败,暂不推进本地版本,等待下次重试") apply_face_profiles(server_mappings) if not delete_failed: save_sync_version(server_version) else: print("[SYNC_STATE] 有删除失败项,暂不推进本地版本,等待下次重试") return not delete_failed # --- K210时间管理 --- k210_base_timestamp = 0 # 基准时间戳(Unix时间戳) k210_base_ticks = 0 # 基准时刻的ticks def set_base_time(unix_timestamp): """设置K210的基准时间""" global k210_base_timestamp, k210_base_ticks k210_base_timestamp = unix_timestamp k210_base_ticks = utime.ticks_ms() print("[TIME] Base time set:", unix_timestamp) def get_current_timestamp(): """获取当前Unix时间戳""" if k210_base_timestamp == 0: # 如果还没设置基准时间,使用运行时间 return utime.ticks_ms() // 1000 else: # 计算当前时间戳 elapsed_seconds = (utime.ticks_ms() - k210_base_ticks) // 1000 return k210_base_timestamp + elapsed_seconds # --- 2. KPU模型和硬件初始化 --- try: task_fd = kpu.load("/sd/FaceDetection.smodel") task_ld = kpu.load("/sd/FaceLandmarkDetection.smodel") task_fe = kpu.load("/sd/FeatureExtraction.smodel") except Exception as e: print("Error loading KPU models:", e) while True: utime.sleep_ms(1000) clock = time.clock() fm.register(12, fm.fpioa.GPIO0) fm.register(13, fm.fpioa.GPIO1) fm.register(14, fm.fpioa.GPIO2) LED_B = GPIO(GPIO.GPIO0, GPIO.OUT, value=1) LED_G = GPIO(GPIO.GPIO1, GPIO.OUT, value=1) LED_R = GPIO(GPIO.GPIO2, GPIO.OUT, value=1) key_pin = 16 fm.register(key_pin, fm.fpioa.GPIOHS0) KEY = GPIO(GPIO.GPIOHS0, GPIO.IN) lcd.init() sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.set_hmirror(CAMERA_HMIRROR) sensor.set_vflip(CAMERA_VFLIP) sensor.run(1) anchor = (1.889, 2.5245, 2.9465, 3.94056, 3.99987, 5.3658, 5.155437, 6.92275, 6.718375, 9.01025) dst_point = [(44, 59), (84, 59), (64, 82), (47, 105), (81, 105)] kpu.init_yolo2(task_fd, 0.5, 0.3, 5, anchor) img_face = image.Image(size=(128, 128)) img_face.pix_to_ai() # --- 3. 按键中断处理 --- key_Flag = 0 key_pressed = 0 # 0=无, 1=短按, 2=长按(2秒), 3=超长按(5秒删除全部) t_key = 0 def fun(KEY): global key_Flag, key_pressed, t_key utime.sleep_ms(10) if KEY.value() == 0: if key_Flag == 0: key_Flag = 1 t_key = utime.ticks_ms() else: if key_Flag == 1: press_duration = utime.ticks_diff(utime.ticks_ms(), t_key) if press_duration > 5000: key_pressed = 3 # 超长按5秒:删除全部人脸 elif press_duration > 2000: key_pressed = 2 # 长按2秒:删除模式 else: key_pressed = 1 # 短按:添加人脸 key_Flag = 0 KEY.irq(fun, GPIO.IRQ_BOTH) def delete_all_faces(): """删除全部人脸数据""" global peoples, face_id_mapping try: # 清空人脸列表 peoples = [] # 清空记录文件 with open(record_path, "w") as f: pass # 写入空文件 # 清空映射 face_id_mapping = {} save_local_mapping() delete_all_local_face_photos() print("[DELETE_ALL] 已删除全部人脸数据") return True except Exception as e: print("[DELETE_ALL] 删除失败:", e) load_faces() return False # --- 4. 数据库和人脸管理函数 (已包含之前修复的bug) --- class People: def __init__(self, name="", feature=[]): self.name = name; self.feature = feature def feature_max(self, feature): if not self.feature: return 0 return kpu.face_compare(self.feature, feature) peoples = [] record_path = "/sd/recordftr.txt" def hex_value(ch): if '0' <= ch <= '9': return ord(ch) - ord('0') if 'a' <= ch <= 'f': return ord(ch) - ord('a') + 10 if 'A' <= ch <= 'F': return ord(ch) - ord('A') + 10 return -1 def parse_bytes_literal(text): """安全解析 bytearray(b"...") / b"..." 文本,不使用 eval。""" pos1 = text.find("b'") pos2 = text.find('b"') if pos1 < 0 and pos2 < 0: raise ValueError("bytes literal not found") if pos1 >= 0 and (pos2 < 0 or pos1 < pos2): pos = pos1 quote = "'" else: pos = pos2 quote = '"' i = pos + 2 values = [] while i < len(text): ch = text[i] if ch == quote: if len(values) < 64: raise ValueError("feature too short") return bytearray(values) if ch != "\\": values.append(ord(ch)) i += 1 else: i += 1 if i >= len(text): raise ValueError("bad escape") esc = text[i] if esc == "x": if i + 2 >= len(text): raise ValueError("bad hex escape") hi = hex_value(text[i + 1]) lo = hex_value(text[i + 2]) if hi < 0 or lo < 0: raise ValueError("bad hex escape") values.append(hi * 16 + lo) i += 3 elif esc == "n": values.append(10) i += 1 elif esc == "r": values.append(13) i += 1 elif esc == "t": values.append(9) i += 1 elif esc == "b": values.append(8) i += 1 elif esc == "f": values.append(12) i += 1 elif esc == "v": values.append(11) i += 1 elif esc == "a": values.append(7) i += 1 elif esc == "\\": values.append(92) i += 1 elif esc == quote: values.append(ord(quote)) i += 1 elif '0' <= esc <= '7': val = ord(esc) - ord('0') i += 1 count = 1 while count < 3 and i < len(text) and '0' <= text[i] <= '7': val = val * 8 + ord(text[i]) - ord('0') i += 1 count += 1 values.append(val & 0xff) else: values.append(ord(esc)) i += 1 if len(values) > 4096: raise ValueError("feature too long") raise ValueError("unterminated bytes literal") def parse_number_list(text): start = text.find("[") end = text.rfind("]") if start < 0 or end <= start: raise ValueError("feature must be bytearray/list") inner = text[start + 1:end].strip() if not inner: raise ValueError("empty feature") values = [] has_float = False all_byte = True for token in inner.split(","): token = token.strip() if not token: raise ValueError("empty number") if "." in token or "e" in token or "E" in token: value = float(token) has_float = True all_byte = False else: value = int(token) if value < 0 or value > 255: all_byte = False values.append(value) if len(values) > 4096: raise ValueError("feature too long") if len(values) < 64: raise ValueError("feature too short") if all_byte and not has_float: return bytearray(values) return values def parse_feature_vector(text): text = text.strip() if text.startswith("bytearray(") or text.startswith("b'") or text.startswith('b"'): return parse_bytes_literal(text) return parse_number_list(text) HEX_CHARS = "0123456789abcdef" def byte_to_hex(v): return HEX_CHARS[(v >> 4) & 15] + HEX_CHARS[v & 15] def escape_byte_values(values): out = "" for v in values: v = int(v) & 0xff if v == 92: out += "\\\\" elif v == 34: out += '\\"' elif v == 10: out += "\\n" elif v == 13: out += "\\r" elif v == 9: out += "\\t" elif 32 <= v <= 126: out += chr(v) else: out += "\\x" + byte_to_hex(v) return out def feature_to_record_text(feature): type_text = str(type(feature)) if type_text.find("bytearray") >= 0 or type_text.find("bytes") >= 0: values = [] for v in feature: values.append(int(v) & 0xff) return 'bytearray(b"' + escape_byte_values(values) + '")' return str(feature) def write_face_record(): with open(record_path, "w") as f: for p in peoples: f.write(str(p.name) + '#' + feature_to_record_text(p.feature) + "\n") def load_faces(): global peoples; peoples = [] total_count = 0 bad_count = 0 print("[FACE_DB] Loading {}".format(record_path)) try: with open(record_path, "r") as f: for line in f: line = line.strip() if not line: continue total_count += 1 # 使用 maxsplit=1 参数,只按第一个 '#' 分割,防止特征数据中的'#'导致错误 parts = line.split('#', 1) if len(parts) == 2 and parts[1]: try: peoples.append(People(parts[0], parse_feature_vector(parts[1]))) except Exception as e_eval: bad_count += 1 print("Feature parse error on line {} for {}: {}".format(total_count, parts[0], e_eval)) else: bad_count += 1 print("[FACE_DB] Bad line {}: missing feature".format(total_count)) if total_count == 0: print("[FACE_DB] recordftr.txt is empty") print("成功加载 %d 条人脸数据 (总行:%d, 失败:%d)" % (len(peoples), total_count, bad_count)) except Exception as e: print("读取人脸数据库失败:", e) def collect_used_face_ids(): """收集本地已经占用的K210原始face_id,避免新增时覆盖旧人脸。""" used = {} for p in peoples: name = str(p.name) if name.startswith("ID:") and name.split(":", 1)[1].isdigit(): used[name.split(":", 1)[1]] = 1 for face_id in face_id_mapping.keys(): face_id = str(face_id) if face_id.isdigit(): used[face_id] = 1 if ensure_local_photo_dir(): try: for filename in os.listdir(LOCAL_FACE_PHOTO_DIR): if filename.endswith(".jpg"): face_id = filename[:-4] if face_id.isdigit(): used[face_id] = 1 except Exception as e: print("[ADD] 扫描本地照片ID失败:", e) return used def allocate_next_face_id(): used = collect_used_face_ids() next_id = 1 while str(next_id) in used: next_id += 1 print("[ADD] 已占用face_id: {}, 新face_id: {}".format(sorted(used.keys()), next_id)) return next_id def add_face(feature, face_img=None): new_id_num = allocate_next_face_id() new_name = "ID:{}".format(new_id_num) peoples.append(People(new_name, feature)) try: if face_img is not None: print("[PHOTO] 正在保存人脸照片并加入上传队列...") if not capture_and_upload_face(new_id_num, face_img): print("[PHOTO] 本地照片保存失败,取消添加人脸") peoples.pop() return False with open(record_path, "a") as f: f.write(new_name + '#' + feature_to_record_text(feature) + "\n") print("添加成功:", new_name) return True except Exception as e: print("写入文件失败:", e) delete_local_face_photo(new_id_num) peoples.pop() return False def delete_face_by_index(idx): if not (0 <= idx < len(peoples)): return False deleted_name = peoples[idx].name peoples.pop(idx) try: write_face_record() if deleted_name.startswith("ID:"): delete_local_face_photo(deleted_name.split(":", 1)[1]) else: delete_local_face_photo(deleted_name) print("删除 %s 成功" % deleted_name); return True except Exception as e: print("重写文件失败:", e); load_faces(); return False # --- 5. 支持多张人脸检测的函数 (保持不变) --- def detect_and_recognize_faces(img): codes = kpu.run_yolo2(task_fd, img) if not codes: return [] results = [] for item in codes: face_cut = img.cut(item.x(), item.y(), item.w(), item.h()) face_cut_128 = face_cut.resize(128, 128); face_cut_128.pix_to_ai() fmap = kpu.forward(task_ld, face_cut_128) plist = fmap[:] le=(item.x()+int(plist[0]*item.w()-10), item.y()+int(plist[1]*item.h())); re=(item.x()+int(plist[2]*item.w()), item.y()+int(plist[3]*item.h())); nose=(item.x()+int(plist[4]*item.w()), item.y()+int(plist[5]*item.h())); lm=(item.x()+int(plist[6]*item.w()), item.y()+int(plist[7]*item.h())); rm=(item.x()+int(plist[8]*item.w()), item.y()+int(plist[9]*item.h())) src_point = [le, re, nose, lm, rm] T = image.get_affine_transform(src_point, dst_point) image.warp_affine_ai(img, img_face, T) del(face_cut); del(face_cut_128) fmap = kpu.forward(task_fe, img_face) feature = kpu.face_encode(fmap[:]) max_score = 0; match_index = -1 for i, p in enumerate(peoples): score = p.feature_max(feature) if score > max_score: max_score = score; match_index = i result_item = {'score': max_score, 'rect': item.rect(), 'feature': feature} if max_score > 85: result_item['state'] = peoples[match_index].name else: result_item['state'] = "stranger" results.append(result_item) return results # --- 6. 主程序启动前准备 - 添加发送逻辑所需变量 --- load_faces() last_send_time = 0 SEND_INTERVAL_MS = 1000 last_sent_data = "" # 改为记录完整数据而不是state last_saved_id = "" # ✨ 添加:初始化上次保存的学号 # --- 分时网络:照片上传队列操作 --- def queue_photo_upload(face_id): """将本地照片ID添加到上传队列(非阻塞)""" global photo_upload_queue face_id = str(face_id) if face_id in photo_upload_queue: print("[QUEUE] 照片已在队列中: ID={}".format(face_id)) return photo_upload_queue.append(face_id) print("[QUEUE] 照片已加入队列: ID={},队列长度: {}".format(face_id, len(photo_upload_queue))) def queue_local_photos_for_upload(): """启动时扫描SD卡照片,补传可能遗漏到服务器的照片""" if not ensure_local_photo_dir(): return server_photo_ids = None if wifi_connected: print("[QUEUE] 正在获取服务端照片列表,用于判断缺失照片...") response = esp_http_get("/api/v1/faces/photos", 8000) if response: server_photo_ids = parse_photo_ids(response) print("[QUEUE] 服务端已有照片: {} 张".format(len(server_photo_ids))) else: print("[QUEUE] 服务端照片列表获取失败,将本地照片全部加入补传队列") queued = 0 skipped = 0 try: for name in os.listdir(LOCAL_FACE_PHOTO_DIR): if name.endswith(".jpg"): face_id = name[:-4] if face_id: if server_photo_ids is not None: server_has_photo = face_id in server_photo_ids if server_has_photo: skipped += 1 continue queue_photo_upload(face_id) queued += 1 print("[QUEUE] 本地照片补传扫描完成: queued={}, skipped={}".format(queued, skipped)) except Exception as e: print("[QUEUE] 扫描本地照片失败:", e) def do_network_task(): """执行一个网络任务(每次只做一件事)""" global photo_upload_queue, last_sync_time # 优先处理照片上传 if len(photo_upload_queue) > 0: face_id = photo_upload_queue.pop(0) jpeg_data = load_local_face_photo(face_id) if jpeg_data is None or len(jpeg_data) < 100: print("[NET] 本地照片无效,跳过上传: ID={}".format(face_id)) return True print("[NET] 上传照片: ID={}".format(face_id)) try: if esp_http_post_photo(face_id, jpeg_data): print("[NET] 照片上传完成") else: print("[NET] 照片上传失败,保留队列等待重试") queue_photo_upload(face_id) except Exception as e: print("[NET] 上传错误:", e) queue_photo_upload(face_id) return True return False # 启动时扫描本地照片,补传可能遗漏到服务器的照片 queue_local_photos_for_upload() # 启动时执行一次同步 print("[BOOT] 启动同步...") if wifi_connected: try: sync_unified_state() except Exception as e: print("[BOOT] 统一同步错误:", e) print("[BOOT] 同步完成") else: print("[BOOT] WiFi未连接,跳过同步") # QT触发同步标志 sync_requested = False last_sync_check_time = 0 def check_sync_request(): """检查服务器是否有同步请求""" global sync_requested if not wifi_connected: return False try: response = esp_http_get("/api/v1/sync/request", 2000) if response and '"sync":true' in response: print("[SYNC] 收到服务器同步请求") sync_requested = True # 确认收到请求 esp_http_post_json("/api/v1/sync/request/ack", '{"ack":true}', 1000) return True except Exception as e: print("[SYNC] 检查同步请求错误:", e) return False def trigger_sync(): """执行一次完整同步(由QT端触发)""" global sync_requested if not wifi_connected: print("[SYNC] WiFi未连接,无法同步") return False print("[SYNC] ========== QT触发同步开始 ==========") try: sync_unified_state() except Exception as e: print("[SYNC] 统一同步错误:", e) print("[SYNC] ========== QT触发同步完成 ==========") sync_requested = False return True # --- 7. 主循环 (状态机) --- while True: clock.tick() # ------------------ 模式0:人脸识别与智能通信 (多人脸 & 无脸不发送) ------------------ if key_pressed == 0: img = sensor.snapshot() detected_faces = detect_and_recognize_faces(img) current_state = "" recognized_ids = [] if not detected_faces: LED_G.value(1); LED_R.value(1) else: has_stranger = False; has_known_person = False for face in detected_faces: rect = face['rect'] img.draw_rectangle(rect) if face['state'] == "stranger": img.draw_string(rect[0], rect[1], "Stranger", color=(255, 0, 0), scale=2) recognized_ids.append("0"); has_stranger = True else: # 使用映射后的学号显示 display_name = get_display_name(face['state']) img.draw_string(rect[0], rect[1], display_name, color=(0, 255, 0), scale=2) recognized_ids.append(face['state']); has_known_person = True if has_known_person: LED_G.value(0); LED_R.value(1) elif has_stranger: LED_G.value(1); LED_R.value(0) recognized_ids.sort(); current_state = ",".join(recognized_ids) time_now = utime.ticks_ms() if utime.ticks_diff(time_now, last_send_time) > SEND_INTERVAL_MS: if detected_faces: # 发送第一个识别到的人脸数据(可以改为发送得分最高的) best_face = max(detected_faces, key=lambda x: x['score']) if best_face['state'] == "stranger": # 陌生人,发送UNKNOWN msg_to_send = "UNKNOWN\r\n" # 陌生人不保存到SD卡 last_saved_id = "" # 重置保存记录 else: # 已识别用户,发送完整信息(STM32期望的格式) face_id = extract_face_id(best_face['state']) mapped_student_id = get_mapped_student_id(best_face['state']) if not mapped_student_id: print("[UART SEND] Face {} has no student mapping, fallback to face_id".format(face_id)) student_id = mapped_student_id if mapped_student_id else face_id student_name = mapped_student_id if mapped_student_id else "FACE_{}".format(face_id) confidence = best_face['score'] / 100.0 # 构造STM32期望的格式;未绑定学号时默认使用K210原始face_id。 msg_to_send = "ID:{},NAME:{},CONF:{:.2f}\r\n".format( student_id, student_name, confidence ) if msg_to_send != last_sent_data: uart.write(msg_to_send) print("[UART SEND] -> {}".format(msg_to_send.strip())) last_sent_data = msg_to_send last_send_time = time_now else: # 没有检测到人脸 if last_sent_data != "": print("[STATE CHANGE] Faces disappeared. No data sent.") last_sent_data = "" last_saved_id = "" # 重置保存记录 last_send_time = time_now lcd.display(img) # ------------------ 【核心修改】模式1:添加人脸 (添加了实时框) ------------------ elif key_pressed == 1: key_pressed = 0 last_sent_data = "" # 修改变量名 last_saved_id = "" # 重置保存记录 last_send_time = 0 print("进入添加人脸模式...") while True: img = sensor.snapshot() img.draw_string(0, 0, "Add Face Mode", color=(0, 0, 255), scale=2) img.draw_string(0, 24, "Click to Confirm", color=(0, 0, 255), scale=2) detected_faces = detect_and_recognize_faces(img) best_face = None if detected_faces: # 选出最清晰(得分最高)的脸进行操作 best_face = max(detected_faces, key=lambda x: x['score']) if best_face: rect = best_face['rect'] # 获取目标人脸的框 if best_face['state'] != "stranger": # 【新】如果是已存在的用户,画一个橙色的框 img.draw_rectangle(rect, color=(255, 128, 0), thickness=2) img.draw_string(rect[0], rect[1], "Exists!", color=(255, 128, 0), scale=2) else: # 【新】如果是可以添加的陌生人,画一个绿色的框 img.draw_rectangle(rect, color=(0, 255, 0), thickness=2) img.draw_string(rect[0], rect[1], "Ready", color=(0, 255, 0), scale=2) # 按键确认逻辑 (保持不变) if key_pressed == 1: key_pressed = 0 if best_face: if best_face['state'] != "stranger": img.draw_string(best_face['rect'][0], best_face['rect'][1] + 24, "Already exists!", color=(255, 0, 0), scale=2); LED_R.value(0) else: if add_face(best_face['feature'], img): img.draw_string(best_face['rect'][0], best_face['rect'][1] + 24, "Add OK!", color=(0, 255, 0), scale=2); LED_G.value(0) else: img.draw_string(best_face['rect'][0], best_face['rect'][1] + 24, "Add Failed!", color=(255, 0, 0), scale=2); LED_R.value(0) else: img.draw_string(10, 100, "No Face!", color=(255, 0, 0), scale=2); LED_R.value(0) lcd.display(img); utime.sleep_ms(2000); LED_R.value(1); LED_G.value(1); break if key_pressed == 2: key_pressed = 0; break # 长按退出 lcd.display(img) print("退出添加人脸模式") # ------------------ 模式2:删除人脸 (保持不变) ------------------ elif key_pressed == 2: key_pressed = 0 last_sent_data = "" # 修改变量名 last_saved_id = "" # 重置保存记录 last_send_time = 0 print("进入删除人脸模式...") select_index = -1 while True: img = image.Image(); img.clear() img.draw_string(10, 10, "Delete Mode", color=(255, 0, 0), scale=2); img.draw_string(10, 40, "Click=Next, Long Press=Confirm", scale=1.8) if key_pressed == 1: key_pressed = 0; select_index += 1 if select_index >= len(peoples): select_index = -1 if select_index == -1: img.draw_string(50, 100, "-> Cancel", color=(0, 255, 0), scale=2) else: img.draw_string(50, 100, "-> Del: {}".format(peoples[select_index].name), color=(255, 128, 0), scale=2) if key_pressed == 2: key_pressed = 0 if select_index != -1: if delete_face_by_index(select_index): img.clear(); img.draw_string(50, 100, "Delete Success!", color=(0, 255, 0), scale=2); LED_G.value(0) else: img.clear(); img.draw_string(50, 100, "Delete Failed!", color=(255, 0, 0), scale=2); LED_R.value(0) else: img.clear(); img.draw_string(50, 100, "Canceled", color=(255, 255, 0), scale=2) lcd.display(img); utime.sleep_ms(2000); LED_G.value(1); LED_R.value(1); load_faces(); break lcd.display(img) print("退出删除人脸模式") # ------------------ 模式3:删除全部人脸 (长按5秒) ------------------ elif key_pressed == 3: key_pressed = 0 print("进入删除全部人脸模式...") img = image.Image(); img.clear() img.draw_string(10, 10, "DELETE ALL FACES?", color=(255, 0, 0), scale=2) img.draw_string(10, 50, "Count: {}".format(len(peoples)), color=(255, 255, 0), scale=2) img.draw_string(10, 90, "Click=Confirm", color=(255, 128, 0), scale=1.8) img.draw_string(10, 120, "Long Press=Cancel", color=(0, 255, 0), scale=1.8) lcd.display(img) LED_R.value(0) # 红灯警告 # 等待用户确认 confirm_timeout = utime.ticks_ms() while utime.ticks_diff(utime.ticks_ms(), confirm_timeout) < 10000: # 10秒超时 if key_pressed == 1: # 短按确认删除 key_pressed = 0 img.clear() img.draw_string(30, 80, "Deleting...", color=(255, 0, 0), scale=2) lcd.display(img) if delete_all_faces(): img.clear() img.draw_string(20, 80, "All Deleted!", color=(0, 255, 0), scale=2) LED_R.value(1); LED_G.value(0) else: img.clear() img.draw_string(20, 80, "Delete Failed!", color=(255, 0, 0), scale=2) lcd.display(img) utime.sleep_ms(2000) break elif key_pressed == 2 or key_pressed == 3: # 长按取消 key_pressed = 0 img.clear() img.draw_string(50, 80, "Canceled", color=(255, 255, 0), scale=2) lcd.display(img) utime.sleep_ms(1500) break utime.sleep_ms(50) LED_R.value(1); LED_G.value(1) print("退出删除全部人脸模式") # 分时网络处理:每N帧检查一次网络任务(只处理照片上传和同步请求) frames_since_network += 1 if frames_since_network >= NETWORK_FRAME_INTERVAL: frames_since_network = 0 # 优先处理照片上传队列 if len(photo_upload_queue) > 0: do_network_task() # 检查是否有同步请求待执行 elif sync_requested: trigger_sync() # 定期检查服务器是否有新的同步请求 elif wifi_connected and utime.ticks_diff(utime.ticks_ms(), last_sync_check_time) > SYNC_CHECK_INTERVAL_MS: last_sync_check_time = utime.ticks_ms() check_sync_request() gc.collect()