main.ino INO FILE
#include <WiFi.h>
#include <HTTPClient.h>
#include <Adafruit_Fingerprint.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Fonts/FreeSansBold12pt7b.h> // Font kustom
#include <Fonts/FreeSansBold9pt7b.h> // Font kustom
#include <Fonts/FreeSerif9pt7b.h> // Font kustom
#include <Fonts/FreeSans9pt7b.h> // Font kustom
#include <NTPClient.h>
#include <WiFiUdp.h> 
#include <Fonts/TomThumb.h>  // Menyertakan font TomThumb

#define MODEM_RX 16
#define MODEM_TX 17
#define mySerial Serial2 // gunakan untuk ESP32

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64

#define BUZZER_PIN 5 // Pin untuk buzzer
#define LED_PIN 18 // Pin untuk buzzer

Adafruit_Fingerprint finger = Adafruit_Fingerprint(&mySerial);
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

// Tambahkan untuk NTP Client
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org", 25200, 60000); // GMT+7 (25200 detik)

// Struktur data untuk menyimpan ID dan nama
struct FingerprintData {
  uint16_t id;
  const char* name;
};

// Daftar ID dan nama
FingerprintData userData[] = {
  {1, "innoiti"},
  {2, "lorem"},
  {3, "ipsum"},
  {4, "dolor"},
  {6, "Siroj"},
  {7, "Naufal"},
  {8, "Shofia"},
  {9, "Ryan"},
  {10, "Juli"},
  {11, "Zamroni"},
  {12, "Ibad"},
  {13, "Fani"},
  {14, "Kipli"},
  {15, ""},
  {16, ""},
  {17, ""}

};

const int userCount = sizeof(userData) / sizeof(userData[0]); // Hitung jumlah data

// Informasi Wi-Fi
const char* ssid = "KASIR"; // Ganti dengan nama Wi-Fi
const char* password = "tentukan"; // Ganti dengan password Wi-Fi
const char* serverName = "http://zidcreative.com/innoiti/cust/mastercust_3/api/fingerprint.php"; // URL PHP server

// Variabel untuk menyimpan waktu absensi terakhir
unsigned long lastAttendanceTime[130] = {0}; // Menyimpan waktu absensi per ID pengguna

int lastWifiUpdate = 0;
const int wifiUpdateInterval = 5000; // Update setiap 5 detik

int dailyAttendanceCount = 0; // Variabel untuk menyimpan jumlah absen hari ini
unsigned long currentAttendanceDay = 0; // Variabel untuk menyimpan hari terakhir absens

float currentSpeedKBps = 0; 
unsigned long lastSpeedTestTime = 0;
const long speedTestInterval = 10000; // Ukur kecepatan setiap 10 detik

void setup() {
  Serial.begin(9600);
  while (!Serial);
  delay(100);
  Serial.println("\n\nzidCreative Innoiti TF 1 Verifikasi");

  // Koneksi Wi-Fi
  Serial.print("Menghubungkan ke Wi-Fi");
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWi-Fi Terhubung!");
  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());

  // Mulai komunikasi dengan sensor sidik jari
  finger.begin(57600);
  if (finger.verifyPassword()) {
    Serial.println("Sensor sidik jari ditemukan!");
  } else {
    Serial.println("Sensor sidik jari tidak ditemukan :(");
    while (1) { delay(1); }
  }

  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println(F("SSD1306 allocation failed"));
    for (;;);
  }
  display.clearDisplay();
  display.setFont(&FreeSansBold12pt7b); // Menggunakan font kustom
  display.setTextColor(WHITE);
  display.setCursor(5, 20);
  display.println("Innoiti");
  display.setCursor(5, 45);
  display.println("FT1");
  display.display();

  pinMode(BUZZER_PIN, OUTPUT);
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(BUZZER_PIN, LOW);
  digitalWrite(LED_PIN, LOW);

  timeClient.begin();
}

