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
This commit is contained in:
flems 2026-06-25 18:28:52 +07:00
parent ef9a79df9e
commit 349a275594
5 changed files with 492 additions and 145 deletions

View File

@ -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 (?, ?, ?, ?, ?, ?, ?, ?, ?)`,

View File

@ -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}`);

View File

@ -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

View File

@ -101,13 +101,21 @@ class _DashboardScreenWithApiState extends State<DashboardScreenWithApi> {
// 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<DashboardScreenWithApi> {
Widget _buildStatusCard() {
Color statusColor;
IconData statusIcon;
String statusText = statusSistem;
switch (statusSistem) {
case 'SELESAI':
@ -297,6 +306,7 @@ class _DashboardScreenWithApiState extends State<DashboardScreenWithApi> {
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<DashboardScreenWithApi> {
),
const SizedBox(height: 4),
Text(
statusSistem,
statusText,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
@ -357,7 +367,9 @@ class _DashboardScreenWithApiState extends State<DashboardScreenWithApi> {
),
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<DashboardScreenWithApi> {
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<DashboardScreenWithApi> {
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,
),

View File

@ -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<void> notifyPengeringanDimulai() async {
await showNotification(
id: 1,
title: '🚀 Pengeringan Dimulai',
body: 'Proses pengeringan ikan telah dimulai',
payload: 'pengeringan_dimulai',
);
}
// Show notification for ikan terdeteksi
Future<void> 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<void> 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<void> 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