TinyVen_System.ino INO FILE
// ============================================
// TINYVEN_SYSTEM_V3_ENHANCED.ino
// ============================================
// Sistem TinyVen dengan tampilan baru dan 3 LED
// FITUR KEAMANAN KOMPARTEMEN - OPTION 3 HYBRID

#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <vector>
#include <time.h>

// ================= CONFIG ===================
#define WIFI_SSID "AL-MUNAWWARAH"
#define WIFI_PASS "55557777"
#define API_BASE_URL "https://zidcreative.com/sishopint/api"
#define GET_PRESENSI_ENDPOINT API_BASE_URL "/get_latest_presensi.php"
#define LOG_HADIAH_ENDPOINT API_BASE_URL "/log_hadiah.php"
#define GET_ELIGIBLE_USERS_ENDPOINT API_BASE_URL "/get_eligible_users.php"

// NTP Server untuk waktu
#define NTP_SERVER "pool.ntp.org"
#define GMT_OFFSET_SEC 7 * 3600  // WIB (UTC+7)
#define DAYLIGHT_OFFSET_SEC 0

// Hardware Pins
#define TRIG_PIN 27
#define ECHO_PIN 26
#define SERVO_PIN 13
#define LED_COMPARTMENT_PIN 25  // LED kompartemen utama
#define LED_BLUE_PIN 18         // LED biru kanan display
#define LED_WHITE_PIN 19        // LED putih kiri display
#define BUZZER_PIN 33

// LCD I2C
#define LCD_I2C_ADDRESS 0x27
#define LCD_COLUMNS 20
#define LCD_ROWS 4
#define SDA_PIN 21
#define SCL_PIN 22

// Timing
#define POLL_INTERVAL_MS 3000
#define SERVO_PULSE_MIN 500
#define SERVO_PULSE_MAX 2500
#define DISPLAY_UPDATE_MS 1000
#define MAX_RETRY_ATTEMPTS 3
#define REWARD_DETECTION_CM 7.0  // Diubah dari 5 ke 10 cm
#define REWARD_TAKE_TIMEOUT_MS 30000
#define MONITOR_INTERVAL_MS 1000
#define SAFETY_CHECK_INTERVAL_MS 500      // Cek keamanan setiap 500ms
#define SAFETY_WARNING_DURATION_MS 5000   // Durasi peringatan 5 detik

// ================= STRUCTURES ===============
struct EligibleUser {
  int peserta_id;
  String nama;
  int total_poin;
  int threshold;
  bool already_rewarded;
  unsigned long last_presensi_time;
};

struct CurrentReward {
  int peserta_id;
  String nama;
  int total_poin;
  int threshold;
  int servo_position;
  unsigned long dispense_time;
  bool is_active;
  int attempt_count;
};

struct RewardQueueItem {
  EligibleUser user;
  unsigned long added_time;
};

// Struktur untuk monitoring keamanan
struct SafetyMonitor {
  bool warning_active;           // Status peringatan aktif
  unsigned long warning_start;   // Waktu mulai peringatan
  unsigned long last_check;      // Waktu terakhir cek sensor
  float last_distance;           // Jarak terakhir yang terbaca
  bool is_safe;                  // Status aman/tidak aman
  unsigned long last_sound;      // Waktu terakhir sound diputar
};

// ================= GLOBALS ==================
LiquidCrystal_I2C lcd(LCD_I2C_ADDRESS, LCD_COLUMNS, LCD_ROWS);

// System State
enum SystemState {
  STATE_STARTUP,
  STATE_STANDBY,
  STATE_CHECK_ELIGIBILITY,
  STATE_DISPENSING,
  STATE_WAITING_FOR_TAKE,
  STATE_RETURNING_HOME,
  STATE_REWARD_TAKEN,
  STATE_ERROR,
  STATE_WARNING
};

SystemState system_state = STATE_STARTUP;

// Data
int last_processed_id = 0;
unsigned long last_poll_time = 0;
unsigned long last_display_update = 0;
unsigned long last_monitor_time = 0;
unsigned long last_led_blink = 0;
bool wifi_connected = false;
int total_rewards_given = 0;

// Servo Control
int current_servo_position = 0;
bool compartment_loaded = true;
unsigned long servo_move_start = 0;
bool servo_is_moving = false;
int servo_target_position = 0;

// Current Processing
CurrentReward current_reward;
std::vector<RewardQueueItem> reward_queue;

