From 349a275594c8a9da09aa71dafb25fe803dd5f60b Mon Sep 17 00:00:00 2001 From: flems Date: Thu, 25 Jun 2026 18:28:52 +0700 Subject: [PATCH] Fix notifikasi dan tambah auto-disconnect detection - Backend: mapping status ESP32 (SELESAI -> COMPLETED) - Backend: auto-detect disconnect setelah 30 detik no data - ESP32: kirim status relay setelah pengeringan selesai - Flutter: tambah debug log dan UI untuk status DISCONNECTED - Flutter: perbaiki overflow di device card - Notifikasi pengeringan selesai sudah berfungsi dengan baik --- backend/database.js | 10 + backend/server.js | 107 +++++- esp32_pengering_ikan_railway.ino | 393 +++++++++++++++++---- lib/screens/dashboard_screen_with_api.dart | 46 ++- lib/services/notification_service.dart | 81 ++--- 5 files changed, 492 insertions(+), 145 deletions(-) diff --git a/backend/database.js b/backend/database.js index 8d97bf6..b1372aa 100644 --- a/backend/database.js +++ b/backend/database.js @@ -153,6 +153,16 @@ async function insertSensorData(data) { const relay3 = data.relay3 ? 1 : 0; const relay4 = data.relay4 ? 1 : 0; + // Debug logging + console.log('📝 Inserting sensor data:'); + console.log(' Raw relay values:', { + relay1_raw: data.relay1, + relay2_raw: data.relay2, + relay3_raw: data.relay3, + relay4_raw: data.relay4 + }); + console.log(' Converted relay values:', { relay1, relay2, relay3, relay4 }); + const [result] = await pool.query( `INSERT INTO sensor_data (suhu, berat, target, relay1, relay2, relay3, relay4, status, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, diff --git a/backend/server.js b/backend/server.js index 66acee9..895f2e5 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,3 +1,5 @@ +console.log('🔄 SERVER STARTING - Version 1.0.3-relay-fix - ' + new Date().toISOString()); + const express = require('express'); const cors = require('cors'); const morgan = require('morgan'); @@ -50,6 +52,30 @@ let latestData = { timestamp: new Date().toISOString() }; +// Last data received timestamp +let lastDataTimestamp = Date.now(); + +// Connection timeout (30 seconds) +const CONNECTION_TIMEOUT = 30000; + +// Check connection status periodically +setInterval(() => { + const now = Date.now(); + const timeSinceLastData = now - lastDataTimestamp; + + // If no data received for more than CONNECTION_TIMEOUT, mark as disconnected + if (timeSinceLastData > CONNECTION_TIMEOUT && latestData.status !== 'DISCONNECTED') { + console.log('⚠️ No data received for', Math.floor(timeSinceLastData / 1000), 'seconds'); + console.log(' Marking as DISCONNECTED'); + latestData.status = 'DISCONNECTED'; + // Optionally reset relay states + latestData.relay1 = false; + latestData.relay2 = false; + latestData.relay3 = false; + latestData.relay4 = false; + } +}, 5000); // Check every 5 seconds + // Initialize database on startup (async () => { try { @@ -148,23 +174,62 @@ mqttClient.on('message', async (topic, message) => { try { const data = JSON.parse(msg); + // Debug: log parsed data + console.log('📦 Parsed MQTT data:', JSON.stringify(data, null, 2)); + console.log(' Data type check:', { + relay1_type: typeof data.relay1, + relay1_value: data.relay1, + relay2_type: typeof data.relay2, + relay2_value: data.relay2, + relay3_type: typeof data.relay3, + relay3_value: data.relay3, + relay4_type: typeof data.relay4, + relay4_value: data.relay4 + }); + // Get current time in WIB (UTC+7) const now = new Date(); const wibTime = new Date(now.getTime() + (7 * 60 * 60 * 1000)); + // Update last data timestamp + lastDataTimestamp = Date.now(); + + // Helper function to map ESP32 status to backend status + const mapStatus = (esp32Status) => { + if (!esp32Status) return latestData.status; + const statusMap = { + 'SELESAI': 'COMPLETED', + 'BERJALAN': 'RUNNING', + 'SCANNING': 'SCANNING', + 'READY': 'READY', + 'CONNECTED': 'CONNECTED', + 'ERROR': 'ERROR' + }; + return statusMap[esp32Status.toUpperCase()] || esp32Status; + }; + // Update latestData dengan data dari ESP32 (termasuk status relay) + // Use explicit check for undefined/null to handle false values correctly latestData = { suhu: data.suhu || 0, berat: data.berat || 0, target: data.target || 0, - relay1: data.relay1 !== undefined ? data.relay1 : latestData.relay1, // Update dari ESP32 - relay2: data.relay2 !== undefined ? data.relay2 : latestData.relay2, - relay3: data.relay3 !== undefined ? data.relay3 : latestData.relay3, - relay4: data.relay4 !== undefined ? data.relay4 : latestData.relay4, - status: latestData.status, // Keep current status + relay1: (data.relay1 !== undefined && data.relay1 !== null) ? Boolean(data.relay1) : latestData.relay1, + relay2: (data.relay2 !== undefined && data.relay2 !== null) ? Boolean(data.relay2) : latestData.relay2, + relay3: (data.relay3 !== undefined && data.relay3 !== null) ? Boolean(data.relay3) : latestData.relay3, + relay4: (data.relay4 !== undefined && data.relay4 !== null) ? Boolean(data.relay4) : latestData.relay4, + status: mapStatus(data.status), // Map ESP32 status to backend status timestamp: wibTime.toISOString() }; + console.log(' Final latestData relay values:', { + relay1: latestData.relay1, + relay2: latestData.relay2, + relay3: latestData.relay3, + relay4: latestData.relay4, + status: latestData.status + }); + // Save to database try { await db.insertSensorData(latestData); @@ -305,16 +370,34 @@ aedes.on('publish', async (packet, client) => { try { const data = JSON.parse(message); + // Update last data timestamp + lastDataTimestamp = Date.now(); + + // Helper function to map ESP32 status to backend status + const mapStatus = (esp32Status) => { + if (!esp32Status) return latestData.status; + const statusMap = { + 'SELESAI': 'COMPLETED', + 'BERJALAN': 'RUNNING', + 'SCANNING': 'SCANNING', + 'READY': 'READY', + 'CONNECTED': 'CONNECTED', + 'ERROR': 'ERROR' + }; + return statusMap[esp32Status.toUpperCase()] || esp32Status; + }; + // Update latestData dengan data dari ESP32 (termasuk status relay) + // Use explicit check for undefined/null to handle false values correctly latestData = { suhu: data.suhu || 0, berat: data.berat || 0, target: data.target || 0, - relay1: data.relay1 !== undefined ? data.relay1 : latestData.relay1, // Update dari ESP32 - relay2: data.relay2 !== undefined ? data.relay2 : latestData.relay2, - relay3: data.relay3 !== undefined ? data.relay3 : latestData.relay3, - relay4: data.relay4 !== undefined ? data.relay4 : latestData.relay4, - status: latestData.status, // Keep current status + relay1: (data.relay1 !== undefined && data.relay1 !== null) ? Boolean(data.relay1) : latestData.relay1, + relay2: (data.relay2 !== undefined && data.relay2 !== null) ? Boolean(data.relay2) : latestData.relay2, + relay3: (data.relay3 !== undefined && data.relay3 !== null) ? Boolean(data.relay3) : latestData.relay3, + relay4: (data.relay4 !== undefined && data.relay4 !== null) ? Boolean(data.relay4) : latestData.relay4, + status: mapStatus(data.status), // Map ESP32 status to backend status timestamp: new Date().toISOString() }; @@ -466,7 +549,7 @@ app.get('/', (req, res) => { res.json({ status: 'OK', message: 'Pengering Ikan Backend Server - MQTT Client Enabled', - version: '1.0.1', + version: '1.0.3-relay-fix', // Updated version with relay boolean fix uptime: process.uptime(), timestamp: new Date().toISOString() }); @@ -783,7 +866,7 @@ app.use((err, req, res, next) => { const server = app.listen(PORT, '0.0.0.0', () => { console.log(''); console.log('='.repeat(60)); - console.log('🚀 PENGERING IKAN BACKEND SERVER'); + console.log('🚀 PENGERING IKAN BACKEND SERVER v1.0.3-relay-fix'); console.log('='.repeat(60)); console.log(`📡 REST API: http://0.0.0.0:${PORT}`); console.log(`🔗 MQTT Broker: ${MQTT_BROKER_URL}`); diff --git a/esp32_pengering_ikan_railway.ino b/esp32_pengering_ikan_railway.ino index 39167e4..16204b1 100644 --- a/esp32_pengering_ikan_railway.ino +++ b/esp32_pengering_ikan_railway.ino @@ -50,8 +50,8 @@ DHT dht(DHTPIN, DHTTYPE); // ===================================================== #define DT1 25 #define SCK1 26 -#define DT2 32 -#define SCK2 33 +#define DT2 33 // Sesuai dengan kode test yang working +#define SCK2 32 // Sesuai dengan kode test yang working HX711 scale1; HX711 scale2; @@ -64,10 +64,10 @@ HX711 scale2; #define RELAY_EXHAUST 27 // RELAY4 - Exhaust // ===================================================== -// KALIBRASI +// KALIBRASI LOADCELL // ===================================================== -float calibration_factor1 = 208.0; -float calibration_factor2 = 208.0; +float calibration_factor1 = 709.0; // Sesuai dengan kode test yang working +float calibration_factor2 = 709.0; // Sesuai dengan kode test yang working // ===================================================== // VARIABEL @@ -100,7 +100,7 @@ void setup_wifi() { lcd.setCursor(0, 0); lcd.print("Connecting WiFi"); lcd.setCursor(0, 1); - lcd.print("Please wait..."); + lcd.print("Please wait"); Serial.print("Connecting to: "); Serial.println(ssid); @@ -108,14 +108,31 @@ void setup_wifi() { WiFi.begin(ssid, password); int attempts = 0; - while (WiFi.status() != WL_CONNECTED && attempts < 20) { + int maxAttempts = 20; + + // Loop dengan update LCD setiap detik untuk mencegah stuck + while (WiFi.status() != WL_CONNECTED && attempts < maxAttempts) { delay(500); Serial.print("."); + + // Update LCD setiap 2 attempts (1 detik) + if (attempts % 2 == 0) { + lcd.setCursor(0, 1); + lcd.print(" "); // Clear line + lcd.setCursor(0, 1); + lcd.print("Wait "); + lcd.print(attempts / 2); + lcd.print("s/"); + lcd.print(maxAttempts / 2); + lcd.print("s"); + } + attempts++; } + Serial.println(); // New line after dots + if (WiFi.status() == WL_CONNECTED) { - Serial.println(); Serial.println("WiFi Connected"); Serial.print("IP Address : "); Serial.println(WiFi.localIP()); @@ -129,7 +146,6 @@ void setup_wifi() { lcd.print(WiFi.localIP()); delay(2000); } else { - Serial.println(); Serial.println("WiFi Connection Failed!"); // Tampilkan error di LCD @@ -231,24 +247,34 @@ void callback(char* topic, byte* payload, unsigned int length) { Serial.print("Scale 2 ready: "); Serial.println(scale2Ready ? "YES" : "NO"); - if (scale1Ready) { + if (scale1Ready && scale2Ready) { + // Tare dengan tracking waktu + unsigned long tareStart = millis(); scale1.tare(); + unsigned long tare1Time = millis() - tareStart; + Serial.print("Scale 1 tared in "); + Serial.print(tare1Time); + Serial.println("ms"); delay(100); - Serial.println("Scale 1 tared"); - } else { - Serial.println("Scale 1 NOT READY - skipping tare"); - } - - if (scale2Ready) { + + tareStart = millis(); scale2.tare(); + unsigned long tare2Time = millis() - tareStart; + Serial.print("Scale 2 tared in "); + Serial.print(tare2Time); + Serial.println("ms"); delay(100); - Serial.println("Scale 2 tared"); + + Serial.println("Scales tare complete"); } else { - Serial.println("Scale 2 NOT READY - skipping tare"); + Serial.println("Scale(s) NOT READY - skipping tare"); + + if (!scale1Ready) Serial.println("Scale 1 not responding"); + if (!scale2Ready) Serial.println("Scale 2 not responding"); + + client.publish(topicStatus, "WARNING: Scale not ready"); } - Serial.println("Scales tare complete"); - modePengeringan = true; pengeringanSelesai = false; beratTersimpan = false; @@ -280,6 +306,47 @@ void callback(char* topic, byte* payload, unsigned int length) { tampilReady(); client.publish(topicStatus, "PENGERINGAN SIAP"); } + // ================================================= + // TARE COMMAND (Kalibrasi ulang load cell ke 0) + // ================================================= + else if (message == "TARE") { + Serial.println("TARE COMMAND received"); + + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("Taring..."); + lcd.setCursor(0, 1); + lcd.print("Remove all load"); + + delay(2000); + + bool scale1Ready = scale1.wait_ready_timeout(1000); + bool scale2Ready = scale2.wait_ready_timeout(1000); + + if (scale1Ready && scale2Ready) { + scale1.tare(10); + delay(100); + scale2.tare(10); + delay(100); + + Serial.println("Manual tare successful"); + client.publish(topicStatus, "TARE COMPLETED"); + + lcd.setCursor(0, 1); + lcd.print("Done! "); + delay(1000); + } else { + Serial.println("TARE FAILED - scales not ready"); + client.publish(topicStatus, "TARE FAILED"); + + lcd.setCursor(0, 1); + lcd.print("Failed! "); + delay(2000); + } + + lcd.clear(); + tampilReady(); + } } } @@ -288,16 +355,25 @@ void callback(char* topic, byte* payload, unsigned int length) { // ===================================================== void reconnect() { int attempts = 0; - while (!client.connected() && attempts < 3) { - Serial.println("Connecting MQTT..."); + int maxAttempts = 3; + + while (!client.connected() && attempts < maxAttempts) { + Serial.print("Connecting MQTT (attempt "); + Serial.print(attempts + 1); + Serial.print("/"); + Serial.print(maxAttempts); + Serial.println(")..."); - // Tampilkan status di LCD saat connecting MQTT pertama kali - if (attempts == 0 && modePengeringan == false && pengeringanSelesai == false) { + // Tampilkan status di LCD dengan update setiap attempt + if (modePengeringan == false && pengeringanSelesai == false) { lcd.clear(); lcd.setCursor(0, 0); lcd.print("Connecting MQTT"); lcd.setCursor(0, 1); - lcd.print("Please wait..."); + lcd.print("Attempt "); + lcd.print(attempts + 1); + lcd.print("/"); + lcd.print(maxAttempts); } // Generate unique client ID @@ -326,12 +402,39 @@ void reconnect() { lcd.print("System Ready"); delay(2000); } + + return; // Keluar dari fungsi setelah berhasil connect } else { - Serial.print("MQTT Failed : "); - Serial.println(client.state()); + Serial.print("MQTT Failed, rc="); + Serial.print(client.state()); + Serial.println(" retrying..."); + + // Update LCD menunjukkan gagal + if (modePengeringan == false && pengeringanSelesai == false) { + lcd.setCursor(0, 1); + lcd.print("Failed! Retry..."); + } + attempts++; - delay(2000); + + if (attempts < maxAttempts) { + delay(2000); // Tunggu sebelum retry + } + } + } + + // Jika gagal setelah semua attempts + if (!client.connected()) { + Serial.println("MQTT Connection failed after all attempts"); + + if (modePengeringan == false && pengeringanSelesai == false) { + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("MQTT Failed!"); + lcd.setCursor(0, 1); + lcd.print("Check network"); + delay(3000); } } } @@ -389,21 +492,107 @@ void setup() { dht.begin(); delay(100); - // Setup HX711 - SKIP TARE untuk cepat boot + // Setup HX711 Serial.println("Initializing HX711..."); + + // Inisialisasi HX711 (style kode test yang working) scale1.begin(DT1, SCK1); + Serial.println("HX711 #1 begin OK"); + scale2.begin(DT2, SCK2); - delay(100); + Serial.println("HX711 #2 begin OK"); scale1.set_scale(calibration_factor1); scale2.set_scale(calibration_factor2); - delay(100); + Serial.println("Calibration factor applied"); - // SKIP tare - akan dilakukan saat mulai pengeringan - // scale1.tare(); - // scale2.tare(); + // Update LCD + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("Checking scales"); + lcd.setCursor(0, 1); + lcd.print("Please wait..."); - Serial.println("HX711 initialized (tare skipped)"); + // Menunggu HX711 siap dengan timeout 10 detik (style kode test) + Serial.println("Menunggu HX711 siap..."); + unsigned long startTime = millis(); + bool scale1Ready = false; + bool scale2Ready = false; + + while ((!scale1.is_ready() || !scale2.is_ready()) && + millis() - startTime < 10000) { + + scale1Ready = scale1.is_ready(); + scale2Ready = scale2.is_ready(); + + Serial.print("HX1: "); + Serial.print(scale1Ready ? "1" : "0"); + Serial.print(" | HX2: "); + Serial.println(scale2Ready ? "1" : "0"); + + // Update LCD dengan status + lcd.setCursor(0, 1); + lcd.print("HX1:"); + lcd.print(scale1Ready ? "OK" : ".."); + lcd.print(" HX2:"); + lcd.print(scale2Ready ? "OK" : ".."); + lcd.print(" "); + + delay(500); + + // Jika keduanya sudah ready, keluar dari loop + if (scale1Ready && scale2Ready) break; + } + + Serial.println(); + + // Check final status + scale1Ready = scale1.is_ready(); + scale2Ready = scale2.is_ready(); + + if (!scale1Ready) { + Serial.println("ERROR: HX711 #1 tidak terdeteksi!"); + } else { + Serial.println("HX711 #1 siap"); + } + + if (!scale2Ready) { + Serial.println("ERROR: HX711 #2 tidak terdeteksi!"); + } else { + Serial.println("HX711 #2 siap"); + } + + // Tare hanya jika siap (style kode test) + if (scale1Ready) { + Serial.println("Tare HX711 #1..."); + lcd.setCursor(0, 1); + lcd.print("Taring scale 1.."); + scale1.tare(); + delay(100); + } + + if (scale2Ready) { + Serial.println("Tare HX711 #2..."); + lcd.setCursor(0, 1); + lcd.print("Taring scale 2.."); + scale2.tare(); + delay(100); + } + + if (scale1Ready || scale2Ready) { + lcd.setCursor(0, 1); + lcd.print("Tare complete! "); + delay(1000); + } else { + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("Scale Warning!"); + lcd.setCursor(0, 1); + lcd.print("Check wiring"); + delay(3000); + } + + Serial.println("HX711 Setup selesai"); // Setup Relay (Active LOW) pinMode(RELAY_HEATER1, OUTPUT); @@ -454,13 +643,15 @@ void loop() { if (!modePengeringan && !pengeringanSelesai) { Serial.println("START PENGERINGAN"); - // Tare HX711 saat mulai pengeringan dengan timeout + // Tare HX711 saat mulai pengeringan (style kode test) Serial.println("Taring scales..."); lcd.clear(); lcd.setCursor(0, 0); - lcd.print("Calibrating..."); + lcd.print("Zeroing..."); lcd.setCursor(0, 1); - lcd.print("Please wait"); + lcd.print("Remove all load"); + + delay(2000); // Kasih waktu user untuk pastikan rak kosong // Cek apakah scale ready sebelum tare bool scale1Ready = scale1.is_ready(); @@ -471,20 +662,50 @@ void loop() { Serial.print("Scale 2 ready: "); Serial.println(scale2Ready ? "YES" : "NO"); - if (scale1Ready) { + bool tareSuccess = false; + + if (scale1Ready && scale2Ready) { + lcd.setCursor(0, 1); + lcd.print("Calibrating... "); + scale1.tare(); - delay(100); Serial.println("Scale 1 tared"); + delay(100); + + scale2.tare(); + Serial.println("Scale 2 tared"); + delay(100); + + lcd.setCursor(0, 1); + lcd.print("Done! Put fish "); + delay(1000); + + tareSuccess = true; } else { - Serial.println("Scale 1 NOT READY - skipping tare"); + if (!scale1Ready) Serial.println("Scale 1 not ready!"); + if (!scale2Ready) Serial.println("Scale 2 not ready!"); } - if (scale2Ready) { - scale2.tare(); - delay(100); - Serial.println("Scale 2 tared"); - } else { - Serial.println("Scale 2 NOT READY - skipping tare"); + if (!tareSuccess) { + Serial.println("WARNING: Tare FAILED"); + + lcd.setCursor(0, 1); + lcd.print("Scale Error! "); + delay(2000); + + // Batal start jika scale tidak ready + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("Cannot Start!"); + lcd.setCursor(0, 1); + lcd.print("Check scales"); + delay(3000); + + lcd.clear(); + tampilReady(); + + lastButtonState = buttonState; + return; // Keluar dari loop, jangan lanjut start } Serial.println("Scales tare complete"); @@ -541,21 +762,14 @@ void loop() { float berat1 = 0; float berat2 = 0; - // Cek scale ready dengan timeout untuk menghindari hang - if (scale1.wait_ready_timeout(200)) { - if (scale1.is_ready()) { - berat1 = scale1.get_units(5); - } - } else { - Serial.println("Scale 1 timeout"); + // Cek dan baca scale 1 (style kode test) + if (scale1.is_ready()) { + berat1 = scale1.get_units(5); } - if (scale2.wait_ready_timeout(200)) { - if (scale2.is_ready()) { - berat2 = scale2.get_units(5); - } - } else { - Serial.println("Scale 2 timeout"); + // Cek dan baca scale 2 (style kode test) + if (scale2.is_ready()) { + berat2 = scale2.get_units(5); } // ===================================================== @@ -622,6 +836,9 @@ void loop() { Serial.println("PENGERINGAN SELESAI"); client.publish(topicStatus, "PENGERINGAN SELESAI"); + // Kirim data dengan status COMPLETED untuk trigger notifikasi + kirimStatusRelay(suhu, berat); + // Tampilkan selesai selama 5 detik lcd.clear(); lcd.setCursor(0, 0); @@ -674,20 +891,35 @@ void loop() { } // ===================================================== - // RELAY CONTROL berdasarkan suhu - // Heater 1 & 2 ON, Fan ON - // Exhaust ON jika suhu >= 60°C + // RELAY CONTROL berdasarkan status dan suhu + // Relay HANYA ON setelah berat_awal & target tersimpan // ===================================================== - digitalWrite(RELAY_HEATER1, LOW); // HEATER 1 ON - digitalWrite(RELAY_HEATER2, LOW); // HEATER 2 ON - digitalWrite(RELAY_FAN, LOW); // FAN ON - - // Exhaust kontrol otomatis berdasarkan suhu - if (suhu >= 60.0) { - digitalWrite(RELAY_EXHAUST, LOW); // EXHAUST ON - Serial.println("EXHAUST ON (Suhu >= 60C)"); + if (beratTersimpan) { + // Berat sudah tersimpan, mulai pengeringan + + // FAN selalu ON saat pengeringan + digitalWrite(RELAY_FAN, LOW); // FAN ON + + // Kontrol HEATER dan EXHAUST berdasarkan suhu + if (suhu >= 60.0) { + // Suhu sudah tinggi (≥60°C) + digitalWrite(RELAY_HEATER1, HIGH); // HEATER 1 OFF + digitalWrite(RELAY_HEATER2, HIGH); // HEATER 2 OFF + digitalWrite(RELAY_EXHAUST, LOW); // EXHAUST ON untuk buang panas + Serial.println("Suhu >= 60C: HEATER OFF, EXHAUST ON"); + } else { + // Suhu masih rendah (<60°C) + digitalWrite(RELAY_HEATER1, LOW); // HEATER 1 ON + digitalWrite(RELAY_HEATER2, LOW); // HEATER 2 ON + digitalWrite(RELAY_EXHAUST, HIGH); // EXHAUST OFF + Serial.println("Suhu < 60C: HEATER ON, EXHAUST OFF"); + } } else { - digitalWrite(RELAY_EXHAUST, HIGH); // EXHAUST OFF + // Masih scanning berat, relay tetap OFF + digitalWrite(RELAY_HEATER1, HIGH); // HEATER 1 OFF + digitalWrite(RELAY_HEATER2, HIGH); // HEATER 2 OFF + digitalWrite(RELAY_FAN, HIGH); // FAN OFF + digitalWrite(RELAY_EXHAUST, HIGH); // EXHAUST OFF } // ===================================================== @@ -737,7 +969,19 @@ void kirimStatusRelay(float suhu, float berat) { bool fan_on = (digitalRead(RELAY_FAN) == LOW); bool exhaust_on = (digitalRead(RELAY_EXHAUST) == LOW); - // Format JSON dengan status relay + // Tentukan status string + String statusString = "UNKNOWN"; + if (pengeringanSelesai) { + statusString = "SELESAI"; + } else if (modePengeringan && beratTersimpan) { + statusString = "BERJALAN"; + } else if (modePengeringan && !beratTersimpan) { + statusString = "SCANNING"; + } else { + statusString = "READY"; + } + + // Format JSON dengan status relay DAN status String payload = "{"; payload += "\"suhu\":" + String(suhu, 1) + ","; payload += "\"berat\":" + String(berat, 0) + ","; @@ -745,7 +989,8 @@ void kirimStatusRelay(float suhu, float berat) { payload += "\"relay1\":" + String(heater1_on ? "true" : "false") + ","; payload += "\"relay2\":" + String(heater2_on ? "true" : "false") + ","; payload += "\"relay3\":" + String(fan_on ? "true" : "false") + ","; - payload += "\"relay4\":" + String(exhaust_on ? "true" : "false"); + payload += "\"relay4\":" + String(exhaust_on ? "true" : "false") + ","; + payload += "\"status\":\"" + statusString + "\""; payload += "}"; // Publish ke topic data diff --git a/lib/screens/dashboard_screen_with_api.dart b/lib/screens/dashboard_screen_with_api.dart index 159aad3..eb2a23f 100644 --- a/lib/screens/dashboard_screen_with_api.dart +++ b/lib/screens/dashboard_screen_with_api.dart @@ -101,13 +101,21 @@ class _DashboardScreenWithApiState extends State { // Update status dan trigger notifikasi String newStatus = data.status; - // Notifikasi berdasarkan perubahan status + // Notifikasi hanya untuk pengeringan selesai if (newStatus != _previousStatus) { - if (newStatus == 'READY' && _previousStatus != 'READY') { - _notificationManager.notifyPengeringanDimulai(); - } else if (newStatus == 'BERJALAN' && _previousStatus != 'BERJALAN') { - _notificationManager.notifyIkanTerdeteksi(berat); - } else if (newStatus == 'SELESAI' && _previousStatus != 'SELESAI') { + // HAPUS notifikasi untuk status lain + // if (newStatus == 'READY' && _previousStatus != 'READY') { + // _notificationManager.notifyPengeringanDimulai(); + // } else if (newStatus == 'BERJALAN' && _previousStatus != 'BERJALAN') { + // _notificationManager.notifyIkanTerdeteksi(berat); + // } + + // HANYA notifikasi pengeringan selesai + if (newStatus == 'SELESAI' && _previousStatus != 'SELESAI') { + print('🔔 Dashboard: Status changed to SELESAI'); + print(' Previous: $_previousStatus → Current: $newStatus'); + print(' Berat: $berat'); + print(' Triggering notification...'); _notificationManager.notifyPengeringanSelesai(berat); } @@ -272,6 +280,7 @@ class _DashboardScreenWithApiState extends State { Widget _buildStatusCard() { Color statusColor; IconData statusIcon; + String statusText = statusSistem; switch (statusSistem) { case 'SELESAI': @@ -297,6 +306,7 @@ class _DashboardScreenWithApiState extends State { case 'DISCONNECTED': statusColor = Colors.red; statusIcon = Icons.error_outline; + statusText = 'TERPUTUS'; break; case 'CONNECTING': statusColor = Colors.orange; @@ -348,7 +358,7 @@ class _DashboardScreenWithApiState extends State { ), const SizedBox(height: 4), Text( - statusSistem, + statusText, style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, @@ -357,7 +367,9 @@ class _DashboardScreenWithApiState extends State { ), const SizedBox(height: 4), Text( - lastStatusMessage, + statusSistem == 'DISCONNECTED' + ? 'ESP32 tidak terhubung. Periksa koneksi perangkat.' + : lastStatusMessage, style: const TextStyle( fontSize: 12, color: Colors.grey, @@ -462,7 +474,7 @@ class _DashboardScreenWithApiState extends State { mainAxisSize: MainAxisSize.min, children: [ Container( - padding: const EdgeInsets.all(10), + padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: status ? iconColor.withOpacity(0.1) : Colors.grey.shade100, shape: BoxShape.circle, @@ -470,29 +482,31 @@ class _DashboardScreenWithApiState extends State { child: Icon( icon, color: status ? iconColor : Colors.grey, - size: 28, + size: 24, ), ), - const SizedBox(height: 8), + const SizedBox(height: 4), Text( label, style: const TextStyle( - fontSize: 12, + fontSize: 11, fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - const SizedBox(height: 6), + const SizedBox(height: 3), Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: status ? iconColor.withOpacity(0.15) : Colors.grey.shade100, - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(8), ), child: Text( status ? 'ON' : 'OFF', style: TextStyle( - fontSize: 11, + fontSize: 10, fontWeight: FontWeight.bold, color: status ? iconColor : Colors.grey, ), diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 7e751f9..028ec6b 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -1,5 +1,6 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; class NotificationService { static final NotificationService _instance = NotificationService._internal(); @@ -57,29 +58,43 @@ class NotificationService { required String title, required String body, String? payload, + bool playSound = true, + bool showBadge = true, }) async { if (!_initialized) { await initialize(); } - const androidDetails = AndroidNotificationDetails( + final androidDetails = AndroidNotificationDetails( 'pengering_ikan_channel', 'Pengering Ikan', channelDescription: 'Notifikasi untuk sistem pengering ikan', - importance: Importance.high, - priority: Priority.high, + importance: Importance.max, + priority: Priority.max, showWhen: true, enableVibration: true, - playSound: true, + playSound: playSound, + enableLights: true, + color: const Color(0xFF2196F3), + ledColor: const Color(0xFF2196F3), + ledOnMs: 1000, + ledOffMs: 500, + ongoing: false, + autoCancel: true, + channelShowBadge: showBadge, + visibility: NotificationVisibility.public, + fullScreenIntent: true, ); const iosDetails = DarwinNotificationDetails( presentAlert: true, presentBadge: true, presentSound: true, + sound: 'default', + badgeNumber: 1, ); - const details = NotificationDetails( + final details = NotificationDetails( android: androidDetails, iOS: iosDetails, ); @@ -92,51 +107,31 @@ class NotificationService { details, payload: payload, ); - print('📱 Notification shown: $title'); + print('📱 Notification shown: $title - $body'); } catch (e) { print('❌ Error showing notification: $e'); } } - // Show notification for pengeringan dimulai - Future notifyPengeringanDimulai() async { - await showNotification( - id: 1, - title: '🚀 Pengeringan Dimulai', - body: 'Proses pengeringan ikan telah dimulai', - payload: 'pengeringan_dimulai', - ); - } - - // Show notification for ikan terdeteksi - Future notifyIkanTerdeteksi(double berat) async { - await showNotification( - id: 2, - title: '🐟 Ikan Terdeteksi', - body: 'Berat ikan: ${berat.toStringAsFixed(0)} gram', - payload: 'ikan_terdeteksi', - ); - } - - // Show notification for pengeringan berjalan - Future notifyPengeringanBerjalan(double berat, double target) async { - double progress = ((1 - (berat - target) / (berat - target)) * 100).clamp(0, 100); - await showNotification( - id: 3, - title: '⏳ Pengeringan Berjalan', - body: 'Berat: ${berat.toStringAsFixed(0)}g | Target: ${target.toStringAsFixed(0)}g', - payload: 'pengeringan_berjalan', - ); - } - // Show notification for pengeringan selesai Future notifyPengeringanSelesai(double beratAkhir) async { - await showNotification( - id: 4, - title: '✅ Pengeringan Selesai', - body: 'Target tercapai! Berat akhir: ${beratAkhir.toStringAsFixed(0)} gram', - payload: 'pengeringan_selesai', - ); + print('🔔 NotificationService: Showing pengeringan selesai notification'); + print(' Initialized: $_initialized'); + print(' Berat akhir: $beratAkhir'); + + try { + await showNotification( + id: 4, + title: '✅ Pengeringan Selesai', + body: 'Target tercapai! Berat akhir: ${beratAkhir.toStringAsFixed(0)} gram', + payload: 'pengeringan_selesai', + playSound: true, + showBadge: true, + ); + print('✅ NotificationService: Notification sent successfully'); + } catch (e) { + print('❌ NotificationService: Error sending notification: $e'); + } } // Show notification for suhu tinggi