Release v1.1.0: Real-time Relay Status & Smart Exhaust Control

 Fitur Baru:
- Status relay real-time di aplikasi (Heater 1, Heater 2, Fan, Exhaust)
- Exhaust otomatis ON saat suhu >= 60°C
- Cooling down 20 detik dengan countdown setelah pengeringan selesai
- Auto reset ke Ready to Dry setelah cooling down

🔧 Backend:
- Parse relay1-4 dari JSON MQTT ESP32
- Real-time update status relay di database dan API

🔧 ESP32:
- Kirim status semua relay dalam JSON ke MQTT
- Support 4 relay: Heater1 (GPIO13), Heater2 (GPIO12), Fan (GPIO14), Exhaust (GPIO27)
- Kontrol exhaust otomatis berdasarkan suhu
- Error handling load cell dengan timeout

🐛 Bug Fixes:
- Fix perhitungan berat load cell (dari rata-rata ke penjumlahan)
- Fix crash saat inisialisasi HX711
- Fix status relay tidak update di Flutter

📦 Version: 1.1.0 (Build 2)
This commit is contained in:
flems 2026-06-24 02:29:22 +07:00
parent 63ffe9c8ed
commit bee56e2cd0
108 changed files with 6916 additions and 4324 deletions

12
.flutter-plugins Normal file
View File

@ -0,0 +1,12 @@
# This is a generated file; do not edit or check into version control.
flutter_local_notifications=C:\\Users\\abilh\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\flutter_local_notifications-17.2.4\\
flutter_local_notifications_linux=C:\\Users\\abilh\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\flutter_local_notifications_linux-4.0.1\\
flutter_native_splash=C:\\Users\\abilh\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\flutter_native_splash-2.4.4\\
path_provider_linux=C:\\Users\\abilh\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\path_provider_linux-2.2.1\\
path_provider_windows=C:\\Users\\abilh\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\path_provider_windows-2.3.0\\
shared_preferences=C:\\Users\\abilh\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\shared_preferences-2.5.3\\
shared_preferences_android=C:\\Users\\abilh\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\shared_preferences_android-2.4.7\\
shared_preferences_foundation=C:\\Users\\abilh\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\shared_preferences_foundation-2.5.4\\
shared_preferences_linux=C:\\Users\\abilh\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\shared_preferences_linux-2.4.1\\
shared_preferences_web=C:\\Users\\abilh\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\shared_preferences_web-2.4.3\\
shared_preferences_windows=C:\\Users\\abilh\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\shared_preferences_windows-2.4.1\\

382
APK_MONITORING_FINAL.md Normal file
View File

@ -0,0 +1,382 @@
# 📱 APK Monitoring Pengering Ikan - FINAL VERSION
## 🎯 Aplikasi Monitoring Only (Tanpa Kontrol)
---
## ✅ Perubahan Terakhir
### 1. **Hapus Lamp** 🔦
- ❌ Lamp dihapus dari tampilan
- ✅ Hanya 3 device: **Heater, Fan, Exhaust**
- ✅ Grid 3 kolom (lebih rapi)
### 2. **Hapus Semua Kontrol** 🎮
- ❌ Tidak ada tombol toggle device
- ❌ Tidak ada floating action button (START/RESET)
- ✅ **Monitoring Only** - hanya menampilkan status
- ✅ Device cards tidak bisa diklik
### 3. **Auto-Update Data** 🔄
- ✅ Data update otomatis setiap **5 detik**
- ✅ Tidak perlu pull-to-refresh manual
- ✅ Polling dari Railway API
- ✅ Real-time via MQTT (jika tersedia)
### 4. **Notifikasi Push** 🔔
- ✅ **Pengeringan Siap** (status READY)
- ✅ **Ikan Terdeteksi** (status BERJALAN)
- ✅ **Pengeringan Selesai** (status SELESAI)
- ✅ Notifikasi muncul di dalam & luar aplikasi
- ✅ Notifikasi tersimpan di history
---
## 📊 Tampilan Aplikasi
### Status Card
```
┌─────────────────────────────────────┐
│ 🟣 SCANNING │
│ Menunggu deteksi ikan... │
│ │
│ Waktu Mulai: 10:30:45 │
│ Durasi: 00:15:32 │
└─────────────────────────────────────┘
```
### Metrics
```
┌──────────────┐ ┌──────────────┐
│ 🌡️ Suhu │ │ ⚖️ Berat │
│ 25.9°C │ │ 2183 g │
└──────────────┘ └──────────────┘
┌──────────────────────────────────┐
│ 🎯 Target Berat │
│ 628 g │
└──────────────────────────────────┘
```
### Device Status (3 Kolom)
```
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 🔥 Heater│ │ 💨 Fan │ │ 🌪️ Exhaust│
│ ON │ │ ON │ │ OFF │
└──────────┘ └──────────┘ └──────────┘
```
---
## 🔔 Notifikasi
### Kapan Notifikasi Muncul?
#### 1. **Pengeringan Dimulai** (READY)
```
🔵 Pengeringan Dimulai
Proses pengeringan ikan telah dimulai
```
- Trigger: Status berubah ke READY
- Muncul: Saat ESP32 mulai proses
#### 2. **Ikan Terdeteksi** (BERJALAN)
```
🔵 Ikan Terdeteksi
Berat ikan: 2183 gram
```
- Trigger: Status berubah ke BERJALAN
- Muncul: Saat ikan diletakkan di timbangan
#### 3. **Pengeringan Selesai** (SELESAI)
```
🟢 Pengeringan Selesai
Target tercapai! Berat akhir: 628 gram
```
- Trigger: Status berubah ke SELESAI
- Muncul: Saat berat mencapai target
### Notifikasi di Luar Aplikasi
- ✅ Muncul di notification bar Android
- ✅ Dengan suara & vibration
- ✅ Bisa diklik untuk buka aplikasi
- ✅ Tersimpan di history notifikasi
---
## 🔄 Auto-Update Mechanism
### Polling Timer (5 Detik)
```dart
Timer.periodic(Duration(seconds: 5), (timer) {
// Fetch latest data dari API
_loadLatestData();
// Cek perubahan status
if (newStatus != previousStatus) {
// Trigger notifikasi
_notificationManager.notify...();
}
});
```
### Flow:
1. **Setiap 5 detik** → Fetch data dari Railway API
2. **Cek status** → Bandingkan dengan status sebelumnya
3. **Jika berubah** → Trigger notifikasi
4. **Update UI** → Tampilkan data terbaru
---
## 📱 APK Info
- **Nama:** PengeringIkan.apk
- **Lokasi:** Desktop
- **Ukuran:** 20.65 MB
- **Build:** Release
- **Version:** 1.0.0 (Monitoring Only)
### Fitur:
- ✅ Monitoring only (no control)
- ✅ Auto-update setiap 5 detik
- ✅ Push notifications
- ✅ Connection indicators
- ✅ 3 device cards (Heater, Fan, Exhaust)
- ✅ Status tracking
- ✅ Timer durasi
- ✅ Data dari Railway API
---
## 🚀 Cara Install & Test
### 1. Uninstall APK Lama
```
Settings → Apps → Pengering Ikan → Uninstall
```
### 2. Install APK Baru
```
File Manager → Desktop → PengeringIkan.apk → Install
```
### 3. Izinkan Notifikasi
```
Saat pertama buka, izinkan:
- ✅ Show notifications
- ✅ Sound
- ✅ Vibration
```
### 4. Test Auto-Update
- Buka aplikasi
- Tunggu 5 detik
- Data harus update otomatis
- Tidak perlu pull-to-refresh
### 5. Test Notifikasi
- Ubah status di ESP32 (READY → BERJALAN → SELESAI)
- Notifikasi harus muncul otomatis
- Cek notification bar Android
- Cek history notifikasi di app
---
## 🎨 UI Changes
### Before (4 Device Cards):
```
┌─────────┐ ┌─────────┐
│ Heater │ │ Fan │
└─────────┘ └─────────┘
┌─────────┐ ┌─────────┐
│ Lamp │ │ Exhaust │
└─────────┘ └─────────┘
```
### After (3 Device Cards):
```
┌────────┐ ┌────────┐ ┌────────┐
│ Heater │ │ Fan │ │Exhaust │
└────────┘ └────────┘ └────────┘
```
### Device Card Styling:
- ✅ Compact design
- ✅ Icon dengan background color
- ✅ Status badge (ON/OFF)
- ✅ Tidak bisa diklik
- ✅ Responsive 3 kolom
---
## 🔍 Troubleshooting
### Notifikasi Tidak Muncul?
**Cek 1: Permission**
```
Settings → Apps → Pengering Ikan → Notifications
- Pastikan "Show notifications" ON
```
**Cek 2: Do Not Disturb**
```
Settings → Sound → Do Not Disturb
- Pastikan OFF atau allow app notifications
```
**Cek 3: Battery Optimization**
```
Settings → Battery → Battery Optimization
- Set "Pengering Ikan" to "Don't optimize"
```
**Cek 4: Status Berubah**
```
- Notifikasi hanya muncul saat status berubah
- Cek ESP32 mengirim status baru
```
### Data Tidak Update?
**Cek 1: Connection Icons**
- WiFi icon hijau? → MQTT OK
- Cloud icon hijau? → API OK
**Cek 2: Backend Status**
```
Browser: https://web-production-47eb.up.railway.app/
Harus return: {"status":"OK",...}
```
**Cek 3: ESP32 Status**
- ESP32 harus nyala
- ESP32 harus connect WiFi
- ESP32 harus publish data
**Cek 4: Polling Timer**
- Tunggu 5 detik
- Data harus update otomatis
- Cek timestamp data
---
## 📊 Status Flow
```
DISCONNECTED
CONNECTING
READY (🔔 Notif: Pengeringan Dimulai)
SCANNING (Menunggu ikan)
BERJALAN (🔔 Notif: Ikan Terdeteksi)
SELESAI (🔔 Notif: Pengeringan Selesai)
```
---
## 🎯 Testing Checklist
### Installation
- [ ] Uninstall APK lama
- [ ] Install APK baru
- [ ] Izinkan notifikasi
- [ ] Buka aplikasi
### UI
- [ ] Hanya 3 device cards (Heater, Fan, Exhaust)
- [ ] Tidak ada Lamp
- [ ] Device cards tidak bisa diklik
- [ ] Tidak ada floating action button
- [ ] Connection icons terlihat
### Auto-Update
- [ ] Data update setiap 5 detik
- [ ] Tidak perlu pull-to-refresh
- [ ] Timestamp berubah
- [ ] Status update otomatis
### Notifikasi
- [ ] Notif muncul saat status READY
- [ ] Notif muncul saat status BERJALAN
- [ ] Notif muncul saat status SELESAI
- [ ] Notif muncul di notification bar
- [ ] Notif tersimpan di history
- [ ] Notif bisa diklik
### Monitoring
- [ ] Suhu update real-time
- [ ] Berat update real-time
- [ ] Target terlihat
- [ ] Status berubah sesuai ESP32
- [ ] Device status (ON/OFF) update
- [ ] Timer durasi jalan
---
## 📝 Technical Details
### Auto-Update
- **Interval:** 5 detik
- **Method:** Polling via API
- **Endpoint:** `/api/data/latest`
- **Fallback:** MQTT real-time
### Notifikasi
- **Library:** flutter_local_notifications
- **Storage:** SharedPreferences
- **Trigger:** Status change detection
- **Types:** Info, Success, Warning, Error
### Device Cards
- **Count:** 3 (Heater, Fan, Exhaust)
- **Layout:** GridView 3 columns
- **Aspect Ratio:** 0.9
- **Interaction:** Display only (no tap)
---
## 🔄 Update History
### Version 1.0.0 - Monitoring Only (1 Juni 2026)
- ✅ Hapus Lamp dari tampilan
- ✅ Hapus semua kontrol (monitoring only)
- ✅ Auto-update data setiap 5 detik
- ✅ Notifikasi push untuk status
- ✅ Device cards 3 kolom
- ✅ Compact UI design
- ✅ No interaction (display only)
---
## 💡 Tips
1. **Biarkan Aplikasi Berjalan**
- Auto-update bekerja di background
- Notifikasi tetap muncul meski app tertutup
2. **Cek Notifikasi**
- Swipe down untuk lihat notification bar
- Tap notifikasi untuk buka app
3. **Monitor Status**
- Perhatikan perubahan status
- Notifikasi muncul saat status berubah
4. **Connection Indicators**
- WiFi icon = MQTT status
- Cloud icon = API status
- Hijau = Connected, Merah = Disconnected
---
**Build Date:** 1 Juni 2026
**Version:** 1.0.0 (Monitoring Only)
**Status:** ✅ PRODUCTION READY
🎉 **Aplikasi siap digunakan untuk monitoring!**

67
CHANGELOG.md Normal file
View File

@ -0,0 +1,67 @@
# Changelog - Aplikasi Pengering Ikan
## Version 1.1.0 (Build 2) - 2024-12-XX
### 🎉 Fitur Baru
- ✅ **Status Relay Real-time**: Aplikasi sekarang menampilkan status relay (Heater 1, Heater 2, Fan, Exhaust) secara real-time dari ESP32
- ✅ **Exhaust Otomatis**: Exhaust akan otomatis menyala saat suhu mencapai 60°C atau lebih
- ✅ **Cooling Down 20 Detik**: Setelah pengeringan selesai, sistem akan menjalankan cooling down selama 20 detik dengan exhaust menyala dan countdown di LCD
- ✅ **Auto Reset**: Setelah cooling down, sistem otomatis kembali ke mode "Ready to Dry" untuk batch berikutnya
### 🔧 Perbaikan Backend
- ✅ **Parse Relay Status**: Backend sekarang membaca dan menyimpan status relay1, relay2, relay3, relay4 dari data MQTT ESP32
- ✅ **Real-time Update**: Status relay di database dan API diupdate secara real-time
### 🔧 Perbaikan ESP32
- ✅ **Kirim Status Relay**: ESP32 sekarang mengirim status semua relay (relay1-4) dalam format JSON ke MQTT
- ✅ **Kontrol Exhaust Otomatis**: Exhaust dikendalikan berdasarkan suhu (ON jika >= 60°C)
- ✅ **4 Relay Support**: Mendukung Heater 1 (GPIO 13), Heater 2 (GPIO 12), Fan (GPIO 14), Exhaust (GPIO 27)
- ✅ **Load Cell Error Handling**: Penanganan error yang lebih baik untuk load cell dengan timeout
- ✅ **LCD Status**: Tampilan LCD yang lebih informatif di setiap tahap proses
### 🐛 Bug Fixes
- ✅ Fixed: Perhitungan berat load cell (dari rata-rata menjadi penjumlahan)
- ✅ Fixed: Crash saat inisialisasi HX711 (skip tare di setup, tare saat start pengeringan)
- ✅ Fixed: Status relay tidak update di aplikasi Flutter
### 📊 Format Data MQTT (ESP32 → Backend)
```json
{
"suhu": 55.5,
"berat": 850,
"target": 600,
"relay1": true, // Heater 1
"relay2": true, // Heater 2
"relay3": true, // Fan
"relay4": false // Exhaust (ON jika suhu >= 60°C)
}
```
### 🎯 Alur Pengeringan Baru
1. Ready to Dry → Tekan Button
2. Calibrating (Tare Load Cell)
3. Pengeringan Berjalan (Heater 1, 2, Fan ON | Exhaust ON jika suhu >= 60°C)
4. Target Tercapai → "PENGERINGAN SELESAI" (5 detik)
5. Cooling Down dengan Exhaust ON (20 detik dengan countdown)
6. Auto Reset → Kembali ke "Ready to Dry"
---
## Version 1.0.0 (Build 1) - 2024-XX-XX
### 🎉 Rilis Pertama
- ✅ Dashboard monitoring suhu dan berat real-time
- ✅ Grafik history suhu dan berat
- ✅ Kontrol relay (Heater, Fan, Exhaust) via MQTT
- ✅ Notifikasi pengeringan selesai
- ✅ Koneksi ke backend Railway dengan MySQL database
- ✅ Support MQTT broker HiveMQ
---
## 🔮 Roadmap (Coming Soon)
- [ ] Push notification ke smartphone
- [ ] Export data ke Excel/CSV
- [ ] Multi-user dengan login
- [ ] Dashboard web version
- [ ] Schedule pengeringan otomatis

114
DOKUMENTASI.md Normal file
View File

@ -0,0 +1,114 @@
# 📚 Dokumentasi Aplikasi Pengering Ikan
## 📄 File Dokumentasi
### 1. **README.md** (Dokumentasi Utama)
**Isi:**
- Overview aplikasi
- Arsitektur sistem
- Quick start guide
- Troubleshooting
- Tech stack
**Untuk:** Developer & user baru
---
### 2. **APK_MONITORING_FINAL.md** (Panduan Aplikasi)
**Isi:**
- Fitur aplikasi lengkap
- Cara install & test
- Penjelasan UI
- Notifikasi system
- Testing checklist
**Untuk:** User yang install APK
---
### 3. **PERBAIKAN_APK_FINAL.md** (Changelog)
**Isi:**
- Masalah yang diperbaiki
- Perubahan fitur
- Update history
- Technical details
**Untuk:** Developer & maintenance
---
### 4. **PERBAIKAN_STATUS_MQTT.md** (API Only)
**Isi:**
- Penjelasan kenapa MQTT dihapus
- Cara kerja API polling
- Status icon
- Troubleshooting koneksi
**Untuk:** User yang bertanya tentang MQTT
---
### 5. **backend/API_DOCUMENTATION.md** (API Reference)
**Isi:**
- Semua endpoint API
- Request/response format
- Error handling
- Testing dengan curl/Postman
**Untuk:** Developer backend & integration
---
## 🗂️ Struktur Folder
```
aplikasi_novil/
├── README.md ← Baca ini dulu!
├── APK_MONITORING_FINAL.md ← Panduan install APK
├── PERBAIKAN_APK_FINAL.md ← Changelog
├── PERBAIKAN_STATUS_MQTT.md ← Penjelasan API only
├── DOKUMENTASI.md ← File ini
├── backend/
│ ├── API_DOCUMENTATION.md ← API reference
│ ├── server.js ← Backend code
│ ├── database.js ← Database config
│ └── ...
├── lib/ ← Flutter app code
│ ├── screens/ ← UI screens
│ ├── services/ ← API & notification
│ └── models/ ← Data models
├── esp32_pengering_ikan_railway.ino ← ESP32 code
└── build-with-icon.ps1 ← Build script
```
---
## 🚀 Quick Links
### Untuk User:
1. **Install APK** → Baca `APK_MONITORING_FINAL.md`
2. **Masalah koneksi** → Baca `PERBAIKAN_STATUS_MQTT.md`
3. **Overview** → Baca `README.md`
### Untuk Developer:
1. **Setup project** → Baca `README.md`
2. **API integration** → Baca `backend/API_DOCUMENTATION.md`
3. **Changelog** → Baca `PERBAIKAN_APK_FINAL.md`
---
## 📝 Catatan
- Semua file MD lama sudah dihapus (36 files)
- Hanya tersisa 5 file penting
- Dokumentasi lebih rapi dan terorganisir
- Mudah dicari dan dipahami
---
**Last Updated:** 1 Juni 2026
**Total Docs:** 5 files
**Status:** ✅ Clean & Organized

296
PERBAIKAN_APK_FINAL.md Normal file
View File

@ -0,0 +1,296 @@
# 🔧 Perbaikan APK - Pengering Ikan (FINAL)
## Tanggal: 1 Juni 2026
---
## ✅ MASALAH TERPECAHKAN
### 1. **Loading Stuck saat Refresh** 🔄
- ✅ Tambah timeout 10 detik
- ✅ Error handling yang proper
- ✅ Loading pasti berhenti
### 2. **Timer Durasi Stuck** ⏱️
- ✅ Timer jalan smooth setiap detik
- ✅ Inisialisasi waktu mulai otomatis
- ✅ Tidak tergantung status sistem
### 3. **Data Tidak Update** 📊 ⭐ BARU!
- ✅ Ganti ke dashboard dengan API
- ✅ Data langsung dari Railway backend
- ✅ Real-time update via MQTT
- ✅ Fallback ke multiple MQTT brokers
---
## 🔄 Perubahan Utama
### Dashboard Baru: API + MQTT Hybrid
**Sebelumnya:**
- Hanya MQTT (tidak reliable)
- Data sering tidak update
- Tergantung koneksi MQTT saja
**Sekarang:**
- **API Railway** untuk data awal
- **MQTT** untuk real-time updates
- **Hybrid approach** = lebih reliable!
### File yang Diubah:
1. **lib/screens/splash_screen.dart**
```dart
// Ganti dari dashboard_screen.dart
import 'dashboard_screen_with_api.dart';
// Navigasi ke DashboardScreenWithApi
const DashboardScreenWithApi()
```
2. **pubspec.yaml**
```yaml
# Tambah HTTP package
http: ^1.1.0
```
3. **lib/services/api_service.dart**
- URL: `https://web-production-47eb.up.railway.app`
- Endpoint: `/api/data/latest`
- Timeout: 10 detik
4. **lib/services/mqtt_service.dart**
- Multiple brokers fallback
- Auto reconnect
- Better error handling
---
## 📡 Cara Kerja Baru
### Saat Aplikasi Dibuka:
1. **Test API Connection** → Cek Railway backend
2. **Load Latest Data** → Ambil data terakhir dari database
3. **Connect MQTT** → Subscribe untuk real-time updates
4. **Listen Streams** → Update UI otomatis
### Saat Pull-to-Refresh:
1. **Fetch dari API** → Data terbaru dari database
2. **Update UI** → Tampilkan data baru
3. **Max 10 detik** → Timeout jika lambat
### Real-time Updates:
- ESP32 publish → MQTT broker
- Backend subscribe → Save ke database
- App subscribe → Update UI langsung
---
## 🧪 Data Test dari Backend
**Backend Status:** ✅ AKTIF
```json
{
"status": "OK",
"message": "Pengering Ikan Backend Server - MQTT Client Enabled",
"version": "1.0.1",
"uptime": 248399 seconds
}
```
**Latest Data:** ✅ ADA DATA
```json
{
"suhu": 25.9,
"berat": 2183,
"target": 628,
"relay1": false,
"relay2": false,
"relay3": false,
"relay4": false,
"status": "SCANNING",
"timestamp": "2026-06-01T02:47:58.176Z"
}
```
---
## 📱 APK Info
- **Nama:** PengeringIkan.apk
- **Lokasi:** Desktop
- **Ukuran:** 20.69 MB
- **Build:** Release
- **Version:** 1.0.0
### Fitur:
- ✅ Data dari Railway API
- ✅ Real-time MQTT updates
- ✅ Auto refresh
- ✅ Timer durasi jalan
- ✅ Loading tidak stuck
- ✅ Error handling
- ✅ Connection indicators (WiFi + Cloud icons)
---
## 🚀 Cara Install & Test
### 1. Uninstall APK Lama
```
Settings → Apps → Pengering Ikan → Uninstall
```
### 2. Install APK Baru
```
File Manager → Desktop → PengeringIkan.apk → Install
```
### 3. Test Koneksi
- Buka aplikasi
- Lihat icon di kanan atas:
- **WiFi icon hijau** = MQTT connected
- **Cloud icon hijau** = API connected
- **Icon merah** = Disconnected
### 4. Test Data Update
- Pull-to-refresh (swipe down)
- Data harus update dalam 10 detik
- Cek suhu, berat, status
### 5. Test Timer
- Timer durasi harus jalan setiap detik
- Format: HH:MM:SS (00:00:01, 00:00:02, dst)
---
## 🔍 Troubleshooting
### Data Tidak Update?
**Cek 1: Connection Icons**
- WiFi icon merah? → MQTT gagal connect
- Cloud icon merah? → API gagal connect
- Kedua merah? → Cek internet HP
**Cek 2: Pull-to-Refresh**
- Swipe down untuk refresh manual
- Loading harus muncul dan berhenti
- Data harus berubah
**Cek 3: Backend Status**
- Buka browser di HP
- Akses: `https://web-production-47eb.up.railway.app/`
- Harus return JSON dengan status OK
**Cek 4: ESP32 Status**
- ESP32 harus nyala dan connect WiFi
- ESP32 harus publish data ke MQTT
- Cek serial monitor ESP32
### Loading Stuck?
- Max 10 detik, pasti berhenti
- Jika lebih dari 10 detik, restart app
- Cek koneksi internet
### Timer Tidak Jalan?
- Restart aplikasi
- Timer harus jalan sejak app dibuka
- Tidak tergantung status pengeringan
---
## 📊 Monitoring
### Indikator Koneksi (Kanan Atas):
- 🟢 **WiFi hijau** = MQTT OK
- 🔴 **WiFi merah** = MQTT gagal
- 🟢 **Cloud hijau** = API OK
- 🔴 **Cloud merah** = API gagal
### Status Sistem:
- **CONNECTING** = Sedang connect
- **READY** = Siap, tunggu ikan
- **SCANNING** = Deteksi berat ikan
- **BERJALAN** = Proses pengeringan
- **SELESAI** = Pengeringan selesai
- **DISCONNECTED** = Tidak terkoneksi
---
## 🎯 Checklist Testing
- [ ] Install APK baru
- [ ] Buka aplikasi
- [ ] Cek connection icons (hijau semua)
- [ ] Cek timer jalan (detik bergerak)
- [ ] Cek data (suhu, berat, status)
- [ ] Pull-to-refresh (loading berhenti)
- [ ] Refresh berkali-kali (tidak stuck)
- [ ] Tunggu 1 menit (data auto update via MQTT)
- [ ] Test dengan ESP32 aktif
---
## 📝 Technical Details
### API Endpoints Used:
- `GET /` - Health check
- `GET /api/data/latest` - Latest sensor data
- `GET /api/data/history` - Historical data
- `GET /api/status/history` - Status history
- `GET /api/stats` - Statistics
- `POST /api/control` - Send commands
### MQTT Topics:
- `novil/pengering/data` - Sensor data
- `novil/pengering/status` - Status messages
- `novil/pengering/control` - Control commands
### MQTT Brokers (Fallback):
1. broker.hivemq.com:1883
2. broker.emqx.io:1883
3. test.mosquitto.org:1883
---
## 🔄 Update History
### Version 1.0.0 (1 Juni 2026)
- ✅ Fix loading stuck
- ✅ Fix timer stuck
- ✅ **Fix data tidak update (API integration)**
- ✅ Add connection indicators
- ✅ Add error handling
- ✅ Add timeout protection
- ✅ Hybrid API + MQTT approach
---
## 💡 Tips
1. **Koneksi Internet Stabil**
- Pastikan HP connect WiFi/data
- Backend Railway butuh internet
2. **ESP32 Harus Aktif**
- ESP32 publish data ke MQTT
- Backend subscribe dan save ke database
- App ambil dari database + MQTT
3. **Refresh Manual**
- Jika data lama, pull-to-refresh
- Data langsung dari database
4. **Monitor Logs**
- Jika ada masalah, cek logs di console
- Error akan terprint dengan emoji ❌
---
**Build Date:** 1 Juni 2026
**Version:** 1.0.0 (API Integrated)
**Status:** ✅ PRODUCTION READY
🎉 **Semua masalah sudah diperbaiki!**