// Time variables
bool time_synced = false;
struct tm timeinfo;

// LED States
bool blue_led_state = false;
bool white_led_state = false;
bool compartment_led_state = false;

// Safety Monitor
SafetyMonitor safety = {false, 0, 0, 0.0, true, 0};

// ================= FUNCTION DECLARATIONS ==========
void servoWrite(int angle);
void updateServo();
void setServoPosition(int angle);
void processServoMovement();
void connectWiFi();
bool syncNTPTime();
void updateTime();
String getFormattedTime();
String getFormattedDate();
bool checkUserEligibility(int peserta_id, int total_poin);
bool checkIfAlreadyRewarded(int peserta_id, int threshold);
void fetchEligibleUsersBatch();
void addToRewardQueue(int peserta_id, String nama, int total_poin);
void pollPresensi();
void updateDisplay();
void updateLEDs();
void startupSequence();
void playTone(int frequency, int duration);
void successSound();
void warningSound();
void errorSound();
void dispensingSound();
void waitingSound();
void compartmentWarningSound();
float readDistance();
void logRewardToServer(int peserta_id, String nama, int total_poin, int threshold, 
                       String status, int attempts, int servo_pos, float distance);
void monitorSafety();  // Fungsi monitoring keamanan

// ================= SERVO FUNCTIONS ==========
void servoWrite(int angle) {
  if (angle != 0 && angle != 180) {
    if (angle < 90) angle = 0;
    else angle = 180;
  }
  
  int pulseWidth;
  if (angle == 0) {
    pulseWidth = SERVO_PULSE_MIN;
  } else {
    pulseWidth = SERVO_PULSE_MAX;
  }
  
  digitalWrite(SERVO_PIN, HIGH);
  delayMicroseconds(pulseWidth);
  digitalWrite(SERVO_PIN, LOW);
  
  current_servo_position = angle;
  servo_target_position = angle;
}

void updateServo() {
  static unsigned long last_servo_update = 0;
  unsigned long current_time = micros();
  
  if (current_time - last_servo_update >= 20000) {
    last_servo_update = current_time;
    
    int pulseWidth;
    if (current_servo_position == 0) {
      pulseWidth = SERVO_PULSE_MIN;
    } else {
      pulseWidth = SERVO_PULSE_MAX;
    }
    
    digitalWrite(SERVO_PIN, HIGH);
    delayMicroseconds(pulseWidth);
    digitalWrite(SERVO_PIN, LOW);
  }
}

void setServoPosition(int angle) {
  if (angle != 0 && angle != 180) {
    if (angle < 90) angle = 0;
    else angle = 180;
  }
  
  servo_target_position = angle;
  servo_is_moving = true;
  servo_move_start = millis();
}

void processServoMovement() {
  if (servo_is_moving) {
    unsigned long move_time = millis() - servo_move_start;
    
    if (move_time >= 1500) {
      servo_is_moving = false;
      current_servo_position = servo_target_position;
    }
  }
  
  updateServo();
}

// ================= TIME FUNCTIONS ==========
bool syncNTPTime() {
  configTime(GMT_OFFSET_SEC, DAYLIGHT_OFFSET_SEC, NTP_SERVER);
  
  for (int i = 0; i < 10; i++) {
    delay(1000);
    if (getLocalTime(&timeinfo)) {
      time_synced = true;
      Serial.println("Time synchronized with NTP");
      return true;
    }
  }
  return false;
}

void updateTime() {
  if (!time_synced) return;
  getLocalTime(&timeinfo);
}

String getFormattedTime() {
  if (!time_synced) return "--:--:--";
  char buffer[9];
  strftime(buffer, sizeof(buffer), "%H:%M:%S", &timeinfo);
  return String(buffer);
}

String getFormattedDate() {
  if (!time_synced) return "--/--/--";
  char buffer[9];
  strftime(buffer, sizeof(buffer), "%d/%m/%y", &timeinfo);
  return String(buffer);
}

