351 lines
12 KiB
Python
351 lines
12 KiB
Python
import re
|
|
|
|
with open('esp32_serial_backend/esp32_serial_backend.ino', 'r', encoding='utf8') as f:
|
|
content = f.read()
|
|
|
|
# 1. Add Task Variables at the top
|
|
if 'volatile bool gUploadTaskBusy' not in content:
|
|
task_vars = '''// ===== BACKGROUND UPLOAD TASK =====
|
|
volatile bool gUploadTaskBusy = false;
|
|
volatile bool gUploadFinished = false;
|
|
uint8_t *gUploadBuf = NULL;
|
|
size_t gUploadLen = 0;
|
|
String gUploadUrl = "";
|
|
String gUploadCmd = "";
|
|
String gUploadResult = "";
|
|
TaskHandle_t UploadTaskHandle = NULL;
|
|
|
|
void uploadTask(void *pvParameters)
|
|
{
|
|
while (true)
|
|
{
|
|
if (gUploadTaskBusy && !gUploadFinished)
|
|
{
|
|
gUploadResult = postFrameWithRetry(gUploadUrl, gUploadBuf, gUploadLen, 60000);
|
|
|
|
if (gUploadBuf != NULL) {
|
|
free(gUploadBuf);
|
|
gUploadBuf = NULL;
|
|
}
|
|
|
|
gUploadFinished = true;
|
|
}
|
|
vTaskDelay(50 / portTICK_PERIOD_MS); // yield
|
|
}
|
|
}
|
|
'''
|
|
content = content.replace('void pollAndRunJob()', task_vars + '\nvoid pollAndRunJob()')
|
|
|
|
# 2. Start Task in setup()
|
|
if 'xTaskCreatePinnedToCore(' not in content:
|
|
content = content.replace('Serial.println("Starting AP Mode");',
|
|
'xTaskCreatePinnedToCore(uploadTask, "UploadTask", 16384, NULL, 1, &UploadTaskHandle, 0);\n Serial.println("Starting AP Mode");')
|
|
content = content.replace('connectWiFi();',
|
|
'xTaskCreatePinnedToCore(uploadTask, "UploadTask", 16384, NULL, 1, &UploadTaskHandle, 0);\n connectWiFi();')
|
|
|
|
# 3. Modify handleCommand to only trigger the task
|
|
old_handle_cmd = ''' // Kita biarkan kamera di format RGB565 saat ini, encode software
|
|
camera_fb_t *fb = esp_camera_fb_get();
|
|
if (!fb)
|
|
{
|
|
Serial.println("ERROR no_frame");
|
|
showStatus("KAMERA ERROR", TFT_RED, "Camera fb null");
|
|
gBusy = false;
|
|
return false;
|
|
}
|
|
|
|
uint8_t *jpeg_buf = NULL;
|
|
size_t jpeg_len = 0;
|
|
bool is_allocated = false;
|
|
|
|
if (fb->format != PIXFORMAT_JPEG) {
|
|
if (!frame2jpg(fb, 40, &jpeg_buf, &jpeg_len)) {
|
|
showStatus("JPEG ENCODE FAIL", TFT_RED, "");
|
|
esp_camera_fb_return(fb);
|
|
gBusy = false;
|
|
return false;
|
|
}
|
|
is_allocated = true;
|
|
} else {
|
|
jpeg_buf = fb->buf;
|
|
jpeg_len = fb->len;
|
|
}
|
|
|
|
if (cmd.startsWith("ENROLL "))
|
|
{
|
|
String name = cmd.substring(7);
|
|
name.trim();
|
|
if (name.length() == 0)
|
|
{
|
|
showStatus("NAMA KOSONG", TFT_RED, "Silakan isi nama");
|
|
if (fb->format != PIXFORMAT_JPEG) free(jpeg_buf);
|
|
esp_camera_fb_return(fb);
|
|
gBusy = false;
|
|
return false;
|
|
}
|
|
String url = String(SERVER_BASE) + "/enroll?name=" + urlEncode(name);
|
|
if (isDigits(gJobPersonId))
|
|
{
|
|
url += String("&person_id=") + gJobPersonId;
|
|
}
|
|
else if (gJobPersonId.length())
|
|
{
|
|
Serial.println(String("[ENROLL] skipping invalid person_id=") + gJobPersonId);
|
|
}
|
|
// ENROLL runs DeepFace too (slower), so give it a longer timeout.
|
|
String resp = postFrameWithRetry(url, jpeg_buf, jpeg_len, 60000);
|
|
if (resp.startsWith("HTTP 400")) {
|
|
showStatus("COBA LAGI...", TFT_ORANGE, "Wajah tidak jelas");
|
|
} else if (resp.startsWith("HTTP ERR") || resp.startsWith("HTTP 4") || resp.startsWith("HTTP 5") || resp.startsWith("WIFI")) {
|
|
showStatus("ERR: " + resp, TFT_RED, "");
|
|
} else {
|
|
showStatus("ENROLL SUKSES", TFT_GREEN, resp);
|
|
gLastActionSuccess = true;
|
|
}
|
|
}
|
|
else if (cmd == "RECOG")
|
|
{
|
|
// Include threshold + uid from job so server uses the asked params and can log attendance.
|
|
String uid = "";
|
|
if (gJobRecogUid.length())
|
|
{
|
|
uid = gJobRecogUid;
|
|
}
|
|
else if (gExpectedUid.length())
|
|
{
|
|
uid = gExpectedUid;
|
|
Serial.println(String("[RECOG] using expected uid=") + gExpectedUid);
|
|
}
|
|
|
|
String endpoint = "/recognize";
|
|
if (uid.length())
|
|
{
|
|
endpoint = "/rfid/attendance";
|
|
}
|
|
|
|
String url = String(SERVER_BASE) + endpoint + "?threshold=" + String(gJobRecogThreshold, 2);
|
|
if (uid.length())
|
|
{
|
|
url += String("&uid=") + uid;
|
|
}
|
|
|
|
Serial.println(String("[RECOG] endpoint=") + endpoint + String(" uid=") + uid);
|
|
// RECOG can be slow (DeepFace + DB compare). ERR -11 = read timeout.
|
|
showStatus("MENGIRIM...", TFT_CYAN, endpoint);
|
|
Serial.println(String("[RECOG] upload url=") + url);
|
|
String resp = postFrameWithRetry(url, jpeg_buf, jpeg_len, 60000);
|
|
Serial.println(String("[RECOG] resp=") + resp);
|
|
|
|
if (resp.startsWith("HTTP ERR") || resp.startsWith("HTTP 4") || resp.startsWith("HTTP 5") || resp.startsWith("WIFI"))
|
|
{
|
|
Serial.println("[RECOG] upload FAILED: " + resp);
|
|
// Jika server merespon HTTP 400, kemungkinan besar "No face detected"
|
|
if (resp.indexOf("400") >= 0) {
|
|
showStatus("COBA LAGI...", TFT_ORANGE, "Wajah tidak jelas");
|
|
} else {
|
|
showStatus("ERR: " + resp, TFT_RED, "");
|
|
}
|
|
if (fb->format != PIXFORMAT_JPEG) free(jpeg_buf);
|
|
esp_camera_fb_return(fb);
|
|
gBusy = false;
|
|
return false;
|
|
}
|
|
|
|
// If we have an expected RFID identity, validate it.
|
|
bool hasExpected = gExpectedName.length() > 0 && (millis() - gExpectedSetAt) <= EXPECTED_TTL_MS;
|
|
if (hasExpected)
|
|
{
|
|
// We expect server JSON payload to contain the recognized name somewhere.
|
|
// Typical examples:
|
|
// {"name":"andi","distance":0.31}
|
|
// {"match":true,"name":"andi"...}
|
|
// We'll do a simple substring match, case-insensitive.
|
|
String respLower = resp;
|
|
respLower.toLowerCase();
|
|
String expLower = gExpectedName;
|
|
expLower.toLowerCase();
|
|
|
|
if (respLower.indexOf(expLower) >= 0)
|
|
{
|
|
showStatus("ABSEN SUKSES", TFT_GREEN, gExpectedName);
|
|
gLastActionSuccess = true;
|
|
}
|
|
else
|
|
{
|
|
showStatus("ABSEN GAGAL", TFT_RED, "Wajah tidak cocok");
|
|
}
|
|
|
|
// Clear expectation after one attempt.
|
|
gExpectedName = "";
|
|
gExpectedUid = "";
|
|
gExpectedSetAt = 0;
|
|
}
|
|
else
|
|
{
|
|
if (resp.indexOf("true") >= 0 || resp.indexOf("\\"match\\": true") >= 0 || resp.indexOf("\\"match\\":true") >= 0) {
|
|
showStatus("ABSEN SUKSES", TFT_GREEN, "");
|
|
gLastActionSuccess = true;
|
|
} else {
|
|
showStatus("SERVER MERESPON", TFT_CYAN, resp);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Ignore unknown commands to avoid spamming the TFT (UART noise can trigger this).
|
|
// keep silent
|
|
}
|
|
|
|
esp_camera_fb_return(fb);
|
|
|
|
gBusy = false;
|
|
return true;'''
|
|
|
|
new_handle_cmd = ''' if (cmd.startsWith("ENROLL ") || cmd == "RECOG")
|
|
{
|
|
if (gUploadTaskBusy) {
|
|
Serial.println("[CMD] Upload task is currently busy!");
|
|
gBusy = false;
|
|
return false;
|
|
}
|
|
|
|
if (cmd.startsWith("ENROLL ")) {
|
|
String name = cmd.substring(7);
|
|
name.trim();
|
|
if (name.length() == 0) {
|
|
showStatus("NAMA KOSONG", TFT_RED, "Silakan isi nama");
|
|
gBusy = false; return false;
|
|
}
|
|
gUploadUrl = String(SERVER_BASE) + "/enroll?name=" + urlEncode(name);
|
|
if (isDigits(gJobPersonId)) gUploadUrl += String("&person_id=") + gJobPersonId;
|
|
} else {
|
|
String uid = gJobRecogUid.length() ? gJobRecogUid : gExpectedUid;
|
|
String endpoint = uid.length() ? "/rfid/attendance" : "/recognize";
|
|
gUploadUrl = String(SERVER_BASE) + endpoint + "?threshold=" + String(gJobRecogThreshold, 2);
|
|
if (uid.length()) gUploadUrl += "&uid=" + uid;
|
|
}
|
|
|
|
camera_fb_t *fb = esp_camera_fb_get();
|
|
if (!fb) {
|
|
showStatus("KAMERA ERROR", TFT_RED, "Camera fb null");
|
|
gBusy = false; return false;
|
|
}
|
|
|
|
// Konversi ke JPEG secepat kilat (beberapa ms)
|
|
if (!frame2jpg(fb, 40, &gUploadBuf, &gUploadLen)) {
|
|
showStatus("JPEG ENCODE FAIL", TFT_RED, "");
|
|
esp_camera_fb_return(fb);
|
|
gBusy = false; return false;
|
|
}
|
|
esp_camera_fb_return(fb); // KEMBALIKAN FRAME SEGERA AGAR LAYAR TIDAK MACET!
|
|
|
|
gUploadCmd = cmd;
|
|
gUploadFinished = false;
|
|
gUploadTaskBusy = true; // TRIGGER TASK UPLOAD DI BACKGROUND
|
|
|
|
showStatus("MENGIRIM...", TFT_CYAN, "");
|
|
gBusy = false;
|
|
return true;
|
|
}
|
|
|
|
gBusy = false;
|
|
return true;'''
|
|
|
|
content = content.replace(old_handle_cmd, new_handle_cmd)
|
|
|
|
# 4. Handle Result in loop()
|
|
old_loop_retry = ''' if (now >= gNextCaptureTime)
|
|
{
|
|
Serial.println(String("[AUTO] Executing ") + gKeepTryingCmd);
|
|
showStatus("MEMPROSES...", TFT_CYAN, "Jangan bergerak", 2000);
|
|
handleCommand(gKeepTryingCmd);
|
|
lastJobPoll = millis(); // Reset poll timer
|
|
|
|
if (gLastActionSuccess)
|
|
{
|
|
postJobResult(true);
|
|
gKeepTryingCmd = ""; // Stop trying
|
|
gCameraActive = false; // Turn off camera on success
|
|
showStatus("BERHASIL", TFT_GREEN, "", 3000);
|
|
}
|
|
else
|
|
{
|
|
// Tunggu 1.5 detik sebelum ambil foto lagi agar preview kamera terlihat
|
|
gNextCaptureTime = millis() + 1500;
|
|
}
|
|
}'''
|
|
|
|
new_loop_retry = ''' // PROSES HASIL BACKGROUND UPLOAD
|
|
if (gUploadTaskBusy && gUploadFinished)
|
|
{
|
|
String resp = gUploadResult;
|
|
String cmd = gUploadCmd;
|
|
gUploadTaskBusy = false;
|
|
gUploadFinished = false;
|
|
|
|
if (cmd == "RECOG") {
|
|
if (resp.startsWith("HTTP 400")) {
|
|
showStatus("COBA LAGI...", TFT_ORANGE, "Wajah tidak jelas", 1000);
|
|
} else if (resp.startsWith("HTTP ERR") || resp.startsWith("HTTP 4") || resp.startsWith("HTTP 5") || resp.startsWith("WIFI")) {
|
|
showStatus("ERR: " + resp, TFT_RED, "", 1000);
|
|
} else {
|
|
bool hasExpected = gExpectedName.length() > 0 && (millis() - gExpectedSetAt) <= EXPECTED_TTL_MS;
|
|
if (hasExpected) {
|
|
String respLower = resp; respLower.toLowerCase();
|
|
String expLower = gExpectedName; expLower.toLowerCase();
|
|
if (respLower.indexOf(expLower) >= 0) {
|
|
showStatus("ABSEN SUKSES", TFT_GREEN, gExpectedName);
|
|
gLastActionSuccess = true;
|
|
} else {
|
|
showStatus("ABSEN GAGAL", TFT_RED, "Wajah tidak cocok", 1000);
|
|
}
|
|
gExpectedName = ""; gExpectedUid = ""; gExpectedSetAt = 0;
|
|
} else {
|
|
if (resp.indexOf("true") >= 0 || resp.indexOf("\\"match\\": true") >= 0 || resp.indexOf("\\"match\\":true") >= 0) {
|
|
showStatus("ABSEN SUKSES", TFT_GREEN, "");
|
|
gLastActionSuccess = true;
|
|
} else {
|
|
showStatus("SERVER MERESPON", TFT_CYAN, resp, 1000);
|
|
}
|
|
}
|
|
}
|
|
} else if (cmd.startsWith("ENROLL ")) {
|
|
if (resp.startsWith("HTTP 400")) {
|
|
showStatus("COBA LAGI...", TFT_ORANGE, "Wajah tidak jelas", 1000);
|
|
} else if (resp.startsWith("HTTP ERR") || resp.startsWith("HTTP 4") || resp.startsWith("HTTP 5") || resp.startsWith("WIFI")) {
|
|
showStatus("ERR: " + resp, TFT_RED, "", 1000);
|
|
} else {
|
|
showStatus("ENROLL SUKSES", TFT_GREEN, resp);
|
|
gLastActionSuccess = true;
|
|
}
|
|
}
|
|
|
|
if (gLastActionSuccess)
|
|
{
|
|
postJobResult(true);
|
|
gKeepTryingCmd = ""; // Stop trying
|
|
gCameraActive = false; // Turn off camera on success
|
|
showStatus("BERHASIL", TFT_GREEN, "", 3000);
|
|
}
|
|
else
|
|
{
|
|
// Tunggu 500ms saja sebelum mengambil foto berikutnya (karena upload selesai)
|
|
gNextCaptureTime = millis() + 500;
|
|
}
|
|
}
|
|
|
|
if (now >= gNextCaptureTime && !gUploadTaskBusy)
|
|
{
|
|
Serial.println(String("[AUTO] Capture frame untuk ") + gKeepTryingCmd);
|
|
handleCommand(gKeepTryingCmd);
|
|
lastJobPoll = millis(); // Reset poll timer
|
|
// Note: gNextCaptureTime will be set when upload finishes above
|
|
}'''
|
|
|
|
content = content.replace(old_loop_retry, new_loop_retry)
|
|
|
|
with open('esp32_serial_backend/esp32_serial_backend.ino', 'w', encoding='utf8') as f:
|
|
f.write(content)
|
|
|
|
print("Update complete")
|