235
PERBAIKAN_STATUS_MQTT.md Normal file
View File

@ -0,0 +1,235 @@
# 🔧 Perbaikan Status MQTT - API Only
## Masalah yang Diperbaiki
### ❌ Masalah Sebelumnya:
- Status stuck di "Menghubungkan..."
- Icon WiFi merah terus (MQTT gagal connect)
- Membingungkan user padahal data sudah update
- MQTT tidak diperlukan untuk monitoring
### ✅ Solusi:
- **MQTT dihapus** - tidak digunakan lagi
- **Icon WiFi dihapus** - tidak membingungkan
- **Hanya API** - polling setiap 5 detik
- **Status langsung READY** - tidak stuck
---
## 📱 Tampilan Baru
### AppBar (Kanan Atas):
```
┌─────────────────────────────────────┐
│ Dashboard Pengering Ikan ☁️🟢 │ ← Hanya 1 icon
└─────────────────────────────────────┘
```
**Sebelumnya:** 📶🔴 ☁️🟢 (2 icon, WiFi merah membingungkan)
**Sekarang:** ☁️🟢 (1 icon, jelas API connected)
---
## ☁️ Status Icon
### Icon Cloud (API Status):
- 🟢 **Hijau** = Terhubung ke Railway API
- 🔴 **Merah** = Tidak terhubung ke API
**Artinya:**
- Hijau = Data update setiap 5 detik dari database
- Merah = Tidak bisa ambil data, cek internet
---
## 🔄 Cara Kerja Baru
### Sistem Sederhana:
```
ESP32 (Hardware)
↓ publish ke MQTT
Backend Railway
↓ save to database
Database MySQL
↑ fetch via API (polling 5 detik)
Aplikasi Flutter
```
### Update Data:
1. **Setiap 5 detik** → Fetch data dari API
2. **Update UI** → Tampilkan data terbaru
3. **Cek status** → Trigger notifikasi jika berubah
**Tidak ada MQTT di aplikasi!**
---
## 📊 Status Sistem
### Saat Buka Aplikasi:
```
DISCONNECTED (loading...)
Test API connection
✅ API Connected
READY (Terhubung ke server)
Load data pertama kali
Mulai polling setiap 5 detik
```
### Status yang Muncul:
- **READY** = Siap, menunggu data
- **SCANNING** = Mendeteksi ikan
- **BERJALAN** = Proses pengeringan
- **SELESAI** = Pengeringan selesai
- **DISCONNECTED** = API gagal (cek internet)
**Tidak ada "CONNECTING" atau "Menghubungkan..." lagi!**
---
## ✅ Keuntungan API Only
### 1. **Lebih Sederhana**
- Tidak perlu MQTT broker
- Tidak perlu handle MQTT connection
- Tidak ada icon WiFi yang membingungkan
### 2. **Lebih Reliable**
- API Railway stabil
- Database sebagai single source of truth
- Tidak tergantung MQTT broker publik
### 3. **Lebih Jelas**
- 1 icon = 1 status
- Cloud hijau = semua OK
- Cloud merah = cek internet
### 4. **Tetap Real-time**
- Update setiap 5 detik
- Cukup cepat untuk monitoring
- Notifikasi tetap instant
---
## 🧪 Testing
### Cek Status Icon:
1. Buka aplikasi
2. Lihat icon kanan atas
3. **Harus cloud hijau** ☁️🟢
4. Jika merah, cek internet HP
### Cek Data Update:
1. Lihat timestamp data
2. Tunggu 5 detik
3. Data harus berubah
4. Status harus update
### Cek Notifikasi:
1. Ubah status di ESP32
2. Tunggu max 5 detik
3. Notifikasi harus muncul
4. Status di app harus berubah
---
## 🔍 Troubleshooting
### Icon Cloud Merah?
**Penyebab:**
- HP tidak connect internet
- Railway backend down
- Firewall block HTTPS
**Solusi:**
- Cek WiFi/data HP
- Test di browser: https://web-production-47eb.up.railway.app/
- Restart aplikasi
### Data Tidak Update?
**Penyebab:**
- Icon cloud merah
- ESP32 tidak kirim data
- Backend tidak save data
**Solusi:**
- Pastikan icon cloud hijau
- Cek ESP32 status
- Cek backend logs
### Status Stuck?
**Tidak akan terjadi lagi!**
- Status langsung READY saat API connected
- Tidak ada proses "Menghubungkan..." MQTT
- Jika stuck, restart aplikasi
---
## 📝 Technical Details
### Removed:
- ❌ MQTT client connection
- ❌ MQTT service instance
- ❌ MQTT stream listeners
- ❌ WiFi icon indicator
- ❌ isConnected variable
- ❌ _connectMQTT() function
### Kept:
- ✅ API service
- ✅ Polling timer (5 seconds)
- ✅ Notification manager
- ✅ Cloud icon indicator
- ✅ isApiConnected variable
- ✅ _loadLatestData() function
### Added:
- ✅ Direct status set to READY
- ✅ Cleaner initialization
- ✅ Simpler connection flow
---
## 📦 APK Info
- **Nama:** PengeringIkan.apk
- **Lokasi:** Desktop
- **Ukuran:** 20.44 MB (lebih kecil!)
- **Mode:** API Only (No MQTT)
---
## 🎯 Kesimpulan
### Sebelumnya:
- 2 icon (WiFi + Cloud)
- MQTT gagal connect
- Status stuck "Menghubungkan..."
- Membingungkan user
### Sekarang:
- 1 icon (Cloud saja)
- API polling 5 detik
- Status langsung READY
- Jelas dan sederhana
**Data tetap update, notifikasi tetap jalan, monitoring tetap berjalan!** ✅
---
**Build Date:** 1 Juni 2026
**Version:** 1.0.0 (API Only)
**Status:** ✅ PRODUCTION READY
🎉 **Tidak ada lagi status "Menghubungkan..." yang stuck!**

249
README.md
View File

@ -1,17 +1,244 @@
# aplikasi_novil
# 📱 Aplikasi Monitoring Pengering Ikan
A new Flutter project.
Aplikasi mobile untuk monitoring sistem pengering ikan otomatis berbasis IoT dengan ESP32.
## Getting Started
---
This project is a starting point for a Flutter application.
## 🎯 Fitur Utama
A few resources to get you started if this is your first Flutter project:
- ✅ **Monitoring Real-time** - Data update setiap 5 detik
- ✅ **Push Notifications** - Notifikasi status pengeringan
- ✅ **Dashboard Lengkap** - Suhu, berat, target, status device
- ✅ **Cloud Backend** - Data tersimpan di Railway MySQL
- ✅ **Display Only** - Monitoring tanpa kontrol manual
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
---
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
## 🏗️ Arsitektur Sistem
```
ESP32 (Hardware)
↓ MQTT Publish
Backend Railway (Node.js)
↓ Save to Database
MySQL Database
↑ API Polling (5 detik)
Aplikasi Flutter (Android)
```
---
## 📦 Komponen
### 1. **Hardware (ESP32)**
- File: `esp32_pengering_ikan_railway.ino`
- Sensor: DHT22 (suhu), HX711 (berat)
- Relay: 4 channel (Heater, Fan, Lamp, Exhaust)
- Koneksi: WiFi + MQTT
### 2. **Backend (Node.js)**
- Folder: `backend/`
- Framework: Express.js
- Database: MySQL (Railway)
- MQTT Client: Subscribe dari ESP32
- REST API: Endpoint untuk aplikasi
### 3. **Mobile App (Flutter)**
- Folder: `lib/`
- Platform: Android
- Update: API polling 5 detik
- Notifikasi: flutter_local_notifications
---
## 🚀 Quick Start
### Build APK:
```bash
# Jalankan script build
.\build-with-icon.ps1
# Atau manual
flutter build apk --release
```
### Install di HP:
1. Copy `PengeringIkan.apk` dari Desktop
2. Install di HP Android
3. Izinkan notifikasi
4. Buka aplikasi
---
## 📱 Tampilan Aplikasi
### Status Card
- Status sistem (READY, SCANNING, BERJALAN, SELESAI)
- Waktu mulai & durasi
- Pesan status
### Metrics
- 🌡️ Suhu (°C)
- ⚖️ Berat (gram)
- 🎯 Target berat (gram)
### Device Status
- 🔥 Heater (ON/OFF)
- 💨 Fan (ON/OFF)
- 🌪️ Exhaust (ON/OFF)
### Connection Indicator
- ☁️🟢 API Connected
- ☁️🔴 API Disconnected
---
## 🔔 Notifikasi
Aplikasi mengirim push notification saat:
1. **Pengeringan Siap** (Status → READY)
2. **Ikan Terdeteksi** (Status → BERJALAN)
3. **Pengeringan Selesai** (Status → SELESAI)
Notifikasi muncul di dalam & luar aplikasi.
---
## 🔧 Konfigurasi
### Backend URL:
```dart
// lib/services/api_service.dart
static const String baseUrl = 'https://web-production-47eb.up.railway.app';
```
### Polling Interval:
```dart
// lib/screens/dashboard_screen_with_api.dart
Timer.periodic(const Duration(seconds: 5), ...);
```
### MQTT Topics (Backend):
```javascript
// backend/server.js
const TOPIC_DATA = 'novil/pengering/data';
const TOPIC_STATUS = 'novil/pengering/status';
const TOPIC_CONTROL = 'novil/pengering/control';
```
---
## 📊 API Endpoints
Dokumentasi lengkap: `backend/API_DOCUMENTATION.md`
### Main Endpoints:
- `GET /` - Health check
- `GET /api/data/latest` - Data sensor terbaru
- `GET /api/data/history` - Riwayat data
- `GET /api/status/history` - Riwayat status
- `GET /api/stats` - Statistik
---
## 🛠️ Development
### Requirements:
- Flutter SDK 3.5+
- Android SDK
- Node.js 18+
- MySQL 8.0+
### Setup Backend:
```bash
cd backend
npm install
npm start
```
### Setup Flutter:
```bash
flutter pub get
flutter run
```
---
## 📄 Dokumentasi
- **APK_MONITORING_FINAL.md** - Panduan lengkap aplikasi
- **PERBAIKAN_APK_FINAL.md** - Changelog & perbaikan
- **PERBAIKAN_STATUS_MQTT.md** - Penjelasan API only
- **backend/API_DOCUMENTATION.md** - API reference
---
## 🔍 Troubleshooting
### Icon Cloud Merah?
- Cek koneksi internet HP
- Test backend: https://web-production-47eb.up.railway.app/
- Restart aplikasi
### Data Tidak Update?
- Pastikan icon cloud hijau
- Cek ESP32 status (WiFi connected)
- Cek backend logs di Railway
### Notifikasi Tidak Muncul?
- Izinkan notifikasi di Settings
- Disable battery optimization
- Cek status berubah di ESP32
---
## 📝 Version History
### v1.0.0 (1 Juni 2026) - Current
- ✅ Monitoring only (no control)
- ✅ API polling 5 detik
- ✅ Push notifications
- ✅ 3 device cards (Heater, Fan, Exhaust)
- ✅ Auto-update data
- ✅ Connection indicator
---
## 👨‍💻 Tech Stack
- **Mobile:** Flutter 3.5, Dart 3.0
- **Backend:** Node.js 18, Express.js
- **Database:** MySQL 8.0 (Railway)
- **IoT:** ESP32, Arduino IDE
- **Protocol:** MQTT, REST API
- **Hosting:** Railway.app
---
## 📞 Support
Untuk pertanyaan atau masalah:
1. Cek dokumentasi di folder `docs/`
2. Lihat troubleshooting di README
3. Cek logs di Railway dashboard
---
## 📜 License
Private project - All rights reserved
---
**Build Date:** 1 Juni 2026
**Version:** 1.0.0
**Status:** ✅ Production Ready
🎉 **Aplikasi siap digunakan untuk monitoring pengering ikan!**

View File

@ -1,414 +0,0 @@
# 🔧 SOLUSI: Data ESP32 Tidak Masuk ke MySQL Railway
## 🎯 Masalah
ESP32 sudah publish data ke MQTT, tapi data **tidak masuk ke MySQL** di Railway.
---
## 🔍 Penyebab
### Arsitektur yang Salah:
```
❌ SEBELUM:
ESP32 → HiveMQ (broker.hivemq.com)
(data hilang di sini)
Backend Railway (Aedes Broker Internal - DISABLED)
MySQL (tidak ada data)
```
**Masalah:**
1. ESP32 publish ke **HiveMQ** public broker
2. Backend Railway menggunakan **Aedes** (MQTT broker internal)
3. Aedes **DISABLED** di Railway (tidak bisa bind port 1883)
4. Backend **TIDAK connect** ke HiveMQ
5. Backend **TIDAK subscribe** ke topic ESP32
6. **Data hilang** karena tidak ada yang terima
---
## ✅ Solusi
### Arsitektur yang Benar:
```
✅ SESUDAH:
ESP32 → HiveMQ (broker.hivemq.com)
Backend Railway (MQTT Client)
↓ (subscribe & save)
MySQL Database
```
**Perubahan:**
1. Backend connect ke **HiveMQ sebagai MQTT Client**
2. Backend **subscribe** ke topic ESP32
3. Backend **terima data** dari ESP32
4. Backend **save ke MySQL**
---
## 🛠️ Perubahan yang Sudah Dilakukan
### 1. **Install Package MQTT Client**
```bash
npm install mqtt
```
### 2. **Update `server.js`**
- Tambah import `mqtt` client
- Connect ke HiveMQ sebagai client
- Subscribe ke topic: `novil/pengering/data`, `status`, `button`
- Handle message dan save ke MySQL
- Publish command via HiveMQ
### 3. **Update `.env`**
- Tambah `MQTT_BROKER_URL=mqtt://broker.hivemq.com:1883`
- Tambah template untuk MySQL credentials
---
## 🚀 Cara Deploy
### 1. **Test Lokal (Optional)**
```bash
cd backend
npm install
npm start
```
**Expected Output:**
```
✅ Connected to MQTT Broker (HiveMQ)
✅ Subscribed to topics:
- novil/pengering/data
- novil/pengering/status
- novil/pengering/button
🚀 REST API running on port 3000
```
### 2. **Push ke GitHub**
```bash
cd backend
git add .
git commit -m "Fix: Backend connect to HiveMQ as MQTT client"
git push
```
### 3. **Configure Railway**
Di Railway Dashboard:
1. Buka project → **Variables**
2. Tambahkan variable baru:
- **Key:** `MQTT_BROKER_URL`
- **Value:** `mqtt://broker.hivemq.com:1883`
3. MySQL credentials sudah otomatis dari Railway MySQL Plugin (tidak perlu diubah)
### 4. **Wait for Auto-Deploy**
Railway akan otomatis deploy setelah push ke GitHub.
---
## 🧪 Testing
### 1. **Check MQTT Connection**
```bash
curl https://your-app.railway.app/api/stats
```
**Expected Response:**
```json
{
"success": true,
"stats": {
"mqttConnected": true, // ← HARUS TRUE
"sensorDataCount": 0,
"statusHistoryCount": 0,
...
}
}
```
**Jika `mqttConnected: false`:**
- Check Railway logs
- Pastikan `MQTT_BROKER_URL` sudah ditambahkan
- Redeploy
---
### 2. **Test dengan ESP32**
1. **Upload sketch ESP32** (tidak perlu diubah)
2. **Buka Serial Monitor** (115200 baud)
3. **Tekan button** untuk start pengeringan
4. **Tunggu 5 detik** (scan berat)
5. **Check Serial Monitor:**
```
MQTT Data Sent: {"suhu":28.5,"berat":450,"target":315}
MQTT Data Sent: {"suhu":28.6,"berat":448,"target":315}
```
---
### 3. **Check Data di MySQL**
```bash
curl https://your-app.railway.app/api/data/latest
```
**Expected Response:**
```json
{
"success": true,
"data": {
"suhu": 28.5,
"berat": 450.0,
"target": 315.0,
"status": "RUNNING",
"timestamp": "2026-05-28T10:30:00.000Z"
}
}
```
**Jika data masih kosong:**
- Check Railway logs untuk error
- Pastikan ESP32 sudah publish data
- Check MySQL credentials
---
### 4. **Check History**
```bash
curl https://your-app.railway.app/api/data/history?limit=10
```
**Expected Response:**
```json
{
"success": true,
"count": 10,
"data": [
{
"id": 1,
"suhu": 28.5,
"berat": 450.0,
"target": 315.0,
"relay1": true,
"relay2": true,
"relay3": true,
"relay4": false,
"status": "RUNNING",
"timestamp": "2026-05-28 10:30:00"
},
...
]
}
```
---
### 5. **Test Send Command**
```bash
curl -X POST https://your-app.railway.app/api/control \
-H "Content-Type: application/json" \
-d '{"command":"HEATER_ON"}'
```
**Expected Response:**
```json
{
"success": true,
"message": "Command sent successfully",
"command": "HEATER_ON"
}
```
**Check ESP32 Serial Monitor:**
```
MQTT Message [novil/pengering/control] : HEATER_ON
HEATER ON
```
---
## 📊 Monitoring
### Railway Logs
Di Railway Dashboard → Deployments → View Logs
**Expected Logs:**
```
🔄 Connecting to MQTT Broker: mqtt://broker.hivemq.com:1883
✅ Connected to MQTT Broker (HiveMQ)
✅ Subscribed to topics:
- novil/pengering/data
- novil/pengering/status
- novil/pengering/button
📨 MQTT Message received:
Topic: novil/pengering/data
Message: {"suhu":28.5,"berat":450,"target":315}
✅ Data saved to MySQL database
📨 MQTT Message received:
Topic: novil/pengering/status
Message: PENGERINGAN DIMULAI
✅ Status saved to MySQL database
```
---
## 🔍 Troubleshooting
### Problem 1: `mqttConnected: false`
**Penyebab:** Backend tidak bisa connect ke HiveMQ
**Solusi:**
1. Check Railway logs untuk error message
2. Pastikan variable `MQTT_BROKER_URL` sudah ditambahkan
3. Pastikan value: `mqtt://broker.hivemq.com:1883` (bukan `http://`)
4. Redeploy
---
### Problem 2: Data masih tidak masuk ke MySQL
**Penyebab:** Database credentials salah atau database belum diinit
**Solusi:**
1. **Test database connection:**
```bash
curl https://your-app.railway.app/api/database/test
```
2. **Initialize database tables:**
```bash
curl -X POST https://your-app.railway.app/api/database/init
```
3. **Check MySQL credentials di Railway:**
- `MYSQLHOST`
- `MYSQLPORT`
- `MYSQLUSER`
- `MYSQLPASSWORD`
- `MYSQLDATABASE`
---
### Problem 3: ESP32 tidak terima command
**Penyebab:** ESP32 tidak subscribe atau MQTT disconnect
**Solusi:**
1. **Check ESP32 Serial Monitor:**
```
MQTT Connected
Subscribed to: novil/pengering/control
```
2. **Restart ESP32**
3. **Check WiFi connection**
---
### Problem 4: Railway logs error "Cannot find module 'mqtt'"
**Penyebab:** Package `mqtt` belum terinstall
**Solusi:**
1. **Pastikan `package.json` sudah update:**
```json
"dependencies": {
"mqtt": "^5.3.5",
...
}
```
2. **Push ke GitHub lagi:**
```bash
git add package.json
git commit -m "Add mqtt package"
git push
```
---
## ✅ Checklist
### Pre-Deploy:
- [x] Package `mqtt` sudah ditambahkan di `package.json`
- [x] `server.js` sudah diupdate dengan MQTT client
- [x] `.env` sudah ada template MySQL credentials
- [x] `npm install` berhasil
### Deploy:
- [ ] Push ke GitHub
- [ ] Tambahkan `MQTT_BROKER_URL` di Railway Variables
- [ ] Wait for auto-deploy (2-3 menit)
- [ ] Check Railway logs untuk "Connected to MQTT Broker"
### Testing:
- [ ] Test `/api/stats``mqttConnected: true`
- [ ] Upload ESP32 sketch
- [ ] Test ESP32 publish data
- [ ] Test `/api/data/latest` → ada data
- [ ] Test `/api/data/history` → ada history
- [ ] Test send command → ESP32 terima
---
## 📝 Catatan Penting
1. **ESP32 tidak perlu diubah** - sketch tetap sama
2. **MySQL credentials** sudah otomatis dari Railway MySQL Plugin
3. **HiveMQ** adalah public broker, gratis, tidak perlu registrasi
4. **Backend sekarang sebagai MQTT Client**, bukan broker
5. **Aedes broker tetap ada** tapi disabled (untuk future use)
---
## 🎯 Expected Result
Setelah deploy:
1. ✅ Backend connect ke HiveMQ
2. ✅ Backend subscribe ke topic ESP32
3. ✅ ESP32 publish data → Backend terima
4. ✅ Backend save data ke MySQL
5. ✅ Data history tersimpan
6. ✅ Status history tersimpan
7. ✅ Command dari Flutter → ESP32 berfungsi
---
## 📞 Support
Jika masih ada masalah:
1. **Check Railway Logs** untuk error detail
2. **Check ESP32 Serial Monitor** untuk MQTT status
3. **Test API endpoints** untuk verify data
4. **Baca dokumentasi lengkap** di `backend/FIX_MQTT_MYSQL.md`
---
Selamat mencoba! 🚀🎉

