Files
bishe-K210/main.py
T
2026-05-04 21:02:28 +08:00

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