1728 lines
63 KiB
Python
1728 lines
63 KiB
Python
# ====================================================================================
|
||
# 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
|
||
|
||
# --- 增加堆内存大小 ---
|
||
# 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 = "change-me-token" # 必须与服务端 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" # 本地人脸照片缓存目录
|
||
|
||
# 摄像头安装方向:当前实物画面上下左右都反,水平镜像+垂直翻转等价于旋转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):
|
||
"""更新SD卡中人脸记录的学号"""
|
||
global face_id_mapping, peoples
|
||
|
||
# 更新内存中的映射表
|
||
face_id_mapping[face_id] = new_student_id
|
||
save_local_mapping()
|
||
|
||
# 先打印当前peoples列表中的名称,用于调试
|
||
print("[UPDATE] 当前peoples列表:")
|
||
for i, p in enumerate(peoples):
|
||
print("[UPDATE] [{}] {}".format(i, p.name))
|
||
|
||
# 更新 recordftr.txt 中的名称
|
||
try:
|
||
new_lines = []
|
||
updated = False
|
||
with open(record_path, "r") as f:
|
||
for line in f:
|
||
parts = line.strip().split('#', 1)
|
||
if len(parts) == 2:
|
||
name = parts[0]
|
||
feature = parts[1]
|
||
|
||
# 打印当前处理的记录
|
||
print("[UPDATE] 检查记录: name='{}', face_id='{}'".format(name, face_id))
|
||
|
||
# 检查是否是要更新的ID(支持多种格式匹配)
|
||
should_update = False
|
||
|
||
# 格式1: ID:X(原始格式)- 匹配 ID:1, ID:2 等
|
||
if name == "ID:{}".format(face_id):
|
||
should_update = True
|
||
print("[UPDATE] 匹配格式1: ID:X")
|
||
|
||
# 格式2: 名称中包含 :X
|
||
if ':' in name:
|
||
current_id = name.split(':')[1]
|
||
if current_id == face_id:
|
||
should_update = True
|
||
print("[UPDATE] 匹配格式2: :X")
|
||
|
||
# 格式3: 直接匹配face_id本身
|
||
if name == face_id:
|
||
should_update = True
|
||
print("[UPDATE] 匹配格式3: 直接匹配")
|
||
|
||
if should_update:
|
||
# 更新为新学号
|
||
new_name = new_student_id
|
||
new_lines.append(new_name + '#' + feature + '\n')
|
||
updated = True
|
||
print("[UPDATE] {} -> {}".format(name, new_name))
|
||
continue
|
||
|
||
new_lines.append(line)
|
||
|
||
if updated:
|
||
with open(record_path, "w") as f:
|
||
for line in new_lines:
|
||
f.write(line)
|
||
# 重新加载人脸数据
|
||
load_faces()
|
||
print("[UPDATE] Face record updated successfully")
|
||
return True
|
||
else:
|
||
print("[UPDATE] Face ID {} not found in records".format(face_id))
|
||
except Exception as e:
|
||
print("[UPDATE] Error:", e)
|
||
return False
|
||
|
||
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\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
|
||
# 检查是否收到完整响应(有JSON且连接关闭)
|
||
if "}" in resp and "{" in 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')))
|
||
|
||
# 提取JSON
|
||
json_start = resp.find('{"')
|
||
if json_start != -1:
|
||
json_end = resp.rfind('}')
|
||
if json_end != -1 and json_end > json_start:
|
||
result = resp[json_start:json_end+1]
|
||
print("[HTTP_GET] 提取JSON成功: {}".format(result[:100]))
|
||
return result
|
||
|
||
print("[HTTP_GET] 未找到JSON")
|
||
return None
|
||
|
||
def sync_face_id_mapping():
|
||
"""从服务器同步人脸ID映射配置并更新本地记录"""
|
||
global face_id_mapping
|
||
|
||
if not wifi_connected:
|
||
print("[SYNC] WiFi not connected, skip sync")
|
||
return False
|
||
|
||
try:
|
||
print("[SYNC] ========== 开始同步映射配置 ==========")
|
||
print("[SYNC] 请求: GET /api/v1/faces/mappings")
|
||
response = esp_http_get("/api/v1/faces/mappings", 2000)
|
||
|
||
if response is None:
|
||
print("[SYNC] 服务器无响应")
|
||
return False
|
||
|
||
print("[SYNC] 收到响应: {}".format(response[:150] if len(response) > 150 else response))
|
||
|
||
# 简单解析JSON
|
||
mappings_start = response.find('"mappings":{')
|
||
if mappings_start == -1:
|
||
print("[SYNC] 响应中无mappings字段")
|
||
return False
|
||
|
||
mappings_start += len('"mappings":{')
|
||
mappings_end = response.find('}', mappings_start)
|
||
if mappings_end == -1:
|
||
return False
|
||
|
||
mappings_str = response[mappings_start:mappings_end]
|
||
print("[SYNC] 解析映射字符串: {}".format(mappings_str))
|
||
|
||
# 解析键值对
|
||
server_mappings = {}
|
||
parts = mappings_str.split(',')
|
||
for part in parts:
|
||
part = part.strip()
|
||
if ':' in part:
|
||
kv = part.split(':')
|
||
if len(kv) == 2:
|
||
key = kv[0].strip().strip('"')
|
||
value = kv[1].strip().strip('"')
|
||
if key and value:
|
||
server_mappings[key] = value
|
||
|
||
print("[SYNC] 服务器映射数量: {}".format(len(server_mappings)))
|
||
for k, v in server_mappings.items():
|
||
print("[SYNC] 人脸ID: {} -> 学号: {}".format(k, v))
|
||
|
||
if len(server_mappings) == 0:
|
||
print("[SYNC] 服务器无映射配置")
|
||
return True
|
||
|
||
# 对比本地映射
|
||
print("[SYNC] 本地映射数量: {}".format(len(face_id_mapping)))
|
||
for k, v in face_id_mapping.items():
|
||
print("[SYNC] 本地: {} -> {}".format(k, v))
|
||
|
||
# 更新本地人脸记录文件(recordftr.txt)
|
||
updated_count = 0
|
||
for face_id, student_id in server_mappings.items():
|
||
print("[SYNC] 检查是否需要更新: ID:{} -> {}".format(face_id, student_id))
|
||
if update_face_record(face_id, student_id):
|
||
updated_count += 1
|
||
|
||
# 更新内存中的映射表
|
||
face_id_mapping = server_mappings
|
||
save_local_mapping()
|
||
|
||
print("[SYNC] ========== 同步完成,更新了 {} 条记录 ==========".format(updated_count))
|
||
return True
|
||
|
||
except Exception as e:
|
||
print("[SYNC] 同步出错:", e)
|
||
return False
|
||
return False
|
||
|
||
def get_display_name(face_name):
|
||
"""获取用于显示的名称(直接使用人脸记录中的名称)"""
|
||
return 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), # 原始格式 ID:5
|
||
face_id # 直接匹配 (可能是学号如 20210001)
|
||
]
|
||
|
||
# 如果face_id在映射表中,也添加映射后的学号作为匹配模式
|
||
if face_id in face_id_mapping:
|
||
target_patterns.append(face_id_mapping[face_id])
|
||
|
||
# 反向查找:如果face_id是学号,查找对应的原始ID
|
||
for orig_id, student_id in face_id_mapping.items():
|
||
if student_id == face_id:
|
||
target_patterns.append("ID:{}".format(orig_id))
|
||
target_patterns.append(orig_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:
|
||
with open(record_path, "w") as f:
|
||
for p in peoples:
|
||
f.write(p.name + '#' + str(p.feature) + "\n")
|
||
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
|
||
|
||
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/json\r\nX-Api-Token: {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}".format(
|
||
url_path, SERVER_IP, SERVER_PORT, API_TOKEN, len(json_data), json_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 "success" in resp or "}" in resp:
|
||
break
|
||
utime.sleep_ms(10)
|
||
|
||
esp_send_cmd("AT+CIPCLOSE", 300)
|
||
return resp
|
||
|
||
def sync_pending_delete():
|
||
"""从服务器同步待删除的人脸列表"""
|
||
if not wifi_connected:
|
||
print("[DELETE_SYNC] WiFi未连接,跳过")
|
||
return False
|
||
|
||
try:
|
||
print("[DELETE_SYNC] ========== 开始同步删除列表 ==========")
|
||
print("[DELETE_SYNC] 请求: GET /api/v1/faces/pending-delete")
|
||
response = esp_http_get("/api/v1/faces/pending-delete", 8000)
|
||
|
||
if response is None:
|
||
print("[DELETE_SYNC] 服务器无响应")
|
||
return False
|
||
|
||
print("[DELETE_SYNC] 收到响应: {}".format(response))
|
||
|
||
# 解析JSON
|
||
start = response.find('"pending_delete":[')
|
||
if start == -1:
|
||
print("[DELETE_SYNC] 响应中无pending_delete字段")
|
||
return True
|
||
|
||
start += len('"pending_delete":[')
|
||
end = response.find(']', start)
|
||
if end == -1:
|
||
return False
|
||
|
||
arr_str = response[start:end]
|
||
print("[DELETE_SYNC] 待删除列表字符串: '{}'".format(arr_str))
|
||
|
||
if not arr_str.strip():
|
||
print("[DELETE_SYNC] 无待删除的人脸")
|
||
return True
|
||
|
||
# 解析数组
|
||
ids_to_delete = []
|
||
parts = arr_str.split(',')
|
||
for part in parts:
|
||
part = part.strip().strip('"')
|
||
if part:
|
||
ids_to_delete.append(part)
|
||
|
||
print("[DELETE_SYNC] 待删除人脸ID列表: {}".format(ids_to_delete))
|
||
|
||
if not ids_to_delete:
|
||
print("[DELETE_SYNC] 解析后无待删除项")
|
||
return True
|
||
|
||
# 显示当前本地人脸列表
|
||
print("[DELETE_SYNC] 当前本地人脸数量: {}".format(len(peoples)))
|
||
for i, p in enumerate(peoples):
|
||
print("[DELETE_SYNC] [{}] {}".format(i, p.name))
|
||
|
||
# 执行删除(每次最多处理一个,避免长时间阻塞)
|
||
face_id = ids_to_delete[0]
|
||
print("[DELETE_SYNC] 正在删除: {}".format(face_id))
|
||
|
||
delete_result = delete_face_by_id(face_id)
|
||
print("[DELETE_SYNC] 删除结果: {}".format("成功" if delete_result else "失败"))
|
||
|
||
if delete_result:
|
||
# 只有本地特征真正删除成功后才确认,避免服务端过早清掉待删除任务
|
||
print("[DELETE_SYNC] 发送确认: POST /api/v1/faces/delete-ack")
|
||
confirm_result = esp_http_post_confirm(face_id)
|
||
print("[DELETE_SYNC] 确认结果: {}".format("成功" if confirm_result else "失败"))
|
||
else:
|
||
print("[DELETE_SYNC] 本地未删除成功,暂不确认,等待下次同步重试")
|
||
|
||
# 显示删除后的人脸列表
|
||
print("[DELETE_SYNC] 删除后本地人脸数量: {}".format(len(peoples)))
|
||
for i, p in enumerate(peoples):
|
||
print("[DELETE_SYNC] [{}] {}".format(i, p.name))
|
||
|
||
print("[DELETE_SYNC] ========== 删除同步完成 ==========")
|
||
return True
|
||
|
||
except Exception as e:
|
||
print("[DELETE_SYNC] 同步出错:", e)
|
||
return False
|
||
|
||
def esp_http_post_confirm(face_id):
|
||
"""发送删除确认请求"""
|
||
if not wifi_connected:
|
||
return False
|
||
|
||
try:
|
||
esp_send_cmd("AT+CIPCLOSE", 500)
|
||
utime.sleep_ms(200)
|
||
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("[CONFIRM] TCP连接失败")
|
||
return False
|
||
|
||
# 构建POST请求
|
||
json_data = '{{"face_id":"{}"}}'.format(face_id)
|
||
http_request = "POST /api/v1/faces/delete-ack HTTP/1.1\r\n"
|
||
http_request += "Host: {}:{}\r\n".format(SERVER_IP, SERVER_PORT)
|
||
http_request += "Content-Type: application/json\r\n"
|
||
http_request += "X-Api-Token: {}\r\n".format(API_TOKEN)
|
||
http_request += "Content-Length: {}\r\n".format(len(json_data))
|
||
http_request += "Connection: close\r\n\r\n"
|
||
http_request += json_data
|
||
|
||
# 发送请求
|
||
cmd = "AT+CIPSEND={}".format(len(http_request))
|
||
uart2.write(cmd + "\r\n")
|
||
utime.sleep_ms(100)
|
||
|
||
# 等待>提示符
|
||
start = utime.ticks_ms()
|
||
prompt = ""
|
||
while utime.ticks_diff(utime.ticks_ms(), start) < 2000:
|
||
if uart2.any():
|
||
prompt += uart2.read().decode('utf-8', 'ignore')
|
||
if ">" in prompt:
|
||
break
|
||
utime.sleep_ms(10)
|
||
|
||
if ">" not in prompt:
|
||
esp_send_cmd("AT+CIPCLOSE", 300)
|
||
return False
|
||
|
||
# 发送数据
|
||
uart2.write(http_request)
|
||
|
||
# 等待响应
|
||
resp = ""
|
||
start = utime.ticks_ms()
|
||
while utime.ticks_diff(utime.ticks_ms(), start) < 5000:
|
||
if uart2.any():
|
||
data = uart2.read()
|
||
if data:
|
||
resp += data.decode('utf-8', 'ignore')
|
||
if "success" in resp:
|
||
print("[CONFIRM] 服务器确认成功")
|
||
esp_send_cmd("AT+CIPCLOSE", 300)
|
||
return True
|
||
utime.sleep_ms(20)
|
||
|
||
esp_send_cmd("AT+CIPCLOSE", 300)
|
||
print("[CONFIRM] 响应: {}".format(resp[:100] if resp else "(空)"))
|
||
return "success" in resp
|
||
|
||
except Exception as e:
|
||
print("[CONFIRM] 错误:", e)
|
||
return False
|
||
|
||
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_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_mappings(server_mappings):
|
||
global face_id_mapping
|
||
print("[SYNC_STATE] 服务器映射数量: {}".format(len(server_mappings)))
|
||
updated_count = 0
|
||
for face_id, student_id in server_mappings.items():
|
||
print("[SYNC_STATE] 映射: ID:{} -> {}".format(face_id, student_id))
|
||
if update_face_record(face_id, student_id):
|
||
updated_count += 1
|
||
face_id_mapping = server_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
|
||
|
||
pending_arr = extract_json_block(response, "pending_delete", '[', ']')
|
||
pending_delete = parse_string_array_block(pending_arr)
|
||
deleted_ids = []
|
||
delete_failed = False
|
||
print("[SYNC_STATE] 待删除: {}".format(pending_delete))
|
||
|
||
for face_id in pending_delete:
|
||
if delete_face_by_id(face_id):
|
||
deleted_ids.append(face_id)
|
||
else:
|
||
delete_failed = True
|
||
|
||
if deleted_ids:
|
||
deleted_json = ",".join(['"{}"'.format(x) for x in deleted_ids])
|
||
ack_body = '{{"client":"k210","version":{},"deleted":[{}]}}'.format(server_version, deleted_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 "(空)")
|
||
|
||
mappings_obj = extract_json_block(response, "face_mappings", '{', '}')
|
||
server_mappings = parse_mapping_object(mappings_obj)
|
||
apply_face_mappings(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 load_faces():
|
||
global peoples; peoples = []
|
||
try:
|
||
with open(record_path, "r") as f:
|
||
for line in f:
|
||
# 使用 maxsplit=1 参数,只按第一个 '#' 分割,防止特征数据中的'#'导致错误
|
||
parts = line.strip().split('#', 1)
|
||
if len(parts) == 2 and parts[1]:
|
||
try: peoples.append(People(parts[0], eval(parts[1])))
|
||
except Exception as e_eval: print("Eval error on line for {}: {}".format(parts[0], e_eval))
|
||
print("成功加载 %d 条人脸数据" % len(peoples))
|
||
except Exception as e: print("读取人脸数据库失败:", e)
|
||
def add_face(feature, face_img=None):
|
||
new_id_num = 1
|
||
if peoples:
|
||
id_nums = [int(p.name.split(':')[1]) for p in peoples if ':' in p.name and p.name.split(':')[1].isdigit()]
|
||
if id_nums: new_id_num = max(id_nums) + 1
|
||
else: new_id_num = len(peoples) + 1
|
||
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 + '#' + str(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:
|
||
with open(record_path, "w") as f:
|
||
for p in peoples: f.write(p.name + '#' + str(p.feature) + "\n")
|
||
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 not server_has_photo and face_id in face_id_mapping:
|
||
server_has_photo = face_id_mapping[face_id] in server_photo_ids
|
||
if not server_has_photo:
|
||
for orig_id, student_id in face_id_mapping.items():
|
||
if student_id == face_id and orig_id in server_photo_ids:
|
||
server_has_photo = True
|
||
break
|
||
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期望的格式)
|
||
student_id = best_face['state'] # 例如 "ID:2"
|
||
student_name = best_face['state'].replace(":", "_") # 将 "ID:2" 转为 "ID_2"
|
||
confidence = best_face['score'] / 100.0
|
||
|
||
# 构造STM32期望的格式:ID:2,NAME:ID_2,CONF:0.95
|
||
msg_to_send = "ID:{},NAME:{},CONF:{:.2f}\r\n".format(
|
||
student_id.split(':')[1] if ':' in student_id else 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()
|
||
|