void loop() {
  measureNetworkSpeed();

  // Tunggu sidik jari
  uint8_t result = getFingerprintID();
  if (result == FINGERPRINT_OK) {
    handleAttendance(); // Tangani absensi
  } else if (result == FINGERPRINT_NOTFOUND) {
    displayError("Siapa Ya?");
    playErrorSound();
  } else {
    displayTime();
  }
  delay(1000); // Tunggu 1 detik sebelum mencoba lagi
}

void handleAttendance() {
  int userID = finger.fingerID;
  unsigned long currentTime = timeClient.getEpochTime();
  unsigned long currentDay = currentTime / 86400; // Hari sejak 1 Jan 1970

  // Reset counter jika hari baru
  if (currentDay != currentAttendanceDay) {
    dailyAttendanceCount = 0;
    currentAttendanceDay = currentDay;
  }

  // Proses absensi
  dailyAttendanceCount++; // Tambah counter
  lastAttendanceTime[userID] = currentDay;
  displayFingerprintID();
  playSuccessSound();

}

// Fungsi untuk menampilkan pesan kesalahan
void displayError(String errorMsg) {
  display.clearDisplay();
  display.setFont(&FreeSansBold12pt7b);
  display.setTextColor(WHITE);

  display.setCursor(0, 40);
  display.println(errorMsg);
  display.display();
}

uint8_t getFingerprintID() {
  uint8_t p = finger.getImage();
  if (p != FINGERPRINT_OK) {
    if (p == FINGERPRINT_NOFINGER) {
      Serial.println("Tidak ada jari yang terdeteksi.");
    }
    return p;
  }

  p = finger.image2Tz();
  if (p != FINGERPRINT_OK) {
    Serial.println("Gagal mengonversi gambar menjadi template.");
    return p;
  }

  p = finger.fingerFastSearch();
  if (p == FINGERPRINT_OK) {
    Serial.print("Sidik jari ditemukan dengan ID: ");
    Serial.println(finger.fingerID);
    Serial.print("Confidence: ");
    Serial.println(finger.confidence);
  }
  return p;
}

const char* getNameByID(uint16_t id) {
  for (int i = 0; i < userCount; i++) {
    if (userData[i].id == id) {
      return userData[i].name;
    }
  }
  return "-";
}

void displayFingerprintID() {
  displayProcessing(); // Tampilkan animasi processing
  
  const char* userName = getNameByID(finger.fingerID);
  sendData(finger.fingerID, userName);

  Serial.print("ID: ");
  Serial.print(finger.fingerID);
  Serial.print(", Nama: ");
  Serial.println(userName);
}

void sendData(uint16_t id_jari, const char* name) {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("WiFi tidak terhubung! Mencoba reconnect...");
    WiFi.disconnect();
    WiFi.reconnect();
    delay(5000); // Tunggu 5 detik sebelum mencoba lagi
    if (WiFi.status() != WL_CONNECTED) {
      Serial.println("WiFi gagal terhubung. Data tidak dikirim.");
      displayError("WiFi Error!");
      return;
    }
  }

  HTTPClient http;
  http.begin(serverName);
  http.addHeader("Content-Type", "application/x-www-form-urlencoded");

  String postData = "id_jari=" + String(id_jari) + "&name=" + String(name);
  
  // Kirim data hanya 1 kali
  int httpResponseCode = http.POST(postData);
  
  if (httpResponseCode > 0) {
    Serial.print("Data berhasil dikirim! HTTP Response: ");
    Serial.println(httpResponseCode);

    // Tambahkan notifikasi OLED
    const char* userName = getNameByID(finger.fingerID);
    displaySuccess(userName);
    
  } else {
    Serial.println("Gagal mengirim data.");
    displayError("restart!");
    playErrorSound();
  }

  http.end(); // Tutup HTTPClient
}