// ================= LED FUNCTIONS ==========
void updateLEDs() {
  unsigned long current_time = millis();
  
  switch(system_state) {
    case STATE_STARTUP:
      // Semua LED akan diatur di startupSequence()
      break;
      
    case STATE_STANDBY:
      compartment_led_state = false;
      // LED biru berkedip setiap 500ms
      if (current_time - last_led_blink >= 500) {
        last_led_blink = current_time;
        blue_led_state = !blue_led_state;
        white_led_state = false;
      }
      break;
      
    case STATE_CHECK_ELIGIBILITY:
      if (!safety.is_safe) {
        // Mode peringatan: semua LED blink cepat
        if (current_time - last_led_blink >= 100) {
          last_led_blink = current_time;
          blue_led_state = !blue_led_state;
          white_led_state = blue_led_state;
          compartment_led_state = blue_led_state;
        }
      } else {
        // Mode normal: biru nyala tetap
        compartment_led_state = false;
        blue_led_state = true;
        white_led_state = false;
      }
      break;
      
    case STATE_DISPENSING:
      compartment_led_state = true;   // LED kompartemen nyala
      blue_led_state = false;
      white_led_state = true;         // Putih nyala
      break;
      
    case STATE_WAITING_FOR_TAKE:
      compartment_led_state = true;   // LED kompartemen nyala
      blue_led_state = false;
      // Putih berkedip cepat
      if (current_time - last_led_blink >= 200) {
        last_led_blink = current_time;
        white_led_state = !white_led_state;
      }
      break;
      
    case STATE_RETURNING_HOME:
      compartment_led_state = false;
      blue_led_state = false;
      white_led_state = true;         // Putih nyala tetap
      break;
      
    case STATE_WARNING:
      compartment_led_state = true;   // LED kompartemen nyala
      // Biru dan putih blink bersamaan cepat
      if (current_time - last_led_blink >= 150) {
        last_led_blink = current_time;
        blue_led_state = !blue_led_state;
        white_led_state = blue_led_state;
      }
      break;
      
    case STATE_ERROR:
      // Semua LED blink bersamaan
      if (current_time - last_led_blink >= 250) {
        last_led_blink = current_time;
        blue_led_state = !blue_led_state;
        white_led_state = blue_led_state;
        compartment_led_state = blue_led_state;
      }
      break;
      
    default:
      compartment_led_state = false;
      blue_led_state = false;
      white_led_state = false;
      break;
  }
  
  // Apply LED states
  digitalWrite(LED_COMPARTMENT_PIN, compartment_led_state ? HIGH : LOW);
  digitalWrite(LED_BLUE_PIN, blue_led_state ? HIGH : LOW);
  digitalWrite(LED_WHITE_PIN, white_led_state ? HIGH : LOW);
}

// ================= SOUND FUNCTIONS ==========
void playTone(int frequency, int duration) {
  ledcWriteTone(0, frequency);
  delay(duration);
  ledcWriteTone(0, 0);
}

void startupSequence() {
  Serial.println("\n=== STARTUP SEQUENCE ===");
  
  // Tampilan startup
  lcd.clear();
  lcd.setCursor(2, 1);
  lcd.print("INNOITI - TINYVEN");
  lcd.setCursor(4, 2);
  lcd.print("BY ZIDCREATIVE");
  
  // Sequence 1: LED bergantian dengan nada
  for (int i = 0; i < 3; i++) {
    digitalWrite(LED_BLUE_PIN, HIGH);
    playTone(800, 200);
    digitalWrite(LED_BLUE_PIN, LOW);
    delay(100);
    
    digitalWrite(LED_WHITE_PIN, HIGH);
    playTone(1000, 200);
    digitalWrite(LED_WHITE_PIN, LOW);
    delay(100);
    
    digitalWrite(LED_COMPARTMENT_PIN, HIGH);
    playTone(1200, 200);
    digitalWrite(LED_COMPARTMENT_PIN, LOW);
    delay(100);
  }
  
  // Sequence 2: Semua LED nyala dengan ascending tone
  digitalWrite(LED_BLUE_PIN, HIGH);
  digitalWrite(LED_WHITE_PIN, HIGH);
  digitalWrite(LED_COMPARTMENT_PIN, HIGH);
  
  for (int freq = 200; freq <= 2000; freq += 100) {
    ledcWriteTone(0, freq);
    delay(10);
  }
  ledcWriteTone(0, 0);
  delay(500);
  
  // Sequence 3: Semua mati dengan descending tone
  for (int freq = 2000; freq >= 200; freq -= 100) {
    ledcWriteTone(0, freq);
    delay(10);
  }
  ledcWriteTone(0, 0);
  
  digitalWrite(LED_BLUE_PIN, LOW);
  digitalWrite(LED_WHITE_PIN, LOW);
  digitalWrite(LED_COMPARTMENT_PIN, LOW);
  
  delay(1000);
  
  // Tampilkan versi
  lcd.clear();
  lcd.setCursor(0, 2);
  lcd.print("System Ready v3.0");
  lcd.setCursor(0, 3);
  lcd.print("Enhanced Display");
  
  delay(2000);
  
  system_state = STATE_STANDBY;
  Serial.println("=== STARTUP COMPLETE ===");
}

