first commit
This commit is contained in:
commit
82328255c0
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
session_start();
|
||||
|
||||
// kalau belum login → lempar ke index
|
||||
if (!isset($_SESSION['logged_in'])) {
|
||||
header("Location: index.php");
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
|
|
@ -0,0 +1,856 @@
|
|||
#include <Arduino.h>
|
||||
#include <WiFi.h>
|
||||
// Enable verbose debug from Firebase_ESP_Client (helps reveal which host/step fails)
|
||||
#define FIREBASE_ESP_CLIENT_DEBUG_PORT Serial
|
||||
#define FIREBASE_ESP_CLIENT_DEBUG_LEVEL 2
|
||||
#include <Firebase_ESP_Client.h>
|
||||
#include <WiFiClientSecure.h>
|
||||
#include "DHT.h"
|
||||
#include <Wire.h>
|
||||
#include "RTClib.h"
|
||||
#include "time.h"
|
||||
#include <esp_heap_caps.h>
|
||||
|
||||
static const char* WIFI_SSID = "oven";
|
||||
static const char* WIFI_PASSWORD = "1sampai8";
|
||||
|
||||
static const char* FIREBASE_API_KEY = "AIzaSyC-Wdu6vNC0dRyF33S3_C2wpVt9I6wnp0w";
|
||||
static const char* FIREBASE_DATABASE_URL = "https://smartoven-f9fdd-default-rtdb.firebaseio.com/";
|
||||
|
||||
char apiKey[100] = "";
|
||||
char databaseUrl[200] = "";
|
||||
|
||||
FirebaseData fbdo;
|
||||
FirebaseAuth auth;
|
||||
FirebaseConfig config;
|
||||
static bool signupOK = false;
|
||||
|
||||
static bool signUpAnonymousWithRetry(uint32_t retryDelayMs = 2000) {
|
||||
// Anonymous sign-in: email/password kosong
|
||||
for (;;) {
|
||||
signupOK = Firebase.signUp(&config, &auth, "", "");
|
||||
if (signupOK) {
|
||||
Serial.println("[FB] signUp(anonymous): OK");
|
||||
return true;
|
||||
}
|
||||
Serial.print("[FB] signUp(anonymous): FAIL ");
|
||||
Serial.println(config.signer.signupError.message.c_str());
|
||||
delay(retryDelayMs);
|
||||
}
|
||||
}
|
||||
|
||||
static bool waitFirebaseReady(uint32_t timeoutMs = 15000) {
|
||||
Serial.print("[FB] Waiting ready");
|
||||
uint32_t start = millis();
|
||||
while (!Firebase.ready() && (millis() - start) < timeoutMs) {
|
||||
delay(250);
|
||||
Serial.print('.');
|
||||
}
|
||||
Serial.println();
|
||||
Serial.println(Firebase.ready() ? "[FB] Ready" : "[FB] Not ready (timeout)");
|
||||
return Firebase.ready();
|
||||
}
|
||||
|
||||
static bool fbIsPathNotExist() {
|
||||
// Firebase_ESP_Client uses negative codes for internal errors.
|
||||
// -103 is commonly used for "path not exist" in RTDB get.
|
||||
if (fbdo.httpCode() == -103) return true;
|
||||
String r = fbdo.errorReason();
|
||||
r.toLowerCase();
|
||||
return r.indexOf("path not exist") >= 0;
|
||||
}
|
||||
|
||||
// ===== HISTORY (RTDB) =====
|
||||
static const bool ENABLE_HISTORY = true;
|
||||
static const uint32_t HISTORY_INTERVAL_MS = 1800000; // 30 menit
|
||||
static uint32_t lastHistoryPushMs = 0;
|
||||
|
||||
static bool fbPushHistory(float temp, float hum, int heaterDb, int fanDb, int mode, bool active) {
|
||||
if (WiFi.status() != WL_CONNECTED) return false;
|
||||
if (!Firebase.ready()) return false;
|
||||
|
||||
time_t epoch = time(nullptr);
|
||||
if (epoch <= 0) {
|
||||
Serial.println("[FB] WARN pushHistory skip (time not set)");
|
||||
return false;
|
||||
}
|
||||
|
||||
FirebaseJson json;
|
||||
json.set("ts", (int)epoch);
|
||||
json.set("temp", temp);
|
||||
json.set("hum", hum);
|
||||
json.set("heater", heaterDb);
|
||||
json.set("fan", fanDb);
|
||||
json.set("mode", mode);
|
||||
json.set("active", active ? 1 : 0);
|
||||
|
||||
bool ok = Firebase.RTDB.pushJSON(&fbdo, "/history", &json);
|
||||
if (ok) {
|
||||
Serial.print("[FB] OK pushHistory key=");
|
||||
Serial.println(fbdo.pushName());
|
||||
} else {
|
||||
Serial.print("[FB] FAIL pushHistory code=");
|
||||
Serial.print(fbdo.httpCode());
|
||||
Serial.print(" reason=");
|
||||
Serial.println(fbdo.errorReason());
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
// ===== FIREBASE HELPERS (STATUS LOG) =====
|
||||
static bool fbSetFloat(const char* path, float value) {
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
Serial.print("[FB] FAIL setFloat ");
|
||||
Serial.print(path);
|
||||
Serial.println(" (WiFi not connected)");
|
||||
return false;
|
||||
}
|
||||
if (!Firebase.ready()) {
|
||||
Serial.print("[FB] FAIL setFloat ");
|
||||
Serial.print(path);
|
||||
Serial.println(" (Firebase not ready)");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = Firebase.RTDB.setFloat(&fbdo, path, value);
|
||||
if (ok) {
|
||||
Serial.print("[FB] OK setFloat ");
|
||||
Serial.print(path);
|
||||
Serial.print(" = ");
|
||||
Serial.println(value);
|
||||
} else {
|
||||
Serial.print("[FB] FAIL setFloat ");
|
||||
Serial.print(path);
|
||||
Serial.print(" code=");
|
||||
Serial.print(fbdo.httpCode());
|
||||
Serial.print(" reason=");
|
||||
Serial.println(fbdo.errorReason());
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
static bool fbSetInt(const char* path, int value) {
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
Serial.print("[FB] FAIL setInt ");
|
||||
Serial.print(path);
|
||||
Serial.println(" (WiFi not connected)");
|
||||
return false;
|
||||
}
|
||||
if (!Firebase.ready()) {
|
||||
Serial.print("[FB] FAIL setInt ");
|
||||
Serial.print(path);
|
||||
Serial.println(" (Firebase not ready)");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = Firebase.RTDB.setInt(&fbdo, path, value);
|
||||
if (ok) {
|
||||
Serial.print("[FB] OK setInt ");
|
||||
Serial.print(path);
|
||||
Serial.print(" = ");
|
||||
Serial.println(value);
|
||||
} else {
|
||||
Serial.print("[FB] FAIL setInt ");
|
||||
Serial.print(path);
|
||||
Serial.print(" code=");
|
||||
Serial.print(fbdo.httpCode());
|
||||
Serial.print(" reason=");
|
||||
Serial.println(fbdo.errorReason());
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
static bool fbGetInt(const char* path, int& outValue) {
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
Serial.print("[FB] FAIL getInt ");
|
||||
Serial.print(path);
|
||||
Serial.println(" (WiFi not connected)");
|
||||
return false;
|
||||
}
|
||||
if (!Firebase.ready()) {
|
||||
Serial.print("[FB] FAIL getInt ");
|
||||
Serial.print(path);
|
||||
Serial.println(" (Firebase not ready)");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = Firebase.RTDB.getInt(&fbdo, path);
|
||||
if (ok) {
|
||||
outValue = fbdo.intData();
|
||||
Serial.print("[FB] OK getInt ");
|
||||
Serial.print(path);
|
||||
Serial.print(" = ");
|
||||
Serial.println(outValue);
|
||||
} else {
|
||||
if (fbIsPathNotExist()) {
|
||||
outValue = 0;
|
||||
Serial.print("[FB] WARN getInt ");
|
||||
Serial.print(path);
|
||||
Serial.println(" (path not exist -> default 0)");
|
||||
return true;
|
||||
}
|
||||
Serial.print("[FB] FAIL getInt ");
|
||||
Serial.print(path);
|
||||
Serial.print(" code=");
|
||||
Serial.print(fbdo.httpCode());
|
||||
Serial.print(" reason=");
|
||||
Serial.println(fbdo.errorReason());
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
static bool fbGetFloat(const char* path, float& outValue) {
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
Serial.print("[FB] FAIL getFloat ");
|
||||
Serial.print(path);
|
||||
Serial.println(" (WiFi not connected)");
|
||||
return false;
|
||||
}
|
||||
if (!Firebase.ready()) {
|
||||
Serial.print("[FB] FAIL getFloat ");
|
||||
Serial.print(path);
|
||||
Serial.println(" (Firebase not ready)");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = Firebase.RTDB.getFloat(&fbdo, path);
|
||||
if (ok) {
|
||||
outValue = fbdo.floatData();
|
||||
Serial.print("[FB] OK getFloat ");
|
||||
Serial.print(path);
|
||||
Serial.print(" = ");
|
||||
Serial.println(outValue);
|
||||
} else {
|
||||
if (fbIsPathNotExist()) {
|
||||
outValue = 0.0f;
|
||||
Serial.print("[FB] WARN getFloat ");
|
||||
Serial.print(path);
|
||||
Serial.println(" (path not exist -> default 0)");
|
||||
return true;
|
||||
}
|
||||
Serial.print("[FB] FAIL getFloat ");
|
||||
Serial.print(path);
|
||||
Serial.print(" code=");
|
||||
Serial.print(fbdo.httpCode());
|
||||
Serial.print(" reason=");
|
||||
Serial.println(fbdo.errorReason());
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
// ===== RTC =====
|
||||
RTC_DS3231 rtc;
|
||||
static bool rtcAvailable = false;
|
||||
|
||||
static void scanI2C() {
|
||||
Serial.println("I2C scan...");
|
||||
uint8_t found = 0;
|
||||
|
||||
for (uint8_t addr = 1; addr < 127; addr++) {
|
||||
Wire.beginTransmission(addr);
|
||||
uint8_t err = Wire.endTransmission();
|
||||
if (err == 0) {
|
||||
Serial.print(" Found device at 0x");
|
||||
if (addr < 16) Serial.print('0');
|
||||
Serial.println(addr, HEX);
|
||||
found++;
|
||||
}
|
||||
}
|
||||
|
||||
if (found == 0) {
|
||||
Serial.println(" No I2C devices found (cek wiring SDA/SCL + pullup + pin Wire.begin)");
|
||||
}
|
||||
}
|
||||
|
||||
static void printHeap(const char* tag) {
|
||||
Serial.print("[HEAP] ");
|
||||
Serial.print(tag);
|
||||
Serial.print(" free=");
|
||||
Serial.print(ESP.getFreeHeap());
|
||||
Serial.print(" min=");
|
||||
Serial.print(ESP.getMinFreeHeap());
|
||||
Serial.print(" 8bit=");
|
||||
Serial.println((uint32_t)heap_caps_get_free_size(MALLOC_CAP_8BIT));
|
||||
}
|
||||
|
||||
static bool extractHostFromUrl(const char* url, char* outHost, size_t outSize) {
|
||||
if (!url || !outHost || outSize < 4) return false;
|
||||
outHost[0] = '\0';
|
||||
|
||||
const char* p = url;
|
||||
if (strncmp(p, "https://", 8) == 0) p += 8;
|
||||
else if (strncmp(p, "http://", 7) == 0) p += 7;
|
||||
|
||||
const char* end = strchr(p, '/');
|
||||
size_t len = end ? (size_t)(end - p) : strlen(p);
|
||||
if (len == 0 || len >= outSize) return false;
|
||||
|
||||
memcpy(outHost, p, len);
|
||||
outHost[len] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool testTcpConnect(const char* host, uint16_t port) {
|
||||
WiFiClient client;
|
||||
client.setTimeout(5000);
|
||||
bool ok = client.connect(host, port);
|
||||
client.stop();
|
||||
Serial.print("[NET] TCP ");
|
||||
Serial.print(host);
|
||||
Serial.print(":");
|
||||
Serial.print(port);
|
||||
Serial.print(" => ");
|
||||
Serial.println(ok ? "OK" : "FAIL");
|
||||
return ok;
|
||||
}
|
||||
|
||||
static bool testTlsConnectInsecure(const char* host, uint16_t port) {
|
||||
WiFiClientSecure client;
|
||||
client.setInsecure();
|
||||
client.setTimeout(7000);
|
||||
printHeap("before TLS");
|
||||
bool ok = client.connect(host, port);
|
||||
Serial.print("[NET] TLS(insecure) ");
|
||||
Serial.print(host);
|
||||
Serial.print(":");
|
||||
Serial.print(port);
|
||||
Serial.print(" => ");
|
||||
Serial.println(ok ? "OK" : "FAIL");
|
||||
client.stop();
|
||||
printHeap("after TLS");
|
||||
return ok;
|
||||
}
|
||||
|
||||
static void netDiagnostics() {
|
||||
Serial.println("[NET] Diagnostics...");
|
||||
printHeap("start");
|
||||
|
||||
// DNS test
|
||||
IPAddress ip;
|
||||
bool dnsOk = WiFi.hostByName("www.google.com", ip);
|
||||
Serial.print("[NET] DNS www.google.com => ");
|
||||
Serial.println(dnsOk ? ip.toString() : String("FAIL"));
|
||||
|
||||
// TCP/TLS to Google
|
||||
testTcpConnect("www.google.com", 443);
|
||||
testTlsConnectInsecure("www.google.com", 443);
|
||||
|
||||
// Firebase host test
|
||||
char fbHost[128];
|
||||
if (extractHostFromUrl(FIREBASE_DATABASE_URL, fbHost, sizeof(fbHost))) {
|
||||
bool fbDnsOk = WiFi.hostByName(fbHost, ip);
|
||||
Serial.print("[NET] DNS ");
|
||||
Serial.print(fbHost);
|
||||
Serial.print(" => ");
|
||||
Serial.println(fbDnsOk ? ip.toString() : String("FAIL"));
|
||||
testTcpConnect(fbHost, 443);
|
||||
testTlsConnectInsecure(fbHost, 443);
|
||||
} else {
|
||||
Serial.println("[NET] Could not parse Firebase host from URL");
|
||||
}
|
||||
}
|
||||
|
||||
// ===== NTP =====
|
||||
const char* ntpServer1 = "pool.ntp.org";
|
||||
const char* ntpServer2 = "time.google.com";
|
||||
const char* ntpServer3 = "time.nist.gov";
|
||||
const long gmtOffset_sec = 7 * 3600;
|
||||
const int daylightOffset_sec = 0;
|
||||
|
||||
unsigned long lastSync = 0;
|
||||
const unsigned long syncInterval = 3600000; // 1 jam
|
||||
|
||||
// ===== DHT =====
|
||||
#define DHTPIN 23
|
||||
#define DHTTYPE DHT22
|
||||
DHT dht(DHTPIN, DHTTYPE);
|
||||
|
||||
// ===== RELAY =====
|
||||
const int relayHeater = 12;
|
||||
const int relayFan = 13;
|
||||
|
||||
// ===== LED =====
|
||||
const int ledHeater = 12;
|
||||
|
||||
// ===== RELAY LOGIC =====
|
||||
#define RELAY_ON LOW
|
||||
#define RELAY_OFF HIGH
|
||||
|
||||
// ===== VARIABLE =====
|
||||
int startHour, startMinute;
|
||||
int endHour, endMinute;
|
||||
|
||||
float targetTemp;
|
||||
float targetTempFan;
|
||||
|
||||
// ===== KALIBRASI =====
|
||||
float tempOffset = 0;
|
||||
float humOffset = 0;
|
||||
|
||||
int mode = 0;
|
||||
int manualHeater = 0;
|
||||
int manualFan = 0;
|
||||
|
||||
// ===== WIFI CONNECT =====
|
||||
static bool connectWiFi(uint32_t timeoutMs = 20000) {
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.setSleep(false);
|
||||
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
|
||||
|
||||
Serial.print("WiFi connecting to: ");
|
||||
Serial.println(WIFI_SSID);
|
||||
|
||||
uint32_t start = millis();
|
||||
while (WiFi.status() != WL_CONNECTED && (millis() - start) < timeoutMs) {
|
||||
delay(300);
|
||||
Serial.print('.');
|
||||
}
|
||||
Serial.println();
|
||||
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
Serial.print("WiFi connected, IP: ");
|
||||
Serial.println(WiFi.localIP());
|
||||
return true;
|
||||
}
|
||||
|
||||
Serial.println("WiFi gagal connect");
|
||||
return false;
|
||||
}
|
||||
|
||||
static int monthStrToIndex(const char* mon) {
|
||||
// __DATE__ format: "Mmm dd yyyy"
|
||||
static const char* months[] = {"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"};
|
||||
for (int i = 0; i < 12; i++) {
|
||||
if (strncmp(mon, months[i], 3) == 0) return i;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool setTimeFromCompileUtc() {
|
||||
// fallback kalau NTP diblokir: set waktu dari waktu compile sketch
|
||||
// Ini cukup buat TLS (sertifikat) asal firmware baru di-upload (waktunya mendekati sekarang).
|
||||
const char* dateStr = __DATE__; // "May 20 2026"
|
||||
const char* timeStr = __TIME__; // "HH:MM:SS"
|
||||
|
||||
char mon[4] = {0};
|
||||
int day = 1;
|
||||
int year = 2020;
|
||||
int hour = 0;
|
||||
int minute = 0;
|
||||
int second = 0;
|
||||
|
||||
memcpy(mon, dateStr, 3);
|
||||
mon[3] = '\0';
|
||||
day = atoi(dateStr + 4);
|
||||
year = atoi(dateStr + 7);
|
||||
hour = atoi(timeStr);
|
||||
minute = atoi(timeStr + 3);
|
||||
second = atoi(timeStr + 6);
|
||||
|
||||
struct tm t;
|
||||
memset(&t, 0, sizeof(t));
|
||||
t.tm_year = year - 1900;
|
||||
t.tm_mon = monthStrToIndex(mon);
|
||||
t.tm_mday = day;
|
||||
t.tm_hour = hour;
|
||||
t.tm_min = minute;
|
||||
t.tm_sec = second;
|
||||
t.tm_isdst = 0;
|
||||
|
||||
setenv("TZ", "UTC0", 1);
|
||||
tzset();
|
||||
|
||||
time_t epoch = mktime(&t);
|
||||
if (epoch <= 0) return false;
|
||||
|
||||
struct timeval tv;
|
||||
tv.tv_sec = epoch;
|
||||
tv.tv_usec = 0;
|
||||
settimeofday(&tv, nullptr);
|
||||
|
||||
Serial.print("[TIME] Fallback set from compile UTC: ");
|
||||
Serial.println(ctime(&epoch));
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===== SYNC RTC =====
|
||||
static bool syncNtpTime(uint32_t timeoutMs = 20000) {
|
||||
// set timezone for localtime
|
||||
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer1, ntpServer2, ntpServer3);
|
||||
|
||||
struct tm timeinfo;
|
||||
uint32_t start = millis();
|
||||
while (!getLocalTime(&timeinfo) && (millis() - start) < timeoutMs) {
|
||||
delay(500);
|
||||
Serial.print('#');
|
||||
}
|
||||
Serial.println();
|
||||
|
||||
if (!getLocalTime(&timeinfo)) {
|
||||
Serial.println("NTP gagal");
|
||||
return false;
|
||||
}
|
||||
|
||||
// validasi kasar: tahun harus sudah masuk akal
|
||||
if (timeinfo.tm_year + 1900 < 2020) {
|
||||
Serial.println("NTP dapat tapi waktu belum valid");
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.print("NTP OK: ");
|
||||
Serial.print(timeinfo.tm_year + 1900);
|
||||
Serial.print('-');
|
||||
Serial.print(timeinfo.tm_mon + 1);
|
||||
Serial.print('-');
|
||||
Serial.print(timeinfo.tm_mday);
|
||||
Serial.print(' ');
|
||||
Serial.print(timeinfo.tm_hour);
|
||||
Serial.print(':');
|
||||
Serial.print(timeinfo.tm_min);
|
||||
Serial.print(':');
|
||||
Serial.println(timeinfo.tm_sec);
|
||||
|
||||
if (rtcAvailable) {
|
||||
rtc.adjust(DateTime(
|
||||
timeinfo.tm_year + 1900,
|
||||
timeinfo.tm_mon + 1,
|
||||
timeinfo.tm_mday,
|
||||
timeinfo.tm_hour,
|
||||
timeinfo.tm_min,
|
||||
timeinfo.tm_sec
|
||||
));
|
||||
Serial.println("RTC Sync NTP");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void syncRTC() {
|
||||
if (syncNtpTime()) {
|
||||
Serial.println(rtcAvailable ? "RTC/NTP sync OK" : "NTP sync OK (RTC tidak ada)");
|
||||
}
|
||||
}
|
||||
|
||||
// ===== SETUP =====
|
||||
void setup() {
|
||||
|
||||
Serial.begin(115200);
|
||||
delay(2000);
|
||||
|
||||
Serial.println("BOOT...");
|
||||
|
||||
// ===== PIN MODE =====
|
||||
pinMode(relayHeater, OUTPUT);
|
||||
pinMode(relayFan, OUTPUT);
|
||||
pinMode(ledHeater, OUTPUT);
|
||||
|
||||
digitalWrite(relayHeater, RELAY_OFF);
|
||||
digitalWrite(relayFan, RELAY_OFF);
|
||||
digitalWrite(ledHeater, LOW);
|
||||
|
||||
// ===== DHT =====
|
||||
dht.begin();
|
||||
|
||||
// ===== I2C =====
|
||||
Wire.begin(13, 14);
|
||||
scanI2C();
|
||||
|
||||
// ===== RTC =====
|
||||
rtcAvailable = rtc.begin();
|
||||
if (!rtcAvailable) {
|
||||
Serial.println("RTC error (DS3231 tidak terdeteksi). Lanjut tanpa RTC (pakai NTP). ");
|
||||
}
|
||||
|
||||
// ===== FIREBASE CONFIG (HARDCODE) =====
|
||||
strncpy(apiKey, FIREBASE_API_KEY, sizeof(apiKey) - 1);
|
||||
apiKey[sizeof(apiKey) - 1] = '\0';
|
||||
|
||||
strncpy(databaseUrl, FIREBASE_DATABASE_URL, sizeof(databaseUrl) - 1);
|
||||
databaseUrl[sizeof(databaseUrl) - 1] = '\0';
|
||||
|
||||
if (strlen(apiKey) == 0 || strlen(databaseUrl) == 0) {
|
||||
Serial.println("[WARN] FIREBASE_API_KEY / FIREBASE_DATABASE_URL masih kosong. Isi dulu di bagian atas sketch.");
|
||||
}
|
||||
|
||||
// ===== WIFI =====
|
||||
if (!connectWiFi()) {
|
||||
// retry terus sampai konek
|
||||
while (!connectWiFi()) {
|
||||
delay(2000);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== NTP TIME (PENTING UNTUK SSL/TLS) =====
|
||||
// Banyak error SSL Firebase terjadi kalau waktu belum terset saat TLS handshake.
|
||||
Serial.println("Sync NTP...");
|
||||
bool ntpOk = syncNtpTime(60000);
|
||||
if (!ntpOk) {
|
||||
Serial.println("[TIME] NTP gagal. Coba fallback waktu compile...");
|
||||
setTimeFromCompileUtc();
|
||||
}
|
||||
|
||||
time_t nowEpoch = time(nullptr);
|
||||
Serial.print("[TIME] Now epoch: ");
|
||||
Serial.println((long)nowEpoch);
|
||||
if (nowEpoch > 0) {
|
||||
Serial.print("[TIME] Now: ");
|
||||
Serial.println(ctime(&nowEpoch));
|
||||
}
|
||||
|
||||
Serial.print("Heap before Firebase: ");
|
||||
Serial.println(ESP.getFreeHeap());
|
||||
|
||||
// ===== NETWORK DIAGNOSTICS =====
|
||||
// Ini bantu bedain: DNS/port443 block vs TLS init/heap issue.
|
||||
netDiagnostics();
|
||||
|
||||
// ===== FIREBASE =====
|
||||
config.api_key = apiKey;
|
||||
config.database_url = databaseUrl;
|
||||
|
||||
// Anonymous auth ONLY (tanpa test_mode).
|
||||
// Pastikan di Firebase Console: Authentication -> Sign-in method -> Anonymous di-enable.
|
||||
signUpAnonymousWithRetry();
|
||||
|
||||
// buffer & timeout tuning (membantu masalah SSL init / write error)
|
||||
// coba lebih kecil dulu biar nggak kehabisan heap
|
||||
fbdo.setBSSLBufferSize(2048, 512);
|
||||
fbdo.setResponseSize(2048);
|
||||
config.timeout.serverResponse = 10 * 1000;
|
||||
|
||||
Firebase.begin(&config, &auth);
|
||||
Firebase.reconnectWiFi(true);
|
||||
|
||||
waitFirebaseReady();
|
||||
|
||||
// ===== RTC SYNC =====
|
||||
if (rtcAvailable) {
|
||||
// kalau RTC ada, pastikan sudah disesuaikan
|
||||
syncRTC();
|
||||
}
|
||||
lastSync = millis();
|
||||
|
||||
Serial.println("🔥 SYSTEM READY");
|
||||
}
|
||||
|
||||
// ===== LOOP =====
|
||||
void loop() {
|
||||
|
||||
// ===== WIFI CHECK =====
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
WiFi.reconnect();
|
||||
}
|
||||
|
||||
// ===== RTC PERIODIC SYNC =====
|
||||
if (WiFi.status() == WL_CONNECTED &&
|
||||
millis() - lastSync > syncInterval) {
|
||||
|
||||
syncRTC();
|
||||
lastSync = millis();
|
||||
}
|
||||
|
||||
// ===== FIREBASE READY CHECK =====
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
Serial.println("[FB] Skip: WiFi not connected");
|
||||
delay(1000);
|
||||
return;
|
||||
}
|
||||
if (!Firebase.ready()) {
|
||||
static uint32_t lastNotReadyLog = 0;
|
||||
if (millis() - lastNotReadyLog > 5000) {
|
||||
Serial.println("[FB] Skip: Firebase not ready");
|
||||
lastNotReadyLog = millis();
|
||||
}
|
||||
delay(200);
|
||||
// tetap lanjut loop (biar logic lokal jalan), tapi operasi Firebase akan FAIL di wrapper
|
||||
}
|
||||
if (!signupOK) {
|
||||
Serial.println("[FB] Skip: signUp not OK");
|
||||
delay(2000);
|
||||
return;
|
||||
}
|
||||
|
||||
// ===== GET FIREBASE =====
|
||||
|
||||
fbGetInt("/control/start_hour", startHour);
|
||||
fbGetInt("/control/start_minute", startMinute);
|
||||
fbGetInt("/control/end_hour", endHour);
|
||||
fbGetInt("/control/end_minute", endMinute);
|
||||
fbGetFloat("/control/target_temp", targetTemp);
|
||||
fbGetFloat("/control/target_temp_fan", targetTempFan);
|
||||
fbGetInt("/control/mode", mode);
|
||||
fbGetInt("/manual/heater", manualHeater);
|
||||
fbGetInt("/manual/fan", manualFan);
|
||||
|
||||
// fallback kalau target fan belum diset
|
||||
if (targetTempFan <= 0) targetTempFan = targetTemp;
|
||||
|
||||
// ===== GET KALIBRASI =====
|
||||
fbGetFloat("/calibration/temp_offset", tempOffset);
|
||||
fbGetFloat("/calibration/hum_offset", humOffset);
|
||||
|
||||
// ===== CURRENT TIME =====
|
||||
int hourNow = 0;
|
||||
int minuteNow = 0;
|
||||
bool timeOk = false;
|
||||
|
||||
if (rtcAvailable) {
|
||||
DateTime now = rtc.now();
|
||||
hourNow = now.hour();
|
||||
minuteNow = now.minute();
|
||||
timeOk = true;
|
||||
} else {
|
||||
struct tm timeinfo;
|
||||
if (getLocalTime(&timeinfo, 1000)) {
|
||||
hourNow = timeinfo.tm_hour;
|
||||
minuteNow = timeinfo.tm_min;
|
||||
timeOk = true;
|
||||
}
|
||||
}
|
||||
|
||||
int currentTime = hourNow * 60 + minuteNow;
|
||||
int startTime = startHour * 60 + startMinute;
|
||||
int endTime = endHour * 60 + endMinute;
|
||||
|
||||
bool active = false;
|
||||
if (timeOk) {
|
||||
active = (startTime <= endTime)
|
||||
? (currentTime >= startTime &&
|
||||
currentTime <= endTime)
|
||||
: (currentTime >= startTime ||
|
||||
currentTime <= endTime);
|
||||
} else {
|
||||
// kalau waktu tidak valid, fail-safe: nonaktif
|
||||
Serial.println("[TIME] Tidak dapat ambil waktu (RTC/NTP). Mode AUTO akan nonaktif.");
|
||||
active = false;
|
||||
}
|
||||
|
||||
// ===== SENSOR =====
|
||||
float temp = dht.readTemperature();
|
||||
float hum = dht.readHumidity();
|
||||
|
||||
// DHT read kadang gagal (NaN) karena timing/interrupt/WiFi atau wiring.
|
||||
// Coba retry sekali agar lebih stabil.
|
||||
if (isnan(temp) || isnan(hum)) {
|
||||
Serial.println("[SENSOR] DHT read failed, retry...");
|
||||
delay(250);
|
||||
temp = dht.readTemperature();
|
||||
hum = dht.readHumidity();
|
||||
}
|
||||
|
||||
// ===== VALIDASI SENSOR =====
|
||||
if (isnan(temp) || isnan(hum)) {
|
||||
Serial.println("[SENSOR] DHT error (NaN). Cek wiring VCC/GND/DATA, pullup 4.7k-10k ke VCC, kabel jangan panjang, pastikan sensor DHT22 & pin GPIO benar.");
|
||||
delay(2000);
|
||||
return;
|
||||
}
|
||||
|
||||
// ===== APPLY KALIBRASI =====
|
||||
temp += tempOffset;
|
||||
hum += humOffset;
|
||||
|
||||
// ===== CONTROL =====
|
||||
bool heaterState = false;
|
||||
bool fanState = false;
|
||||
|
||||
if (mode == 1) {
|
||||
|
||||
// ===== MANUAL MODE =====
|
||||
heaterState = (manualHeater != 0);
|
||||
fanState = (manualFan != 0);
|
||||
|
||||
digitalWrite(
|
||||
relayFan,
|
||||
fanState ? RELAY_ON : RELAY_OFF
|
||||
);
|
||||
|
||||
} else {
|
||||
|
||||
// ===== AUTO MODE =====
|
||||
if (active) {
|
||||
|
||||
heaterState = (temp < targetTemp);
|
||||
fanState = (temp > targetTempFan);
|
||||
|
||||
digitalWrite(
|
||||
relayFan,
|
||||
fanState ? RELAY_ON : RELAY_OFF
|
||||
);
|
||||
|
||||
} else {
|
||||
|
||||
heaterState = false;
|
||||
fanState = false;
|
||||
|
||||
digitalWrite(relayFan, RELAY_OFF);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== APPLY HEATER =====
|
||||
digitalWrite(
|
||||
relayHeater,
|
||||
heaterState ? RELAY_ON : RELAY_OFF
|
||||
);
|
||||
|
||||
digitalWrite(
|
||||
ledHeater,
|
||||
heaterState ? HIGH : LOW
|
||||
);
|
||||
|
||||
// ===== SEND SENSOR =====
|
||||
bool okTemp = fbSetFloat("/sensor/temperature", temp);
|
||||
bool okHum = fbSetFloat("/sensor/humidity", hum);
|
||||
|
||||
// ===== SEND RELAY =====
|
||||
int heaterDb = heaterState ? 1 : 0;
|
||||
int fanDb = fanState ? 1 : 0;
|
||||
|
||||
bool okHeater = fbSetInt("/relay/heater", heaterDb);
|
||||
bool okFan = fbSetInt("/relay/fan", fanDb);
|
||||
|
||||
// Mirror manual state to match relay state (biar UI manual & relay selalu sama)
|
||||
bool okManualHeater = fbSetInt("/manual/heater", heaterDb);
|
||||
bool okManualFan = fbSetInt("/manual/fan", fanDb);
|
||||
|
||||
Serial.print("[FB] Upload summary: ");
|
||||
Serial.println((okTemp && okHum && okHeater && okFan && okManualHeater && okManualFan) ? "OK" : "FAIL");
|
||||
|
||||
// ===== DEBUG =====
|
||||
|
||||
Serial.print("Time: ");
|
||||
Serial.print(hourNow);
|
||||
Serial.print(":");
|
||||
Serial.print(minuteNow);
|
||||
Serial.print(" (");
|
||||
Serial.print(rtcAvailable ? "RTC" : "NTP");
|
||||
Serial.println(")");
|
||||
|
||||
Serial.print("Temp: ");
|
||||
Serial.println(temp);
|
||||
|
||||
Serial.print("Humidity: ");
|
||||
Serial.println(hum);
|
||||
|
||||
Serial.print("Temp Offset: ");
|
||||
Serial.println(tempOffset);
|
||||
|
||||
Serial.print("Hum Offset: ");
|
||||
Serial.println(humOffset);
|
||||
|
||||
Serial.print("Mode: ");
|
||||
Serial.println(mode == 1 ? "MANUAL" : "AUTO");
|
||||
|
||||
Serial.print("Active: ");
|
||||
Serial.println(active);
|
||||
|
||||
// ===== PUSH HISTORY =====
|
||||
if (ENABLE_HISTORY) {
|
||||
uint32_t nowMs = millis();
|
||||
if (lastHistoryPushMs == 0 || (nowMs - lastHistoryPushMs) >= HISTORY_INTERVAL_MS) {
|
||||
fbPushHistory(temp, hum, heaterDb, fanDb, mode, active);
|
||||
lastHistoryPushMs = nowMs;
|
||||
}
|
||||
}
|
||||
|
||||
delay(3000);
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
session_start();
|
||||
if(!isset($_SESSION['logged_in'])) header("Location:index.php");
|
||||
|
||||
$page = $_GET['page'] ?? 'home';
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>Dashboard IoT</title>
|
||||
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script type="module" src="firebase-dashboard.js"></script>
|
||||
<style>
|
||||
* {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
body { font-family: 'Poppins', sans-serif; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-gradient-to-br from-slate-100 to-blue-100 flex">
|
||||
|
||||
<!-- SIDEBAR -->
|
||||
<div class="w-64 bg-gradient-to-b from-blue-600 to-indigo-700 text-white min-h-screen p-6 shadow-xl">
|
||||
|
||||
<h1 class="text-2xl font-bold mb-10 tracking-wide">
|
||||
⚡ IoT Dryer
|
||||
</h1>
|
||||
|
||||
<ul class="space-y-3 text-sm">
|
||||
|
||||
<li onclick="window.location='dashboard.php?page=home'"
|
||||
class="p-3 rounded-xl cursor-pointer hover:bg-white/20 transition duration-300 flex items-center gap-2">
|
||||
<i class="fas fa-chart-line"></i> Dashboard
|
||||
</li>
|
||||
|
||||
<li onclick="window.location='dashboard.php?page=monitoring'"
|
||||
class="p-3 rounded-xl cursor-pointer hover:bg-white/20 transition duration-300 flex items-center gap-2">
|
||||
<i class="fas fa-thermometer-half"></i> Monitoring
|
||||
</li>
|
||||
|
||||
<li onclick="window.location='dashboard.php?page=kontrol'"
|
||||
class="p-3 rounded-xl cursor-pointer hover:bg-white/20 transition duration-300 flex items-center gap-2">
|
||||
<i class="fas fa-sliders-h"></i> Kontrol
|
||||
</li>
|
||||
|
||||
<li onclick="window.location='logout.php'"
|
||||
class="p-3 rounded-xl cursor-pointer hover:bg-red-500 transition duration-300 flex items-center gap-2">
|
||||
<i class="fas fa-sign-out-alt"></i> Logout
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- MAIN -->
|
||||
<div class="flex-1 <?= ($page == 'home') ? 'p-0' : 'p-8' ?>">
|
||||
|
||||
<?php
|
||||
|
||||
if ($page == 'home') {
|
||||
include 'home.php';
|
||||
} elseif ($page == 'monitoring') {
|
||||
include 'monitoring.php';
|
||||
} elseif ($page == 'kontrol') {
|
||||
include 'kontrol.php';
|
||||
} else {
|
||||
echo "<h2>Halaman tidak ditemukan</h2>";
|
||||
}
|
||||
?>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
// 🔥 IMPORT
|
||||
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-app.js";
|
||||
|
||||
import {
|
||||
getAuth,
|
||||
createUserWithEmailAndPassword,
|
||||
signInWithEmailAndPassword
|
||||
} from "https://www.gstatic.com/firebasejs/10.12.2/firebase-auth.js";
|
||||
|
||||
import {
|
||||
getDatabase,
|
||||
ref,
|
||||
set
|
||||
} from "https://www.gstatic.com/firebasejs/10.12.2/firebase-database.js";
|
||||
|
||||
|
||||
// 🔥 CONFIG (FIX SEMUA)
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyC-Wdu6vNC0dRyF33S3_C2wpVt9I6wnp0w",
|
||||
authDomain: "smartoven-f9fdd.firebaseapp.com",
|
||||
databaseURL: "https://smartoven-f9fdd-default-rtdb.firebaseio.com/",
|
||||
projectId: "smartoven-f9fdd",
|
||||
appId: "1:1005261996032:web:1bcb483b922275f08f98e5"
|
||||
};
|
||||
|
||||
|
||||
// 🔥 INIT
|
||||
const app = initializeApp(firebaseConfig);
|
||||
const auth = getAuth(app);
|
||||
const db = getDatabase(app);
|
||||
|
||||
|
||||
// =====================
|
||||
// 🔥 REGISTER
|
||||
// =====================
|
||||
const registerForm = document.getElementById("registerForm");
|
||||
|
||||
if (registerForm) {
|
||||
registerForm.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const username = document.querySelector('[name="reg_username"]').value;
|
||||
const email = document.querySelector('[name="reg_email"]').value;
|
||||
const password = document.querySelector('[name="reg_password"]').value;
|
||||
const confirm = document.querySelector('[name="reg_confirm"]').value;
|
||||
|
||||
if (password !== confirm) {
|
||||
alert("Password tidak sama!");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const userCred = await createUserWithEmailAndPassword(auth, email, password);
|
||||
const user = userCred.user;
|
||||
|
||||
// 🔥 SIMPAN USER KE DATABASE
|
||||
await set(ref(db, "users/" + user.uid), {
|
||||
username: username,
|
||||
email: email
|
||||
});
|
||||
|
||||
alert("Registrasi berhasil!");
|
||||
window.location.href = "index.php";
|
||||
|
||||
} catch (err) {
|
||||
alert("Error: " + err.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// =====================
|
||||
// 🔥 LOGIN
|
||||
// =====================
|
||||
const loginForm = document.getElementById("loginForm");
|
||||
|
||||
if (loginForm) {
|
||||
loginForm.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const email = document.querySelector('[name="username"]').value;
|
||||
const password = document.querySelector('[name="password"]').value;
|
||||
|
||||
try {
|
||||
await signInWithEmailAndPassword(auth, email, password);
|
||||
|
||||
// 🔥 KIRIM SESSION KE PHP
|
||||
await fetch("login_process.php", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
},
|
||||
body: `email=${email}`
|
||||
});
|
||||
|
||||
window.location.href = "dashboard.php";
|
||||
|
||||
} catch (err) {
|
||||
alert("Login gagal: " + err.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-app.js";
|
||||
import { getDatabase, ref, onValue, set, push } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-database.js";
|
||||
|
||||
// 🔥 CONFIG
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyC-Wdu6vNC0dRyF33S3_C2wpVt9I6wnp0w",
|
||||
authDomain: "smartoven-f9fdd.firebaseapp.com",
|
||||
databaseURL: "https://smartoven-f9fdd-default-rtdb.firebaseio.com/",
|
||||
projectId: "smartoven-f9fdd",
|
||||
appId: "1:1005261996032:web:1bcb483b922275f08f98e5"
|
||||
};
|
||||
|
||||
const app = initializeApp(firebaseConfig);
|
||||
const db = getDatabase(app);
|
||||
|
||||
// 🔥 ELEMENT
|
||||
const suhuEl = document.getElementById("suhu");
|
||||
const humEl = document.getElementById("kelembaban");
|
||||
const logEl = document.getElementById("log");
|
||||
const heaterEl = document.getElementById("heater");
|
||||
const fanEl = document.getElementById("fan");
|
||||
const historyTable = document.getElementById("historyTable");
|
||||
|
||||
// =======================
|
||||
// 🔥 REALTIME SENSOR
|
||||
// =======================
|
||||
onValue(ref(db, 'sensor'), (snapshot) => {
|
||||
const data = snapshot.val();
|
||||
if (!data) return;
|
||||
|
||||
const time = new Date().toLocaleTimeString();
|
||||
|
||||
if (suhuEl) suhuEl.innerText = data.suhu + " °C";
|
||||
if (humEl) humEl.innerText = data.kelembaban + " %";
|
||||
|
||||
if (logEl) {
|
||||
logEl.innerHTML += `
|
||||
<tr>
|
||||
<td>${time}</td>
|
||||
<td>${data.suhu}</td>
|
||||
<td>${data.kelembaban}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
// simpan ke history
|
||||
push(ref(db, 'history'), {
|
||||
suhu: data.suhu,
|
||||
kelembaban: data.kelembaban,
|
||||
waktu: time
|
||||
});
|
||||
});
|
||||
|
||||
// =======================
|
||||
// 🔥 STATUS CONTROL
|
||||
// =======================
|
||||
onValue(ref(db, 'control'), (snap) => {
|
||||
const d = snap.val();
|
||||
if (!d) return;
|
||||
|
||||
if (heaterEl) {
|
||||
heaterEl.innerText = d.heater ? "ON" : "OFF";
|
||||
heaterEl.className = d.heater ? "text-green-500" : "text-red-500";
|
||||
}
|
||||
|
||||
if (fanEl) {
|
||||
fanEl.innerText = d.fan ? "ON" : "OFF";
|
||||
fanEl.className = d.fan ? "text-green-500" : "text-red-500";
|
||||
}
|
||||
});
|
||||
|
||||
// =======================
|
||||
// 🔥 KONTROL
|
||||
// =======================
|
||||
window.setHeater = (val) => {
|
||||
set(ref(db, 'control/heater'), val);
|
||||
};
|
||||
|
||||
window.setFan = (val) => {
|
||||
set(ref(db, 'control/fan'), val);
|
||||
};
|
||||
|
||||
window.setTarget = () => {
|
||||
const suhu = document.getElementById("targetSuhu").value;
|
||||
if (!suhu) return alert("Isi suhu!");
|
||||
set(ref(db, 'control/target_suhu'), parseInt(suhu));
|
||||
};
|
||||
|
||||
// =======================
|
||||
// 🔥 HISTORY TABLE
|
||||
// =======================
|
||||
onValue(ref(db, 'history'), (snap) => {
|
||||
const data = snap.val();
|
||||
if (!data || !historyTable) return;
|
||||
|
||||
historyTable.innerHTML = "";
|
||||
|
||||
Object.values(data).reverse().forEach(d => {
|
||||
|
||||
let status = "Normal";
|
||||
let color = "text-green-500";
|
||||
|
||||
if (d.suhu > 50) {
|
||||
status = "Panas";
|
||||
color = "text-red-500";
|
||||
} else if (d.suhu < 30) {
|
||||
status = "Dingin";
|
||||
color = "text-blue-500";
|
||||
}
|
||||
|
||||
historyTable.innerHTML += `
|
||||
<tr class="hover:bg-gray-50 transition">
|
||||
<td class="px-4 py-3">${d.waktu}</td>
|
||||
<td class="px-4 py-3 font-semibold">${d.suhu} °C</td>
|
||||
<td class="px-4 py-3">${d.kelembaban} %</td>
|
||||
<td class="px-4 py-3 ${color} font-semibold">${status}</td>
|
||||
</tr>`;
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<!-- 🔥 WRAPPER KHUSUS HOME -->
|
||||
<div class="relative w-full h-screen overflow-hidden">
|
||||
|
||||
<!-- 🔥 BACKGROUND GAMBAR -->
|
||||
<div class="absolute inset-0">
|
||||
<img src="assets/img/ikan.jpg"
|
||||
class="w-full h-full object-cover blur-sm scale-105">
|
||||
|
||||
<!-- overlay biar lebih soft -->
|
||||
<div class="absolute inset-0 bg-black/30"></div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- 🔥 ISI DASHBOARD -->
|
||||
<div class="relative z-10 p-6 text-white">
|
||||
|
||||
<h2 class="text-2xl font-bold mb-6">Dashboard</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
|
||||
<!-- Monitoring -->
|
||||
<div onclick="window.location='dashboard.php?page=monitoring'"
|
||||
class="bg-white/80 backdrop-blur-md p-6 rounded-2xl shadow hover:shadow-xl cursor-pointer text-black">
|
||||
|
||||
<i class="fas fa-chart-line text-3xl text-blue-500 mb-3"></i>
|
||||
<h3 class="font-semibold text-lg">Monitoring</h3>
|
||||
<p class="text-sm text-gray-600">Lihat data suhu & kelembaban</p>
|
||||
</div>
|
||||
|
||||
<!-- Kontrol -->
|
||||
<div onclick="window.location='dashboard.php?page=kontrol'"
|
||||
class="bg-white/80 backdrop-blur-md p-6 rounded-2xl shadow hover:shadow-xl cursor-pointer text-black">
|
||||
|
||||
<i class="fas fa-sliders-h text-3xl text-green-500 mb-3"></i>
|
||||
<h3 class="font-semibold text-lg">Kontrol</h3>
|
||||
<p class="text-sm text-gray-600">Atur alat pengering</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
<?php session_start(); ?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Pengering Ikan IoT</title>
|
||||
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
body {
|
||||
background: url('assets/img/bg.jpg') center/cover no-repeat;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
.hidden-form { display: none; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="min-h-screen flex items-center justify-center p-4 relative">
|
||||
|
||||
<!-- 🔥 BLUR BACKGROUND -->
|
||||
<div class="absolute inset-0 bg-black/40 backdrop-blur-sm z-0"></div>
|
||||
|
||||
<!-- 🔥 CARD UTAMA -->
|
||||
<div class="relative z-10 max-w-4xl w-full flex flex-col md:flex-row
|
||||
rounded-3xl overflow-hidden shadow-2xl
|
||||
bg-white/20 backdrop-blur-xl border border-white/30">
|
||||
|
||||
<!-- 🔵 KIRI -->
|
||||
<div class="md:w-1/2 bg-blue-500/60 backdrop-blur-lg
|
||||
p-12 text-white flex flex-col justify-between items-center text-center">
|
||||
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">PENGERING IKAN IoT</h1>
|
||||
<div class="mt-8 overflow-hidden rounded-2xl shadow-xl">
|
||||
<img src="assets/img/ikan.jpg"
|
||||
class="w-full h-48 object-cover transition duration-300 hover:scale-110">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-sm italic mt-[-20px]">Monitoring real-time</p>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ⚪ KANAN -->
|
||||
<div class="md:w-1/2 p-8 md:p-12 bg-white/60 backdrop-blur-lg">
|
||||
|
||||
<div id="login-section">
|
||||
<?php include 'login_form.php'; ?>
|
||||
</div>
|
||||
|
||||
<div id="register-section" class="hidden-form">
|
||||
<?php include 'register_form.php'; ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleForm(type) {
|
||||
const login = document.getElementById('login-section');
|
||||
const register = document.getElementById('register-section');
|
||||
|
||||
if (type === 'register') {
|
||||
login.classList.add('hidden-form');
|
||||
register.classList.remove('hidden-form');
|
||||
} else {
|
||||
register.classList.add('hidden-form');
|
||||
login.classList.remove('hidden-form');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type="module" src="firebase-auth.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
<h2 class="text-2xl font-bold mb-6 text-gray-700">Kontrol Alat</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
|
||||
<!-- HEATER -->
|
||||
<div class="bg-white p-6 rounded-2xl shadow">
|
||||
<div class="flex justify-between items-center">
|
||||
<p class="font-semibold text-gray-700 flex items-center gap-2">
|
||||
🔥 Heater
|
||||
</p>
|
||||
<input type="checkbox" id="heaterToggle"
|
||||
onchange="setHeater(this)"
|
||||
class="w-6 h-6 cursor-pointer">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FAN -->
|
||||
<div class="bg-white p-6 rounded-2xl shadow">
|
||||
<div class="flex justify-between items-center">
|
||||
<p class="font-semibold text-gray-700 flex items-center gap-2">
|
||||
💨 Fan
|
||||
</p>
|
||||
<input type="checkbox" id="fanToggle"
|
||||
onchange="setFan(this)"
|
||||
class="w-6 h-6 cursor-pointer">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- JADWAL -->
|
||||
<div class="bg-white p-6 rounded-2xl shadow mt-6">
|
||||
<p class="font-semibold mb-3 text-gray-700">
|
||||
⏰ Jadwal Otomatis
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="text-sm text-gray-500">Start Time</label>
|
||||
<input id="startTime" type="time" class="border p-2 rounded-xl w-full">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm text-gray-500">End Time</label>
|
||||
<input id="endTime" type="time" class="border p-2 rounded-xl w-full">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button onclick="setSchedule()"
|
||||
class="mt-4 bg-purple-500 text-white px-6 py-2 rounded-xl">
|
||||
Simpan Jadwal
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- MODE -->
|
||||
<div class="bg-white p-4 rounded-2xl shadow mt-4">
|
||||
<p class="text-sm text-gray-500">Status Mode</p>
|
||||
|
||||
<div class="flex gap-3 mt-2">
|
||||
<span id="modeAuto"
|
||||
onclick="setModeAuto()"
|
||||
class="px-4 py-1 rounded-xl bg-gray-300 text-white cursor-pointer">
|
||||
CONTROL
|
||||
</span>
|
||||
<span id="modeManual"
|
||||
onclick="setModeManual()"
|
||||
class="px-4 py-1 rounded-xl bg-gray-300 text-white cursor-pointer">
|
||||
MANUAL
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
|
||||
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-app.js";
|
||||
import { getDatabase, ref, set, update, onValue } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-database.js";
|
||||
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyC-Wdu6vNC0dRyF33S3_C2wpVt9I6wnp0w",
|
||||
authDomain: "smartoven-f9fdd.firebaseapp.com",
|
||||
databaseURL: "https://smartoven-f9fdd-default-rtdb.firebaseio.com/",
|
||||
projectId: "smartoven-f9fdd",
|
||||
appId: "1:1005261996032:web:1bcb483b922275f08f98e5"
|
||||
};
|
||||
|
||||
const app = initializeApp(firebaseConfig);
|
||||
const db = getDatabase(app);
|
||||
|
||||
const heaterToggleEl = document.getElementById("heaterToggle");
|
||||
const fanToggleEl = document.getElementById("fanToggle");
|
||||
const startTimeEl = document.getElementById("startTime");
|
||||
const endTimeEl = document.getElementById("endTime");
|
||||
const modeAutoEl = document.getElementById("modeAuto");
|
||||
const modeManualEl = document.getElementById("modeManual");
|
||||
|
||||
let currentMode = 0; // 0 = CONTROL/AUTO, 1 = MANUAL
|
||||
|
||||
// ===== UI MODE =====
|
||||
const setModeUi = (mode) => {
|
||||
currentMode = mode;
|
||||
|
||||
if (mode === 0) {
|
||||
// CONTROL aktif
|
||||
modeAutoEl.classList.replace("bg-gray-300", "bg-green-500");
|
||||
modeManualEl.classList.replace("bg-green-500", "bg-gray-300");
|
||||
|
||||
heaterToggleEl.disabled = true;
|
||||
fanToggleEl.disabled = true;
|
||||
startTimeEl.disabled = false;
|
||||
endTimeEl.disabled = false;
|
||||
} else {
|
||||
// MANUAL aktif
|
||||
modeManualEl.classList.replace("bg-gray-300", "bg-green-500");
|
||||
modeAutoEl.classList.replace("bg-green-500", "bg-gray-300");
|
||||
|
||||
heaterToggleEl.disabled = false;
|
||||
fanToggleEl.disabled = false;
|
||||
startTimeEl.disabled = true;
|
||||
endTimeEl.disabled = true;
|
||||
}
|
||||
};
|
||||
|
||||
// ===== SET MODE =====
|
||||
window.setModeAuto = function () {
|
||||
set(ref(db, 'control/mode'), 0);
|
||||
};
|
||||
|
||||
window.setModeManual = function () {
|
||||
set(ref(db, 'control/mode'), 1);
|
||||
};
|
||||
|
||||
// ===== MANUAL CONTROL =====
|
||||
window.setHeater = function (el) {
|
||||
if (currentMode !== 1) {
|
||||
alert("Pakai mode MANUAL dulu!");
|
||||
el.checked = !el.checked;
|
||||
return;
|
||||
}
|
||||
set(ref(db, 'manual/heater'), el.checked ? 1 : 0);
|
||||
};
|
||||
|
||||
window.setFan = function (el) {
|
||||
if (currentMode !== 1) {
|
||||
alert("Pakai mode MANUAL dulu!");
|
||||
el.checked = !el.checked;
|
||||
return;
|
||||
}
|
||||
set(ref(db, 'manual/fan'), el.checked ? 1 : 0);
|
||||
};
|
||||
|
||||
// ===== JADWAL =====
|
||||
window.setSchedule = function () {
|
||||
const start = startTimeEl.value;
|
||||
const end = endTimeEl.value;
|
||||
|
||||
if (!start || !end) {
|
||||
alert("Isi waktu dulu!");
|
||||
return;
|
||||
}
|
||||
|
||||
const [sh, sm] = start.split(":").map(Number);
|
||||
const [eh, em] = end.split(":").map(Number);
|
||||
|
||||
update(ref(db, 'control'), {
|
||||
start_hour: sh,
|
||||
start_minute: sm,
|
||||
end_hour: eh,
|
||||
end_minute: em
|
||||
})
|
||||
.then(() => alert(`Jadwal disimpan!\nStart: ${start}\nEnd: ${end}`))
|
||||
.catch((err) => alert("Gagal simpan: " + err));
|
||||
};
|
||||
|
||||
// ===== LISTENERS =====
|
||||
|
||||
// Mode
|
||||
onValue(ref(db, 'control/mode'), (snap) => {
|
||||
const mode = snap.val();
|
||||
if (mode == null) return;
|
||||
setModeUi(mode);
|
||||
});
|
||||
|
||||
// Toggle manual (heater & fan)
|
||||
onValue(ref(db, 'manual'), (snap) => {
|
||||
const d = snap.val();
|
||||
if (!d) return;
|
||||
heaterToggleEl.checked = d.heater === 1;
|
||||
fanToggleEl.checked = d.fan === 1;
|
||||
});
|
||||
|
||||
// Jadwal — digabung dalam 1 listener
|
||||
onValue(ref(db, 'control'), (snap) => {
|
||||
const d = snap.val();
|
||||
if (!d) return;
|
||||
|
||||
if (d.start_hour != null && d.start_minute != null) {
|
||||
startTimeEl.value =
|
||||
String(d.start_hour).padStart(2, '0') + ":" +
|
||||
String(d.start_minute).padStart(2, '0');
|
||||
}
|
||||
|
||||
if (d.end_hour != null && d.end_minute != null) {
|
||||
endTimeEl.value =
|
||||
String(d.end_hour).padStart(2, '0') + ":" +
|
||||
String(d.end_minute).padStart(2, '0');
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
<?php ?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>Login - Pengering Ikan IoT</title>
|
||||
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
body { font-family: 'Poppins', sans-serif; }
|
||||
</style>
|
||||
</head>
|
||||
<body style="
|
||||
background-image: url('assets/img/bg.jpg');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
">
|
||||
|
||||
<body class="bg-[#f5f7fa] flex items-center justify-center min-h-screen">
|
||||
|
||||
<div class="bg-white/50 backdrop-blur-lg p-8 rounded-3xl shadow-2xl w-full max-w-md border border-white/20">
|
||||
|
||||
|
||||
<h2 class="text-2xl font-bold text-center mb-6">Login</h2>
|
||||
|
||||
<form id="loginForm" class="space-y-4">
|
||||
|
||||
<div>
|
||||
<label class="text-sm">Email</label>
|
||||
<input name="username" type="email"
|
||||
class="w-full mt-1 p-3 border rounded-xl"
|
||||
placeholder="email@gmail.com" required>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="text-sm">Password</label>
|
||||
<input name="password" type="password"
|
||||
class="w-full mt-1 p-3 border rounded-xl"
|
||||
placeholder="••••••" required>
|
||||
</div>
|
||||
|
||||
<button type="submit"
|
||||
class="w-full bg-blue-500 text-white p-3 rounded-xl font-semibold">
|
||||
MASUK
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
<p class="text-sm text-center mt-5">
|
||||
Belum punya akun?
|
||||
<a href="register.php" class="text-green-500 font-semibold">Daftar</a>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 🔥 FIREBASE LOGIN -->
|
||||
<script type="module">
|
||||
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-app.js";
|
||||
|
||||
import {
|
||||
getAuth,
|
||||
signInWithEmailAndPassword
|
||||
} from "https://www.gstatic.com/firebasejs/10.12.2/firebase-auth.js";
|
||||
|
||||
|
||||
// 🔥 CONFIG (ISI PUNYAMU)
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyC-Wdu6vNC0dRyF33S3_C2wpVt9I6wnp0w", // ❗ FIX (tadi double ")
|
||||
authDomain: "smartoven-f9fdd.firebaseapp.com",
|
||||
projectId: "smartoven-f9fdd", // 🔥 ISI BENER
|
||||
appId: "1:1005261996032:web:1bcb483b922275f08f98e5"
|
||||
};
|
||||
|
||||
const app = initializeApp(firebaseConfig);
|
||||
const auth = getAuth(app);
|
||||
|
||||
|
||||
// 🔥 LOGIN PROCESS
|
||||
const form = document.getElementById("loginForm");
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const email = document.querySelector('[name="username"]').value;
|
||||
const password = document.querySelector('[name="password"]').value;
|
||||
|
||||
try {
|
||||
await signInWithEmailAndPassword(auth, email, password);
|
||||
|
||||
// 🔥 KIRIM KE PHP SESSION
|
||||
await fetch("login_process.php", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/x-www-form-urlencoded"},
|
||||
body: `email=${email}`
|
||||
});
|
||||
|
||||
window.location.href = "dashboard.php";
|
||||
|
||||
} catch (err) {
|
||||
alert("Login gagal: " + err.message);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
session_start();
|
||||
|
||||
$_SESSION['logged_in'] = true;
|
||||
$_SESSION['user'] = $_POST['email'];
|
||||
|
||||
echo "ok";
|
||||
?>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
session_start();
|
||||
session_destroy();
|
||||
header("Location: index.php");
|
||||
exit;
|
||||
?>
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
<!-- HEADER -->
|
||||
<div class="mb-6 flex justify-between items-center">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-700">Dashboard Monitoring</h2>
|
||||
<p class="text-gray-500 text-sm">Realtime IoT Fish Dryer System</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
|
||||
<!-- Suhu -->
|
||||
<div class="bg-white p-5 rounded-2xl shadow hover:shadow-xl hover:-translate-y-1 transition">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-gray-500">Suhu</p>
|
||||
<i class="fas fa-thermometer-half text-red-400"></i>
|
||||
</div>
|
||||
<h2 id="suhu" class="text-3xl font-bold text-gray-700 mt-2">-- °C</h2>
|
||||
</div>
|
||||
|
||||
<!-- Kelembaban -->
|
||||
<div class="bg-white p-5 rounded-2xl shadow hover:shadow-xl hover:-translate-y-1 transition">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-gray-500">Kelembaban</p>
|
||||
<i class="fas fa-tint text-blue-400"></i>
|
||||
</div>
|
||||
<h2 id="kelembaban" class="text-3xl font-bold text-gray-700 mt-2">-- %</h2>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- GRAFIK -->
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
|
||||
<!-- GRAFIK -->
|
||||
<div class="bg-white p-6 rounded-2xl shadow h-[320px] flex flex-col">
|
||||
<h3 class="text-lg font-semibold mb-2 text-gray-700">
|
||||
Grafik Suhu & Kelembaban
|
||||
</h3>
|
||||
|
||||
<div class="flex-1">
|
||||
<canvas id="chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- STATUS DEVICE -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6 mb-6">
|
||||
|
||||
<!-- Heater -->
|
||||
<div class="bg-white border rounded-lg p-4 flex justify-between items-center">
|
||||
<p class="text-gray-600">Heater</p>
|
||||
<span id="heaterStatus" class="font-semibold text-gray-400">OFF</span>
|
||||
</div>
|
||||
|
||||
<!-- Fan -->
|
||||
<div class="bg-white border rounded-lg p-4 flex justify-between items-center">
|
||||
<p class="text-gray-600">Fan</p>
|
||||
<span id="fanStatus" class="font-semibold text-gray-400">OFF</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-app.js";
|
||||
import { getDatabase, ref, onValue } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-database.js";
|
||||
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyC-Wdu6vNC0dRyF33S3_C2wpVt9I6wnp0w",
|
||||
authDomain: "smartoven-f9fdd.firebaseapp.com",
|
||||
databaseURL: "https://smartoven-f9fdd-default-rtdb.firebaseio.com/",
|
||||
projectId: "smartoven-f9fdd",
|
||||
appId: "1:1005261996032:web:1bcb483b922275f08f98e5"
|
||||
};
|
||||
|
||||
const app = initializeApp(firebaseConfig);
|
||||
const db = getDatabase(app);
|
||||
|
||||
// ELEMENT
|
||||
const suhuEl = document.getElementById("suhu");
|
||||
const humEl = document.getElementById("kelembaban");
|
||||
const heaterEl = document.getElementById("heaterStatus");
|
||||
const fanEl = document.getElementById("fanStatus");
|
||||
|
||||
// ==========================
|
||||
// 📊 CHART FIX
|
||||
// ==========================
|
||||
const chart = new Chart(document.getElementById("chart"), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: "Suhu (°C)",
|
||||
data: [],
|
||||
borderColor: "#ef4444",
|
||||
backgroundColor: "rgba(239,68,68,0.1)",
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
},
|
||||
{
|
||||
label: "Kelembaban (%)",
|
||||
data: [],
|
||||
borderColor: "#3b82f6",
|
||||
backgroundColor: "rgba(59,130,246,0.1)",
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false
|
||||
}
|
||||
});
|
||||
|
||||
// ==========================
|
||||
// 🔥 SENSOR REALTIME
|
||||
// ==========================
|
||||
onValue(ref(db, 'sensor'), (snapshot) => {
|
||||
const data = snapshot.val();
|
||||
if (!data) return;
|
||||
|
||||
const time = new Date().toLocaleTimeString();
|
||||
|
||||
// 🔥 SESUAIKAN FIELD FIREBASE
|
||||
const suhu = data.temperature ?? data.suhu ?? 0;
|
||||
const hum = data.humidity ?? data.kelembaban ?? 0;
|
||||
|
||||
|
||||
// tampil ke card
|
||||
suhuEl.innerText = suhu + " °C";
|
||||
humEl.innerText = hum + " %";
|
||||
|
||||
// masuk ke chart
|
||||
chart.data.labels.push(time);
|
||||
chart.data.datasets[0].data.push(suhu);
|
||||
chart.data.datasets[1].data.push(hum);
|
||||
|
||||
// batasi data (biar ga numpuk)
|
||||
if (chart.data.labels.length > 10) {
|
||||
chart.data.labels.shift();
|
||||
chart.data.datasets.forEach(ds => ds.data.shift());
|
||||
}
|
||||
|
||||
chart.update();
|
||||
});
|
||||
onValue(ref(db, 'manual'), (snapshot) => {
|
||||
const data = snapshot.val();
|
||||
if (!data) return;
|
||||
|
||||
// HEATER
|
||||
if (data.heater == 1) {
|
||||
heaterEl.innerText = "🔥 ON";
|
||||
heaterEl.classList.remove("text-gray-400");
|
||||
heaterEl.classList.add("text-red-500");
|
||||
} else {
|
||||
heaterEl.innerText = "OFF";
|
||||
heaterEl.classList.remove("text-red-500");
|
||||
heaterEl.classList.add("text-gray-400");
|
||||
}
|
||||
|
||||
// FAN
|
||||
if (data.fan == 1) {
|
||||
fanEl.innerText = "🌀 ON";
|
||||
fanEl.classList.remove("text-gray-400");
|
||||
fanEl.classList.add("text-blue-500");
|
||||
} else {
|
||||
fanEl.innerText = "OFF";
|
||||
fanEl.classList.remove("text-blue-500");
|
||||
fanEl.classList.add("text-gray-400");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
<?php session_start(); ?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>Register - Pengering Ikan IoT</title>
|
||||
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
body { font-family: 'Poppins', sans-serif; }
|
||||
</style>
|
||||
</head>
|
||||
<body style="
|
||||
background-image: url('assets/img/bg.jpg');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
"></body>
|
||||
|
||||
<body class="bg-[#f5f7fa] flex items-center justify-center min-h-screen">
|
||||
|
||||
<div class="bg-white p-8 rounded-3xl shadow-sm w-full max-w-md">
|
||||
|
||||
<h2 class="text-2xl font-bold text-center mb-6">Daftar Akun</h2>
|
||||
|
||||
<form id="registerForm" class="space-y-4">
|
||||
|
||||
<div>
|
||||
<label class="text-sm">Username</label>
|
||||
<input name="reg_username" type="text"
|
||||
class="w-full mt-1 p-3 border rounded-xl"
|
||||
placeholder="Masukkan username" required>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="text-sm">Email</label>
|
||||
<input name="reg_email" type="email"
|
||||
class="w-full mt-1 p-3 border rounded-xl"
|
||||
placeholder="email@gmail.com" required>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="text-sm">Password</label>
|
||||
<input name="reg_password" type="password"
|
||||
class="w-full mt-1 p-3 border rounded-xl"
|
||||
placeholder="••••••" required>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="text-sm">Konfirmasi Password</label>
|
||||
<input name="reg_confirm" type="password"
|
||||
class="w-full mt-1 p-3 border rounded-xl"
|
||||
placeholder="••••••" required>
|
||||
</div>
|
||||
|
||||
<button type="submit"
|
||||
class="w-full bg-green-500 text-white p-3 rounded-xl font-semibold">
|
||||
DAFTAR
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
<p class="text-sm text-center mt-5">
|
||||
Sudah punya akun?
|
||||
<a href="index.php" class="text-blue-500 font-semibold">Login</a>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 🔥 FIREBASE REGISTER -->
|
||||
<script type="module">
|
||||
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-app.js";
|
||||
|
||||
import {
|
||||
getAuth,
|
||||
createUserWithEmailAndPassword
|
||||
} from "https://www.gstatic.com/firebasejs/10.12.2/firebase-auth.js";
|
||||
|
||||
import {
|
||||
getDatabase,
|
||||
ref,
|
||||
set
|
||||
} from "https://www.gstatic.com/firebasejs/10.12.2/firebase-database.js";
|
||||
|
||||
|
||||
// 🔥 CONFIG (ISI PUNYAMU)
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyC-Wdu6vNC0dRyF33S3_C2wpVt9I6wnp0w",
|
||||
authDomain: "smartoven-f9fdd.firebaseapp.com",
|
||||
databaseURL: "https://smartoven-f9fdd-default-rtdb.firebaseio.com/",
|
||||
projectId: "smartoven-f9fdd",
|
||||
appId: "1:1005261996032:web:1bcb483b922275f08f98e5"
|
||||
};
|
||||
|
||||
const app = initializeApp(firebaseConfig);
|
||||
const auth = getAuth(app);
|
||||
const db = getDatabase(app);
|
||||
|
||||
|
||||
// 🔥 REGISTER PROCESS
|
||||
const form = document.getElementById("registerForm");
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const username = document.querySelector('[name="reg_username"]').value;
|
||||
const email = document.querySelector('[name="reg_email"]').value;
|
||||
const password = document.querySelector('[name="reg_password"]').value;
|
||||
const confirm = document.querySelector('[name="reg_confirm"]').value;
|
||||
|
||||
if (password !== confirm) {
|
||||
alert("Password tidak sama!");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const userCred = await createUserWithEmailAndPassword(auth, email, password);
|
||||
const user = userCred.user;
|
||||
|
||||
// 🔥 SIMPAN KE DATABASE
|
||||
await set(ref(db, "users/" + user.uid), {
|
||||
username: username,
|
||||
email: email
|
||||
});
|
||||
|
||||
alert("Registrasi berhasil!");
|
||||
window.location.href = "index.php";
|
||||
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
<h2 class="text-2xl font-bold mb-6 text-gray-700">Riwayat Sensor</h2>
|
||||
|
||||
<div class="bg-white p-6 rounded-2xl shadow hover:shadow-xl transition">
|
||||
|
||||
<!-- HEADER -->
|
||||
<div class="flex flex-col md:flex-row md:justify-between md:items-center gap-3 mb-4">
|
||||
|
||||
<div>
|
||||
<p class="text-gray-700 font-semibold">Data Sensor</p>
|
||||
<p class="text-gray-400 text-sm">Realtime dari perangkat IoT</p>
|
||||
</div>
|
||||
|
||||
<input type="text" placeholder="🔍 Cari data..."
|
||||
class="border border-gray-200 px-4 py-2 rounded-xl text-sm focus:ring-2 focus:ring-blue-400 outline-none w-full md:w-64">
|
||||
</div>
|
||||
|
||||
<!-- TABLE -->
|
||||
<div class="overflow-y-auto max-h-72 rounded-xl border border-gray-100">
|
||||
|
||||
<table class="w-full text-sm">
|
||||
|
||||
<thead class="bg-gray-50 text-gray-500 uppercase text-xs sticky top-0">
|
||||
<tr>
|
||||
<th>Waktu</th>
|
||||
<th>Suhu</th>
|
||||
<th>Kelembaban</th>
|
||||
<th>Heater</th>
|
||||
<th>Fan</th>
|
||||
<th>Mode</th>
|
||||
<th>Active</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody id="historyTable" class="divide-y text-gray-600">
|
||||
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script type="module">
|
||||
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-app.js";
|
||||
import { getDatabase, ref, onValue } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-database.js";
|
||||
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyC-Wdu6vNC0dRyF33S3_C2wpVt9I6wnp0w",
|
||||
authDomain: "smartoven-f9fdd.firebaseapp.com",
|
||||
databaseURL: "https://smartoven-f9fdd-default-rtdb.firebaseio.com/",
|
||||
projectId: "smartoven-f9fdd",
|
||||
appId: "1:1005261996032:web:1bcb483b922275f08f98e5"
|
||||
};
|
||||
|
||||
const app = initializeApp(firebaseConfig);
|
||||
const db = getDatabase(app);
|
||||
|
||||
const table = document.getElementById("historyTable");
|
||||
|
||||
function fmtDateTime(epochSec) {
|
||||
const d = new Date(Number(epochSec) * 1000);
|
||||
// contoh: 20/05/2026 14.32.10 (format lokal)
|
||||
return d.toLocaleString('id-ID');
|
||||
}
|
||||
|
||||
function fmtNum(n, digits = 1) {
|
||||
const x = Number(n);
|
||||
if (!Number.isFinite(x)) return '--';
|
||||
return x.toFixed(digits);
|
||||
}
|
||||
|
||||
// ==========================
|
||||
// 🔥 LOAD HISTORY
|
||||
// ==========================
|
||||
onValue(ref(db, 'history'), (snapshot) => {
|
||||
const data = snapshot.val();
|
||||
if (!data) {
|
||||
table.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-4 text-gray-400">
|
||||
Belum ada data
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
table.innerHTML = "";
|
||||
|
||||
const rows = Object.keys(data)
|
||||
.map((key) => ({ key, ...data[key] }))
|
||||
.filter((r) => r && r.ts != null)
|
||||
.sort((a, b) => Number(b.ts) - Number(a.ts));
|
||||
|
||||
rows.forEach((r) => {
|
||||
const modeText = Number(r.mode) === 1 ? 'MANUAL' : 'AUTO';
|
||||
const heaterText = Number(r.heater) === 1 ? 'ON' : 'OFF';
|
||||
const fanText = Number(r.fan) === 1 ? 'ON' : 'OFF';
|
||||
const activeText = Number(r.active) === 1 ? 'YA' : 'TIDAK';
|
||||
|
||||
table.innerHTML += `
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-2 whitespace-nowrap">${fmtDateTime(r.ts)}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap">${fmtNum(r.temp)} °C</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap">${fmtNum(r.hum)} %</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap">${heaterText}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap">${fanText}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap">${modeText}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap">${activeText}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
Loading…
Reference in New Issue