commit 4324d820b1c1e40bd223b3421c220c837e7abec2 Author: AnangSandi Date: Tue Aug 5 15:21:40 2025 +0700 Upload files to "/" diff --git a/brankasq_arduino.ino b/brankasq_arduino.ino new file mode 100644 index 0000000..726dedd --- /dev/null +++ b/brankasq_arduino.ino @@ -0,0 +1,82 @@ +#include "logic_keypad.h" +#include "cek_kamera.h" +#define BUZZER 13 +#define RELAY 3 + +void setup() { + pinMode(BUZZER, OUTPUT); + pinMode(RELAY, OUTPUT); + digitalWrite(RELAY, HIGH); + + Serial.begin(9600); + + lcd.init(); + lcd.backlight(); + + if (!cekKoneksiKamera()) { + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("Kamera Error"); + lcd.setCursor(0, 1); + lcd.print("Periksa Sistem"); + while (true); + } + + checkLockoutFromEEPROM(); + bool pinUpdated = false; + unsigned long startWait = millis(); + const unsigned long timeout = 5000; // Tunggu 5 detik + + while (millis() - startWait < timeout) { + if (Serial.available()) { + String command = Serial.readStringUntil('\n'); + command.trim(); + + if (command == "resetpin") { + savePINToEEPROM("000000"); + storedPIN = "000000"; + isFirstTime = true; + + lcd.clear(); + centerText("Silakan Tambah", 0); + centerText("Wajah PIN di Web", 1); + soundUbahPIN(); + pinUpdated = true; + break; + } + else if (command.startsWith("setpin:")) { + String newPIN = command.substring(7); + savePINToEEPROM(newPIN); + storedPIN = newPIN; + isFirstTime = false; + pinUpdated = true; + break; + } + } + } + + // Jika tidak ada kiriman dari ESP32-CAM, cek EEPROM manual + if (!pinUpdated) { + checkEEPROMForPIN(); + if (storedPIN == "000000") { + isFirstTime = true; + lcd.clear(); + centerText("Silakan Tambah", 0); + centerText("Wajah PIN Website", 1); + soundUbahPIN(); + } + } + + // Jika sudah punya PIN valid, siap input + if (!isFirstTime) { + lcd.clear(); + centerText("INPUT PIN", 0); + resetInput(); // siap mode input + } +} + + +void loop() { + handleSerialFromESP32(); + handleKeypadLogic(); +} diff --git a/brankasq_esp32cam.ino b/brankasq_esp32cam.ino new file mode 100644 index 0000000..40f35da --- /dev/null +++ b/brankasq_esp32cam.ino @@ -0,0 +1,659 @@ +#include +#include "esp_http_server.h" +#include "esp_timer.h" +#include "esp_camera.h" +#include "camera_index.h" +#include "Arduino.h" +#include "fd_forward.h" +#include "fr_forward.h" +#include "fr_flash.h" + +using namespace websockets; +WebsocketsServer socket_server; + +const char* ssid = "KOPI"; +const char* password = "digoreng123"; + +#define ENROLL_CONFIRM_TIMES 5 +#define FACE_ID_SAVE_NUMBER 5 +#define flash_pin 4 + +#define CAMERA_MODEL_AI_THINKER +#include "camera_pins.h" + +camera_fb_t * fb = NULL; + +unsigned long recognition_start_time = 0; +bool recognition_active = false; +unsigned long detect_start_time = 0; +bool detect_active = false; + +face_id_name_list st_face_list; +static dl_matrix3du_t *aligned_face = NULL; + +typedef struct { + uint8_t *image; + box_array_t *net_boxes; + dl_matrix3d_t *face_id; +} http_img_process_result; + +static inline mtmn_config_t app_mtmn_config() { + mtmn_config_t mtmn_config = {0}; + mtmn_config.type = FAST; + mtmn_config.min_face = 100; + mtmn_config.pyramid = 0.707; + mtmn_config.pyramid_times = 4; + mtmn_config.p_threshold.score = 0.6; + mtmn_config.p_threshold.nms = 0.7; + mtmn_config.p_threshold.candidate_number = 20; + mtmn_config.r_threshold.score = 0.7; + mtmn_config.r_threshold.nms = 0.7; + mtmn_config.r_threshold.candidate_number = 10; + mtmn_config.o_threshold.score = 0.7; + mtmn_config.o_threshold.nms = 0.7; + mtmn_config.o_threshold.candidate_number = 1; + return mtmn_config; +} + +mtmn_config_t mtmn_config = app_mtmn_config(); + +typedef enum { + START_STREAM, + START_DETECT, + SHOW_FACES, + START_RECOGNITION, + START_ENROLL, + ENROLL_COMPLETE, +} en_fsm_state; +en_fsm_state g_state; + +typedef struct { + char enroll_name[ENROLL_NAME_LEN]; +} httpd_resp_value; + +httpd_resp_value st_name; + +WebsocketsClient activeClient; + +void app_facenet_main() { + face_id_name_init(&st_face_list, FACE_ID_SAVE_NUMBER, ENROLL_CONFIRM_TIMES); + aligned_face = dl_matrix3du_alloc(1, FACE_WIDTH, FACE_HEIGHT, 3); + read_face_id_from_flash_with_name(&st_face_list); + print_all_faces(); +} + +static inline int do_enrollment(face_id_name_list *face_list, dl_matrix3d_t *new_id) +{ + ESP_LOGD(TAG, "START ENROLLING"); + int left_sample_face = enroll_face_id_to_flash_with_name(face_list, new_id, st_name.enroll_name); + ESP_LOGD(TAG, "Face ID %s Enrollment: Sample %d", + st_name.enroll_name, + ENROLL_CONFIRM_TIMES - left_sample_face); + return left_sample_face; +} + +void app_httpserver_init() { + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + httpd_handle_t camera_httpd = NULL; + if (httpd_start(&camera_httpd, &config) == ESP_OK) { + httpd_uri_t index_uri = { + .uri = "/", + .method = HTTP_GET, + .handler = [](httpd_req_t *req) -> esp_err_t { + httpd_resp_set_type(req, "text/html"); + httpd_resp_set_hdr(req, "Content-Encoding", "gzip"); + return httpd_resp_send(req, (const char *)index_html_gz, index_html_gz_len); + }, + .user_ctx = NULL + }; + httpd_register_uri_handler(camera_httpd, &index_uri); + } +} + +void send_face_list(WebsocketsClient &client) { + String names = ""; + face_id_node *head = st_face_list.head; + for (int i = 0; i < st_face_list.count; i++) { + names += head->id_name; + if (i < st_face_list.count - 1) names += ","; + head = head->next; + } + + if (client.available()) { + client.send("facelist:" + names); // hanya kirim satu kali + } +} + + +void send_feedback(String message) { + if (activeClient.available()) { + activeClient.send(message); + } else { + Serial.println(message); + } +} + +bool search_face_id_in_flash_by_name(face_id_name_list *face_list, const char *name) { + if (!name || strlen(name) == 0) return false; + + face_id_node *current = face_list->head; + while (current != NULL) { + if (strncmp(current->id_name, name, ENROLL_NAME_LEN) == 0) { + return true; // ditemukan + } + current = current->next; + } + return false; // tidak ditemukan +} + +void handle_command(String command) { + command.trim(); + + if (command == "stream") { + g_state = START_STREAM; + send_feedback("STREAMING"); + } + else if (command == "detect") { + g_state = START_DETECT; + detect_start_time = millis(); + detect_active = true; + digitalWrite(flash_pin, HIGH); + send_feedback("DETECTING"); + } + else if (command == "flash:on") { + digitalWrite(flash_pin, HIGH); + send_feedback("FLASH ON"); + } + else if (command == "flash:off") { + digitalWrite(flash_pin, LOW); + send_feedback("FLASH OFF"); + } + else if (command.startsWith("capture:")) { + String person = command.substring(8); + person.trim(); + if (person.length() == 0) { + send_feedback("FACE DATA CORRUPTED: NAME EMPTY"); + Serial.println("ERROR: Tidak boleh kosong. Nama wajah tidak valid."); + return; + } + + // Cek karakter valid + bool valid = true; + for (unsigned int i = 0; i < person.length(); i++) { + char c = person.charAt(i); + if (c < 32 || c > 126) { + valid = false; + break; + } + } + if (!valid) { + send_feedback("FACE DATA CORRUPTED: INVALID CHAR"); + Serial.println("ERROR: Nama wajah mengandung karakter tidak valid."); + return; + } + + if (search_face_id_in_flash_by_name(&st_face_list, person.c_str())) { + send_feedback("NAME ALREADY EXISTS"); + Serial.println("ERROR: Nama sudah ada."); + return; + } + + person.toCharArray(st_name.enroll_name, ENROLL_NAME_LEN - 1); + st_name.enroll_name[ENROLL_NAME_LEN - 1] = '\0'; + + g_state = START_ENROLL; + digitalWrite(flash_pin, HIGH); + send_feedback("CAPTURING"); + } + + else if (command == "recognise" || command == "DETEKSI") { + Serial.println("READY"); + g_state = START_RECOGNITION; + recognition_start_time = millis(); + recognition_active = true; + digitalWrite(flash_pin, HIGH); + send_feedback("RECOGNISING"); + } + + else if (command == "getpin") { + Serial.println("getpin"); + } + + else if (command.startsWith("newpin:")) { + String newPin = command.substring(7); + newPin.trim(); + if (st_face_list.count > 0) { + send_feedback("PIN TIDAK BISA DIUBAH"); + return; + } + + if (newPin.length() == 6 && newPin.toInt() >= 0) { + Serial.println("SET_PIN:" + newPin); + send_feedback("PIN BARU TERKIRIM"); + } else { + send_feedback("PIN INVALID"); + } + } + + else if (command.startsWith("remove:")) { + String person = command.substring(7); + char person_buf[ENROLL_NAME_LEN * FACE_ID_SAVE_NUMBER]; + strncpy(person_buf, person.c_str(), sizeof(person_buf)); + person_buf[sizeof(person_buf) - 1] = '\0'; + + bool result = delete_face_id_in_flash_with_name(&st_face_list, person_buf); + if (result) { + send_feedback("remove:ok"); + if (st_face_list.count == 0) { + Serial.println("resetpin"); + } + } else { + send_feedback("remove:error"); + } + } + + else if (command == "delete:allfaces") { + wipe_all_faces_manual(); + send_face_list(activeClient); + } + + else if (command == "getfacelist") { + String names = ""; + face_id_node *node = st_face_list.head; + while (node != nullptr) { + names += node->id_name; + names += ","; + node = node->next; + } + + Serial.println("facelist:" + names); + if (activeClient.available()) { + activeClient.send("facelist:" + names); + } + } +} + + +void handle_message(WebsocketsClient &client, WebsocketsMessage msg) { + handle_command(msg.data()); +} + +void handle_serial() { + if (Serial.available()) { + String line = Serial.readStringUntil('\n'); + line.trim(); + + if (line.startsWith("PIN:")) { + // Kirim ke WebSocket + if (activeClient.available()) { + activeClient.send("pin:" + line.substring(4)); // kirim hanya angkanya + } + } else { + handle_command(line); // tetap proses perintah lainnya + } + } +} + +void handle_websocket() { + if (!activeClient.available()) { + auto client = socket_server.accept(); + if (client.available()) { + Serial.println("Web Client Connected"); + activeClient = client; + activeClient.onMessage(handle_message); + send_feedback("READY WEBSOCKET"); + } + } + if (activeClient.available()) { + activeClient.poll(); + } +} + +void handle_camera() { + dl_matrix3du_t *image_matrix = dl_matrix3du_alloc(1, 320, 240, 3); + http_img_process_result out_res = {0}; + out_res.image = image_matrix->item; + + fb = esp_camera_fb_get(); + if (!fb) { + Serial.println("Camera capture failed"); + dl_matrix3du_free(image_matrix); + return; + } + + if (g_state == START_ENROLL) + { + fmt2rgb888(fb->buf, fb->len, fb->format, out_res.image); + out_res.net_boxes = face_detect(image_matrix, &mtmn_config); + + if (out_res.net_boxes) + { + if (align_face(out_res.net_boxes, image_matrix, aligned_face) == ESP_OK) + { + out_res.face_id = get_face_id(aligned_face); + + if (out_res.face_id && out_res.face_id->item) { + bool valid_face = false; + for (int i = 0; i < FACE_ID_SIZE; i++) { + float val = out_res.face_id->item[i]; + if (!isnan(val) && fabs(val) > 1e-5) { + valid_face = true; + break; + } + } + + if (valid_face) { + static int last_enroll_count = -1; + int left_sample_face = do_enrollment(&st_face_list, out_res.face_id); + + if (left_sample_face != last_enroll_count) { + last_enroll_count = left_sample_face; + char enrolling_message[64]; + sprintf(enrolling_message, "SAMPLE NUMBER %d FOR %s", ENROLL_CONFIRM_TIMES - left_sample_face, st_name.enroll_name); + send_feedback(enrolling_message); + } + + if (left_sample_face == 0) { + g_state = START_STREAM; + last_enroll_count = -1; + digitalWrite(flash_pin, LOW); + + // Reload daftar wajah untuk cek jumlah + read_face_id_from_flash_with_name(&st_face_list); + + send_feedback("capture:done"); // beri tahu website bahwa capture selesai + send_face_list(activeClient); + + if (st_face_list.count == 1) { + Serial.println("Waiting for newpin..."); + } + + } + + } else { + send_feedback("FACE DATA INVALID, PLEASE RETRY"); + Serial.println("Face ID tidak valid: seluruh vektor nol/NaN"); + } + + dl_matrix3d_free(out_res.face_id); + } else { + send_feedback("FAILED TO GET FACE ID"); + Serial.println("Face ID pointer null atau kosong"); + } + } + else + { + send_feedback("FACE ALIGNMENT FAILED"); + Serial.println("align_face gagal"); + } + } + + if (out_res.net_boxes) + free(out_res.net_boxes); + } + + if (g_state == START_RECOGNITION && recognition_active) { + fmt2rgb888(fb->buf, fb->len, fb->format, out_res.image); + out_res.net_boxes = face_detect(image_matrix, &mtmn_config); + + if (out_res.net_boxes) { + if (align_face(out_res.net_boxes, image_matrix, aligned_face) == ESP_OK) { + out_res.face_id = get_face_id(aligned_face); + + if (out_res.face_id && st_face_list.count > 0) { + face_id_node *f = recognize_face_with_name(&st_face_list, out_res.face_id); + + if (f) { + Serial.printf("DIKENALI: %s\n", f->id_name); + char recognised_message[64]; + sprintf(recognised_message, "DOOR OPEN FOR %s", f->id_name); + send_feedback(recognised_message); + g_state = START_STREAM; + recognition_active = false; + digitalWrite(flash_pin, LOW); + } else if (millis() - recognition_start_time >= 5000) { + Serial.println("TAKKENAL"); + send_feedback("FACE NOT RECOGNISED"); + g_state = START_STREAM; + recognition_active = false; + digitalWrite(flash_pin, LOW); + } + } else if (millis() - recognition_start_time >= 5000) { + Serial.println("TAKKENAL"); + send_feedback("FACE NOT RECOGNISED"); + g_state = START_STREAM; + recognition_active = false; + digitalWrite(flash_pin, LOW); + } + + if (out_res.face_id) { + dl_matrix3d_free(out_res.face_id); + } + } else if (millis() - recognition_start_time >= 5000) { + Serial.println("TAKADA"); + send_feedback("NO FACE DETECTED"); + g_state = START_STREAM; + recognition_active = false; + digitalWrite(flash_pin, LOW); + } + + free(out_res.net_boxes); + } else if (millis() - recognition_start_time >= 5000) { + Serial.println("TAKADA"); + send_feedback("NO FACE DETECTED"); + g_state = START_STREAM; + recognition_active = false; + digitalWrite(flash_pin, LOW); + } + } + + if (g_state == START_DETECT && detect_active) { + fmt2rgb888(fb->buf, fb->len, fb->format, out_res.image); + out_res.net_boxes = face_detect(image_matrix, &mtmn_config); + + if (out_res.net_boxes) { + Serial.println("Wajah Terdeteksi"); + send_feedback("FACE DETECTED"); + g_state = START_STREAM; + detect_active = false; + digitalWrite(flash_pin, LOW); + } + else { + if (millis() - detect_start_time >= 5000) { + Serial.println("Tidak ada wajah"); + send_feedback("FACE NOT RECOGNISED"); + g_state = START_STREAM; + detect_active = false; + digitalWrite(flash_pin, LOW); + } + } + } + + if (activeClient.available()) { + activeClient.sendBinary((const char *)fb->buf, fb->len); + } + + esp_camera_fb_return(fb); + fb = NULL; + dl_matrix3du_free(image_matrix); +} + +void print_all_faces() { + face_id_node *head = st_face_list.head; + int index = 0; + + while (head) { + Serial.printf("[%d] Nama: ", index++); + + // Tampilkan karakter satu per satu secara heksadesimal dan normal + for (int i = 0; i < ENROLL_NAME_LEN; i++) { + char c = head->id_name[i]; + if (c == '\0') break; + if (c < 32 || c > 126) { + Serial.printf("[0x%02X]", c); // karakter non-printable + } else { + Serial.print(c); // karakter biasa + } + } + Serial.println(); + head = head->next; + } + + if (index == 0) { + Serial.println("Tidak ada wajah yang tersimpan."); + } +} + +void wipe_all_faces_manual() { + face_id_node *f = st_face_list.head; + while (f != NULL) { + delete_face_id_in_flash_with_name(&st_face_list, f->id_name); + f = f->next; + } + face_id_name_init(&st_face_list, FACE_ID_SAVE_NUMBER, ENROLL_CONFIRM_TIMES); + Serial.println("Semua data wajah dihapus manual."); +} + +void remove_corrupted_faces() { + int corrupt_count = 0; + bool found_corrupt; + + do { + found_corrupt = false; + + face_id_node *current = st_face_list.head; + while (current != NULL) { + bool corrupt = true; + + // Cek semua byte = 0xFF + for (int i = 0; i < ENROLL_NAME_LEN; i++) { + if ((uint8_t)current->id_name[i] != 0xFF) { + corrupt = false; + break; + } + } + + // Cek juga karakter ASCII printable + if (!corrupt) { + for (int i = 0; i < ENROLL_NAME_LEN; i++) { + char c = current->id_name[i]; + if (c == '\0') break; + if (c < 32 || c > 126) { + corrupt = true; + break; + } + } + } + + if (corrupt) { + Serial.print("Menghapus wajah korup: "); + for (int i = 0; i < ENROLL_NAME_LEN; i++) { + Serial.printf("[0x%02X]", (uint8_t)current->id_name[i]); + } + Serial.println(); + + delete_face_id_in_flash_with_name(&st_face_list, current->id_name); + corrupt_count++; + + // Reload list wajah dan mulai ulang dari head + read_face_id_from_flash_with_name(&st_face_list); + found_corrupt = true; + break; + } + + current = current->next; + } + + } while (found_corrupt); + + Serial.printf("Total wajah korup yang dihapus: %d\n", corrupt_count); +} + + + +void setup() { + Serial.begin(9600); + Serial.setDebugOutput(true); + + camera_config_t config; + config.ledc_channel = LEDC_CHANNEL_0; + config.ledc_timer = LEDC_TIMER_0; + config.pin_d0 = Y2_GPIO_NUM; + config.pin_d1 = Y3_GPIO_NUM; + config.pin_d2 = Y4_GPIO_NUM; + config.pin_d3 = Y5_GPIO_NUM; + config.pin_d4 = Y6_GPIO_NUM; + config.pin_d5 = Y7_GPIO_NUM; + config.pin_d6 = Y8_GPIO_NUM; + config.pin_d7 = Y9_GPIO_NUM; + config.pin_xclk = XCLK_GPIO_NUM; + config.pin_pclk = PCLK_GPIO_NUM; + config.pin_vsync = VSYNC_GPIO_NUM; + config.pin_href = HREF_GPIO_NUM; + config.pin_sscb_sda = SIOD_GPIO_NUM; + config.pin_sscb_scl = SIOC_GPIO_NUM; + config.pin_pwdn = PWDN_GPIO_NUM; + config.pin_reset = RESET_GPIO_NUM; + config.xclk_freq_hz = 20000000; + config.pixel_format = PIXFORMAT_JPEG; + + if (psramFound()) { + config.frame_size = FRAMESIZE_UXGA; + config.jpeg_quality = 10; + config.fb_count = 2; + } else { + config.frame_size = FRAMESIZE_SVGA; + config.jpeg_quality = 12; + config.fb_count = 1; + } + + esp_err_t err = esp_camera_init(&config); + if (err != ESP_OK) { + Serial.printf("Camera init failed with error 0x%x", err); + return; + } + + sensor_t * s = esp_camera_sensor_get(); + s->set_framesize(s, FRAMESIZE_QVGA); + s->set_vflip(s, 1); + s->set_hmirror(s, 1); + pinMode(flash_pin, OUTPUT); + digitalWrite(flash_pin, LOW); + + WiFi.begin(ssid, password); + unsigned long startAttemptTime = millis(); + const unsigned long wifiTimeout = 10000; + + while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < wifiTimeout) { + delay(500); + Serial.println("MENGHUBUNGKAN WIFI..."); + } + + if (WiFi.status() == WL_CONNECTED) { + Serial.println("READY"); + Serial.println(WiFi.localIP()); + } else { + Serial.println("WIFI NOT CONNECTED"); + } + + // cek apakah ada data yang tidak valid + read_face_id_from_flash_with_name(&st_face_list); + remove_corrupted_faces(); + + if (st_face_list.count == 0) { + delay(1200); + Serial.println("resetpin"); + } + + app_httpserver_init(); + app_facenet_main(); + socket_server.listen(82); + g_state = START_STREAM; + +} + +void loop() { + handle_serial(); + handle_camera(); + handle_websocket(); +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..8d97336 --- /dev/null +++ b/index.html @@ -0,0 +1,725 @@ + + + + + + + Brankas-Q + + + + + + +
+
+
+
+ + +
+
+ + +
+ + +
+
+ +
+
+

Captured Faces

+ +
+ + + + + + + + + + + +
NameGenderRegisteredAction
+
+ +
+

New User

+

Setelah mengisi formulir, arahkan wajah Anda ke kamera lalu klik "Enroll Face"

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + + + + + + + \ No newline at end of file