Upload files to "/"
This commit is contained in:
commit
4324d820b1
|
@ -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();
|
||||
}
|
|
@ -0,0 +1,659 @@
|
|||
#include <ArduinoWebsockets.h>
|
||||
#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();
|
||||
}
|
|
@ -0,0 +1,725 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Brankas-Q</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
#stream {
|
||||
width: 320px;
|
||||
height: 240px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background-color: white;
|
||||
padding: 1rem 2rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.navbar h1 {
|
||||
margin: 0;
|
||||
color: #2f6aa3;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 2rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.left-panel,
|
||||
.right-panel {
|
||||
background: white;
|
||||
padding: 1rem;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
flex: 1;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
flex: 2;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 240px;
|
||||
aspect-ratio: 4 / 3;
|
||||
background-color: #e0e0e0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stream-row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stream-row button {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 0.25rem 0;
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.stream-btn {
|
||||
background-color: #b52b2b;
|
||||
}
|
||||
|
||||
.detect-btn {
|
||||
background-color: #2e8b57;
|
||||
}
|
||||
|
||||
.recognise-btn {
|
||||
background-color: #4f2e8b;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
background-color: #6a2626;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.status-btn {
|
||||
background-color: #2d7f9c;
|
||||
padding: 1rem 1rem;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.header-row h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
background-color: #2e8b57;
|
||||
padding: 0.5rem 1rem;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
width: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table th,
|
||||
table td {
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.action-icons {
|
||||
cursor: pointer;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.right-panel.form-panel h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.right-panel.form-panel p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.form-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.form-panel.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
padding: 0.4rem;
|
||||
margin-top: 0.25rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
justify-content: space-between;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background-color: #e0e0e0;
|
||||
flex: 1;
|
||||
color: #2e2e2e;
|
||||
}
|
||||
|
||||
.enroll-btn {
|
||||
background-color: #2e8b57;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status-flash-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.status-btn {
|
||||
flex: 9;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.flash-btn {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
background-color: #e0e0e0;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.form-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.stream-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="navbar">
|
||||
<img src="https://i.imghippo.com/files/lw7907Ks.png" alt="" border="0" style="height: 35px;">
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="left-panel">
|
||||
<div class="image-container" id="stream-container"> <img id="stream" src=""> </div>
|
||||
<div class="status-flash-row">
|
||||
<button id="status-display" class="status-btn">
|
||||
<span id="current-status"></span>
|
||||
</button>
|
||||
<button id="flash-button" class="flash-btn">💡</button>
|
||||
</div>
|
||||
<div class="stream-row">
|
||||
<button class="stream-btn" id="button-stream">STREAM CAMERA</button>
|
||||
<button class="detect-btn" id="button-detect">DETECT FACES</button>
|
||||
</div>
|
||||
<button class="recognise-btn" id="button-recognise">RECOGNIZE</button>
|
||||
<button class="delete-btn" id="button-delete-all">DELETE ALL FACES</button>
|
||||
<div id="face-list" style="margin-top: 10px; color: #333;"></div>
|
||||
</div>
|
||||
|
||||
<div class="right-panel" id="user-panel">
|
||||
<div class="header-row">
|
||||
<h2>Captured Faces</h2>
|
||||
<button class="add-btn" onclick="showForm()">Add User</button>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Gender</th>
|
||||
<th>Registered</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="userTable">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="right-panel form-panel" id="form-panel">
|
||||
<h2>New User</h2>
|
||||
<p>Setelah mengisi formulir, arahkan wajah Anda ke kamera lalu klik "Enroll Face"</p>
|
||||
<div class="form-group">
|
||||
<label>Name <span class="required">*</span></label>
|
||||
<input id="name" type="text" placeholder="Enter your name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Email <span class="required">*</span></label>
|
||||
<input id="email" type="email" placeholder="Enter your email" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Age <span class="required">*</span></label>
|
||||
<input id="age" type="number" placeholder="Enter your age" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Gender <span class="required">*</span></label>
|
||||
<select id="gender" required>
|
||||
<option value="">Choose an option</option>
|
||||
<option value="Male">Male</option>
|
||||
<option value="Female">Female</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Phone <span class="required">*</span></label>
|
||||
<input id="phone" type="tel" placeholder="xxxx xxxx xxxx" required>
|
||||
</div>
|
||||
<div class="form-buttons">
|
||||
<button class="cancel-btn" onclick="cancelForm()">Cancel</button>
|
||||
<button class="enroll-btn" id="button-capture">Enroll Face</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://www.gstatic.com/firebasejs/9.22.2/firebase-app-compat.js"></script>
|
||||
<script src="https://www.gstatic.com/firebasejs/9.22.2/firebase-firestore-compat.js"></script>
|
||||
<script>
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyDAulqgtU760t6_Ki8gBbvPmsqumgFeYco",
|
||||
authDomain: "brankas-q1.firebaseapp.com",
|
||||
projectId: "brankas-q1",
|
||||
storageBucket: "brankas-q1.appspot.com",
|
||||
messagingSenderId: "194299707390",
|
||||
appId: "1:194299707390:web:da93f6642093d5791ecaaf",
|
||||
measurementId: "G-QJ2SKQZN80"
|
||||
};
|
||||
firebase.initializeApp(firebaseConfig);
|
||||
const db = firebase.firestore();
|
||||
let isFirstUser = false;
|
||||
db.collection("users").onSnapshot((snapshot) => {
|
||||
const table = document.getElementById("userTable");
|
||||
table.innerHTML = "";
|
||||
isFirstUser = snapshot.empty;
|
||||
|
||||
// 👉 Cek jumlah user
|
||||
const userCount = snapshot.size;
|
||||
|
||||
// 👉 Atur tombol Add User
|
||||
const addButton = document.querySelector(".add-btn");
|
||||
if (userCount >= 2) {
|
||||
addButton.disabled = true;
|
||||
addButton.style.opacity = 0.5;
|
||||
addButton.style.cursor = "not-allowed";
|
||||
addButton.title = "Maksimal 2 pengguna terdaftar.";
|
||||
} else {
|
||||
addButton.disabled = false;
|
||||
addButton.style.opacity = 1;
|
||||
addButton.style.cursor = "pointer";
|
||||
addButton.title = "";
|
||||
}
|
||||
|
||||
snapshot.forEach((doc) => {
|
||||
const user = doc.data();
|
||||
const row = document.createElement("tr");
|
||||
row.innerHTML = `
|
||||
<td>${user.name}</td>
|
||||
<td>${user.gender}</td>
|
||||
<td>${user.registered}</td>
|
||||
<td>
|
||||
<span class="action-icons" onclick="editUser('${doc.id}')">📝</span>
|
||||
<span class="action-icons" onclick="deleteUser('${doc.id}')">🚫</span>
|
||||
</td>
|
||||
`;
|
||||
table.appendChild(row);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
let waitingForPIN = false;
|
||||
let pendingUserData = null;
|
||||
|
||||
|
||||
const view = document.getElementById("stream");
|
||||
var baseHost = document.location.origin;
|
||||
const WS_URL = "ws://" + window.location.hostname + ":82";
|
||||
const ws = new WebSocket(WS_URL);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log("WebSocket connected to", WS_URL);
|
||||
document.getElementById("current-status").textContent = "CONNECTED";
|
||||
ws.send("getfacelist");
|
||||
};
|
||||
ws.onclose = () => {
|
||||
console.log("WebSocket closed");
|
||||
document.getElementById("current-status").textContent = "DISCONNECTED";
|
||||
};
|
||||
ws.onerror = (err) => {
|
||||
console.error("WebSocket error:", err);
|
||||
document.getElementById("current-status").textContent = "CONNECTION ERROR";
|
||||
};
|
||||
window.ws = ws;
|
||||
|
||||
let justSetPIN = false;
|
||||
ws.onmessage = async (event) => {
|
||||
let message;
|
||||
|
||||
if (typeof event.data === "string") {
|
||||
message = event.data;
|
||||
} else if (event.data instanceof Blob) {
|
||||
const urlObject = URL.createObjectURL(event.data);
|
||||
view.src = urlObject;
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.startsWith("listface:")) {
|
||||
const recognizedName = message.substring(9);
|
||||
document.getElementById("current-status").textContent = `${recognizedName} face recognized`;
|
||||
} else if (message === "capture:done") {
|
||||
if (isFirstUser && pendingUserData) {
|
||||
try {
|
||||
await db.collection("users").add(pendingUserData);
|
||||
console.log("Data user pertama disimpan ke Firebase.");
|
||||
pendingUserData = null;
|
||||
isFirstUser = false;
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
} catch (err) {
|
||||
console.error("Gagal simpan data pertama:", err);
|
||||
alert("Gagal menyimpan data ke Firebase!");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// User kedua dan seterusnya: tunggu getpin, lalu baru simpan user
|
||||
if (pendingUserData) {
|
||||
ws.send("getpin");
|
||||
waitingForPIN = true;
|
||||
}
|
||||
|
||||
} else if (message === "neednewpin") {
|
||||
openPINModal();
|
||||
} else if (message.startsWith("pin:") && waitingForPIN) {
|
||||
waitingForPIN = false;
|
||||
const pin = message.substring(4);
|
||||
alert("PIN brankas saat ini: " + pin);
|
||||
|
||||
if (!pendingUserData) {
|
||||
console.warn("PIN diterima tapi tidak ada data user tertunda.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await db.collection("users").add(pendingUserData);
|
||||
console.log("Data user disimpan ke Firebase setelah menerima PIN.");
|
||||
} catch (err) {
|
||||
console.error("Gagal simpan ke Firebase setelah PIN:", err);
|
||||
alert("Gagal menyimpan data ke Firebase!");
|
||||
}
|
||||
pendingUserData = null;
|
||||
|
||||
if (justSetPIN) {
|
||||
justSetPIN = false;
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
|
||||
} else if (message === "resetpin") {
|
||||
const newPIN = prompt("Semua wajah telah dihapus. Masukkan PIN baru untuk brankas:");
|
||||
if (newPIN && newPIN.length === 6 && /^\d+$/.test(newPIN)) {
|
||||
ws.send("setpin:" + newPIN);
|
||||
alert("PIN baru berhasil disimpan.");
|
||||
} else {
|
||||
alert("PIN tidak valid. Harus 6 digit angka.");
|
||||
}
|
||||
} else if (message.startsWith("facelist:")) {
|
||||
const names = message.substring(9).split(",").filter(n => n.trim());
|
||||
const faceListDiv = document.getElementById("face-list");
|
||||
if (names.length === 0) {
|
||||
faceListDiv.innerHTML = "<strong>Tidak ada wajah terdaftar.</strong>";
|
||||
} else {
|
||||
faceListDiv.innerHTML = "<strong>Wajah Terdaftar:</strong><ul>" +
|
||||
names.map(n => `<li>${n}</li>`).join("") + "</ul>";
|
||||
}
|
||||
} else {
|
||||
document.getElementById("current-status").textContent = message;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const streamButton = document.getElementById("button-stream");
|
||||
const detectButton = document.getElementById("button-detect");
|
||||
const captureButton = document.getElementById("button-capture");
|
||||
const recogniseButton = document.getElementById("button-recognise");
|
||||
const deleteAllButton = document.getElementById("button-delete-all");
|
||||
|
||||
a = new AudioContext();
|
||||
function alertSound(w, x, y) {
|
||||
v = a.createOscillator();
|
||||
u = a.createGain();
|
||||
v.connect(u);
|
||||
v.frequency.value = x;
|
||||
v.type = "square";
|
||||
u.connect(a.destination);
|
||||
u.gain.value = w * 0.01;
|
||||
v.start(a.currentTime);
|
||||
v.stop(a.currentTime + y * 0.001);
|
||||
}
|
||||
|
||||
let editingUserId = null;
|
||||
captureButton.onclick = async () => {
|
||||
if (waitingForPIN) {
|
||||
// alert("Sedang menunggu respons dari ESP32-CAM...");
|
||||
return;
|
||||
}
|
||||
const name = document.getElementById("name").value;
|
||||
const email = document.getElementById("email").value;
|
||||
const age = document.getElementById("age").value;
|
||||
const gender = document.getElementById("gender").value;
|
||||
const phone = document.getElementById("phone").value;
|
||||
|
||||
if (!name || !email || !age || !gender || !phone) {
|
||||
alert("Semua field wajib diisi!");
|
||||
return;
|
||||
}
|
||||
const timestamp = new Date().toLocaleString("id-ID", {
|
||||
day: "2-digit", month: "2-digit", year: "numeric",
|
||||
hour: "2-digit", minute: "2-digit", hour12: false
|
||||
});
|
||||
const userData = { name, email, age: Number(age), gender, phone, registered: timestamp };
|
||||
|
||||
if (isFirstUser) {
|
||||
pendingUserData = userData;
|
||||
openPINModal();
|
||||
} else {
|
||||
pendingUserData = userData;
|
||||
waitingForPIN = true;
|
||||
ws.send("capture:" + name);
|
||||
// alert("Tunggu proses perekaman wajah dari ESP32-CAM...");
|
||||
}
|
||||
|
||||
};
|
||||
streamButton.onclick = () => {
|
||||
ws.send("stream");
|
||||
};
|
||||
detectButton.onclick = () => {
|
||||
ws.send("detect");
|
||||
};
|
||||
recogniseButton.onclick = () => {
|
||||
ws.send("recognise");
|
||||
};
|
||||
deleteAllButton.onclick = () => {
|
||||
if (confirm("Yakin ingin menghapus semua wajah dari ESP32-CAM?")) {
|
||||
ws.send("delete:allfaces");
|
||||
}
|
||||
};
|
||||
|
||||
let flashOn = false;
|
||||
document.getElementById("flash-button").addEventListener("click", function () {
|
||||
flashOn = !flashOn;
|
||||
const message = flashOn ? "flash:on" : "flash:off";
|
||||
ws.send(message);
|
||||
this.style.backgroundColor = flashOn ? "#f1c40f" : "#bbbbbb";
|
||||
});
|
||||
|
||||
function showForm() {
|
||||
const addButton = document.querySelector(".add-btn");
|
||||
if (addButton.disabled) {
|
||||
alert("Maksimal 2 pengguna saja yang dapat didaftarkan.");
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById("user-panel").style.display = "none";
|
||||
document.getElementById("form-panel").classList.add("active");
|
||||
document.getElementById("name").value = "";
|
||||
document.getElementById("email").value = "";
|
||||
document.getElementById("age").value = "";
|
||||
document.getElementById("gender").value = "";
|
||||
document.getElementById("phone").value = "";
|
||||
document.getElementById("name").readOnly = false;
|
||||
document.querySelector("#form-panel h2").textContent = "New User";
|
||||
document.querySelector("#form-panel p").textContent = "Setelah mengisi formulir, arahkan wajah Anda ke kamera lalu klik \"Enroll Face\"";
|
||||
document.getElementById("button-capture").textContent = "Enroll Face";
|
||||
editingUserId = null;
|
||||
}
|
||||
|
||||
|
||||
function cancelForm() {
|
||||
document.getElementById("user-panel").style.display = "block";
|
||||
document.getElementById("form-panel").classList.remove("active");
|
||||
pendingUserData = null;
|
||||
waitingForPIN = false;
|
||||
|
||||
const form = document.getElementById("userForm");
|
||||
if (form) {
|
||||
form.reset();
|
||||
}
|
||||
document.getElementById("name").readOnly = false;
|
||||
editingUserId = null;
|
||||
const titleEl = document.getElementById("form-title");
|
||||
if (titleEl) {
|
||||
titleEl.textContent = "New User";
|
||||
}
|
||||
const captureBtn = document.getElementById("button-capture");
|
||||
if (captureBtn) {
|
||||
captureBtn.textContent = "Enroll Face";
|
||||
}
|
||||
}
|
||||
|
||||
async function editUser(id) {
|
||||
const doc = await db.collection("users").doc(id).get();
|
||||
if (!doc.exists) return alert("User tidak ditemukan");
|
||||
const data = doc.data();
|
||||
document.getElementById("name").value = data.name;
|
||||
document.getElementById("email").value = data.email;
|
||||
document.getElementById("age").value = data.age;
|
||||
document.getElementById("gender").value = data.gender;
|
||||
document.getElementById("phone").value = data.phone;
|
||||
document.getElementById("name").readOnly = true;
|
||||
editingUserId = id;
|
||||
document.getElementById("user-panel").style.display = "none";
|
||||
document.getElementById("form-panel").classList.add("active");
|
||||
document.querySelector("#form-panel h2").textContent = "Edit User";
|
||||
document.querySelector("#form-panel p").textContent = "Ubah data pengguna lalu klik 'Save'.";
|
||||
const captureBtn = document.getElementById("button-capture");
|
||||
captureBtn.textContent = "Save";
|
||||
}
|
||||
|
||||
async function deleteUser(id) {
|
||||
if (confirm("Hapus user ini dari sistem?")) {
|
||||
try {
|
||||
const userDoc = await db.collection("users").doc(id).get();
|
||||
if (!userDoc.exists) {
|
||||
alert("Data user tidak ditemukan.");
|
||||
return;
|
||||
}
|
||||
const userData = userDoc.data();
|
||||
const person_name = userData.name;
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||
alert("ESP32-CAM tidak terhubung. Tidak dapat menghapus wajah.");
|
||||
return;
|
||||
}
|
||||
const deleteFromESP32 = new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject("Timeout dari ESP32-CAM");
|
||||
}, 3000);
|
||||
function handleWSResponse(event) {
|
||||
if (event.data === "remove:ok") {
|
||||
clearTimeout(timeout);
|
||||
ws.removeEventListener("message", handleWSResponse);
|
||||
resolve();
|
||||
} else if (event.data.startsWith("remove:error")) {
|
||||
clearTimeout(timeout);
|
||||
ws.removeEventListener("message", handleWSResponse);
|
||||
reject("ESP32-CAM gagal hapus wajah.");
|
||||
}
|
||||
}
|
||||
ws.addEventListener("message", handleWSResponse);
|
||||
ws.send("remove:" + person_name);
|
||||
});
|
||||
await deleteFromESP32;
|
||||
await db.collection("users").doc(id).delete();
|
||||
alert("User berhasil dihapus.");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert("Gagal menghapus user: " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openPINModal() {
|
||||
document.getElementById("pinModal").style.display = "flex";
|
||||
}
|
||||
|
||||
function closePINModal() {
|
||||
document.getElementById("pinModal").style.display = "none";
|
||||
}
|
||||
|
||||
function submitPIN() {
|
||||
const pin = document.getElementById("newPIN").value.trim();
|
||||
if (pin.length !== 6 || !/^\d+$/.test(pin)) {
|
||||
alert("PIN harus 6 digit angka.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pendingUserData) {
|
||||
alert("Data user belum lengkap. Silakan ulangi.");
|
||||
return;
|
||||
}
|
||||
|
||||
ws.send("newpin:" + pin);
|
||||
justSetPIN = true;
|
||||
closePINModal();
|
||||
|
||||
ws.send("capture:" + pendingUserData.name);
|
||||
waitingForPIN = true;
|
||||
alert("Tunggu proses perekaman wajah dari ESP32-CAM...");
|
||||
}
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
<div id="pinModal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%;
|
||||
background-color: rgba(0,0,0,0.5); align-items:center; justify-content:center; z-index:9999;">
|
||||
<div style="background:white; padding:20px; border-radius:10px; max-width:300px; width:90%; text-align:center;">
|
||||
<h3>Masukkan PIN Brankas</h3>
|
||||
<input type="password" id="newPIN" maxlength="6" placeholder="6 digit angka"
|
||||
style="padding:10px; width:90%; font-size:16px; margin-top:10px;" />
|
||||
<div style="margin-top:15px;">
|
||||
<button onclick="submitPIN()"
|
||||
style="padding:10px 20px; background:#2e8b57; color:white; border:none; border-radius:5px;">Simpan</button>
|
||||
<button onclick="closePINModal()"
|
||||
style="padding:10px 20px; background:#ccc; color:black; border:none; border-radius:5px;">Batal</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
Loading…
Reference in New Issue