feat: Implement dual mode system (Smart & Fixed) and fix logout bugs
- Add Smart Mode: auto-stop when target reached or timeout - Add Fixed Duration Mode: water for exact duration - Fix auth state management to prevent unexpected logout - Add error handling across all pages - Update automation service with dual mode logic - Add mode selection UI in sensor config page - Update Firebase structure: mode_sensor, durasi_sensor - Enhance logging for debugging - Add flutter_launcher_icons for app icon management - Add comprehensive documentation (3 MD files) PERUBHAAN BESAR
|
|
@ -0,0 +1,243 @@
|
|||
# Laporan Perbaikan Bug ApsGo
|
||||
|
||||
**Tanggal**: 26 Januari 2026
|
||||
**Status**: ✅ SELESAI
|
||||
|
||||
## Ringkasan Bug yang Diperbaiki
|
||||
|
||||
### 1. 🔒 Bug Logout Tiba-tiba ke Landing Page
|
||||
|
||||
**Masalah**:
|
||||
- Aplikasi terkadang logout secara tiba-tiba saat klik menu Kontrol atau menu lainnya
|
||||
- User ter-redirect ke Landing Page tanpa sengaja
|
||||
|
||||
**Penyebab**:
|
||||
- Tidak ada monitoring auth state di DashboardPage
|
||||
- Ketika Firebase Auth token expired atau ada perubahan auth state, tidak ada handler yang tepat
|
||||
- Navigation stack tidak terlindungi dengan baik
|
||||
|
||||
**Solusi**:
|
||||
✅ Tambahkan `authStateChanges` listener di `initState()` DashboardPage
|
||||
```dart
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Monitor auth state untuk mencegah logout tidak terduga
|
||||
_authService.authStateChanges.listen((user) {
|
||||
if (user == null && mounted) {
|
||||
// User logged out, redirect to login
|
||||
Navigator.of(context).pushNamedAndRemoveUntil(
|
||||
'/login',
|
||||
(route) => false,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**File yang Diubah**:
|
||||
- `lib/screens/dashboard_page.dart`
|
||||
|
||||
---
|
||||
|
||||
### 2. 💧 Bug Pompa Tidak Langsung Aktif Saat Soil Moisture Rendah
|
||||
|
||||
**Masalah**:
|
||||
- Pompa terkadang tidak langsung aktif ketika soil moisture di bawah ambang bawah
|
||||
- Delay response terlalu lama (10 detik)
|
||||
- Cooldown period terlalu lama (5 menit)
|
||||
|
||||
**Penyebab**:
|
||||
- Timer check sensor berjalan setiap 10 detik → terlalu lambat
|
||||
- Cooldown 5 menit antar penyiraman → terlalu lama untuk kondisi kritis
|
||||
- Kurang logging untuk debugging
|
||||
|
||||
**Solusi**:
|
||||
✅ **Interval Check**: Ubah dari 10 detik → **5 detik** (lebih responsif)
|
||||
```dart
|
||||
_sensorCheckTimer = Timer.periodic(
|
||||
const Duration(seconds: 5), // Sebelumnya: 10 detik
|
||||
(timer) => _checkSensorThreshold(),
|
||||
);
|
||||
```
|
||||
|
||||
✅ **Cooldown Period**: Kurangi dari 5 menit → **2 menit**
|
||||
```dart
|
||||
if (diff.inMinutes < 2) { // Sebelumnya: 5 menit
|
||||
debugPrint('⏳ POT $potNumber: Cooldown active (${2 - diff.inMinutes} min remaining)');
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
✅ **Tambah Logging Detail**:
|
||||
- Log setiap check sensor dengan nilai threshold
|
||||
- Log setiap trigger penyiraman
|
||||
- Log progress penyiraman per 5 detik
|
||||
- Log saat target tercapai
|
||||
|
||||
✅ **Pastikan Flag Reset**: Tambah delay sebelum reset flag untuk memastikan cleanup selesai
|
||||
```dart
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
_isWateringActive[potKey] = false;
|
||||
```
|
||||
|
||||
✅ **Error Handling**: Tambahkan try-catch untuk matikan pompa saat error
|
||||
```dart
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error watering pot by sensor: $e');
|
||||
// Matikan semua untuk safety
|
||||
try {
|
||||
await _dbService.setPompaAir(false);
|
||||
await _dbService.setPot(potNumber, false);
|
||||
} catch (cleanupError) {
|
||||
debugPrint('❌ Error during cleanup: $cleanupError');
|
||||
}
|
||||
_isWateringActive[potKey] = false;
|
||||
}
|
||||
```
|
||||
|
||||
**File yang Diubah**:
|
||||
- `lib/services/kontrol_automation_service.dart`
|
||||
|
||||
---
|
||||
|
||||
### 3. 🔌 Verifikasi Mapping Soil Sensor ke Mosfet
|
||||
|
||||
**Masalah yang Diperiksa**:
|
||||
- Apakah soil_1 bisa mempengaruhi mosvet_3 dst
|
||||
- Apakah mapping soil_x ke mosvet_y sudah benar
|
||||
|
||||
**Hasil Verifikasi**:
|
||||
✅ Mapping sudah **BENAR**:
|
||||
```
|
||||
soil_1 (Pot 1) → mosvet_3 (Valve Pot 1)
|
||||
soil_2 (Pot 2) → mosvet_4 (Valve Pot 2)
|
||||
soil_3 (Pot 3) → mosvet_5 (Valve Pot 3)
|
||||
soil_4 (Pot 4) → mosvet_6 (Valve Pot 4)
|
||||
soil_5 (Pot 5) → mosvet_7 (Valve Pot 5)
|
||||
```
|
||||
|
||||
**Catatan**:
|
||||
- mosvet_1 = Pompa Air
|
||||
- mosvet_2 = Pompa Nutrisi
|
||||
- mosvet_3 s/d mosvet_7 = Valve Pot 1-5
|
||||
|
||||
**Perbaikan**:
|
||||
✅ Tambah komentar dan logging untuk memperjelas mapping
|
||||
```dart
|
||||
// pot 1 → mosvet_3, pot 2 → mosvet_4, ... pot 5 → mosvet_7
|
||||
debugPrint('💧 Starting watering: POT $potNumber (mosvet_${potNumber + 2})');
|
||||
```
|
||||
|
||||
✅ Tambah logging tracking nilai soil sensor
|
||||
```dart
|
||||
debugPrint('🌡️ soil_$i = $soilValue (threshold: $batasBawah)');
|
||||
debugPrint('⚠️ $soilKey ($soilValue) < batasBawah ($batasBawah) → Triggering watering for POT $i');
|
||||
```
|
||||
|
||||
**File yang Diubah**:
|
||||
- `lib/services/kontrol_automation_service.dart`
|
||||
|
||||
---
|
||||
|
||||
## Perubahan Detail
|
||||
|
||||
### File: `lib/screens/dashboard_page.dart`
|
||||
|
||||
**Perubahan**:
|
||||
1. Import `AuthService`
|
||||
2. Tambah instance `_authService`
|
||||
3. Tambah `initState()` dengan auth state listener
|
||||
|
||||
### File: `lib/services/kontrol_automation_service.dart`
|
||||
|
||||
**Perubahan**:
|
||||
1. ✅ Timer interval: 10s → 5s (line ~183)
|
||||
2. ✅ Cooldown: 5 min → 2 min (line ~248)
|
||||
3. ✅ Tambah logging detail di `_checkSensorThreshold()` (line ~217)
|
||||
4. ✅ Tambah logging mapping pot (line ~254)
|
||||
5. ✅ Tambah logging progress watering (line ~265)
|
||||
6. ✅ Tambah delay sebelum reset flag (line ~283)
|
||||
7. ✅ Improve error handling dengan cleanup (line ~286)
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
Setelah update, pastikan test hal-hal berikut:
|
||||
|
||||
### Auth & Navigation
|
||||
- [ ] Login dan navigate ke Dashboard
|
||||
- [ ] Klik menu Kontrol → tidak logout
|
||||
- [ ] Klik menu Histori → tidak logout
|
||||
- [ ] Buka drawer dan klik menu → tidak logout
|
||||
- [ ] Test dengan koneksi internet ON/OFF
|
||||
- [ ] Test dengan token yang akan expired
|
||||
|
||||
### Sensor Automation
|
||||
- [ ] Aktifkan mode Sensor
|
||||
- [ ] Set soil moisture di bawah batas bawah (misal: 25%)
|
||||
- [ ] Cek pompa aktif dalam **< 5 detik**
|
||||
- [ ] Cek valve pot yang tepat aktif (soil_1 → mosvet_3, dst)
|
||||
- [ ] Cek log di console untuk tracking
|
||||
- [ ] Test dengan multiple pot di bawah threshold bersamaan
|
||||
- [ ] Test cooldown period (2 menit)
|
||||
|
||||
### Edge Cases
|
||||
- [ ] Test saat Firebase disconnect
|
||||
- [ ] Test saat sensor data tidak valid (0, null, string)
|
||||
- [ ] Test saat automation service crash
|
||||
- [ ] Test multiple user logout/login
|
||||
|
||||
---
|
||||
|
||||
## Logging untuk Debugging
|
||||
|
||||
Untuk monitor aktivitas, cek console log dengan pattern:
|
||||
|
||||
**Sensor Mode**:
|
||||
```
|
||||
🌡️ Sensor Mode: Started
|
||||
🌡️ Checking thresholds: batas_bawah=30, batas_atas=80
|
||||
🌡️ soil_1 = 25 (threshold: 30)
|
||||
⚠️ soil_1 (25) < batasBawah (30) → Triggering watering for POT 1
|
||||
💧 Starting watering: POT 1 (mosvet_3)
|
||||
🌡️ Target: soil_1 should reach >= 80 (currently: 25)
|
||||
💧 POT 1 watering: 5s, soil_1: 35
|
||||
💧 POT 1 watering: 10s, soil_1: 50
|
||||
✅ POT 1 reached target: 80 >= 80
|
||||
✅ Sensor Mode: Watering completed for POT 1 (15s)
|
||||
```
|
||||
|
||||
**Cooldown**:
|
||||
```
|
||||
⏳ POT 1: Cooldown active (1 min remaining)
|
||||
```
|
||||
|
||||
**Errors**:
|
||||
```
|
||||
❌ Error watering pot by sensor: [error message]
|
||||
❌ Error during cleanup: [error message]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rekomendasi Lanjutan
|
||||
|
||||
1. **Tambah UI Indicator**: Tampilkan status automation (active/inactive) di Dashboard
|
||||
2. **Notification**: Push notification saat pompa aktif otomatis
|
||||
3. **History Log**: Simpan log automation ke Firebase untuk review
|
||||
4. **Manual Override**: Button untuk force stop automation
|
||||
5. **Sensor Calibration**: UI untuk kalibrasi sensor soil moisture
|
||||
|
||||
---
|
||||
|
||||
## Kontak
|
||||
|
||||
Jika masih ada masalah setelah update ini, catat:
|
||||
1. Timestamp kejadian
|
||||
2. Screenshot console log
|
||||
3. Nilai sensor saat kejadian
|
||||
4. Action yang dilakukan user
|
||||
|
||||
Happy Gardening! 🌱
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
# 📱 Panduan Mengubah Logo/Icon APK
|
||||
|
||||
## ✅ Cara 1: Otomatis dengan flutter_launcher_icons (SUDAH DISETUP)
|
||||
|
||||
### Langkah 1: Install Package
|
||||
Saya sudah menambahkan `flutter_launcher_icons` ke `pubspec.yaml`. Sekarang jalankan:
|
||||
|
||||
```bash
|
||||
flutter pub get
|
||||
```
|
||||
|
||||
### Langkah 2: Siapkan Logo
|
||||
**Requirement:**
|
||||
- Format: PNG
|
||||
- Ukuran: **1024x1024 px** (recommended) atau minimal 512x512 px
|
||||
- Background: Transparent atau solid color
|
||||
- File: `assets/images/logo_apsgo.png` (sudah ada)
|
||||
|
||||
**Jika ingin ganti logo:**
|
||||
1. Siapkan file PNG 1024x1024 px
|
||||
2. Replace file `assets/images/logo_apsgo.png`
|
||||
|
||||
### Langkah 3: Generate Icon
|
||||
Jalankan command ini di terminal:
|
||||
|
||||
```bash
|
||||
flutter pub run flutter_launcher_icons
|
||||
```
|
||||
|
||||
### Langkah 4: Verify
|
||||
Check folder ini, icon seharusnya sudah ter-generate:
|
||||
- `android/app/src/main/res/mipmap-hdpi/ic_launcher.png`
|
||||
- `android/app/src/main/res/mipmap-mdpi/ic_launcher.png`
|
||||
- `android/app/src/main/res/mipmap-xhdpi/ic_launcher.png`
|
||||
- `android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png`
|
||||
- `android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png`
|
||||
|
||||
### Langkah 5: Build APK
|
||||
```bash
|
||||
flutter build apk --release
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Cara 2: Manual (Advanced)
|
||||
|
||||
Jika cara otomatis tidak berhasil, gunakan cara manual:
|
||||
|
||||
### Langkah 1: Prepare Icon dengan Berbagai Ukuran
|
||||
|
||||
| Resolusi | Ukuran | Folder |
|
||||
|----------|--------|--------|
|
||||
| MDPI | 48x48 px | `mipmap-mdpi/` |
|
||||
| HDPI | 72x72 px | `mipmap-hdpi/` |
|
||||
| XHDPI | 96x96 px | `mipmap-xhdpi/` |
|
||||
| XXHDPI | 144x144 px | `mipmap-xxhdpi/` |
|
||||
| XXXHDPI | 192x192 px | `mipmap-xxxhdpi/` |
|
||||
|
||||
### Langkah 2: Replace File
|
||||
|
||||
Replace semua file `ic_launcher.png` di folder berikut:
|
||||
```
|
||||
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
|
||||
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
|
||||
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
|
||||
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
|
||||
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
|
||||
```
|
||||
|
||||
### Langkah 3: Build APK
|
||||
```bash
|
||||
flutter build apk --release
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌟 Adaptive Icon (Android 8.0+) - Optional
|
||||
|
||||
Untuk icon yang lebih modern dengan adaptive background:
|
||||
|
||||
### Update pubspec.yaml:
|
||||
```yaml
|
||||
flutter_launcher_icons:
|
||||
android: true
|
||||
ios: false
|
||||
image_path: "assets/images/logo_apsgo.png"
|
||||
adaptive_icon_background: "#4CAF50" # Warna background
|
||||
adaptive_icon_foreground: "assets/images/logo_apsgo.png"
|
||||
```
|
||||
|
||||
### Generate:
|
||||
```bash
|
||||
flutter pub run flutter_launcher_icons
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Tools Online untuk Resize Icon
|
||||
|
||||
Jika tidak punya tool untuk resize, gunakan:
|
||||
|
||||
1. **Android Asset Studio**: https://romannurik.github.io/AndroidAssetStudio/
|
||||
2. **App Icon Generator**: https://appicon.co/
|
||||
3. **Icon Kitchen**: https://icon.kitchen/
|
||||
|
||||
Upload logo 1024x1024, download semua ukuran, copy ke folder mipmap.
|
||||
|
||||
---
|
||||
|
||||
## 📝 Checklist
|
||||
|
||||
- [x] ✅ Package `flutter_launcher_icons` sudah ditambahkan
|
||||
- [x] ✅ Konfigurasi di `pubspec.yaml` sudah disetup
|
||||
- [ ] ⏳ Jalankan `flutter pub get`
|
||||
- [ ] ⏳ Siapkan logo 1024x1024 px di `assets/images/logo_apsgo.png`
|
||||
- [ ] ⏳ Jalankan `flutter pub run flutter_launcher_icons`
|
||||
- [ ] ⏳ Build APK dengan `flutter build apk --release`
|
||||
|
||||
---
|
||||
|
||||
## ❓ Troubleshooting
|
||||
|
||||
**Q: Error "flutter_launcher_icons not found"**
|
||||
A: Jalankan `flutter pub get` dulu
|
||||
|
||||
**Q: Icon tidak berubah setelah install**
|
||||
A: Uninstall app dulu, baru install ulang APK baru
|
||||
|
||||
**Q: Logo terlihat terpotong**
|
||||
A: Gunakan logo dengan padding/margin agar tidak terpotong di adaptive icon
|
||||
|
||||
**Q: Ingin icon berbeda untuk debug & release**
|
||||
A: Tambahkan konfigurasi terpisah:
|
||||
```yaml
|
||||
flutter_launcher_icons:
|
||||
android: true
|
||||
image_path: "assets/images/logo_apsgo.png"
|
||||
image_path_android: "assets/images/logo_debug.png" # untuk debug
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Command
|
||||
|
||||
Jalankan command ini secara berurutan:
|
||||
|
||||
```bash
|
||||
# 1. Install dependencies
|
||||
flutter pub get
|
||||
|
||||
# 2. Generate icons
|
||||
flutter pub run flutter_launcher_icons
|
||||
|
||||
# 3. Build APK
|
||||
flutter build apk --release
|
||||
|
||||
# 4. APK location
|
||||
# build/app/outputs/flutter-apk/app-release.apk
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Selamat! Icon APK Anda sekarang sudah bisa diganti! 🎉**
|
||||
|
|
@ -0,0 +1,466 @@
|
|||
# 🎉 Implementasi Dual Mode System & Bug Fixes
|
||||
|
||||
**Tanggal**: 26 Januari 2026
|
||||
**Status**: ✅ COMPLETED
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Fitur Baru: Dual Mode System
|
||||
|
||||
### Mode 1: Smart Mode (Adaptif) 🧠
|
||||
|
||||
**Konsep:**
|
||||
- Pompa ON: ketika `soil < Batas Minimal`
|
||||
- Pompa OFF: ketika `soil >= Batas Maksimal` ATAU timeout (durasi habis)
|
||||
- Prioritas: **Mencapai target** > durasi
|
||||
|
||||
**Cara Kerja:**
|
||||
```
|
||||
Trigger: soil_1 = 25% < 30% (batas minimal)
|
||||
├─ Pompa Air ON
|
||||
├─ Valve POT 1 ON
|
||||
├─ Loop setiap 5 detik:
|
||||
│ ├─ Check soil_1 value
|
||||
│ ├─ Jika soil_1 >= 80% → STOP (target tercapai) ✅
|
||||
│ └─ Jika waktu >= durasi (misal 60s) → STOP (timeout) ⏱️
|
||||
└─ Pompa & Valve OFF
|
||||
```
|
||||
|
||||
**Log Example:**
|
||||
```
|
||||
🧠 Smart Mode: Target soil_1 >= 80 (currently: 25), max 60s
|
||||
💧 POT 1 watering: 5s, soil_1: 35
|
||||
💧 POT 1 watering: 10s, soil_1: 55
|
||||
💧 POT 1 watering: 15s, soil_1: 82
|
||||
✅ POT 1 reached target: 82 >= 80 (Smart Mode)
|
||||
✅ Sensor Mode: Watering completed for POT 1 (15s, mode: smart)
|
||||
```
|
||||
|
||||
**Use Case:**
|
||||
- Tanaman sensitif terhadap over-watering
|
||||
- Ingin efisiensi air maksimal
|
||||
- Sensor soil moisture berfungsi baik
|
||||
|
||||
---
|
||||
|
||||
### Mode 2: Fixed Duration (Durasi Tetap) ⏱️
|
||||
|
||||
**Konsep:**
|
||||
- Pompa ON: ketika `soil < Batas Minimal`
|
||||
- Pompa OFF: setelah **tepat durasi yang ditentukan**
|
||||
- Prioritas: **Durasi tetap** (tidak peduli sensor)
|
||||
|
||||
**Cara Kerja:**
|
||||
```
|
||||
Trigger: soil_1 = 25% < 30% (batas minimal)
|
||||
├─ Pompa Air ON
|
||||
├─ Valve POT 1 ON
|
||||
├─ Loop setiap 5 detik (monitoring only):
|
||||
│ ├─ Log soil_1 value (tidak break)
|
||||
│ └─ Continue sampai durasi habis
|
||||
└─ Setelah durasi (misal 30s) → Pompa & Valve OFF
|
||||
```
|
||||
|
||||
**Log Example:**
|
||||
```
|
||||
⏱️ Fixed Duration Mode: Watering for exactly 30s
|
||||
💧 POT 1 watering: 5s/30s, soil_1: 40
|
||||
💧 POT 1 watering: 10s/30s, soil_1: 60
|
||||
💧 POT 1 watering: 15s/30s, soil_1: 78
|
||||
💧 POT 1 watering: 20s/30s, soil_1: 88
|
||||
💧 POT 1 watering: 25s/30s, soil_1: 92
|
||||
💧 POT 1 watering: 30s/30s, soil_1: 95
|
||||
✅ POT 1 fixed duration completed: 30s
|
||||
✅ Sensor Mode: Watering completed for POT 1 (30s, mode: fixed)
|
||||
```
|
||||
|
||||
**Use Case:**
|
||||
- Sensor tidak akurat atau rusak
|
||||
- Ingin penyiraman konsisten setiap kali
|
||||
- Sudah tahu berapa lama perlu menyiram
|
||||
|
||||
---
|
||||
|
||||
## 🎨 UI Changes
|
||||
|
||||
### Sensor Config Page
|
||||
|
||||
**Tambahan Baru:**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Mode Penyiraman │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ ● Smart Mode (Adaptif) │
|
||||
│ Siram hingga batas atas tercapai atau │
|
||||
│ timeout │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ ○ Fixed Duration (Durasi Tetap) │
|
||||
│ Siram selama durasi yang ditentukan │
|
||||
└─────────────────────────────────────────────┘
|
||||
|
||||
Batas Minimal (%): 30
|
||||
Batas Maksimal (%): 80
|
||||
Durasi Penyiraman: 10 detik
|
||||
|
||||
ℹ️ Smart Mode: Pompa akan mati otomatis saat
|
||||
mencapai batas atas atau durasi habis
|
||||
sebagai safety timeout
|
||||
|
||||
Fixed Duration: Pompa akan menyiram selama
|
||||
durasi yang ditentukan, tidak peduli nilai
|
||||
sensor
|
||||
```
|
||||
|
||||
**Interaksi:**
|
||||
- Radio buttons untuk pilih mode
|
||||
- Info text berubah dinamis sesuai mode
|
||||
- Durasi memiliki fungsi berbeda per mode:
|
||||
- Smart: Max timeout (safety)
|
||||
- Fixed: Durasi aktual penyiraman
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Technical Implementation
|
||||
|
||||
### 1. Data Model Changes
|
||||
|
||||
**Local Storage (sensor_config):**
|
||||
```dart
|
||||
{
|
||||
'batasMinimal': '30',
|
||||
'batasMaksimal': '80',
|
||||
'durasi': '10',
|
||||
'durasiUnit': 'detik',
|
||||
'mode': 'smart', // ← NEW: 'smart' or 'fixed'
|
||||
}
|
||||
```
|
||||
|
||||
**Firebase (kontrol):**
|
||||
```json
|
||||
{
|
||||
"batas_atas": 80,
|
||||
"batas_bawah": 30,
|
||||
"durasi_sensor": 10, // ← NEW: dalam detik
|
||||
"mode_sensor": "smart", // ← NEW: 'smart' or 'fixed'
|
||||
"otomatis": true
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Code Changes
|
||||
|
||||
**File Modified:**
|
||||
1. ✅ `lib/screens/sensor_config_page.dart`
|
||||
- Tambah radio buttons untuk mode selection
|
||||
- Update `_sensorConfig` dengan field `mode`
|
||||
- Convert durasi ke seconds sebelum save ke Firebase
|
||||
- Dynamic info text berdasarkan mode
|
||||
|
||||
2. ✅ `lib/services/kontrol_automation_service.dart`
|
||||
- Update `_checkSensorThreshold()` untuk ambil `mode_sensor` dan `durasi_sensor`
|
||||
- Update `_waterPotBySensor()` dengan parameter `mode` dan `durasiSeconds`
|
||||
- Implementasi dual logic dengan if-else:
|
||||
- Smart mode: break jika `soil >= batasAtas`
|
||||
- Fixed mode: continue sampai durasi habis
|
||||
|
||||
3. ✅ `lib/services/firebase_database_service.dart`
|
||||
- Tidak perlu perubahan, sudah support `updateKontrolConfig()`
|
||||
|
||||
### 3. Logic Flow
|
||||
|
||||
**Smart Mode:**
|
||||
```dart
|
||||
if (mode == 'smart') {
|
||||
while (elapsedSeconds < durasiSeconds && _isSensorModeActive) {
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
elapsedSeconds += 5;
|
||||
|
||||
currentSoil = await getSensorData();
|
||||
|
||||
if (currentSoil >= batasAtas) {
|
||||
break; // ✅ Target tercapai
|
||||
}
|
||||
}
|
||||
|
||||
if (elapsedSeconds >= durasiSeconds) {
|
||||
// ⏰ Timeout (safety)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fixed Mode:**
|
||||
```dart
|
||||
if (mode == 'fixed') {
|
||||
while (elapsedSeconds < durasiSeconds && _isSensorModeActive) {
|
||||
await Future.delayed(Duration(seconds: 5));
|
||||
elapsedSeconds += 5;
|
||||
|
||||
currentSoil = await getSensorData(); // Log only, tidak break
|
||||
}
|
||||
|
||||
// ✅ Durasi selesai
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
### Bug #1: Logout Tiba-tiba ke Login Page ✅ FIXED
|
||||
|
||||
**Root Cause:**
|
||||
- Auth state listener trigger saat first load
|
||||
- Listener tidak memeriksa apakah user sudah login sebelumnya
|
||||
- Bisa menyebabkan loop redirect
|
||||
|
||||
**Solution:**
|
||||
```dart
|
||||
class _DashboardPageState extends State<DashboardPage> {
|
||||
bool _isInitialized = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Verify user on init
|
||||
if (_authService.currentUser == null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pushNamedAndRemoveUntil(
|
||||
'/login',
|
||||
(route) => false,
|
||||
);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
_isInitialized = true;
|
||||
|
||||
// Listener hanya trigger jika sudah initialized
|
||||
_authService.authStateChanges.listen((user) {
|
||||
if (_isInitialized && user == null && mounted) {
|
||||
Navigator.of(context).pushNamedAndRemoveUntil(
|
||||
'/login',
|
||||
(route) => false,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Changes:**
|
||||
1. ✅ Tambah flag `_isInitialized`
|
||||
2. ✅ Check `currentUser` di `initState()`
|
||||
3. ✅ Listener hanya trigger setelah initialized
|
||||
4. ✅ Always check `mounted` sebelum navigate
|
||||
|
||||
### Bug #2: Crash yang Menyebabkan Logout
|
||||
|
||||
**Root Cause:**
|
||||
- Unhandled exceptions di Firebase calls
|
||||
- setState() dipanggil setelah widget disposed
|
||||
- StreamBuilder error tidak di-handle
|
||||
|
||||
**Solution:**
|
||||
```dart
|
||||
// Always wrap await dengan try-catch
|
||||
try {
|
||||
final data = await _dbService.getSensorData();
|
||||
if (mounted) { // ← Check mounted
|
||||
setState(() {
|
||||
// Update state
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error: $e');
|
||||
if (mounted) {
|
||||
// Show error tapi tidak crash
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Files Enhanced with Error Handling:**
|
||||
1. ✅ `dashboard_page.dart` - StreamBuilder error state
|
||||
2. ✅ `kontrol_page.dart` - Load Firebase state
|
||||
3. ✅ `sensor_config_page.dart` - Load config
|
||||
4. ✅ `waktu_config_page.dart` - Load config
|
||||
5. ✅ `pot_selection_page.dart` - Load mode status
|
||||
|
||||
---
|
||||
|
||||
## 📊 Comparison Table
|
||||
|
||||
| Feature | Smart Mode 🧠 | Fixed Duration ⏱️ |
|
||||
|---------|---------------|-------------------|
|
||||
| **Stop Condition** | Target OR timeout | Durasi habis |
|
||||
| **Sensor Dependency** | ✅ High | ❌ Low |
|
||||
| **Water Efficiency** | ✅ Optimal | ⚠️ Depends |
|
||||
| **Predictability** | ⚠️ Variable | ✅ Consistent |
|
||||
| **If Sensor Fails** | ⚠️ Use timeout | ✅ Still works |
|
||||
| **Over-watering Risk** | ❌ Low | ⚠️ Possible |
|
||||
| **Best For** | Smart irrigation | Manual control |
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing Checklist
|
||||
|
||||
### Dual Mode Testing
|
||||
|
||||
**Smart Mode:**
|
||||
- [ ] Trigger penyiraman (soil < 30%)
|
||||
- [ ] Pompa mati saat mencapai target (soil >= 80%)
|
||||
- [ ] Pompa mati saat timeout (durasi habis)
|
||||
- [ ] Log menunjukkan "Smart Mode" dengan target
|
||||
- [ ] Cooldown 2 menit bekerja
|
||||
|
||||
**Fixed Duration:**
|
||||
- [ ] Trigger penyiraman (soil < 30%)
|
||||
- [ ] Pompa menyiram selama durasi penuh
|
||||
- [ ] Pompa TIDAK mati meskipun target tercapai
|
||||
- [ ] Log menunjukkan "Fixed Duration Mode"
|
||||
- [ ] Progress counter: 5s/30s, 10s/30s, dst
|
||||
|
||||
**Edge Cases:**
|
||||
- [ ] Switch mode saat automation aktif
|
||||
- [ ] Durasi 0 atau negatif (validation)
|
||||
- [ ] Multiple pot trigger bersamaan
|
||||
- [ ] Sensor return invalid value (null, "", 0)
|
||||
- [ ] Firebase disconnected saat watering
|
||||
|
||||
### Bug Fixes Testing
|
||||
|
||||
**Auth & Navigation:**
|
||||
- [ ] Login → Dashboard (tidak logout)
|
||||
- [ ] Dashboard → Kontrol → tidak logout
|
||||
- [ ] Dashboard → Histori → tidak logout
|
||||
- [ ] Drawer navigation → tidak logout
|
||||
- [ ] Back button → tidak logout
|
||||
- [ ] Network ON/OFF → tidak logout
|
||||
- [ ] Multiple tab/window → tidak logout
|
||||
|
||||
**Error Handling:**
|
||||
- [ ] Firebase call error → show snackbar, tidak crash
|
||||
- [ ] setState after dispose → tidak crash
|
||||
- [ ] StreamBuilder error → tampil error state
|
||||
- [ ] Invalid sensor data → handle gracefully
|
||||
|
||||
---
|
||||
|
||||
## 📝 Migration Guide
|
||||
|
||||
### Existing Users
|
||||
|
||||
**Jika sudah ada konfigurasi lama:**
|
||||
1. App akan auto-migrate ke format baru
|
||||
2. Default mode: `smart`
|
||||
3. Default durasi unit: `detik`
|
||||
4. Konfigurasi lama tetap tersimpan
|
||||
|
||||
**Jika belum ada konfigurasi:**
|
||||
1. Default values:
|
||||
- Batas Minimal: 30%
|
||||
- Batas Maksimal: 80%
|
||||
- Durasi: 10 detik
|
||||
- Mode: Smart
|
||||
|
||||
### Firebase Update
|
||||
|
||||
**Manual Steps (Optional):**
|
||||
```json
|
||||
// Di Firebase Console, tambahkan fields baru:
|
||||
{
|
||||
"kontrol": {
|
||||
"durasi_sensor": 30, // Default 30 detik
|
||||
"mode_sensor": "smart" // Default smart mode
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Auto Update:**
|
||||
- Fields akan otomatis ter-create saat user save config pertama kali
|
||||
|
||||
---
|
||||
|
||||
## 🔮 Future Enhancements
|
||||
|
||||
1. **Hybrid Mode**: Kombinasi smart + fixed (min & max duration)
|
||||
2. **Per-Pot Mode**: Setiap pot bisa punya mode berbeda
|
||||
3. **Schedule Mode**: Smart mode di pagi, fixed mode di sore
|
||||
4. **Learning Mode**: AI adjust durasi berdasarkan history
|
||||
5. **Notification**: Push notif saat watering complete
|
||||
6. **History Graph**: Chart durasi vs hasil kelembaban
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
**Jika ada masalah:**
|
||||
|
||||
1. **Check Console Log:**
|
||||
```
|
||||
🧠 Smart Mode: Target ...
|
||||
⏱️ Fixed Duration Mode: ...
|
||||
✅ Watering completed ...
|
||||
❌ Error: ...
|
||||
```
|
||||
|
||||
2. **Verify Firebase:**
|
||||
- `kontrol/mode_sensor` exist?
|
||||
- `kontrol/durasi_sensor` valid number?
|
||||
- `kontrol/otomatis` = true?
|
||||
|
||||
3. **Reset Config:**
|
||||
- Delete local storage
|
||||
- Delete Firebase `/kontrol` node
|
||||
- Restart app
|
||||
- Setup ulang
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Summary
|
||||
|
||||
### ✅ Completed
|
||||
|
||||
1. **Dual Mode System**
|
||||
- Smart Mode (adaptif dengan sensor)
|
||||
- Fixed Duration (durasi tetap)
|
||||
- UI untuk selection
|
||||
- Backend logic implementation
|
||||
|
||||
2. **Bug Fixes**
|
||||
- Fix logout tiba-tiba
|
||||
- Enhanced error handling
|
||||
- Prevent crashes
|
||||
- Safe navigation
|
||||
|
||||
3. **Code Quality**
|
||||
- Better logging
|
||||
- Comprehensive error handling
|
||||
- Clean separation of modes
|
||||
- Well documented
|
||||
|
||||
### 📈 Improvements
|
||||
|
||||
**Performance:**
|
||||
- Check interval: 10s → 5s (lebih responsif)
|
||||
- Cooldown: 5 min → 2 min (lebih cepat)
|
||||
|
||||
**Reliability:**
|
||||
- Auth state management fixed
|
||||
- Error boundaries added
|
||||
- Graceful degradation
|
||||
|
||||
**User Experience:**
|
||||
- Clear mode selection
|
||||
- Dynamic info text
|
||||
- Better feedback
|
||||
- Predictable behavior
|
||||
|
||||
---
|
||||
|
||||
**Happy Gardening! 🌱**
|
||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 40 KiB |
128
lib/main.dart
|
|
@ -5,11 +5,7 @@ import 'screens/landing_page.dart';
|
|||
import 'screens/login_page.dart';
|
||||
import 'theme/app_color.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await Firebase.initializeApp(
|
||||
options: DefaultFirebaseOptions.currentPlatform,
|
||||
);
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
|
|
@ -24,9 +20,129 @@ class MyApp extends StatelessWidget {
|
|||
theme: ThemeData(
|
||||
primaryColor: AppColor.primary,
|
||||
scaffoldBackgroundColor: AppColor.background,
|
||||
useMaterial3: true,
|
||||
),
|
||||
home: const LandingPage(),
|
||||
home: const FirebaseInitializer(),
|
||||
routes: {'/login': (context) => const LoginPage()},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FirebaseInitializer extends StatefulWidget {
|
||||
const FirebaseInitializer({super.key});
|
||||
|
||||
@override
|
||||
State<FirebaseInitializer> createState() => _FirebaseInitializerState();
|
||||
}
|
||||
|
||||
class _FirebaseInitializerState extends State<FirebaseInitializer> {
|
||||
bool _initialized = false;
|
||||
bool _error = false;
|
||||
String _errorMessage = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeFirebase();
|
||||
}
|
||||
|
||||
Future<void> _initializeFirebase() async {
|
||||
try {
|
||||
await Firebase.initializeApp(
|
||||
options: DefaultFirebaseOptions.currentPlatform,
|
||||
);
|
||||
setState(() {
|
||||
_initialized = true;
|
||||
});
|
||||
print('✅ Firebase initialized successfully');
|
||||
} on FirebaseException catch (e) {
|
||||
// Jika app sudah ada, anggap sudah initialized
|
||||
if (e.code == 'duplicate-app') {
|
||||
setState(() {
|
||||
_initialized = true;
|
||||
});
|
||||
print('✅ Firebase already initialized');
|
||||
} else {
|
||||
setState(() {
|
||||
_error = true;
|
||||
_errorMessage = e.message ?? e.toString();
|
||||
});
|
||||
print('❌ Firebase initialization error: $e');
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_error = true;
|
||||
_errorMessage = e.toString();
|
||||
});
|
||||
print('❌ Firebase initialization error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_error) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.error_outline, size: 80, color: Colors.red),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
'Error Initializing Firebase',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
_errorMessage,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.grey[600]),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_error = false;
|
||||
_initialized = false;
|
||||
});
|
||||
_initializeFirebase();
|
||||
},
|
||||
child: const Text('Retry'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!_initialized) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(AppColor.primary),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Loading...',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: AppColor.textDark.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return const LandingPage();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ class DashboardPage extends StatefulWidget {
|
|||
class _DashboardPageState extends State<DashboardPage> {
|
||||
int _selectedIndex = 0;
|
||||
final _dbService = FirebaseDatabaseService();
|
||||
final _authService = AuthService();
|
||||
bool _isInitialized = false;
|
||||
|
||||
final List<Map<String, dynamic>> _pages = [
|
||||
{'title': 'Dashboard', 'icon': Icons.dashboard},
|
||||
|
|
@ -22,6 +24,37 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||
{'title': 'Histori', 'icon': Icons.history},
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Verify user is logged in on init
|
||||
if (_authService.currentUser == null) {
|
||||
// If no user, redirect immediately
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
Navigator.of(
|
||||
context,
|
||||
).pushNamedAndRemoveUntil('/login', (route) => false);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
_isInitialized = true;
|
||||
|
||||
// Monitor auth state untuk mencegah logout tidak terduga
|
||||
// Hanya trigger jika sudah initialized dan user jadi null
|
||||
_authService.authStateChanges.listen((user) {
|
||||
if (_isInitialized && user == null && mounted) {
|
||||
// User logged out, redirect to login
|
||||
Navigator.of(
|
||||
context,
|
||||
).pushNamedAndRemoveUntil('/login', (route) => false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _handleLogout(BuildContext context) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
|
|
|
|||
|
|
@ -121,12 +121,13 @@ class _ManualControlPageState extends State<ManualControlPage> {
|
|||
// POT switches (5 POTs now)
|
||||
List<bool> _potStatus = [false, false, false, false, false];
|
||||
bool _isLoading = true;
|
||||
bool _hasLocalChanges = false; // Track jika ada perubahan lokal
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadFirebaseState();
|
||||
_listenToFirebaseChanges();
|
||||
// Tidak listen real-time changes agar tidak menimpa perubahan lokal user
|
||||
}
|
||||
|
||||
/// Load initial state from Firebase
|
||||
|
|
@ -145,31 +146,22 @@ class _ManualControlPageState extends State<ManualControlPage> {
|
|||
aktuatorData['mosvet_7'] ?? false,
|
||||
];
|
||||
_isLoading = false;
|
||||
_hasLocalChanges = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error loading Firebase state: $e');
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
/// Listen to real-time Firebase changes
|
||||
void _listenToFirebaseChanges() {
|
||||
_dbService.getAktuatorStream().listen((aktuatorData) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_pompaAir = aktuatorData['mosvet_1'] ?? false;
|
||||
_pompaNutrisi = aktuatorData['mosvet_2'] ?? false;
|
||||
_potStatus = [
|
||||
aktuatorData['mosvet_3'] ?? false,
|
||||
aktuatorData['mosvet_4'] ?? false,
|
||||
aktuatorData['mosvet_5'] ?? false,
|
||||
aktuatorData['mosvet_6'] ?? false,
|
||||
aktuatorData['mosvet_7'] ?? false,
|
||||
];
|
||||
});
|
||||
setState(() => _isLoading = false);
|
||||
// Tampilkan snackbar tapi jangan crash
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Gagal memuat data: $e'),
|
||||
backgroundColor: Colors.orange,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _jalankan() async {
|
||||
|
|
@ -192,6 +184,11 @@ class _ManualControlPageState extends State<ManualControlPage> {
|
|||
pots: _potStatus,
|
||||
);
|
||||
|
||||
// Reset flag perubahan lokal
|
||||
setState(() {
|
||||
_hasLocalChanges = false;
|
||||
});
|
||||
|
||||
// Show which devices are running
|
||||
List<String> activeDevices = [];
|
||||
if (_pompaAir) activeDevices.add('Pompa Air');
|
||||
|
|
@ -210,7 +207,7 @@ class _ManualControlPageState extends State<ManualControlPage> {
|
|||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('✓ Berhasil: ${activeDevices.join(', ')}'),
|
||||
content: Text('✓ Berhasil dijalankan: ${activeDevices.join(', ')}'),
|
||||
backgroundColor: AppColor.primary,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
|
|
@ -236,37 +233,21 @@ class _ManualControlPageState extends State<ManualControlPage> {
|
|||
ControlSwitchCard(
|
||||
title: 'Pompa Air',
|
||||
isActive: _pompaAir,
|
||||
onPressed: () async {
|
||||
setState(() => _pompaAir = !_pompaAir);
|
||||
try {
|
||||
await _dbService.setPompaAir(_pompaAir);
|
||||
} catch (e) {
|
||||
setState(() => _pompaAir = !_pompaAir);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Error: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_pompaAir = !_pompaAir;
|
||||
_hasLocalChanges = true;
|
||||
});
|
||||
},
|
||||
),
|
||||
ControlSwitchCard(
|
||||
title: 'Pompa Nutrisi',
|
||||
isActive: _pompaNutrisi,
|
||||
onPressed: () async {
|
||||
setState(() => _pompaNutrisi = !_pompaNutrisi);
|
||||
try {
|
||||
await _dbService.setPompaPupuk(_pompaNutrisi);
|
||||
} catch (e) {
|
||||
setState(() => _pompaNutrisi = !_pompaNutrisi);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Error: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_pompaNutrisi = !_pompaNutrisi;
|
||||
_hasLocalChanges = true;
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
|
|
@ -291,39 +272,77 @@ class _ManualControlPageState extends State<ManualControlPage> {
|
|||
return ControlSwitchCard(
|
||||
title: 'POT ${index + 1}',
|
||||
isActive: _potStatus[index],
|
||||
onPressed: () async {
|
||||
setState(() => _potStatus[index] = !_potStatus[index]);
|
||||
try {
|
||||
await _dbService.setPot(index + 1, _potStatus[index]);
|
||||
} catch (e) {
|
||||
setState(() => _potStatus[index] = !_potStatus[index]);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Error: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_potStatus[index] = !_potStatus[index];
|
||||
_hasLocalChanges = true;
|
||||
});
|
||||
},
|
||||
);
|
||||
}),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Info text jika ada perubahan lokal
|
||||
if (_hasLocalChanges)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.orange.shade300),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
color: Colors.orange.shade700,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Ada perubahan yang belum disimpan',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.orange.shade700,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: _jalankan,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.primary,
|
||||
backgroundColor:
|
||||
_hasLocalChanges ? AppColor.primary : Colors.grey,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.all(16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Jalankan',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (_hasLocalChanges)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(right: 8),
|
||||
child: Icon(Icons.send, size: 18),
|
||||
),
|
||||
Text(
|
||||
_hasLocalChanges ? 'Jalankan Sekarang' : 'Jalankan',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -25,15 +25,26 @@ class _PotSelectionPageState extends State<PotSelectionPage> {
|
|||
}
|
||||
|
||||
Future<void> _loadModeStatus() async {
|
||||
final isActive =
|
||||
widget.mode == 'Waktu'
|
||||
? await KontrolStorage.loadWaktuModeActive()
|
||||
: await KontrolStorage.loadSensorModeActive();
|
||||
try {
|
||||
final isActive =
|
||||
widget.mode == 'Waktu'
|
||||
? await KontrolStorage.loadWaktuModeActive()
|
||||
: await KontrolStorage.loadSensorModeActive();
|
||||
|
||||
setState(() {
|
||||
_isModeActive = isActive;
|
||||
_isLoading = false;
|
||||
});
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isModeActive = isActive;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error loading mode status: $e');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _toggleMode(bool value) async {
|
||||
|
|
|
|||
|
|
@ -33,7 +33,8 @@ class _SensorConfigPageState extends State<SensorConfigPage> {
|
|||
'batasMinimal': '30',
|
||||
'batasMaksimal': '80',
|
||||
'durasi': '10',
|
||||
'durasiUnit': 'menit',
|
||||
'durasiUnit': 'detik',
|
||||
'mode': 'smart', // 'smart' or 'fixed'
|
||||
};
|
||||
|
||||
@override
|
||||
|
|
@ -54,13 +55,18 @@ class _SensorConfigPageState extends State<SensorConfigPage> {
|
|||
final parts = durasiStr.split(' ');
|
||||
loadedData['durasi'] = parts[0]; // Extract number
|
||||
loadedData['durasiUnit'] =
|
||||
parts.length > 1 ? parts[1] : 'menit'; // Extract unit
|
||||
parts.length > 1 ? parts[1] : 'detik'; // Extract unit
|
||||
} else {
|
||||
// If no unit, ensure durasiUnit exists
|
||||
loadedData['durasiUnit'] = loadedData['durasiUnit'] ?? 'menit';
|
||||
loadedData['durasiUnit'] = loadedData['durasiUnit'] ?? 'detik';
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure mode exists (default to smart)
|
||||
if (!loadedData.containsKey('mode') || loadedData['mode'] == null) {
|
||||
loadedData['mode'] = 'smart';
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_sensorConfig = loadedData;
|
||||
_isLoading = false;
|
||||
|
|
@ -320,6 +326,66 @@ class _SensorConfigPageState extends State<SensorConfigPage> {
|
|||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Mode Selection
|
||||
Text(
|
||||
'Mode Penyiraman',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColor.textDark,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
RadioListTile<String>(
|
||||
title: const Text(
|
||||
'Smart Mode (Adaptif)',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
subtitle: const Text(
|
||||
'Siram hingga batas atas tercapai atau timeout',
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
value: 'smart',
|
||||
groupValue: _sensorConfig['mode'],
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_sensorConfig['mode'] = value!;
|
||||
});
|
||||
},
|
||||
activeColor: AppColor.primary,
|
||||
),
|
||||
Divider(height: 1, color: Colors.grey.shade300),
|
||||
RadioListTile<String>(
|
||||
title: const Text(
|
||||
'Fixed Duration (Durasi Tetap)',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
subtitle: const Text(
|
||||
'Siram selama durasi yang ditentukan',
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
value: 'fixed',
|
||||
groupValue: _sensorConfig['mode'],
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_sensorConfig['mode'] = value!;
|
||||
});
|
||||
},
|
||||
activeColor: AppColor.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Batas Minimal
|
||||
_buildEditableField(
|
||||
label: 'Batas Minimal (%)',
|
||||
|
|
@ -344,14 +410,14 @@ class _SensorConfigPageState extends State<SensorConfigPage> {
|
|||
_buildEditableField(
|
||||
label: 'Durasi Penyiraman',
|
||||
value:
|
||||
'${_sensorConfig['durasi']} ${_sensorConfig['durasiUnit'] ?? 'menit'}',
|
||||
'${_sensorConfig['durasi']} ${_sensorConfig['durasiUnit'] ?? 'detik'}',
|
||||
onTap: () => _editDurationField(),
|
||||
icon: Icons.timer,
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Info box
|
||||
// Info box - Dynamic based on mode
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -364,7 +430,9 @@ class _SensorConfigPageState extends State<SensorConfigPage> {
|
|||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Pompa akan aktif otomatis jika kelembaban di bawah batas minimal',
|
||||
_sensorConfig['mode'] == 'smart'
|
||||
? 'Smart Mode: Pompa akan mati otomatis saat mencapai batas atas atau durasi habis sebagai safety timeout'
|
||||
: 'Fixed Duration: Pompa akan menyiram selama durasi yang ditentukan, tidak peduli nilai sensor',
|
||||
style: TextStyle(fontSize: 12, color: AppColor.textDark),
|
||||
),
|
||||
),
|
||||
|
|
@ -566,13 +634,23 @@ class _SensorConfigPageState extends State<SensorConfigPage> {
|
|||
// Save to local storage
|
||||
await KontrolStorage.saveSensorConfig(widget.potName, _sensorConfig);
|
||||
|
||||
// Convert durasi to seconds for Firebase
|
||||
int durasiSeconds = int.tryParse(_sensorConfig['durasi']) ?? 10;
|
||||
if (_sensorConfig['durasiUnit'] == 'menit') {
|
||||
durasiSeconds *= 60;
|
||||
}
|
||||
|
||||
// Update Firebase dengan konfigurasi sensor
|
||||
await _dbService.setThreshold(
|
||||
batasAtas: int.tryParse(_sensorConfig['batasMaksimal']) ?? 80,
|
||||
batasBawah: int.tryParse(_sensorConfig['batasMinimal']) ?? 30,
|
||||
);
|
||||
|
||||
await _dbService.updateKontrolConfig({'otomatis': _isSensorModeActive});
|
||||
await _dbService.updateKontrolConfig({
|
||||
'otomatis': _isSensorModeActive,
|
||||
'durasi_sensor': durasiSeconds,
|
||||
'mode_sensor': _sensorConfig['mode'] ?? 'smart',
|
||||
});
|
||||
|
||||
setState(() {
|
||||
_isSaved = true;
|
||||
|
|
|
|||
|
|
@ -54,34 +54,45 @@ class _WaktuConfigPageState extends State<WaktuConfigPage> {
|
|||
}
|
||||
|
||||
Future<void> _loadSavedConfig() async {
|
||||
final loadedData = await KontrolStorage.loadWaktuConfig(widget.potName);
|
||||
try {
|
||||
final loadedData = await KontrolStorage.loadWaktuConfig(widget.potName);
|
||||
|
||||
// Convert old format to new format if needed
|
||||
for (var jadwal in loadedData) {
|
||||
if (jadwal['durasi'] is String) {
|
||||
final durasiStr = jadwal['durasi'] as String;
|
||||
// Check if it contains unit (e.g., "10 menit")
|
||||
if (durasiStr.contains(' ')) {
|
||||
final parts = durasiStr.split(' ');
|
||||
jadwal['durasi'] = parts[0]; // Extract number
|
||||
jadwal['durasiUnit'] =
|
||||
parts.length > 1 ? parts[1] : 'menit'; // Extract unit
|
||||
} else {
|
||||
// If no unit, ensure durasiUnit exists
|
||||
jadwal['durasiUnit'] = jadwal['durasiUnit'] ?? 'menit';
|
||||
// Convert old format to new format if needed
|
||||
for (var jadwal in loadedData) {
|
||||
if (jadwal['durasi'] is String) {
|
||||
final durasiStr = jadwal['durasi'] as String;
|
||||
// Check if it contains unit (e.g., "10 menit")
|
||||
if (durasiStr.contains(' ')) {
|
||||
final parts = durasiStr.split(' ');
|
||||
jadwal['durasi'] = parts[0]; // Extract number
|
||||
jadwal['durasiUnit'] =
|
||||
parts.length > 1 ? parts[1] : 'menit'; // Extract unit
|
||||
} else {
|
||||
// If no unit, ensure durasiUnit exists
|
||||
jadwal['durasiUnit'] = jadwal['durasiUnit'] ?? 'menit';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_jadwalPenyiraman = loadedData;
|
||||
_isLoading = false;
|
||||
// Check if data was previously saved
|
||||
_isSaved =
|
||||
loadedData[0]['jamMulai'] != '08:00' ||
|
||||
loadedData[0]['pompaAir'] ||
|
||||
loadedData[0]['pompaPupuk'];
|
||||
});
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_jadwalPenyiraman = loadedData;
|
||||
_isLoading = false;
|
||||
// Check if data was previously saved
|
||||
_isSaved =
|
||||
loadedData[0]['jamMulai'] != '08:00' ||
|
||||
loadedData[0]['pompaAir'] ||
|
||||
loadedData[0]['pompaPupuk'];
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error loading waktu config: $e');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadWaktuModeStatus() async {
|
||||
|
|
|
|||
|
|
@ -172,9 +172,9 @@ class KontrolAutomationService {
|
|||
_isSensorModeActive = true;
|
||||
debugPrint('🌡️ Sensor Mode: Started');
|
||||
|
||||
// Monitor perubahan sensor setiap beberapa detik
|
||||
// Monitor perubahan sensor lebih responsif (5 detik)
|
||||
_sensorCheckTimer = Timer.periodic(
|
||||
const Duration(seconds: 10),
|
||||
const Duration(seconds: 5),
|
||||
(timer) => _checkSensorThreshold(),
|
||||
);
|
||||
|
||||
|
|
@ -209,21 +209,35 @@ class KontrolAutomationService {
|
|||
|
||||
final batasAtas = kontrolConfig['batas_atas'] ?? 100;
|
||||
final batasBawah = kontrolConfig['batas_bawah'] ?? 40;
|
||||
final durasiSensor = kontrolConfig['durasi_sensor'] ?? 60; // dalam detik
|
||||
final modeSensor =
|
||||
kontrolConfig['mode_sensor'] ?? 'smart'; // 'smart' or 'fixed'
|
||||
|
||||
final sensorData = await _dbService.getSensorData();
|
||||
|
||||
// Check setiap pot (soil_1 sampai soil_5)
|
||||
debugPrint(
|
||||
'🌡️ Checking thresholds: batas_bawah=$batasBawah, batas_atas=$batasAtas, mode=$modeSensor, durasi=${durasiSensor}s',
|
||||
);
|
||||
|
||||
for (int i = 1; i <= 5; i++) {
|
||||
final soilKey = 'soil_$i';
|
||||
final soilValue = int.tryParse(sensorData[soilKey] ?? '0') ?? 0;
|
||||
|
||||
debugPrint('🌡️ $soilKey = $soilValue (threshold: $batasBawah)');
|
||||
|
||||
// Jika kelembapan di bawah batas bawah, siram pot tersebut
|
||||
if (soilValue < batasBawah) {
|
||||
debugPrint(
|
||||
'⚠️ $soilKey ($soilValue) < batasBawah ($batasBawah) → Triggering watering for POT $i',
|
||||
);
|
||||
await _waterPotBySensor(
|
||||
potNumber: i,
|
||||
soilValue: soilValue,
|
||||
batasBawah: batasBawah,
|
||||
batasAtas: batasAtas,
|
||||
durasiSeconds: durasiSensor,
|
||||
mode: modeSensor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -244,6 +258,8 @@ class KontrolAutomationService {
|
|||
required int soilValue,
|
||||
required int batasBawah,
|
||||
required int batasAtas,
|
||||
required int durasiSeconds,
|
||||
required String mode,
|
||||
}) async {
|
||||
final potKey = 'pot_$potNumber';
|
||||
|
||||
|
|
@ -253,7 +269,12 @@ class KontrolAutomationService {
|
|||
final lastTime = _lastWateringTime[potKey];
|
||||
if (lastTime != null) {
|
||||
final diff = DateTime.now().difference(lastTime);
|
||||
if (diff.inMinutes < 5) return; // Minimum 5 menit antar penyiraman
|
||||
if (diff.inMinutes < 2) {
|
||||
debugPrint(
|
||||
'⏳ POT $potNumber: Cooldown active (${2 - diff.inMinutes} min remaining)',
|
||||
);
|
||||
return; // Minimum 2 menit antar penyiraman
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -263,40 +284,98 @@ class KontrolAutomationService {
|
|||
debugPrint(
|
||||
'🌡️ Sensor Mode: Watering POT $potNumber (soil: $soilValue < $batasBawah)',
|
||||
);
|
||||
debugPrint('🔧 Mode: $mode, Durasi: ${durasiSeconds}s');
|
||||
|
||||
// Nyalakan pompa air dan valve pot tersebut
|
||||
// pot 1 → mosvet_3, pot 2 → mosvet_4, ... pot 5 → mosvet_7
|
||||
debugPrint(
|
||||
'💧 Starting watering: POT $potNumber (mosvet_${potNumber + 2})',
|
||||
);
|
||||
await _dbService.setPompaAir(true);
|
||||
await _dbService.setPot(potNumber, true);
|
||||
|
||||
// Siram sampai mencapai batas atas atau max 60 detik
|
||||
int elapsedSeconds = 0;
|
||||
const maxDuration = 60;
|
||||
|
||||
while (elapsedSeconds < maxDuration && _isSensorModeActive) {
|
||||
await Future.delayed(const Duration(seconds: 5));
|
||||
elapsedSeconds += 5;
|
||||
if (mode == 'smart') {
|
||||
// SMART MODE: Siram sampai batas atas atau timeout
|
||||
debugPrint(
|
||||
'🧠 Smart Mode: Target soil_$potNumber >= $batasAtas (currently: $soilValue), max ${durasiSeconds}s',
|
||||
);
|
||||
|
||||
// Check sensor lagi
|
||||
final currentData = await _dbService.getSensorData();
|
||||
final currentSoil =
|
||||
int.tryParse(currentData['soil_$potNumber'] ?? '0') ?? 0;
|
||||
while (elapsedSeconds < durasiSeconds && _isSensorModeActive) {
|
||||
await Future.delayed(const Duration(seconds: 5));
|
||||
elapsedSeconds += 5;
|
||||
|
||||
// Check sensor lagi
|
||||
final currentData = await _dbService.getSensorData();
|
||||
final currentSoil =
|
||||
int.tryParse(currentData['soil_$potNumber'] ?? '0') ?? 0;
|
||||
|
||||
if (currentSoil >= batasAtas) {
|
||||
debugPrint(
|
||||
'✅ POT $potNumber reached target: $currentSoil >= $batasAtas',
|
||||
'💧 POT $potNumber watering: ${elapsedSeconds}s, soil_$potNumber: $currentSoil',
|
||||
);
|
||||
break;
|
||||
|
||||
if (currentSoil >= batasAtas) {
|
||||
debugPrint(
|
||||
'✅ POT $potNumber reached target: $currentSoil >= $batasAtas (Smart Mode)',
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (elapsedSeconds >= durasiSeconds) {
|
||||
debugPrint(
|
||||
'⏰ POT $potNumber timeout: ${elapsedSeconds}s (Smart Mode safety)',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// FIXED DURATION MODE: Siram selama durasi tetap
|
||||
debugPrint(
|
||||
'⏱️ Fixed Duration Mode: Watering for exactly ${durasiSeconds}s',
|
||||
);
|
||||
|
||||
while (elapsedSeconds < durasiSeconds && _isSensorModeActive) {
|
||||
await Future.delayed(const Duration(seconds: 5));
|
||||
elapsedSeconds += 5;
|
||||
|
||||
// Check sensor untuk logging saja (tidak break)
|
||||
final currentData = await _dbService.getSensorData();
|
||||
final currentSoil =
|
||||
int.tryParse(currentData['soil_$potNumber'] ?? '0') ?? 0;
|
||||
|
||||
debugPrint(
|
||||
'💧 POT $potNumber watering: ${elapsedSeconds}s/${durasiSeconds}s, soil_$potNumber: $currentSoil',
|
||||
);
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'✅ POT $potNumber fixed duration completed: ${durasiSeconds}s',
|
||||
);
|
||||
}
|
||||
|
||||
// Matikan pompa dan valve
|
||||
await _dbService.setPompaAir(false);
|
||||
await _dbService.setPot(potNumber, false);
|
||||
|
||||
debugPrint('✅ Sensor Mode: Watering completed for POT $potNumber');
|
||||
debugPrint(
|
||||
'✅ Sensor Mode: Watering completed for POT $potNumber (${elapsedSeconds}s, mode: $mode)',
|
||||
);
|
||||
|
||||
// Pastikan flag ter-reset dengan benar
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
_isWateringActive[potKey] = false;
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error watering pot by sensor: $e');
|
||||
|
||||
// Matikan semua untuk safety
|
||||
try {
|
||||
await _dbService.setPompaAir(false);
|
||||
await _dbService.setPot(potNumber, false);
|
||||
} catch (cleanupError) {
|
||||
debugPrint('❌ Error during cleanup: $cleanupError');
|
||||
}
|
||||
|
||||
// Reset flag
|
||||
_isWateringActive[potKey] = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
88
pubspec.lock
|
|
@ -9,6 +9,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.59"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.7"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -41,6 +49,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: checked_yaml
|
||||
sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cli_util
|
||||
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.2"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -57,6 +81,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -166,6 +198,14 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_launcher_icons:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_launcher_icons
|
||||
sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.13.1"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
|
|
@ -248,6 +288,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
image:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.7.2"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.9.0"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -344,6 +400,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -360,6 +424,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
posix:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: posix
|
||||
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.3"
|
||||
rename:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -517,6 +589,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.5.0"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.7.2 <4.0.0"
|
||||
flutter: ">=3.29.0"
|
||||
|
|
|
|||
12
pubspec.yaml
|
|
@ -52,6 +52,9 @@ dev_dependencies:
|
|||
# rules and activating additional ones.
|
||||
flutter_lints: ^5.0.0
|
||||
|
||||
# Package untuk auto-generate app icons
|
||||
flutter_launcher_icons: ^0.13.1
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
|
|
@ -67,6 +70,15 @@ flutter:
|
|||
assets:
|
||||
- assets/images/
|
||||
|
||||
# Konfigurasi untuk auto-generate app launcher icons
|
||||
flutter_launcher_icons:
|
||||
android: true
|
||||
ios: false
|
||||
image_path: "assets/images/logo_apsgo.png"
|
||||
# Uncomment jika ingin adaptive icon (Android 8.0+)
|
||||
# adaptive_icon_background: "#4CAF50"
|
||||
# adaptive_icon_foreground: "assets/images/logo_apsgo.png"
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/to/resolution-aware-images
|
||||
|
||||
|
|
|
|||