void successSound() {
  playTone(1500, 150);
  delay(50);
  playTone(2000, 150);
  delay(50);
  playTone(2500, 300);
}

void warningSound() {
  for (int i = 0; i < 3; i++) {
    playTone(1000, 200);
    delay(200);
  }
}

void errorSound() {
  for (int i = 0; i < 3; i++) {
    playTone(500, 300);
    delay(300);
  }
}

void dispensingSound() {
  // Sound naik saat servo bergerak
  for (int i = 0; i < 5; i++) {
    playTone(800 + (i * 100), 100);
    delay(50);
  }
  ledcWriteTone(0, 0);
}

void waitingSound() {
  playTone(1200, 100);
  delay(900);
}

void compartmentWarningSound() {
  // Sound peringatan untuk kompartemen
  playTone(800, 150);
  delay(100);
  playTone(600, 150);
  delay(100);
  playTone(400, 200);
}

// ================= SAFETY MONITOR FUNCTIONS ==========
void monitorSafety() {
  unsigned long current_time = millis();
  
  // Hanya monitor di state tertentu
  if (system_state == STATE_CHECK_ELIGIBILITY || 
      system_state == STATE_STANDBY) {
    
    // Cek sensor setiap SAFETY_CHECK_INTERVAL_MS
    if (current_time - safety.last_check >= SAFETY_CHECK_INTERVAL_MS) {
      safety.last_check = current_time;
      
      float distance = readDistance();
      safety.last_distance = distance;
      
      // Deteksi objek dalam jarak < 7cm
      if (distance > 0 && distance < REWARD_DETECTION_CM) {
        if (!safety.warning_active) {
          // Aktifkan peringatan pertama kali
          safety.warning_active = true;
          safety.warning_start = current_time;
          safety.is_safe = false;
          safety.last_sound = current_time;
          
          Serial.println("[SAFETY] Peringatan! Objek terdeteksi di kompartemen");
          Serial.print("[SAFETY] Jarak: ");
          Serial.print(distance);
          Serial.println(" cm");
          
          // Mainkan sound peringatan
          compartmentWarningSound();
        }
      } else {
        // Reset status jika objek sudah hilang
        if (safety.warning_active) {
          safety.warning_active = false;
          safety.is_safe = true;
          Serial.println("[SAFETY] Kompartemen kembali aman");
        }
      }
      
      // Cek timeout peringatan (5 detik)
      if (safety.warning_active && 
          (current_time - safety.warning_start >= SAFETY_WARNING_DURATION_MS)) {
        safety.warning_active = false;
        safety.is_safe = true;
        Serial.println("[SAFETY] Peringatan timeout, reset ke mode aman");
      }
    }
    
    // Mainkan sound peringatan berulang setiap 1 detik saat warning aktif
    if (safety.warning_active && 
        (current_time - safety.last_sound >= 1000)) {
      compartmentWarningSound();
      safety.last_sound = current_time;
    }
  } else {
    // Reset safety monitor jika bukan di state yang dimonitor
    if (safety.warning_active) {
      safety.warning_active = false;
      safety.is_safe = true;
    }
  }
}

// ================= SETUP ====================
void setup() {
  Serial.begin(115200);
  delay(1000);
  
  Serial.println("\n========================================");
  Serial.println("     TINYVEN V3 - ENHANCED SYSTEM      ");
  Serial.println("========================================\n");
  
  // Initialize I2C
  Wire.begin(SDA_PIN, SCL_PIN);
  delay(100);
  
  // Initialize LCD
  lcd.init();
  lcd.backlight();
  lcd.clear();
  
  // Initialize pins
  pinMode(SERVO_PIN, OUTPUT);
  digitalWrite(SERVO_PIN, LOW);
  
  pinMode(LED_COMPARTMENT_PIN, OUTPUT);
  pinMode(LED_BLUE_PIN, OUTPUT);
  pinMode(LED_WHITE_PIN, OUTPUT);
  digitalWrite(LED_COMPARTMENT_PIN, LOW);
  digitalWrite(LED_BLUE_PIN, LOW);
  digitalWrite(LED_WHITE_PIN, LOW);
  
  pinMode(TRIG_PIN, OUTPUT);
  pinMode(ECHO_PIN, INPUT);
  digitalWrite(TRIG_PIN, LOW);
  
  // Initialize Buzzer
  ledcSetup(0, 2000, 8);
  ledcAttachPin(BUZZER_PIN, 0);
  ledcWriteTone(0, 0);
  
  // Initialize servo position
  servoWrite(0);
  delay(1000);
  
  // Initialize variables
  memset(&current_reward, 0, sizeof(current_reward));
  current_reward.is_active = false;
  
  // Initialize safety monitor
  safety.warning_active = false;
  safety.is_safe = true;
  safety.last_check = 0;
  safety.warning_start = 0;
  safety.last_distance = 0;
  safety.last_sound = 0;
  
  // Connect to WiFi
  connectWiFi();
  
  if (wifi_connected) {
    syncNTPTime();
  }
  
  // Start dengan sequence
  startupSequence();
}

