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")