// ============================================
// 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(¤t_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(¤t_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();
}