// ================= MAIN LOOP ================
void loop() {
  unsigned long current_time = millis();
  
  // Process servo movement
  processServoMovement();
  
  // Update time jika sudah sync
  if (time_synced) {
    updateTime();
  }
  
  // Update LEDs
  updateLEDs();
  
  // Update display
  if (current_time - last_display_update >= DISPLAY_UPDATE_MS) {
    updateDisplay();
    last_display_update = current_time;
  }
  
  // Monitor keamanan kompartemen (dijalankan terus menerus)
  monitorSafety();
  
  // State Machine
  switch(system_state) {
    case STATE_STANDBY:
      handleStandbyState(current_time);
      break;
      
    case STATE_CHECK_ELIGIBILITY:
      handleCheckEligibilityState();
      break;
      
    case STATE_DISPENSING:
      handleDispensingState();
      break;
      
    case STATE_WAITING_FOR_TAKE:
      handleWaitingForTakeState(current_time);
      break;
      
    case STATE_RETURNING_HOME:
      handleReturningHomeState();
      break;
      
    case STATE_REWARD_TAKEN:
      handleRewardTakenState();
      break;
      
    case STATE_WARNING:
      handleWarningState(current_time);
      break;
      
    case STATE_ERROR:
      handleErrorState();
      break;
  }
  
  delay(10);
}

// ================= STATE HANDLERS ===========
void handleStandbyState(unsigned long current_time) {
  // Poll API periodically
  if (current_time - last_poll_time >= POLL_INTERVAL_MS) {
    if (wifi_connected) {
      pollPresensi();
      
      if (!current_reward.is_active && reward_queue.empty()) {
        system_state = STATE_CHECK_ELIGIBILITY;
      }
    } else {
      connectWiFi();
      if (wifi_connected && !time_synced) {
        syncNTPTime();
      }
    }
    last_poll_time = current_time;
  }
  
  if (!reward_queue.empty() && !current_reward.is_active) {
    system_state = STATE_CHECK_ELIGIBILITY;
  }
}

void handleCheckEligibilityState() {
  // Cek keamanan kompartemen - skip jika tidak aman
  if (!safety.is_safe) {
    return;
  }
  
  if (!reward_queue.empty()) {
    RewardQueueItem item = reward_queue.front();
    reward_queue.erase(reward_queue.begin());
    
    if (checkUserEligibility(item.user.peserta_id, item.user.total_poin)) {
      if (!compartment_loaded) {
        reward_queue.push_back(item);
        system_state = STATE_STANDBY;
        return;
      }
      
      current_reward.peserta_id = item.user.peserta_id;
      current_reward.nama = item.user.nama;
      current_reward.total_poin = item.user.total_poin;
       current_reward.threshold = (item.user.total_poin / 40) * 40;  // ← Ganti 25 → 40
      current_reward.is_active = true;
      current_reward.attempt_count = 0;
      
      system_state = STATE_DISPENSING;
    } else {
      system_state = STATE_STANDBY;
    }
  } else {
    system_state = STATE_STANDBY;
  }
}

