Files
bishe-STM32/k210/main.py
T
2026-05-01 21:34:10 +08:00

1833 lines
66 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ====================================================================================
# 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 = "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" # 本地人脸照片缓存目录
# 摄像头安装方向:当前实物画面上下左右都反,水平镜像+垂直翻转等价于旋转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_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_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
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 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失败,暂不推进本地版本,等待下次重试")
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 parse_feature_vector(text):
"""只解析浮点数组文本,避免执行SD卡中的任意代码。"""
allowed = "0123456789+-eE.,[] \t\r\n"
for ch in text:
if ch not in allowed:
raise ValueError("invalid char in feature")
text = text.strip()
if not text.startswith("[") or not text.endswith("]"):
raise ValueError("feature must be list")
inner = text[1:-1].strip()
if not inner:
raise ValueError("empty feature")
values = []
for token in inner.split(","):
token = token.strip()
if not token:
raise ValueError("empty number")
values.append(float(token))
if len(values) > 512:
raise ValueError("feature too long")
if len(values) < 64:
raise ValueError("feature too short")
return values
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], parse_feature_vector(parts[1])))
except Exception as e_eval: print("Feature parse error on line for {}: {}".format(parts[0], e_eval))
print("成功加载 %d 条人脸数据" % len(peoples))
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
elif name.isdigit() and name in face_id_mapping:
used[name] = 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 + '#' + 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()