this is the program, the website always displays "connection refused"
/*
* ESP32-2432S024 + LVGL 9.2.0 + EEZ Studio - Islamic Prayer Clock
* ARCHITECTURE: FreeRTOS Multi-Task Design - FIXED & WEB API COMPLETED
* * Update Log:
* - Added full Web API support for index.html
* - Added AP Configuration storage
* - Fixed route mismatches
*/
#include <WiFiClientSecure.h>
#include <lvgl.h>
#include <TFT_eSPI.h>
#include <XPT2046_Touchscreen.h>
#include <SPI.h>
#include <LittleFS.h>
#include <FS.h>
#include "ArduinoJson.h"
#include "WiFi.h"
#include "ESPAsyncWebServer.h"
#include "TimeLib.h"
#include "NTPClient.h"
#include "WiFiUdp.h"
#include "HTTPClient.h"
#include "esp_task_wdt.h"
// EEZ generated files
#include "src/ui.h"
#include "src/screens.h"
// ================================
// PIN DEFINITIONS
// ================================
#define TFT_BL 27
#define TOUCH_CS 33
#define TOUCH_IRQ 36
#define TOUCH_MOSI 13
#define TOUCH_MISO 12
#define TOUCH_CLK 14
#define SCREEN_WIDTH 240
#define SCREEN_HEIGHT 320
// Touch Calibration
#define TS_MIN_X 200
#define TS_MAX_X 3800
#define TS_MIN_Y 200
#define TS_MAX_Y 3800
// ================================
// RTOS CONFIGURATION
// ================================
#define UI_TASK_STACK_SIZE 16384
#define WIFI_TASK_STACK_SIZE 8192
#define NTP_TASK_STACK_SIZE 8192
#define WEB_TASK_STACK_SIZE 16384
#define PRAYER_TASK_STACK_SIZE 8192
#define UI_TASK_PRIORITY 3
#define WIFI_TASK_PRIORITY 2
#define NTP_TASK_PRIORITY 2
#define WEB_TASK_PRIORITY 1
#define PRAYER_TASK_PRIORITY 1
// Task Handles
TaskHandle_t uiTaskHandle = NULL;
TaskHandle_t wifiTaskHandle = NULL;
TaskHandle_t ntpTaskHandle = NULL;
TaskHandle_t webTaskHandle = NULL;
TaskHandle_t prayerTaskHandle = NULL;
// ================================
// SEMAPHORES & MUTEXES
// ================================
SemaphoreHandle_t displayMutex;
SemaphoreHandle_t timeMutex;
SemaphoreHandle_t wifiMutex;
SemaphoreHandle_t settingsMutex;
SemaphoreHandle_t spiMutex;
// Queue for display updates
QueueHandle_t displayQueue;
// ================================
// GLOBAL OBJECTS
// ================================
static lv_display_t *display;
static lv_indev_t *indev;
static uint8_t buf[SCREEN_WIDTH * 10];
TFT_eSPI tft = TFT_eSPI();
SPIClass touchSPI = SPIClass(VSPI);
XPT2046_Touchscreen touch(TOUCH_CS, TOUCH_IRQ);
// ================================
// CONFIGURATION STRUCTURES
// ================================
struct WiFiConfig {
char apSSID[33]; // Diperbarui untuk menyimpan setting AP
char apPassword[65]; // Diperbarui untuk menyimpan setting AP
String routerSSID;
String routerPassword;
bool isConnected;
IPAddress localIP;
};
struct TimeConfig {
time_t currentTime;
bool ntpSynced;
unsigned long lastNTPUpdate;
String ntpServer;
};
struct PrayerConfig {
String subuhTime;
String zuhurTime;
String asarTime;
String maghribTime;
String isyaTime;
String selectedCityId;
};
struct CityCache {
String jsonData;
unsigned long timestamp;
bool isValid;
} cityCache = {"", 0, false};
const unsigned long CACHE_DURATION = 86400000; // 24 jam dalam milliseconds
// Global configurations
WiFiConfig wifiConfig;
TimeConfig timeConfig;
PrayerConfig prayerConfig;
// ================================
// DISPLAY UPDATE STRUCTURE
// ================================
struct DisplayUpdate {
enum Type {
TIME_UPDATE,
PRAYER_UPDATE,
STATUS_UPDATE
} type;
String data;
};
// ================================
// NETWORK OBJECTS
// ================================
AsyncWebServer server(80);
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP);
// ================================
// TOUCH VARIABLES
// ================================
bool touchPressed = false;
int16_t lastX = 0;
int16_t lastY = 0;
unsigned long lastTouchTime = 0;
// ================================
// STATE VARIABLES
// ================================
bool colonOn = true;
bool displayNeedsUpdate = false;
enum WiFiState {
WIFI_IDLE,
WIFI_CONNECTING,
WIFI_CONNECTED,
WIFI_FAILED
};
volatile WiFiState wifiState = WIFI_IDLE;
enum NTPState {
NTP_IDLE,
NTP_UPDATING,
NTP_SYNCED,
NTP_FAILED
};
volatile NTPState ntpState = NTP_IDLE;
// Forward Declarations
void updateTimeDisplay();
void updatePrayerDisplay();
void getPrayerTimes(String cityId);
void saveWiFiCredentials();
void savePrayerTimes();
void setupServerRoutes(); // New Function Prototype
// ================================
// FLUSH CALLBACK
// ================================
void my_disp_flush(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map)
{
if (spiMutex != NULL && xSemaphoreTake(spiMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
uint32_t w = area->x2 - area->x1 + 1;
uint32_t h = area->y2 - area->y1 + 1;
uint16_t *color_p = (uint16_t *)px_map;
tft.startWrite();
tft.setAddrWindow(area->x1, area->y1, w, h);
tft.pushColors(color_p, w * h);
tft.endWrite();
xSemaphoreGive(spiMutex);
}
lv_display_flush_ready(disp);
}
// ================================
// TOUCH CALLBACK
// ================================
void my_touchpad_read(lv_indev_t *indev_driver, lv_indev_data_t *data)
{
static unsigned long lastTouchRead = 0;
unsigned long now = millis();
if (now - lastTouchRead < 20) {
data->state = touchPressed ? LV_INDEV_STATE_PR : LV_INDEV_STATE_REL;
if (touchPressed) {
data->point.x = lastX;
data->point.y = lastY;
}
return;
}
lastTouchRead = now;
bool irqActive = (digitalRead(TOUCH_IRQ) == LOW);
if (irqActive) {
delayMicroseconds(100);
if (spiMutex != NULL && xSemaphoreTake(spiMutex, pdMS_TO_TICKS(10)) == pdTRUE) {
if (touch.touched()) {
TS_Point p = touch.getPoint();
if (p.z > 200) {
lastX = map(p.x, TS_MIN_X, TS_MAX_X, 0, SCREEN_WIDTH);
lastY = map(p.y, TS_MIN_Y, TS_MAX_Y, 0, SCREEN_HEIGHT);
lastX = constrain(lastX, 0, SCREEN_WIDTH - 1);
lastY = constrain(lastY, 0, SCREEN_HEIGHT - 1);
data->state = LV_INDEV_STATE_PR;
data->point.x = lastX;
data->point.y = lastY;
touchPressed = true;
xSemaphoreGive(spiMutex);
return;
}
}
xSemaphoreGive(spiMutex);
}
}
data->state = LV_INDEV_STATE_REL;
touchPressed = false;
}
// ================================
// LITTLEFS FUNCTIONS
// ================================
bool init_littlefs() {
Serial.println("💾 Initializing LittleFS...");
if (!LittleFS.begin(true)) {
Serial.println("❌ LittleFS Mount Failed!");
return false;
}
Serial.println("✓ LittleFS Mounted");
return true;
}
void loadWiFiCredentials() {
if (xSemaphoreTake(settingsMutex, portMAX_DELAY) == pdTRUE) {
// Load Router Creds
if (LittleFS.exists("/wifi_creds.txt")) {
fs::File file = LittleFS.open("/wifi_creds.txt", "r");
if (file) {
wifiConfig.routerSSID = file.readStringUntil('\n');
wifiConfig.routerPassword = file.readStringUntil('\n');
wifiConfig.routerSSID.trim();
wifiConfig.routerPassword.trim();
file.close();
}
}
// Load AP Creds
if (LittleFS.exists("/ap_creds.txt")) {
fs::File file = LittleFS.open("/ap_creds.txt", "r");
if (file) {
String ssid = file.readStringUntil('\n');
String pass = file.readStringUntil('\n');
ssid.trim();
pass.trim();
ssid.toCharArray(wifiConfig.apSSID, 33);
pass.toCharArray(wifiConfig.apPassword, 65);
file.close();
}
} else {
// Defaults
strcpy(wifiConfig.apSSID, "JWS ESP32");
strcpy(wifiConfig.apPassword, "12345678");
}
xSemaphoreGive(settingsMutex);
}
}
void saveWiFiCredentials() {
if (xSemaphoreTake(settingsMutex, portMAX_DELAY) == pdTRUE) {
fs::File file = LittleFS.open("/wifi_creds.txt", "w");
if (file) {
file.println(wifiConfig.routerSSID);
file.println(wifiConfig.routerPassword);
file.close();
}
xSemaphoreGive(settingsMutex);
}
}
void saveAPCredentials() {
if (xSemaphoreTake(settingsMutex, portMAX_DELAY) == pdTRUE) {
fs::File file = LittleFS.open("/ap_creds.txt", "w");
if (file) {
file.println(wifiConfig.apSSID);
file.println(wifiConfig.apPassword);
file.close();
}
xSemaphoreGive(settingsMutex);
}
}
void loadPrayerTimes() {
if (xSemaphoreTake(settingsMutex, portMAX_DELAY) == pdTRUE) {
if (LittleFS.exists("/prayer_times.txt")) {
fs::File file = LittleFS.open("/prayer_times.txt", "r");
if (file) {
prayerConfig.subuhTime = file.readStringUntil('\n'); prayerConfig.subuhTime.trim();
prayerConfig.zuhurTime = file.readStringUntil('\n'); prayerConfig.zuhurTime.trim();
prayerConfig.asarTime = file.readStringUntil('\n'); prayerConfig.asarTime.trim();
prayerConfig.maghribTime = file.readStringUntil('\n'); prayerConfig.maghribTime.trim();
prayerConfig.isyaTime = file.readStringUntil('\n'); prayerConfig.isyaTime.trim();
// Load city ID if available
if (file.available()) {
prayerConfig.selectedCityId = file.readStringUntil('\n');
prayerConfig.selectedCityId.trim();
}
file.close();
}
}
xSemaphoreGive(settingsMutex);
}
}
void savePrayerTimes() {
if (xSemaphoreTake(settingsMutex, portMAX_DELAY) == pdTRUE) {
fs::File file = LittleFS.open("/prayer_times.txt", "w");
if (file) {
file.println(prayerConfig.subuhTime);
file.println(prayerConfig.zuhurTime);
file.println(prayerConfig.asarTime);
file.println(prayerConfig.maghribTime);
file.println(prayerConfig.isyaTime);
file.println(prayerConfig.selectedCityId); // Save City ID too
file.close();
}
xSemaphoreGive(settingsMutex);
}
}
// ================================
// TASKS
// ================================
// TASK 1: UI
void uiTask(void *parameter) {
TickType_t xLastWakeTime = xTaskGetTickCount();
const TickType_t xFrequency = pdMS_TO_TICKS(20);
while (true) {
if (xSemaphoreTake(displayMutex, pdMS_TO_TICKS(20)) == pdTRUE) {
lv_timer_handler();
xSemaphoreGive(displayMutex);
}
DisplayUpdate update;
if (xQueueReceive(displayQueue, &update, 0) == pdTRUE) {
if (xSemaphoreTake(displayMutex, pdMS_TO_TICKS(50)) == pdTRUE) {
switch (update.type) {
case DisplayUpdate::TIME_UPDATE: updateTimeDisplay(); break;
case DisplayUpdate::PRAYER_UPDATE: updatePrayerDisplay(); break;
default: break;
}
xSemaphoreGive(displayMutex);
}
}
vTaskDelayUntil(&xLastWakeTime, xFrequency);
}
}
// TASK 2: WiFi
void wifiTask(void *parameter) {
while (true) {
switch (wifiState) {
case WIFI_IDLE:
if (wifiConfig.routerSSID.length() > 0 && !wifiConfig.isConnected) {
if (xSemaphoreTake(wifiMutex, portMAX_DELAY) == pdTRUE) {
WiFi.begin(wifiConfig.routerSSID.c_str(), wifiConfig.routerPassword.c_str());
wifiState = WIFI_CONNECTING;
xSemaphoreGive(wifiMutex);
}
}
vTaskDelay(pdMS_TO_TICKS(5000));
break;
case WIFI_CONNECTING:
if (WiFi.status() == WL_CONNECTED) {
if (xSemaphoreTake(wifiMutex, portMAX_DELAY) == pdTRUE) {
wifiConfig.isConnected = true; // ← Pastikan ini di-set TRUE
wifiConfig.localIP = WiFi.localIP();
wifiState = WIFI_CONNECTED;
Serial.println("✅ WiFi Connected!");
Serial.print("IP: ");
Serial.println(wifiConfig.localIP);
xSemaphoreGive(wifiMutex);
if (ntpTaskHandle != NULL) xTaskNotifyGive(ntpTaskHandle);
}
}
vTaskDelay(pdMS_TO_TICKS(500));
break;
case WIFI_CONNECTED:
if (WiFi.status() != WL_CONNECTED) {
if (xSemaphoreTake(wifiMutex, portMAX_DELAY) == pdTRUE) {
wifiConfig.isConnected = false;
wifiState = WIFI_IDLE;
xSemaphoreGive(wifiMutex);
}
}
vTaskDelay(pdMS_TO_TICKS(10000));
break;
case WIFI_FAILED:
vTaskDelay(pdMS_TO_TICKS(30000));
wifiState = WIFI_IDLE;
break;
}
esp_task_wdt_reset();
}
}
// TASK 3: NTP
void ntpTask(void *parameter) {
while (true) {
// 1. TUNGGU DI SINI sampai ada notifikasi (dari tombol Web atau Timer)
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
Serial.println("🔄 Melakukan Sync NTP ke Internet...");
if (xSemaphoreTake(timeMutex, portMAX_DELAY) == pdTRUE) {
// 2. Update alamat server sesuai settingan terakhir
timeClient.setPoolServerName(timeConfig.ntpServer.c_str());
// 3. Set Offset ke WIB (Penting agar tidak kembali ke UTC)
timeClient.setTimeOffset(25200);
timeClient.begin();
if (timeClient.update()) {
timeConfig.currentTime = timeClient.getEpochTime();
setTime(timeConfig.currentTime);
timeConfig.ntpSynced = true;
// Kirim ke layar
DisplayUpdate update;
update.type = DisplayUpdate::TIME_UPDATE;
xQueueSend(displayQueue, &update, 0);
Serial.println("✅ NTP Sukses. Waktu terupdate.");
} else {
Serial.println("❌ Gagal connect ke NTP Server.");
}
xSemaphoreGive(timeMutex);
}
// Reset watchdog
esp_task_wdt_reset();
}
}
// TASK 4: Web Server
void webTask(void *parameter) {
setupServerRoutes();
server.begin();
while (true) {
vTaskDelay(pdMS_TO_TICKS(1000));
esp_task_wdt_reset();
}
}
// TASK 5: Prayer
void prayerTask(void *parameter) {
while (true) {
if (xSemaphoreTake(timeMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
if (hour(timeConfig.currentTime) == 0 &&
minute(timeConfig.currentTime) == 0 &&
wifiConfig.isConnected &&
prayerConfig.selectedCityId.length() > 0) {
xSemaphoreGive(timeMutex);
getPrayerTimes(prayerConfig.selectedCityId);
DisplayUpdate update;
update.type = DisplayUpdate::PRAYER_UPDATE;
xQueueSend(displayQueue, &update, 0);
} else {
xSemaphoreGive(timeMutex);
}
}
vTaskDelay(pdMS_TO_TICKS(60000));
esp_task_wdt_reset();
}
}
// Clock Tick Task
void clockTickTask(void *parameter) {
TickType_t xLastWakeTime = xTaskGetTickCount();
const TickType_t xFrequency = pdMS_TO_TICKS(1000);
static int autoSyncCounter = 0;
while (true) {
if (xSemaphoreTake(timeMutex, pdMS_TO_TICKS(50)) == pdTRUE) {
timeConfig.currentTime++;
xSemaphoreGive(timeMutex);
DisplayUpdate update;
update.type = DisplayUpdate::TIME_UPDATE;
xQueueSend(displayQueue, &update, 0);
}
// LOGIKA AUTO SYNC TIAP 1 JAM (3600 detik)
// Auto Sync hanya berjalan jika WiFi terhubung
if (wifiConfig.isConnected) {
autoSyncCounter++;
if (autoSyncCounter >= 3600) { // 3600 detik = 1 jam
autoSyncCounter = 0;
if (ntpTaskHandle != NULL) xTaskNotifyGive(ntpTaskHandle); // Panggil NTP Task
}
}
vTaskDelayUntil(&xLastWakeTime, xFrequency);
}
}
// ================================
// HELPER FUNCTIONS
// ================================
void updateTimeDisplay() {
if (xSemaphoreTake(timeMutex, pdMS_TO_TICKS(50)) == pdTRUE) {
char timeStr[20];
char dateStr[15];
sprintf(timeStr, "%02d%c%02d",
hour(timeConfig.currentTime),
colonOn ? ':' : ' ',
minute(timeConfig.currentTime));
sprintf(dateStr, "%02d/%02d/%04d",
day(timeConfig.currentTime),
month(timeConfig.currentTime),
year(timeConfig.currentTime));
String dateTimeStr = String(timeStr) + " " + String(dateStr);
if (objects.date_time) lv_label_set_text(objects.date_time, dateTimeStr.c_str());
colonOn = !colonOn;
xSemaphoreGive(timeMutex);
}
}
void updatePrayerDisplay() {
if(objects.subuh_time) lv_label_set_text(objects.subuh_time, prayerConfig.subuhTime.c_str());
if(objects.zuhur_time) lv_label_set_text(objects.zuhur_time, prayerConfig.zuhurTime.c_str());
if(objects.ashar_time) lv_label_set_text(objects.ashar_time, prayerConfig.asarTime.c_str());
if(objects.maghrib_time) lv_label_set_text(objects.maghrib_time, prayerConfig.maghribTime.c_str());
if(objects.isya_time) lv_label_set_text(objects.isya_time, prayerConfig.isyaTime.c_str());
}
void getPrayerTimes(String cityId) {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("❌ WiFi not connected, cannot fetch prayer times");
return;
}
// Format tanggal: YYYY-MM-DD (pakai dash, bukan slash!)
char dateStr[20];
sprintf(dateStr, "%04d-%02d-%02d",
year(timeConfig.currentTime),
month(timeConfig.currentTime),
day(timeConfig.currentTime));
String url = "https://api.myquran.com/v2/sholat/jadwal/" + cityId + "/" + String(dateStr);
Serial.println("🕌 Fetching prayer times from: " + url);
HTTPClient http;
WiFiClientSecure client;
client.setInsecure(); // Skip SSL certificate verification
http.begin(client, url);
http.setTimeout(10000);
int httpCode = http.GET();
Serial.printf("HTTP Response Code: %d\n", httpCode);
if (httpCode == 200) {
String payload = http.getString();
Serial.println("Response received, parsing JSON...");
StaticJsonDocument<2048> doc;
DeserializationError error = deserializeJson(doc, payload);
if (!error) {
// Cek apakah response valid
if (doc["status"].as<bool>() && !doc["data"]["jadwal"].isNull()) {
JsonObject jadwal = doc["data"]["jadwal"];
// Ambil data jadwal shalat
prayerConfig.subuhTime = jadwal["subuh"].as<String>();
prayerConfig.zuhurTime = jadwal["dzuhur"].as<String>(); // Perhatikan: dzuhur (pakai 'd')
prayerConfig.asarTime = jadwal["ashar"].as<String>();
prayerConfig.maghribTime = jadwal["maghrib"].as<String>();
prayerConfig.isyaTime = jadwal["isya"].as<String>();
savePrayerTimes();
Serial.println("✅ Prayer times updated successfully:");
Serial.println(" Subuh: " + prayerConfig.subuhTime);
Serial.println(" Dzuhur: " + prayerConfig.zuhurTime);
Serial.println(" Ashar: " + prayerConfig.asarTime);
Serial.println(" Maghrib: " + prayerConfig.maghribTime);
Serial.println(" Isya: " + prayerConfig.isyaTime);
} else {
Serial.println("❌ Invalid JSON structure or status false");
}
} else {
Serial.print("❌ JSON parse error: ");
Serial.println(error.c_str());
}
} else {
Serial.printf("❌ HTTP GET failed, code: %d\n", httpCode);
if (httpCode > 0) {
String payload = http.getString();
Serial.println("Error response: " + payload);
}
}
http.end();
}
bool isWiFiReallyConnected() {
if (WiFi.status() != WL_CONNECTED) {
return false;
}
// Double check dengan ping DNS
IPAddress testIP;
if (!WiFi.hostByName("www.google.com", testIP)) {
return false;
}
return true;
}
// ================================
// WEB SERVER ROUTES - FULLY FIXED
// ================================
void setupServerRoutes() {
// Root
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(LittleFS, "/index.html", "text/html");
});
// CSS
server.on("/assets/css/foundation.css", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(LittleFS, "/assets/css/foundation.css", "text/css");
});
// Device Status
server.on("/devicestatus", HTTP_GET, [](AsyncWebServerRequest *request) {
char timeStr[20];
sprintf(timeStr, "%02d:%02d:%02d",
hour(timeConfig.currentTime),
minute(timeConfig.currentTime),
second(timeConfig.currentTime));
// Debug print
Serial.printf("WiFi.status() = %d\n", WiFi.status());
Serial.printf("wifiConfig.isConnected = %d\n", wifiConfig.isConnected);
String response = "{";
response += "\"ssid\":\"" + String(WiFi.SSID()) + "\",";
response += "\"ip\":\"" + wifiConfig.localIP.toString() + "\",";
response += "\"ntpSynced\":" + String(timeConfig.ntpSynced ? "true" : "false") + ",";
response += "\"currentTime\":\"" + String(timeStr) + "\",";
response += "\"freeHeap\":\"" + String(ESP.getFreeHeap()) + "\"";
response += "}";
request->send(200, "application/json", response);
});
// Get Cities List from API
server.on("/getcities", HTTP_GET, [](AsyncWebServerRequest *request){
Serial.println("\n========================================");
Serial.println("📡 GET CITIES REQUEST");
// CEK CACHE TERLEBIH DAHULU
unsigned long now = millis();
if (cityCache.isValid &&
cityCache.jsonData.length() > 0 &&
(now - cityCache.timestamp) < CACHE_DURATION) {
Serial.println("✅ Menggunakan cache (tidak perlu fetch API)");
Serial.printf(" Cache age: %lu ms\n", now - cityCache.timestamp);
AsyncWebServerResponse *response = request->beginResponse(
200,
"application/json",
cityCache.jsonData
);
response->addHeader("X-Cache", "HIT");
request->send(response);
return;
}
Serial.println("❌ Cache expired atau kosong, fetch dari API...");
// Validasi WiFi
if (WiFi.status() != WL_CONNECTED) {
Serial.println("❌ WiFi not connected");
request->send(200, "application/json",
"{\"status\":\"offline\",\"message\":\"WiFi not connected\"}");
return;
}
esp_task_wdt_reset();
// DNS Resolution
IPAddress apiIP;
if (!WiFi.hostByName("api.myquran.com", apiIP)) {
Serial.println("❌ DNS failed");
request->send(200, "application/json",
"{\"status\":\"offline\",\"message\":\"DNS failed\"}");
return;
}
Serial.printf("✅ DNS: %s\n", apiIP.toString().c_str());
// HTTP Client
WiFiClientSecure *client = new WiFiClientSecure();
if (!client) {
request->send(200, "application/json",
"{\"status\":\"offline\",\"message\":\"Memory error\"}");
return;
}
client->setInsecure();
client->setHandshakeTimeout(10000);
client->setTimeout(10000);
HTTPClient http;
http.setTimeout(10000);
http.setConnectTimeout(8000);
http.setReuse(false);
String url = "https://api.myquran.com/v2/sholat/kota/semua";
if (!http.begin(*client, url)) {
delete client;
request->send(200, "application/json",
"{\"status\":\"offline\",\"message\":\"HTTP begin failed\"}");
return;
}
http.addHeader("User-Agent", "ESP32-JWS/1.0");
http.addHeader("Accept", "application/json");
Serial.println("⏳ Fetching from API...");
esp_task_wdt_reset();
int httpCode = http.GET();
Serial.printf("📊 HTTP Code: %d\n", httpCode);
if (httpCode == HTTP_CODE_OK) {
WiFiClient* stream = http.getStreamPtr();
size_t size = http.getSize();
Serial.printf("📦 Size: %d bytes\n", size);
// Cek memory
if (ESP.getFreeHeap() < size + 10000) {
Serial.println("❌ Not enough memory");
http.end();
delete client;
request->send(200, "application/json",
"{\"status\":\"offline\",\"message\":\"Memory low\"}");
return;
}
// Baca data
String payload = "";
payload.reserve(size + 100);
uint8_t buff[512];
size_t totalRead = 0;
while (http.connected() && totalRead < size) {
esp_task_wdt_reset();
size_t available = stream->available();
if (available) {
int c = stream->readBytes(buff, min((size_t)512, available));
if (c > 0) {
payload += String((char*)buff).substring(0, c);
totalRead += c;
}
}
delay(1);
}
Serial.printf("✅ Downloaded: %d bytes\n", totalRead);
// Validasi JSON
StaticJsonDocument<512> testDoc;
DeserializationError error = deserializeJson(testDoc, payload.substring(0, 512));
if (!error && testDoc["status"].as<bool>()) {
// SIMPAN KE CACHE
cityCache.jsonData = payload;
cityCache.timestamp = millis();
cityCache.isValid = true;
Serial.println("✅ Data cached successfully");
AsyncWebServerResponse *response = request->beginResponse(
200,
"application/json",
payload
);
response->addHeader("X-Cache", "MISS");
request->send(response);
} else {
Serial.println("❌ Invalid JSON");
request->send(200, "application/json",
"{\"status\":\"offline\",\"message\":\"Invalid data\"}");
}
} else {
Serial.printf("❌ HTTP failed: %d\n", httpCode);
request->send(200, "application/json",
"{\"status\":\"offline\",\"message\":\"API error\"}");
}
http.end();
delete client;
esp_task_wdt_reset();
Serial.println("========================================\n");
});
server.on("/clearcache", HTTP_GET, [](AsyncWebServerRequest *request){
cityCache.isValid = false;
cityCache.jsonData = "";
cityCache.timestamp = 0;
Serial.println("🗑️ Cache cleared");
request->send(200, "text/plain", "Cache cleared");
});
// Get Prayer Times
server.on("/getprayertimes", HTTP_GET, [](AsyncWebServerRequest *request){
String json = "{";
json += "\"subuh\":\"" + prayerConfig.subuhTime + "\",";
json += "\"dzuhur\":\"" + prayerConfig.zuhurTime + "\",";
json += "\"ashar\":\"" + prayerConfig.asarTime + "\",";
json += "\"maghrib\":\"" + prayerConfig.maghribTime + "\",";
json += "\"isya\":\"" + prayerConfig.isyaTime + "\"";
json += "}";
request->send(200, "application/json", json);
});
// Get Selected City ID
server.on("/getselectedcity", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(200, "text/plain", prayerConfig.selectedCityId);
});
// Update Prayer Times (Triggered by UI button)
server.on("/updateprayertimes", HTTP_GET, [](AsyncWebServerRequest *request) {
if (request->hasParam("cityId")) {
prayerConfig.selectedCityId = request->getParam("cityId")->value();
Serial.println("🕌 Updating prayer times for city ID: " + prayerConfig.selectedCityId);
// Update immediately
getPrayerTimes(prayerConfig.selectedCityId);
// Update Display
DisplayUpdate update;
update.type = DisplayUpdate::PRAYER_UPDATE;
xQueueSend(displayQueue, &update, 0);
// Return new data
String json = "{";
json += "\"subuh\":\"" + prayerConfig.subuhTime + "\",";
json += "\"dzuhur\":\"" + prayerConfig.zuhurTime + "\",";
json += "\"ashar\":\"" + prayerConfig.asarTime + "\",";
json += "\"maghrib\":\"" + prayerConfig.maghribTime + "\",";
json += "\"isya\":\"" + prayerConfig.isyaTime + "\"";
json += "}";
Serial.println("Sending response: " + json);
request->send(200, "application/json", json);
} else {
request->send(400, "text/plain", "Missing cityId");
}
});
// Set WiFi
server.on("/setwifi", HTTP_POST, [](AsyncWebServerRequest *request) {
if (request->hasParam("ssid", true) && request->hasParam("password", true)) {
wifiConfig.routerSSID = request->getParam("ssid", true)->value();
wifiConfig.routerPassword = request->getParam("password", true)->value();
saveWiFiCredentials();
request->send(200, "text/plain", "WiFi Credentials Saved. Restarting...");
vTaskDelay(pdMS_TO_TICKS(1000));
ESP.restart();
} else {
request->send(400, "text/plain", "Invalid Data");
}
});
// Set AP
server.on("/setap", HTTP_POST, [](AsyncWebServerRequest *request) {
if (request->hasParam("ssid", true) && request->hasParam("password", true)) {
String ssid = request->getParam("ssid", true)->value();
String pass = request->getParam("password", true)->value();
ssid.toCharArray(wifiConfig.apSSID, 33);
pass.toCharArray(wifiConfig.apPassword, 65);
saveAPCredentials();
request->send(200, "text/plain", "AP Settings Saved. Restart required.");
} else {
request->send(400, "text/plain", "Invalid Data");
}
});
// Set NTP Server & Trigger Sync
server.on("/setntpserver", HTTP_POST, [](AsyncWebServerRequest *request) {
if (request->hasParam("server", true)) {
timeConfig.ntpServer = request->getParam("server", true)->value();
if (ntpTaskHandle != NULL) {
xTaskNotifyGive(ntpTaskHandle);
}
request->send(200, "text/plain", "Server NTP disimpan. Mengambil waktu dari " + timeConfig.ntpServer + "...");
} else {
request->send(400, "text/plain", "Data server tidak ditemukan");
}
});
// Sync Time
server.on("/synctime", HTTP_POST, [](AsyncWebServerRequest *request) {
if (request->hasParam("y", true) && request->hasParam("m", true) &&
request->hasParam("d", true) && request->hasParam("h", true) &&
request->hasParam("i", true) && request->hasParam("s", true)) {
int y = request->getParam("y", true)->value().toInt();
int m = request->getParam("m", true)->value().toInt();
int d = request->getParam("d", true)->value().toInt();
int h = request->getParam("h", true)->value().toInt();
int i = request->getParam("i", true)->value().toInt();
int s = request->getParam("s", true)->value().toInt();
if (xSemaphoreTake(timeMutex, portMAX_DELAY) == pdTRUE) {
setTime(h, i, s, d, m, y);
timeConfig.currentTime = now();
timeConfig.ntpSynced = true;
DisplayUpdate update;
update.type = DisplayUpdate::TIME_UPDATE;
xQueueSend(displayQueue, &update, 0);
xSemaphoreGive(timeMutex);
}
request->send(200, "text/plain", "Waktu berhasil disamakan dengan HP/Laptop!");
} else if (wifiConfig.isConnected && ntpTaskHandle != NULL) {
xTaskNotifyGive(ntpTaskHandle);
request->send(200, "text/plain", "Sync NTP Internet...");
} else {
request->send(500, "text/plain", "Gagal mengambil data waktu dari browser");
}
});
// Factory Reset
server.on("/reset", HTTP_POST, [](AsyncWebServerRequest *request) {
LittleFS.remove("/wifi_creds.txt");
LittleFS.remove("/prayer_times.txt");
LittleFS.remove("/ap_creds.txt");
request->send(200, "text/plain", "Resetting...");
vTaskDelay(pdMS_TO_TICKS(1000));
ESP.restart();
});
server.onNotFound([](AsyncWebServerRequest *request){
request->send(404, "text/plain", "Not found");
});
}
// ================================
// SETUP
// ================================
void setup()
{
Serial.begin(115200);
delay(1000);
// Create mutexes
displayMutex = xSemaphoreCreateMutex();
timeMutex = xSemaphoreCreateMutex();
wifiMutex = xSemaphoreCreateMutex();
settingsMutex = xSemaphoreCreateMutex();
spiMutex = xSemaphoreCreateMutex();
displayQueue = xQueueCreate(10, sizeof(DisplayUpdate));
init_littlefs();
loadWiFiCredentials();
loadPrayerTimes();
// Init Hardware
pinMode(TFT_BL, OUTPUT);
digitalWrite(TFT_BL, HIGH);
pinMode(TOUCH_IRQ, INPUT_PULLUP);
tft.begin();
tft.setRotation(1);
tft.fillScreen(TFT_BLACK);
touchSPI.begin(TOUCH_CLK, TOUCH_MISO, TOUCH_MOSI, TOUCH_CS);
touch.begin(touchSPI);
touch.setRotation(1);
// Init LVGL
lv_init();
lv_tick_set_cb([]() { return (uint32_t)millis(); });
display = lv_display_create(SCREEN_WIDTH, SCREEN_HEIGHT);
lv_display_set_color_format(display, LV_COLOR_FORMAT_RGB565);
lv_display_set_buffers(display, buf, NULL, sizeof(buf), LV_DISPLAY_RENDER_MODE_PARTIAL);
lv_display_set_flush_cb(display, my_disp_flush);
indev = lv_indev_create();
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(indev, my_touchpad_read);
ui_init();
// Init AP
WiFi.mode(WIFI_AP_STA);
WiFi.softAP(wifiConfig.apSSID, wifiConfig.apPassword);
Serial.printf("AP Started: %s\n", wifiConfig.apSSID);
// Defaults
timeConfig.ntpServer = "pool.ntp.org";
timeConfig.currentTime = 0;
timeConfig.ntpSynced = false;
// Start Tasks
xTaskCreatePinnedToCore(uiTask, "UI", UI_TASK_STACK_SIZE, NULL, UI_TASK_PRIORITY, &uiTaskHandle, 1);
xTaskCreatePinnedToCore(wifiTask, "WiFi", WIFI_TASK_STACK_SIZE, NULL, WIFI_TASK_PRIORITY, &wifiTaskHandle, 0);
xTaskCreatePinnedToCore(ntpTask, "NTP", NTP_TASK_STACK_SIZE, NULL, NTP_TASK_PRIORITY, &ntpTaskHandle, 0);
xTaskCreatePinnedToCore(webTask, "Web", WEB_TASK_STACK_SIZE, NULL, WEB_TASK_PRIORITY, &webTaskHandle, 0);
xTaskCreatePinnedToCore(prayerTask, "Prayer", PRAYER_TASK_STACK_SIZE, NULL, PRAYER_TASK_PRIORITY, &prayerTaskHandle, 0);
xTaskCreatePinnedToCore(clockTickTask, "Clock", 4096, NULL, 2, NULL, 0);
}
// ================================
// LOOP
// ================================
void loop()
{
vTaskDelay(pdMS_TO_TICKS(1000));
esp_task_wdt_reset();
}
HTML
<div class="large-6 medium-6 small-6 cell">
<div class="grid-x grid-margin-x">
<div class="large-12 medium-12 small-12 cell">
<h4>Jadwal Shalat</h4>
<div class="grid-x grid-margin-x">
<!-- Search Box -->
<div class="large-12 medium-12 small-12 cell">
<label>Cari Kota
<input type="text" id="citySearch" placeholder="Ketik nama kota..."
style="margin-bottom: 5px;">
</label>
</div>
<!-- Select Dropdown -->
<div class="large-12 medium-12 small-12 cell">
<label>Pilih Kota
<select id="citySelect" size="5" style="height: 150px;">
<option value="">Loading...</option>
</select>
</label>
<button class="secondary button small" onclick="refreshCities()"
id="refreshBtn" style="margin-top: 5px;">
🔄 Refresh List
</button>
<small id="cityCount" style="display: block; margin-top: 5px; color: #666;">
<!-- City count akan muncul di sini -->
</small>
</div>
<div class="large-12 medium-12 small-12 cell">
<button class="button" onclick="updatePrayerTimes()">
Update Jadwal Shalat
</button>
</div>
</div>
<div id="prayerTimes" style="text-align: left; margin-top: 10px;"></div>
</div>
</div>
</div>
<script>
// GANTI SEMUA VARIABEL DAN FUNGSI CITIES dengan ini:
let isRefreshing = false;
let allCities = []; // Simpan semua data cities
let filteredCities = []; // Cities yang sudah difilter
function refreshCities() {
if (isRefreshing) {
console.log('⚠️ Refresh in progress...');
return;
}
isRefreshing = true;
const refreshBtn = document.getElementById('refreshBtn');
if (refreshBtn) {
refreshBtn.disabled = true;
refreshBtn.innerHTML = '⏳ Loading...';
}
retryCount = 0;
citiesLoaded = false;
fetchCities().finally(() => {
setTimeout(() => {
isRefreshing = false;
if (refreshBtn) {
refreshBtn.disabled = false;
refreshBtn.innerHTML = '🔄 Refresh List';
}
}, 1000);
});
}
function fetchCities() {
const citySelect = document.getElementById('citySelect');
console.log(`📡 Fetching cities...`);
citySelect.innerHTML = '<option value="">Loading...</option>';
citySelect.disabled = true;
return fetch('/getcities', {
method: 'GET',
headers: {
'Cache-Control': 'max-age=86400', // Cache 24 jam
}
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Cek apakah dari cache atau fresh
const cacheStatus = response.headers.get('X-Cache');
if (cacheStatus === 'HIT') {
console.log('✅ Loaded from ESP32 cache');
} else {
console.log('📡 Fresh data from API');
}
return response.json();
})
.then(response => {
console.log('📦 Response received');
citySelect.innerHTML = '';
if (response.status === "offline" || response.status === false) {
const option = document.createElement('option');
option.value = "";
option.textContent = response.message || "Tidak ada koneksi internet";
citySelect.appendChild(option);
citySelect.disabled = true;
console.log('❌ Offline');
return;
}
if (response.status === true && Array.isArray(response.data)) {
allCities = response.data; // Simpan semua data
filteredCities = [...allCities]; // Copy untuk filtered
console.log(`✅ ${allCities.length} cities loaded`);
renderCities(filteredCities);
citySelect.disabled = false;
citiesLoaded = true;
retryCount = 0;
// Update counter
updateCityCount(filteredCities.length, allCities.length);
// Restore selected city
loadSavedCity();
// Enable search
enableCitySearch();
} else {
throw new Error('Invalid data format');
}
})
.catch(error => {
console.error('❌ Error:', error);
citySelect.innerHTML = '<option value="">Error loading (klik refresh)</option>';
citySelect.disabled = true;
retryCount++;
});
}
function renderCities(cities) {
const citySelect = document.getElementById('citySelect');
citySelect.innerHTML = '';
// Default option
const defaultOption = document.createElement('option');
defaultOption.value = "";
defaultOption.textContent = "-- Pilih Kota --";
citySelect.appendChild(defaultOption);
// Render hanya 100 pertama untuk performa (user bisa search)
const citiesToRender = cities.slice(0, 100);
citiesToRender.forEach(city => {
const option = document.createElement('option');
option.value = city.id;
option.textContent = city.lokasi;
citySelect.appendChild(option);
});
if (cities.length > 100) {
const moreOption = document.createElement('option');
moreOption.value = "";
moreOption.disabled = true;
moreOption.textContent = `... dan ${cities.length - 100} lainnya (gunakan search)`;
citySelect.appendChild(moreOption);
}
}
function enableCitySearch() {
const searchInput = document.getElementById('citySearch');
// Remove old listener
const newSearchInput = searchInput.cloneNode(true);
searchInput.parentNode.replaceChild(newSearchInput, searchInput);
// Add new listener
newSearchInput.addEventListener('input', function(e) {
const searchTerm = e.target.value.toLowerCase().trim();
if (searchTerm === '') {
filteredCities = [...allCities];
} else {
filteredCities = allCities.filter(city =>
city.lokasi.toLowerCase().includes(searchTerm)
);
}
console.log(`🔍 Search: "${searchTerm}" -> ${filteredCities.length} results`);
renderCities(filteredCities);
updateCityCount(filteredCities.length, allCities.length);
});
}
function updateCityCount(filtered, total) {
const countEl = document.getElementById('cityCount');
if (filtered === total) {
countEl.textContent = `${total} kota tersedia`;
} else {
countEl.textContent = `Menampilkan ${filtered} dari ${total} kota`;
}
}
// Event listener untuk auto-update saat pilih kota
document.addEventListener('DOMContentLoaded', function() {
const citySelect = document.getElementById('citySelect');
if (citySelect) {
citySelect.addEventListener('change', function() {
if (this.value) {
updatePrayerTimes();
}
});
}
});
</script>