void handleDispensingState() {
  static bool phase1_complete = false;
  
  if (!phase1_complete) {
    // PHASE 1: Putar servo
    current_reward.attempt_count++;
    
    setServoPosition(180);
    
    // Mainkan sound dispensing
    dispensingSound();
    
    // Tunggu servo selesai
    unsigned long start_wait = millis();
    while (servo_is_moving && (millis() - start_wait < 3000)) {
      processServoMovement();
      delay(10);
    }
    
    compartment_loaded = false;
    phase1_complete = true;
    current_reward.servo_position = 180;
    
  } else {
    // PHASE 2: Verifikasi dengan sensor
    float distance = readDistance();
    
    if (distance > 0 && distance < REWARD_DETECTION_CM) {
      // SUCCESS
      successSound();
      
      logRewardToServer(current_reward.peserta_id, current_reward.nama,
                       current_reward.total_poin, current_reward.threshold,
                       "success", current_reward.attempt_count,
                       current_reward.servo_position, distance);
      
      total_rewards_given++;
      current_reward.dispense_time = millis();
      
      phase1_complete = false;
      system_state = STATE_WAITING_FOR_TAKE;
      
    } else {
      // FAILED
      if (current_reward.attempt_count < MAX_RETRY_ATTEMPTS) {
        errorSound();
        delay(1000);
        
        phase1_complete = false;
        compartment_loaded = true;
        
      } else {
        logRewardToServer(current_reward.peserta_id, current_reward.nama,
                         current_reward.total_poin, current_reward.threshold,
                         "failed", current_reward.attempt_count,
                         current_reward.servo_position, distance);
        
        errorSound();
        delay(3000);
        
        phase1_complete = false;
        system_state = STATE_RETURNING_HOME;
      }
    }
  }
}

void handleWaitingForTakeState(unsigned long current_time) {
  if (current_time - last_monitor_time >= MONITOR_INTERVAL_MS) {
    last_monitor_time = current_time;
    
    float distance = readDistance();
    
    if (distance > REWARD_DETECTION_CM && distance > 0) {
      // Hadiah diambil
      successSound();
      system_state = STATE_RETURNING_HOME;
      
    } else if (distance < REWARD_DETECTION_CM && distance > 0) {
      // Masih ada, cek timeout
      unsigned long waiting_time = current_time - current_reward.dispense_time;
      
      if (waiting_time > REWARD_TAKE_TIMEOUT_MS) {
        system_state = STATE_WARNING;
      } else {
        // Mainkan waiting sound setiap 5 detik
        if ((current_time / 5000) % 2 == 0) {
          waitingSound();
        }
      }
    }
  }
}

void handleReturningHomeState() {
  setServoPosition(0);
  
  // Tunggu servo selesai
  unsigned long start_wait = millis();
  while (servo_is_moving && (millis() - start_wait < 3000)) {
    processServoMovement();
    delay(10);
  }
  
  // Simulasi pengisian
  delay(2000);
  compartment_loaded = true;
  
  system_state = STATE_REWARD_TAKEN;
}

void handleRewardTakenState() {
  delay(2000);
  
  current_reward.is_active = false;
  memset(&current_reward, 0, sizeof(current_reward));
  
  if (!reward_queue.empty() && compartment_loaded) {
    system_state = STATE_CHECK_ELIGIBILITY;
  } else {
    system_state = STATE_STANDBY;
  }
}

void handleWarningState(unsigned long current_time) {
  warningSound();
  
  if (current_time - last_monitor_time >= MONITOR_INTERVAL_MS) {
    last_monitor_time = current_time;
    
    float distance = readDistance();
    if (distance > REWARD_DETECTION_CM && distance > 0) {
      system_state = STATE_RETURNING_HOME;
    }
  }
}

void handleErrorState() {
  errorSound();
  delay(5000);
  system_state = STATE_STANDBY;
}

