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
|
||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
## 🏗️ Arsitektur Sistem
|
||||
|
||||
```
|
||||
ESP32 (Hardware)
|
||||
↓
|
||||
↓ MQTT Publish
|
||||
↓
|
||||
Backend Railway (Node.js)
|
||||
↓
|
||||
↓ Save to Database
|
||||
↓
|
||||
MySQL Database
|
||||
↑
|
||||
↑ API Polling (5 detik)
|
||||
↑
|
||||
Aplikasi Flutter (Android)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 Komponen
|
||||
|
||||
### 1. **Hardware (ESP32)**
|
||||
- File: `esp32_pengering_ikan_railway.ino`
|
||||
- Sensor: DHT22 (suhu), HX711 (berat)
|
||||
- Relay: 4 channel (Heater, Fan, Lamp, Exhaust)
|
||||
- Koneksi: WiFi + MQTT
|
||||
|
||||
### 2. **Backend (Node.js)**
|
||||
- Folder: `backend/`
|
||||
- Framework: Express.js
|
||||
- Database: MySQL (Railway)
|
||||
- MQTT Client: Subscribe dari ESP32
|
||||
- REST API: Endpoint untuk aplikasi
|
||||
|
||||
### 3. **Mobile App (Flutter)**
|
||||
- Folder: `lib/`
|
||||
- Platform: Android
|
||||
- Update: API polling 5 detik
|
||||
- Notifikasi: flutter_local_notifications
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Build APK:
|
||||
```bash
|
||||
# Jalankan script build
|
||||
.\build-with-icon.ps1
|
||||
|
||||
# Atau manual
|
||||
flutter build apk --release
|
||||
```
|
||||
|
||||
### Install di HP:
|
||||
1. Copy `PengeringIkan.apk` dari Desktop
|
||||
2. Install di HP Android
|
||||
3. Izinkan notifikasi
|
||||
4. Buka aplikasi
|
||||
|
||||
---
|
||||
|
||||
## 📱 Tampilan Aplikasi
|
||||
|
||||
### Status Card
|
||||
- Status sistem (READY, SCANNING, BERJALAN, SELESAI)
|
||||
- Waktu mulai & durasi
|
||||
- Pesan status
|
||||
|
||||
### Metrics
|
||||
- 🌡️ Suhu (°C)
|
||||
- ⚖️ Berat (gram)
|
||||
- 🎯 Target berat (gram)
|
||||
|
||||
### Device Status
|
||||
- 🔥 Heater (ON/OFF)
|
||||
- 💨 Fan (ON/OFF)
|
||||
- 🌪️ Exhaust (ON/OFF)
|
||||
|
||||
### Connection Indicator
|
||||
- ☁️🟢 API Connected
|
||||
- ☁️🔴 API Disconnected
|
||||
|
||||
---
|
||||
|
||||
## 🔔 Notifikasi
|
||||
|
||||
Aplikasi mengirim push notification saat:
|
||||
|
||||
1. **Pengeringan Siap** (Status → READY)
|
||||
2. **Ikan Terdeteksi** (Status → BERJALAN)
|
||||
3. **Pengeringan Selesai** (Status → SELESAI)
|
||||
|
||||
Notifikasi muncul di dalam & luar aplikasi.
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Konfigurasi
|
||||
|
||||
### Backend URL:
|
||||
```dart
|
||||
// lib/services/api_service.dart
|
||||
static const String baseUrl = 'https://web-production-47eb.up.railway.app';
|
||||
```
|
||||
|
||||
### Polling Interval:
|
||||
```dart
|
||||
// lib/screens/dashboard_screen_with_api.dart
|
||||
Timer.periodic(const Duration(seconds: 5), ...);
|
||||
```
|
||||
|
||||
### MQTT Topics (Backend):
|
||||
```javascript
|
||||
// backend/server.js
|
||||
const TOPIC_DATA = 'novil/pengering/data';
|
||||
const TOPIC_STATUS = 'novil/pengering/status';
|
||||
const TOPIC_CONTROL = 'novil/pengering/control';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 API Endpoints
|
||||
|
||||
Dokumentasi lengkap: `backend/API_DOCUMENTATION.md`
|
||||
|
||||
### Main Endpoints:
|
||||
- `GET /` - Health check
|
||||
- `GET /api/data/latest` - Data sensor terbaru
|
||||
- `GET /api/data/history` - Riwayat data
|
||||
- `GET /api/status/history` - Riwayat status
|
||||
- `GET /api/stats` - Statistik
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Development
|
||||
|
||||
### Requirements:
|
||||
- Flutter SDK 3.5+
|
||||
- Android SDK
|
||||
- Node.js 18+
|
||||
- MySQL 8.0+
|
||||
|
||||
### Setup Backend:
|
||||
```bash
|
||||
cd backend
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
### Setup Flutter:
|
||||
```bash
|
||||
flutter pub get
|
||||
flutter run
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📄 Dokumentasi
|
||||
|
||||
- **APK_MONITORING_FINAL.md** - Panduan lengkap aplikasi
|
||||
- **PERBAIKAN_APK_FINAL.md** - Changelog & perbaikan
|
||||
- **PERBAIKAN_STATUS_MQTT.md** - Penjelasan API only
|
||||
- **backend/API_DOCUMENTATION.md** - API reference
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Troubleshooting
|
||||
|
||||
### Icon Cloud Merah?
|
||||
- Cek koneksi internet HP
|
||||
- Test backend: https://web-production-47eb.up.railway.app/
|
||||
- Restart aplikasi
|
||||
|
||||
### Data Tidak Update?
|
||||
- Pastikan icon cloud hijau
|
||||
- Cek ESP32 status (WiFi connected)
|
||||
- Cek backend logs di Railway
|
||||
|
||||
### Notifikasi Tidak Muncul?
|
||||
- Izinkan notifikasi di Settings
|
||||
- Disable battery optimization
|
||||
- Cek status berubah di ESP32
|
||||
|
||||
---
|
||||
|
||||
## 📝 Version History
|
||||
|
||||
### v1.0.0 (1 Juni 2026) - Current
|
||||
- ✅ Monitoring only (no control)
|
||||
- ✅ API polling 5 detik
|
||||
- ✅ Push notifications
|
||||
- ✅ 3 device cards (Heater, Fan, Exhaust)
|
||||
- ✅ Auto-update data
|
||||
- ✅ Connection indicator
|
||||
|
||||
---
|
||||
|
||||
## 👨💻 Tech Stack
|
||||
|
||||
- **Mobile:** Flutter 3.5, Dart 3.0
|
||||
- **Backend:** Node.js 18, Express.js
|
||||
- **Database:** MySQL 8.0 (Railway)
|
||||
- **IoT:** ESP32, Arduino IDE
|
||||
- **Protocol:** MQTT, REST API
|
||||
- **Hosting:** Railway.app
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
Untuk pertanyaan atau masalah:
|
||||
1. Cek dokumentasi di folder `docs/`
|
||||
2. Lihat troubleshooting di README
|
||||
3. Cek logs di Railway dashboard
|
||||
|
||||
---
|
||||
|
||||
## 📜 License
|
||||
|
||||
Private project - All rights reserved
|
||||
|
||||
---
|
||||
|
||||
**Build Date:** 1 Juni 2026
|
||||
**Version:** 1.0.0
|
||||
**Status:** ✅ Production Ready
|
||||
|
||||
🎉 **Aplikasi siap digunakan untuk monitoring pengering ikan!**
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
namespace = "com.example.aplikasi_novil"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
ndkVersion = "27.0.12077973"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
|
|
@ -42,3 +43,7 @@ android {
|
|||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Permission untuk internet (MQTT) -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<application
|
||||
android:label="aplikasi_novil"
|
||||
android:name="${applicationName}"
|
||||
|
|
|
|||
|
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"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<!-- Simple launch background - no splash image -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
<item android:drawable="@android:color/white" />
|
||||
</layer-list>
|
||||
|
|
|
|||
|
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"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<!-- Simple launch background - no splash image -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
|
|
|
|||
|
|
@ -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
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
|
|
|
|||
|
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
|
||||
// Format: {"suhu":28.5,"berat":450,"target":315}
|
||||
// Format: {"suhu":28.5,"berat":450,"target":315,"relay1":true,"relay2":true,"relay3":true,"relay4":false}
|
||||
// =====================================================
|
||||
if (topic === MQTT_TOPICS.data) {
|
||||
try {
|
||||
|
|
@ -152,15 +152,15 @@ mqttClient.on('message', async (topic, message) => {
|
|||
const now = new Date();
|
||||
const wibTime = new Date(now.getTime() + (7 * 60 * 60 * 1000));
|
||||
|
||||
// Update latestData dengan data dari ESP32
|
||||
// Update latestData dengan data dari ESP32 (termasuk status relay)
|
||||
latestData = {
|
||||
suhu: data.suhu || 0,
|
||||
berat: data.berat || 0,
|
||||
target: data.target || 0,
|
||||
relay1: latestData.relay1 || false, // Keep current relay status
|
||||
relay2: latestData.relay2 || false,
|
||||
relay3: latestData.relay3 || false,
|
||||
relay4: latestData.relay4 || false,
|
||||
relay1: data.relay1 !== undefined ? data.relay1 : latestData.relay1, // Update dari ESP32
|
||||
relay2: data.relay2 !== undefined ? data.relay2 : latestData.relay2,
|
||||
relay3: data.relay3 !== undefined ? data.relay3 : latestData.relay3,
|
||||
relay4: data.relay4 !== undefined ? data.relay4 : latestData.relay4,
|
||||
status: latestData.status, // Keep current status
|
||||
timestamp: wibTime.toISOString()
|
||||
};
|
||||
|
|
@ -299,21 +299,21 @@ aedes.on('publish', async (packet, client) => {
|
|||
|
||||
// =====================================================
|
||||
// TOPIC: novil/pengering/data
|
||||
// Format: {"suhu":28.5,"berat":450,"target":315}
|
||||
// Format: {"suhu":28.5,"berat":450,"target":315,"relay1":true,"relay2":true,"relay3":true,"relay4":false}
|
||||
// =====================================================
|
||||
if (topic === 'novil/pengering/data') {
|
||||
try {
|
||||
const data = JSON.parse(message);
|
||||
|
||||
// Update latestData dengan data dari ESP32
|
||||
// Update latestData dengan data dari ESP32 (termasuk status relay)
|
||||
latestData = {
|
||||
suhu: data.suhu || 0,
|
||||
berat: data.berat || 0,
|
||||
target: data.target || 0,
|
||||
relay1: latestData.relay1 || false, // Keep current relay status
|
||||
relay2: latestData.relay2 || false,
|
||||
relay3: latestData.relay3 || false,
|
||||
relay4: latestData.relay4 || false,
|
||||
relay1: data.relay1 !== undefined ? data.relay1 : latestData.relay1, // Update dari ESP32
|
||||
relay2: data.relay2 !== undefined ? data.relay2 : latestData.relay2,
|
||||
relay3: data.relay3 !== undefined ? data.relay3 : latestData.relay3,
|
||||
relay4: data.relay4 !== undefined ? data.relay4 : latestData.relay4,
|
||||
status: latestData.status, // Keep current status
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
|
|
@ -488,7 +488,7 @@
|
|||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
|
|
|
|||
|
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" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
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">
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||
</imageView>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" image="LaunchBackground" translatesAutoresizingMaskIntoConstraints="NO" id="tWc-Dq-wcI"/>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"></imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="3T2-ad-Qdv"/>
|
||||
<constraint firstItem="tWc-Dq-wcI" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="RPx-PI-7Xg"/>
|
||||
<constraint firstItem="tWc-Dq-wcI" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="SdS-ul-q2q"/>
|
||||
<constraint firstAttribute="trailing" secondItem="tWc-Dq-wcI" secondAttribute="trailing" id="Swv-Gf-Rwn"/>
|
||||
<constraint firstAttribute="trailing" secondItem="YRO-k0-Ey4" secondAttribute="trailing" id="TQA-XW-tRk"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="duK-uY-Gun"/>
|
||||
<constraint firstItem="tWc-Dq-wcI" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="kV7-tw-vXt"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="xPn-NY-SIU"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
|
|
@ -32,6 +38,7 @@
|
|||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="LaunchImage" width="168" height="185"/>
|
||||
<image name="LaunchImage" width="1080" height="1920"/>
|
||||
<image name="LaunchBackground" width="1" height="1"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
|
|
|||
|
|
@ -1,70 +1,72 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Aplikasi Novil</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>aplikasi_novil</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Aplikasi Novil</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>aplikasi_novil</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneClassName</key>
|
||||
<string>UIWindowScene</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>flutter</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneClassName</key>
|
||||
<string>UIWindowScene</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>flutter</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'screens/login_screen.dart';
|
||||
import 'screens/splash_screen.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
|
|
@ -15,8 +15,9 @@ class MyApp extends StatelessWidget {
|
|||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.blue,
|
||||
useMaterial3: true,
|
||||
),
|
||||
home: const LoginScreen(),
|
||||
home: const SplashScreen(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||