void playSuccessSound() {
  for (int i = 0; i < 2; i++) {
    digitalWrite(BUZZER_PIN, HIGH);
    digitalWrite(LED_PIN, HIGH);
    delay(100);
    digitalWrite(BUZZER_PIN, LOW);
    digitalWrite(LED_PIN, LOW);
    delay(100);
  }
}

void playErrorSound() {
  digitalWrite(BUZZER_PIN, HIGH);
  digitalWrite(LED_PIN, HIGH);
  delay(1000);
  digitalWrite(BUZZER_PIN, LOW);
  digitalWrite(LED_PIN, LOW);
}

void displayTime() {
  display.clearDisplay(); // Bersihkan layar setiap loop

  timeClient.update();
  String formattedTime = timeClient.getFormattedTime();

  // 1. Tampilkan teks "Granit Cafe" (PASTIKAN FONT DAN POSISI KONSISTEN)
  display.setFont(&FreeSerif9pt7b); // Gunakan font yang sama setiap kali
  display.setTextColor(WHITE); // Pastikan warna teks putih
  display.setCursor(2, 14); // Koordinat HARUS tetap sama
  display.println("-  Granit Cafe  -");

  // 2. Tampilkan waktu/jam
  display.setFont(&FreeSansBold12pt7b); // Font untuk waktu
  display.setCursor(17, 42); // Posisi jam
  display.println(timeClient.getFormattedTime());

  // 3. Fungsi tambahan (indikator WiFi dan counter)
  drawWifiIndicator();
  drawAttendanceCounter();

  display.display(); // Update layar
}

void displaySuccess(const char* message) {
  display.clearDisplay();
  
  // Animasi lingkaran loading kecil
  for(int i=0; i<=360; i+=30) {
    display.clearDisplay();
    drawAnimatedCircle(64, 20, 10, i);
    display.display();
    delay(20);
  }
  
  // Gambar centang animasi
  drawAnimatedCheckmark(64, 20);
  
  // Tampilkan pesan sukses
  display.setFont(&FreeSansBold9pt7b);
  display.setTextColor(WHITE);
  
  // Hitung lebar teks untuk posisi tengah
  int16_t x1, y1;
  uint16_t w, h;
  display.getTextBounds(message, 0, 0, &x1, &y1, &w, &h);
  
  display.setCursor((SCREEN_WIDTH - w)/2, 50);
  display.println(message);
  
  // Tampilkan garis pembatas dekoratif
  display.drawFastHLine(10, 55, SCREEN_WIDTH-20, WHITE);
  
  display.display();
}

void drawAnimatedCircle(int16_t x, int16_t y, int16_t r, int16_t angle) {
  float rad = angle * PI / 180;
  int16_t x2 = x + r * cos(rad);
  int16_t y2 = y + r * sin(rad);
  display.drawLine(x, y, x2, y2, WHITE);
}

void drawAnimatedCheckmark(int16_t x, int16_t y) {
  // Gambar lingkaran latar belakang
  display.fillCircle(x, y, 12, WHITE);
  
  // Animasi centang bagian per bagian
  for(int i=0; i<=10; i++) {
    display.fillCircle(x, y, 12, WHITE);
    
    // Bagian pertama centang (garis miring ke bawah)
    int16_t x1 = x - 8;
    int16_t y1 = y;
    int16_t x2 = x - 2;
    int16_t y2 = y + 6;
    int16_t drawX2 = x1 + (x2 - x1) * i / 10;
    int16_t drawY2 = y1 + (y2 - y1) * i / 10;
    display.drawLine(x1, y1, drawX2, drawY2, BLACK);
    
    // Bagian kedua centang (garis miring ke atas)
    if(i > 5) {
      int16_t x3 = x2;
      int16_t y3 = y2;
      int16_t x4 = x + 8;
      int16_t y4 = y - 6;
      int16_t drawX4 = x3 + (x4 - x3) * (i-5) / 5;
      int16_t drawY4 = y3 + (y4 - y3) * (i-5) / 5;
      display.drawLine(x3, y3, drawX4, drawY4, BLACK);
    }
    
    display.display();
    delay(30);
  }
}