// ================= DISPLAY FUNCTIONS ==========
void updateDisplay() {
  lcd.clear();
  
  if (!wifi_connected) {
    lcd.setCursor(0, 0);
    lcd.print("WiFi OFFLINE");
    lcd.setCursor(0, 1);
    lcd.print("Trying reconnect...");
    return;
  }
  
  switch(system_state) {
    case STATE_STANDBY:
      // Baris 2: Judul Sistem
      lcd.setCursor(1, 1);
      lcd.print("PRESENSI SHOLAT");
      // Baris 3: Sub Judul
      lcd.setCursor(1, 2);
      lcd.print("TPQ AL-MUNAWWARAH");
      break;
      
    case STATE_CHECK_ELIGIBILITY:
      if (!safety.is_safe) {
        // Tampilan peringatan keamanan
        lcd.setCursor(0, 0);
        lcd.print("**** PERINGATAN ****");
        lcd.setCursor(0, 1);
        lcd.print("Jangan masukkan apa");
        lcd.setCursor(0, 2);
        lcd.print("pun ke kompartemen!");
        lcd.setCursor(0, 3);
        
        // Tampilkan countdown
        unsigned long warning_time = millis() - safety.warning_start;
        int remaining_sec = (SAFETY_WARNING_DURATION_MS - warning_time) / 1000;
        if (remaining_sec > 0) {
          lcd.print("Tunggu ");
          lcd.print(remaining_sec);
          lcd.print(" detik...");
        } else {
          lcd.print("Segera ambil tangan!");
        }
      } else {
        // Tampilan normal
        lcd.setCursor(0, 0);
        lcd.print("--------------------");
        lcd.setCursor(2, 1);
        lcd.print("SISTEM PRESENSI");
        lcd.setCursor(3, 2);
        lcd.print("SHOLAT PINTAR");
        // Baris 4: Queue dan History
        lcd.setCursor(0, 3);
        lcd.print("Q=");
        lcd.print(reward_queue.size());
        lcd.setCursor(7, 3);
        lcd.print("-----");
        lcd.setCursor(17, 3);
        lcd.print("H=");
        lcd.print(total_rewards_given);
      }
      break;
      
    case STATE_DISPENSING:
      lcd.setCursor(0, 0);
      lcd.print("-------");
      lcd.setCursor(0, 1);
      lcd.print("Hadiah Untuk");
      lcd.setCursor(0, 2);
      lcd.print(current_reward.nama);
      lcd.setCursor(0, 3);
      lcd.print("Tunggu.....");
      break;
      
    case STATE_WAITING_FOR_TAKE:
      {
        lcd.setCursor(4, 0);
        lcd.print("Alhamdulillah,");
        lcd.setCursor(2, 1);
        lcd.print("Selamat ");
        lcd.print(current_reward.nama);
        lcd.setCursor(3, 2);
        lcd.print("Silahkan ambil");
        lcd.setCursor(4, 3);
        lcd.print("hadiah");
        
        // Countdown
        unsigned long waiting_time = millis() - current_reward.dispense_time;
        int remaining_sec = (REWARD_TAKE_TIMEOUT_MS - waiting_time) / 1000;
        if (remaining_sec > 0) {
          lcd.setCursor(12, 3);
          lcd.print("(");
          lcd.print(remaining_sec);
          lcd.print("s)");
        }
      }
      break;
      
    case STATE_WARNING:
      lcd.setCursor(2, 0);
      lcd.print("-----------");
      lcd.setCursor(2, 1);
      lcd.print("Waktu Habis,");
      lcd.setCursor(2, 2);
      lcd.print("Segera ambil,");
      lcd.setCursor(2, 3);
      lcd.print(current_reward.nama);
      lcd.print("!");
      break;
      
    case STATE_RETURNING_HOME:
      lcd.setCursor(2, 0);
      lcd.print("----------------");
      lcd.setCursor(2, 1);
      lcd.print("Sedang");
      lcd.setCursor(2, 2);
      lcd.print("Menyiapkan");
      lcd.setCursor(2, 3);
      lcd.print("----------------");
      break;
      
    case STATE_ERROR:
      lcd.setCursor(5, 1);
      lcd.print("SYSTEM");
      lcd.setCursor(6, 2);
      lcd.print("ERROR");
      break;
  }
}