View File

@ -8,11 +8,12 @@ plugins {
android {
namespace = "com.example.aplikasi_novil"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
ndkVersion = "27.0.12077973"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true
}
kotlinOptions {
@ -42,3 +43,7 @@ android {
flutter {
source = "../.."
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
}

View File

@ -1,4 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Permission untuk internet (MQTT) -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:label="aplikasi_novil"
android:name="${applicationName}"

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@ -1,12 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<!-- Simple launch background - no splash image -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
<item android:drawable="@android:color/white" />
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@ -1,12 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<!-- Simple launch background - no splash image -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 963 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -5,6 +5,10 @@
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#1976D2</color>
</resources>

View File

@ -5,6 +5,10 @@
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your

BIN
assets/images/app_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -1,253 +0,0 @@
# 🔍 Check Railway Deployment Status
## 📊 Latest Commits
1. **5c950c1** - Add better error handling and logging ← **LATEST**
2. **794201c** - Add missing websocket-stream dependency
3. **b08ad68** - Fix: Remove digitalRead error and disable MQTT broker
## 🚀 Deployment Progress
### Build Status: ✅ SUCCESS
```
✓ Dependencies installed
✓ Build completed
✓ Image pushed (275.6 MB)
```
### Container Status: ⏳ STARTING
Tunggu 30-60 detik untuk container start.
---
## 🧪 Test Deployment
### Wait 1 Minute, Then Test:
```powershell
# PowerShell
Start-Sleep -Seconds 60
Invoke-WebRequest -Uri "https://web-production-47eb.up.railway.app/" -UseBasicParsing
```
### Expected Response:
```json
{
"status": "OK",
"message": "Pengering Ikan Backend Server",
"version": "1.0.0",
"uptime": 12.34,
"timestamp": "2026-05-28T..."
}
```
---
## 📋 Improvements in Latest Commit
### 1. Better Database Error Handling
```javascript
try {
const connected = await db.testConnection();
if (connected) {
await db.initDatabase();
}
} catch (error) {
console.error('❌ Database initialization error:', error.message);
console.warn('⚠️ Continuing without database...');
}
```
**Benefit:** Server tetap start meskipun database gagal connect.
### 2. Express Server Error Handling
```javascript
const server = app.listen(PORT, '0.0.0.0', () => {
console.log('✅ Server is ready!');
});
server.on('error', (error) => {
console.error('❌ Server error:', error);
process.exit(1);
});
```
**Benefit:** Error lebih jelas di logs.
### 3. Global Error Handlers
```javascript
process.on('uncaughtException', (error) => {
console.error('❌ Uncaught Exception:', error);
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('❌ Unhandled Rejection:', reason);
process.exit(1);
});
```
**Benefit:** Catch semua error yang tidak ter-handle.
### 4. Bind to 0.0.0.0
```javascript
app.listen(PORT, '0.0.0.0', () => {
// Server accessible from outside
});
```
**Benefit:** Server bisa diakses dari luar container.
---
## 🔍 How to Check Railway Logs
### Via Railway Dashboard:
1. Go to https://railway.app/
2. Login to your account
3. Select your project
4. Click on your service
5. Click "Deployments" tab
6. Click latest deployment
7. View "Deploy Logs" and "Application Logs"
### Look for These Logs:
**Success:**
```
🔄 Initializing database connection...
✅ Database connected, initializing tables...
✅ Database tables initialized
✅ Latest data loaded from database
MQTT Broker disabled (use external MQTT broker like HiveMQ)
WebSocket MQTT disabled
🚀 REST API running on port 3000
✅ Server is ready!
```
**Failure:**
```
❌ Database initialization error: ...
❌ Server error: ...
❌ Uncaught Exception: ...
```
---
## 🛠️ If Still Getting 502
### Check 1: Railway Service Status
- Go to Railway Dashboard
- Check if service is "Active" (green)
- If "Crashed" (red), view logs for error
### Check 2: Environment Variables
Ensure these are set:
```
MYSQL_URL=mysql://root:...@mysql.railway.internal:3306/railway
NODE_ENV=production
PORT=3000
```
### Check 3: Database Connection
Test database separately:
```sql
-- Connect to Railway MySQL via CLI
railway connect mysql
-- Check if database exists
SHOW DATABASES;
-- Check if tables exist
USE railway;
SHOW TABLES;
```
### Check 4: Restart Service
In Railway Dashboard:
- Click service
- Click "Settings"
- Scroll down
- Click "Restart"
---
## 📝 Troubleshooting Steps
### Step 1: Wait Full 2 Minutes
Railway needs time to:
1. Pull new code
2. Build image
3. Push image
4. Start container
5. Initialize app
### Step 2: Test Health Check
```powershell
Invoke-WebRequest -Uri "https://web-production-47eb.up.railway.app/" -UseBasicParsing
```
### Step 3: If 502, Check Logs
Look for error messages in Railway logs.
### Step 4: Test Database
```powershell
Invoke-WebRequest -Uri "https://web-production-47eb.up.railway.app/api/database/test" -UseBasicParsing
```
### Step 5: Initialize Tables
```powershell
Invoke-WebRequest -Uri "https://web-production-47eb.up.railway.app/api/database/init" -Method POST -UseBasicParsing
```
---
## ⏰ Timeline
- **04:40 UTC** - Build completed
- **04:40 UTC** - Image pushed
- **04:40 UTC** - Container starting
- **04:41 UTC** - Expected: Container ready
- **04:42 UTC** - Test endpoints
**Current Time:** Check your clock
**Wait Until:** 04:42 UTC (or 2 minutes from build completion)
---
## 🎯 Next Steps
1. **Wait 2 minutes** from build completion
2. **Test health check** endpoint
3. **If success:** Initialize database and test with ESP32
4. **If 502:** Check Railway logs for specific error
5. **If database error:** Check MySQL service status
---
## ✅ Success Criteria
- [ ] Health check returns 200 OK
- [ ] Database test returns success
- [ ] Database init creates tables
- [ ] Latest data endpoint returns data
- [ ] Stats endpoint returns statistics
---
## 📞 Support
If still having issues:
1. Check `TROUBLESHOOTING_RAILWAY.md`
2. View Railway logs for specific errors
3. Test locally: `npm install && npm start`
4. Check Railway status: https://status.railway.app/
---
**Latest Deploy:** 5c950c1 - Add better error handling and logging
**Status:** ⏳ Deploying...
**ETA:** 2 minutes from build completion
Good luck! 🚀

View File

@ -1,281 +0,0 @@
# 🎉 DEPLOYMENT SUCCESS!
## ✅ Backend Railway Fully Operational
**Deployment Time:** 2026-05-28 04:55 UTC
**Status:** ✅ RUNNING
**Uptime:** 508+ seconds
---
## 📊 Test Results
### ✅ All Tests Passed
1. **Health Check**
- Status: OK
- Message: Pengering Ikan Backend Server
- Version: 1.0.0
- Uptime: 508.59s
2. **Database Connection**
- Message: Database connection successful
- Host: mysql.railway.internal
- Database: railway
3. **Latest Data**
- Suhu: 0°C
- Berat: 0g
- Status: DISCONNECTED
4. **Statistics**
- Sensor Data Count: 0
- Status History Count: 0
- Connected Clients: 0
---
## 🔧 Fixes Applied
### Fix 1: Remove `digitalRead()` Error
**Commit:** b08ad68
**Status:** ✅ FIXED
### Fix 2: Disable MQTT Broker in Railway
**Commit:** b08ad68
**Status:** ✅ FIXED
### Fix 3: Add Missing `websocket-stream` Dependency
**Commit:** 794201c
**Status:** ✅ FIXED
### Fix 4: Add Better Error Handling
**Commit:** 5c950c1
**Status:** ✅ FIXED
---
## 🌐 API Endpoints
**Base URL:** `https://web-production-47eb.up.railway.app`
### Available Endpoints:
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/` | Health check |
| GET | `/api/data/latest` | Get latest sensor data |
| GET | `/api/data/history?limit=50` | Get data history |
| GET | `/api/status/history?limit=50` | Get status history |
| GET | `/api/stats` | Get server statistics |
| POST | `/api/control` | Send control command |
| DELETE | `/api/history/clear` | Clear history |
| GET | `/api/database/test` | Test database connection |
| POST | `/api/database/init` | Initialize database tables |
---
## 📡 MQTT Configuration
### Backend (Railway):
- **MQTT Broker:** ❌ Disabled (Railway tidak support custom ports)
- **WebSocket MQTT:** ❌ Disabled
### External MQTT Broker (HiveMQ):
- **Host:** `broker.hivemq.com`
- **Port:** `1883` (TCP) atau `8883` (WebSocket)
- **Topics:**
- `novil/pengering/data` - Data sensor dari ESP32
- `novil/pengering/status` - Status messages
- `novil/pengering/button` - Button events
- `novil/pengering/control` - Control commands
---
## 🔄 Architecture
```
┌─────────────┐
│ ESP32 │
└──────┬──────┘
│ MQTT (broker.hivemq.com)
┌─────────────────────┐
│ HiveMQ Cloud │
│ (Public Broker) │
└──────┬──────────────┘
│ Subscribe
┌─────────────────────┐ ┌──────────────┐
│ Backend Railway │◄────►│ MySQL Railway│
│ (REST API) │ │ │
└──────┬──────────────┘ └──────────────┘
│ REST API
┌─────────────┐
│ Flutter App │
└─────────────┘
```
---
## 🚀 Next Steps
### 1. Test dengan ESP32
**Update ESP32 Code:**
```cpp
const char* mqtt_server = "broker.hivemq.com"; // Public MQTT broker
```
**Upload ke ESP32 dan test:**
- ESP32 connect ke WiFi
- ESP32 connect ke HiveMQ
- ESP32 publish data ke `novil/pengering/data`
- Backend terima dan simpan ke database
### 2. Test dengan Flutter App
**Update Flutter Code:**
```dart
// MQTT Service
final mqttService = MqttService();
await mqttService.connectWithRetry(); // Connect ke HiveMQ
// API Service
final apiService = ApiService();
final data = await apiService.getLatestData(); // Get dari Railway
```
**Test:**
- Flutter connect ke HiveMQ (real-time data)
- Flutter call Railway API (control & history)
- Send command START/RESET
- Manual control relay
### 3. Initialize Database Tables
Jika belum ada data, initialize tables:
```powershell
Invoke-WebRequest -Uri "https://web-production-47eb.up.railway.app/api/database/init" -Method POST -UseBasicParsing
```
---
## 📝 Environment Variables (Railway)
```bash
# Database (Auto-set by Railway MySQL Plugin)
MYSQL_URL=mysql://root:...@mysql.railway.internal:3306/railway
MYSQLHOST=mysql.railway.internal
MYSQLPORT=3306
MYSQLUSER=root
MYSQLPASSWORD=...
MYSQLDATABASE=railway
# Server
NODE_ENV=production
PORT=8080 # Auto-set by Railway
# MQTT (Optional, default: disabled)
ENABLE_MQTT_BROKER=false # Tidak perlu set, default disabled
```
---
## 🧪 Test Commands
### PowerShell:
```powershell
# Health check
Invoke-WebRequest -Uri "https://web-production-47eb.up.railway.app/" -UseBasicParsing
# Test database
Invoke-WebRequest -Uri "https://web-production-47eb.up.railway.app/api/database/test" -UseBasicParsing
# Get latest data
Invoke-WebRequest -Uri "https://web-production-47eb.up.railway.app/api/data/latest" -UseBasicParsing
# Send command START
Invoke-WebRequest -Uri "https://web-production-47eb.up.railway.app/api/control" -Method POST -Body '{"command":"START"}' -ContentType "application/json" -UseBasicParsing
# Get statistics
Invoke-WebRequest -Uri "https://web-production-47eb.up.railway.app/api/stats" -UseBasicParsing
```
### Browser:
```
https://web-production-47eb.up.railway.app/
https://web-production-47eb.up.railway.app/api/data/latest
https://web-production-47eb.up.railway.app/api/stats
```
---
## 📊 Monitoring
### Railway Dashboard:
- **Deployments:** View deployment history
- **Logs:** View application logs
- **Metrics:** View CPU, memory, network usage
- **Settings:** Manage environment variables
### Application Logs:
```
📦 Using individual environment variables for connection
🔧 Database Config: { host: 'mysql.railway.internal', ... }
🔄 Initializing database connection...
✅ Database connected successfully
✅ Database tables initialized
MQTT Broker disabled (use external MQTT broker like HiveMQ)
🚀 REST API running on port 8080
✅ Server is ready!
```
---
## 🎯 Success Metrics
- ✅ Server uptime: 508+ seconds
- ✅ Database connection: Active
- ✅ API response time: < 100ms
- ✅ Error rate: 0%
- ✅ All endpoints: Operational
---
## 📚 Documentation
- **API Documentation:** `API_DOCUMENTATION.md`
- **Railway Config:** `RAILWAY_CONFIG.md`
- **Troubleshooting:** `TROUBLESHOOTING_RAILWAY.md`
- **Flutter Update:** `../FLUTTER_UPDATE.md`
- **Quick Start:** `../QUICK_START.md`
---
## 🎉 Congratulations!
Backend Railway sudah fully operational dan siap digunakan!
**What's Working:**
- ✅ REST API endpoints
- ✅ MySQL database connection
- ✅ Data persistence
- ✅ Error handling
- ✅ Logging
**Next:**
- 🔄 Test dengan ESP32
- 🔄 Test dengan Flutter app
- 🔄 End-to-end testing
Selamat! 🚀🎊

View File

@ -1,319 +0,0 @@
# Deploy Perbaikan Control Commands ke Railway
## Perubahan yang Dilakukan
✅ Backend sekarang **subscribe ke topic `novil/pengering/control`**
✅ Handler untuk menyimpan pesan control ke database `control_commands`
✅ Handler untuk update status relay dari ESP32
## Cara Deploy ke Railway
### Opsi 1: Deploy via Git (Recommended)
#### 1. Commit Perubahan
```bash
cd c:\Users\abilh\aplikasi_novil
# Cek status
git status
# Add file yang berubah
git add backend/server.js
git add PERBAIKAN_CONTROL_COMMANDS.md
git add backend/DEPLOY_PERBAIKAN.md
# Commit
git commit -m "Fix: Subscribe to control topic and save to database"
# Push ke repository
git push origin main
```
#### 2. Railway Auto-Deploy
- Railway akan otomatis detect perubahan
- Build dan deploy akan berjalan otomatis
- Tunggu hingga status "Deployed" (biasanya 2-3 menit)
#### 3. Verifikasi Deployment
```bash
# Cek health check
curl https://your-railway-app.up.railway.app/
# Cek stats
curl https://your-railway-app.up.railway.app/api/stats
```
---
### Opsi 2: Deploy via Railway CLI
#### 1. Install Railway CLI (jika belum)
```bash
# Windows (PowerShell)
iwr https://railway.app/install.ps1 | iex
```
#### 2. Login ke Railway
```bash
railway login
```
#### 3. Link Project
```bash
cd c:\Users\abilh\aplikasi_novil\backend
railway link
```
#### 4. Deploy
```bash
railway up
```
#### 5. Cek Logs
```bash
railway logs
```
---
### Opsi 3: Manual Upload via Railway Dashboard
#### 1. Buka Railway Dashboard
- Login ke https://railway.app
- Pilih project "aplikasi_novil" atau nama project Anda
#### 2. Buka Service Backend
- Klik service backend Anda
- Pilih tab "Deployments"
#### 3. Trigger Redeploy
- Klik tombol "Deploy" atau "Redeploy"
- Pilih branch yang benar (main/master)
- Railway akan pull code terbaru dan deploy
---
## Verifikasi Setelah Deploy
### 1. Cek Logs Railway
Di Railway Dashboard → Service → Logs, pastikan muncul:
```
✅ Subscribed to topics:
- novil/pengering/data
- novil/pengering/status
- novil/pengering/button
- novil/pengering/control ← HARUS ADA INI
```
### 2. Test API Health Check
```bash
# Ganti dengan URL Railway Anda
curl https://your-app.up.railway.app/
```
Response:
```json
{
"status": "OK",
"message": "Pengering Ikan Backend Server - MQTT Client Enabled",
"version": "1.0.1",
"uptime": 123.45,
"timestamp": "2026-05-29T10:30:00.000Z"
}
```
### 3. Test Stats API
```bash
curl https://your-app.up.railway.app/api/stats
```
Response harus include:
```json
{
"success": true,
"stats": {
"mqttConnected": true,
"sensorDataCount": 100,
"statusHistoryCount": 50,
"controlCommandsCount": 10, ← HARUS ADA INI
...
}
}
```
### 4. Test Control Command
```bash
# Kirim command test
curl -X POST https://your-app.up.railway.app/api/control \
-H "Content-Type: application/json" \
-d '{"command":"HEATER_ON"}'
```
Response:
```json
{
"success": true,
"message": "Command sent successfully",
"command": "HEATER_ON"
}
```
### 5. Cek Database Railway
Login ke Railway MySQL:
```bash
# Via Railway CLI
railway connect MySQL
```
Atau via Railway Dashboard → MySQL → Connect → Copy connection string
Query untuk verifikasi:
```sql
-- Cek apakah ada data di control_commands
SELECT COUNT(*) as total FROM control_commands;
-- Lihat 10 command terakhir
SELECT * FROM control_commands ORDER BY timestamp DESC LIMIT 10;
-- Lihat status relay terakhir
SELECT * FROM status_history
WHERE message LIKE 'RELAY:%'
ORDER BY timestamp DESC LIMIT 10;
```
---
## Testing dengan ESP32
### 1. Update Kode ESP32 (jika perlu)
Pastikan ESP32 menggunakan URL Railway yang benar:
```cpp
// Di file esp32_pengering_ikan_railway.ino
const char* mqtt_server = "broker.hivemq.com";
const int mqtt_port = 1883;
```
### 2. Upload ke ESP32
```bash
# Via Arduino IDE atau PlatformIO
# Upload esp32_pengering_ikan_v2.ino
```
### 3. Monitor Serial ESP32
Buka Serial Monitor (115200 baud), pastikan muncul:
```
WiFi Connected
MQTT Connected
Heater → ON
Fan → ON
Data sent: {"suhu":28.5,"berat":450,...}
```
### 4. Cek Railway Logs Real-time
Di Railway Dashboard → Logs, pastikan muncul:
```
📨 MQTT Message received:
Topic: novil/pengering/control
Message: STATUS:HEATER_ON
🎛️ Control Message: STATUS:HEATER_ON
📊 Relay Status Update: HEATER_ON
✅ Relay status saved to MySQL database
```
---
## Troubleshooting
### ❌ Logs tidak muncul "Subscribed to control topic"
**Solusi:**
1. Pastikan file `server.js` sudah ter-commit dan ter-push
2. Trigger manual redeploy di Railway Dashboard
3. Cek Railway logs untuk error
### ❌ Database connection failed
**Solusi:**
1. Cek environment variables di Railway Dashboard
2. Pastikan MySQL plugin sudah ter-install
3. Verifikasi `DATABASE_URL` atau `MYSQL*` variables ada
### ❌ MQTT not connected
**Solusi:**
1. Cek `MQTT_BROKER_URL` di Railway environment variables
2. Default: `mqtt://broker.hivemq.com:1883`
3. Pastikan Railway tidak block port 1883
### ❌ Control commands tidak masuk database
**Solusi:**
1. Cek Railway logs: `railway logs`
2. Pastikan ESP32 kirim ke topic yang benar: `novil/pengering/control`
3. Verifikasi database table `control_commands` sudah ada:
```sql
SHOW TABLES;
DESCRIBE control_commands;
```
---
## Checklist Deployment
- [ ] Commit perubahan `server.js`
- [ ] Push ke Git repository
- [ ] Railway auto-deploy selesai (status: Deployed)
- [ ] Cek logs: "Subscribed to control topic" muncul
- [ ] Test API health check berhasil
- [ ] Test API stats menunjukkan `controlCommandsCount`
- [ ] Test kirim command via API berhasil
- [ ] Database table `control_commands` ada data
- [ ] ESP32 connect ke MQTT berhasil
- [ ] ESP32 kirim status relay, muncul di Railway logs
- [ ] Database `control_commands` bertambah saat ESP32 kirim status
---
## URL Penting
Ganti dengan URL Railway Anda:
- **Backend API:** `https://your-app.up.railway.app`
- **Health Check:** `https://your-app.up.railway.app/`
- **Stats:** `https://your-app.up.railway.app/api/stats`
- **Control:** `https://your-app.up.railway.app/api/control`
- **Data History:** `https://your-app.up.railway.app/api/data/history`
---
## Setelah Deploy Berhasil
1. ✅ Update dokumentasi dengan URL Railway yang benar
2. ✅ Test dengan Flutter app (jika ada)
3. ✅ Monitor logs Railway untuk memastikan tidak ada error
4. ✅ Setup monitoring/alerting (optional)
5. ✅ Backup database secara berkala
---
## Kontak & Support
Jika ada masalah:
1. Cek Railway logs: `railway logs`
2. Cek Railway Dashboard → Service → Logs
3. Cek Railway Dashboard → MySQL → Metrics
4. Review dokumentasi: `PERBAIKAN_CONTROL_COMMANDS.md`

View File

@ -1,450 +0,0 @@
# 🔧 Fix: Data Tidak Masuk ke MySQL Railway
## 🔍 Masalah yang Ditemukan
### 1. **Backend Tidak Connect ke HiveMQ**
- Backend menggunakan **Aedes** sebagai MQTT Broker internal
- Aedes broker **DISABLED** di Railway (tidak bisa bind port 1883)
- ESP32 publish data ke **HiveMQ** (`broker.hivemq.com`)
- Backend **TIDAK subscribe** ke HiveMQ, jadi tidak terima data dari ESP32
### 2. **Alur Data yang Salah**
```
❌ SEBELUM (TIDAK BERFUNGSI):
ESP32 → HiveMQ (broker.hivemq.com)
(data hilang)
Backend Railway (Aedes Broker) → MySQL
(tidak ada data)
```
### 3. **Solusi yang Benar**
```
✅ SESUDAH (BERFUNGSI):
ESP32 → HiveMQ (broker.hivemq.com)
Backend Railway (MQTT Client)
MySQL Database
```
---
## ✅ Perubahan yang Dilakukan
### 1. **Install MQTT Client Package**
**File: `package.json`**
```json
"dependencies": {
"mqtt": "^5.3.5", // ← TAMBAHAN BARU
...
}
```
**Install:**
```bash
cd backend
npm install mqtt
```
---
### 2. **Backend Connect ke HiveMQ sebagai Client**
**File: `server.js`**
#### Import MQTT Client:
```javascript
const mqtt = require('mqtt'); // ← TAMBAHAN BARU
```
#### Konfigurasi:
```javascript
const MQTT_BROKER_URL = process.env.MQTT_BROKER_URL || 'mqtt://broker.hivemq.com:1883';
const MQTT_TOPICS = {
data: 'novil/pengering/data',
status: 'novil/pengering/status',
button: 'novil/pengering/button',
control: 'novil/pengering/control'
};
```
#### Connect ke HiveMQ:
```javascript
const mqttClient = mqtt.connect(MQTT_BROKER_URL, {
clientId: `backend_${Math.random().toString(16).slice(2, 10)}`,
clean: true,
connectTimeout: 4000,
reconnectPeriod: 1000,
keepalive: 60
});
mqttClient.on('connect', () => {
console.log('✅ Connected to MQTT Broker (HiveMQ)');
// Subscribe to all topics
mqttClient.subscribe([
MQTT_TOPICS.data,
MQTT_TOPICS.status,
MQTT_TOPICS.button
]);
});
```
#### Handle Messages dari ESP32:
```javascript
mqttClient.on('message', async (topic, message) => {
const msg = message.toString();
// TOPIC: novil/pengering/data
if (topic === MQTT_TOPICS.data) {
const data = JSON.parse(msg);
// Update latestData
latestData = {
suhu: data.suhu || 0,
berat: data.berat || 0,
target: data.target || 0,
...
};
// Save to MySQL
await db.insertSensorData(latestData);
console.log('✅ Data saved to MySQL database');
}
// TOPIC: novil/pengering/status
if (topic === MQTT_TOPICS.status) {
// Update status
latestData.status = ...;
// Save to MySQL
await db.insertStatusHistory(msg);
console.log('✅ Status saved to MySQL database');
}
// TOPIC: novil/pengering/button
if (topic === MQTT_TOPICS.button) {
// Save button event
await db.insertStatusHistory(`BUTTON: ${msg}`);
console.log('✅ Button event saved to MySQL database');
}
});
```
#### Publish Command ke ESP32:
```javascript
app.post('/api/control', async (req, res) => {
const { command } = req.body;
// Publish to HiveMQ
mqttClient.publish(MQTT_TOPICS.control, command, { qos: 1 }, (err) => {
if (err) {
return res.status(500).json({ success: false, error: err.message });
}
console.log(`✅ Command published to MQTT: ${command}`);
res.json({ success: true, command: command });
});
});
```
---
### 3. **Update Environment Variables**
**File: `.env`**
```env
# MQTT Broker Configuration
MQTT_BROKER_URL=mqtt://broker.hivemq.com:1883
# MySQL Database Configuration (Railway)
# Isi dengan credentials dari Railway MySQL Plugin
DATABASE_URL=mysql://user:password@host:port/database
# atau:
MYSQLHOST=your-railway-mysql-host.railway.app
MYSQLPORT=3306
MYSQLUSER=root
MYSQLPASSWORD=your-password
MYSQLDATABASE=railway
```
---
## 🚀 Cara Deploy ke Railway
### 1. **Install Dependencies**
```bash
cd backend
npm install
```
### 2. **Test Lokal**
```bash
npm start
```
**Expected Output:**
```
🔄 Connecting to MQTT Broker: mqtt://broker.hivemq.com:1883
✅ Connected to MQTT Broker (HiveMQ)
✅ Subscribed to topics:
- novil/pengering/data
- novil/pengering/status
- novil/pengering/button
🚀 REST API running on port 3000
✅ Server is ready!
```
### 3. **Test dengan ESP32**
- Upload sketch ESP32
- Tekan button untuk start pengeringan
- Cek log backend:
```
📨 MQTT Message received:
Topic: novil/pengering/status
Message: PENGERINGAN DIMULAI
✅ Status saved to MySQL database
📨 MQTT Message received:
Topic: novil/pengering/data
Message: {"suhu":28.5,"berat":450,"target":315}
✅ Data saved to MySQL database
```
### 4. **Push ke GitHub**
```bash
git add .
git commit -m "Fix: Backend connect to HiveMQ as MQTT client"
git push
```
### 5. **Configure Railway Environment Variables**
Di Railway Dashboard:
1. Buka project → Variables
2. Tambahkan:
- `MQTT_BROKER_URL` = `mqtt://broker.hivemq.com:1883`
3. MySQL credentials sudah otomatis dari Railway MySQL Plugin:
- `MYSQLHOST`
- `MYSQLPORT`
- `MYSQLUSER`
- `MYSQLPASSWORD`
- `MYSQLDATABASE`
### 6. **Redeploy**
Railway akan auto-deploy setelah push ke GitHub.
---
## 🧪 Testing
### 1. **Test MQTT Connection**
```bash
curl https://your-app.railway.app/api/stats
```
**Expected Response:**
```json
{
"success": true,
"stats": {
"mqttConnected": true, // ← Harus true
"sensorDataCount": 123,
"statusHistoryCount": 45,
...
}
}
```
### 2. **Test Data dari ESP32**
- Jalankan ESP32
- Tunggu beberapa detik
- Check database:
```bash
curl https://your-app.railway.app/api/data/latest
```
**Expected Response:**
```json
{
"success": true,
"data": {
"suhu": 28.5,
"berat": 450.0,
"target": 315.0,
"status": "RUNNING",
"timestamp": "2026-05-28T10:30:00.000Z"
}
}
```
### 3. **Test History**
```bash
curl https://your-app.railway.app/api/data/history?limit=10
```
**Expected Response:**
```json
{
"success": true,
"count": 10,
"data": [
{
"id": 1,
"suhu": 28.5,
"berat": 450.0,
"target": 315.0,
"timestamp": "2026-05-28 10:30:00"
},
...
]
}
```
### 4. **Test Send Command**
```bash
curl -X POST https://your-app.railway.app/api/control \
-H "Content-Type: application/json" \
-d '{"command":"HEATER_ON"}'
```
**Expected Response:**
```json
{
"success": true,
"message": "Command sent successfully",
"command": "HEATER_ON"
}
```
**Check ESP32 Serial Monitor:**
```
MQTT Message [novil/pengering/control] : HEATER_ON
HEATER ON
```
---
## 📊 Monitoring
### Railway Logs
```bash
# Di Railway Dashboard → Deployments → View Logs
```
**Expected Logs:**
```
✅ Connected to MQTT Broker (HiveMQ)
✅ Subscribed to topics
📨 MQTT Message received: novil/pengering/data
✅ Data saved to MySQL database
📨 MQTT Message received: novil/pengering/status
✅ Status saved to MySQL database
```
### ESP32 Serial Monitor
```
MQTT Data Sent: {"suhu":28.5,"berat":450,"target":315}
MQTT Data Sent: {"suhu":28.6,"berat":448,"target":315}
```
---
## 🔍 Troubleshooting
### Problem: `mqttConnected: false`
**Cause:** Backend tidak bisa connect ke HiveMQ
**Solution:**
1. Check Railway logs untuk error
2. Pastikan `MQTT_BROKER_URL` benar
3. Check firewall/network Railway
---
### Problem: Data masih tidak masuk ke MySQL
**Cause:** Database credentials salah
**Solution:**
1. Check Railway MySQL Plugin variables
2. Pastikan `MYSQLHOST`, `MYSQLUSER`, `MYSQLPASSWORD`, `MYSQLDATABASE` benar
3. Test connection:
```bash
curl https://your-app.railway.app/api/database/test
```
---
### Problem: ESP32 tidak terima command
**Cause:** ESP32 tidak subscribe ke topic control
**Solution:**
1. Check ESP32 Serial Monitor:
```
Subscribed to: novil/pengering/control
```
2. Restart ESP32
3. Check MQTT connection
---
## 📝 Ringkasan Perubahan
| Aspek | Sebelum | Sesudah |
|-------|---------|---------|
| MQTT Role | ❌ Broker (Aedes) | ✅ Client (mqtt.js) |
| Connect to HiveMQ | ❌ Tidak | ✅ Ya |
| Subscribe Topics | ❌ Tidak | ✅ Ya (data, status, button) |
| Publish Commands | ⚠️ Via Aedes | ✅ Via HiveMQ |
| Save to MySQL | ❌ Tidak ada data | ✅ Berfungsi |
| Package | Aedes only | ✅ Aedes + mqtt |
---
## ✅ Checklist
### Pre-Deploy:
- [x] Install package `mqtt`
- [x] Update `server.js` dengan MQTT client
- [x] Update `.env` dengan MQTT_BROKER_URL
- [x] Test lokal
### Deploy:
- [ ] Push ke GitHub
- [ ] Configure Railway environment variables
- [ ] Wait for auto-deploy
- [ ] Check Railway logs
### Post-Deploy:
- [ ] Test `/api/stats``mqttConnected: true`
- [ ] Test ESP32 → data masuk ke MySQL
- [ ] Test `/api/data/latest` → ada data
- [ ] Test `/api/data/history` → ada history
- [ ] Test send command → ESP32 terima
---
## 🎯 Expected Result
Setelah fix ini:
1. ✅ Backend connect ke HiveMQ sebagai MQTT client
2. ✅ Backend subscribe ke topic `novil/pengering/data`, `status`, `button`
3. ✅ ESP32 publish data → Backend terima → Save ke MySQL
4. ✅ Flutter send command → Backend publish ke HiveMQ → ESP32 terima
5. ✅ Data history tersimpan di MySQL Railway
6. ✅ Status history tersimpan di MySQL Railway
---
Selamat mencoba! 🚀🎉

View File

@ -1,255 +0,0 @@
# 🔧 Fix Railway 502 Error
## ❌ Masalah yang Ditemukan
### 1. **Error: `digitalRead()` is not defined**
Di `server.js` baris 105-108, ada kode Arduino yang tidak valid di Node.js:
```javascript
relay1: digitalRead(RELAY1) === 'LOW', // ❌ ERROR!
```
**Penyebab:** Copy-paste dari kode Arduino ke Node.js
**Fix:** Ganti dengan keep current relay status:
```javascript
relay1: latestData.relay1 || false, // ✅ FIXED
```
---
### 2. **Error: MQTT Broker Port Binding Failed**
Railway tidak support custom ports (1883, 8883) untuk MQTT broker.
**Penyebab:**
```javascript
mqttServer.listen(MQTT_PORT, () => { // ❌ Port 1883 tidak tersedia
console.log(`🚀 MQTT Broker running on port ${MQTT_PORT}`);
});
```
**Fix:** Disable MQTT broker di Railway, gunakan external broker (HiveMQ):
```javascript
if (process.env.ENABLE_MQTT_BROKER === 'true') {
mqttServer.listen(MQTT_PORT, () => {
console.log(`🚀 MQTT Broker running on port ${MQTT_PORT}`);
}).on('error', (err) => {
console.warn(`⚠️ MQTT Broker failed to start`);
});
} else {
console.log(' MQTT Broker disabled (use external MQTT broker)');
}
```
---
## ✅ Perubahan yang Dilakukan
### File: `server.js`
#### 1. Fix `digitalRead()` Error
**Baris 100-111:**
```javascript
// SEBELUM (❌ ERROR):
latestData = {
suhu: data.suhu || 0,
berat: data.berat || 0,
target: data.target || 0,
relay1: digitalRead(RELAY1) === 'LOW', // ❌
relay2: digitalRead(RELAY2) === 'LOW', // ❌
relay3: digitalRead(RELAY3) === 'LOW', // ❌
relay4: digitalRead(RELAY4) === 'LOW', // ❌
status: latestData.status,
timestamp: new Date().toISOString()
};
// SESUDAH (✅ FIXED):
latestData = {
suhu: data.suhu || 0,
berat: data.berat || 0,
target: data.target || 0,
relay1: latestData.relay1 || false, // ✅ Keep current status
relay2: latestData.relay2 || false, // ✅
relay3: latestData.relay3 || false, // ✅
relay4: latestData.relay4 || false, // ✅
status: latestData.status,
timestamp: new Date().toISOString()
};
```
#### 2. Disable MQTT Broker di Railway
**Baris 180-210:**
```javascript
// SEBELUM (❌ CRASH):
mqttServer.listen(MQTT_PORT, () => {
console.log(`🚀 MQTT Broker running on port ${MQTT_PORT}`);
});
httpServer.listen(WS_PORT, () => {
console.log(`🌐 WebSocket MQTT running on port ${WS_PORT}`);
});
// SESUDAH (✅ FIXED):
if (process.env.ENABLE_MQTT_BROKER === 'true') {
mqttServer.listen(MQTT_PORT, () => {
console.log(`🚀 MQTT Broker running on port ${MQTT_PORT}`);
}).on('error', (err) => {
console.warn(`⚠️ MQTT Broker failed to start on port ${MQTT_PORT}:`, err.message);
console.log(' MQTT Broker disabled. Use external MQTT broker (e.g., HiveMQ)');
});
} else {
console.log(' MQTT Broker disabled (use external MQTT broker like HiveMQ)');
}
if (process.env.ENABLE_MQTT_BROKER === 'true') {
httpServer.listen(WS_PORT, () => {
console.log(`🌐 WebSocket MQTT running on port ${WS_PORT}`);
}).on('error', (err) => {
console.warn(`⚠️ WebSocket MQTT failed to start on port ${WS_PORT}:`, err.message);
});
} else {
console.log(' WebSocket MQTT disabled');
}
```
---
## 🚀 Deploy
```bash
cd backend
git add .
git commit -m "Fix: Remove digitalRead error and disable MQTT broker in Railway"
git push
```
**Pushed to GitHub!** Railway akan auto-deploy dalam 2-3 menit.
---
## 🧪 Test Setelah Deploy
Tunggu 2-3 menit, lalu test:
### PowerShell:
```powershell
# Health check
Invoke-WebRequest -Uri "https://web-production-47eb.up.railway.app/" -UseBasicParsing
# Test database
Invoke-WebRequest -Uri "https://web-production-47eb.up.railway.app/api/database/test" -UseBasicParsing
# Initialize tables
Invoke-WebRequest -Uri "https://web-production-47eb.up.railway.app/api/database/init" -Method POST -UseBasicParsing
# Get latest data
Invoke-WebRequest -Uri "https://web-production-47eb.up.railway.app/api/data/latest" -UseBasicParsing
```
### Browser:
```
https://web-production-47eb.up.railway.app/
https://web-production-47eb.up.railway.app/api/database/test
https://web-production-47eb.up.railway.app/api/data/latest
```
---
## 📊 Expected Logs di Railway
Setelah deploy berhasil, Railway logs akan menampilkan:
```
📦 Using MYSQL_URL for connection
🔧 Database Config: {
host: 'mysql.railway.internal',
port: 3306,
user: 'root',
database: 'railway',
password: '***'
}
✅ Database connected successfully
✅ Database tables initialized
MQTT Broker disabled (use external MQTT broker like HiveMQ)
WebSocket MQTT disabled
🚀 REST API running on port 3000
✅ Server is ready!
```
---
## 🔄 Arsitektur Baru
### Sebelum (❌ CRASH):
```
Backend di Railway:
├── REST API ✅
├── MQTT Broker (port 1883) ❌ CRASH!
└── WebSocket MQTT (port 8883) ❌ CRASH!
```
### Sesudah (✅ WORKS):
```
Backend di Railway:
└── REST API ✅ (port 3000)
MQTT Broker External (HiveMQ):
└── broker.hivemq.com:1883 ✅
ESP32:
├── Connect ke HiveMQ ✅
└── Publish data ✅
Flutter:
├── Connect ke HiveMQ (real-time) ✅
└── Call REST API (control) ✅
```
---
## 📝 Catatan Penting
### 1. **MQTT Broker di Railway**
Railway **TIDAK SUPPORT** custom ports untuk MQTT broker.
- ❌ Port 1883 (MQTT) tidak bisa digunakan
- ❌ Port 8883 (WebSocket MQTT) tidak bisa digunakan
- ✅ Solusi: Gunakan **HiveMQ Cloud** (public MQTT broker)
### 2. **ESP32 Configuration**
ESP32 harus connect ke **HiveMQ**, bukan Railway:
```cpp
const char* mqtt_server = "broker.hivemq.com"; // ✅ Public broker
// BUKAN: "web-production-47eb.up.railway.app" // ❌ Tidak support MQTT
```
### 3. **Relay Status Tracking**
Backend tidak bisa tahu relay status dari MQTT (ESP32 tidak kirim).
- Backend track relay status saat kirim command via `/api/control`
- Flutter ambil relay status dari `/api/data/latest`
---
## ✅ Checklist
- [x] Fix `digitalRead()` error
- [x] Disable MQTT broker di Railway
- [x] Add error handling untuk port binding
- [x] Commit dan push ke GitHub
- [ ] Tunggu Railway deploy (2-3 menit)
- [ ] Test endpoints
- [ ] Cek Railway logs
- [ ] Initialize database tables
- [ ] Test dengan ESP32
---
## 🎉 Done!
Backend sekarang akan:
- ✅ Start tanpa crash
- ✅ Connect ke MySQL Railway
- ✅ Handle REST API requests
- ✅ Tidak mencoba start MQTT broker (karena tidak support)
ESP32 dan Flutter akan connect ke **HiveMQ** untuk MQTT, dan call **Railway** untuk REST API.
Selamat! 🚀

View File

@ -1,263 +0,0 @@
# 🔄 Perubahan Backend untuk ESP32
## ✅ Perubahan yang Dilakukan
### 1. **Update MQTT Handler di `server.js`**
#### ✨ Handler untuk `novil/pengering/data`
**Sebelum:**
```javascript
// Hanya parse JSON dan simpan
latestData = { ...data, timestamp: new Date().toISOString() };
```
**Sesudah:**
```javascript
// Parse JSON dengan field yang sesuai ESP32
latestData = {
suhu: data.suhu || 0,
berat: data.berat || 0,
target: data.target || 0,
relay1: false, // Akan diupdate dari status
relay2: false,
relay3: false,
relay4: false,
status: latestData.status,
timestamp: new Date().toISOString()
};
```
**Alasan:** ESP32 hanya kirim `suhu`, `berat`, `target` (tidak kirim relay status)
---
#### ✨ Handler untuk `novil/pengering/status`
**Ditambahkan:** Auto-update status berdasarkan pesan
```javascript
if (message.includes('ESP32 CONNECTED')) {
latestData.status = 'CONNECTED';
} else if (message.includes('PENGERINGAN SIAP')) {
latestData.status = 'READY';
} else if (message.includes('PENGERINGAN DIMULAI')) {
latestData.status = 'RUNNING';
} else if (message.includes('PENGERINGAN BERJALAN')) {
latestData.status = 'RUNNING';
} else if (message.includes('PENGERINGAN SELESAI')) {
latestData.status = 'COMPLETED';
} else if (message.includes('SCAN BERAT')) {
latestData.status = 'SCANNING';
} else if (message.includes('DHT ERROR')) {
latestData.status = 'ERROR';
}
```
**Alasan:** Status otomatis terupdate sesuai pesan dari ESP32
---
#### ✨ Handler untuk `novil/pengering/button` (BARU)
**Ditambahkan:** Handler untuk event button
```javascript
if (topic === 'novil/pengering/button') {
console.log('🔘 Button Event:', message);
await db.insertStatusHistory(`BUTTON: ${message}`);
}
```
**Alasan:** ESP32 publish event button press ke topic ini
---
### 2. **Update Endpoint `/api/control`**
**Ditambahkan:** Command `START` dan `RESET`
```javascript
const validCommands = [
'HEATER_ON', 'HEATER_OFF',
'FAN_ON', 'FAN_OFF',
'LAMP_ON', 'LAMP_OFF',
'EXHAUST_ON', 'EXHAUST_OFF',
'START', // ← BARU
'RESET' // ← BARU
];
```
**Alasan:** Flutter bisa start/reset pengeringan via API
---
### 3. **Fix Timestamp Format di `database.js`**
**Sebelum:**
```javascript
data.timestamp || new Date() // ❌ Format ISO tidak diterima MySQL
```
**Sesudah:**
```javascript
const date = new Date(data.timestamp);
const mysqlTimestamp = date.toISOString().slice(0, 19).replace('T', ' ');
// Hasil: '2026-05-28 10:30:00' ✅
```
**Alasan:** MySQL DATETIME tidak terima format ISO dengan 'Z'
---
## 📡 MQTT Topics yang Digunakan
| Topic | Direction | Format | Keterangan |
|-------|-----------|--------|------------|
| `novil/pengering/data` | ESP32 → Backend | JSON | Data sensor (suhu, berat, target) |
| `novil/pengering/status` | ESP32 → Backend | String | Status/event penting |
| `novil/pengering/button` | ESP32 → Backend | String | Event button press |
| `novil/pengering/control` | Backend → ESP32 | String | Command kontrol relay/sistem |
---
## 🌐 REST API Endpoints
### Endpoint untuk Flutter App:
1. **GET /api/data/latest** - Ambil data terbaru
2. **GET /api/data/history?limit=50** - Ambil riwayat data
3. **GET /api/status/history?limit=50** - Ambil riwayat status
4. **POST /api/control** - Kirim command (START, RESET, HEATER_ON, dll)
5. **GET /api/stats** - Ambil statistik
6. **DELETE /api/history/clear** - Hapus riwayat
### Endpoint untuk Testing:
7. **GET /** - Health check
8. **POST /api/database/init** - Init database (sekali saja)
9. **GET /api/database/test** - Test koneksi database
---
## 🔄 Flow Komunikasi
### ESP32 → Backend (via MQTT)
```
ESP32 publish:
├── novil/pengering/data → Data sensor setiap 1 detik
├── novil/pengering/status → Status/event penting
└── novil/pengering/button → Event button press
Backend:
├── Terima via MQTT
├── Update latestData
└── Simpan ke MySQL
```
### Flutter → ESP32 (via Backend)
```
Flutter:
└── POST /api/control {"command":"HEATER_ON"}
Backend:
├── Terima REST API
├── Simpan command ke database
└── Publish ke MQTT: novil/pengering/control
ESP32:
└── Subscribe novil/pengering/control
└── Eksekusi command (nyalakan relay)
```
---
## 🚀 Cara Deploy
### 1. Push ke GitHub
```bash
cd backend
git add .
git commit -m "Update backend untuk ESP32 MQTT integration"
git push
```
### 2. Railway Auto-Deploy
Railway akan otomatis detect perubahan dan deploy ulang.
### 3. Test Endpoint
```bash
# Health check
curl https://web-production-47eb.up.railway.app/
# Get latest data
curl https://web-production-47eb.up.railway.app/api/data/latest
# Send command
curl -X POST https://web-production-47eb.up.railway.app/api/control \
-H "Content-Type: application/json" \
-d '{"command":"START"}'
```
---
## 📝 Catatan Penting
### ✅ Yang Sudah Kompatibel:
- ✅ MQTT topics sesuai dengan ESP32
- ✅ Format data JSON sesuai
- ✅ Command START dan RESET tersedia
- ✅ Status auto-update dari pesan ESP32
- ✅ Timestamp format MySQL sudah fix
### ⚠️ Yang Perlu Diperhatikan:
1. **ESP32 tidak kirim relay status** di topic `data`
- Backend tidak bisa tahu relay ON/OFF dari data
- Solusi: Tracking relay status di backend saat kirim command
2. **MQTT Broker**
- ESP32 pakai: `broker.hivemq.com` (public)
- Railway mungkin tidak bisa host MQTT broker sendiri
- Solusi: Tetap pakai HiveMQ public atau upgrade Railway plan
3. **Database Connection**
- Pastikan Railway MySQL sudah setup
- Environment variables harus diisi:
- `DB_HOST`
- `DB_PORT`
- `DB_USER`
- `DB_PASSWORD`
- `DB_NAME`
---
## 🧪 Testing Checklist
- [ ] ESP32 connect ke MQTT broker
- [ ] ESP32 publish data ke `novil/pengering/data`
- [ ] Backend terima dan simpan data ke MySQL
- [ ] ESP32 publish status ke `novil/pengering/status`
- [ ] Backend update status otomatis
- [ ] Flutter kirim command via `/api/control`
- [ ] ESP32 terima command dari `novil/pengering/control`
- [ ] Relay ON/OFF sesuai command
- [ ] Flutter ambil data via `/api/data/latest`
- [ ] Flutter ambil history via `/api/data/history`
---
## 📚 File yang Berubah
1. ✅ `server.js` - Update MQTT handlers dan endpoint
2. ✅ `database.js` - Fix timestamp format
3. ✅ `API_DOCUMENTATION.md` - Dokumentasi lengkap API (BARU)
4. ✅ `PERUBAHAN_BACKEND.md` - Ringkasan perubahan (BARU)
---
## 🎯 Next Steps
1. **Push ke GitHub dan deploy ke Railway**
2. **Test ESP32 connection**
3. **Update Flutter app untuk pakai endpoint baru**
4. **Test end-to-end flow**
Selamat mencoba! 🚀

View File

@ -1,289 +0,0 @@
# ✅ Railway Configuration - Verified
## 🎯 Environment Variables di Railway Anda
Railway Anda sudah menggunakan format yang **BENAR** dan **SUDAH DIDUKUNG** oleh backend:
```bash
DB_HOST=mysql.railway.internal
DB_NAME=railway
DB_PASSWORD=jIRceppgKCeUdEAjeYtzLaiAdPbBhDPX
DB_PORT=3306
DB_USER=root
MQTT_PORT=1883
NODE_ENV=production
WS_PORT=8883
```
✅ **Backend otomatis detect dan gunakan variables ini!**
---
## 🔍 Penjelasan Variables
### Database Variables (MySQL):
| Variable | Value | Keterangan |
|----------|-------|------------|
| `DB_HOST` | `mysql.railway.internal` | Internal hostname Railway MySQL |
| `DB_PORT` | `3306` | Port MySQL standard |
| `DB_USER` | `root` | Username MySQL |
| `DB_PASSWORD` | `jIRc...hDPX` | Password MySQL (disembunyikan) |
| `DB_NAME` | `railway` | Nama database |
### Server Variables:
| Variable | Value | Keterangan |
|----------|-------|------------|
| `NODE_ENV` | `production` | Environment mode |
| `MQTT_PORT` | `1883` | Port MQTT broker (tidak digunakan di Railway) |
| `WS_PORT` | `8883` | Port WebSocket MQTT (tidak digunakan di Railway) |
---
## 📝 Catatan Penting
### 1. **`mysql.railway.internal`**
Ini adalah **internal hostname** Railway untuk MySQL.
- ✅ Hanya bisa diakses dari dalam Railway network
- ✅ Lebih cepat dan aman
- ✅ Tidak perlu expose ke public
### 2. **MQTT_PORT dan WS_PORT**
Railway **TIDAK SUPPORT** custom ports untuk MQTT broker.
- ❌ Port 1883 dan 8883 tidak bisa digunakan di Railway
- ✅ Solusi: Gunakan **HiveMQ Cloud** (public MQTT broker)
- ✅ ESP32 connect ke `broker.hivemq.com`
- ✅ Backend hanya handle REST API, tidak host MQTT broker
### 3. **Database Name: `railway`**
Railway otomatis create database dengan nama `railway`.
- ✅ Backend sudah support ini
- ✅ Tidak perlu ganti nama database
---
## 🚀 Cara Kerja Backend di Railway
### 1. **Backend Startup**
```javascript
// Backend baca environment variables
DB_HOST=mysql.railway.internal
DB_PORT=3306
DB_USER=root
DB_PASSWORD=jIRc...
DB_NAME=railway
// Backend connect ke MySQL
📦 Using individual environment variables for connection
🔧 Database Config: {
host: 'mysql.railway.internal',
port: 3306,
user: 'root',
database: 'railway',
password: '***'
}
✅ Database connected successfully
✅ Database tables initialized
```
### 2. **Backend Create Tables**
Backend otomatis create 3 tables di database `railway`:
- `sensor_data` - Data sensor dari ESP32
- `status_history` - Riwayat status
- `control_commands` - Riwayat command
### 3. **Backend Ready**
```
🚀 REST API running on port 3000
📡 MQTT Broker: mqtt://localhost:1883 (DISABLED di Railway)
🌐 WebSocket MQTT: ws://localhost:8883 (DISABLED di Railway)
✅ Server is ready!
```
---
## 🔄 Flow Komunikasi
### ESP32 → Backend (via MQTT Public Broker)
```
ESP32:
├── Connect ke broker.hivemq.com (PUBLIC)
└── Publish data ke topic: novil/pengering/data
Backend di Railway:
├── Connect ke broker.hivemq.com (PUBLIC)
├── Subscribe topic: novil/pengering/data
├── Terima data dari ESP32
└── Simpan ke MySQL (mysql.railway.internal)
```
### Flutter → Backend (via REST API)
```
Flutter:
└── POST https://web-production-47eb.up.railway.app/api/control
Backend di Railway:
├── Terima REST API request
├── Simpan command ke MySQL
└── Publish command ke broker.hivemq.com
ESP32:
└── Terima command dari broker.hivemq.com
```
---
## 🧪 Test Connection
### 1. Test Database Connection
```bash
curl https://web-production-47eb.up.railway.app/api/database/test
```
**Expected Response:**
```json
{
"success": true,
"message": "Database connection successful"
}
```
### 2. Initialize Database Tables
```bash
curl -X POST https://web-production-47eb.up.railway.app/api/database/init
```
**Expected Response:**
```json
{
"success": true,
"message": "Database tables initialized successfully"
}
```
### 3. Check Server Status
```bash
curl https://web-production-47eb.up.railway.app/
```
**Expected Response:**
```json
{
"status": "OK",
"message": "Pengering Ikan Backend Server",
"version": "1.0.0",
"uptime": 123.45,
"timestamp": "2026-05-28T10:30:00.000Z"
}
```
---
## 🛠️ Troubleshooting
### Error: "Database connection failed"
**Cek Railway Logs:**
```bash
# Di Railway Dashboard:
# 1. Go to your service
# 2. Click "Deployments"
# 3. Click latest deployment
# 4. View logs
```
**Cari log:**
```
📦 Using individual environment variables for connection
🔧 Database Config: { ... }
❌ Database connection failed: ...
```
**Solusi:**
1. Pastikan MySQL service running di Railway
2. Pastikan environment variables benar
3. Pastikan `mysql.railway.internal` bisa diakses
---
### Error: "Access denied for user 'root'"
**Solusi:**
1. Cek `DB_PASSWORD` benar
2. Cek `DB_USER` benar
3. Restart MySQL service di Railway
---
### Error: "Unknown database 'railway'"
**Solusi:**
1. Database belum dibuat
2. Connect ke MySQL via Railway CLI:
```bash
railway connect mysql
```
3. Create database:
```sql
CREATE DATABASE railway;
```
---
## ✅ Checklist Setup
- [x] Environment variables sudah set di Railway
- [x] Backend code sudah support `DB_*` variables
- [ ] Deploy backend ke Railway
- [ ] Test `/api/database/test`
- [ ] Initialize tables via `/api/database/init`
- [ ] Test insert data via ESP32
- [ ] Test query data via `/api/data/latest`
---
## 🎯 Next Steps
### 1. Deploy Backend
```bash
cd backend
git add .
git commit -m "Update database config untuk Railway"
git push
```
### 2. Wait for Railway Deploy
Railway akan auto-deploy (2-3 menit).
### 3. Test Endpoints
```bash
# Health check
curl https://web-production-47eb.up.railway.app/
# Test database
curl https://web-production-47eb.up.railway.app/api/database/test
# Initialize tables
curl -X POST https://web-production-47eb.up.railway.app/api/database/init
```
### 4. Check Logs
Di Railway Dashboard, cek logs untuk:
```
✅ Database connected successfully
✅ Database tables initialized
✅ Server is ready!
```
---
## 🎉 Done!
Konfigurasi Railway Anda sudah **BENAR** dan **SIAP DIGUNAKAN**!
Backend akan otomatis:
- ✅ Connect ke MySQL di `mysql.railway.internal`
- ✅ Create tables di database `railway`
- ✅ Handle REST API requests
- ✅ Connect ke MQTT broker public (HiveMQ)
Tinggal deploy dan test! 🚀

View File

@ -1,317 +0,0 @@
# 🚂 Railway MySQL Setup Guide
## 📋 Environment Variables di Railway
Railway menyediakan environment variables dengan format yang berbeda tergantung cara setup MySQL.
### Format 1: Railway MySQL Plugin (Recommended)
Jika Anda menggunakan Railway MySQL Plugin, Railway otomatis menyediakan:
```bash
MYSQLHOST=containers-us-west-xxx.railway.app
MYSQLPORT=6543
MYSQLUSER=root
MYSQLPASSWORD=xxxxxxxxxxxxx
MYSQLDATABASE=railway
```
**Backend sudah support format ini!** Tidak perlu setting manual.
---
### Format 2: DATABASE_URL
Beberapa Railway service menyediakan `DATABASE_URL`:
```bash
DATABASE_URL=mysql://root:password@host:port/database
```
**Backend sudah support format ini!** Tidak perlu setting manual.
---
### Format 3: MYSQL_URL
Alternative format:
```bash
MYSQL_URL=mysql://root:password@host:port/database
```
**Backend sudah support format ini!** Tidak perlu setting manual.
---
### Format 4: Custom Variables (Local Development)
Untuk development lokal, gunakan:
```bash
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=your_password
DB_NAME=pengering_ikan
```
✅ **Backend sudah support format ini!**
---
## 🚀 Setup Railway MySQL
### Opsi 1: Menggunakan Railway MySQL Plugin (Recommended)
1. **Buka Railway Dashboard**
- Go to your project
- Click "New" → "Database" → "Add MySQL"
2. **Railway akan otomatis:**
- Create MySQL database
- Set environment variables:
- `MYSQLHOST`
- `MYSQLPORT`
- `MYSQLUSER`
- `MYSQLPASSWORD`
- `MYSQLDATABASE`
3. **Link ke Service Anda:**
- Railway otomatis link database ke service
- Environment variables tersedia di service
4. **Deploy:**
- Push code ke GitHub
- Railway auto-deploy
- Backend otomatis connect ke MySQL
**DONE!** Backend akan otomatis detect dan gunakan variables ini.
---
### Opsi 2: Menggunakan External MySQL (Aiven, PlanetScale, dll)
1. **Dapatkan Connection String:**
```
mysql://user:password@host:port/database
```
2. **Set di Railway:**
- Go to your service
- Click "Variables"
- Add variable:
- Name: `DATABASE_URL`
- Value: `mysql://user:password@host:port/database`
3. **Deploy:**
- Railway auto-redeploy
- Backend otomatis connect
✅ **DONE!**
---
## 🔍 Cek Connection di Railway
### 1. Lihat Logs
Di Railway Dashboard:
- Go to your service
- Click "Deployments"
- Click latest deployment
- View logs
Cari log:
```
📦 Using MYSQLHOST for connection
🔧 Database Config: { host: '...', port: 3306, ... }
✅ Database connected successfully
✅ Database tables initialized
```
### 2. Test via API
Setelah deploy, test endpoint:
```bash
# Test connection
curl https://your-app.railway.app/api/database/test
# Response jika berhasil:
{
"success": true,
"message": "Database connection successful"
}
```
### 3. Initialize Tables
Jika belum ada tabel, initialize:
```bash
curl -X POST https://your-app.railway.app/api/database/init
# Response:
{
"success": true,
"message": "Database tables initialized successfully"
}
```
---
## 🛠️ Troubleshooting
### Error: "Database connection failed"
**Cek 1: Environment Variables**
```bash
# Di Railway Dashboard → Variables, pastikan ada salah satu:
# - MYSQLHOST, MYSQLPORT, MYSQLUSER, MYSQLPASSWORD, MYSQLDATABASE
# - DATABASE_URL
# - MYSQL_URL
```
**Cek 2: MySQL Service Running**
```bash
# Di Railway Dashboard, pastikan MySQL service status = "Active"
```
**Cek 3: Network Access**
```bash
# Pastikan Railway service bisa akses MySQL
# Jika pakai external MySQL, cek firewall/whitelist
```
---
### Error: "Access denied for user"
**Solusi:**
1. Cek username dan password benar
2. Cek user punya permission ke database
3. Jika pakai Railway MySQL Plugin, coba restart service
---
### Error: "Unknown database"
**Solusi:**
1. Database belum dibuat
2. Jika pakai Railway MySQL Plugin, database otomatis dibuat dengan nama `railway`
3. Jika pakai external MySQL, buat database manual:
```sql
CREATE DATABASE pengering_ikan;
```
---
### Error: "Too many connections"
**Solusi:**
1. Backend sudah pakai connection pool (max 10 connections)
2. Jika masih error, cek MySQL max_connections setting
3. Atau upgrade Railway plan untuk lebih banyak connections
---
## 📊 Database Schema
Backend otomatis create 3 tables:
### 1. `sensor_data`
```sql
CREATE TABLE sensor_data (
id INT AUTO_INCREMENT PRIMARY KEY,
suhu FLOAT NOT NULL,
berat FLOAT NOT NULL,
target FLOAT NOT NULL,
relay1 BOOLEAN DEFAULT FALSE,
relay2 BOOLEAN DEFAULT FALSE,
relay3 BOOLEAN DEFAULT FALSE,
relay4 BOOLEAN DEFAULT FALSE,
status VARCHAR(50) DEFAULT 'DISCONNECTED',
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_timestamp (timestamp)
);
```
### 2. `status_history`
```sql
CREATE TABLE status_history (
id INT AUTO_INCREMENT PRIMARY KEY,
message TEXT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_timestamp (timestamp)
);
```
### 3. `control_commands`
```sql
CREATE TABLE control_commands (
id INT AUTO_INCREMENT PRIMARY KEY,
command VARCHAR(50) NOT NULL,
source VARCHAR(50) DEFAULT 'API',
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_timestamp (timestamp)
);
```
---
## 🎯 Best Practices
### 1. Gunakan Railway MySQL Plugin
- ✅ Otomatis setup
- ✅ Otomatis backup
- ✅ Otomatis scaling
- ✅ Gratis untuk development
### 2. Set Connection Pool
Backend sudah set:
```javascript
connectionLimit: 10 // Max 10 concurrent connections
```
### 3. Regular Cleanup
Backend punya endpoint untuk clear old data:
```bash
curl -X DELETE https://your-app.railway.app/api/history/clear
```
### 4. Monitor Logs
Cek Railway logs secara berkala untuk detect issues early.
---
## 📝 Checklist Setup
- [ ] Railway MySQL Plugin installed
- [ ] Environment variables tersedia
- [ ] Backend deployed
- [ ] Test `/api/database/test` berhasil
- [ ] Initialize tables via `/api/database/init`
- [ ] Test insert data via ESP32
- [ ] Test query data via `/api/data/latest`
---
## 🆘 Need Help?
1. **Cek Railway Logs** untuk error messages
2. **Cek Environment Variables** di Railway Dashboard
3. **Test Connection** via `/api/database/test`
4. **Check MySQL Status** di Railway Dashboard
---
## 🎉 Done!
Setelah setup berhasil:
- ✅ Backend otomatis connect ke MySQL
- ✅ Tables otomatis dibuat
- ✅ Data dari ESP32 otomatis tersimpan
- ✅ Flutter app bisa query data via API
Selamat! 🚀

View File

@ -1,285 +0,0 @@
# 🔧 Troubleshooting Railway Deployment
## ❌ Error 502: Application Failed to Respond
### Penyebab Umum:
1. **Missing Dependencies**
2. **Port Binding Error**
3. **Database Connection Failed**
4. **Syntax Error di Code**
5. **Environment Variables Salah**
---
## 🔍 Cara Debug
### 1. Cek Railway Logs
**Di Railway Dashboard:**
1. Go to your project
2. Click service name
3. Click "Deployments"
4. Click latest deployment
5. View "Deploy Logs" dan "Application Logs"
**Cari error:**
```
Error: Cannot find module 'websocket-stream'
Error: listen EADDRINUSE: address already in use :::1883
Error: connect ECONNREFUSED
SyntaxError: Unexpected token
```
---
## 🛠️ Fix yang Sudah Dilakukan
### Fix 1: Remove `digitalRead()` Error
**Error:**
```javascript
relay1: digitalRead(RELAY1) === 'LOW', // ❌ Arduino function in Node.js
```
**Fix:**
```javascript
relay1: latestData.relay1 || false, // ✅ Keep current status
```
**Status:** ✅ FIXED (commit b08ad68)
---
### Fix 2: Disable MQTT Broker
**Error:**
```
Error: listen EADDRINUSE: address already in use :::1883
```
**Penyebab:** Railway tidak support custom ports (1883, 8883)
**Fix:**
```javascript
if (process.env.ENABLE_MQTT_BROKER === 'true') {
mqttServer.listen(MQTT_PORT, () => {
console.log(`🚀 MQTT Broker running`);
}).on('error', (err) => {
console.warn(`⚠️ MQTT Broker failed`);
});
} else {
console.log(' MQTT Broker disabled');
}
```
**Status:** ✅ FIXED (commit b08ad68)
---
### Fix 3: Add Missing Dependency
**Error:**
```
Error: Cannot find module 'websocket-stream'
```
**Penyebab:** `websocket-stream` digunakan di code tapi tidak ada di `package.json`
**Fix:**
```json
{
"dependencies": {
"websocket-stream": "^5.5.2" // ✅ Added
}
}
```
**Status:** ✅ FIXED (commit 794201c)
---
## 🧪 Test Deployment
### Tunggu 2-3 Menit
Railway perlu waktu untuk:
1. Pull code dari GitHub
2. Install dependencies (`npm install`)
3. Build (jika perlu)
4. Start server (`npm start`)
### Test Health Check
```powershell
# PowerShell
Invoke-WebRequest -Uri "https://web-production-47eb.up.railway.app/" -UseBasicParsing
# Expected Response:
# StatusCode: 200
# Content: {"status":"OK","message":"Pengering Ikan Backend Server",...}
```
### Test Database
```powershell
Invoke-WebRequest -Uri "https://web-production-47eb.up.railway.app/api/database/test" -UseBasicParsing
# Expected Response:
# {"success":true,"message":"Database connection successful"}
```
---
## 📊 Expected Railway Logs
### Successful Deployment:
```
Building...
✓ Dependencies installed
✓ Build completed
Starting...
📦 Using MYSQL_URL for connection
🔧 Database Config: { host: 'mysql.railway.internal', ... }
✅ Database connected successfully
✅ Database tables initialized
MQTT Broker disabled (use external MQTT broker like HiveMQ)
WebSocket MQTT disabled
🚀 REST API running on port 3000
✅ Server is ready!
```
### Failed Deployment:
```
Building...
✓ Dependencies installed
Starting...
Error: Cannot find module 'xxx'
at Function.Module._resolveFilename
...
Application exited with code 1
```
---
## 🔄 Jika Masih Error 502
### 1. Cek Railway Logs
Lihat error message di logs.
### 2. Cek Environment Variables
Pastikan ada:
```
MYSQL_URL=mysql://root:...@mysql.railway.internal:3306/railway
NODE_ENV=production
PORT=3000
```
### 3. Cek Dependencies
Pastikan semua dependencies ada di `package.json`:
```json
{
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"aedes": "^0.51.3",
"ws": "^8.16.0",
"websocket-stream": "^5.5.2", // ← Harus ada!
"dotenv": "^16.4.5",
"morgan": "^1.10.0",
"mysql2": "^3.9.1"
}
}
```
### 4. Test Locally
```bash
cd backend
npm install
npm start
# Jika error, fix dulu sebelum push
```
### 5. Restart Railway Service
Di Railway Dashboard:
- Click service
- Click "Settings"
- Click "Restart"
---
## 📝 Checklist Deployment
- [x] Fix `digitalRead()` error
- [x] Disable MQTT broker
- [x] Add `websocket-stream` dependency
- [ ] Wait for Railway deploy (2-3 menit)
- [ ] Test health check endpoint
- [ ] Test database connection
- [ ] Initialize database tables
- [ ] Test with ESP32
---
## 🎯 Next Steps
### 1. Tunggu Deploy Selesai
Railway sedang deploy dengan fix terbaru.
### 2. Test Endpoints
```powershell
cd C:\Users\abilh\aplikasi_novil\backend
.\test-railway.ps1
```
### 3. Cek Logs
Jika masih error, cek Railway logs untuk error message.
### 4. Initialize Database
```powershell
Invoke-WebRequest -Uri "https://web-production-47eb.up.railway.app/api/database/init" -Method POST -UseBasicParsing
```
---
## 🆘 Jika Masih Gagal
### Option 1: Simplify Server
Buat versi minimal tanpa MQTT:
```javascript
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.json({ status: 'OK' });
});
app.listen(3000, () => {
console.log('Server running');
});
```
Test apakah ini bisa deploy. Jika bisa, tambahkan fitur satu per satu.
### Option 2: Check Railway Status
Cek https://status.railway.app/ untuk service outage.
### Option 3: Redeploy
Di Railway Dashboard:
- Click "Deployments"
- Click "Redeploy" pada deployment terakhir
---
## 📚 Resources
- **Railway Docs:** https://docs.railway.app/
- **Railway Logs:** Railway Dashboard → Deployments → View Logs
- **GitHub Repo:** https://github.com/novil04/backend_aplikasi_novil
---
## ✅ Current Status
**Latest Commit:** 794201c - Add missing websocket-stream dependency
**Deploy Status:** ⏳ Deploying...
**Expected Time:** 2-3 minutes
Tunggu deploy selesai, lalu test! 🚀

View File

@ -142,7 +142,7 @@ mqttClient.on('message', async (topic, message) => {
// =====================================================
// TOPIC: novil/pengering/data
// Format: {"suhu":28.5,"berat":450,"target":315}
// Format: {"suhu":28.5,"berat":450,"target":315,"relay1":true,"relay2":true,"relay3":true,"relay4":false}
// =====================================================
if (topic === MQTT_TOPICS.data) {
try {
@ -152,15 +152,15 @@ mqttClient.on('message', async (topic, message) => {
const now = new Date();
const wibTime = new Date(now.getTime() + (7 * 60 * 60 * 1000));
// Update latestData dengan data dari ESP32
// Update latestData dengan data dari ESP32 (termasuk status relay)
latestData = {
suhu: data.suhu || 0,
berat: data.berat || 0,
target: data.target || 0,
relay1: latestData.relay1 || false, // Keep current relay status
relay2: latestData.relay2 || false,
relay3: latestData.relay3 || false,
relay4: latestData.relay4 || false,
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
timestamp: wibTime.toISOString()
};
@ -299,21 +299,21 @@ aedes.on('publish', async (packet, client) => {
// =====================================================
// TOPIC: novil/pengering/data
// Format: {"suhu":28.5,"berat":450,"target":315}
// Format: {"suhu":28.5,"berat":450,"target":315,"relay1":true,"relay2":true,"relay3":true,"relay4":false}
// =====================================================
if (topic === 'novil/pengering/data') {
try {
const data = JSON.parse(message);
// Update latestData dengan data dari ESP32
// Update latestData dengan data dari ESP32 (termasuk status relay)
latestData = {
suhu: data.suhu || 0,
berat: data.berat || 0,
target: data.target || 0,
relay1: latestData.relay1 || false, // Keep current relay status
relay2: latestData.relay2 || false,
relay3: latestData.relay3 || false,
relay4: latestData.relay4 || false,
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
timestamp: new Date().toISOString()
};

View File

@ -1,206 +0,0 @@
# =====================================================
# Script Test Control Topic di Railway
# =====================================================
param(
[Parameter(Mandatory=$false)]
[string]$RailwayUrl = ""
)
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " TEST CONTROL TOPIC" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
# Minta URL jika tidak diberikan
if ([string]::IsNullOrWhiteSpace($RailwayUrl)) {
$RailwayUrl = Read-Host "Masukkan URL Railway (contoh: https://your-app.up.railway.app)"
}
# Remove trailing slash
$RailwayUrl = $RailwayUrl.TrimEnd('/')
Write-Host "🔗 Railway URL: $RailwayUrl" -ForegroundColor Cyan
Write-Host ""
# =====================================================
# 1. HEALTH CHECK
# =====================================================
Write-Host "1⃣ Testing Health Check..." -ForegroundColor Yellow
try {
$response = Invoke-RestMethod -Uri "$RailwayUrl/" -Method Get
Write-Host " ✅ Status: $($response.status)" -ForegroundColor Green
Write-Host " 📦 Version: $($response.version)" -ForegroundColor Cyan
Write-Host " ⏱️ Uptime: $([math]::Round($response.uptime, 2))s" -ForegroundColor Cyan
} catch {
Write-Host " ❌ Failed: $_" -ForegroundColor Red
exit 1
}
Write-Host ""
# =====================================================
# 2. STATS CHECK
# =====================================================
Write-Host "2⃣ Testing Stats API..." -ForegroundColor Yellow
try {
$response = Invoke-RestMethod -Uri "$RailwayUrl/api/stats" -Method Get
Write-Host " ✅ Stats retrieved successfully" -ForegroundColor Green
Write-Host " 🔌 MQTT Connected: $($response.stats.mqttConnected)" -ForegroundColor Cyan
Write-Host " 📊 Sensor Data: $($response.stats.sensorDataCount)" -ForegroundColor Cyan
Write-Host " 📝 Status History: $($response.stats.statusHistoryCount)" -ForegroundColor Cyan
Write-Host " 🎛️ Control Commands: $($response.stats.controlCommandsCount)" -ForegroundColor Cyan
if ($response.stats.controlCommandsCount -eq $null) {
Write-Host " ⚠️ WARNING: controlCommandsCount is null!" -ForegroundColor Yellow
Write-Host " Kemungkinan table control_commands belum ada atau kosong" -ForegroundColor Yellow
}
} catch {
Write-Host " ❌ Failed: $_" -ForegroundColor Red
}
Write-Host ""
# =====================================================
# 3. TEST CONTROL COMMANDS
# =====================================================
Write-Host "3⃣ Testing Control Commands..." -ForegroundColor Yellow
$commands = @("HEATER_ON", "FAN_ON", "EXHAUST_ON", "HEATER_OFF", "FAN_OFF", "EXHAUST_OFF")
foreach ($cmd in $commands) {
Write-Host " 📤 Sending: $cmd" -ForegroundColor Cyan
try {
$body = @{
command = $cmd
} | ConvertTo-Json
$response = Invoke-RestMethod -Uri "$RailwayUrl/api/control" -Method Post -Body $body -ContentType "application/json"
if ($response.success) {
Write-Host " ✅ Success" -ForegroundColor Green
} else {
Write-Host " ❌ Failed: $($response.message)" -ForegroundColor Red
}
} catch {
Write-Host " ❌ Error: $_" -ForegroundColor Red
}
Start-Sleep -Milliseconds 500
}
Write-Host ""
# =====================================================
# 4. CHECK STATS AGAIN
# =====================================================
Write-Host "4⃣ Checking Stats After Commands..." -ForegroundColor Yellow
try {
$response = Invoke-RestMethod -Uri "$RailwayUrl/api/stats" -Method Get
Write-Host " ✅ Stats retrieved" -ForegroundColor Green
Write-Host " 🎛️ Control Commands Count: $($response.stats.controlCommandsCount)" -ForegroundColor Cyan
if ($response.stats.controlCommandsCount -gt 0) {
Write-Host " ✅ Control commands berhasil tersimpan!" -ForegroundColor Green
} else {
Write-Host " ⚠️ Control commands masih 0" -ForegroundColor Yellow
}
} catch {
Write-Host " ❌ Failed: $_" -ForegroundColor Red
}
Write-Host ""
# =====================================================
# 5. TEST INVALID COMMAND
# =====================================================
Write-Host "5⃣ Testing Invalid Command..." -ForegroundColor Yellow
try {
$body = @{
command = "INVALID_COMMAND"
} | ConvertTo-Json
$response = Invoke-RestMethod -Uri "$RailwayUrl/api/control" -Method Post -Body $body -ContentType "application/json"
Write-Host " ⚠️ Invalid command accepted (should be rejected)" -ForegroundColor Yellow
} catch {
$errorResponse = $_.ErrorDetails.Message | ConvertFrom-Json
if ($errorResponse.message -like "*Invalid command*") {
Write-Host " ✅ Invalid command correctly rejected" -ForegroundColor Green
} else {
Write-Host " ❌ Unexpected error: $($errorResponse.message)" -ForegroundColor Red
}
}
Write-Host ""
# =====================================================
# 6. GET DATA HISTORY
# =====================================================
Write-Host "6⃣ Getting Data History..." -ForegroundColor Yellow
try {
$response = Invoke-RestMethod -Uri "$RailwayUrl/api/data/history?limit=5" -Method Get
Write-Host " ✅ Retrieved $($response.count) records" -ForegroundColor Green
if ($response.count -gt 0) {
$latest = $response.data[0]
Write-Host " 📊 Latest Data:" -ForegroundColor Cyan
Write-Host " Suhu: $($latest.suhu)°C" -ForegroundColor White
Write-Host " Berat: $($latest.berat)g" -ForegroundColor White
Write-Host " Relay1 (Heater): $($latest.relay1)" -ForegroundColor White
Write-Host " Relay2 (Fan): $($latest.relay2)" -ForegroundColor White
Write-Host " Relay3 (Exhaust): $($latest.relay3)" -ForegroundColor White
Write-Host " Status: $($latest.status)" -ForegroundColor White
Write-Host " Timestamp: $($latest.timestamp)" -ForegroundColor White
}
} catch {
Write-Host " ❌ Failed: $_" -ForegroundColor Red
}
Write-Host ""
# =====================================================
# 7. GET STATUS HISTORY
# =====================================================
Write-Host "7⃣ Getting Status History..." -ForegroundColor Yellow
try {
$response = Invoke-RestMethod -Uri "$RailwayUrl/api/status/history?limit=10" -Method Get
Write-Host " ✅ Retrieved $($response.count) records" -ForegroundColor Green
if ($response.count -gt 0) {
Write-Host " 📝 Recent Status Messages:" -ForegroundColor Cyan
foreach ($status in $response.data | Select-Object -First 5) {
Write-Host " - $($status.message)" -ForegroundColor White
}
}
} catch {
Write-Host " ❌ Failed: $_" -ForegroundColor Red
}
Write-Host ""
# =====================================================
# SUMMARY
# =====================================================
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " 📋 TEST SUMMARY" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "✅ Checklist:" -ForegroundColor Yellow
Write-Host " [ ] Health check berhasil" -ForegroundColor White
Write-Host " [ ] Stats API menampilkan controlCommandsCount" -ForegroundColor White
Write-Host " [ ] Control commands berhasil dikirim" -ForegroundColor White
Write-Host " [ ] Control commands tersimpan di database" -ForegroundColor White
Write-Host " [ ] Invalid command ditolak" -ForegroundColor White
Write-Host " [ ] Data history dapat diambil" -ForegroundColor White
Write-Host " [ ] Status history dapat diambil" -ForegroundColor White
Write-Host ""
Write-Host "📚 Next Steps:" -ForegroundColor Yellow
Write-Host " 1. Cek Railway logs untuk melihat MQTT messages" -ForegroundColor White
Write-Host " 2. Upload ESP32 code dan test dengan hardware" -ForegroundColor White
Write-Host " 3. Monitor database Railway untuk memastikan data masuk" -ForegroundColor White
Write-Host ""
Write-Host "🔗 Useful URLs:" -ForegroundColor Yellow
Write-Host " Health: $RailwayUrl/" -ForegroundColor Cyan
Write-Host " Stats: $RailwayUrl/api/stats" -ForegroundColor Cyan
Write-Host " Control: $RailwayUrl/api/control" -ForegroundColor Cyan
Write-Host " History: $RailwayUrl/api/data/history" -ForegroundColor Cyan
Write-Host ""

View File

@ -1,106 +0,0 @@
# Test Railway API Endpoints
# Run this script after Railway deployment completes
$baseUrl = "https://web-production-47eb.up.railway.app"
Write-Host "🧪 Testing Railway Backend..." -ForegroundColor Cyan
Write-Host ""
# Test 1: Health Check
Write-Host "1⃣ Testing Health Check..." -ForegroundColor Yellow
try {
$response = Invoke-WebRequest -Uri "$baseUrl/" -Method GET -UseBasicParsing
$json = $response.Content | ConvertFrom-Json
Write-Host "✅ Health Check OK" -ForegroundColor Green
Write-Host " Status: $($json.status)" -ForegroundColor Gray
Write-Host " Message: $($json.message)" -ForegroundColor Gray
Write-Host " Version: $($json.version)" -ForegroundColor Gray
} catch {
Write-Host "❌ Health Check FAILED" -ForegroundColor Red
Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Red
}
Write-Host ""
# Test 2: Database Connection
Write-Host "2⃣ Testing Database Connection..." -ForegroundColor Yellow
try {
$response = Invoke-WebRequest -Uri "$baseUrl/api/database/test" -Method GET -UseBasicParsing
$json = $response.Content | ConvertFrom-Json
if ($json.success) {
Write-Host "✅ Database Connection OK" -ForegroundColor Green
Write-Host " Message: $($json.message)" -ForegroundColor Gray
} else {
Write-Host "❌ Database Connection FAILED" -ForegroundColor Red
Write-Host " Message: $($json.message)" -ForegroundColor Red
}
} catch {
Write-Host "❌ Database Connection FAILED" -ForegroundColor Red
Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Red
}
Write-Host ""
# Test 3: Initialize Database
Write-Host "3⃣ Initializing Database Tables..." -ForegroundColor Yellow
try {
$response = Invoke-WebRequest -Uri "$baseUrl/api/database/init" -Method POST -UseBasicParsing
$json = $response.Content | ConvertFrom-Json
if ($json.success) {
Write-Host "✅ Database Initialized" -ForegroundColor Green
Write-Host " Message: $($json.message)" -ForegroundColor Gray
} else {
Write-Host "⚠️ Database Init Warning" -ForegroundColor Yellow
Write-Host " Message: $($json.message)" -ForegroundColor Yellow
}
} catch {
Write-Host "❌ Database Init FAILED" -ForegroundColor Red
Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Red
}
Write-Host ""
# Test 4: Get Latest Data
Write-Host "4⃣ Getting Latest Data..." -ForegroundColor Yellow
try {
$response = Invoke-WebRequest -Uri "$baseUrl/api/data/latest" -Method GET -UseBasicParsing
$json = $response.Content | ConvertFrom-Json
if ($json.success) {
Write-Host "✅ Latest Data Retrieved" -ForegroundColor Green
Write-Host " Suhu: $($json.data.suhu)°C" -ForegroundColor Gray
Write-Host " Berat: $($json.data.berat)g" -ForegroundColor Gray
Write-Host " Target: $($json.data.target)g" -ForegroundColor Gray
Write-Host " Status: $($json.data.status)" -ForegroundColor Gray
} else {
Write-Host "❌ Get Latest Data FAILED" -ForegroundColor Red
}
} catch {
Write-Host "❌ Get Latest Data FAILED" -ForegroundColor Red
Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Red
}
Write-Host ""
# Test 5: Get Statistics
Write-Host "5⃣ Getting Statistics..." -ForegroundColor Yellow
try {
$response = Invoke-WebRequest -Uri "$baseUrl/api/stats" -Method GET -UseBasicParsing
$json = $response.Content | ConvertFrom-Json
if ($json.success) {
Write-Host "✅ Statistics Retrieved" -ForegroundColor Green
Write-Host " Connected Clients: $($json.stats.connectedClients)" -ForegroundColor Gray
Write-Host " Sensor Data Count: $($json.stats.sensorDataCount)" -ForegroundColor Gray
Write-Host " Status History Count: $($json.stats.statusHistoryCount)" -ForegroundColor Gray
Write-Host " Uptime: $([math]::Round($json.stats.uptime, 2))s" -ForegroundColor Gray
} else {
Write-Host "❌ Get Statistics FAILED" -ForegroundColor Red
}
} catch {
Write-Host "❌ Get Statistics FAILED" -ForegroundColor Red
Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Red
}
Write-Host ""
Write-Host "🎉 Testing Complete!" -ForegroundColor Cyan
Write-Host ""
Write-Host "📝 Next Steps:" -ForegroundColor Yellow
Write-Host " 1. Check Railway logs for any errors" -ForegroundColor Gray
Write-Host " 2. Test ESP32 connection to HiveMQ" -ForegroundColor Gray
Write-Host " 3. Test Flutter app connection" -ForegroundColor Gray
Write-Host ""

View File

@ -1,67 +0,0 @@
# Wait for Railway Deployment and Test
# This script will wait for Railway to finish deploying and then test the endpoints
$baseUrl = "https://web-production-47eb.up.railway.app"
$maxAttempts = 20
$waitSeconds = 10
Write-Host "⏳ Waiting for Railway deployment to complete..." -ForegroundColor Cyan
Write-Host " This may take 2-3 minutes" -ForegroundColor Gray
Write-Host ""
for ($i = 1; $i -le $maxAttempts; $i++) {
Write-Host "Attempt $i/$maxAttempts..." -ForegroundColor Yellow
try {
$response = Invoke-WebRequest -Uri "$baseUrl/" -Method GET -UseBasicParsing -TimeoutSec 5
if ($response.StatusCode -eq 200) {
$json = $response.Content | ConvertFrom-Json
Write-Host ""
Write-Host "✅ Deployment Successful!" -ForegroundColor Green
Write-Host ""
Write-Host "📊 Server Info:" -ForegroundColor Cyan
Write-Host " Status: $($json.status)" -ForegroundColor Gray
Write-Host " Message: $($json.message)" -ForegroundColor Gray
Write-Host " Version: $($json.version)" -ForegroundColor Gray
Write-Host " Uptime: $([math]::Round($json.uptime, 2))s" -ForegroundColor Gray
Write-Host ""
# Run full test
Write-Host "🧪 Running full test suite..." -ForegroundColor Cyan
Write-Host ""
& "$PSScriptRoot\test-railway.ps1"
exit 0
}
} catch {
$errorMessage = $_.Exception.Message
if ($errorMessage -like "*502*") {
Write-Host " ⏳ Server still starting (502)..." -ForegroundColor Yellow
} elseif ($errorMessage -like "*503*") {
Write-Host " ⏳ Service unavailable (503)..." -ForegroundColor Yellow
} elseif ($errorMessage -like "*timeout*") {
Write-Host " ⏳ Connection timeout..." -ForegroundColor Yellow
} else {
Write-Host " ❌ Error: $errorMessage" -ForegroundColor Red
}
}
if ($i -lt $maxAttempts) {
Write-Host " Waiting $waitSeconds seconds before retry..." -ForegroundColor Gray
Start-Sleep -Seconds $waitSeconds
}
}
Write-Host ""
Write-Host "❌ Deployment did not complete within expected time" -ForegroundColor Red
Write-Host ""
Write-Host "📝 Next Steps:" -ForegroundColor Yellow
Write-Host " 1. Check Railway Dashboard for deployment status" -ForegroundColor Gray
Write-Host " 2. View Railway logs for error messages" -ForegroundColor Gray
Write-Host " 3. Try manual test: Invoke-WebRequest -Uri '$baseUrl/'" -ForegroundColor Gray
Write-Host ""
exit 1

View File

@ -0,0 +1,797 @@
#include <WiFi.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <LiquidCrystal_PCF8574.h>
#include <DHT.h>
#include "HX711.h"
// =====================================================
// WIFI
// =====================================================
const char* ssid = "Bemby 1";
const char* password = "BISMILLAH88";
// =====================================================
// MQTT
// =====================================================
const char* mqtt_server = "broker.hivemq.com";
const int mqtt_port = 1883;
WiFiClient espClient;
PubSubClient client(espClient);
// =====================================================
// MQTT TOPIC (Sesuai dengan Backend Railway)
// =====================================================
const char* topicData = "novil/pengering/data";
const char* topicStatus = "novil/pengering/status";
const char* topicControl = "novil/pengering/control";
const char* topicButton = "novil/pengering/button";
// =====================================================
// LCD
// =====================================================
LiquidCrystal_PCF8574 lcd(0x27);
// =====================================================
// BUTTON
// =====================================================
#define BUTTON_PIN 18
bool lastButtonState = HIGH;
// =====================================================
// DHT22
// =====================================================
#define DHTPIN 4
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);
// =====================================================
// HX711
// =====================================================
#define DT1 25
#define SCK1 26
#define DT2 32
#define SCK2 33
HX711 scale1;
HX711 scale2;
// =====================================================
// RELAY
// =====================================================
#define RELAY_HEATER1 13 // RELAY1 - Heater 1
#define RELAY_HEATER2 12 // RELAY2 - Heater 2
#define RELAY_FAN 14 // RELAY3 - Fan
#define RELAY_EXHAUST 27 // RELAY4 - Exhaust
// =====================================================
// KALIBRASI
// =====================================================
float calibration_factor1 = 208.0;
float calibration_factor2 = 208.0;
// =====================================================
// VARIABEL
// =====================================================
float berat_awal = 0;
float berat_trigger = 0;
bool modePengeringan = false;
bool pengeringanSelesai = false;
bool beratTersimpan = false;
unsigned long waktuMulaiBerat = 0;
// =====================================================
// FUNCTION
// =====================================================
void tampilReady();
void tampilSelesai();
void kirimStatusRelay(float suhu, float berat);
void kirimDataMQTT(float suhu, float berat);
// =====================================================
// WIFI CONNECT
// =====================================================
void setup_wifi() {
Serial.println();
Serial.println("Starting WiFi connection...");
// Tampilkan status connecting di LCD
lcd.clear();
delay(50);
lcd.setCursor(0, 0);
lcd.print("Connecting WiFi");
lcd.setCursor(0, 1);
lcd.print("Please wait...");
Serial.print("Connecting to: ");
Serial.println(ssid);
WiFi.begin(ssid, password);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println();
Serial.println("WiFi Connected");
Serial.print("IP Address : ");
Serial.println(WiFi.localIP());
// Tampilkan WiFi connected di LCD
lcd.clear();
delay(50);
lcd.setCursor(0, 0);
lcd.print("WiFi Connected!");
lcd.setCursor(0, 1);
lcd.print(WiFi.localIP());
delay(2000);
} else {
Serial.println();
Serial.println("WiFi Connection Failed!");
// Tampilkan error di LCD
lcd.clear();
delay(50);
lcd.setCursor(0, 0);
lcd.print("WiFi Failed!");
lcd.setCursor(0, 1);
lcd.print("Check settings");
delay(3000);
}
}
// =====================================================
// MQTT CALLBACK
// =====================================================
void callback(char* topic, byte* payload, unsigned int length) {
String message = "";
for (int i = 0; i < length; i++) {
message += (char)payload[i];
}
Serial.print("MQTT Message [");
Serial.print(topic);
Serial.print("] : ");
Serial.println(message);
if (String(topic) == topicControl) {
// =================================================
// HEATER 1
// =================================================
if (message == "HEATER_ON") {
digitalWrite(RELAY_HEATER1, LOW);
digitalWrite(RELAY_HEATER2, LOW);
Serial.println("HEATER 1 & 2 ON");
}
else if (message == "HEATER_OFF") {
digitalWrite(RELAY_HEATER1, HIGH);
digitalWrite(RELAY_HEATER2, HIGH);
Serial.println("HEATER 1 & 2 OFF");
}
// =================================================
// FAN
// =================================================
else if (message == "FAN_ON") {
digitalWrite(RELAY_FAN, LOW);
Serial.println("FAN ON");
}
else if (message == "FAN_OFF") {
digitalWrite(RELAY_FAN, HIGH);
Serial.println("FAN OFF");
}
// =================================================
// LAMP (tidak digunakan, untuk kompatibilitas)
// =================================================
else if (message == "LAMP_ON") {
Serial.println("LAMP ON (tidak tersedia)");
}
else if (message == "LAMP_OFF") {
Serial.println("LAMP OFF (tidak tersedia)");
}
// =================================================
// EXHAUST
// =================================================
else if (message == "EXHAUST_ON") {
digitalWrite(RELAY_EXHAUST, LOW);
Serial.println("EXHAUST ON");
}
else if (message == "EXHAUST_OFF") {
digitalWrite(RELAY_EXHAUST, HIGH);
Serial.println("EXHAUST OFF");
}
// =================================================
// START MQTT (dari Flutter via Backend)
// =================================================
else if (message == "START") {
if (modePengeringan || pengeringanSelesai) {
Serial.println("START DIABAIKAN - Sistem sedang berjalan atau selesai");
client.publish(topicStatus, "TIDAK BISA START SEKARANG");
return;
}
Serial.println("START MQTT (dari aplikasi)");
// Tare HX711 saat mulai pengeringan dengan timeout
Serial.println("Taring scales...");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Calibrating...");
lcd.setCursor(0, 1);
lcd.print("Please wait");
// Cek apakah scale ready sebelum tare
bool scale1Ready = scale1.is_ready();
bool scale2Ready = scale2.is_ready();
Serial.print("Scale 1 ready: ");
Serial.println(scale1Ready ? "YES" : "NO");
Serial.print("Scale 2 ready: ");
Serial.println(scale2Ready ? "YES" : "NO");
if (scale1Ready) {
scale1.tare();
delay(100);
Serial.println("Scale 1 tared");
} else {
Serial.println("Scale 1 NOT READY - skipping tare");
}
if (scale2Ready) {
scale2.tare();
delay(100);
Serial.println("Scale 2 tared");
} else {
Serial.println("Scale 2 NOT READY - skipping tare");
}
Serial.println("Scales tare complete");
modePengeringan = true;
pengeringanSelesai = false;
beratTersimpan = false;
berat_awal = 0;
berat_trigger = 0;
waktuMulaiBerat = 0;
lcd.clear();
client.publish(topicStatus, "PENGERINGAN DIMULAI");
}
// =================================================
// RESET MQTT (dari Flutter via Backend)
// =================================================
else if (message == "RESET") {
Serial.println("RESET MQTT (dari aplikasi)");
modePengeringan = false;
pengeringanSelesai = false;
beratTersimpan = false;
berat_awal = 0;
berat_trigger = 0;
waktuMulaiBerat = 0;
// Matikan semua relay
digitalWrite(RELAY_HEATER1, HIGH);
digitalWrite(RELAY_HEATER2, HIGH);
digitalWrite(RELAY_FAN, HIGH);
digitalWrite(RELAY_EXHAUST, HIGH);
lcd.clear();
tampilReady();
client.publish(topicStatus, "PENGERINGAN SIAP");
}
}
}
// =====================================================
// MQTT CONNECT
// =====================================================
void reconnect() {
int attempts = 0;
while (!client.connected() && attempts < 3) {
Serial.println("Connecting MQTT...");
// Tampilkan status di LCD saat connecting MQTT pertama kali
if (attempts == 0 && modePengeringan == false && pengeringanSelesai == false) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Connecting MQTT");
lcd.setCursor(0, 1);
lcd.print("Please wait...");
}
// Generate unique client ID
String clientId = "ESP32_PENGERING_";
clientId += String(random(0xffff), HEX);
if (client.connect(clientId.c_str())) {
Serial.println("MQTT Connected");
Serial.print("Client ID: ");
Serial.println(clientId);
// Subscribe ke topic control
client.subscribe(topicControl);
Serial.print("Subscribed to: ");
Serial.println(topicControl);
// Publish status connected
client.publish(topicStatus, "ESP32 CONNECTED");
// Tampilkan MQTT connected di LCD
if (modePengeringan == false && pengeringanSelesai == false) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("MQTT Connected!");
lcd.setCursor(0, 1);
lcd.print("System Ready");
delay(2000);
}
}
else {
Serial.print("MQTT Failed : ");
Serial.println(client.state());
attempts++;
delay(2000);
}
}
}
// =====================================================
// SETUP
// =====================================================
void setup() {
Serial.begin(115200);
delay(1000); // Beri waktu serial monitor untuk ready
Serial.println();
Serial.println("=================================");
Serial.println("ESP32 Pengering Ikan - Railway");
Serial.println("=================================");
// Setup I2C untuk LCD
Serial.println("Initializing I2C...");
Wire.begin(21, 22);
delay(100);
// Setup LCD dengan error handling
Serial.println("Initializing LCD...");
lcd.begin(16, 2);
delay(100);
lcd.setBacklight(255);
delay(100);
lcd.clear();
delay(100);
// Test LCD - tampilkan pesan booting
lcd.setCursor(0, 0);
lcd.print("ESP32 Pengering");
lcd.setCursor(0, 1);
lcd.print("Initializing...");
delay(2000);
Serial.println("LCD initialized successfully");
// Setup WiFi
setup_wifi();
// Setup MQTT
client.setServer(mqtt_server, mqtt_port);
client.setCallback(callback);
client.setKeepAlive(60);
// Setup Button
pinMode(BUTTON_PIN, INPUT_PULLUP);
// Setup DHT22
Serial.println("Initializing DHT22...");
dht.begin();
delay(100);
// Setup HX711 - SKIP TARE untuk cepat boot
Serial.println("Initializing HX711...");
scale1.begin(DT1, SCK1);
scale2.begin(DT2, SCK2);
delay(100);
scale1.set_scale(calibration_factor1);
scale2.set_scale(calibration_factor2);
delay(100);
// SKIP tare - akan dilakukan saat mulai pengeringan
// scale1.tare();
// scale2.tare();
Serial.println("HX711 initialized (tare skipped)");
// Setup Relay (Active LOW)
pinMode(RELAY_HEATER1, OUTPUT);
pinMode(RELAY_HEATER2, OUTPUT);
pinMode(RELAY_FAN, OUTPUT);
pinMode(RELAY_EXHAUST, OUTPUT);
// Matikan semua relay
digitalWrite(RELAY_HEATER1, HIGH);
digitalWrite(RELAY_HEATER2, HIGH);
digitalWrite(RELAY_FAN, HIGH);
digitalWrite(RELAY_EXHAUST, HIGH);
// Tampilkan ready
lcd.clear();
delay(100);
tampilReady();
delay(500);
Serial.println("Setup Complete!");
Serial.println("=================================");
}
// =====================================================
// LOOP
// =====================================================
void loop() {
// =====================================================
// MQTT Connection Check
// =====================================================
if (!client.connected()) {
reconnect();
}
client.loop();
// =====================================================
// BUTTON
// =====================================================
bool buttonState = digitalRead(BUTTON_PIN);
if (lastButtonState == HIGH && buttonState == LOW) {
Serial.println("BUTTON PRESSED");
client.publish(topicButton, "BUTTON PRESSED");
// =================================================
// STATE: READY TO DRY
// Tekan button → mulai pengeringan
// =================================================
if (!modePengeringan && !pengeringanSelesai) {
Serial.println("START PENGERINGAN");
// Tare HX711 saat mulai pengeringan dengan timeout
Serial.println("Taring scales...");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Calibrating...");
lcd.setCursor(0, 1);
lcd.print("Please wait");
// Cek apakah scale ready sebelum tare
bool scale1Ready = scale1.is_ready();
bool scale2Ready = scale2.is_ready();
Serial.print("Scale 1 ready: ");
Serial.println(scale1Ready ? "YES" : "NO");
Serial.print("Scale 2 ready: ");
Serial.println(scale2Ready ? "YES" : "NO");
if (scale1Ready) {
scale1.tare();
delay(100);
Serial.println("Scale 1 tared");
} else {
Serial.println("Scale 1 NOT READY - skipping tare");
}
if (scale2Ready) {
scale2.tare();
delay(100);
Serial.println("Scale 2 tared");
} else {
Serial.println("Scale 2 NOT READY - skipping tare");
}
Serial.println("Scales tare complete");
// Publish ke topicButton dan topicControl
client.publish(topicButton, "START_BUTTON");
client.publish(topicControl, "START");
modePengeringan = true;
pengeringanSelesai = false;
beratTersimpan = false;
berat_awal = 0;
berat_trigger = 0;
waktuMulaiBerat = 0;
lcd.clear();
client.publish(topicStatus, "PENGERINGAN DIMULAI");
}
delay(300);
}
lastButtonState = buttonState;
// =====================================================
// MODE READY — belum ada button press
// =====================================================
if (!modePengeringan) {
tampilReady();
delay(500);
return;
}
// =====================================================
// MODE PENGERINGAN
// =====================================================
// =====================================================
// DHT22
// =====================================================
float suhu = dht.readTemperature();
if (isnan(suhu)) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("DHT ERROR");
Serial.println("DHT ERROR");
client.publish(topicStatus, "DHT ERROR");
delay(1000);
return;
}
// =====================================================
// HX711
// =====================================================
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");
}
if (scale2.wait_ready_timeout(200)) {
if (scale2.is_ready()) {
berat2 = scale2.get_units(5);
}
} else {
Serial.println("Scale 2 timeout");
}
// =====================================================
// ANTI NOISE
// =====================================================
if (abs(berat1) < 0.5) berat1 = 0;
if (abs(berat2) < 0.5) berat2 = 0;
// =====================================================
// TOTAL BERAT (Jumlah dari kedua load cell)
// =====================================================
float berat = berat1 + berat2;
// =====================================================
// DETEKSI IKAN
// =====================================================
if (!beratTersimpan && berat > 50 && waktuMulaiBerat == 0) {
waktuMulaiBerat = millis();
Serial.println("IKAN TERDETEKSI");
client.publish(topicStatus, "SCAN BERAT...");
}
// =====================================================
// SIMPAN BERAT AWAL
// =====================================================
if (!beratTersimpan &&
waktuMulaiBerat > 0 &&
millis() - waktuMulaiBerat >= 5000) {
berat_awal = berat;
float berat_daging = berat_awal * 0.3;
float berat_air_awal = berat_awal * 0.7;
float sisa_air = berat_air_awal * 0.4;
berat_trigger = berat_daging + sisa_air;
beratTersimpan = true;
Serial.println("BERAT AWAL TERSIMPAN");
Serial.print("Berat Awal : ");
Serial.println(berat_awal);
Serial.print("Target : ");
Serial.println(berat_trigger);
String msg = "PENGERINGAN BERJALAN - Awal:";
msg += String(berat_awal, 0);
msg += "g Target:";
msg += String(berat_trigger, 0);
msg += "g";
client.publish(topicStatus, msg.c_str());
}
// =====================================================
// TARGET TERCAPAI → SELESAI
// =====================================================
if (beratTersimpan && berat <= berat_trigger) {
modePengeringan = false;
pengeringanSelesai = true;
// Matikan heater 1, 2 dan fan, nyalakan exhaust untuk cooling
digitalWrite(RELAY_HEATER1, HIGH);
digitalWrite(RELAY_HEATER2, HIGH);
digitalWrite(RELAY_FAN, HIGH);
digitalWrite(RELAY_EXHAUST, LOW); // EXHAUST ON
Serial.println("PENGERINGAN SELESAI");
client.publish(topicStatus, "PENGERINGAN SELESAI");
// Tampilkan selesai selama 5 detik
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("PENGERINGAN");
lcd.setCursor(0, 1);
lcd.print("SELESAI");
delay(5000);
// Exhaust jalan 20 detik untuk buang panas
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Cooling down...");
lcd.setCursor(0, 1);
lcd.print("20 seconds");
// Countdown 20 detik
for (int i = 20; i > 0; i--) {
lcd.setCursor(0, 1);
lcd.print(" ");
lcd.setCursor(0, 1);
lcd.print(String(i) + " seconds left");
delay(1000);
// Tetap kirim MQTT status relay saat cooling
kirimStatusRelay(suhu, berat);
client.loop();
}
// Matikan exhaust dan kembali ke ready
digitalWrite(RELAY_EXHAUST, HIGH); // EXHAUST OFF
// Reset variabel
pengeringanSelesai = false;
beratTersimpan = false;
berat_awal = 0;
berat_trigger = 0;
waktuMulaiBerat = 0;
lcd.clear();
tampilReady();
client.publish(topicStatus, "PENGERINGAN SIAP");
// Kirim status relay setelah reset
kirimStatusRelay(suhu, berat);
Serial.println("Sistem kembali ke READY");
delay(500);
return;
}
// =====================================================
// RELAY CONTROL berdasarkan suhu
// Heater 1 & 2 ON, Fan ON
// Exhaust ON jika suhu >= 60°C
// =====================================================
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)");
} else {
digitalWrite(RELAY_EXHAUST, HIGH); // EXHAUST OFF
}
// =====================================================
// LCD BARIS 1
// =====================================================
lcd.setCursor(0, 0);
lcd.print(" ");
lcd.setCursor(0, 0);
lcd.print("S:");
lcd.print(suhu, 1);
lcd.print("C");
lcd.setCursor(9, 0);
lcd.print("B:");
lcd.print(berat, 0);
lcd.print("g");
// =====================================================
// LCD BARIS 2
// =====================================================
lcd.setCursor(0, 1);
lcd.print(" ");
lcd.setCursor(0, 1);
if (!beratTersimpan) {
lcd.print("Scan Berat...");
}
else {
lcd.print("T:");
lcd.print(berat_trigger, 0);
lcd.print("g");
}
// =====================================================
// MQTT DATA (kirim ke Backend Railway)
// =====================================================
kirimStatusRelay(suhu, berat);
delay(1000);
}
// =====================================================
// MQTT DATA dengan Status Relay (Format JSON untuk Backend Railway)
// =====================================================
void kirimStatusRelay(float suhu, float berat) {
// Baca status relay saat ini
bool heater1_on = (digitalRead(RELAY_HEATER1) == LOW);
bool heater2_on = (digitalRead(RELAY_HEATER2) == LOW);
bool fan_on = (digitalRead(RELAY_FAN) == LOW);
bool exhaust_on = (digitalRead(RELAY_EXHAUST) == LOW);
// Format JSON dengan status relay
String payload = "{";
payload += "\"suhu\":" + String(suhu, 1) + ",";
payload += "\"berat\":" + String(berat, 0) + ",";
payload += "\"target\":" + String(berat_trigger, 0) + ",";
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 += "}";
// Publish ke topic data
bool success = client.publish(topicData, payload.c_str());
if (success) {
Serial.print("MQTT Data Sent: ");
Serial.println(payload);
} else {
Serial.println("MQTT Data Failed to Send");
}
}
// =====================================================
// MQTT DATA (Backward compatibility - deprecated)
// =====================================================
void kirimDataMQTT(float suhu, float berat) {
kirimStatusRelay(suhu, berat);
}
// =====================================================
// READY LCD
// =====================================================
void tampilReady() {
lcd.setCursor(0, 0);
lcd.print(" ");
lcd.setCursor(0, 0);
lcd.print("Ready to Dry");
lcd.setCursor(0, 1);
lcd.print(" ");
lcd.setCursor(0, 1);
lcd.print("Press Button");
}
// =====================================================
// SELESAI LCD
// =====================================================
void tampilSelesai() {
lcd.setCursor(0, 0);
lcd.print(" ");
lcd.setCursor(0, 0);
lcd.print("PENGERINGAN");
lcd.setCursor(0, 1);
lcd.print(" ");
lcd.setCursor(0, 1);
lcd.print("SELESAI");
}

BIN
flutter_01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

155
generate_icon.py Normal file
View File

@ -0,0 +1,155 @@
"""
Script untuk generate icon dan splash screen
Sesuai dengan desain loading screen aplikasi Pengering Ikan
"""
from PIL import Image, ImageDraw, ImageFont
import os
# Buat folder assets jika belum ada
os.makedirs("assets/images", exist_ok=True)
# ========================================
# 1. GENERATE APP ICON (1024x1024)
# ========================================
print("🎨 Generating app icon...")
# Buat canvas
icon_size = 1024
icon = Image.new('RGB', (icon_size, icon_size), color='white')
draw = ImageDraw.Draw(icon)
# Background gradient biru (simplified - solid blue)
# Untuk gradient yang lebih bagus, butuh library tambahan
# Kita pakai solid blue yang matching
blue_color = (25, 118, 210) # #1976D2
draw.rectangle([0, 0, icon_size, icon_size], fill=blue_color)
# Gambar lingkaran putih di tengah
circle_radius = 350
circle_center = (icon_size // 2, icon_size // 2)
draw.ellipse(
[
circle_center[0] - circle_radius,
circle_center[1] - circle_radius,
circle_center[0] + circle_radius,
circle_center[1] + circle_radius
],
fill='white'
)
# Gambar icon ikan (simplified - gunakan emoji atau text)
# Karena kita tidak bisa render Material Icons, kita gunakan emoji ikan
try:
# Coba load font yang support emoji
font_size = 400
# Windows default font
font = ImageFont.truetype("seguiemj.ttf", font_size)
except:
# Fallback ke default font
font = ImageFont.load_default()
# Gambar emoji ikan di tengah
fish_emoji = "🐟"
# Get text bounding box
bbox = draw.textbbox((0, 0), fish_emoji, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
text_x = (icon_size - text_width) // 2
text_y = (icon_size - text_height) // 2 - 50
draw.text((text_x, text_y), fish_emoji, fill=blue_color, font=font)
# Save icon
icon.save("assets/images/app_icon.png")
print("✅ App icon saved: assets/images/app_icon.png")
# ========================================
# 2. GENERATE SPLASH SCREEN (1080x1920)
# ========================================
print("🖼️ Generating splash screen...")
# Buat canvas
splash_width = 1080
splash_height = 1920
splash = Image.new('RGB', (splash_width, splash_height), color='white')
draw = ImageDraw.Draw(splash)
# Background gradient biru (simplified - solid blue)
draw.rectangle([0, 0, splash_width, splash_height], fill=blue_color)
# Gambar lingkaran putih di tengah (lebih kecil untuk splash)
circle_radius_splash = 200
circle_center_splash = (splash_width // 2, splash_height // 2 - 200)
draw.ellipse(
[
circle_center_splash[0] - circle_radius_splash,
circle_center_splash[1] - circle_radius_splash,
circle_center_splash[0] + circle_radius_splash,
circle_center_splash[1] + circle_radius_splash
],
fill='white'
)
# Gambar icon ikan
try:
font_splash = ImageFont.truetype("seguiemj.ttf", 220)
except:
font_splash = ImageFont.load_default()
bbox = draw.textbbox((0, 0), fish_emoji, font=font_splash)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
text_x = (splash_width - text_width) // 2
text_y = circle_center_splash[1] - text_height // 2 - 30
draw.text((text_x, text_y), fish_emoji, fill=blue_color, font=font_splash)
# Tambah text "Pengering Ikan"
try:
title_font = ImageFont.truetype("arialbd.ttf", 80)
except:
title_font = ImageFont.load_default()
title_text = "Pengering Ikan"
bbox = draw.textbbox((0, 0), title_text, font=title_font)
title_width = bbox[2] - bbox[0]
title_x = (splash_width - title_width) // 2
title_y = circle_center_splash[1] + circle_radius_splash + 100
draw.text((title_x, title_y), title_text, fill='white', font=title_font)
# Tambah subtitle
try:
subtitle_font = ImageFont.truetype("arial.ttf", 40)
except:
subtitle_font = ImageFont.load_default()
subtitle_text = "Monitoring & Control System"
bbox = draw.textbbox((0, 0), subtitle_text, font=subtitle_font)
subtitle_width = bbox[2] - bbox[0]
subtitle_x = (splash_width - subtitle_width) // 2
subtitle_y = title_y + 100
# Warna putih dengan opacity (simplified - pakai putih biasa)
draw.text((subtitle_x, subtitle_y), subtitle_text, fill=(255, 255, 255, 180), font=subtitle_font)
# Save splash
splash.save("assets/images/splash.png")
print("✅ Splash screen saved: assets/images/splash.png")
print("")
print("========================================")
print("✅ DONE!")
print("========================================")
print("")
print("📊 Generated files:")
print(" - assets/images/app_icon.png (1024x1024)")
print(" - assets/images/splash.png (1080x1920)")
print("")
print("🚀 Next steps:")
print(" 1. Run: .\\setup-icon-splash.ps1")
print(" 2. Run: .\\build-apk.ps1")
print("")

View File

@ -431,7 +431,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@ -488,7 +488,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 439 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 582 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1012 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "background.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@ -1,23 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -16,13 +16,19 @@
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" image="LaunchBackground" translatesAutoresizingMaskIntoConstraints="NO" id="tWc-Dq-wcI"/>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"></imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="3T2-ad-Qdv"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="RPx-PI-7Xg"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="SdS-ul-q2q"/>
<constraint firstAttribute="trailing" secondItem="tWc-Dq-wcI" secondAttribute="trailing" id="Swv-Gf-Rwn"/>
<constraint firstAttribute="trailing" secondItem="YRO-k0-Ey4" secondAttribute="trailing" id="TQA-XW-tRk"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="duK-uY-Gun"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="kV7-tw-vXt"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="xPn-NY-SIU"/>
</constraints>
</view>
</viewController>
@ -32,6 +38,7 @@
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
<image name="LaunchImage" width="1080" height="1920"/>
<image name="LaunchBackground" width="1" height="1"/>
</resources>
</document>

View File

@ -1,70 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Aplikasi Novil</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>aplikasi_novil</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Aplikasi Novil</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>aplikasi_novil</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIStatusBarHidden</key>
<false/>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'screens/login_screen.dart';
import 'screens/splash_screen.dart';
void main() {
runApp(const MyApp());
@ -15,8 +15,9 @@ class MyApp extends StatelessWidget {
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
home: const LoginScreen(),
home: const SplashScreen(),
);
}
}

View File

@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
class NotificationItem {
final String id;
final String title;
final String message;
final NotificationType type;
final DateTime timestamp;
bool isRead;
NotificationItem({
required this.id,
required this.title,
required this.message,
required this.type,
required this.timestamp,
this.isRead = false,
});
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'message': message,
'type': type.toString(),
'timestamp': timestamp.toIso8601String(),
'isRead': isRead,
};
}
factory NotificationItem.fromJson(Map<String, dynamic> json) {
return NotificationItem(
id: json['id'],
title: json['title'],
message: json['message'],
type: NotificationType.values.firstWhere(
(e) => e.toString() == json['type'],
orElse: () => NotificationType.info,
),
timestamp: DateTime.parse(json['timestamp']),
isRead: json['isRead'] ?? false,
);
}
}
enum NotificationType {
success,
warning,
error,
info,
}
class NotificationHelper {
static Color getColor(NotificationType type) {
switch (type) {
case NotificationType.success:
return Colors.green;
case NotificationType.warning:
return Colors.orange;
case NotificationType.error:
return Colors.red;
case NotificationType.info:
default:
return Colors.blue;
}
}
static IconData getIcon(NotificationType type) {
switch (type) {
case NotificationType.success:
return Icons.check_circle;
case NotificationType.warning:
return Icons.warning;
case NotificationType.error:
return Icons.error;
case NotificationType.info:
default:
return Icons.info;
}
}
static Color getBackgroundColor(NotificationType type) {
switch (type) {
case NotificationType.success:
return Colors.green.shade50;
case NotificationType.warning:
return Colors.orange.shade50;
case NotificationType.error:
return Colors.red.shade50;
case NotificationType.info:
default:
return Colors.blue.shade50;
}
}
}

View File

@ -0,0 +1,96 @@
class SensorData {
final double suhu;
final double berat;
final double target;
final bool relay1; // Heater
final bool relay2; // Fan In
final bool relay3; // Lamp
final bool relay4; // Exhaust
final String status;
final String? timestamp;
SensorData({
required this.suhu,
required this.berat,
required this.target,
required this.relay1,
required this.relay2,
required this.relay3,
required this.relay4,
required this.status,
this.timestamp,
});
// Parse dari JSON MQTT (ESP32 Flutter via MQTT)
// Format: {"suhu":28.5,"berat":450,"target":315,"relay1":true,"relay2":true,"relay3":true,"relay4":false}
factory SensorData.fromJson(Map<String, dynamic> json) {
return SensorData(
suhu: (json['suhu'] ?? 0).toDouble(),
berat: (json['berat'] ?? 0).toDouble(),
target: (json['target'] ?? 0).toDouble(),
relay1: json['relay1'] == true || json['relay1'] == 1,
relay2: json['relay2'] == true || json['relay2'] == 1,
relay3: json['relay3'] == true || json['relay3'] == 1,
relay4: json['relay4'] == true || json['relay4'] == 1,
status: json['status'] ?? 'UNKNOWN',
timestamp: json['timestamp'],
);
}
// Parse dari JSON API (Backend Flutter)
// Format: {"suhu":28.5,"berat":450,"target":315,"relay1":true,"relay2":true,...,"status":"RUNNING","timestamp":"2026-05-28T10:30:00.000Z"}
factory SensorData.fromApiJson(Map<String, dynamic> json) {
return SensorData(
suhu: (json['suhu'] ?? 0).toDouble(),
berat: (json['berat'] ?? 0).toDouble(),
target: (json['target'] ?? 0).toDouble(),
relay1: json['relay1'] == true || json['relay1'] == 1,
relay2: json['relay2'] == true || json['relay2'] == 1,
relay3: json['relay3'] == true || json['relay3'] == 1,
relay4: json['relay4'] == true || json['relay4'] == 1,
status: _mapStatus(json['status'] ?? 'UNKNOWN'),
timestamp: json['timestamp'],
);
}
// Map status dari backend ke format Flutter
static String _mapStatus(String backendStatus) {
switch (backendStatus.toUpperCase()) {
case 'CONNECTED':
return 'CONNECTED';
case 'READY':
return 'READY';
case 'RUNNING':
return 'BERJALAN';
case 'SCANNING':
return 'SCANNING';
case 'COMPLETED':
return 'SELESAI';
case 'ERROR':
return 'ERROR';
default:
return backendStatus;
}
}
// Convert ke JSON
Map<String, dynamic> toJson() {
return {
'suhu': suhu,
'berat': berat,
'target': target,
'relay1': relay1,
'relay2': relay2,
'relay3': relay3,
'relay4': relay4,
'status': status,
'timestamp': timestamp,
};
}
@override
String toString() {
return 'SensorData(suhu: $suhu°C, berat: ${berat}g, target: ${target}g, status: $status)';
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,505 @@
// CONTOH IMPLEMENTASI DASHBOARD DENGAN API SERVICE
// File ini adalah contoh, copy kode yang diperlukan ke dashboard_screen.dart
import 'dart:async';
import 'package:flutter/material.dart';
import '../services/api_service.dart';
import '../services/notification_manager.dart';
import '../models/sensor_data.dart';
class DashboardScreenWithApi extends StatefulWidget {
const DashboardScreenWithApi({super.key});
@override
State<DashboardScreenWithApi> createState() => _DashboardScreenWithApiState();
}
class _DashboardScreenWithApiState extends State<DashboardScreenWithApi> {
final ApiService _apiService = ApiService();
final NotificationManager _notificationManager = NotificationManager();
// Data dari sensor
double suhu = 0.0;
double berat = 0.0;
double target = 0.0;
// Status relay
bool heater = false;
bool fanIn = false;
bool exhaust = false;
// Status sistem
String statusSistem = 'DISCONNECTED';
bool isApiConnected = false;
String lastStatusMessage = 'Menghubungkan...';
// Track previous status untuk notifikasi
String _previousStatus = '';
// Timer untuk auto-update
Timer? _pollingTimer;
@override
void initState() {
super.initState();
_initializeNotifications();
_initializeConnections();
_startPolling();
}
// Initialize notifications
Future<void> _initializeNotifications() async {
await _notificationManager.initialize();
}
// Auto-update data setiap 5 detik
void _startPolling() {
_pollingTimer = Timer.periodic(const Duration(seconds: 5), (timer) {
if (mounted && isApiConnected) {
_loadLatestData();
}
});
}
Future<void> _initializeConnections() async {
// Test API connection
final apiConnected = await _apiService.testConnection();
setState(() {
isApiConnected = apiConnected;
// Set status langsung ke READY jika API connected
if (apiConnected) {
statusSistem = 'READY';
lastStatusMessage = 'Terhubung ke server';
} else {
statusSistem = 'DISCONNECTED';
lastStatusMessage = 'Gagal terhubung ke server';
}
});
if (apiConnected) {
print('✅ API connected');
// Load initial data from API
await _loadLatestData();
}
// MQTT dihapus - hanya pakai API polling
}
Future<void> _loadLatestData() async {
try {
final data = await _apiService.getLatestData();
if (data != null && mounted) {
setState(() {
suhu = data.suhu;
berat = data.berat;
target = data.target;
heater = data.relay1;
fanIn = data.relay2;
exhaust = data.relay4;
// Update status dan trigger notifikasi
String newStatus = data.status;
// Notifikasi berdasarkan perubahan status
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') {
_notificationManager.notifyPengeringanSelesai(berat);
}
_previousStatus = newStatus;
}
statusSistem = newStatus;
});
} else if (mounted) {
// Data null, tampilkan error
_showErrorSnackBar('Gagal memuat data dari server');
}
} catch (e) {
print('❌ Error loading data: $e');
if (mounted) {
_showErrorSnackBar('Error: ${e.toString()}');
}
}
}
// =====================================================
// UI HELPERS
// =====================================================
void _showSuccessSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.green,
duration: const Duration(seconds: 2),
),
);
}
void _showErrorSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
duration: const Duration(seconds: 3),
),
);
}
@override
void dispose() {
_pollingTimer?.cancel();
// Jangan disconnect MQTT karena singleton
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Dashboard Pengering Ikan'),
actions: [
// API status only
Padding(
padding: const EdgeInsets.only(right: 16),
child: Icon(
isApiConnected ? Icons.cloud_done : Icons.cloud_off,
color: isApiConnected ? Colors.green : Colors.red,
size: 24,
),
),
],
),
body: RefreshIndicator(
onRefresh: _loadLatestData,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Status Card
_buildStatusCard(),
const SizedBox(height: 16),
// Metrics
Row(
children: [
Expanded(
child: _buildMetricCard(
icon: Icons.thermostat,
iconColor: Colors.orange,
label: 'Suhu',
value: suhu.toStringAsFixed(1),
unit: '°C',
),
),
const SizedBox(width: 12),
Expanded(
child: _buildMetricCard(
icon: Icons.scale,
iconColor: Colors.blue,
label: 'Berat',
value: berat.toStringAsFixed(0),
unit: 'g',
),
),
],
),
const SizedBox(height: 12),
// Target
_buildMetricCard(
icon: Icons.flag,
iconColor: Colors.green,
label: 'Target Berat',
value: target.toStringAsFixed(0),
unit: 'g',
),
const SizedBox(height: 16),
// Devices
const Text(
'Perangkat',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
GridView.count(
crossAxisCount: 3,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 0.9,
children: [
_buildDeviceCard(
icon: Icons.whatshot,
iconColor: Colors.deepOrange,
label: 'Heater',
status: heater,
),
_buildDeviceCard(
icon: Icons.air,
iconColor: Colors.blue,
label: 'Fan',
status: fanIn,
),
_buildDeviceCard(
icon: Icons.wind_power,
iconColor: Colors.teal,
label: 'Exhaust',
status: exhaust,
),
],
),
],
),
),
),
);
}
Widget _buildStatusCard() {
Color statusColor;
IconData statusIcon;
switch (statusSistem) {
case 'SELESAI':
statusColor = Colors.green;
statusIcon = Icons.check_circle;
break;
case 'BERJALAN':
statusColor = Colors.blue;
statusIcon = Icons.play_circle;
break;
case 'READY':
statusColor = Colors.orange;
statusIcon = Icons.hourglass_empty;
break;
case 'SCANNING':
statusColor = Colors.purple;
statusIcon = Icons.search;
break;
case 'CONNECTED':
statusColor = Colors.green;
statusIcon = Icons.check_circle_outline;
break;
case 'DISCONNECTED':
statusColor = Colors.red;
statusIcon = Icons.error_outline;
break;
case 'CONNECTING':
statusColor = Colors.orange;
statusIcon = Icons.sync;
break;
default:
statusColor = Colors.grey;
statusIcon = Icons.info;
}
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
statusIcon,
color: statusColor,
size: 32,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Status Sistem',
style: TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
const SizedBox(height: 4),
Text(
statusSistem,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: statusColor,
),
),
const SizedBox(height: 4),
Text(
lastStatusMessage,
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
);
}
Widget _buildMetricCard({
required IconData icon,
required Color iconColor,
required String label,
required String value,
required String unit,
}) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: iconColor, size: 20),
const SizedBox(width: 8),
Text(
label,
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
],
),
const SizedBox(height: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
value,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 4),
Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Text(
unit,
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
),
],
),
],
),
);
}
Widget _buildDeviceCard({
required IconData icon,
required Color iconColor,
required String label,
required bool status,
}) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: status ? iconColor.withOpacity(0.1) : Colors.grey.shade100,
shape: BoxShape.circle,
),
child: Icon(
icon,
color: status ? iconColor : Colors.grey,
size: 28,
),
),
const SizedBox(height: 8),
Text(
label,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3),
decoration: BoxDecoration(
color: status ? iconColor.withOpacity(0.15) : Colors.grey.shade100,
borderRadius: BorderRadius.circular(10),
),
child: Text(
status ? 'ON' : 'OFF',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: status ? iconColor : Colors.grey,
),
),
),
],
),
);
}
}

View File

@ -1,184 +0,0 @@
import 'package:flutter/material.dart';
import 'register_screen.dart';
import 'dashboard_screen.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
LoginScreenState createState() => LoginScreenState();
}
class LoginScreenState extends State<LoginScreen> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _obscurePassword = true;
void _login() {
String email = _emailController.text.trim();
String password = _passwordController.text.trim();
if (email.isEmpty || password.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Email dan password tidak boleh kosong!")),
);
return;
}
// Simulasi login berhasil langsung ke Dashboard
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const DashboardScreen()),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.blue.shade50,
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(28),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo / Icon
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.blue.shade200,
blurRadius: 20,
spreadRadius: 4,
)
],
),
child: const Icon(Icons.set_meal, size: 60, color: Colors.white),
),
const SizedBox(height: 24),
const Text(
"Pengering Ikan",
style: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
const Text(
"Masuk ke akun Anda",
style: TextStyle(fontSize: 14, color: Colors.grey),
),
const SizedBox(height: 32),
// Card Form
Card(
elevation: 6,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
// Email
TextField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: "Email",
prefixIcon: const Icon(Icons.email_outlined),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
const SizedBox(height: 16),
// Password
TextField(
controller: _passwordController,
obscureText: _obscurePassword,
decoration: InputDecoration(
labelText: "Password",
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility_off
: Icons.visibility,
),
onPressed: () => setState(
() => _obscurePassword = !_obscurePassword),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
const SizedBox(height: 24),
// Tombol Login
SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
onPressed: _login,
child: const Text(
"Masuk",
style: TextStyle(fontSize: 16, color: Colors.white),
),
),
),
],
),
),
),
const SizedBox(height: 20),
// Link ke Register
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("Belum punya akun? "),
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const RegisterScreen()),
);
},
child: const Text(
"Daftar",
style: TextStyle(
color: Colors.blue,
fontWeight: FontWeight.bold,
),
),
),
],
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,322 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../models/notification_model.dart';
import '../services/notification_manager.dart';
class NotificationScreen extends StatefulWidget {
const NotificationScreen({super.key});
@override
NotificationScreenState createState() => NotificationScreenState();
}
class NotificationScreenState extends State<NotificationScreen> {
final NotificationManager _notificationManager = NotificationManager();
@override
void initState() {
super.initState();
_notificationManager.addListener(_onNotificationsChanged);
}
@override
void dispose() {
_notificationManager.removeListener(_onNotificationsChanged);
super.dispose();
}
void _onNotificationsChanged() {
if (mounted) {
setState(() {});
}
}
@override
Widget build(BuildContext context) {
final notifications = _notificationManager.notifications;
final unreadCount = _notificationManager.unreadCount;
return Scaffold(
backgroundColor: Colors.grey.shade50,
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Notifikasi',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
Text(
'${notifications.length} notifikasi',
style: const TextStyle(
fontSize: 11,
color: Colors.grey,
),
),
],
),
actions: [
if (unreadCount > 0)
TextButton(
onPressed: () => _notificationManager.markAllAsRead(),
child: const Text(
'Tandai Semua',
style: TextStyle(
fontSize: 12,
color: Colors.blue,
),
),
),
PopupMenuButton(
icon: const Icon(Icons.more_vert, color: Colors.black),
itemBuilder: (context) => [
const PopupMenuItem(
value: 'clear',
child: Row(
children: [
Icon(Icons.delete_outline, size: 20, color: Colors.red),
SizedBox(width: 8),
Text('Hapus Semua'),
],
),
),
],
onSelected: (value) {
if (value == 'clear') {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Hapus Semua Notifikasi?'),
content: const Text('Semua notifikasi akan dihapus permanen.'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Batal'),
),
TextButton(
onPressed: () {
_notificationManager.clearAll();
Navigator.pop(context);
},
child: const Text(
'Hapus',
style: TextStyle(color: Colors.red),
),
),
],
),
);
}
},
),
],
),
body: notifications.isEmpty
? _buildEmptyState()
: ListView.builder(
padding: const EdgeInsets.all(12),
itemCount: notifications.length,
itemBuilder: (context, index) {
final notification = notifications[index];
return _buildNotificationCard(notification);
},
),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.notifications_none,
size: 80,
color: Colors.grey.shade300,
),
const SizedBox(height: 16),
Text(
'Tidak Ada Notifikasi',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 8),
Text(
'Semua notifikasi akan muncul di sini',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade500,
),
),
],
),
);
}
Widget _buildNotificationCard(NotificationItem notification) {
final iconColor = NotificationHelper.getColor(notification.type);
final iconData = NotificationHelper.getIcon(notification.type);
final bgColor = NotificationHelper.getBackgroundColor(notification.type);
return Dismissible(
key: Key(notification.id),
direction: DismissDirection.endToStart,
background: Container(
margin: const EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(12),
),
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
child: const Icon(
Icons.delete,
color: Colors.white,
size: 28,
),
),
onDismissed: (direction) {
_notificationManager.deleteNotification(notification.id);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Notifikasi dihapus'),
duration: Duration(seconds: 2),
),
);
},
child: GestureDetector(
onTap: () {
if (!notification.isRead) {
_notificationManager.markAsRead(notification.id);
}
},
child: Container(
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: notification.isRead ? Colors.white : bgColor.withOpacity(0.3),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: notification.isRead ? Colors.grey.shade200 : iconColor.withOpacity(0.3),
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Icon
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
iconData,
color: iconColor,
size: 20,
),
),
const SizedBox(width: 12),
// Content
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
notification.title,
style: TextStyle(
fontSize: 13,
fontWeight: notification.isRead ? FontWeight.w600 : FontWeight.bold,
color: Colors.black87,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
if (!notification.isRead)
Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
),
],
),
const SizedBox(height: 4),
Text(
notification.message,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade700,
height: 1.3,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 6),
Row(
children: [
Icon(
Icons.access_time,
size: 12,
color: Colors.grey.shade500,
),
const SizedBox(width: 4),
Text(
_formatTimestamp(notification.timestamp),
style: TextStyle(
fontSize: 11,
color: Colors.grey.shade500,
),
),
],
),
],
),
),
],
),
),
),
);
}
String _formatTimestamp(DateTime timestamp) {
final now = DateTime.now();
final difference = now.difference(timestamp);
if (difference.inMinutes < 1) {
return 'Baru saja';
} else if (difference.inMinutes < 60) {
return '${difference.inMinutes} menit lalu';
} else if (difference.inHours < 24) {
return '${difference.inHours} jam lalu';
} else if (difference.inDays < 7) {
return '${difference.inDays} hari lalu';
} else {
return DateFormat('dd MMM yyyy, HH:mm').format(timestamp);
}
}
}

View File

@ -1,227 +0,0 @@
import 'package:flutter/material.dart';
import 'login_screen.dart';
class RegisterScreen extends StatefulWidget {
const RegisterScreen({super.key});
@override
RegisterScreenState createState() => RegisterScreenState();
}
class RegisterScreenState extends State<RegisterScreen> {
final _namaController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _konfirmasiController = TextEditingController();
bool _obscurePassword = true;
bool _obscureKonfirmasi = true;
void _register() {
String nama = _namaController.text.trim();
String email = _emailController.text.trim();
String password = _passwordController.text.trim();
String konfirmasi = _konfirmasiController.text.trim();
if (nama.isEmpty || email.isEmpty || password.isEmpty || konfirmasi.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Semua kolom harus diisi!")),
);
return;
}
if (password != konfirmasi) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Password tidak cocok!")),
);
return;
}
// Simulasi register berhasil kembali ke Login
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Registrasi berhasil! Silakan login.")),
);
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const LoginScreen()),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.green.shade50,
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(28),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Icon
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.green.shade200,
blurRadius: 20,
spreadRadius: 4,
)
],
),
child: const Icon(Icons.person_add, size: 60, color: Colors.white),
),
const SizedBox(height: 24),
const Text(
"Buat Akun Baru",
style: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
const Text(
"Daftarkan diri Anda",
style: TextStyle(fontSize: 14, color: Colors.grey),
),
const SizedBox(height: 32),
// Card Form
Card(
elevation: 6,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
// Nama
TextField(
controller: _namaController,
decoration: InputDecoration(
labelText: "Nama Lengkap",
prefixIcon: const Icon(Icons.person_outline),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
const SizedBox(height: 16),
// Email
TextField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: "Email",
prefixIcon: const Icon(Icons.email_outlined),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
const SizedBox(height: 16),
// Password
TextField(
controller: _passwordController,
obscureText: _obscurePassword,
decoration: InputDecoration(
labelText: "Password",
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(_obscurePassword
? Icons.visibility_off
: Icons.visibility),
onPressed: () => setState(
() => _obscurePassword = !_obscurePassword),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
const SizedBox(height: 16),
// Konfirmasi Password
TextField(
controller: _konfirmasiController,
obscureText: _obscureKonfirmasi,
decoration: InputDecoration(
labelText: "Konfirmasi Password",
prefixIcon: const Icon(Icons.lock_reset_outlined),
suffixIcon: IconButton(
icon: Icon(_obscureKonfirmasi
? Icons.visibility_off
: Icons.visibility),
onPressed: () => setState(
() => _obscureKonfirmasi = !_obscureKonfirmasi),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
const SizedBox(height: 24),
// Tombol Daftar
SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
onPressed: _register,
child: const Text(
"Daftar",
style: TextStyle(fontSize: 16, color: Colors.white),
),
),
),
],
),
),
),
const SizedBox(height: 20),
// Link ke Login
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("Sudah punya akun? "),
GestureDetector(
onTap: () => Navigator.pop(context),
child: const Text(
"Masuk",
style: TextStyle(
color: Colors.green,
fontWeight: FontWeight.bold,
),
),
),
],
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,229 @@
import 'package:flutter/material.dart';
import 'dart:async';
import 'dashboard_screen_with_api.dart';
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@override
SplashScreenState createState() => SplashScreenState();
}
class SplashScreenState extends State<SplashScreen>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
// Setup animation controller
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
);
// Fade animation
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _animationController,
curve: const Interval(0.0, 0.6, curve: Curves.easeIn),
),
);
// Scale animation
_scaleAnimation = Tween<double>(begin: 0.5, end: 1.0).animate(
CurvedAnimation(
parent: _animationController,
curve: const Interval(0.0, 0.6, curve: Curves.easeOutBack),
),
);
// Start animation
_animationController.forward();
// Navigate to dashboard after delay
Timer(const Duration(seconds: 3), () {
if (mounted) {
Navigator.pushReplacement(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const DashboardScreenWithApi(),
transitionsBuilder:
(context, animation, secondaryAnimation, child) {
const begin = Offset(1.0, 0.0);
const end = Offset.zero;
const curve = Curves.easeInOut;
var tween = Tween(begin: begin, end: end)
.chain(CurveTween(curve: curve));
var offsetAnimation = animation.drive(tween);
return SlideTransition(
position: offsetAnimation,
child: child,
);
},
transitionDuration: const Duration(milliseconds: 500),
),
);
}
});
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.blue.shade700,
Colors.blue.shade400,
Colors.lightBlue.shade300,
],
),
),
child: Center(
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return FadeTransition(
opacity: _fadeAnimation,
child: ScaleTransition(
scale: _scaleAnimation,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Icon/Logo - Gambar Ikan
Container(
width: 160,
height: 160,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 20,
spreadRadius: 5,
),
],
),
child: CustomPaint(
painter: FishPainter(),
),
),
const SizedBox(height: 40),
// App Name
const Text(
"Pengering Ikan",
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 1.5,
),
),
const SizedBox(height: 10),
// Subtitle
const Text(
"Monitoring System",
style: TextStyle(
fontSize: 16,
color: Colors.white70,
letterSpacing: 0.5,
),
),
const SizedBox(height: 50),
// Loading indicator
const SizedBox(
width: 40,
height: 40,
child: CircularProgressIndicator(
valueColor:
AlwaysStoppedAnimation<Color>(Colors.white),
strokeWidth: 3,
),
),
const SizedBox(height: 20),
const Text(
"Loading...",
style: TextStyle(
fontSize: 14,
color: Colors.white70,
),
),
],
),
),
);
},
),
),
),
);
}
}
// Custom Painter untuk gambar ikan
class FishPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.blue.shade700
..style = PaintingStyle.fill
..isAntiAlias = true;
// Badan ikan (oval) - lebih kecil dan centered
final bodyRect = Rect.fromLTWH(
size.width * 0.3, // Lebih ke kanan
size.height * 0.35, // Lebih ke tengah
size.width * 0.5, // Lebih kecil
size.height * 0.3, // Lebih kecil
);
canvas.drawOval(bodyRect, paint);
// Ekor ikan (segitiga) - adjust posisi
final tailPath = Path();
tailPath.moveTo(size.width * 0.3, size.height * 0.5); // Kiri tengah
tailPath.lineTo(size.width * 0.1, size.height * 0.4); // Atas kiri
tailPath.lineTo(size.width * 0.1, size.height * 0.6); // Bawah kiri
tailPath.close();
canvas.drawPath(tailPath, paint);
// Mata ikan (lingkaran kecil putih) - adjust posisi
final eyePaint = Paint()
..color = Colors.white
..style = PaintingStyle.fill
..isAntiAlias = true;
canvas.drawCircle(
Offset(size.width * 0.65, size.height * 0.47),
size.width * 0.05,
eyePaint,
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

View File

@ -0,0 +1,236 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/sensor_data.dart';
class ApiService {
// Singleton pattern
static final ApiService _instance = ApiService._internal();
factory ApiService() => _instance;
ApiService._internal();
// Base URL Railway
static const String baseUrl = 'https://web-production-47eb.up.railway.app';
// Timeout
static const Duration timeout = Duration(seconds: 10);
// =====================================================
// GET LATEST DATA
// =====================================================
Future<SensorData?> getLatestData() async {
try {
final response = await http
.get(
Uri.parse('$baseUrl/api/data/latest'),
headers: {'Content-Type': 'application/json'},
)
.timeout(timeout);
if (response.statusCode == 200) {
final jsonData = jsonDecode(response.body);
if (jsonData['success'] == true && jsonData['data'] != null) {
return SensorData.fromApiJson(jsonData['data']);
}
}
print('❌ Failed to get latest data: ${response.statusCode}');
return null;
} catch (e) {
print('❌ Error getting latest data: $e');
return null;
}
}
// =====================================================
// GET DATA HISTORY
// =====================================================
Future<List<SensorData>> getDataHistory({int limit = 50}) async {
try {
final response = await http
.get(
Uri.parse('$baseUrl/api/data/history?limit=$limit'),
headers: {'Content-Type': 'application/json'},
)
.timeout(timeout);
if (response.statusCode == 200) {
final jsonData = jsonDecode(response.body);
if (jsonData['success'] == true && jsonData['data'] != null) {
final List<dynamic> dataList = jsonData['data'];
return dataList
.map((item) => SensorData.fromApiJson(item))
.toList();
}
}
print('❌ Failed to get data history: ${response.statusCode}');
return [];
} catch (e) {
print('❌ Error getting data history: $e');
return [];
}
}
// =====================================================
// GET STATUS HISTORY
// =====================================================
Future<List<Map<String, dynamic>>> getStatusHistory({int limit = 50}) async {
try {
final response = await http
.get(
Uri.parse('$baseUrl/api/status/history?limit=$limit'),
headers: {'Content-Type': 'application/json'},
)
.timeout(timeout);
if (response.statusCode == 200) {
final jsonData = jsonDecode(response.body);
if (jsonData['success'] == true && jsonData['data'] != null) {
return List<Map<String, dynamic>>.from(jsonData['data']);
}
}
print('❌ Failed to get status history: ${response.statusCode}');
return [];
} catch (e) {
print('❌ Error getting status history: $e');
return [];
}
}
// =====================================================
// GET STATISTICS
// =====================================================
Future<Map<String, dynamic>?> getStatistics() async {
try {
final response = await http
.get(
Uri.parse('$baseUrl/api/stats'),
headers: {'Content-Type': 'application/json'},
)
.timeout(timeout);
if (response.statusCode == 200) {
final jsonData = jsonDecode(response.body);
if (jsonData['success'] == true && jsonData['stats'] != null) {
return jsonData['stats'];
}
}
print('❌ Failed to get statistics: ${response.statusCode}');
return null;
} catch (e) {
print('❌ Error getting statistics: $e');
return null;
}
}
// =====================================================
// SEND CONTROL COMMAND
// =====================================================
Future<bool> sendCommand(String command) async {
try {
final response = await http
.post(
Uri.parse('$baseUrl/api/control'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'command': command}),
)
.timeout(timeout);
if (response.statusCode == 200) {
final jsonData = jsonDecode(response.body);
if (jsonData['success'] == true) {
print('✅ Command sent: $command');
return true;
}
}
print('❌ Failed to send command: ${response.statusCode}');
return false;
} catch (e) {
print('❌ Error sending command: $e');
return false;
}
}
// =====================================================
// CONTROL COMMANDS
// =====================================================
// System commands
Future<bool> startPengeringan() => sendCommand('START');
Future<bool> resetPengeringan() => sendCommand('RESET');
// Heater commands
Future<bool> heaterOn() => sendCommand('HEATER_ON');
Future<bool> heaterOff() => sendCommand('HEATER_OFF');
// Fan commands
Future<bool> fanOn() => sendCommand('FAN_ON');
Future<bool> fanOff() => sendCommand('FAN_OFF');
// Lamp commands
Future<bool> lampOn() => sendCommand('LAMP_ON');
Future<bool> lampOff() => sendCommand('LAMP_OFF');
// Exhaust commands
Future<bool> exhaustOn() => sendCommand('EXHAUST_ON');
Future<bool> exhaustOff() => sendCommand('EXHAUST_OFF');
// =====================================================
// CLEAR HISTORY
// =====================================================
Future<bool> clearHistory() async {
try {
final response = await http
.delete(
Uri.parse('$baseUrl/api/history/clear'),
headers: {'Content-Type': 'application/json'},
)
.timeout(timeout);
if (response.statusCode == 200) {
final jsonData = jsonDecode(response.body);
if (jsonData['success'] == true) {
print('✅ History cleared');
return true;
}
}
print('❌ Failed to clear history: ${response.statusCode}');
return false;
} catch (e) {
print('❌ Error clearing history: $e');
return false;
}
}
// =====================================================
// TEST CONNECTION
// =====================================================
Future<bool> testConnection() async {
try {
final response = await http
.get(
Uri.parse('$baseUrl/'),
headers: {'Content-Type': 'application/json'},
)
.timeout(timeout);
if (response.statusCode == 200) {
final jsonData = jsonDecode(response.body);
if (jsonData['status'] == 'OK') {
print('✅ API connection successful');
return true;
}
}
print('❌ API connection failed: ${response.statusCode}');
return false;
} catch (e) {
print('❌ Error testing API connection: $e');
return false;
}
}
}

Some files were not shown because too many files have changed in this diff Show More