void drawWifiIndicator() {
  int rssi = WiFi.RSSI();
  int bars;
  
  if (rssi > -55) { // Sinyal sangat kuat
    bars = 4;
  } else if (rssi > -65) {
    bars = 3;
  } else if (rssi > -75) {
    bars = 2;
  } else if (rssi > -85) {
    bars = 1;
  } else { // Sinyal sangat lemah atau tidak ada
    bars = 0;
  }

  // Gambar indikator WiFi
  int x = 7;
  int y = 61;
  
  // Bersihkan area indikator sebelumnya
  display.fillRect(x, y-10, 30, 12, BLACK);
  
  // Gambar latar belakang ikon WiFi
  display.drawPixel(x, y-2, WHITE);
  display.drawPixel(x+1, y-2, WHITE);
  display.drawPixel(x+2, y-3, WHITE);
  display.drawPixel(x+3, y-3, WHITE);
  display.drawPixel(x+4, y-4, WHITE);
  display.drawPixel(x+5, y-4, WHITE);
  
  // Gambar bar sesuai kekuatan sinyal
  for (int i = 0; i < bars; i++) {
    display.fillRect(x + 8 + (i*5), y - 2 - (i*2), 3, 2 + (i*2), WHITE);
  }
}

void drawAttendanceCounter() {
  display.setFont(NULL); // Gunakan font default (5x7)
  display.setTextSize(1); // Ukuran standar
  
  // Tampilkan kecepatan jaringan (kiri)
  display.setCursor(42, 55);
  display.print(currentSpeedKBps, 1); // 1 digit desimal
  display.print("KB/s");

  // Tampilkan counter absen (kanan)
  display.setCursor(100, 55);
  display.print("A:");
  display.print(dailyAttendanceCount);
}

// ===== Fungsi baru untuk mengukur kecepatan =====
void measureNetworkSpeed() {
  if (millis() - lastSpeedTestTime >= speedTestInterval) {
    HTTPClient http;
    const char* testUrl = "http://httpbin.org/stream-bytes/1024"; // URL test 1KB
    long fileSize = 1024; // Ukuran file test dalam bytes
    
    http.begin(testUrl);
    int httpCode = http.GET();
    
    if (httpCode == HTTP_CODE_OK) {
      long startTime = millis();
      WiFiClient *stream = http.getStreamPtr();
      while (stream->available()) {
        stream->read();
      }
      long duration = millis() - startTime;
      
      if (duration > 0) {
        currentSpeedKBps = (fileSize / 1024.0) / (duration / 1000.0);
        Serial.print("Kecepatan: ");
        Serial.print(currentSpeedKBps);
        Serial.println(" KB/s");
      }
    }
    http.end();
    lastSpeedTestTime = millis();
  }
}

void displayProcessing() {
  display.clearDisplay();
  
  // Animasi loading circle (minimalis)
  static float angle = 0;
  const int centerX = SCREEN_WIDTH/2;
  const int centerY = SCREEN_HEIGHT/2 - 5;
  const int radius = 12;
  
  // Gambar lingkaran outline
  display.drawCircle(centerX, centerY, radius, WHITE);
  
  // Gambar "dot" yang berputar
  int dotX = centerX + radius * cos(angle);
  int dotY = centerY + radius * sin(angle);
  display.fillCircle(dotX, dotY, 2, WHITE);
  
  angle += 0.2; // Kecepatan rotasi
  if(angle > 2*PI) angle = 0;
  
  // Teks minimalis
  display.setFont(&FreeSansBold9pt7b);
  display.setTextColor(WHITE);
  
  // Hitung lebar teks untuk posisi tengah
  int16_t x1, y1;
  uint16_t w, h;
  display.getTextBounds("Memproses", 0, 0, &x1, &y1, &w, &h);
  display.setCursor((SCREEN_WIDTH - w)/2, centerY + radius + 20);
  display.println("Memproses");
  
  display.display();
}