// ================= HARDWARE FUNCTIONS ========
float readDistance() {
  digitalWrite(TRIG_PIN, LOW);
  delayMicroseconds(2);
  digitalWrite(TRIG_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(TRIG_PIN, LOW);
  
  long duration = pulseIn(ECHO_PIN, HIGH, 30000);
  if (duration == 0) return -1;
  
  float distance = duration * 0.0343 / 2;
  if (distance < 2 || distance > 400) return -1;
  
  return distance;
}

// ================= WIFI & API FUNCTIONS ======
void connectWiFi() {
  if (WiFi.status() == WL_CONNECTED) {
    wifi_connected = true;
    return;
  }
  
  Serial.print("[WiFi] Connecting...");
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  
  int attempts = 0;
  while (WiFi.status() != WL_CONNECTED && attempts < 20) {
    delay(500);
    Serial.print(".");
    attempts++;
  }
  
  if (WiFi.status() == WL_CONNECTED) {
    wifi_connected = true;
    Serial.println("OK");
  } else {
    wifi_connected = false;
    Serial.println("FAILED");
  }
}

bool checkUserEligibility(int peserta_id, int total_poin) {
  int threshold = (total_poin / 40) * 40;  // ← Ganti 25 → 40
  if (threshold < 40) return false;        // ← Ganti 25 → 40
  
  bool already_rewarded = checkIfAlreadyRewarded(peserta_id, threshold);
  
  if (!already_rewarded) {
    Serial.print("[ELIGIBILITY] User ");
    Serial.print(peserta_id);
    Serial.print(" eligible for threshold ");
    Serial.println(threshold);
    return true;
  }
  return false;
}

bool checkIfAlreadyRewarded(int peserta_id, int threshold) {
  if (!wifi_connected) return false;
  
  String url = String(API_BASE_URL) + "/check_reward.php?" +
               "peserta_id=" + String(peserta_id) +
               "&threshold=" + String(threshold);
  
  HTTPClient http;
  http.begin(url);
  http.setTimeout(3000);
  
  int httpCode = http.GET();
  
  if (httpCode == HTTP_CODE_OK) {
    String response = http.getString();
    JsonDocument doc;
    DeserializationError error = deserializeJson(doc, response);
    
    if (!error && doc["success"]) {
      bool already_rewarded = doc["already_rewarded"];
      return already_rewarded;
    }
  }
  return false;
}

void fetchEligibleUsersBatch() {
  if (!wifi_connected) return;
  
  String url = String(GET_ELIGIBLE_USERS_ENDPOINT);
  HTTPClient http;
  http.begin(url);
  http.setTimeout(5000);
  
  int httpCode = http.GET();
  
  if (httpCode == HTTP_CODE_OK) {
    String response = http.getString();
    JsonDocument doc;
    DeserializationError error = deserializeJson(doc, response);
    
    if (!error && doc["success"]) {
      JsonArray data = doc["data"];
      
      reward_queue.clear();
      
      for (JsonObject user : data) {
        int peserta_id = user["peserta_id"];
        String nama = user["nama"].as<String>();
        int total_poin = user["total_poin"];
        
        addToRewardQueue(peserta_id, nama, total_poin);
      }
      
      if (!reward_queue.empty()) {
        system_state = STATE_CHECK_ELIGIBILITY;
      }
    }
  }
  
  http.end();
}

void addToRewardQueue(int peserta_id, String nama, int total_poin) {
  for (auto& item : reward_queue) {
    if (item.user.peserta_id == peserta_id) {
      if (total_poin > item.user.total_poin) {
        item.user.total_poin = total_poin;
        item.added_time = millis();
      }
      return;
    }
  }
  
  EligibleUser user;
  user.peserta_id = peserta_id;
  user.nama = nama;
  user.total_poin = total_poin;
  user.threshold = (total_poin / 40) * 40;  // ← Ganti 25 → 40
  
  RewardQueueItem item;
  item.user = user;
  item.added_time = millis();
  
  reward_queue.push_back(item);
}

void pollPresensi() {
  if (!wifi_connected) return;
  
  String url = String(GET_PRESENSI_ENDPOINT) + "?last_id=" + String(last_processed_id);
  HTTPClient http;
  http.begin(url);
  http.setTimeout(5000);
  
  int httpCode = http.GET();
  
  if (httpCode == HTTP_CODE_OK) {
    String response = http.getString();
    JsonDocument doc;
    DeserializationError error = deserializeJson(doc, response);
    
    if (!error) {
      bool success = doc["success"];
      if (success) {
        int new_last_id = doc["last_id"];
        JsonArray data = doc["data"];
        
        if (data.size() > 0) {
          for (JsonObject presensi : data) {
            last_processed_id = new_last_id;
          }
          fetchEligibleUsersBatch();
        }
      }
    }
  }
  
  http.end();
}

void logRewardToServer(int peserta_id, String nama, int total_poin, int threshold, 
                       String status, int attempts, int servo_pos, float distance) {
  if (!wifi_connected) return;
  
  JsonDocument doc;
  doc["peserta_id"] = peserta_id;
  doc["nama_peserta"] = nama;
  doc["total_poin"] = total_poin;
  doc["threshold_poin"] = threshold;
  doc["status"] = status;
  doc["attempt_count"] = attempts;
  doc["servo_position"] = servo_pos;
  doc["jarak_terdeteksi"] = distance;
  
  String json;
  serializeJson(doc, json);
  
  HTTPClient http;
  http.begin(LOG_HADIAH_ENDPOINT);
  http.addHeader("Content-Type", "application/json");
  http.POST(json);
  http.end();
}