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)
|
|
@ -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\\
|
||||||
|
|
@ -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!**
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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!**
|
||||||
|
|
@ -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
|
|
@ -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
|
## 🏗️ Arsitektur Sistem
|
||||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
|
||||||
samples, guidance on mobile development, and a full API reference.
|
```
|
||||||
|
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!**
|
||||||
|
|
|
||||||
|
|
@ -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! 🚀🎉
|
|
||||||
|
|
@ -8,11 +8,12 @@ plugins {
|
||||||
android {
|
android {
|
||||||
namespace = "com.example.aplikasi_novil"
|
namespace = "com.example.aplikasi_novil"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
ndkVersion = flutter.ndkVersion
|
ndkVersion = "27.0.12077973"
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
|
|
@ -42,3 +43,7 @@ android {
|
||||||
flutter {
|
flutter {
|
||||||
source = "../.."
|
source = "../.."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<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
|
<application
|
||||||
android:label="aplikasi_novil"
|
android:label="aplikasi_novil"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 69 B |
|
|
@ -1,12 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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">
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:drawable="?android:colorBackground" />
|
<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>
|
</layer-list>
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 69 B |
|
|
@ -1,12 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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">
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:drawable="@android:color/white" />
|
<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>
|
</layer-list>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 963 B |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 3.6 KiB |
|
|
@ -5,6 +5,10 @@
|
||||||
<!-- Show a splash screen on the activity. Automatically removed when
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
the Flutter engine draws its first frame -->
|
the Flutter engine draws its first frame -->
|
||||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
<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>
|
</style>
|
||||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
This theme determines the color of the Android Window while your
|
This theme determines the color of the Android Window while your
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#1976D2</color>
|
||||||
|
</resources>
|
||||||
|
|
@ -5,6 +5,10 @@
|
||||||
<!-- Show a splash screen on the activity. Automatically removed when
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
the Flutter engine draws its first frame -->
|
the Flutter engine draws its first frame -->
|
||||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
<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>
|
</style>
|
||||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
This theme determines the color of the Android Window while your
|
This theme determines the color of the Android Window while your
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 14 KiB |
|
|
@ -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! 🚀
|
|
||||||
|
|
@ -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! 🚀🎊
|
|
||||||
|
|
@ -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`
|
|
||||||
|
|
@ -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! 🚀🎉
|
|
||||||
|
|
@ -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! 🚀
|
|
||||||
|
|
@ -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! 🚀
|
|
||||||
|
|
@ -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! 🚀
|
|
||||||
|
|
@ -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! 🚀
|
|
||||||
|
|
@ -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! 🚀
|
|
||||||
|
|
@ -142,7 +142,7 @@ mqttClient.on('message', async (topic, message) => {
|
||||||
|
|
||||||
// =====================================================
|
// =====================================================
|
||||||
// TOPIC: novil/pengering/data
|
// 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) {
|
if (topic === MQTT_TOPICS.data) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -152,15 +152,15 @@ mqttClient.on('message', async (topic, message) => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const wibTime = new Date(now.getTime() + (7 * 60 * 60 * 1000));
|
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 = {
|
latestData = {
|
||||||
suhu: data.suhu || 0,
|
suhu: data.suhu || 0,
|
||||||
berat: data.berat || 0,
|
berat: data.berat || 0,
|
||||||
target: data.target || 0,
|
target: data.target || 0,
|
||||||
relay1: latestData.relay1 || false, // Keep current relay status
|
relay1: data.relay1 !== undefined ? data.relay1 : latestData.relay1, // Update dari ESP32
|
||||||
relay2: latestData.relay2 || false,
|
relay2: data.relay2 !== undefined ? data.relay2 : latestData.relay2,
|
||||||
relay3: latestData.relay3 || false,
|
relay3: data.relay3 !== undefined ? data.relay3 : latestData.relay3,
|
||||||
relay4: latestData.relay4 || false,
|
relay4: data.relay4 !== undefined ? data.relay4 : latestData.relay4,
|
||||||
status: latestData.status, // Keep current status
|
status: latestData.status, // Keep current status
|
||||||
timestamp: wibTime.toISOString()
|
timestamp: wibTime.toISOString()
|
||||||
};
|
};
|
||||||
|
|
@ -299,21 +299,21 @@ aedes.on('publish', async (packet, client) => {
|
||||||
|
|
||||||
// =====================================================
|
// =====================================================
|
||||||
// TOPIC: novil/pengering/data
|
// 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') {
|
if (topic === 'novil/pengering/data') {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(message);
|
const data = JSON.parse(message);
|
||||||
|
|
||||||
// Update latestData dengan data dari ESP32
|
// Update latestData dengan data dari ESP32 (termasuk status relay)
|
||||||
latestData = {
|
latestData = {
|
||||||
suhu: data.suhu || 0,
|
suhu: data.suhu || 0,
|
||||||
berat: data.berat || 0,
|
berat: data.berat || 0,
|
||||||
target: data.target || 0,
|
target: data.target || 0,
|
||||||
relay1: latestData.relay1 || false, // Keep current relay status
|
relay1: data.relay1 !== undefined ? data.relay1 : latestData.relay1, // Update dari ESP32
|
||||||
relay2: latestData.relay2 || false,
|
relay2: data.relay2 !== undefined ? data.relay2 : latestData.relay2,
|
||||||
relay3: latestData.relay3 || false,
|
relay3: data.relay3 !== undefined ? data.relay3 : latestData.relay3,
|
||||||
relay4: latestData.relay4 || false,
|
relay4: data.relay4 !== undefined ? data.relay4 : latestData.relay4,
|
||||||
status: latestData.status, // Keep current status
|
status: latestData.status, // Keep current status
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString()
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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 ""
|
|
||||||
|
|
@ -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 ""
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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");
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 187 KiB |
|
|
@ -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("")
|
||||||
|
|
@ -431,7 +431,7 @@
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
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_ANALYZER_NONNULL = YES;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
|
@ -488,7 +488,7 @@
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
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_ANALYZER_NONNULL = YES;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 439 B |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 822 B |
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 582 B |
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 822 B |
|
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1012 B |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 3.6 KiB |
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 69 B |
|
|
@ -1,23 +1,23 @@
|
||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"idiom" : "universal",
|
|
||||||
"filename" : "LaunchImage.png",
|
"filename" : "LaunchImage.png",
|
||||||
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idiom" : "universal",
|
|
||||||
"filename" : "LaunchImage@2x.png",
|
"filename" : "LaunchImage@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
"scale" : "2x"
|
"scale" : "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idiom" : "universal",
|
|
||||||
"filename" : "LaunchImage@3x.png",
|
"filename" : "LaunchImage@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
"scale" : "3x"
|
"scale" : "3x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
"version" : 1,
|
"author" : "xcode",
|
||||||
"author" : "xcode"
|
"version" : 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 31 KiB |
|
|
@ -16,13 +16,19 @@
|
||||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" image="LaunchBackground" translatesAutoresizingMaskIntoConstraints="NO" id="tWc-Dq-wcI"/>
|
||||||
</imageView>
|
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"></imageView>
|
||||||
</subviews>
|
</subviews>
|
||||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
<constraint firstItem="YRO-k0-Ey4" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="3T2-ad-Qdv"/>
|
||||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
<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>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
</viewController>
|
</viewController>
|
||||||
|
|
@ -32,6 +38,7 @@
|
||||||
</scene>
|
</scene>
|
||||||
</scenes>
|
</scenes>
|
||||||
<resources>
|
<resources>
|
||||||
<image name="LaunchImage" width="168" height="185"/>
|
<image name="LaunchImage" width="1080" height="1920"/>
|
||||||
|
<image name="LaunchBackground" width="1" height="1"/>
|
||||||
</resources>
|
</resources>
|
||||||
</document>
|
</document>
|
||||||
|
|
|
||||||
|
|
@ -1,70 +1,72 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<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>
|
<dict>
|
||||||
<key>UIApplicationSupportsMultipleScenes</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<false/>
|
<true/>
|
||||||
<key>UISceneConfigurations</key>
|
<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>
|
<dict>
|
||||||
<key>UIWindowSceneSessionRoleApplication</key>
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
<array>
|
<false/>
|
||||||
<dict>
|
<key>UISceneConfigurations</key>
|
||||||
<key>UISceneClassName</key>
|
<dict>
|
||||||
<string>UIWindowScene</string>
|
<key>UIWindowSceneSessionRoleApplication</key>
|
||||||
<key>UISceneConfigurationName</key>
|
<array>
|
||||||
<string>flutter</string>
|
<dict>
|
||||||
<key>UISceneDelegateClassName</key>
|
<key>UISceneClassName</key>
|
||||||
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
<string>UIWindowScene</string>
|
||||||
<key>UISceneStoryboardFile</key>
|
<key>UISceneConfigurationName</key>
|
||||||
<string>Main</string>
|
<string>flutter</string>
|
||||||
</dict>
|
<key>UISceneDelegateClassName</key>
|
||||||
</array>
|
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||||
|
<key>UISceneStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
</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>
|
</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>
|
</plist>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'screens/login_screen.dart';
|
import 'screens/splash_screen.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
|
|
@ -15,8 +15,9 @@ class MyApp extends StatelessWidget {
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
primarySwatch: Colors.blue,
|
primarySwatch: Colors.blue,
|
||||||
|
useMaterial3: true,
|
||||||
),
|
),
|
||||||
home: const LoginScreen(),
|
home: const SplashScreen(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||