Upload Tugas Akhir TKK
|
|
@ -1,5 +1,9 @@
|
||||||
{
|
{
|
||||||
"chat.tools.terminal.autoApprove": {
|
"chat.tools.terminal.autoApprove": {
|
||||||
"git init": true
|
"git init": true,
|
||||||
}
|
"adb": true,
|
||||||
|
"flutter": true,
|
||||||
|
"type": true
|
||||||
|
},
|
||||||
|
"cmake.sourceDirectory": "F:/irrigo/linux"
|
||||||
}
|
}
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
# 🐛 Bug Fixes & Improvements Report - ApsGo
|
|
||||||
|
|
||||||
Laporan lengkap bug yang ditemukan dan perbaikan yang telah dilakukan.
|
|
||||||
|
|
||||||
## 📊 Summary
|
|
||||||
|
|
||||||
- **Total Issues Found**: 9
|
|
||||||
- **Critical**: 4 ✅ Fixed
|
|
||||||
- **Medium**: 3 ✅ Fixed
|
|
||||||
- **Minor**: 2 ⚠️ Noted
|
|
||||||
- **New Features Added**: 3
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔴 CRITICAL BUGS (Fixed)
|
|
||||||
|
|
||||||
### Bug #1: Memory Leak - StreamSubscription Tidak Di-dispose
|
|
||||||
|
|
||||||
**Severity**: 🔴 Critical
|
|
||||||
**Lokasi**: `lib/screens/dashboard_page.dart`
|
|
||||||
|
|
||||||
**Deskripsi:**
|
|
||||||
- `_authService.authStateChanges.listen()` tidak pernah di-cancel
|
|
||||||
- Memory leak setiap navigation
|
|
||||||
|
|
||||||
**Fix Applied:**
|
|
||||||
```dart
|
|
||||||
class _DashboardPageState extends State<DashboardPage> {
|
|
||||||
StreamSubscription? _authSubscription;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_authSubscription?.cancel(); // ✅ Cleanup
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Bug #2: Background Services Tidak Berhenti
|
|
||||||
|
|
||||||
**Severity**: 🔴 Critical
|
|
||||||
**Lokasi**: Singleton services
|
|
||||||
|
|
||||||
**Fix Applied:**
|
|
||||||
- Implement AppLifecycleListener di `main.dart`
|
|
||||||
- Services auto-stop ketika app paused/terminated
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Bug #3: Race Condition di Multi-Pot Watering
|
|
||||||
|
|
||||||
**Severity**: 🔴 Critical
|
|
||||||
**Solution**: Railway Worker dengan BullMQ (concurrency: 1)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Bug #4: No Firebase Connection Check
|
|
||||||
|
|
||||||
**Severity**: 🔴 Critical
|
|
||||||
**Fix**: Created `ConnectionMonitorService`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🟡 MEDIUM BUGS (Fixed)
|
|
||||||
|
|
||||||
### Bug #5-7: Error Handling, Time Comparison, Magic Numbers
|
|
||||||
|
|
||||||
**Fixes:**
|
|
||||||
- Improved error handling dengan user feedback
|
|
||||||
- Proper time formatting di Railway Worker
|
|
||||||
- Created `automation_constants.dart` untuk centralized config
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🟢 MINOR BUGS (Noted)
|
|
||||||
|
|
||||||
### Bug #8-9: WillPopScope Deprecated, No Input Validation
|
|
||||||
|
|
||||||
**Status**: Low priority, functionality works
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 NEW FEATURES
|
|
||||||
|
|
||||||
### 1. Railway Worker (Complete Solution)
|
|
||||||
- ✅ 24/7 automation bahkan saat HP mati
|
|
||||||
- ✅ Redis queue untuk reliable task management
|
|
||||||
- ✅ Production-grade architecture
|
|
||||||
|
|
||||||
### 2. Connection Monitoring
|
|
||||||
- ✅ Real-time Firebase status
|
|
||||||
- ✅ Better error messages
|
|
||||||
|
|
||||||
### 3. Constants & Validation
|
|
||||||
- ✅ Centralized configuration
|
|
||||||
- ✅ Validation helpers
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Documentation
|
|
||||||
|
|
||||||
1. ✅ `DEPLOYMENT_GUIDE.md` - Step-by-step Railway deployment
|
|
||||||
2. ✅ `railway-worker/README.md` - Worker documentation
|
|
||||||
3. ✅ This bug report
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Result
|
|
||||||
|
|
||||||
**Before:** Not production-ready (memory leaks, race conditions, no offline support)
|
|
||||||
**After:** Production-ready dengan reliable 24/7 automation
|
|
||||||
|
|
||||||
**Next Step:** Deploy Railway Worker following DEPLOYMENT_GUIDE.md
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Last Updated:** February 10, 2026
|
|
||||||
|
|
@ -1,243 +0,0 @@
|
||||||
# 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! 🌱
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
# 🧪 **DEBUG MODE TESTING GUIDE**
|
|
||||||
|
|
||||||
## **Langkah Testing untuk Diagnosa Log Issue**
|
|
||||||
|
|
||||||
### **STEP 1: Test Flutter App dengan Debug Console**
|
|
||||||
|
|
||||||
1. **Buka Flutter App di Debug Mode**
|
|
||||||
```bash
|
|
||||||
cd f:\ApsGo\ApsGo
|
|
||||||
flutter run --debug
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Lihat Debug Console Output**
|
|
||||||
- Buka VS Code Debug Console atau Terminal
|
|
||||||
- Perhatikan log output saat save configuration
|
|
||||||
|
|
||||||
### **STEP 2: Test Waktu Mode Setting**
|
|
||||||
|
|
||||||
1. **Di Flutter App:**
|
|
||||||
- Buka **Kontrol** → **Waktu Mode**
|
|
||||||
- Set jadwal 2-3 menit dari sekarang
|
|
||||||
- **PASTIKAN toggle "Waktu Mode" AKTIF** ✅
|
|
||||||
- Set durasi: 10 detik
|
|
||||||
- Klik **SIMPAN**
|
|
||||||
|
|
||||||
2. **Yang Harus Muncul di Console:**
|
|
||||||
```
|
|
||||||
🔧 [DEBUG] Starting save configuration...
|
|
||||||
✅ [DEBUG] Local storage saved
|
|
||||||
📊 [DEBUG] Config to Firebase: {waktu_1: 14:30, waktu_2: 18:00, durasi_1: 10, durasi_2: 600, waktu: true}
|
|
||||||
🔥 [DEBUG] Updating Firebase kontrol config: ...
|
|
||||||
✅ [DEBUG] Firebase config saved successfully
|
|
||||||
🚀 [DEBUG] Starting Waktu Mode automation...
|
|
||||||
✅ [DEBUG] Waktu Mode started
|
|
||||||
```
|
|
||||||
|
|
||||||
### **STEP 3: Cek Railway Logs**
|
|
||||||
|
|
||||||
1. **Buka Railway Dashboard**
|
|
||||||
- Pergi ke: https://railway.app/dashboard
|
|
||||||
- Pilih project: **myreppril**
|
|
||||||
- Klik **Deploy Logs**
|
|
||||||
|
|
||||||
2. **Yang Harus Muncul Setiap 30 Detik:**
|
|
||||||
```
|
|
||||||
⏰ [DEBUG] Checking scheduled watering... 2026-02-10T...
|
|
||||||
📊 [DEBUG] Retrieved kontrol config: {
|
|
||||||
"waktu_1": "14:30",
|
|
||||||
"waktu_2": "18:00",
|
|
||||||
"durasi_1": 10,
|
|
||||||
"durasi_2": 600,
|
|
||||||
"waktu": true
|
|
||||||
}
|
|
||||||
🕐 [DEBUG] Current time: 14:28, Date: 2026-02-10
|
|
||||||
📅 [DEBUG] Scheduled times - Jadwal 1: 14:30, Jadwal 2: 18:00
|
|
||||||
⏰ [DEBUG] Jadwal 1 time mismatch - Expected: 14:30, Current: 14:28
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Saat Jadwal Trigger (di waktu yang tepat):**
|
|
||||||
```
|
|
||||||
🕐 JADWAL 1 TRIGGERED: 14:30
|
|
||||||
📌 Added to queue: jadwal_1_2026-02-10_14:30
|
|
||||||
|
|
||||||
💧 Processing Job: jadwal_1_2026-02-10_14:30
|
|
||||||
Type: waktu_jadwal_1
|
|
||||||
Pots: [1, 2, 3, 4, 5]
|
|
||||||
Duration: 10s
|
|
||||||
🔛 Turning ON: mosvet_1, mosvet_2, mosvet_3, mosvet_4...
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚨 **KEMUNGKINAN MASALAH & SOLUSI**
|
|
||||||
|
|
||||||
### **Jika Flutter Debug Log Tidak Muncul:**
|
|
||||||
- ❌ **Firebase connection issue**
|
|
||||||
- ✅ **Cek internet connection**
|
|
||||||
- ✅ **Restart app dengan `flutter clean && flutter run`**
|
|
||||||
|
|
||||||
### **Jika Railway Log Tidak Update:**
|
|
||||||
- ❌ **Environment variables salah**
|
|
||||||
- ❌ **Firebase project tidak match**
|
|
||||||
- ✅ **Re-deploy Railway worker**
|
|
||||||
|
|
||||||
### **Jika Config Tersimpan tapi Worker Tidak Jalan:**
|
|
||||||
- ❌ **Toggle "Waktu Mode" tidak aktif**
|
|
||||||
- ❌ **Jadwal time format salah**
|
|
||||||
- ✅ **Pastikan format waktu "HH:mm" (contoh: "14:30")**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📱 **QUICK FIX CHECKLIST**
|
|
||||||
|
|
||||||
- [ ] Toggle "Waktu Mode" sudah **AKTIF** ✅
|
|
||||||
- [ ] Format waktu sudah benar (HH:mm)
|
|
||||||
- [ ] Flutter app berjalan dalam debug mode
|
|
||||||
- [ ] Railway worker masih running (cek status online)
|
|
||||||
- [ ] Firebase project sama (project-ta-951b4)
|
|
||||||
- [ ] Internet connection stabil
|
|
||||||
|
|
@ -1,479 +0,0 @@
|
||||||
# 🚀 Railway Deployment Guide - ApsGo Worker
|
|
||||||
|
|
||||||
Panduan lengkap untuk deploy Railway Worker ke cloud Railway.app dan mengintegrasikannya dengan aplikasi Flutter ApsGo.
|
|
||||||
|
|
||||||
## 📋 Prerequisites
|
|
||||||
|
|
||||||
Sebelum memulai, pastikan Anda sudah punya:
|
|
||||||
|
|
||||||
1. ✅ Akun Railway.app (gratis) - [Daftar di sini](https://railway.app)
|
|
||||||
2. ✅ Firebase project dengan Realtime Database
|
|
||||||
3. ✅ Git installed di komputer
|
|
||||||
4. ✅ Node.js installed (v18+) untuk testing lokal (optional)
|
|
||||||
|
|
||||||
## 📁 Struktur Project
|
|
||||||
|
|
||||||
```
|
|
||||||
ApsGo/
|
|
||||||
├── lib/ # Flutter app
|
|
||||||
├── android/
|
|
||||||
├── railway-worker/ # ← Worker yang akan di-deploy
|
|
||||||
│ ├── worker.js # Main worker code
|
|
||||||
│ ├── package.json
|
|
||||||
│ ├── railway.json # Railway config
|
|
||||||
│ ├── .env.example # Environment variables template
|
|
||||||
│ └── README.md
|
|
||||||
└── README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 STEP 1: Setup Firebase Service Account
|
|
||||||
|
|
||||||
Worker butuh Firebase Admin SDK untuk akses database. Ikuti langkah berikut:
|
|
||||||
|
|
||||||
### 1.1. Download Service Account Key
|
|
||||||
|
|
||||||
1. Buka [Firebase Console](https://console.firebase.google.com)
|
|
||||||
2. Pilih project ApsGo Anda
|
|
||||||
3. Klik ⚙️ **Project Settings** (di sidebar kiri)
|
|
||||||
4. Tab **Service Accounts**
|
|
||||||
5. Klik **Generate New Private Key**
|
|
||||||
6. Download file JSON (jangan share file ini ke siapapun!)
|
|
||||||
|
|
||||||
### 1.2. Extract Credentials dari JSON
|
|
||||||
|
|
||||||
Buka file JSON yang di-download, cari 3 informasi ini:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "service_account",
|
|
||||||
"project_id": "your-project-id-123", // ← SIMPAN INI
|
|
||||||
"private_key_id": "...",
|
|
||||||
"private_key": "-----BEGIN PRIVATE KEY-----\n....\n-----END PRIVATE KEY-----\n", // ← SIMPAN INI
|
|
||||||
"client_email": "firebase-adminsdk-xxxxx@your-project-id.iam.gserviceaccount.com", // ← SIMPAN INI
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Simpan 3 nilai ini, akan digunakan di Step 3.
|
|
||||||
|
|
||||||
### 1.3. Get Firebase Database URL
|
|
||||||
|
|
||||||
Format: `https://YOUR-PROJECT-ID-default-rtdb.firebaseio.com`
|
|
||||||
|
|
||||||
Cek di Firebase Console → Realtime Database → Copy URL di bagian atas.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚂 STEP 2: Setup Railway Project
|
|
||||||
|
|
||||||
### 2.1. Login ke Railway
|
|
||||||
|
|
||||||
1. Buka [railway.app](https://railway.app)
|
|
||||||
2. Klik **Login** → Login dengan GitHub (recommended)
|
|
||||||
3. Authorize Railway untuk akses GitHub Anda
|
|
||||||
|
|
||||||
### 2.2. Create New Project
|
|
||||||
|
|
||||||
1. Klik **New Project**
|
|
||||||
2. Pilih **Deploy from GitHub repo**
|
|
||||||
3. Jika belum connect GitHub:
|
|
||||||
- Klik **Configure GitHub App**
|
|
||||||
- Authorize Railway
|
|
||||||
- Pilih repository **ApsGo**
|
|
||||||
|
|
||||||
4. Railway akan scan repository Anda
|
|
||||||
|
|
||||||
### 2.3. Setup Worker Service
|
|
||||||
|
|
||||||
1. Railway detect root project (Flutter), kita perlu custom path
|
|
||||||
2. Klik **Settings** (di sidebar kiri service)
|
|
||||||
3. Scroll ke **Build & Deploy**
|
|
||||||
4. Set **Root Directory**: `railway-worker`
|
|
||||||
5. **Build Command**: `npm install`
|
|
||||||
6. **Start Command**: `npm start`
|
|
||||||
7. Klik **Save Changes**
|
|
||||||
|
|
||||||
### 2.4. Add Redis Service
|
|
||||||
|
|
||||||
Worker butuh Redis untuk queue system:
|
|
||||||
|
|
||||||
1. Klik **New** (di sidebar project)
|
|
||||||
2. Pilih **Database** → **Add Redis**
|
|
||||||
3. Railway akan auto-provision Redis
|
|
||||||
4. Redis akan otomatis terhubung ke worker (melalui private network)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔐 STEP 3: Configure Environment Variables
|
|
||||||
|
|
||||||
### 3.1. Add Variables di Railway
|
|
||||||
|
|
||||||
1. Klik service **worker** (bukan Redis)
|
|
||||||
2. Klik tab **Variables**
|
|
||||||
3. Tambahkan variables berikut:
|
|
||||||
|
|
||||||
| Variable Name | Value | Cara Isi |
|
|
||||||
|--------------|--------|----------|
|
|
||||||
| `FIREBASE_PROJECT_ID` | your-project-id-123 | Dari Step 1.2 |
|
|
||||||
| `FIREBASE_CLIENT_EMAIL` | firebase-adminsdk-xxx@... | Dari Step 1.2 |
|
|
||||||
| `FIREBASE_PRIVATE_KEY` | "-----BEGIN PRIVATE KEY..." | Copy SELURUH private_key dari JSON, pastikan ada quotes |
|
|
||||||
| `FIREBASE_DATABASE_URL` | https://xxx.firebaseio.com | Dari Step 1.3 |
|
|
||||||
|
|
||||||
### 3.2. Redis Variables (Auto)
|
|
||||||
|
|
||||||
Railway akan auto-inject Redis variables:
|
|
||||||
- `REDIS_HOST`: `redis.railway.internal` (auto)
|
|
||||||
- `REDIS_PORT`: `6379` (auto)
|
|
||||||
- `REDIS_PASSWORD`: (auto-generated)
|
|
||||||
|
|
||||||
Atau Railway bisa provide single variable:
|
|
||||||
- `REDIS_URL`: `redis://default:password@redis.railway.internal:6379`
|
|
||||||
|
|
||||||
Worker code sudah handle both formats.
|
|
||||||
|
|
||||||
### 3.3. IMPORTANT: Format Private Key
|
|
||||||
|
|
||||||
Private key HARUS include newlines (`\n`). Contoh:
|
|
||||||
|
|
||||||
```
|
|
||||||
"-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBg...\n...akhir key...\n-----END PRIVATE KEY-----\n"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Jika error "Invalid key":**
|
|
||||||
1. Pastikan ada quotes di awal dan akhir
|
|
||||||
2. Pastikan ada `\n` (bukan enter sesungguhnya)
|
|
||||||
3. Copy paste langsung dari JSON file
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 STEP 4: Deploy!
|
|
||||||
|
|
||||||
### 4.1. Trigger Deployment
|
|
||||||
|
|
||||||
Setelah environment variables di-set:
|
|
||||||
|
|
||||||
1. Railway akan auto-deploy
|
|
||||||
2. Atau klik **Deploy** → **Redeploy**
|
|
||||||
|
|
||||||
### 4.2. Monitor Deployment
|
|
||||||
|
|
||||||
1. Klik tab **Deployments**
|
|
||||||
2. Lihat build logs:
|
|
||||||
- ✅ `npm install` berhasil
|
|
||||||
- ✅ `npm start` running
|
|
||||||
- ✅ Firebase initialized
|
|
||||||
- ✅ Redis connected
|
|
||||||
|
|
||||||
3. Jika error, check **Logs** tab
|
|
||||||
|
|
||||||
### 4.3. Check Worker Logs
|
|
||||||
|
|
||||||
Setelah deploy sukses, check logs:
|
|
||||||
|
|
||||||
```
|
|
||||||
🚀 Starting ApsGo Railway Worker...
|
|
||||||
📡 Firebase Project: your-project-id
|
|
||||||
📦 Redis: redis.railway.internal:6379
|
|
||||||
✅ Firebase Admin initialized
|
|
||||||
✅ Redis connected
|
|
||||||
✅ Waktu Mode scheduler started (check every 30s)
|
|
||||||
✅ Sensor Mode monitoring started
|
|
||||||
✅ Auto history logging started (every 10 minutes)
|
|
||||||
✅ History cleanup scheduled (daily at 2 AM)
|
|
||||||
✨ ApsGo Railway Worker is running!
|
|
||||||
🎯 Worker is ready to process jobs...
|
|
||||||
```
|
|
||||||
|
|
||||||
Jika lihat log seperti ini, **SUKSES!** ✅
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 STEP 5: Testing Worker
|
|
||||||
|
|
||||||
### 5.1. Test Waktu Mode
|
|
||||||
|
|
||||||
1. Buka Flutter app ApsGo
|
|
||||||
2. Masuk ke **Kontrol** → **Waktu Mode**
|
|
||||||
3. Set jadwal 1 menit dari sekarang (misal: sekarang 14:05, set 14:06)
|
|
||||||
4. Set durasi: 10 detik
|
|
||||||
5. Save configuration
|
|
||||||
|
|
||||||
**Di Railway Logs, tunggu 1 menit:**
|
|
||||||
```
|
|
||||||
🕐 JADWAL 1 TRIGGERED: 14:06
|
|
||||||
📌 Added to queue: jadwal_1_2026-02-10_14:06
|
|
||||||
|
|
||||||
💧 Processing Job: jadwal_1_2026-02-10_14:06
|
|
||||||
Type: waktu_jadwal_1
|
|
||||||
Pots: [1, 2, 3, 4, 5]
|
|
||||||
Duration: 10s
|
|
||||||
🔛 Turning ON: mosvet_1, mosvet_2, mosvet_3, mosvet_4...
|
|
||||||
⏳ 10s remaining...
|
|
||||||
🔴 Turning OFF
|
|
||||||
📊 History logged: 2026-02-10 14:06
|
|
||||||
✅ Job completed successfully
|
|
||||||
```
|
|
||||||
|
|
||||||
**Check di Flutter app:**
|
|
||||||
- Pompa dan valve nyala selama 10 detik
|
|
||||||
- Histori tercatat di halaman Histori
|
|
||||||
|
|
||||||
### 5.2. Test Sensor Mode
|
|
||||||
|
|
||||||
1. Buka **Kontrol** → **Sensor Mode**
|
|
||||||
2. Set batas_bawah: 50%
|
|
||||||
3. Enable otomatis
|
|
||||||
4. Set durasi_sensor: 15 detik
|
|
||||||
|
|
||||||
**Simulasi sensor:**
|
|
||||||
- Update Firebase Realtime DB manual:
|
|
||||||
- Path: `/data/soil_1`
|
|
||||||
- Value: `30` (di bawah 50%)
|
|
||||||
|
|
||||||
**Di Railway Logs:**
|
|
||||||
```
|
|
||||||
🌡️ SENSOR TRIGGERED: POT 1
|
|
||||||
Soil moisture: 30% < 50%
|
|
||||||
Mode: fixed, Duration: 15s
|
|
||||||
📌 Added to queue: sensor-pot-1-1707562800000
|
|
||||||
|
|
||||||
💧 Processing Job: sensor-pot-1-1707562800000
|
|
||||||
Type: sensor_threshold
|
|
||||||
Pots: [1]
|
|
||||||
Duration: 15s
|
|
||||||
🔛 Turning ON: mosvet_1, mosvet_3
|
|
||||||
⏳ 15s remaining...
|
|
||||||
🔴 Turning OFF
|
|
||||||
✅ Job completed successfully
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.3. Test Auto History Logging
|
|
||||||
|
|
||||||
**Check setiap 10 menit:**
|
|
||||||
```
|
|
||||||
📊 Auto-logged sensor data: 14:10
|
|
||||||
📊 Auto-logged sensor data: 14:20
|
|
||||||
📊 Auto-logged sensor data: 14:30
|
|
||||||
```
|
|
||||||
|
|
||||||
Verify di Firebase Console → Realtime Database → `/history`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 STEP 6: Monitoring & Maintenance
|
|
||||||
|
|
||||||
### 6.1. Health Check
|
|
||||||
|
|
||||||
Worker auto health check setiap 5 menit:
|
|
||||||
|
|
||||||
```
|
|
||||||
💚 HEALTH CHECK:
|
|
||||||
Firebase: ✅ Connected
|
|
||||||
Redis: ✅ Connected
|
|
||||||
Queue: 0 active, 0 waiting
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.2. View Metrics di Railway
|
|
||||||
|
|
||||||
1. Klik service **worker**
|
|
||||||
2. Tab **Metrics**:
|
|
||||||
- CPU Usage
|
|
||||||
- Memory Usage
|
|
||||||
- Network Traffic
|
|
||||||
|
|
||||||
3. Tab **Logs**:
|
|
||||||
- Real-time logs
|
|
||||||
- Filter by time/keyword
|
|
||||||
|
|
||||||
### 6.3. Setup Alerts (Optional)
|
|
||||||
|
|
||||||
1. Klik **Settings** → **Notifications**
|
|
||||||
2. Connect Slack/Discord/Email
|
|
||||||
3. Get notified jika service down
|
|
||||||
|
|
||||||
### 6.4. Restart Worker
|
|
||||||
|
|
||||||
**Manual Restart:**
|
|
||||||
1. Tab **Deployments**
|
|
||||||
2. Klik **...** (three dots)
|
|
||||||
3. **Redeploy**
|
|
||||||
|
|
||||||
**Auto Restart:**
|
|
||||||
- Railway auto-restart jika crash (configured in `railway.json`)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💰 STEP 7: Billing & Cost Management
|
|
||||||
|
|
||||||
### 7.1. Railway Free Tier
|
|
||||||
|
|
||||||
**Limits:**
|
|
||||||
- $5 credit per month (gratis)
|
|
||||||
- Cukup untuk small IoT project
|
|
||||||
- Auto-sleep jika idle (configurable)
|
|
||||||
|
|
||||||
**Typical Usage (ApsGo Worker):**
|
|
||||||
- Worker: ~$2-3/month
|
|
||||||
- Redis: ~$1-2/month
|
|
||||||
- **Total**: ~$3-5/month (masih dalam free tier!)
|
|
||||||
|
|
||||||
### 7.2. Upgrade to Hobby Plan (Optional)
|
|
||||||
|
|
||||||
Jika butuh lebih:
|
|
||||||
- $5/month
|
|
||||||
- More resources
|
|
||||||
- No sleep
|
|
||||||
- Priority support
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 TROUBLESHOOTING
|
|
||||||
|
|
||||||
### Problem: "Firebase initialization failed"
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
1. Check `FIREBASE_PRIVATE_KEY` format
|
|
||||||
2. Pastikan ada quotes dan `\n`
|
|
||||||
3. Verify project ID benar
|
|
||||||
4. Check Firebase rules allow admin access
|
|
||||||
|
|
||||||
### Problem: "Redis connection error"
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
1. Pastikan Redis service aktif di Railway
|
|
||||||
2. Check Redis variables auto-injected
|
|
||||||
3. Restart worker service
|
|
||||||
|
|
||||||
### Problem: "Worker tidak trigger jadwal"
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
1. Check timezone: Worker use UTC
|
|
||||||
- Convert waktu lokal ke UTC
|
|
||||||
- Atau set `TZ` env variable: `Asia/Jakarta`
|
|
||||||
2. Check Firebase `/kontrol/waktu` = `true`
|
|
||||||
3. Check logs untuk error
|
|
||||||
|
|
||||||
### Problem: "Memory leak / High CPU"
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
1. Check logs untuk infinite loop
|
|
||||||
2. Verify cooldown working
|
|
||||||
3. Check Redis queue size: `await queue.getJobCounts()`
|
|
||||||
|
|
||||||
### Problem: "Too many Firebase reads"
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
1. Worker optimize untuk minimize reads
|
|
||||||
2. Check sensor mode `otomatis` tidak stuck ON
|
|
||||||
3. Consider increase check interval di code
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔄 STEP 8: Update Worker Code
|
|
||||||
|
|
||||||
### 8.1. Update via Git
|
|
||||||
|
|
||||||
1. Edit code di `railway-worker/worker.js`
|
|
||||||
2. Commit & push ke GitHub:
|
|
||||||
```bash
|
|
||||||
git add .
|
|
||||||
git commit -m "Update worker logic"
|
|
||||||
git push
|
|
||||||
```
|
|
||||||
3. Railway auto-detect push → auto-deploy
|
|
||||||
|
|
||||||
### 8.2. Rollback Deployment
|
|
||||||
|
|
||||||
Jika ada bug setelah update:
|
|
||||||
|
|
||||||
1. Tab **Deployments**
|
|
||||||
2. Pilih deployment sebelumnya yang sukses
|
|
||||||
3. Klik **...** → **Redeploy**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📱 STEP 9: Integrate dengan Flutter App
|
|
||||||
|
|
||||||
### 9.1. Update App Logic
|
|
||||||
|
|
||||||
**PENTING:** Sekarang scheduling di-handle oleh Railway Worker, bukan Flutter app.
|
|
||||||
|
|
||||||
**Rekomendasi perubahan:**
|
|
||||||
1. **Disable** local automation services ketika deploy ke production
|
|
||||||
2. Keep local services hanya untuk **testing/development**
|
|
||||||
3. Tambahkan indicator di UI: "Server-side automation active"
|
|
||||||
|
|
||||||
### 9.2. Add Status Indicator (Optional)
|
|
||||||
|
|
||||||
Tambahkan di Flutter app untuk show worker status:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
// Check worker last activity
|
|
||||||
final historyRef = FirebaseDatabase.instance.ref('history');
|
|
||||||
final lastLog = await historyRef.limitToLast(1).once();
|
|
||||||
|
|
||||||
if (lastLog.snapshot.value != null) {
|
|
||||||
// Worker active (logged dalam 10 menit terakhir)
|
|
||||||
showWorkerActiveIcon();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎉 STEP 10: Done!
|
|
||||||
|
|
||||||
Congratulations! Worker Anda sekarang berjalan 24/7 di cloud.
|
|
||||||
|
|
||||||
**Apa yang terjadi sekarang:**
|
|
||||||
- ✅ Jadwal waktu berjalan otomatis (meskipun HP mati)
|
|
||||||
- ✅ Sensor monitoring aktif terus
|
|
||||||
- ✅ History auto-logged setiap 10 menit
|
|
||||||
- ✅ Old history auto-cleanup setiap hari
|
|
||||||
- ✅ Reliable, scalable, production-ready
|
|
||||||
|
|
||||||
**Next Steps:**
|
|
||||||
1. Monitor logs selama 24 jam pertama
|
|
||||||
2. Fine-tune configuration (durasi, threshold, dll)
|
|
||||||
3. Setup alerts untuk critical errors
|
|
||||||
4. Consider backup strategy untuk history data
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Additional Resources
|
|
||||||
|
|
||||||
- [Railway Documentation](https://docs.railway.app)
|
|
||||||
- [BullMQ Documentation](https://docs.bullmq.io)
|
|
||||||
- [Firebase Admin SDK](https://firebase.google.com/docs/admin/setup)
|
|
||||||
- [Node.js Best Practices](https://github.com/goldbergyoni/nodebestpractices)
|
|
||||||
|
|
||||||
## 🆘 Need Help?
|
|
||||||
|
|
||||||
- Railway Discord: [discord.gg/railway](https://discord.gg/railway)
|
|
||||||
- Firebase Support: [firebase.google.com/support](https://firebase.google.com/support)
|
|
||||||
- ApsGo Issues: Create issue di GitHub repository
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 Checklist Deployment
|
|
||||||
|
|
||||||
Copy checklist ini untuk reference:
|
|
||||||
|
|
||||||
- [ ] Firebase Service Account downloaded
|
|
||||||
- [ ] Railway account created
|
|
||||||
- [ ] GitHub repository connected
|
|
||||||
- [ ] Redis service added
|
|
||||||
- [ ] Environment variables configured
|
|
||||||
- [ ] Worker deployed successfully
|
|
||||||
- [ ] Logs showing "Worker is running"
|
|
||||||
- [ ] Waktu Mode tested
|
|
||||||
- [ ] Sensor Mode tested
|
|
||||||
- [ ] Auto logging verified
|
|
||||||
- [ ] Monitoring setup
|
|
||||||
- [ ] Flutter app updated (optional)
|
|
||||||
- [ ] Documentation completed
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**🎊 Happy Automating! Selamat menggunakan ApsGo dengan Railway Worker!**
|
|
||||||
|
|
@ -1,131 +0,0 @@
|
||||||
# 🚀 Cara Deploy Optimasi Firebase ke Railway
|
|
||||||
|
|
||||||
## Perubahan yang Dibuat
|
|
||||||
|
|
||||||
### Masalah Sebelumnya
|
|
||||||
- SDK timeout setiap 60 detik (delay 10s per check)
|
|
||||||
- REST API fallback berfungsi tapi lama
|
|
||||||
- Tidak ada smart caching/skipping untuk SDK yang gagal
|
|
||||||
|
|
||||||
### Solusi yang Diimplementasikan
|
|
||||||
1. ⚡ **Timeout lebih cepat**: 10s → 5s (SDK) & 8s (REST)
|
|
||||||
2. 🧠 **Smart fallback**: Skip SDK setelah 3x gagal berturut
|
|
||||||
3. 📊 **API Statistics**: Track SDK vs REST usage
|
|
||||||
4. 🔧 **Konsistensi**: Semua fungsi pakai timeout yang sama
|
|
||||||
|
|
||||||
## Cara Deploy ke Railway
|
|
||||||
|
|
||||||
### Opsi 1: Git Push (Recommended)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Add & commit changes
|
|
||||||
git add railway-worker/worker.js
|
|
||||||
git commit -m "fix: Optimize Firebase connection with smart fallback"
|
|
||||||
|
|
||||||
# 2. Push ke repository
|
|
||||||
git push origin main
|
|
||||||
|
|
||||||
# 3. Railway akan auto-deploy (jika connected)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Opsi 2: Manual Deploy via Railway CLI
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Install Railway CLI (jika belum)
|
|
||||||
npm install -g @railway/cli
|
|
||||||
|
|
||||||
# 2. Login ke Railway
|
|
||||||
railway login
|
|
||||||
|
|
||||||
# 3. Link project (jika belum)
|
|
||||||
railway link
|
|
||||||
|
|
||||||
# 4. Deploy
|
|
||||||
railway up
|
|
||||||
```
|
|
||||||
|
|
||||||
### Opsi 3: Deploy via Railway Dashboard
|
|
||||||
|
|
||||||
1. Buka [Railway Dashboard](https://railway.app/dashboard)
|
|
||||||
2. Pilih project **ApsGo Worker**
|
|
||||||
3. Klik **Deployments** tab
|
|
||||||
4. Klik **Deploy Now** atau **Redeploy**
|
|
||||||
5. Tunggu deployment selesai (~2-3 menit)
|
|
||||||
|
|
||||||
## Verifikasi Setelah Deploy
|
|
||||||
|
|
||||||
Setelah deploy, cek logs di Railway:
|
|
||||||
|
|
||||||
### Log yang NORMAL:
|
|
||||||
```
|
|
||||||
✅ [SMART] Skipping SDK (3+ consecutive failures), using REST API directly...
|
|
||||||
✅ [DEBUG] REST API successful!
|
|
||||||
📊 API Stats: SDK=0 | REST=15 | Errors=3
|
|
||||||
```
|
|
||||||
|
|
||||||
### Log yang OPTIMAL:
|
|
||||||
```
|
|
||||||
✅ [DEBUG] Attempting SDK fetch...
|
|
||||||
✅ [DEBUG] REST API successful!
|
|
||||||
📊 API Stats: SDK=25 | REST=5 | Errors=0
|
|
||||||
```
|
|
||||||
|
|
||||||
### Indikator Performa:
|
|
||||||
- **Sebelum**: Delay ~10s setiap check
|
|
||||||
- **Sesudah**: Delay ~0-5s setiap check
|
|
||||||
- **Speed up**: 2-10x lebih cepat!
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Jika masih ada warning "SDK fetch failed"
|
|
||||||
✅ **NORMAL** - Ini bukan error, sistem bekerja dengan baik via REST API
|
|
||||||
|
|
||||||
### Jika "Both SDK and REST API failed"
|
|
||||||
❌ **MASALAH** - Check:
|
|
||||||
1. Firebase credentials masih valid?
|
|
||||||
2. Network Railway ke Firebase OK?
|
|
||||||
3. Firebase Database Rules allow?
|
|
||||||
|
|
||||||
### Jika "SMART: Resetting SDK retry counter"
|
|
||||||
✅ **BAGUS** - Sistem mencoba reconnect SDK setelah 50 menit
|
|
||||||
|
|
||||||
## Monitoring
|
|
||||||
|
|
||||||
Untuk monitoring jangka panjang, perhatikan:
|
|
||||||
|
|
||||||
1. **API Stats ratio**:
|
|
||||||
- SDK=0, REST=100 → SDK tidak berfungsi sama sekali
|
|
||||||
- SDK=70, REST=30 → SDK kadang timeout (normal)
|
|
||||||
- SDK=95, REST=5 → SDK sangat baik!
|
|
||||||
|
|
||||||
2. **Consecutive Errors**:
|
|
||||||
- 0-2: Normal fluctuation
|
|
||||||
- 3: Smart fallback activated
|
|
||||||
- >10: Ada masalah serius dengan SDK
|
|
||||||
|
|
||||||
## Tips Tambahan
|
|
||||||
|
|
||||||
### Jika ingin SDK lebih sering retry:
|
|
||||||
Edit line di `worker.js`:
|
|
||||||
```javascript
|
|
||||||
const SKIP_SDK_THRESHOLD = 3; // Ubah ke 5 atau 10
|
|
||||||
const RESET_THRESHOLD_AFTER = 50; // Ubah ke 20 (20 menit)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Jika ingin REST API sebagai default:
|
|
||||||
```javascript
|
|
||||||
const SKIP_SDK_THRESHOLD = 0; // Langsung skip SDK
|
|
||||||
```
|
|
||||||
|
|
||||||
## Status Setelah Optimasi
|
|
||||||
|
|
||||||
- ✅ SDK timeout lebih cepat (10s → 5s)
|
|
||||||
- ✅ Smart fallback implemented
|
|
||||||
- ✅ API statistics tracking
|
|
||||||
- ✅ Consistent timeout across all functions
|
|
||||||
- ✅ Better error handling
|
|
||||||
- ✅ Performance improved 2-10x
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Catatan**: Perubahan ini **backward compatible** - tidak mengubah functionality, hanya optimasi performa.
|
|
||||||
|
|
@ -1,210 +0,0 @@
|
||||||
# 🔍 ANALISIS MASALAH PENJADWALAN
|
|
||||||
|
|
||||||
## ❌ MASALAH TERIDENTIFIKASI
|
|
||||||
|
|
||||||
### 1. **Railway Worker TIDAK TERINSTALL DI RAILWAY**
|
|
||||||
|
|
||||||
Dari screenshot Railway Anda sebelumnya:
|
|
||||||
- **Source Repo:** `awisnuu/myreppril`
|
|
||||||
- **Branch:** `main`
|
|
||||||
- **Root Directory:** TIDAK DI-SET
|
|
||||||
|
|
||||||
**PROBLEM:** Railway mencoba build dari root folder, tapi kode worker ada di subfolder `railway-worker/`
|
|
||||||
|
|
||||||
### 2. **File package.json Tidak Di-Push ke GitHub**
|
|
||||||
|
|
||||||
Railway membutuhkan:
|
|
||||||
- ✅ `worker.js`
|
|
||||||
- ✅ `package.json`
|
|
||||||
- ✅ `railway.json` atau root directory config
|
|
||||||
|
|
||||||
Kalau file tidak ada di GitHub repo, Railway tidak bisa build.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ YANG SUDAH BENAR
|
|
||||||
|
|
||||||
### Flutter Side (Sudah Oke ✓)
|
|
||||||
```dart
|
|
||||||
// waktu_config_page.dart line 578-583
|
|
||||||
await _dbService.updateKontrolConfig({
|
|
||||||
'waktu_1': _jadwalPenyiraman[0]['jamMulai'], // Format: "07:30"
|
|
||||||
'waktu_2': _jadwalPenyiraman[1]['jamMulai'], // Format: "17:00"
|
|
||||||
'durasi_1': durasi1Detik, // Dalam detik
|
|
||||||
'durasi_2': durasi2Detik, // Dalam detik
|
|
||||||
'waktu': _isWaktuModeActive, // true/false
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
**Path Firebase:** `/kontrol/`
|
|
||||||
- ✅ `waktu` = true/false (toggle mode)
|
|
||||||
- ✅ `waktu_1` = "07:30" (string HH:mm)
|
|
||||||
- ✅ `waktu_2` = "17:00" (string HH:mm)
|
|
||||||
- ✅ `durasi_1` = 60 (integer seconds)
|
|
||||||
- ✅ `durasi_2` = 60 (integer seconds)
|
|
||||||
|
|
||||||
### Worker Side (Sudah Oke ✓)
|
|
||||||
```javascript
|
|
||||||
// worker.js line 175-204
|
|
||||||
const kontrolConfig = snapshot.val();
|
|
||||||
if (!kontrolConfig || !kontrolConfig.waktu) return;
|
|
||||||
|
|
||||||
const currentTime = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
|
|
||||||
|
|
||||||
// Check Jadwal 1
|
|
||||||
if (kontrolConfig.waktu_1 && kontrolConfig.waktu_1 === currentTime) {
|
|
||||||
// Trigger penyiraman
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Worker membaca path yang SAMA:** `/kontrol/`
|
|
||||||
- ✅ Format waktu cocok: "HH:mm"
|
|
||||||
- ✅ Check interval: 30 detik
|
|
||||||
- ✅ Debouncing: Prevent double trigger
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚨 ROOT CAUSE
|
|
||||||
|
|
||||||
**Railway worker TIDAK JALAN** karena:
|
|
||||||
|
|
||||||
### Issue #1: Root Directory Tidak Di-Set
|
|
||||||
Railway tidak tahu folder mana yang harus di-build.
|
|
||||||
|
|
||||||
**Solution:** Set Root Directory di Railway Settings:
|
|
||||||
```
|
|
||||||
railway-worker
|
|
||||||
```
|
|
||||||
|
|
||||||
### Issue #2: File Belum Di-Push ke GitHub
|
|
||||||
Worker files mungkin hanya ada di local, belum di-commit/push.
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
```bash
|
|
||||||
git add railway-worker/
|
|
||||||
git commit -m "Add Railway worker for 24/7 scheduling"
|
|
||||||
git push origin main
|
|
||||||
```
|
|
||||||
|
|
||||||
### Issue #3: Environment Variables Belum Di-Set
|
|
||||||
Railway butuh Firebase credentials.
|
|
||||||
|
|
||||||
**Solution:** Add di Railway Variables:
|
|
||||||
- `FIREBASE_PROJECT_ID`
|
|
||||||
- `FIREBASE_CLIENT_EMAIL`
|
|
||||||
- `FIREBASE_PRIVATE_KEY`
|
|
||||||
- `FIREBASE_DATABASE_URL`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 FORMAT DATA YANG BENAR
|
|
||||||
|
|
||||||
### Firebase `/kontrol/` Structure:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"kontrol": {
|
|
||||||
"waktu": true, // ← Mode waktu ON/OFF
|
|
||||||
"waktu_1": "07:30", // ← Jadwal 1 (HH:mm string)
|
|
||||||
"waktu_2": "17:00", // ← Jadwal 2 (HH:mm string)
|
|
||||||
"durasi_1": 60, // ← Durasi 1 (seconds integer)
|
|
||||||
"durasi_2": 60, // ← Durasi 2 (seconds integer)
|
|
||||||
"otomatis": false, // ← Mode sensor ON/OFF
|
|
||||||
"batas_bawah": 40,
|
|
||||||
"batas_atas": 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Worker Logic:
|
|
||||||
```javascript
|
|
||||||
// Cek setiap 30 detik
|
|
||||||
if (kontrolConfig.waktu === true) {
|
|
||||||
const now = new Date();
|
|
||||||
const currentTime = "07:30"; // Format sama dengan waktu_1/waktu_2
|
|
||||||
|
|
||||||
if (kontrolConfig.waktu_1 === currentTime) {
|
|
||||||
// TRIGGER PENYIRAMAN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**✅ Format sudah MATCH antara Flutter dan Worker!**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 LANGKAH PERBAIKAN
|
|
||||||
|
|
||||||
### Step 1: Push Worker Files ke GitHub
|
|
||||||
```bash
|
|
||||||
cd f:\ApsGo\ApsGo
|
|
||||||
git status
|
|
||||||
git add railway-worker/
|
|
||||||
git commit -m "Add Railway worker for 24/7 automation"
|
|
||||||
git push origin main
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: Set Root Directory di Railway
|
|
||||||
1. Railway Dashboard → Select service `myreppril`
|
|
||||||
2. Settings tab
|
|
||||||
3. Cari "Root Directory" atau "Build" section
|
|
||||||
4. Set: `railway-worker`
|
|
||||||
5. Save
|
|
||||||
|
|
||||||
### Step 3: Add Environment Variables
|
|
||||||
Di Railway Settings → Variables:
|
|
||||||
```
|
|
||||||
FIREBASE_PROJECT_ID=project-ta-a119e
|
|
||||||
FIREBASE_CLIENT_EMAIL=firebase-adminsdk-xxxxx@project-ta-a119e.iam.gserviceaccount.com
|
|
||||||
FIREBASE_PRIVATE_KEY=-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----
|
|
||||||
FIREBASE_DATABASE_URL=https://project-ta-a119e-default-rtdb.firebaseio.com
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 4: Redeploy
|
|
||||||
Railway akan auto-redeploy setelah:
|
|
||||||
- Root directory di-set
|
|
||||||
- Environment variables ditambahkan
|
|
||||||
|
|
||||||
### Step 5: Monitor Logs
|
|
||||||
Railway Logs → Harus muncul:
|
|
||||||
```
|
|
||||||
🚀 Starting ApsGo Railway Worker...
|
|
||||||
✅ Firebase Admin initialized
|
|
||||||
✅ Redis connected
|
|
||||||
✅ Waktu Mode scheduler started (check every 30s)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 TESTING
|
|
||||||
|
|
||||||
### Test Manual:
|
|
||||||
1. Set jadwal di APK (misalnya 2 menit dari sekarang)
|
|
||||||
2. Pastikan `/kontrol/waktu` = true di Firebase
|
|
||||||
3. Tunggu 2 menit
|
|
||||||
4. Cek Railway logs → Harus ada:
|
|
||||||
```
|
|
||||||
🕐 JADWAL 1 TRIGGERED: 07:30
|
|
||||||
📌 Added to queue: jadwal_1
|
|
||||||
💧 Processing Job...
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 KESIMPULAN
|
|
||||||
|
|
||||||
### ✅ TIDAK ADA MASALAH DI:
|
|
||||||
- [x] Flutter code (sudah benar menulis ke Firebase)
|
|
||||||
- [x] Worker code (sudah benar membaca dari Firebase)
|
|
||||||
- [x] Format data (sudah compatible)
|
|
||||||
- [x] Path Firebase (sama-sama `/kontrol/`)
|
|
||||||
|
|
||||||
### ❌ MASALAH DI:
|
|
||||||
- [ ] Railway deployment (worker belum jalan)
|
|
||||||
- [ ] Root directory belum di-set
|
|
||||||
- [ ] Environment variables mungkin belum lengkap
|
|
||||||
|
|
||||||
### 🎯 ACTION REQUIRED:
|
|
||||||
1. Push railway-worker ke GitHub
|
|
||||||
2. Set root directory di Railway
|
|
||||||
3. Verify environment variables
|
|
||||||
4. Redeploy & monitor logs
|
|
||||||
|
|
@ -1,466 +0,0 @@
|
||||||
# 🎉 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! 🌱**
|
|
||||||
|
|
@ -1,131 +0,0 @@
|
||||||
# ✅ Complete Firebase Warning Suppression
|
|
||||||
|
|
||||||
## 🎯 Current Status
|
|
||||||
|
|
||||||
Your worker is **WORKING PERFECTLY**! ✅
|
|
||||||
- Worker tidak crash
|
|
||||||
- Firebase connected
|
|
||||||
- Redis connected
|
|
||||||
- Schedule checks berjalan
|
|
||||||
|
|
||||||
**Firebase warnings yang muncul adalah WARNING SAJA**, sistem tetap berjalan normal.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔕 Recommended: Add Environment Variable
|
|
||||||
|
|
||||||
Untuk **COMPLETELY suppress** Firebase warnings di Railway:
|
|
||||||
|
|
||||||
### Step 1: Add Variable di Railway
|
|
||||||
|
|
||||||
1. Railway Dashboard → Service **myreppril**
|
|
||||||
2. Tab **Variables**
|
|
||||||
3. Click **New Variable**
|
|
||||||
4. Add:
|
|
||||||
```
|
|
||||||
Name: NODE_ENV
|
|
||||||
Value: production
|
|
||||||
```
|
|
||||||
5. Click **Add**
|
|
||||||
|
|
||||||
Railway akan auto-redeploy.
|
|
||||||
|
|
||||||
### Step 2: Verify Results
|
|
||||||
|
|
||||||
Setelah redeploy, logs akan **CLEAN**:
|
|
||||||
```
|
|
||||||
✅ Firebase Admin initialized
|
|
||||||
✅ Waktu Mode scheduler started (check every 60s)
|
|
||||||
✅ Sensor Mode monitoring started
|
|
||||||
🔌 Firebase realtime connection active
|
|
||||||
💚 HEALTH CHECK:
|
|
||||||
Firebase: ✅ Connected
|
|
||||||
Redis: ✅ Connected
|
|
||||||
💓 Heartbeat: Worker alive for 0h 1m
|
|
||||||
```
|
|
||||||
|
|
||||||
**NO MORE RED WARNINGS!** 🎉
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🤔 Apakah Warnings Berbahaya?
|
|
||||||
|
|
||||||
**TIDAK!** Firebase warnings ini:
|
|
||||||
|
|
||||||
### ✅ AMAN - Tidak Berbahaya
|
|
||||||
- Internal SDK warnings
|
|
||||||
- Deprecated API notices
|
|
||||||
- Long-polling connection info
|
|
||||||
- **System tetap berfungsi normal**
|
|
||||||
|
|
||||||
### ❌ TIDAK Mempengaruhi:
|
|
||||||
- Scheduling functionality
|
|
||||||
- Firebase connections
|
|
||||||
- Data reads/writes
|
|
||||||
- Worker stability
|
|
||||||
|
|
||||||
### 📊 Proof System Working:
|
|
||||||
```
|
|
||||||
✅ Firebase: Connected
|
|
||||||
✅ Redis: Connected
|
|
||||||
✅ Queue: 0 active, 0 waiting
|
|
||||||
✅ Waktu Mode scheduler: Running
|
|
||||||
✅ Sensor Mode: Running
|
|
||||||
✅ Health checks: Passing
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Kesimpulan
|
|
||||||
|
|
||||||
### Current State (With Warnings):
|
|
||||||
- ✅ Worker **RUNNING** 24/7
|
|
||||||
- ✅ All connections **ACTIVE**
|
|
||||||
- ✅ Schedules will **TRIGGER** correctly
|
|
||||||
- ⚠️ Cosmetic warnings (dapat diabaikan)
|
|
||||||
|
|
||||||
### After Adding NODE_ENV (Recommended):
|
|
||||||
- ✅ Worker **RUNNING** 24/7
|
|
||||||
- ✅ All connections **ACTIVE**
|
|
||||||
- ✅ Schedules will **TRIGGER** correctly
|
|
||||||
- ✅ **CLEAN LOGS** - no warnings!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Testing Schedule
|
|
||||||
|
|
||||||
Untuk verify system bekerja:
|
|
||||||
|
|
||||||
1. **Set jadwal di Flutter app:**
|
|
||||||
- Waktu: **5-10 menit dari sekarang**
|
|
||||||
- Enable Mode Waktu
|
|
||||||
- Simpan
|
|
||||||
|
|
||||||
2. **Monitor Railway logs saat waktu tiba:**
|
|
||||||
```
|
|
||||||
🕐 JADWAL 1 TRIGGERED: HH:MM
|
|
||||||
📌 Added to queue: jadwal_1_...
|
|
||||||
✅ Worker completed job jadwal_1_...
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Check ESP32/Hardware:**
|
|
||||||
- Pompa harus nyala sesuai durasi
|
|
||||||
- Valve harus buka sesuai POT
|
|
||||||
|
|
||||||
**Jika semua ini terjadi = SYSTEM WORKING PERFECTLY!** ✅
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💡 Recommendation
|
|
||||||
|
|
||||||
**Option A: Ignore Warnings (Easiest)**
|
|
||||||
- System sudah berfungsi
|
|
||||||
- Warnings tidak berbahaya
|
|
||||||
- No action needed
|
|
||||||
|
|
||||||
**Option B: Suppress Warnings (Cleanest)**
|
|
||||||
- Add `NODE_ENV=production` di Railway
|
|
||||||
- Logs akan lebih bersih
|
|
||||||
- Same functionality
|
|
||||||
|
|
||||||
**Pilihan Anda!** Both options are valid. Sistema tetap bekerja dengan atau tanpa warnings.
|
|
||||||
|
|
@ -1,237 +0,0 @@
|
||||||
# Full History System Implementation
|
|
||||||
|
|
||||||
## 📋 Overview
|
|
||||||
Sistem history lengkap yang mencatat dan menampilkan data sensor dari Firebase secara otomatis.
|
|
||||||
|
|
||||||
## 🔧 Komponen
|
|
||||||
|
|
||||||
### 1. **History Logging Service**
|
|
||||||
File: `lib/services/history_logging_service.dart`
|
|
||||||
|
|
||||||
**Fungsi:**
|
|
||||||
- Auto-save data sensor setiap **10 menit**
|
|
||||||
- Persistent - berjalan otomatis saat app aktif
|
|
||||||
- Cleanup otomatis data lama (>30 hari)
|
|
||||||
|
|
||||||
**Key Features:**
|
|
||||||
```dart
|
|
||||||
- interval: Duration(minutes: 10)
|
|
||||||
- Auto start saat Firebase initialized
|
|
||||||
- isActive property untuk check status
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. **Firebase Database Service Updates**
|
|
||||||
File: `lib/services/firebase_database_service.dart`
|
|
||||||
|
|
||||||
**Method Baru:**
|
|
||||||
- `saveHistory()` - Simpan snapshot data sensor
|
|
||||||
- `getHistoryByDateRange()` - Load data berdasarkan range tanggal
|
|
||||||
- `getLatestHistory()` - Load data terbaru (100 entries)
|
|
||||||
- `clearOldHistory()` - Hapus data >30 hari
|
|
||||||
|
|
||||||
### 3. **History Page Lengkap**
|
|
||||||
File: `lib/screens/histori_page.dart`
|
|
||||||
|
|
||||||
**Fitur:**
|
|
||||||
- ✅ Load data real dari Firebase
|
|
||||||
- ✅ Filter by date range
|
|
||||||
- ✅ Filter per pot atau semua pot
|
|
||||||
- ✅ Hitung rata-rata otomatis
|
|
||||||
- ✅ Refresh manual dengan IconButton
|
|
||||||
- ✅ Loading states
|
|
||||||
- ✅ Error handling
|
|
||||||
- ✅ Empty state info
|
|
||||||
- ✅ Auto load last 7 days by default
|
|
||||||
|
|
||||||
### 4. **Main App Integration**
|
|
||||||
File: `lib/main.dart`
|
|
||||||
|
|
||||||
**Changes:**
|
|
||||||
- Import HistoryLoggingService
|
|
||||||
- Start service saat Firebase initialized
|
|
||||||
- Service berjalan global di background
|
|
||||||
|
|
||||||
## 📊 Struktur Data Firebase
|
|
||||||
|
|
||||||
```
|
|
||||||
/history
|
|
||||||
/2024-12-28
|
|
||||||
/14:30
|
|
||||||
{
|
|
||||||
suhu: 28.5,
|
|
||||||
kelembapan: 65.2,
|
|
||||||
ldr: 820,
|
|
||||||
soil_1: 45.2,
|
|
||||||
soil_2: 52.1,
|
|
||||||
soil_3: 48.7,
|
|
||||||
soil_4: 50.3,
|
|
||||||
soil_5: 46.9,
|
|
||||||
timestamp: 1703761800000
|
|
||||||
}
|
|
||||||
/14:40
|
|
||||||
{...}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🎯 Flow Data
|
|
||||||
|
|
||||||
1. **Auto Logging (Background)**
|
|
||||||
```
|
|
||||||
App Start → Firebase Init → HistoryLoggingService.start()
|
|
||||||
→ Timer (10 min) → saveHistory() → Firebase /history
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Manual Load (UI)**
|
|
||||||
```
|
|
||||||
User Opens Histori Page → Load Default (7 days)
|
|
||||||
→ getHistoryByDateRange() → Calculate Averages → Display
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Filter & Refresh**
|
|
||||||
```
|
|
||||||
User Selects Date Range → _loadHistoryData()
|
|
||||||
→ Display Updated Data
|
|
||||||
|
|
||||||
User Clicks Refresh → _loadHistoryData() → Display
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📱 UI Features
|
|
||||||
|
|
||||||
### Overall Averages
|
|
||||||
- Suhu Rata-Rata
|
|
||||||
- Kelembaban Udara Rata-Rata
|
|
||||||
- LDR Rata-Rata
|
|
||||||
|
|
||||||
### Per Pot Averages
|
|
||||||
- Expandable cards untuk setiap pot
|
|
||||||
- Detail: Suhu, Kelembaban, Soil Moisture, Light
|
|
||||||
|
|
||||||
### Filter Options
|
|
||||||
- Date Range Picker (calendar UI)
|
|
||||||
- Pot Selector (dropdown)
|
|
||||||
- Semua Pot
|
|
||||||
- Pot 1
|
|
||||||
- Pot 2
|
|
||||||
- Pot 3
|
|
||||||
- Pot 4
|
|
||||||
- Pot 5
|
|
||||||
|
|
||||||
## 🚀 How to Use
|
|
||||||
|
|
||||||
### For Users
|
|
||||||
1. Buka tab "Histori"
|
|
||||||
2. Data akan otomatis dimuat (7 hari terakhir)
|
|
||||||
3. Klik calendar icon untuk pilih date range
|
|
||||||
4. Pilih pot dari dropdown untuk filter
|
|
||||||
5. Klik refresh icon untuk reload data
|
|
||||||
|
|
||||||
### For Developers
|
|
||||||
```dart
|
|
||||||
// Start logging service
|
|
||||||
final loggingService = HistoryLoggingService();
|
|
||||||
loggingService.start();
|
|
||||||
|
|
||||||
// Stop logging service (optional)
|
|
||||||
loggingService.stop();
|
|
||||||
|
|
||||||
// Check if active
|
|
||||||
if (loggingService.isActive) {
|
|
||||||
print('Service is running');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get history data
|
|
||||||
final dbService = FirebaseDatabaseService();
|
|
||||||
final history = await dbService.getHistoryByDateRange(
|
|
||||||
DateTime.now().subtract(Duration(days: 7)),
|
|
||||||
DateTime.now(),
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
## ⚙️ Configuration
|
|
||||||
|
|
||||||
### Logging Interval
|
|
||||||
Edit `lib/services/history_logging_service.dart`:
|
|
||||||
```dart
|
|
||||||
static const Duration interval = Duration(minutes: 10); // Change here
|
|
||||||
```
|
|
||||||
|
|
||||||
### Data Retention
|
|
||||||
Edit `lib/services/firebase_database_service.dart`:
|
|
||||||
```dart
|
|
||||||
await clearOldHistory(30); // Keep last 30 days
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔍 Testing Checklist
|
|
||||||
|
|
||||||
- [ ] Service start otomatis saat app launch
|
|
||||||
- [ ] Data tersimpan setiap 10 menit
|
|
||||||
- [ ] History page load data dari Firebase
|
|
||||||
- [ ] Filter date range berfungsi
|
|
||||||
- [ ] Filter per pot berfungsi
|
|
||||||
- [ ] Calculate averages akurat
|
|
||||||
- [ ] Refresh button berfungsi
|
|
||||||
- [ ] Loading states tampil
|
|
||||||
- [ ] Error handling works
|
|
||||||
- [ ] Empty state tampil jika no data
|
|
||||||
|
|
||||||
## ⚠️ Important Notes
|
|
||||||
|
|
||||||
1. **First Time Use**: Data history akan mulai tersimpan setelah 10 menit pertama. Sebelum itu, akan menampilkan data saat ini.
|
|
||||||
|
|
||||||
2. **Firebase Rules**: Pastikan Firebase Realtime Database rules allow read/write untuk /history:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"rules": {
|
|
||||||
"history": {
|
|
||||||
".read": "auth != null",
|
|
||||||
".write": "auth != null"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Performance**: Cleanup otomatis data >30 hari untuk menjaga performa database.
|
|
||||||
|
|
||||||
4. **Memory**: Service menggunakan Timer, pastikan stop() dipanggil jika tidak dibutuhkan lagi (optional, karena global service).
|
|
||||||
|
|
||||||
## 📈 Future Enhancements
|
|
||||||
|
|
||||||
Possible improvements:
|
|
||||||
- Export data to CSV
|
|
||||||
- Chart/graph visualization
|
|
||||||
- Notification untuk anomali data
|
|
||||||
- Configurable logging interval dari UI
|
|
||||||
- Data comparison antar pot
|
|
||||||
- Weekly/Monthly summary reports
|
|
||||||
|
|
||||||
## 🐛 Troubleshooting
|
|
||||||
|
|
||||||
**Problem**: Data tidak tersimpan
|
|
||||||
- Check Firebase connection
|
|
||||||
- Check auth status
|
|
||||||
- Check console logs untuk error message
|
|
||||||
|
|
||||||
**Problem**: History page kosong
|
|
||||||
- Tunggu 10 menit untuk data pertama
|
|
||||||
- Check Firebase rules
|
|
||||||
- Check internet connection
|
|
||||||
|
|
||||||
**Problem**: Service tidak start
|
|
||||||
- Check Firebase initialization
|
|
||||||
- Check console logs
|
|
||||||
- Restart app
|
|
||||||
|
|
||||||
## 📝 Change Log
|
|
||||||
|
|
||||||
### v1.0 - Full History System
|
|
||||||
- ✅ Auto-logging service (10 min interval)
|
|
||||||
- ✅ Complete history page with Firebase integration
|
|
||||||
- ✅ Date range filter
|
|
||||||
- ✅ Per pot filtering
|
|
||||||
- ✅ Automatic averages calculation
|
|
||||||
- ✅ Cleanup old data (>30 days)
|
|
||||||
- ✅ Loading & error states
|
|
||||||
- ✅ Manual refresh option
|
|
||||||
|
|
||||||
---
|
|
||||||
**Status**: ✅ FULLY IMPLEMENTED
|
|
||||||
**Last Updated**: December 2024
|
|
||||||
|
|
@ -1,423 +0,0 @@
|
||||||
# 📦 IMPLEMENTASI SELESAI - ApsGo Production Ready
|
|
||||||
|
|
||||||
## ✅ SEMUA SELESAI DIKERJAKAN!
|
|
||||||
|
|
||||||
Tanggal: 10 Februari 2026
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📂 File-file Baru yang Dibuat
|
|
||||||
|
|
||||||
### 1. Railway Worker (Backend Service)
|
|
||||||
```
|
|
||||||
railway-worker/
|
|
||||||
├── worker.js ✅ Complete worker implementation
|
|
||||||
├── package.json ✅ Dependencies & scripts
|
|
||||||
├── railway.json ✅ Railway deployment config
|
|
||||||
├── .env.example ✅ Environment variables template
|
|
||||||
├── .gitignore ✅ Git ignore rules
|
|
||||||
└── README.md ✅ Worker documentation
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Flutter Services (Bug Fixes & New)
|
|
||||||
```
|
|
||||||
lib/services/
|
|
||||||
├── automation_constants.dart ✅ NEW - Centralized constants
|
|
||||||
├── connection_monitor_service.dart ✅ NEW - Connection monitoring
|
|
||||||
├── kontrol_automation_service.dart ✅ UPDATED - Fixed & improved
|
|
||||||
├── history_logging_service.dart ✅ UPDATED - Use constants
|
|
||||||
├── firebase_database_service.dart (existing, no changes)
|
|
||||||
└── auth_service.dart (existing, no changes)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Flutter Core (Bug Fixes)
|
|
||||||
```
|
|
||||||
lib/
|
|
||||||
├── main.dart ✅ UPDATED - AppLifecycle observer
|
|
||||||
└── screens/
|
|
||||||
└── dashboard_page.dart ✅ UPDATED - Fixed memory leak
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Documentation
|
|
||||||
```
|
|
||||||
├── DEPLOYMENT_GUIDE.md ✅ Step-by-step Railway deployment (4600+ words)
|
|
||||||
├── BUGS_AND_FIXES.md ✅ Bug report & fixes summary
|
|
||||||
├── RAILWAY_QUICK_START.md ✅ Quick reference guide
|
|
||||||
└── BUG_FIXES_REPORT.md (existing file in repo)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🐛 Bug yang Diperbaiki
|
|
||||||
|
|
||||||
### Critical (4 bugs)
|
|
||||||
1. ✅ **Memory Leak** - StreamSubscription disposal
|
|
||||||
2. ✅ **Background Services** - AppLifecycle management
|
|
||||||
3. ✅ **Race Condition** - Railway Worker dengan BullMQ queue
|
|
||||||
4. ✅ **No Connection Check** - ConnectionMonitorService
|
|
||||||
|
|
||||||
### Medium (3 bugs)
|
|
||||||
5. ✅ **Error Handling** - Improved dengan user feedback
|
|
||||||
6. ✅ **Time Comparison** - Proper formatting di worker
|
|
||||||
7. ✅ **Magic Numbers** - Centralized constants
|
|
||||||
|
|
||||||
### Minor (2 bugs)
|
|
||||||
8. ⚠️ **WillPopScope Deprecated** - Noted (still works)
|
|
||||||
9. ⚠️ **Input Validation** - Helpers created (UI integration pending)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Fitur Baru
|
|
||||||
|
|
||||||
### 1. Railway Worker (24/7 Backend)
|
|
||||||
**Teknologi:**
|
|
||||||
- Node.js 18+
|
|
||||||
- Firebase Admin SDK
|
|
||||||
- BullMQ (job queue)
|
|
||||||
- Redis (in-memory DB)
|
|
||||||
- Cron (scheduled tasks)
|
|
||||||
|
|
||||||
**Kemampuan:**
|
|
||||||
- ✅ Waktu Mode - Schedule penyiraman by time
|
|
||||||
- ✅ Sensor Mode - Auto watering by threshold
|
|
||||||
- ✅ Auto History Logging - Every 10 minutes
|
|
||||||
- ✅ Auto Cleanup - Daily at 2 AM (retain 30 days)
|
|
||||||
- ✅ Health Monitoring - Every 5 minutes
|
|
||||||
- ✅ Graceful Shutdown - Clean resource cleanup
|
|
||||||
- ✅ Error Recovery - Auto-retry & safety turn-off
|
|
||||||
|
|
||||||
**Keuntungan:**
|
|
||||||
- 🌟 Berjalan 24/7 meskipun HP mati
|
|
||||||
- 🌟 Reliable (Railway auto-restart jika crash)
|
|
||||||
- 🌟 Scalable (bisa handle multiple users/devices)
|
|
||||||
- 🌟 Cost-effective ($3-5/month, free tier available)
|
|
||||||
- 🌟 Production-grade architecture
|
|
||||||
|
|
||||||
### 2. Connection Monitoring
|
|
||||||
**Features:**
|
|
||||||
- Real-time Firebase connection status
|
|
||||||
- Stream untuk listen connection changes
|
|
||||||
- Wait-for-connection utility
|
|
||||||
- Callbacks untuk custom handling
|
|
||||||
|
|
||||||
**Usage:**
|
|
||||||
```dart
|
|
||||||
final monitor = ConnectionMonitorService();
|
|
||||||
monitor.start();
|
|
||||||
|
|
||||||
if (monitor.isConnected) {
|
|
||||||
// Safe to proceed with Firebase operations
|
|
||||||
}
|
|
||||||
|
|
||||||
monitor.connectionStream.listen((connected) {
|
|
||||||
print(connected ? 'Connected' : 'Disconnected');
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Automation Constants
|
|
||||||
**Features:**
|
|
||||||
- Centralized configuration values
|
|
||||||
- Validation helpers
|
|
||||||
- Self-documenting code
|
|
||||||
- Easy maintenance
|
|
||||||
|
|
||||||
**Examples:**
|
|
||||||
```dart
|
|
||||||
AutomationConstants.defaultDurasiDetik // 60
|
|
||||||
AutomationConstants.wateringCooldownSeconds // 120
|
|
||||||
AutomationConstants.totalPots // 5
|
|
||||||
AutomationConstants.isValidDurasi(30) // true
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 Perbandingan Sebelum vs Sesudah
|
|
||||||
|
|
||||||
### Sebelum
|
|
||||||
- ❌ Scheduling hanya jalan saat app buka
|
|
||||||
- ❌ Memory leak di dashboard
|
|
||||||
- ❌ Background services tidak berhenti
|
|
||||||
- ❌ Race condition di multi-pot watering
|
|
||||||
- ❌ No connection check (silent failures)
|
|
||||||
- ❌ Magic numbers everywhere
|
|
||||||
- ❌ Poor error handling
|
|
||||||
- ❌ Not production-ready
|
|
||||||
|
|
||||||
### Sesudah
|
|
||||||
- ✅ Scheduling 24/7 dengan Railway Worker
|
|
||||||
- ✅ Clean memory management
|
|
||||||
- ✅ Proper lifecycle handling
|
|
||||||
- ✅ Redis queue prevent race conditions
|
|
||||||
- ✅ Connection monitoring & better errors
|
|
||||||
- ✅ Centralized constants
|
|
||||||
- ✅ Improved error handling & user feedback
|
|
||||||
- ✅ **PRODUCTION-READY!**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🏗️ Arsitektur Sistem
|
|
||||||
|
|
||||||
### Old Architecture (Flutter Only)
|
|
||||||
```
|
|
||||||
┌─────────────┐
|
|
||||||
│ Flutter App │ ← Timer/Stream (hanya saat app buka)
|
|
||||||
└──────┬──────┘
|
|
||||||
↓
|
|
||||||
┌──────────────┐
|
|
||||||
│ Firebase RTDB│
|
|
||||||
└──────┬───────┘
|
|
||||||
↓
|
|
||||||
┌──────────────┐
|
|
||||||
│ ESP32/Sensor │
|
|
||||||
└──────────────┘
|
|
||||||
|
|
||||||
❌ Problem: App tertutup = automation stop
|
|
||||||
```
|
|
||||||
|
|
||||||
### New Architecture (Production)
|
|
||||||
```
|
|
||||||
┌─────────────┐
|
|
||||||
│ Flutter App │ ← UI & Manual Control
|
|
||||||
└──────┬──────┘
|
|
||||||
↓
|
|
||||||
┌──────────────────────────────────┐
|
|
||||||
│ Firebase Realtime DB │ ← Central Data Hub
|
|
||||||
└─────────┬────────────────────────┘
|
|
||||||
↓ ↓
|
|
||||||
┌─────────────────┐ ┌──────────────┐
|
|
||||||
│ Railway Worker │ │ ESP32/Sensor │
|
|
||||||
│ (24/7 Cloud) │ └──────────────┘
|
|
||||||
│ │
|
|
||||||
│ • Scheduler │
|
|
||||||
│ • Automation │
|
|
||||||
│ • History Log │
|
|
||||||
│ │
|
|
||||||
│ ↓ Redis Queue│
|
|
||||||
└─────────────────┘
|
|
||||||
|
|
||||||
✅ Solution: Worker always running, independent dari app
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💰 Cost Estimate
|
|
||||||
|
|
||||||
### Railway Free Tier
|
|
||||||
- **Credit**: $5/month (gratis)
|
|
||||||
- **Worker**: ~$2-3/month
|
|
||||||
- **Redis**: ~$1-2/month
|
|
||||||
- **Total**: ~$3-5/month
|
|
||||||
- **Verdict**: Masuk free tier! 🎉
|
|
||||||
|
|
||||||
### Railway Hobby Plan (Optional)
|
|
||||||
- **Price**: $5/month
|
|
||||||
- **Benefits**:
|
|
||||||
- More resources
|
|
||||||
- No auto-sleep
|
|
||||||
- Priority support
|
|
||||||
- Better for production
|
|
||||||
|
|
||||||
### Recommendation
|
|
||||||
Start dengan **Free Tier**, upgrade ke Hobby jika needed.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📖 Dokumentasi yang Tersedia
|
|
||||||
|
|
||||||
### 1. DEPLOYMENT_GUIDE.md (LENGKAP!)
|
|
||||||
**Sections:**
|
|
||||||
- ✅ Prerequisites checklist
|
|
||||||
- ✅ Step 1: Firebase Service Account setup
|
|
||||||
- ✅ Step 2: Railway project creation
|
|
||||||
- ✅ Step 3: Environment variables
|
|
||||||
- ✅ Step 4: Deploy process
|
|
||||||
- ✅ Step 5: Testing (Waktu & Sensor mode)
|
|
||||||
- ✅ Step 6: Monitoring & maintenance
|
|
||||||
- ✅ Step 7: Billing & cost management
|
|
||||||
- ✅ Step 8: Troubleshooting (common issues)
|
|
||||||
- ✅ Step 9: Update worker code
|
|
||||||
- ✅ Step 10: Flutter app integration
|
|
||||||
- ✅ Deployment checklist
|
|
||||||
|
|
||||||
**4600+ words**, super detail, screenshot-ready!
|
|
||||||
|
|
||||||
### 2. BUGS_AND_FIXES.md
|
|
||||||
- List 9 bugs found
|
|
||||||
- Severity classification
|
|
||||||
- Code examples before/after
|
|
||||||
- Impact analysis
|
|
||||||
- Files changed
|
|
||||||
|
|
||||||
### 3. RAILWAY_QUICK_START.md
|
|
||||||
- TL;DR version untuk quick reference
|
|
||||||
- 5-step deployment ringkas
|
|
||||||
- Troubleshooting ringkas
|
|
||||||
- Cost summary
|
|
||||||
|
|
||||||
### 4. railway-worker/README.md
|
|
||||||
- Worker architecture
|
|
||||||
- Features explanation
|
|
||||||
- Local development setup
|
|
||||||
- Environment variables
|
|
||||||
- How it works (Waktu & Sensor mode)
|
|
||||||
- Safety features
|
|
||||||
- Monitoring guide
|
|
||||||
- Troubleshooting
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 Testing Checklist
|
|
||||||
|
|
||||||
### Before Production Deployment
|
|
||||||
- [ ] Test waktu mode locally (Firebase Emulator optional)
|
|
||||||
- [ ] Test sensor mode with manual threshold change
|
|
||||||
- [ ] Verify Railway deployment successful
|
|
||||||
- [ ] Check logs show "Worker is running"
|
|
||||||
- [ ] Test jadwal 1 trigger
|
|
||||||
- [ ] Test jadwal 2 trigger
|
|
||||||
- [ ] Test sensor threshold trigger
|
|
||||||
- [ ] Verify auto history logging (wait 10 min)
|
|
||||||
- [ ] Verify Firebase RTDB data updated correctly
|
|
||||||
- [ ] Test connection loss scenario
|
|
||||||
- [ ] Monitor for 24 hours (stability test)
|
|
||||||
|
|
||||||
### Production Monitoring (First Week)
|
|
||||||
- [ ] Check Railway logs daily
|
|
||||||
- [ ] Monitor Firebase reads/writes usage
|
|
||||||
- [ ] Monitor Redis memory usage
|
|
||||||
- [ ] Verify schedules executing on time
|
|
||||||
- [ ] Check sensor mode responsiveness
|
|
||||||
- [ ] Monitor app performance (no memory issues)
|
|
||||||
- [ ] User feedback (if any issues)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Langkah Deploy (Summary)
|
|
||||||
|
|
||||||
### Quick Deploy (10 menit)
|
|
||||||
1. **Firebase**: Download service account key
|
|
||||||
2. **Railway**: Create project, connect GitHub repo
|
|
||||||
3. **Redis**: Add Redis database di Railway
|
|
||||||
4. **Config**: Set environment variables di Railway
|
|
||||||
5. **Deploy**: Railway auto-deploy
|
|
||||||
6. **Test**: Check logs & test dari Flutter app
|
|
||||||
|
|
||||||
**Detail:** Lihat `DEPLOYMENT_GUIDE.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 Maintenance Guide
|
|
||||||
|
|
||||||
### Daily
|
|
||||||
- ✅ Auto: Worker health check (every 5 min)
|
|
||||||
- ✅ Auto: History logging (every 10 min)
|
|
||||||
|
|
||||||
### Weekly
|
|
||||||
- Check Railway logs untuk errors
|
|
||||||
- Monitor cost usage
|
|
||||||
- Verify schedules running correctly
|
|
||||||
|
|
||||||
### Monthly
|
|
||||||
- Review Firebase RTDB size
|
|
||||||
- ✅ Auto: History cleanup (daily, retain 30 days)
|
|
||||||
- Check Railway invoice
|
|
||||||
|
|
||||||
### As Needed
|
|
||||||
- Update worker code (git push → auto-deploy)
|
|
||||||
- Adjust automation parameters (batas, durasi, dll)
|
|
||||||
- Scale up if needed (upgrade Railway plan)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚠️ Known Limitations & TODOs
|
|
||||||
|
|
||||||
### Current Limitations
|
|
||||||
1. Worker timezone = UTC (need to convert dari local time)
|
|
||||||
2. Flutter app masih punya local automation (should disable di production)
|
|
||||||
3. No push notifications untuk alerts (future enhancement)
|
|
||||||
|
|
||||||
### TODO (Nice to Have)
|
|
||||||
- [ ] Disable local automation di production build
|
|
||||||
- [ ] Add UI indicator "Server automation active"
|
|
||||||
- [ ] Push notifications untuk alerts (FCM)
|
|
||||||
- [ ] Unit tests untuk automation logic
|
|
||||||
- [ ] Integration tests end-to-end
|
|
||||||
- [ ] Replace WillPopScope dengan PopScope
|
|
||||||
- [ ] Input validation di UI forms
|
|
||||||
- [ ] Error reporting (Sentry/Crashlytics)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 Support & Resources
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
- ✅ DEPLOYMENT_GUIDE.md
|
|
||||||
- ✅ BUGS_AND_FIXES.md
|
|
||||||
- ✅ RAILWAY_QUICK_START.md
|
|
||||||
- ✅ railway-worker/README.md
|
|
||||||
|
|
||||||
### External Resources
|
|
||||||
- [Railway Docs](https://docs.railway.app)
|
|
||||||
- [BullMQ Docs](https://docs.bullmq.io)
|
|
||||||
- [Firebase Admin SDK](https://firebase.google.com/docs/admin/setup)
|
|
||||||
- [Node.js Best Practices](https://github.com/goldbergyoni/nodebestpractices)
|
|
||||||
|
|
||||||
### Community
|
|
||||||
- Railway Discord: discord.gg/railway
|
|
||||||
- Firebase Support: firebase.google.com/support
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎉 Kesimpulan
|
|
||||||
|
|
||||||
### Achievement Unlocked! 🏆
|
|
||||||
|
|
||||||
**✅ Semua yang diminta telah selesai:**
|
|
||||||
|
|
||||||
1. ✅ **Code lengkap Railway Worker**
|
|
||||||
- worker.js (600+ lines)
|
|
||||||
- Full-featured dengan queue, monitoring, auto-cleanup
|
|
||||||
- Production-ready
|
|
||||||
|
|
||||||
2. ✅ **Fix semua bug yang ditemukan**
|
|
||||||
- 4 critical bugs fixed
|
|
||||||
- 3 medium bugs fixed
|
|
||||||
- 2 minor bugs noted
|
|
||||||
- Memory management improved
|
|
||||||
- Error handling improved
|
|
||||||
|
|
||||||
3. ✅ **Step-by-step deployment guide**
|
|
||||||
- DEPLOYMENT_GUIDE.md (4600+ words)
|
|
||||||
- 10 detailed steps dengan screenshots-ready
|
|
||||||
- Troubleshooting section
|
|
||||||
- Testing guide
|
|
||||||
- Deployment checklist
|
|
||||||
|
|
||||||
**Bonus:**
|
|
||||||
- ✅ New services (ConnectionMonitor, Constants)
|
|
||||||
- ✅ AppLifecycle observer
|
|
||||||
- ✅ 4 comprehensive documentation files
|
|
||||||
- ✅ Code formatted & error-free
|
|
||||||
- ✅ Production-ready architecture
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Next Steps untuk Anda
|
|
||||||
|
|
||||||
1. **Read** `DEPLOYMENT_GUIDE.md` (mulai dari sini!)
|
|
||||||
2. **Deploy** Railway Worker (ikuti guide step-by-step)
|
|
||||||
3. **Test** semua fitur (Waktu, Sensor, History)
|
|
||||||
4. **Monitor** logs selama 24 jam pertama
|
|
||||||
5. **Enjoy** reliable 24/7 automation! 🎊
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Status:** ✅ PRODUCTION READY
|
|
||||||
**Version:** 2.0.0
|
|
||||||
**Date:** 10 Februari 2026
|
|
||||||
|
|
||||||
**🎊 Selamat! Aplikasi ApsGo Anda sekarang production-ready dengan automation 24/7!**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*"From local-only scheduling to cloud-powered 24/7 automation - ApsGo is now ready for the real world!"* 🚀
|
|
||||||
|
|
@ -1,121 +0,0 @@
|
||||||
# 📱 Cara Install ApsGo di Handphone
|
|
||||||
|
|
||||||
## ✅ APK Sudah Selesai Di-build!
|
|
||||||
|
|
||||||
Lokasi APK: `f:\ApsGo\ApsGo\build\app\outputs\flutter-apk\app-release.apk`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 Cara Install di Handphone
|
|
||||||
|
|
||||||
### **Metode 1: Transfer via USB Cable**
|
|
||||||
|
|
||||||
1. **Sambungkan HP ke PC** dengan kabel USB
|
|
||||||
2. **Copy file APK** dari:
|
|
||||||
```
|
|
||||||
f:\ApsGo\ApsGo\build\app\outputs\flutter-apk\app-release.apk
|
|
||||||
```
|
|
||||||
3. **Paste ke HP** (folder Download atau Internal Storage)
|
|
||||||
4. **Buka File Manager** di HP
|
|
||||||
5. **Tap file** `app-release.apk`
|
|
||||||
6. **Izinkan install** dari Unknown Sources (jika diminta)
|
|
||||||
7. **Install** → Selesai!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **Metode 2: Transfer via WhatsApp/Telegram**
|
|
||||||
|
|
||||||
1. **Kirim file APK** ke diri sendiri via WhatsApp/Telegram:
|
|
||||||
- Buka WhatsApp Web/Desktop
|
|
||||||
- Kirim ke chat pribadi Anda
|
|
||||||
- File: `app-release.apk`
|
|
||||||
|
|
||||||
2. **Buka di HP** → Download file
|
|
||||||
3. **Tap file APK** → Install
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **Metode 3: Upload ke Google Drive**
|
|
||||||
|
|
||||||
1. **Upload APK** ke Google Drive dari PC
|
|
||||||
2. **Buka Google Drive** di HP
|
|
||||||
3. **Download APK**
|
|
||||||
4. **Install**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚙️ Setting HP (Penting!)
|
|
||||||
|
|
||||||
Sebelum install, aktifkan **Install from Unknown Sources**:
|
|
||||||
|
|
||||||
### **Android 8.0+:**
|
|
||||||
1. Settings → Apps & notifications
|
|
||||||
2. Special app access
|
|
||||||
3. Install unknown apps
|
|
||||||
4. Pilih File Manager/Chrome
|
|
||||||
5. Allow from this source → ON
|
|
||||||
|
|
||||||
### **Android 7.0 dan lebih lama:**
|
|
||||||
1. Settings → Security
|
|
||||||
2. Unknown sources → ON
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔑 Login ke Aplikasi
|
|
||||||
|
|
||||||
Setelah install:
|
|
||||||
|
|
||||||
1. **Buka app ApsGo**
|
|
||||||
2. **Login** dengan:
|
|
||||||
- Email: (akun Firebase Anda)
|
|
||||||
- Password: (password akun)
|
|
||||||
|
|
||||||
3. **Atau Register** akun baru
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📡 Koneksi ke ESP32
|
|
||||||
|
|
||||||
Pastikan:
|
|
||||||
- ✅ HP dan ESP32 sama-sama **terkoneksi internet**
|
|
||||||
- ✅ ESP32 sudah **tersambung ke WiFi**
|
|
||||||
- ✅ Firebase Realtime Database sudah **dikonfigurasi**
|
|
||||||
|
|
||||||
Data akan sync otomatis via Firebase!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🐛 Troubleshooting
|
|
||||||
|
|
||||||
**"App not installed":**
|
|
||||||
- Uninstall versi lama (jika ada)
|
|
||||||
- Coba install ulang
|
|
||||||
|
|
||||||
**"Unknown sources blocked":**
|
|
||||||
- Ikuti langkah Setting HP di atas
|
|
||||||
|
|
||||||
**"App keeps crashing":**
|
|
||||||
- Pastikan Google Services sudah aktif di HP
|
|
||||||
- Check koneksi internet
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔄 Update App
|
|
||||||
|
|
||||||
Jika ada update:
|
|
||||||
1. Build APK baru: `flutter build apk --release`
|
|
||||||
2. Transfer & install (akan replace versi lama)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📦 Informasi APK
|
|
||||||
|
|
||||||
- **Nama:** ApsGo (Project TA)
|
|
||||||
- **Package:** com.example.project_ta
|
|
||||||
- **Size:** ± 20-30 MB
|
|
||||||
- **Min Android:** 5.0 (Lollipop)
|
|
||||||
- **Firebase:** Integrated
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Selamat mencoba! 🎉**
|
|
||||||
|
|
@ -1,284 +0,0 @@
|
||||||
# 📱 Panduan Sistem Penjad walan Baru - Flutter App
|
|
||||||
|
|
||||||
## ✨ Fitur Baru
|
|
||||||
|
|
||||||
Sistem penjadwalan ApsGo telah diupgrade dengan fitur:
|
|
||||||
|
|
||||||
✅ **Multiple Jadwal** - Tidak lagi terbatas 2 jadwal saja
|
|
||||||
✅ **Flexible Pot Selection** - Setiap jadwal bisa pilih pot mana yang aktif
|
|
||||||
✅ **Per-Schedule Config** - Tiap jadwal punya durasi & pompa sendiri
|
|
||||||
✅ **Enable/Disable** - Matikan jadwal tanpa hapus data
|
|
||||||
✅ **Real-time Sync** - Langsung sync dengan Firebase & Railway Worker
|
|
||||||
|
|
||||||
## 📁 File Baru yang Dibuat
|
|
||||||
|
|
||||||
### Models
|
|
||||||
- `lib/models/jadwal_model.dart` - Data model untuk jadwal
|
|
||||||
|
|
||||||
### Services
|
|
||||||
- `lib/services/jadwal_service.dart` - CRUD operations untuk jadwal Firebase
|
|
||||||
|
|
||||||
### Screens
|
|
||||||
- `lib/screens/jadwal_management_page.dart` - List & manage all jadwal
|
|
||||||
- `lib/screens/jadwal_form_page.dart` - Add/edit jadwal form
|
|
||||||
|
|
||||||
### Config
|
|
||||||
- `firebase-structure-complete.json` - Struktur Firebase lengkap
|
|
||||||
|
|
||||||
## 🗄️ Struktur Firebase Baru
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"kontrol": {
|
|
||||||
"waktu": true,
|
|
||||||
"sensor": false,
|
|
||||||
"otomatis": true,
|
|
||||||
|
|
||||||
"jadwal_1": {
|
|
||||||
"aktif": true,
|
|
||||||
"waktu": "08:00",
|
|
||||||
"durasi": 60,
|
|
||||||
"pot_aktif": [1, 2, 3],
|
|
||||||
"pompa_air": true,
|
|
||||||
"pompa_pupuk": false
|
|
||||||
},
|
|
||||||
"jadwal_2": {
|
|
||||||
"aktif": true,
|
|
||||||
"waktu": "09:00",
|
|
||||||
"durasi": 45,
|
|
||||||
"pot_aktif": [4, 5],
|
|
||||||
"pompa_air": true,
|
|
||||||
"pompa_pupuk": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚀 Cara Setup
|
|
||||||
|
|
||||||
### 1. Copy Struktur Firebase
|
|
||||||
|
|
||||||
1. Buka Firebase Console → Realtime Database
|
|
||||||
2. Edit `/kontrol` node
|
|
||||||
3. Tambahkan jadwal sesuai contoh di atas
|
|
||||||
4. Atau import dari `firebase-structure-complete.json`
|
|
||||||
|
|
||||||
### 2. Update Dependency (Jika Perlu)
|
|
||||||
|
|
||||||
Pastikan `pubspec.yaml` sudah punya:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
dependencies:
|
|
||||||
firebase_database: ^latest_version
|
|
||||||
shared_preferences: ^latest_version
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Update Navigation
|
|
||||||
|
|
||||||
File `lib/screens/kontrol_page.dart` sudah diupdate untuk navigasi ke JadwalManagementPage.
|
|
||||||
|
|
||||||
## 📱 Cara Menggunakan di App
|
|
||||||
|
|
||||||
### Akses Jadwal Management
|
|
||||||
|
|
||||||
1. Buka app ApsGo
|
|
||||||
2. Tap tab **Kontrol**
|
|
||||||
3. Tap button **Waktu**
|
|
||||||
4. Akan muncul **Jadwal Management Page**
|
|
||||||
|
|
||||||
### Tambah Jadwal Baru
|
|
||||||
|
|
||||||
1. Di Jadwal Management Page, tap tombol **+ Tambah Jadwal**
|
|
||||||
2. Isi form:
|
|
||||||
- **Waktu**: Pilih jam berapa jadwal akan jalan
|
|
||||||
- **Durasi**: Berapa menit penyiraman (auto convert ke detik)
|
|
||||||
- **Pilih Pot**: Centang pot mana yang akan disiram
|
|
||||||
- **Pompa Air**: Toggle on/off
|
|
||||||
- **Pompa Pupuk**: Toggle on/off
|
|
||||||
- **Status Jadwal**: Aktif or nonaktif
|
|
||||||
3. Tap **TAMBAH JADWAL**
|
|
||||||
|
|
||||||
### Edit Jadwal
|
|
||||||
|
|
||||||
1. Di list jadwal, tap button **Edit**
|
|
||||||
2. Ubah settingan yang diinginkan
|
|
||||||
3. Tap **SIMPAN PERUBAHAN**
|
|
||||||
|
|
||||||
### Aktifkan/Nonaktifkan Jadwal
|
|
||||||
|
|
||||||
Gunakan **Switch** di kanan atas setiap jadwal card untuk toggle on/off cepat.
|
|
||||||
|
|
||||||
### Duplikat Jadwal
|
|
||||||
|
|
||||||
1. Tap button **Duplikat** di jadwal yang ingin dicopy
|
|
||||||
2. Jadwal baru otomatis dibuat dengan ID baru (status: nonaktif)
|
|
||||||
3. Edit jadwal baru sesuai kebutuhan
|
|
||||||
|
|
||||||
### Hapus Jadwal
|
|
||||||
|
|
||||||
1. Tap icon **🗑️** (trash) di jadwal yang ingin dihapus
|
|
||||||
2. Konfirmasi penghapusan
|
|
||||||
|
|
||||||
## 🎨 UI Features
|
|
||||||
|
|
||||||
### Jadwal Management Page
|
|
||||||
|
|
||||||
- **Mode Waktu Toggle**: Enable/disable semua jadwal sekaligus
|
|
||||||
- **Jadwal Cards**: Menampilkan semua jadwal dengan info lengkap
|
|
||||||
- **Color Coding**: Hijau = aktif, Abu-abu = nonaktif
|
|
||||||
- **Real-time Updates**: Pull to refresh untuk sync terbaru
|
|
||||||
|
|
||||||
### Jadwal Form Page
|
|
||||||
|
|
||||||
- **Time Picker**: Native Android/iOS time picker
|
|
||||||
- **Duration Input**: Input menit, auto convert ke detik
|
|
||||||
- **Pot Selection**: Visual checkboxes untuk 5 pot
|
|
||||||
- **Quick Actions**: "Pilih Semua" & "Bersihkan"
|
|
||||||
- **Validation**: Auto validasi sebelum save
|
|
||||||
|
|
||||||
## ⚠️ Catatan Penting
|
|
||||||
|
|
||||||
### Kompatibilitas dengan Worker
|
|
||||||
|
|
||||||
Worker Railway sudah updated untuk support format baru. Kedua sistem (lama & baru) masih compatible:
|
|
||||||
|
|
||||||
**Format Lama** (masih jalan):
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"waktu_1": "08:00",
|
|
||||||
"durasi_1": 60
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Format Baru** (recommended):
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"jadwal_1": {
|
|
||||||
"aktif": true,
|
|
||||||
"waktu": "08:00",
|
|
||||||
"durasi": 60,
|
|
||||||
"pot_aktif": [1, 2, 3],
|
|
||||||
"pompa_air": true,
|
|
||||||
"pompa_pupuk": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Migration Path
|
|
||||||
|
|
||||||
Tidak perlu migrate semua sekaligus. Sistem bisa pakai campuran format lama & baru.
|
|
||||||
|
|
||||||
**Recommended migration:**
|
|
||||||
1. Biarkan format lama tetap jalan
|
|
||||||
2. Tambah jadwal baru dengan format baru
|
|
||||||
3. Test jadwal baru beberapa hari
|
|
||||||
4. Hapus format lama setelah yakin
|
|
||||||
|
|
||||||
## 🔧 Troubleshooting
|
|
||||||
|
|
||||||
### Jadwal tidak muncul di app
|
|
||||||
|
|
||||||
**Check:**
|
|
||||||
1. Firebase connected? (Check Firebase console)
|
|
||||||
2. Struktur Firebase benar?
|
|
||||||
3. Node path: `/kontrol/jadwal_X`
|
|
||||||
|
|
||||||
### Jadwal tidak trigger di worker
|
|
||||||
|
|
||||||
**Check:**
|
|
||||||
1. Worker running di Railway?
|
|
||||||
2. `kontrol.waktu` = `true`?
|
|
||||||
3. `jadwal_X.aktif` = `true`?
|
|
||||||
4. Format waktu "HH:MM" (2 digit)?
|
|
||||||
5. Check Railway logs
|
|
||||||
|
|
||||||
### Pot tidak menyala
|
|
||||||
|
|
||||||
**Check:**
|
|
||||||
1. `pot_aktif` array benar? `[1, 2, 3]`
|
|
||||||
2. Pot number 1-5 (bukan 0-4)
|
|
||||||
3. Check aktuator di Firebase
|
|
||||||
4. Hardware connection OK?
|
|
||||||
|
|
||||||
## 📊 Testing Checklist
|
|
||||||
|
|
||||||
Sebelum production, test:
|
|
||||||
|
|
||||||
- [ ] Add jadwal baru
|
|
||||||
- [ ] Edit jadwal existing
|
|
||||||
- [ ] Toggle jadwal aktif/nonaktif
|
|
||||||
- [ ] Duplikat jadwal
|
|
||||||
- [ ] Hapus jadwal
|
|
||||||
- [ ] Toggle mode waktu on/off
|
|
||||||
- [ ] Test jadwal trigger di waktu yang ditentukan
|
|
||||||
- [ ] Test multiple pot selection
|
|
||||||
- [ ] Test pompa air on/off per jadwal
|
|
||||||
- [ ] Test pompa pupuk on/off per jadwal
|
|
||||||
- [ ] Test pull to refresh
|
|
||||||
- [ ] Test dengan 5+ jadwal sekaligus
|
|
||||||
|
|
||||||
## 🎯 Contoh Use Case
|
|
||||||
|
|
||||||
### Use Case 1: Rumah simple
|
|
||||||
|
|
||||||
**Kebutuhan**: Siram semua pot 2x sehari (pagi & sore)
|
|
||||||
|
|
||||||
**Setup**:
|
|
||||||
```
|
|
||||||
Jadwal 1: 08:00 → Pot [1,2,3,4,5] → 60s
|
|
||||||
Jadwal 2: 16:00 → Pot [1,2,3,4,5] → 60s
|
|
||||||
```
|
|
||||||
|
|
||||||
### Use Case 2: Per-pot berbeda
|
|
||||||
|
|
||||||
**Kebutuhan**:
|
|
||||||
- Pot 1&2: 3x sehari (pagi, siang, sore)
|
|
||||||
- Pot 3&4: 2x sehari (pagi, sore)
|
|
||||||
- Pot 5: 1x sehari (pagi)
|
|
||||||
|
|
||||||
**Setup**:
|
|
||||||
```
|
|
||||||
Jadwal 1: 07:00 → Pot [1,2] → 60s
|
|
||||||
Jadwal 2: 08:00 → Pot [3,4,5] → 60s
|
|
||||||
Jadwal 3: 12:00 → Pot [1,2] → 45s
|
|
||||||
Jadwal 4: 16:00 → Pot [3,4] → 50s
|
|
||||||
Jadwal 5: 17:00 → Pot [1,2] → 40s
|
|
||||||
```
|
|
||||||
|
|
||||||
### Use Case 3: Different durations
|
|
||||||
|
|
||||||
**Kebutuhan**: Setiap pot punya durasi berbeda
|
|
||||||
|
|
||||||
**Setup**:
|
|
||||||
```
|
|
||||||
Jadwal 1: 08:00 → Pot [1] → 30s (Fast)
|
|
||||||
Jadwal 2: 08:05 → Pot [2] → 60s (Medium)
|
|
||||||
Jadwal 3: 08:10 → Pot [3] → 90s (Long)
|
|
||||||
Jadwal 4: 08:15 → Pot [4] → 45s
|
|
||||||
Jadwal 5: 08:20 → Pot [5] → 75s
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📝 Best Practices
|
|
||||||
|
|
||||||
1. **Naming Convention**: Gunakan `jadwal_1`, `jadwal_2`, dst (agar auto-detect worker)
|
|
||||||
2. **Time Format**: Selalu 2 digit "HH:MM" (misal: "08:00" bukan "8:0")
|
|
||||||
3. **Duration**: Input dalam menit di app, akan auto convert ke detik
|
|
||||||
4. **pot_aktif**: Gunakan array number [1, 2, 3], bukan string
|
|
||||||
5. **Testing**: Test jadwal baru dengan status nonaktif dulu sebelum aktifkan
|
|
||||||
|
|
||||||
## 🔗 Related Files
|
|
||||||
|
|
||||||
- Worker: `railway-worker/worker.js` (sudah updated)
|
|
||||||
- Firebase Guide: `railway-worker/FLEXIBLE_SCHEDULE_GUIDE.md`
|
|
||||||
- Example Firebase: `firebase-structure-complete.json`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💡 Tips
|
|
||||||
|
|
||||||
- Gunakan **Duplikat** untuk cepat bikin jadwal mirip
|
|
||||||
- **Pull to refresh** untuk sync perubahan dari Firebase/Worker
|
|
||||||
- **Mode Waktu toggle** untuk disable semua jadwal sekaligus
|
|
||||||
- Jadwal dengan **pot kosong** akan di-skip otomatis
|
|
||||||
|
|
||||||
Selamat menggunakan sistem penjadwalan baru! 🚀🌱
|
|
||||||
|
|
@ -1,185 +0,0 @@
|
||||||
# 🚀 Quick Setup Firebase Jadwal ke kontrol_1
|
|
||||||
|
|
||||||
## ✅ Yang Sudah Diupdate
|
|
||||||
|
|
||||||
1. ✅ **Worker.js** - Sekarang pakai path `/kontrol_1`
|
|
||||||
2. ✅ **jadwal_service.dart** - Flutter app pakai path `/kontrol_1`
|
|
||||||
3. ✅ **Setup script** - Script untuk push data otomatis ke Firebase
|
|
||||||
|
|
||||||
## 🔥 Cara Setup Data ke Firebase
|
|
||||||
|
|
||||||
### Opsi 1: Via Script (Recommended - Otomatis)
|
|
||||||
|
|
||||||
Jalankan script untuk langsung push data ke Firebase:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd f:\ApsGo\ApsGo\railway-worker
|
|
||||||
node setup-firebase-jadwal.js
|
|
||||||
```
|
|
||||||
|
|
||||||
Script akan:
|
|
||||||
- ✅ Connect ke Firebase kamu
|
|
||||||
- ✅ Push data jadwal ke `/kontrol_1`
|
|
||||||
- ✅ Setup 5 jadwal (3 aktif, 2 nonaktif)
|
|
||||||
- ✅ Log summary hasil setup
|
|
||||||
|
|
||||||
**Output yang diharapkan:**
|
|
||||||
```
|
|
||||||
🚀 Starting Firebase setup...
|
|
||||||
📍 Path: /kontrol_1
|
|
||||||
✅ Firebase setup completed successfully!
|
|
||||||
📊 Summary:
|
|
||||||
- Path: /kontrol_1
|
|
||||||
- Mode Waktu: ENABLED
|
|
||||||
- Total Jadwal: 5
|
|
||||||
- Jadwal Aktif: 3
|
|
||||||
|
|
||||||
📝 Jadwal yang di-setup:
|
|
||||||
✅ jadwal_1: 08:00 → Pot [1, 2, 3] (60s)
|
|
||||||
✅ jadwal_2: 09:00 → Pot [4, 5] (45s)
|
|
||||||
✅ jadwal_3: 16:00 → Pot [1, 2, 3, 4, 5] (30s)
|
|
||||||
❌ jadwal_4: 12:00 → Pot [2, 4] (40s)
|
|
||||||
❌ jadwal_5: 18:00 → Pot [1, 5] (50s)
|
|
||||||
|
|
||||||
🎉 Done! Data sudah tersimpan di Firebase.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Opsi 2: Manual via Firebase Console
|
|
||||||
|
|
||||||
1. Buka [Firebase Console](https://console.firebase.google.com)
|
|
||||||
2. Pilih project ApsGo
|
|
||||||
3. Klik **Realtime Database**
|
|
||||||
4. Klik `+` untuk add new node
|
|
||||||
5. Name: `kontrol_1`
|
|
||||||
6. Copy-paste JSON dari `firebase-structure-complete.json`
|
|
||||||
|
|
||||||
## 📱 Test Flutter App
|
|
||||||
|
|
||||||
Setelah data di-setup, test Flutter app:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd f:\ApsGo\ApsGo
|
|
||||||
flutter pub get
|
|
||||||
flutter run
|
|
||||||
```
|
|
||||||
|
|
||||||
**Test checklist:**
|
|
||||||
- [ ] Buka tab **Kontrol**
|
|
||||||
- [ ] Tap button **Waktu**
|
|
||||||
- [ ] Lihat list jadwal (harus ada 5 jadwal)
|
|
||||||
- [ ] Toggle mode waktu on/off
|
|
||||||
- [ ] Tambah jadwal baru
|
|
||||||
- [ ] Edit jadwal existing
|
|
||||||
- [ ] Delete jadwal
|
|
||||||
|
|
||||||
## 🔄 Deploy ke Railway
|
|
||||||
|
|
||||||
Setelah verify lokal OK, push ke Railway:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd railway-worker
|
|
||||||
git add .
|
|
||||||
git commit -m "feat: Update to use kontrol_1 path + flexible jadwal system"
|
|
||||||
git push origin main
|
|
||||||
```
|
|
||||||
|
|
||||||
Railway akan auto-deploy dalam 2-3 menit.
|
|
||||||
|
|
||||||
## 📊 Verify di Railway Logs
|
|
||||||
|
|
||||||
Setelah deploy, check logs Railway:
|
|
||||||
|
|
||||||
```
|
|
||||||
🚀 Starting ApsGo Railway Worker...
|
|
||||||
📍 Kontrol Path: /kontrol_1
|
|
||||||
✅ Firebase /kontrol_1 readable - waktu mode: ENABLED
|
|
||||||
📋 Total Jadwal: 3
|
|
||||||
✅ jadwal_1: 08:00 → Pot [1, 2, 3]
|
|
||||||
✅ jadwal_2: 09:00 → Pot [4, 5]
|
|
||||||
✅ jadwal_3: 16:00 → Pot [1, 2, 3, 4, 5]
|
|
||||||
```
|
|
||||||
|
|
||||||
## ⚠️ Troubleshooting
|
|
||||||
|
|
||||||
### Script error: "Firebase initialization failed"
|
|
||||||
|
|
||||||
**Check:**
|
|
||||||
1. File `.env` ada di folder `railway-worker`?
|
|
||||||
2. Environment variables lengkap?
|
|
||||||
```
|
|
||||||
FIREBASE_PROJECT_ID=...
|
|
||||||
FIREBASE_CLIENT_EMAIL=...
|
|
||||||
FIREBASE_PRIVATE_KEY=...
|
|
||||||
FIREBASE_DATABASE_URL=...
|
|
||||||
```
|
|
||||||
|
|
||||||
### Flutter app: "No jadwal found"
|
|
||||||
|
|
||||||
**Check:**
|
|
||||||
1. Path di `jadwal_service.dart` = `kontrol_1`? ✅ (sudah diupdate)
|
|
||||||
2. Data sudah di-push ke Firebase?
|
|
||||||
3. Firebase rules allow read?
|
|
||||||
|
|
||||||
### Worker tidak detect jadwal
|
|
||||||
|
|
||||||
**Check:**
|
|
||||||
1. Worker sudah redeploy dengan code terbaru?
|
|
||||||
2. Path di worker = `kontrol_1`? ✅ (sudah diupdate)
|
|
||||||
3. Railway logs show path `/kontrol_1`?
|
|
||||||
|
|
||||||
## 🔧 Ubah Path Kembali ke /kontrol
|
|
||||||
|
|
||||||
Jika mau pakai path `/kontrol` (bukan `/kontrol_1`):
|
|
||||||
|
|
||||||
**Worker.js:**
|
|
||||||
```javascript
|
|
||||||
const FIREBASE_PATHS = {
|
|
||||||
kontrol: 'kontrol', // Ubah dari 'kontrol_1' ke 'kontrol'
|
|
||||||
```
|
|
||||||
|
|
||||||
**jadwal_service.dart:**
|
|
||||||
```dart
|
|
||||||
static const String kontrolPath = 'kontrol'; // Ubah dari 'kontrol_1'
|
|
||||||
```
|
|
||||||
|
|
||||||
**Setup script:**
|
|
||||||
```javascript
|
|
||||||
await db.ref('kontrol').set(kontrolData); // Ubah dari 'kontrol_1'
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📋 Summary Changes
|
|
||||||
|
|
||||||
**Files modified:**
|
|
||||||
- ✅ `railway-worker/worker.js` - Added FIREBASE_PATHS constant, updated all refs
|
|
||||||
- ✅ `lib/services/jadwal_service.dart` - Updated path to kontrol_1
|
|
||||||
- ✅ `railway-worker/setup-firebase-jadwal.js` - New script untuk setup
|
|
||||||
|
|
||||||
**Data structure:**
|
|
||||||
```
|
|
||||||
Firebase Root
|
|
||||||
├── aktuator/
|
|
||||||
├── data/
|
|
||||||
├── history/
|
|
||||||
├── kontrol/ ← Your existing data (unchanged)
|
|
||||||
└── kontrol_1/ ← New data with flexible jadwal
|
|
||||||
├── waktu: true
|
|
||||||
├── sensor: false
|
|
||||||
├── jadwal_1/
|
|
||||||
├── jadwal_2/
|
|
||||||
└── jadwal_3/
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🎯 Next Steps
|
|
||||||
|
|
||||||
1. ✅ Run setup script: `node setup-firebase-jadwal.js`
|
|
||||||
2. ✅ Verify data di Firebase Console
|
|
||||||
3. ✅ Test Flutter app locally
|
|
||||||
4. ✅ Commit & push to Railway
|
|
||||||
5. ✅ Monitor Railway logs
|
|
||||||
6. ✅ Test jadwal trigger di waktu yang ditentukan
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Ready to go!** 🚀🌱
|
|
||||||
|
|
||||||
Jika ada error atau pertanyaan, check logs atau tanya!
|
|
||||||
|
|
@ -1,111 +0,0 @@
|
||||||
# 🚀 Quick Start - Railway Worker Setup
|
|
||||||
|
|
||||||
## ✅ Apa yang Sudah Dibuat
|
|
||||||
|
|
||||||
### 1. Railway Worker (Node.js)
|
|
||||||
```
|
|
||||||
railway-worker/
|
|
||||||
├── worker.js # Main worker code
|
|
||||||
├── package.json # Dependencies
|
|
||||||
├── railway.json # Railway config
|
|
||||||
├── .env.example # Environment template
|
|
||||||
└── README.md # Worker documentation
|
|
||||||
```
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
- ✅ Waktu Mode (time-based scheduling)
|
|
||||||
- ✅ Sensor Mode (threshold automation)
|
|
||||||
- ✅ Auto history logging (10 min)
|
|
||||||
- ✅ Redis queue (prevent race conditions)
|
|
||||||
- ✅ Graceful shutdown & error handling
|
|
||||||
|
|
||||||
### 2. Flutter Bug Fixes
|
|
||||||
- ✅ Memory leak fixed (StreamSubscription disposal)
|
|
||||||
- ✅ AppLifecycle observer (stop services saat background)
|
|
||||||
- ✅ Connection monitoring service
|
|
||||||
- ✅ Constants untuk configuration
|
|
||||||
- ✅ Improved error handling
|
|
||||||
|
|
||||||
### 3. Documentation
|
|
||||||
- ✅ `DEPLOYMENT_GUIDE.md` - Langkah deploy ke Railway
|
|
||||||
- ✅ `BUGS_AND_FIXES.md` - Bug report & fixes
|
|
||||||
- ✅ `railway-worker/README.md` - Worker docs
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Langkah Deployment (Ringkas)
|
|
||||||
|
|
||||||
### Step 1: Firebase Setup
|
|
||||||
1. Download Service Account Key dari Firebase Console
|
|
||||||
2. Simpan: `project_id`, `client_email`, `private_key`
|
|
||||||
|
|
||||||
### Step 2: Railway Setup
|
|
||||||
1. Login ke [railway.app](https://railway.app)
|
|
||||||
2. Create new project → Deploy from GitHub
|
|
||||||
3. Add Redis database
|
|
||||||
4. Set root directory: `railway-worker`
|
|
||||||
|
|
||||||
### Step 3: Environment Variables
|
|
||||||
Di Railway, tambahkan:
|
|
||||||
- `FIREBASE_PROJECT_ID`
|
|
||||||
- `FIREBASE_CLIENT_EMAIL`
|
|
||||||
- `FIREBASE_PRIVATE_KEY`
|
|
||||||
- `FIREBASE_DATABASE_URL`
|
|
||||||
|
|
||||||
### Step 4: Deploy!
|
|
||||||
Railway auto-deploy setelah variables di-set.
|
|
||||||
|
|
||||||
### Step 5: Test
|
|
||||||
- Check logs untuk "Worker is running"
|
|
||||||
- Test waktu mode dari Flutter app
|
|
||||||
- Test sensor mode
|
|
||||||
- Monitor logs
|
|
||||||
|
|
||||||
**📖 Detail lengkap:** Lihat `DEPLOYMENT_GUIDE.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Kesimpulan
|
|
||||||
|
|
||||||
### Masalah yang Diselesaikan
|
|
||||||
1. ✅ **Scheduling hanya jalan saat app buka** → Sekarang 24/7 dengan Railway
|
|
||||||
2. ✅ **Memory leaks** → Fixed dengan proper disposal
|
|
||||||
3. ✅ **Race conditions** → Fixed dengan BullMQ queue
|
|
||||||
4. ✅ **No connection check** → Added monitoring service
|
|
||||||
|
|
||||||
### Arsitektur Baru
|
|
||||||
```
|
|
||||||
Flutter App ↔ Firebase RTDB ↔ Railway Worker (24/7) ↔ ESP32
|
|
||||||
↓
|
|
||||||
Redis Queue
|
|
||||||
```
|
|
||||||
|
|
||||||
### Biaya
|
|
||||||
- Railway Free Tier: $5/month credit (cukup untuk IoT project)
|
|
||||||
- Estimated usage: $3-5/month
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🆘 Troubleshooting
|
|
||||||
|
|
||||||
**Worker tidak start:**
|
|
||||||
- Check `FIREBASE_PRIVATE_KEY` format (harus ada `\n`)
|
|
||||||
- Verify Redis service aktif
|
|
||||||
|
|
||||||
**Jadwal tidak trigger:**
|
|
||||||
- Check timezone (worker use UTC, convert dari local)
|
|
||||||
- Verify `/kontrol/waktu` = true
|
|
||||||
|
|
||||||
**Detail:** Lihat DEPLOYMENT_GUIDE.md section Troubleshooting
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 Support
|
|
||||||
|
|
||||||
- Railway Discord: discord.gg/railway
|
|
||||||
- Firebase Support: firebase.google.com/support
|
|
||||||
- Project Issues: GitHub repository
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Selamat! Sistem Anda sekarang production-ready dengan automation 24/7! 🎉**
|
|
||||||
|
|
@ -1,226 +0,0 @@
|
||||||
# 🚨 URGENT: Railway Worker Setup - Step by Step
|
|
||||||
|
|
||||||
## ❌ Masalah: Tidak Ada Log di Railway
|
|
||||||
|
|
||||||
Berdasarkan screenshot Firebase Anda, **data kontrol sudah BENAR** ✅:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"waktu": true,
|
|
||||||
"waktu_1": "06:41",
|
|
||||||
"waktu_2": "16:38",
|
|
||||||
"durasi_1": 600,
|
|
||||||
"durasi_2": 104
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Root Cause:** Railway Worker belum running atau tidak bisa akses Firebase.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ SOLUSI LENGKAP
|
|
||||||
|
|
||||||
### 🎯 Opsi 1: Test Lokal Dulu (RECOMMENDED)
|
|
||||||
|
|
||||||
Sebelum deploy ke Railway, test koneksi Firebase di komputer Anda:
|
|
||||||
|
|
||||||
#### Step 1: Download Firebase Service Account Key
|
|
||||||
|
|
||||||
1. Buka [Firebase Console](https://console.firebase.google.com)
|
|
||||||
2. Pilih project **Project TA** (project-ta-951b4)
|
|
||||||
3. Klik ⚙️ **Project Settings** (pojok kiri bawah)
|
|
||||||
4. Tab **Service Accounts**
|
|
||||||
5. Klik **Generate New Private Key**
|
|
||||||
6. Download file JSON (jangan share ke siapapun!)
|
|
||||||
|
|
||||||
#### Step 2: Setup Environment Variables Lokal
|
|
||||||
|
|
||||||
1. Buka file JSON yang di-download
|
|
||||||
2. Copy 3 nilai ini:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"project_id": "project-ta-951b4",
|
|
||||||
"private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
|
|
||||||
"client_email": "firebase-adminsdk-xxxxx@project-ta-951b4.iam.gserviceaccount.com"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Buat file `.env` di folder `railway-worker/`:
|
|
||||||
```bash
|
|
||||||
cd f:\ApsGo\ApsGo\railway-worker
|
|
||||||
copy .env.template .env
|
|
||||||
notepad .env
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Isi dengan credentials dari JSON:
|
|
||||||
```env
|
|
||||||
FIREBASE_PROJECT_ID=project-ta-951b4
|
|
||||||
FIREBASE_CLIENT_EMAIL=firebase-adminsdk-xxxxx@project-ta-951b4.iam.gserviceaccount.com
|
|
||||||
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEv...\n-----END PRIVATE KEY-----\n"
|
|
||||||
FIREBASE_DATABASE_URL=https://project-ta-951b4-default-rtdb.firebaseio.com
|
|
||||||
```
|
|
||||||
|
|
||||||
**PENTING untuk FIREBASE_PRIVATE_KEY:**
|
|
||||||
- Harus ada quotes: `"..."`
|
|
||||||
- Harus ada `\n` di awal dan akhir
|
|
||||||
- Jangan ubah format dari JSON
|
|
||||||
|
|
||||||
#### Step 3: Test Koneksi Firebase
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
cd f:\ApsGo\ApsGo\railway-worker
|
|
||||||
npm run test:firebase
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected output:**
|
|
||||||
```
|
|
||||||
✅ All required environment variables are set
|
|
||||||
✅ Firebase Admin SDK initialized
|
|
||||||
✅ Successfully read /kontrol data
|
|
||||||
✅ Waktu mode is ENABLED
|
|
||||||
✅ Jadwal 1: 06:41
|
|
||||||
✅ Jadwal 2: 16:38
|
|
||||||
```
|
|
||||||
|
|
||||||
**Jika berhasil:**
|
|
||||||
- Credentials Anda benar ✅
|
|
||||||
- Firebase accessible ✅
|
|
||||||
- Siap deploy ke Railway ✅
|
|
||||||
|
|
||||||
**Jika error:**
|
|
||||||
- Check private key format (harus ada quotes dan \n)
|
|
||||||
- Check client email benar
|
|
||||||
- Check Firebase Database rules allow admin access
|
|
||||||
|
|
||||||
#### Step 4: Test Worker Lokal (Optional)
|
|
||||||
|
|
||||||
Jika koneksi OK, test worker secara lokal:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Install Redis lokal (optional, skip jika tidak punya)
|
|
||||||
# Atau comment out Redis parts di worker.js untuk test
|
|
||||||
|
|
||||||
# Run worker
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Tunggu sampai waktu schedule (06:41 atau 16:38), worker akan trigger otomatis.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🎯 Opsi 2: Deploy Langsung ke Railway
|
|
||||||
|
|
||||||
Jika test lokal berhasil, deploy ke Railway:
|
|
||||||
|
|
||||||
#### Step 1: Commit Changes
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
cd f:\ApsGo\ApsGo
|
|
||||||
git add .
|
|
||||||
git commit -m "Update Railway Worker configuration"
|
|
||||||
git push origin main
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Step 2: Setup Railway Environment Variables
|
|
||||||
|
|
||||||
1. Login ke [Railway Dashboard](https://railway.app)
|
|
||||||
2. Pilih project ApsGo
|
|
||||||
3. Klik service **worker**
|
|
||||||
4. Tab **Variables**
|
|
||||||
5. Tambahkan 4 variables (gunakan nilai yang sama dari .env lokal):
|
|
||||||
|
|
||||||
| Variable | Value |
|
|
||||||
|----------|-------|
|
|
||||||
| `FIREBASE_PROJECT_ID` | `project-ta-951b4` |
|
|
||||||
| `FIREBASE_CLIENT_EMAIL` | `firebase-adminsdk-xxxxx@project-ta-951b4.iam.gserviceaccount.com` |
|
|
||||||
| `FIREBASE_PRIVATE_KEY` | `"-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"` |
|
|
||||||
| `FIREBASE_DATABASE_URL` | `https://project-ta-951b4-default-rtdb.firebaseio.com` |
|
|
||||||
|
|
||||||
**CRITICAL:** Copy-paste EXACT values from your working local .env file!
|
|
||||||
|
|
||||||
#### Step 3: Redeploy
|
|
||||||
|
|
||||||
Railway will auto-redeploy after adding variables, or manually:
|
|
||||||
1. Tab **Deployments**
|
|
||||||
2. Klik **...** (three dots)
|
|
||||||
3. **Redeploy**
|
|
||||||
|
|
||||||
#### Step 4: Check Logs
|
|
||||||
|
|
||||||
Tab **Logs**, expected output:
|
|
||||||
```
|
|
||||||
🚀 Starting ApsGo Railway Worker...
|
|
||||||
✅ All required environment variables are set
|
|
||||||
✅ Firebase Admin initialized
|
|
||||||
✅ Redis connected
|
|
||||||
✨ ApsGo Railway Worker is running!
|
|
||||||
⏰ Checking scheduled watering...
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔍 Quick Troubleshooting
|
|
||||||
|
|
||||||
### Error: "Missing environment variables"
|
|
||||||
→ .env file tidak ada atau belum diisi
|
|
||||||
→ **Fix:** Ikuti Step 2 di atas
|
|
||||||
|
|
||||||
### Error: "Firebase initialization failed"
|
|
||||||
→ Private key format salah
|
|
||||||
→ **Fix:** Copy EXACT value dari JSON, dengan quotes dan \n
|
|
||||||
|
|
||||||
### Error: "PERMISSION_DENIED"
|
|
||||||
→ Firebase rules tidak allow service account
|
|
||||||
→ **Fix:** Check Firebase Realtime Database Rules
|
|
||||||
|
|
||||||
### No Error, tapi No Logs
|
|
||||||
→ Worker belum deploy/running
|
|
||||||
→ **Fix:** Check Railway Deployments tab
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 Verifikasi Data Firebase (From Your Screenshot)
|
|
||||||
|
|
||||||
✅ Your Firebase data is **CORRECT**:
|
|
||||||
```
|
|
||||||
kontrol/
|
|
||||||
├─ waktu: true ✅ Mode enabled
|
|
||||||
├─ waktu_1: "06:41" ✅ Schedule 1
|
|
||||||
├─ waktu_2: "16:38" ✅ Schedule 2
|
|
||||||
├─ durasi_1: 600 ✅ 10 minutes
|
|
||||||
├─ durasi_2: 104 ✅ 1.7 minutes
|
|
||||||
├─ batas_atas: 80 ✅ Sensor threshold
|
|
||||||
├─ batas_bawah: 50 ✅
|
|
||||||
├─ otomatis: false ℹ️ Sensor mode disabled
|
|
||||||
└─ mode_sensor: "smart" ✅
|
|
||||||
```
|
|
||||||
|
|
||||||
**Flutter app sudah benar!** Masalahnya di Railway Worker yang belum running.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Next Steps
|
|
||||||
|
|
||||||
**MULAI DARI SINI:**
|
|
||||||
|
|
||||||
1. ✅ Test koneksi Firebase lokal:
|
|
||||||
```powershell
|
|
||||||
cd f:\ApsGo\ApsGo\railway-worker
|
|
||||||
npm run test:firebase
|
|
||||||
```
|
|
||||||
|
|
||||||
2. ✅ Jika berhasil, deploy ke Railway (ikuti Opsi 2)
|
|
||||||
|
|
||||||
3. ✅ Monitor Railway logs
|
|
||||||
|
|
||||||
4. ✅ Test dengan set jadwal 2-3 menit dari sekarang
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 Need Help?
|
|
||||||
|
|
||||||
Jika masih stuck:
|
|
||||||
1. Screenshot output dari `npm run test:firebase`
|
|
||||||
2. Screenshot Railway logs (jika sudah deploy)
|
|
||||||
3. Screenshot Firebase Service Account settings
|
|
||||||
|
|
||||||
Mari kita debug bersama!
|
|
||||||
|
|
@ -1,148 +0,0 @@
|
||||||
# ❌ Railway Worker Troubleshooting Guide
|
|
||||||
|
|
||||||
## Masalah: Jadwal tidak berjalan, tidak ada log di Railway
|
|
||||||
|
|
||||||
### ✅ Langkah Diagnosis:
|
|
||||||
|
|
||||||
#### 1. Check Railway Worker Status
|
|
||||||
Di Railway Dashboard:
|
|
||||||
- Masuk ke project ApsGo
|
|
||||||
- Klik service **worker**
|
|
||||||
- Tab **Deployments** → Lihat status deployment terakhir
|
|
||||||
- Tab **Logs** → Periksa apakah worker running
|
|
||||||
|
|
||||||
**Expected logs:**
|
|
||||||
```
|
|
||||||
🚀 Starting ApsGo Railway Worker...
|
|
||||||
📡 Firebase Project: project-ta-951b4
|
|
||||||
📦 Redis: xxxx:6379
|
|
||||||
⏰ Timezone: Asia/Jakarta (Current: 11/02/2026 14:30:00)
|
|
||||||
✅ All required environment variables are set
|
|
||||||
✅ Firebase Admin initialized
|
|
||||||
✅ Redis connected
|
|
||||||
✨ ApsGo Railway Worker is running!
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. Test Environment Variables
|
|
||||||
Di Railway Console, jalankan debug script:
|
|
||||||
```bash
|
|
||||||
# Di Railway service console
|
|
||||||
npm run debug
|
|
||||||
```
|
|
||||||
|
|
||||||
Atau manual check:
|
|
||||||
- Go to **Variables** tab
|
|
||||||
- Pastikan ada 4 variables ini:
|
|
||||||
- `FIREBASE_PROJECT_ID` = project-ta-951b4
|
|
||||||
- `FIREBASE_CLIENT_EMAIL` = firebase-adminsdk-xxxxx@project-ta-951b4.iam.gserviceaccount.com
|
|
||||||
- `FIREBASE_PRIVATE_KEY` = "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
|
|
||||||
- `FIREBASE_DATABASE_URL` = https://project-ta-951b4-default-rtdb.firebaseio.com
|
|
||||||
|
|
||||||
#### 3. Test Firebase Configuration
|
|
||||||
Check apakah Flutter app berhasil menyimpan jadwal:
|
|
||||||
- Buka Firebase Console → Realtime Database
|
|
||||||
- Check node `/kontrol`
|
|
||||||
- Harus ada data seperti:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"waktu": true,
|
|
||||||
"waktu_1": "08:00",
|
|
||||||
"waktu_2": "16:00",
|
|
||||||
"durasi_1": 600,
|
|
||||||
"durasi_2": 600
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4. Time Sync Check
|
|
||||||
- Railway Worker menggunakan timezone Asia/Jakarta
|
|
||||||
- Check apakah jadwal yang di-set di Flutter sesuai dengan waktu sekarang
|
|
||||||
- Test dengan set jadwal 1-2 menit dari waktu sekarang
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ Solusi untuk Masalah Umum
|
|
||||||
|
|
||||||
### Problem: "Firebase initialization failed"
|
|
||||||
**Cause:** Environment variables salah/kurang
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
1. Re-download Service Account Key dari Firebase Console
|
|
||||||
2. Extract 4 values yang dibutuhkan
|
|
||||||
3. Set ulang variables di Railway
|
|
||||||
4. **PENTING:** `FIREBASE_PRIVATE_KEY` harus dalam quotes dan dengan `\n` escaped
|
|
||||||
|
|
||||||
### Problem: "Redis connection error"
|
|
||||||
**Cause:** Redis service tidak active
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
1. Railway Dashboard → Add Redis Database
|
|
||||||
2. Link ke worker service
|
|
||||||
3. Restart worker
|
|
||||||
|
|
||||||
### Problem: "Worker tidak trigger jadwal"
|
|
||||||
**Cause:** Timezone mismatch atau konfigurasi salah
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
1. Check `/kontrol` di Firebase ada data
|
|
||||||
2. Check `waktu: true`
|
|
||||||
3. Check jadwal format "HH:MM" (e.g., "08:00")
|
|
||||||
4. Test dengan jadwal dekat waktu sekarang
|
|
||||||
|
|
||||||
### Problem: "No logs di Railway"
|
|
||||||
**Cause:** Worker crash saat startup
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
1. Check **Deployments** tab - ada error saat build?
|
|
||||||
2. Check **Logs** tab - ada error message?
|
|
||||||
3. Run debug script: `npm run debug`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔍 Debug Commands
|
|
||||||
|
|
||||||
### Local Debug (di komputer Anda)
|
|
||||||
```bash
|
|
||||||
cd railway-worker
|
|
||||||
npm install
|
|
||||||
node debug.js
|
|
||||||
```
|
|
||||||
|
|
||||||
### Railway Debug (di Railway Console)
|
|
||||||
```bash
|
|
||||||
npm run debug
|
|
||||||
```
|
|
||||||
|
|
||||||
### Manual Test (di Railway Console)
|
|
||||||
```bash
|
|
||||||
# Check environment
|
|
||||||
env | grep FIREBASE
|
|
||||||
|
|
||||||
# Test Firebase connection
|
|
||||||
node -e "console.log('DB URL:', process.env.FIREBASE_DATABASE_URL)"
|
|
||||||
|
|
||||||
# Test current time
|
|
||||||
node -e "console.log('Time:', new Date().toLocaleString('id-ID', {timeZone: 'Asia/Jakarta'}))"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚡ Quick Fix Checklist
|
|
||||||
|
|
||||||
- [ ] Railway worker service running (green status)
|
|
||||||
- [ ] All 4 Firebase environment variables set correctly
|
|
||||||
- [ ] Redis database service added and linked
|
|
||||||
- [ ] Firebase Realtime Database rules allow admin access
|
|
||||||
- [ ] Flutter app successfully saved schedule (`/kontrol` node exists)
|
|
||||||
- [ ] Timezone configured as Asia/Jakarta
|
|
||||||
- [ ] Test schedule set within 1-2 minutes from current time
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 Still Having Issues?
|
|
||||||
|
|
||||||
1. Export Railway logs: **Logs** tab → Copy all logs
|
|
||||||
2. Export Firebase `/kontrol` data: Firebase Console → Export JSON
|
|
||||||
3. Screenshot Flutter schedule configuration
|
|
||||||
4. Run `npm run debug` dan copy output
|
|
||||||
|
|
||||||
With this information, the issue can be diagnosed precisely.
|
|
||||||
|
|
@ -1,280 +0,0 @@
|
||||||
# 🔥 Setup Firebase untuk Sistem Jadwal Baru
|
|
||||||
|
|
||||||
## 📋 Copy Struktur Ini ke Firebase
|
|
||||||
|
|
||||||
Buka Firebase Console → Realtime Database → Edit `/kontrol` node:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"batas_atas": 32,
|
|
||||||
"batas_bawah": 11,
|
|
||||||
"durasi_sensor": 600,
|
|
||||||
"mode_sensor": "smart",
|
|
||||||
"otomatis": true,
|
|
||||||
"sensor": false,
|
|
||||||
"waktu": true,
|
|
||||||
|
|
||||||
"jadwal_1": {
|
|
||||||
"aktif": true,
|
|
||||||
"waktu": "08:00",
|
|
||||||
"durasi": 60,
|
|
||||||
"pot_aktif": [1, 2, 3],
|
|
||||||
"pompa_air": true,
|
|
||||||
"pompa_pupuk": false
|
|
||||||
},
|
|
||||||
|
|
||||||
"jadwal_2": {
|
|
||||||
"aktif": true,
|
|
||||||
"waktu": "09:00",
|
|
||||||
"durasi": 45,
|
|
||||||
"pot_aktif": [4, 5],
|
|
||||||
"pompa_air": true,
|
|
||||||
"pompa_pupuk": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"jadwal_3": {
|
|
||||||
"aktif": true,
|
|
||||||
"waktu": "16:00",
|
|
||||||
"durasi": 30,
|
|
||||||
"pot_aktif": [1, 2, 3, 4, 5],
|
|
||||||
"pompa_air": true,
|
|
||||||
"pompa_pupuk": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🎯 Contoh Sesuai Request Kamu
|
|
||||||
|
|
||||||
**Kebutuhan:**
|
|
||||||
- Jam 8 pagi: Pot 1, 2, 3 aktif
|
|
||||||
- Jam 9 pagi: Pot 4, 5 aktif
|
|
||||||
- Jam 4 sore: Pot 1, 2, 3, 4, 5 aktif
|
|
||||||
|
|
||||||
**Setup Firebase:**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"waktu": true,
|
|
||||||
|
|
||||||
"jadwal_1": {
|
|
||||||
"aktif": true,
|
|
||||||
"waktu": "08:00",
|
|
||||||
"durasi": 60,
|
|
||||||
"pot_aktif": [1, 2, 3],
|
|
||||||
"pompa_air": true,
|
|
||||||
"pompa_pupuk": false
|
|
||||||
},
|
|
||||||
|
|
||||||
"jadwal_2": {
|
|
||||||
"aktif": true,
|
|
||||||
"waktu": "09:00",
|
|
||||||
"durasi": 60,
|
|
||||||
"pot_aktif": [4, 5],
|
|
||||||
"pompa_air": true,
|
|
||||||
"pompa_pupuk": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"jadwal_3": {
|
|
||||||
"aktif": true,
|
|
||||||
"waktu": "16:00",
|
|
||||||
"durasi": 60,
|
|
||||||
"pot_aktif": [1, 2, 3, 4, 5],
|
|
||||||
"pompa_air": true,
|
|
||||||
"pompa_pupuk": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## ➕ Cara Tambah Jadwal Baru di Firebase
|
|
||||||
|
|
||||||
### Via Firebase Console (Manual)
|
|
||||||
|
|
||||||
1. Buka Firebase Console
|
|
||||||
2. Go to Realtime Database
|
|
||||||
3. Navigate ke `/kontrol`
|
|
||||||
4. Click `+` untuk add child
|
|
||||||
5. Name: `jadwal_4`
|
|
||||||
6. Add children:
|
|
||||||
|
|
||||||
```
|
|
||||||
jadwal_4:
|
|
||||||
├─ aktif: true
|
|
||||||
├─ waktu: "12:00"
|
|
||||||
├─ durasi: 45
|
|
||||||
├─ pot_aktif: [2, 4]
|
|
||||||
├─ pompa_air: true
|
|
||||||
└─ pompa_pupuk: false
|
|
||||||
```
|
|
||||||
|
|
||||||
### Via Flutter App (Recommended)
|
|
||||||
|
|
||||||
1. Buka ApsGo App
|
|
||||||
2. Tap **Kontrol** tab
|
|
||||||
3. Tap **Waktu** button
|
|
||||||
4. Tap tombol **+ Tambah Jadwal**
|
|
||||||
5. Isi form sesuai kebutuhan
|
|
||||||
6. Tap **TAMBAH JADWAL**
|
|
||||||
|
|
||||||
**Keuntungan via App:**
|
|
||||||
- ✅ Lebih mudah & cepat
|
|
||||||
- ✅ Auto validasi
|
|
||||||
- ✅ Auto generate ID
|
|
||||||
- ✅ Visual pot selection
|
|
||||||
- ✅ Time picker
|
|
||||||
|
|
||||||
## 📱 Cara Test di App
|
|
||||||
|
|
||||||
### 1. Run Flutter App
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd f:\ApsGo\ApsGo
|
|
||||||
flutter run
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Navigate ke Jadwal
|
|
||||||
|
|
||||||
1. Login ke app
|
|
||||||
2. Tap tab **Kontrol** di bottom navigation
|
|
||||||
3. Tap button **Waktu**
|
|
||||||
4. Akan muncul **Jadwal Management Page**
|
|
||||||
|
|
||||||
### 3. Test Features
|
|
||||||
|
|
||||||
- ✅ Lihat semua jadwal
|
|
||||||
- ✅ Toggle mode waktu on/off
|
|
||||||
- ✅ Tambah jadwal baru
|
|
||||||
- ✅ Edit jadwal
|
|
||||||
- ✅ Toggle jadwal aktif/nonaktif
|
|
||||||
- ✅ Duplikat jadwal
|
|
||||||
- ✅ Hapus jadwal
|
|
||||||
- ✅ Pull to refresh
|
|
||||||
|
|
||||||
## 🔄 Migration dari Format Lama
|
|
||||||
|
|
||||||
### Format Lama Kamu (di screenshot):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"waktu": true,
|
|
||||||
"waktu_1": "10:02",
|
|
||||||
"durasi_1": 60,
|
|
||||||
"waktu_2": "16:00",
|
|
||||||
"durasi_2": 600
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Konversi ke Format Baru:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"waktu": true,
|
|
||||||
|
|
||||||
"jadwal_1": {
|
|
||||||
"aktif": true,
|
|
||||||
"waktu": "10:02",
|
|
||||||
"durasi": 60,
|
|
||||||
"pot_aktif": [1, 2, 3, 4, 5],
|
|
||||||
"pompa_air": true,
|
|
||||||
"pompa_pupuk": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"jadwal_2": {
|
|
||||||
"aktif": true,
|
|
||||||
"waktu": "16:00",
|
|
||||||
"durasi": 600,
|
|
||||||
"pot_aktif": [1, 2, 3, 4, 5],
|
|
||||||
"pompa_air": true,
|
|
||||||
"pompa_pupuk": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note:** Format lama masih jalan di worker (backward compatible), jadi tidak perlu buru-buru migrate.
|
|
||||||
|
|
||||||
## ⚙️ Field Explanations
|
|
||||||
|
|
||||||
| Field | Type | Required | Default | Description |
|
|
||||||
|-------|------|----------|---------|-------------|
|
|
||||||
| `aktif` | boolean | No | `true` | Enable/disable jadwal |
|
|
||||||
| `waktu` | string | Yes | - | Format "HH:MM" (08:00, 16:00, dll) |
|
|
||||||
| `durasi` | number | No | `60` | Durasi dalam DETIK |
|
|
||||||
| `pot_aktif` | array | Yes | - | Array pot: [1, 2, 3, 4, 5] |
|
|
||||||
| `pompa_air` | boolean | No | `true` | Nyalakan pompa air |
|
|
||||||
| `pompa_pupuk` | boolean | No | `false` | Nyalakan pompa pupuk |
|
|
||||||
|
|
||||||
## ✅ Validation Rules
|
|
||||||
|
|
||||||
Firebase akan accept jadwal jika:
|
|
||||||
|
|
||||||
1. ✅ `waktu` format "HH:MM" dengan 2 digit
|
|
||||||
2. ✅ `durasi` > 0
|
|
||||||
3. ✅ `pot_aktif` tidak kosong
|
|
||||||
4. ✅ `pot_aktif` berisi angka 1-5
|
|
||||||
|
|
||||||
Worker akan skip jadwal jika:
|
|
||||||
- ❌ `aktif` = false
|
|
||||||
- ❌ `pot_aktif` kosong
|
|
||||||
- ❌ `waktu` format salah
|
|
||||||
|
|
||||||
## 🚀 Deploy Changes
|
|
||||||
|
|
||||||
### 1. Update Flutter App
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd f:\ApsGo\ApsGo
|
|
||||||
flutter pub get
|
|
||||||
flutter build apk --release
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Update Railway Worker (Sudah Done!)
|
|
||||||
|
|
||||||
Worker sudah updated dan di-push ke GitHub.
|
|
||||||
|
|
||||||
### 3. Setup Firebase
|
|
||||||
|
|
||||||
Copy struktur JSON di atas ke Firebase Console.
|
|
||||||
|
|
||||||
### 4. Test Everything
|
|
||||||
|
|
||||||
1. Test add jadwal via app
|
|
||||||
2. Test edit jadwal
|
|
||||||
3. Test toggle aktif/nonaktif
|
|
||||||
4. Wait sampai waktu jadwal
|
|
||||||
5. Check logs Railway untuk verifikasi
|
|
||||||
|
|
||||||
## 📊 Expected Logs Railway
|
|
||||||
|
|
||||||
Setelah setup, di Railway logs kamu akan lihat:
|
|
||||||
|
|
||||||
```
|
|
||||||
⏱️ CHECK #15: 08:00:05 | Mode: ✅
|
|
||||||
📋 Total Jadwal: 3
|
|
||||||
✅ jadwal_1: 08:00 → Pot [1, 2, 3] 🔔 MATCH!
|
|
||||||
✅ jadwal_2: 09:00 → Pot [4, 5]
|
|
||||||
✅ jadwal_3: 16:00 → Pot [1, 2, 3, 4, 5]
|
|
||||||
|
|
||||||
🕐 JADWAL_1 TRIGGERED: 08:00
|
|
||||||
🎯 Pot aktif: [1, 2, 3]
|
|
||||||
⏱️ Durasi: 60s
|
|
||||||
💧 Pompa Air: ON
|
|
||||||
🌿 Pompa Pupuk: OFF
|
|
||||||
✅ Successfully added to queue
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🎉 You're All Set!
|
|
||||||
|
|
||||||
Sistem penjadwalan baru siap digunakan! Fitur:
|
|
||||||
|
|
||||||
✅ Multiple jadwal (unlimited)
|
|
||||||
✅ Flexible pot selection
|
|
||||||
✅ Per-schedule configuration
|
|
||||||
✅ Easy add/edit/delete via app
|
|
||||||
✅ Real-time sync
|
|
||||||
✅ Auto validation
|
|
||||||
|
|
||||||
**Next Steps:**
|
|
||||||
1. Setup Firebase dengan struktur di atas
|
|
||||||
2. Run Flutter app
|
|
||||||
3. Test tambah/edit jadwal
|
|
||||||
4. Monitor Railway logs
|
|
||||||
5. Enjoy! 🚀🌱
|
|
||||||
|
|
@ -1,308 +0,0 @@
|
||||||
# 🚨 SOLUSI LENGKAP: Penjadwalan Tidak Masuk Railway
|
|
||||||
|
|
||||||
## 📋 HASIL ANALISIS
|
|
||||||
|
|
||||||
### ✅ Yang SUDAH BENAR:
|
|
||||||
1. **Flutter Code** - Data penjadwalan ditulis dengan benar ke Firebase
|
|
||||||
2. **Worker Code** - Logic membaca dan execute sudah benar
|
|
||||||
3. **Format Data** - Compatible antara Flutter dan Worker
|
|
||||||
4. **Git Repository** - railway-worker sudah di-commit dan push
|
|
||||||
|
|
||||||
### ❌ KEMUNGKINAN MASALAH:
|
|
||||||
1. **Railway Root Directory belum di-set**
|
|
||||||
2. **Environment Variables belum lengkap/salah**
|
|
||||||
3. **Redis service belum di-add ke Railway**
|
|
||||||
4. **Data penjadwalan belum di-set dari Flutter**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 LANGKAH PERBAIKAN (STEP-BY-STEP)
|
|
||||||
|
|
||||||
### **STEP 1: Verifikasi Data Di Firebase Console**
|
|
||||||
|
|
||||||
1. Buka [Firebase Console](https://console.firebase.google.com)
|
|
||||||
2. Pilih project: **project-ta-951b4**
|
|
||||||
3. Klik **Realtime Database** di sidebar
|
|
||||||
4. Cek path `/kontrol/`
|
|
||||||
|
|
||||||
**Yang HARUS ADA:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"kontrol": {
|
|
||||||
"waktu": true, ← MUST BE TRUE!
|
|
||||||
"waktu_1": "07:30", ← Format HH:mm string
|
|
||||||
"waktu_2": "17:00", ← Format HH:mm string
|
|
||||||
"durasi_1": 60, ← Integer (seconds)
|
|
||||||
"durasi_2": 60, ← Integer (seconds)
|
|
||||||
"otomatis": false,
|
|
||||||
"batas_bawah": 40,
|
|
||||||
"batas_atas": 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Jika data TIDAK ADA atau `waktu` = false:**
|
|
||||||
- Buka Flutter app
|
|
||||||
- Masuk ke **Kontrol Waktu**
|
|
||||||
- Set jadwal
|
|
||||||
- **Toggle "Aktif" menjadi ON**
|
|
||||||
- Klik **Simpan**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **STEP 2: Test Firebase Connection Dari Local**
|
|
||||||
|
|
||||||
Di folder railway-worker, buat file `.env`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd f:\ApsGo\ApsGo\railway-worker
|
|
||||||
```
|
|
||||||
|
|
||||||
Create `.env` file (copy dari .env.example):
|
|
||||||
```env
|
|
||||||
FIREBASE_PROJECT_ID=project-ta-951b4
|
|
||||||
FIREBASE_CLIENT_EMAIL=firebase-adminsdk-xxxxx@project-ta-951b4.iam.gserviceaccount.com
|
|
||||||
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nYourKeyHere\n-----END PRIVATE KEY-----\n"
|
|
||||||
FIREBASE_DATABASE_URL=https://project-ta-951b4-default-rtdb.firebaseio.com
|
|
||||||
```
|
|
||||||
|
|
||||||
**Cara dapat Firebase credentials:**
|
|
||||||
1. Firebase Console → Project Settings
|
|
||||||
2. Service Accounts tab
|
|
||||||
3. Klik "Generate New Private Key"
|
|
||||||
4. Download JSON file
|
|
||||||
5. Copy value dari JSON:
|
|
||||||
- `project_id` → FIREBASE_PROJECT_ID
|
|
||||||
- `client_email` → FIREBASE_CLIENT_EMAIL
|
|
||||||
- `private_key` → FIREBASE_PRIVATE_KEY (ganti \n dengan \\n)
|
|
||||||
- Database URL bisa dari Realtime Database page
|
|
||||||
|
|
||||||
**Test connection:**
|
|
||||||
```powershell
|
|
||||||
cd railway-worker
|
|
||||||
npm install
|
|
||||||
node test-firebase-connection.js
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected output:**
|
|
||||||
```
|
|
||||||
✅ Firebase initialized
|
|
||||||
✅ DATA FOUND! Structure:
|
|
||||||
✅ DATA STRUCTURE VALID
|
|
||||||
✅ WAKTU MODE ENABLED
|
|
||||||
```
|
|
||||||
|
|
||||||
**Jika error:**
|
|
||||||
- Check credentials (terutama PRIVATE_KEY format)
|
|
||||||
- Pastikan Database URL benar
|
|
||||||
- Pastikan Firebase Rules mengizinkan read/write
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **STEP 3: Setup Railway - Root Directory**
|
|
||||||
|
|
||||||
1. **Login ke Railway:** https://railway.app
|
|
||||||
2. **Select Project:** mellow-cat
|
|
||||||
3. **Select Service:** myreppril
|
|
||||||
4. **Klik tab "Settings"**
|
|
||||||
5. **Scroll ke section "Build"**
|
|
||||||
6. **Cari field "Root Directory"**
|
|
||||||
7. **Isi dengan:** `railway-worker`
|
|
||||||
8. **Save** (auto-save)
|
|
||||||
|
|
||||||
**PENTING:** Tanpa ini, Railway akan build dari root folder dan gagal!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **STEP 4: Add Redis Database**
|
|
||||||
|
|
||||||
1. Di Railway project dashboard
|
|
||||||
2. Klik tombol **"+ New"**
|
|
||||||
3. Pilih **"Database"**
|
|
||||||
4. Pilih **"Add Redis"**
|
|
||||||
5. Tunggu provisioning selesai
|
|
||||||
|
|
||||||
Railway akan auto-inject environment variables:
|
|
||||||
- `REDIS_HOST`
|
|
||||||
- `REDIS_PORT`
|
|
||||||
- `REDIS_PASSWORD`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **STEP 5: Set Environment Variables**
|
|
||||||
|
|
||||||
Di Railway → Service myreppril → Tab **"Variables"**
|
|
||||||
|
|
||||||
**ADD THESE:**
|
|
||||||
|
|
||||||
| Variable Name | Value |
|
|
||||||
|--------------|-------|
|
|
||||||
| `FIREBASE_PROJECT_ID` | `project-ta-951b4` |
|
|
||||||
| `FIREBASE_CLIENT_EMAIL` | `firebase-adminsdk-xxxxx@project-ta-951b4.iam.gserviceaccount.com` |
|
|
||||||
| `FIREBASE_PRIVATE_KEY` | `-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----` |
|
|
||||||
| `FIREBASE_DATABASE_URL` | `https://project-ta-951b4-default-rtdb.firebaseio.com` |
|
|
||||||
|
|
||||||
**⚠️ PENTING untuk PRIVATE_KEY:**
|
|
||||||
- Harus dalam format string dengan `\n` (backslash-n), BUKAN newline sebenarnya
|
|
||||||
- Contoh: `-----BEGIN PRIVATE KEY-----\nMIIEvQIBADA...\n-----END PRIVATE KEY-----\n`
|
|
||||||
- Copy dari JSON file service account, tapi ganti newline dengan `\n`
|
|
||||||
|
|
||||||
**Cara mudah:**
|
|
||||||
```javascript
|
|
||||||
// Di PowerShell atau Node.js console
|
|
||||||
const fs = require('fs');
|
|
||||||
const key = JSON.parse(fs.readFileSync('service-account.json')).private_key;
|
|
||||||
console.log(key); // Ini sudah format dengan \n
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **STEP 6: Redeploy Railway**
|
|
||||||
|
|
||||||
1. Setelah set Root Directory & Variables
|
|
||||||
2. Klik tab **"Deployments"**
|
|
||||||
3. Klik **"Redeploy"** atau biarkan auto-deploy
|
|
||||||
4. Tunggu build selesai (2-3 menit)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **STEP 7: Monitor Logs**
|
|
||||||
|
|
||||||
1. Klik tab **"Logs"** (di navigation bar atas)
|
|
||||||
2. Enable "Follow logs" (toggle di kanan atas)
|
|
||||||
|
|
||||||
**Expected logs saat startup:**
|
|
||||||
```
|
|
||||||
🚀 Starting ApsGo Railway Worker...
|
|
||||||
📡 Firebase Project: project-ta-951b4
|
|
||||||
📦 Redis: redis.railway.internal:6379
|
|
||||||
✅ Firebase Admin initialized
|
|
||||||
✅ Redis connected
|
|
||||||
✅ Waktu Mode scheduler started (check every 30s)
|
|
||||||
✅ Sensor Mode monitoring started
|
|
||||||
✨ ApsGo Railway Worker is running!
|
|
||||||
```
|
|
||||||
|
|
||||||
**Jika ada error:**
|
|
||||||
- `Firebase initialization failed` → Check credentials
|
|
||||||
- `Redis error` → Pastikan Redis database sudah di-add
|
|
||||||
- `Cannot find module` → Check Root Directory setting
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **STEP 8: Test Penjadwalan**
|
|
||||||
|
|
||||||
**Cara 1: Set jadwal dekat waktu sekarang**
|
|
||||||
1. Buka Flutter app
|
|
||||||
2. Kontrol Waktu → Set Jadwal 1 = (waktu sekarang + 2 menit)
|
|
||||||
3. Durasi = 10 detik
|
|
||||||
4. **Toggle "Aktif" = ON**
|
|
||||||
5. Simpan
|
|
||||||
6. Tunggu 2 menit
|
|
||||||
7. Cek Railway logs
|
|
||||||
|
|
||||||
**Expected di logs:**
|
|
||||||
```
|
|
||||||
🕐 JADWAL 1 TRIGGERED: 14:30
|
|
||||||
📌 Added to queue: jadwal_1_2026-02-10_14:30
|
|
||||||
💧 Processing Job: jadwal_1_...
|
|
||||||
Type: waktu_jadwal_1
|
|
||||||
Pots: [1, 2, 3, 4, 5]
|
|
||||||
Duration: 10s
|
|
||||||
🔛 Turning ON: mosvet_1, mosvet_3, mosvet_4, mosvet_5, mosvet_6, mosvet_7
|
|
||||||
⏳ Waiting 10s...
|
|
||||||
🔚 Turning OFF: mosvet_1, mosvet_3, mosvet_4, mosvet_5, mosvet_6, mosvet_7
|
|
||||||
✅ Worker completed job jadwal_1_...
|
|
||||||
📊 History logged: 2026-02-10 14:30
|
|
||||||
```
|
|
||||||
|
|
||||||
**Cara 2: Check Firebase langsung**
|
|
||||||
1. Firebase Console → Realtime Database
|
|
||||||
2. Monitor path `/kontrol/waktu`
|
|
||||||
3. Monitor path `/aktuator/` (harus berubah dari false → true → false)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🐛 TROUBLESHOOTING CHECKLIST
|
|
||||||
|
|
||||||
### ❌ "No logs at all in Railway"
|
|
||||||
- [ ] Root directory = `railway-worker`?
|
|
||||||
- [ ] Environment variables complete?
|
|
||||||
- [ ] Redis database added?
|
|
||||||
- [ ] Check Deployments tab for build errors
|
|
||||||
|
|
||||||
### ❌ "Logs show but no schedule triggers"
|
|
||||||
- [ ] Firebase `/kontrol/waktu` = true?
|
|
||||||
- [ ] Jadwal sudah di-set di Flutter?
|
|
||||||
- [ ] Format waktu benar (HH:mm string)?
|
|
||||||
- [ ] Current time match jadwal?
|
|
||||||
|
|
||||||
### ❌ "Firebase initialization failed"
|
|
||||||
- [ ] FIREBASE_PRIVATE_KEY format benar (dengan \n)?
|
|
||||||
- [ ] SERVICE_ACCOUNT email benar?
|
|
||||||
- [ ] Database URL benar?
|
|
||||||
- [ ] Firebase Rules allow read/write?
|
|
||||||
|
|
||||||
### ❌ "Redis connection error"
|
|
||||||
- [ ] Redis database sudah di-add di Railway?
|
|
||||||
- [ ] REDIS_HOST/PORT/PASSWORD auto-injected?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 CARA CEK DATA FIREBASE DARI FLUTTER
|
|
||||||
|
|
||||||
Tambahkan button debug di Flutter (temporary):
|
|
||||||
|
|
||||||
```dart
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () async {
|
|
||||||
final data = await FirebaseDatabase.instance.ref('kontrol').get();
|
|
||||||
print('KONTROL DATA: ${data.value}');
|
|
||||||
},
|
|
||||||
child: Text('Debug: Print Kontrol Data'),
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Check debug console untuk melihat data yang tersimpan.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 QUICK TEST SCRIPT
|
|
||||||
|
|
||||||
Jalankan dari terminal:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Test 1: Check Firebase data
|
|
||||||
cd f:\ApsGo\ApsGo\railway-worker
|
|
||||||
node test-firebase-connection.js
|
|
||||||
|
|
||||||
# Test 2: Check queue (if worker running)
|
|
||||||
node check-queue.js
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 LANGKAH SELANJUTNYA
|
|
||||||
|
|
||||||
Setelah mengikuti STEP 1-8:
|
|
||||||
|
|
||||||
1. **Screenshot Railway Logs** yang muncul
|
|
||||||
2. **Screenshot Firebase Console** (path /kontrol/)
|
|
||||||
3. **Screenshot Railway Settings** (Root Directory & Variables)
|
|
||||||
|
|
||||||
Dengan screenshot tersebut, saya bisa bantu troubleshoot lebih lanjut jika masih ada masalah!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ CHECKLIST FINAL
|
|
||||||
|
|
||||||
Sebelum test:
|
|
||||||
- [ ] Data jadwal ada di Firebase `/kontrol/`
|
|
||||||
- [ ] `waktu` = true di Firebase
|
|
||||||
- [ ] Railway root directory = `railway-worker`
|
|
||||||
- [ ] 4 Firebase env variables di Railway
|
|
||||||
- [ ] Redis database added di Railway
|
|
||||||
- [ ] Worker logs muncul di Railway
|
|
||||||
- [ ] Jadwal set 2-3 menit dari sekarang untuk test
|
|
||||||
|
|
||||||
**Jika semua checked, penjadwalan PASTI jalan!** ✨
|
|
||||||
113
STATUS.md
|
|
@ -1,113 +0,0 @@
|
||||||
# Status Proyek ApsGo
|
|
||||||
|
|
||||||
**Tanggal Update:** 11 Februari 2026
|
|
||||||
|
|
||||||
## ✅ Status Saat Ini
|
|
||||||
|
|
||||||
### Sistem Berjalan
|
|
||||||
- ✅ **Real-time automation** dengan delay 20 detik/section
|
|
||||||
- ✅ Railway Worker aktif 24/7
|
|
||||||
- ✅ Firebase Realtime Database sync
|
|
||||||
- ✅ Mobile app Flutter (Android & iOS ready)
|
|
||||||
- ✅ Auto-recovery dari timeout errors
|
|
||||||
|
|
||||||
### Konfigurasi Terkini
|
|
||||||
- **1 waktu jadwal → banyak pompa sekaligus**
|
|
||||||
- Ketika waktu penjadwalan match (contoh: 12:05)
|
|
||||||
- Semua pompa yang terkonfigurasi akan ON bersamaan
|
|
||||||
- Duration: 600 detik (10 menit) default
|
|
||||||
- Mosvets 1-7 bisa dikontrol simultan
|
|
||||||
|
|
||||||
## 🔧 Komponen Teknis
|
|
||||||
|
|
||||||
### Railway Worker
|
|
||||||
- **Repository:** https://github.com/awisnuu/myreppril.git
|
|
||||||
- **Status:** Deployed & Running
|
|
||||||
- **Features:**
|
|
||||||
- Firebase SDK + REST API fallback
|
|
||||||
- BullMQ job queue (concurrency: 1)
|
|
||||||
- Timezone: Asia/Jakarta (UTC+7)
|
|
||||||
- Health checks: Firebase, Redis, Queue
|
|
||||||
- Auto timeout handling (10s limit)
|
|
||||||
- Job-level timeout (15 min max)
|
|
||||||
|
|
||||||
### Firebase Configuration
|
|
||||||
- **Database:** project-ta-951b4-default-rtdb.firebaseio.com
|
|
||||||
- **Nodes:**
|
|
||||||
- `/kontrol` - Scheduling configuration
|
|
||||||
- `/aktuator` - Real-time device control
|
|
||||||
- `/sensor` - Sensor data
|
|
||||||
- `/history` - Activity logs
|
|
||||||
|
|
||||||
### Mobile App
|
|
||||||
- **Framework:** Flutter
|
|
||||||
- **Auth:** Firebase Auth + Google Sign-In
|
|
||||||
- **Features:**
|
|
||||||
- Manual control page
|
|
||||||
- Schedule configuration (waktu mode)
|
|
||||||
- Real-time sensor monitoring
|
|
||||||
- History viewer
|
|
||||||
|
|
||||||
## ⚠️ Known Issues
|
|
||||||
|
|
||||||
### 1. Multiple Pumps per Schedule
|
|
||||||
**Status:** By Design
|
|
||||||
- Satu waktu penjadwalan mengontrol semua pompa bersamaan
|
|
||||||
- Jika butuh kontrol terpisah, perlu implementasi:
|
|
||||||
- Jadwal terpisah per pompa
|
|
||||||
- Queue scheduling dengan interval
|
|
||||||
- Individual pump selection
|
|
||||||
|
|
||||||
### 2. Delay 20 Detik
|
|
||||||
**Status:** Normal Behavior
|
|
||||||
- Scheduler check setiap 60 detik
|
|
||||||
- Processing job: ~20 detik
|
|
||||||
- Total effective delay: 20-60 detik dari waktu target
|
|
||||||
|
|
||||||
## 📊 Performance Metrics
|
|
||||||
|
|
||||||
- **Scheduler Accuracy:** ±20 detik
|
|
||||||
- **Firebase Sync Latency:** <2 detik
|
|
||||||
- **Job Processing Time:** ~20 detik (turn ON + countdown + turn OFF)
|
|
||||||
- **Uptime:** 24/7 dengan auto-recovery
|
|
||||||
- **Error Rate:** <1% (dengan REST API fallback)
|
|
||||||
|
|
||||||
## 🚀 Repository Links
|
|
||||||
|
|
||||||
- **Main Project:** https://github.com/awisnuu/Apsgo.git
|
|
||||||
- **Railway Worker:** https://github.com/awisnuu/myreppril.git
|
|
||||||
|
|
||||||
## 📝 Dokumentasi
|
|
||||||
|
|
||||||
- [README.md](README.md) - Project overview
|
|
||||||
- [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md) - Deployment instructions
|
|
||||||
- [DEBUG_TESTING_GUIDE.md](DEBUG_TESTING_GUIDE.md) - Testing procedures
|
|
||||||
- [RAILWAY_TROUBLESHOOTING.md](RAILWAY_TROUBLESHOOTING.md) - Common issues
|
|
||||||
- [railway-worker/](railway-worker/) - Worker documentation
|
|
||||||
|
|
||||||
## 🔄 Next Steps (Future Improvements)
|
|
||||||
|
|
||||||
1. **Separate Pump Scheduling**
|
|
||||||
- Individual pump control per schedule
|
|
||||||
- Queue-based sequential execution
|
|
||||||
- Configurable delays between pumps
|
|
||||||
|
|
||||||
2. **Enhanced Monitoring**
|
|
||||||
- Real-time dashboard
|
|
||||||
- Alert notifications
|
|
||||||
- Performance analytics
|
|
||||||
|
|
||||||
3. **Advanced Scheduling**
|
|
||||||
- Multiple schedules per day
|
|
||||||
- Day-of-week selection
|
|
||||||
- Holiday exceptions
|
|
||||||
|
|
||||||
4. **Sensor Integration**
|
|
||||||
- Auto-adjust based on soil moisture
|
|
||||||
- Weather API integration
|
|
||||||
- Smart duration calculation
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Last Updated:** 2026-02-11 13:30 WIB
|
|
||||||
**System Status:** ✅ Operational
|
|
||||||
|
|
@ -7,13 +7,14 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.kampus.apsgo"
|
namespace = "com.kampus.irrigo"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
ndkVersion = "27.0.12077973"
|
ndkVersion = "27.0.12077973"
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
|
|
@ -22,10 +23,10 @@ android {
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
applicationId = "com.kampus.apsgo"
|
applicationId = "com.kampus.irrigo"
|
||||||
// You can update the following values to match your application needs.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
minSdk = 23
|
minSdk = flutter.minSdkVersion
|
||||||
targetSdk = flutter.targetSdkVersion
|
targetSdk = flutter.targetSdkVersion
|
||||||
versionCode = flutter.versionCode
|
versionCode = flutter.versionCode
|
||||||
versionName = flutter.versionName
|
versionName = flutter.versionName
|
||||||
|
|
@ -40,6 +41,10 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
|
||||||
|
}
|
||||||
|
|
||||||
flutter {
|
flutter {
|
||||||
source = "../.."
|
source = "../.."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"project_info": {
|
"project_info": {
|
||||||
"project_number": "217854138058",
|
"project_number": "217854138058",
|
||||||
|
"firebase_url": "https://project-ta-951b4-default-rtdb.firebaseio.com",
|
||||||
"project_id": "project-ta-951b4",
|
"project_id": "project-ta-951b4",
|
||||||
"storage_bucket": "project-ta-951b4.firebasestorage.app"
|
"storage_bucket": "project-ta-951b4.firebasestorage.app"
|
||||||
},
|
},
|
||||||
|
|
@ -9,10 +10,18 @@
|
||||||
"client_info": {
|
"client_info": {
|
||||||
"mobilesdk_app_id": "1:217854138058:android:b8bd92665b1766ad0c4633",
|
"mobilesdk_app_id": "1:217854138058:android:b8bd92665b1766ad0c4633",
|
||||||
"android_client_info": {
|
"android_client_info": {
|
||||||
"package_name": "com.kampus.apsgo"
|
"package_name": "com.kampus.irrigo"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"oauth_client": [
|
"oauth_client": [
|
||||||
|
{
|
||||||
|
"client_id": "217854138058-1jj0n8jplpq5sj8b3vu6s60pne3bqkfk.apps.googleusercontent.com",
|
||||||
|
"client_type": 1,
|
||||||
|
"android_info": {
|
||||||
|
"package_name": "com.kampus.irrigo",
|
||||||
|
"certificate_hash": "a47083645 3eb8ca78f84059dc3387f728d6eb4c3"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"client_id": "217854138058-ml03se7pied9l9k3jcb5cpgl2bn3rto0.apps.googleusercontent.com",
|
"client_id": "217854138058-ml03se7pied9l9k3jcb5cpgl2bn3rto0.apps.googleusercontent.com",
|
||||||
"client_type": 3
|
"client_type": 3
|
||||||
|
|
@ -29,6 +38,13 @@
|
||||||
{
|
{
|
||||||
"client_id": "217854138058-ml03se7pied9l9k3jcb5cpgl2bn3rto0.apps.googleusercontent.com",
|
"client_id": "217854138058-ml03se7pied9l9k3jcb5cpgl2bn3rto0.apps.googleusercontent.com",
|
||||||
"client_type": 3
|
"client_type": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_id": "217854138058-1kl7acuvsqkicm00t9m6qn1dkegh9r1e.apps.googleusercontent.com",
|
||||||
|
"client_type": 2,
|
||||||
|
"ios_info": {
|
||||||
|
"bundle_id": "com.kampus.irrigo"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,32 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- Notification Permissions -->
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||||
|
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
|
||||||
|
<!-- Network and Firebase -->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<application
|
<application
|
||||||
android:label="ApsGo"
|
android:label="irrigo"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
|
<!-- Firebase Messaging Configuration -->
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
||||||
|
android:value="irrigp_watering_channel" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.firebase.messaging.default_notification_icon"
|
||||||
|
android:resource="@mipmap/ic_launcher" />
|
||||||
|
|
||||||
|
<!-- Foreground Service for Background Notifications -->
|
||||||
|
<service
|
||||||
|
android:name=".NotificationService"
|
||||||
|
android:foregroundServiceType="dataSync"
|
||||||
|
android:exported="false" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|
@ -25,6 +49,25 @@
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<!-- Broadcast Receiver for handling boot completed and notification callbacks -->
|
||||||
|
<!-- Using the FlutterLocalNotifications plugin's built-in receivers -->
|
||||||
|
<receiver
|
||||||
|
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver"
|
||||||
|
android:exported="true"
|
||||||
|
android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<!-- Notification action receiver -->
|
||||||
|
<receiver
|
||||||
|
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver"
|
||||||
|
android:exported="false"
|
||||||
|
android:permission="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
<meta-data
|
<meta-data
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package com.kampus.irrigo
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
|
||||||
|
class MainActivity: FlutterActivity() {
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "IrriGoNotification"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
Log.d(TAG, "MainActivity started - notifying notification system")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
Log.d(TAG, "MainActivity resumed - notifying notification system")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:ReservedCodeCacheSize=256m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
# This builtInKotlin flag was added automatically by Flutter migrator
|
||||||
|
android.builtInKotlin=false
|
||||||
|
# This newDsl flag was added automatically by Flutter migrator
|
||||||
|
android.newDsl=false
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 687 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
|
@ -368,7 +368,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.apsgo;
|
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.irrigo;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
|
|
@ -384,7 +384,7 @@
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.apsgo;
|
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.irrigo;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
|
@ -401,7 +401,7 @@
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.apsgo;
|
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.irrigo;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
|
@ -416,7 +416,7 @@
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.apsgo;
|
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.irrigo;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
|
@ -547,7 +547,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.apsgo;
|
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.irrigo;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
|
@ -569,7 +569,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.apsgo;
|
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.irrigo;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>ApsGo</string>
|
<string>IrriGo</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>ApsGo</string>
|
<string>IrriGo</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ class DefaultFirebaseOptions {
|
||||||
projectId: 'project-ta-951b4',
|
projectId: 'project-ta-951b4',
|
||||||
storageBucket: 'project-ta-951b4.firebasestorage.app',
|
storageBucket: 'project-ta-951b4.firebasestorage.app',
|
||||||
databaseURL: 'https://project-ta-951b4-default-rtdb.firebaseio.com',
|
databaseURL: 'https://project-ta-951b4-default-rtdb.firebaseio.com',
|
||||||
iosBundleId: 'com.kampus.apsgo',
|
iosBundleId: 'com.kampus.irrigo',
|
||||||
);
|
);
|
||||||
|
|
||||||
static const FirebaseOptions web = FirebaseOptions(
|
static const FirebaseOptions web = FirebaseOptions(
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,23 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'firebase_options.dart';
|
import 'firebase_options.dart';
|
||||||
import 'screens/landing_page.dart';
|
import 'screens/landing_page.dart';
|
||||||
import 'screens/login_page.dart';
|
import 'screens/login_page.dart';
|
||||||
import 'theme/app_color.dart';
|
import 'theme/app_color.dart';
|
||||||
import 'services/history_logging_service.dart';
|
import 'services/history_logging_service.dart';
|
||||||
import 'services/kontrol_automation_service.dart';
|
import 'services/notification_service.dart';
|
||||||
|
|
||||||
|
@pragma('vm:entry-point')
|
||||||
|
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||||
|
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
||||||
|
debugPrint('🔔 Background notification received: ${message.data}');
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
// Start history logging service as soon as app starts
|
// Start history logging service as soon as app starts
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,23 +48,30 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case AppLifecycleState.resumed:
|
case AppLifecycleState.resumed:
|
||||||
// App kembali ke foreground
|
// App kembali ke foreground
|
||||||
print('📱 App resumed - services will auto-start when needed');
|
print('📱 App resumed - checking pending waterings...');
|
||||||
|
// Check any pending waterings that weren't executed (user didn't tap notification)
|
||||||
|
NotificationService.instance.checkAndExecutePendingWaterings();
|
||||||
break;
|
break;
|
||||||
case AppLifecycleState.inactive:
|
case AppLifecycleState.inactive:
|
||||||
// App temporary inactive (misal: phone call)
|
// App temporary inactive (misal: phone call)
|
||||||
print('📱 App inactive');
|
print('📱 App inactive');
|
||||||
break;
|
break;
|
||||||
case AppLifecycleState.paused:
|
case AppLifecycleState.paused:
|
||||||
// App di background, stop services untuk hemat battery
|
// App di background
|
||||||
print('📱 App paused - stopping background services');
|
// ⚠️ NOTE: DO NOT stop KontrolAutomationService!
|
||||||
|
// Scheduled notifications must fire even when app is paused
|
||||||
|
// Only stop history logging to save battery
|
||||||
|
print(
|
||||||
|
'📱 App paused - background services continue running for notifications',
|
||||||
|
);
|
||||||
HistoryLoggingService().stop();
|
HistoryLoggingService().stop();
|
||||||
KontrolAutomationService().stopAll();
|
// KontrolAutomationService().stopAll(); // ❌ REMOVED: Breaks scheduled notifications!
|
||||||
break;
|
break;
|
||||||
case AppLifecycleState.detached:
|
case AppLifecycleState.detached:
|
||||||
// App akan di-terminate
|
// App akan di-terminate
|
||||||
print('📱 App detaching - cleanup services');
|
print('📱 App detaching - cleanup services');
|
||||||
HistoryLoggingService().dispose();
|
HistoryLoggingService().dispose();
|
||||||
KontrolAutomationService().dispose();
|
// KontrolAutomationService().dispose(); // Keep running until truly terminated
|
||||||
break;
|
break;
|
||||||
case AppLifecycleState.hidden:
|
case AppLifecycleState.hidden:
|
||||||
// App hidden (new in Flutter 3.13+)
|
// App hidden (new in Flutter 3.13+)
|
||||||
|
|
@ -69,7 +84,7 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
title: 'ApsGo',
|
title: 'IrriGo',
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
primaryColor: AppColor.primary,
|
primaryColor: AppColor.primary,
|
||||||
scaffoldBackgroundColor: AppColor.background,
|
scaffoldBackgroundColor: AppColor.background,
|
||||||
|
|
@ -109,6 +124,15 @@ class _FirebaseInitializerState extends State<FirebaseInitializer> {
|
||||||
});
|
});
|
||||||
print('✅ Firebase initialized successfully');
|
print('✅ Firebase initialized successfully');
|
||||||
|
|
||||||
|
await NotificationService.instance.initialize();
|
||||||
|
print('✅ Notification service initialized');
|
||||||
|
|
||||||
|
// Print diagnostic info for debugging
|
||||||
|
await NotificationService.instance.printDiagnosticInfo();
|
||||||
|
|
||||||
|
await NotificationService.instance.syncScheduleRemindersFromFirebase();
|
||||||
|
print('✅ Local schedule reminders synced');
|
||||||
|
|
||||||
// Start history logging service after Firebase is initialized
|
// Start history logging service after Firebase is initialized
|
||||||
final loggingService = HistoryLoggingService();
|
final loggingService = HistoryLoggingService();
|
||||||
loggingService.start();
|
loggingService.start();
|
||||||
|
|
@ -121,6 +145,15 @@ class _FirebaseInitializerState extends State<FirebaseInitializer> {
|
||||||
});
|
});
|
||||||
print('✅ Firebase already initialized');
|
print('✅ Firebase already initialized');
|
||||||
|
|
||||||
|
await NotificationService.instance.initialize();
|
||||||
|
print('✅ Notification service initialized');
|
||||||
|
|
||||||
|
// Print diagnostic info for debugging
|
||||||
|
await NotificationService.instance.printDiagnosticInfo();
|
||||||
|
|
||||||
|
await NotificationService.instance.syncScheduleRemindersFromFirebase();
|
||||||
|
print('✅ Local schedule reminders synced');
|
||||||
|
|
||||||
// Start logging service
|
// Start logging service
|
||||||
final loggingService = HistoryLoggingService();
|
final loggingService = HistoryLoggingService();
|
||||||
loggingService.start();
|
loggingService.start();
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import '../services/auth_service.dart';
|
||||||
import '../services/firebase_database_service.dart';
|
import '../services/firebase_database_service.dart';
|
||||||
import 'kontrol_page.dart';
|
import 'kontrol_page.dart';
|
||||||
import 'histori_page.dart';
|
import 'histori_page.dart';
|
||||||
|
// import 'notification_debug_screen.dart';
|
||||||
|
|
||||||
class DashboardPage extends StatefulWidget {
|
class DashboardPage extends StatefulWidget {
|
||||||
const DashboardPage({super.key});
|
const DashboardPage({super.key});
|
||||||
|
|
@ -14,6 +15,12 @@ class DashboardPage extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DashboardPageState extends State<DashboardPage> {
|
class _DashboardPageState extends State<DashboardPage> {
|
||||||
|
|
||||||
|
String formatNumber(dynamic value, {int decimal = 1}) {
|
||||||
|
final number = double.tryParse(value.toString()) ?? 0;
|
||||||
|
return number.toStringAsFixed(decimal);
|
||||||
|
}
|
||||||
|
|
||||||
int _selectedIndex = 0;
|
int _selectedIndex = 0;
|
||||||
final _dbService = FirebaseDatabaseService();
|
final _dbService = FirebaseDatabaseService();
|
||||||
final _authService = AuthService();
|
final _authService = AuthService();
|
||||||
|
|
@ -170,7 +177,7 @@ class _DashboardPageState extends State<DashboardPage> {
|
||||||
),
|
),
|
||||||
child: ClipOval(
|
child: ClipOval(
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
'assets/images/logo_apsgo.png',
|
'assets/images/logo_irrigo.png',
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorBuilder: (context, error, stackTrace) {
|
errorBuilder: (context, error, stackTrace) {
|
||||||
return Icon(
|
return Icon(
|
||||||
|
|
@ -184,7 +191,7 @@ class _DashboardPageState extends State<DashboardPage> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(
|
Text(
|
||||||
'ApsGo',
|
'IrriGo',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
|
|
@ -222,6 +229,26 @@ class _DashboardPageState extends State<DashboardPage> {
|
||||||
title: 'Histori',
|
title: 'Histori',
|
||||||
index: 2,
|
index: 2,
|
||||||
),
|
),
|
||||||
|
// ListTile(
|
||||||
|
// leading: Icon(
|
||||||
|
// Icons.notifications_active,
|
||||||
|
// color: Colors.orange[700],
|
||||||
|
// ),
|
||||||
|
// title: const Text(
|
||||||
|
// 'Debug Notifikasi',
|
||||||
|
// style: TextStyle(fontWeight: FontWeight.w500),
|
||||||
|
// ),
|
||||||
|
// onTap: () {
|
||||||
|
// Navigator.pop(context);
|
||||||
|
// Navigator.push(
|
||||||
|
// context,
|
||||||
|
// MaterialPageRoute(
|
||||||
|
// builder:
|
||||||
|
// (context) => const NotificationDebugScreen(),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.logout, color: Colors.red),
|
leading: const Icon(Icons.logout, color: Colors.red),
|
||||||
|
|
@ -333,9 +360,11 @@ class _DashboardPageState extends State<DashboardPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
final data = snapshot.data!;
|
final data = snapshot.data!;
|
||||||
final suhu = data['suhu'] ?? '0';
|
// final suhu = data['suhu'] ?? '0';
|
||||||
final kelembapan = data['kelembapan'] ?? '0';
|
// final kelembapan = data['kelembapan'] ?? '0';
|
||||||
final ldr = data['ldr'] ?? '0';
|
final suhu = formatNumber(data['suhu'], decimal: 1);
|
||||||
|
final kelembapan = formatNumber(data['kelembapan'], decimal: 1);
|
||||||
|
final ldr = formatNumber(data['ldr'], decimal: 1);
|
||||||
final waterFlow = data['water_flow'] ?? 0;
|
final waterFlow = data['water_flow'] ?? 0;
|
||||||
final soil1 = data['soil_1'] ?? '0';
|
final soil1 = data['soil_1'] ?? '0';
|
||||||
final soil2 = data['soil_2'] ?? '0';
|
final soil2 = data['soil_2'] ?? '0';
|
||||||
|
|
@ -376,7 +405,7 @@ class _DashboardPageState extends State<DashboardPage> {
|
||||||
_buildSensorCard(
|
_buildSensorCard(
|
||||||
title: "Light",
|
title: "Light",
|
||||||
value: ldr,
|
value: ldr,
|
||||||
unit: "%",
|
unit: "",
|
||||||
icon: Icons.wb_sunny_outlined,
|
icon: Icons.wb_sunny_outlined,
|
||||||
color: Colors.amber,
|
color: Colors.amber,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,11 @@ class _JadwalFormPageState extends State<JadwalFormPage> {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((_pompaAir || _pompaPupuk) && _selectedPots.isEmpty) {
|
||||||
|
_showError('Tidak dapat menyimpan jadwal: Pompa aktif tanpa kran/solenoid (POT) yang terbuka.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (_durasi <= 0) {
|
if (_durasi <= 0) {
|
||||||
_showError('Durasi harus lebih dari 0');
|
_showError('Durasi harus lebih dari 0');
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -247,6 +252,28 @@ class _JadwalFormPageState extends State<JadwalFormPage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showAlertDialog(BuildContext context, {required String title, required String message}) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.warning_amber_rounded, color: Colors.orange, size: 28),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: Text(message),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('OK', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|
@ -518,6 +545,14 @@ class _JadwalFormPageState extends State<JadwalFormPage> {
|
||||||
),
|
),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
if (_pompaAir || _pompaPupuk) {
|
||||||
|
_showAlertDialog(
|
||||||
|
context,
|
||||||
|
title: 'Bersihkan Pot',
|
||||||
|
message: 'Tidak dapat menonaktifkan seluruh kran/solenoid saat pompa masih aktif. Matikan pompa terlebih dahulu.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_potSelection = [false, false, false, false, false];
|
_potSelection = [false, false, false, false, false];
|
||||||
});
|
});
|
||||||
|
|
@ -536,6 +571,18 @@ class _JadwalFormPageState extends State<JadwalFormPage> {
|
||||||
Widget _buildPotCheckbox(int potNumber, bool selected) {
|
Widget _buildPotCheckbox(int potNumber, bool selected) {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
final isCurrentlySelected = _potSelection[potNumber - 1];
|
||||||
|
if (isCurrentlySelected) {
|
||||||
|
final otherSelectedCount = _potSelection.where((p) => p).length - 1;
|
||||||
|
if (otherSelectedCount == 0 && (_pompaAir || _pompaPupuk)) {
|
||||||
|
_showAlertDialog(
|
||||||
|
context,
|
||||||
|
title: 'Solenoid Terakhir',
|
||||||
|
message: 'Tidak dapat menonaktifkan kran/solenoid terakhir saat pompa masih aktif. Matikan pompa terlebih dahulu.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_potSelection[potNumber - 1] = !_potSelection[potNumber - 1];
|
_potSelection[potNumber - 1] = !_potSelection[potNumber - 1];
|
||||||
});
|
});
|
||||||
|
|
@ -594,7 +641,17 @@ class _JadwalFormPageState extends State<JadwalFormPage> {
|
||||||
children: [
|
children: [
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
value: _pompaAir,
|
value: _pompaAir,
|
||||||
onChanged: (value) => setState(() => _pompaAir = value),
|
onChanged: (value) {
|
||||||
|
if (value && _selectedPots.isEmpty) {
|
||||||
|
_showAlertDialog(
|
||||||
|
context,
|
||||||
|
title: 'Pompa Aktif Tanpa Solenoid',
|
||||||
|
message: 'Harap aktifkan minimal 1 kran/solenoid (POT) sebelum mengaktifkan pompa untuk menghindari kerusakan!',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() => _pompaAir = value);
|
||||||
|
},
|
||||||
title: const Text('Pompa Air'),
|
title: const Text('Pompa Air'),
|
||||||
subtitle: const Text('Mengalirkan air'),
|
subtitle: const Text('Mengalirkan air'),
|
||||||
secondary: Icon(
|
secondary: Icon(
|
||||||
|
|
@ -606,7 +663,17 @@ class _JadwalFormPageState extends State<JadwalFormPage> {
|
||||||
const Divider(),
|
const Divider(),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
value: _pompaPupuk,
|
value: _pompaPupuk,
|
||||||
onChanged: (value) => setState(() => _pompaPupuk = value),
|
onChanged: (value) {
|
||||||
|
if (value && _selectedPots.isEmpty) {
|
||||||
|
_showAlertDialog(
|
||||||
|
context,
|
||||||
|
title: 'Pompa Aktif Tanpa Solenoid',
|
||||||
|
message: 'Harap aktifkan minimal 1 kran/solenoid (POT) sebelum mengaktifkan pompa untuk menghindari kerusakan!',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() => _pompaPupuk = value);
|
||||||
|
},
|
||||||
title: const Text('Pompa Larutan Nutrisi'),
|
title: const Text('Pompa Larutan Nutrisi'),
|
||||||
subtitle: const Text('Mengalirkan larutan nutrisi cair'),
|
subtitle: const Text('Mengalirkan larutan nutrisi cair'),
|
||||||
secondary: Icon(
|
secondary: Icon(
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../theme/app_color.dart';
|
import '../theme/app_color.dart';
|
||||||
import '../widgets/kontrol_widgets.dart';
|
import '../widgets/kontrol_widgets.dart';
|
||||||
import 'pot_selection_page.dart';
|
|
||||||
import 'jadwal_management_page.dart';
|
import 'jadwal_management_page.dart';
|
||||||
import 'threshold_management_page.dart';
|
import 'threshold_management_page.dart';
|
||||||
import '../services/kontrol_storage.dart';
|
import '../services/kontrol_storage.dart';
|
||||||
|
|
@ -166,6 +165,15 @@ class _ManualControlPageState extends State<ManualControlPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _jalankan() async {
|
Future<void> _jalankan() async {
|
||||||
|
if ((_pompaAir || _pompaNutrisi) && !_potStatus.any((p) => p)) {
|
||||||
|
_showAlertDialog(
|
||||||
|
context,
|
||||||
|
title: 'Pengaturan Tidak Valid',
|
||||||
|
message: 'Pompa aktif tanpa kran/solenoid (POT) yang terbuka. Silakan buka minimal 1 kran/solenoid terlebih dahulu.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Update Firebase with current state
|
// Update Firebase with current state
|
||||||
await _dbService.setMultipleAktuator({
|
await _dbService.setMultipleAktuator({
|
||||||
|
|
@ -186,6 +194,24 @@ class _ManualControlPageState extends State<ManualControlPage> {
|
||||||
pots: _potStatus,
|
pots: _potStatus,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Simpan histori penyiraman manual jika ada pompa & pot aktif
|
||||||
|
if ((_pompaAir || _pompaNutrisi) && _potStatus.any((p) => p)) {
|
||||||
|
final activePotNumbers = <int>[];
|
||||||
|
for (int i = 0; i < _potStatus.length; i++) {
|
||||||
|
if (_potStatus[i]) activePotNumbers.add(i + 1);
|
||||||
|
}
|
||||||
|
// Fire-and-forget (tidak perlu await supaya UI tidak lambat)
|
||||||
|
_dbService
|
||||||
|
.saveHistoriPenyiramanManual(
|
||||||
|
potNumbers: activePotNumbers,
|
||||||
|
pompaAir: _pompaAir,
|
||||||
|
pompaNutrisi: _pompaNutrisi,
|
||||||
|
)
|
||||||
|
.catchError(
|
||||||
|
(e) => debugPrint('Gagal simpan histori manual: $e'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Reset flag perubahan lokal
|
// Reset flag perubahan lokal
|
||||||
setState(() {
|
setState(() {
|
||||||
_hasLocalChanges = false;
|
_hasLocalChanges = false;
|
||||||
|
|
@ -223,6 +249,28 @@ class _ManualControlPageState extends State<ManualControlPage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showAlertDialog(BuildContext context, {required String title, required String message}) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.warning_amber_rounded, color: Colors.orange, size: 28),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: Text(message),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('OK', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_isLoading) {
|
if (_isLoading) {
|
||||||
|
|
@ -237,6 +285,17 @@ class _ManualControlPageState extends State<ManualControlPage> {
|
||||||
title: 'Pompa Air',
|
title: 'Pompa Air',
|
||||||
isActive: _pompaAir,
|
isActive: _pompaAir,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
if (!_pompaAir) {
|
||||||
|
final activePots = _potStatus.where((p) => p).length;
|
||||||
|
if (activePots == 0) {
|
||||||
|
_showAlertDialog(
|
||||||
|
context,
|
||||||
|
title: 'Pompa Aktif Tanpa Solenoid',
|
||||||
|
message: 'Harap aktifkan minimal 1 kran/solenoid (POT) sebelum mengaktifkan pompa untuk menghindari kerusakan!',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_pompaAir = !_pompaAir;
|
_pompaAir = !_pompaAir;
|
||||||
_hasLocalChanges = true;
|
_hasLocalChanges = true;
|
||||||
|
|
@ -247,6 +306,17 @@ class _ManualControlPageState extends State<ManualControlPage> {
|
||||||
title: 'Pompa Nutrisi',
|
title: 'Pompa Nutrisi',
|
||||||
isActive: _pompaNutrisi,
|
isActive: _pompaNutrisi,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
if (!_pompaNutrisi) {
|
||||||
|
final activePots = _potStatus.where((p) => p).length;
|
||||||
|
if (activePots == 0) {
|
||||||
|
_showAlertDialog(
|
||||||
|
context,
|
||||||
|
title: 'Pompa Aktif Tanpa Solenoid',
|
||||||
|
message: 'Harap aktifkan minimal 1 kran/solenoid (POT) sebelum mengaktifkan pompa untuk menghindari kerusakan!',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_pompaNutrisi = !_pompaNutrisi;
|
_pompaNutrisi = !_pompaNutrisi;
|
||||||
_hasLocalChanges = true;
|
_hasLocalChanges = true;
|
||||||
|
|
@ -286,6 +356,18 @@ class _ManualControlPageState extends State<ManualControlPage> {
|
||||||
title: 'POT ${index + 1}',
|
title: 'POT ${index + 1}',
|
||||||
isActive: _potStatus[index],
|
isActive: _potStatus[index],
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
final isCurrentlyActive = _potStatus[index];
|
||||||
|
if (isCurrentlyActive) {
|
||||||
|
final activePots = _potStatus.where((p) => p).length;
|
||||||
|
if (activePots == 1 && (_pompaAir || _pompaNutrisi)) {
|
||||||
|
_showAlertDialog(
|
||||||
|
context,
|
||||||
|
title: 'Solenoid Terakhir',
|
||||||
|
message: 'Tidak dapat menonaktifkan kran/solenoid terakhir saat pompa masih aktif. Matikan pompa terlebih dahulu.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_potStatus[index] = !_potStatus[index];
|
_potStatus[index] = !_potStatus[index];
|
||||||
_hasLocalChanges = true;
|
_hasLocalChanges = true;
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ class _LandingPageState extends State<LandingPage> {
|
||||||
),
|
),
|
||||||
child: ClipOval(
|
child: ClipOval(
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
'assets/images/logo_apsgo.png',
|
'assets/images/logo_irrigo.png',
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
errorBuilder: (context, error, stackTrace) {
|
errorBuilder: (context, error, stackTrace) {
|
||||||
return Icon(Icons.eco, size: 60, color: AppColor.primary);
|
return Icon(Icons.eco, size: 60, color: AppColor.primary);
|
||||||
|
|
@ -93,7 +93,7 @@ class _LandingPageState extends State<LandingPage> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
Text(
|
Text(
|
||||||
"ApsGo",
|
"IrriGo",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|
@ -102,7 +102,7 @@ class _LandingPageState extends State<LandingPage> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
"Kelola Hidupmu\nDengan Lebih Mudah",
|
"Siram Tanamanmu\nDengan Lebih Pintar",
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
|
|
@ -113,7 +113,7 @@ class _LandingPageState extends State<LandingPage> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(
|
Text(
|
||||||
"Semua yang kamu butuhkan dalam satu aplikasi",
|
"Otomatisasi penyiraman untuk menjaga tanaman tetap segar dan sehat setiap hari",
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
|
|
@ -121,7 +121,7 @@ class _LandingPageState extends State<LandingPage> {
|
||||||
height: 1.4,
|
height: 1.4,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 60),
|
const SizedBox(height: 40),
|
||||||
// Tombol Next
|
// Tombol Next
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
|
|
@ -148,6 +148,8 @@ class _LandingPageState extends State<LandingPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
|
_buildCollaborationFooter(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -175,7 +177,7 @@ class _LandingPageState extends State<LandingPage> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
Text(
|
Text(
|
||||||
"Siap Memulai?",
|
"Siap Merawat Tanamanmu?",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|
@ -184,7 +186,7 @@ class _LandingPageState extends State<LandingPage> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
"Bergabunglah Dengan Kami\nDan Rasakan Kemudahannya",
|
"Pantau kelembapan tanah dan atur jadwal penyiraman dalam satu genggaman",
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
|
|
@ -195,7 +197,7 @@ class _LandingPageState extends State<LandingPage> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(
|
Text(
|
||||||
"Daftar sekarang dan mulai perjalananmu",
|
"Mulai otomatisasi sekarang untuk hasil panen terbaik",
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
|
|
@ -203,7 +205,7 @@ class _LandingPageState extends State<LandingPage> {
|
||||||
height: 1.4,
|
height: 1.4,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 60),
|
const SizedBox(height: 40),
|
||||||
// Tombol Get Started
|
// Tombol Get Started
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
|
|
@ -221,14 +223,112 @@ class _LandingPageState extends State<LandingPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: const Text(
|
||||||
"Get Started",
|
"Mulai Dari Sekarang",
|
||||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
|
_buildCollaborationFooter(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildCollaborationFooter() {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// Kampus Logo (Politeknik Negeri Jember)
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Image.asset(
|
||||||
|
'assets/images/logo_polije.png',
|
||||||
|
height: 24,
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
// Fallback to existing asset logo_apsgo.png
|
||||||
|
return Image.asset(
|
||||||
|
'assets/images/logo_poliyey.png',
|
||||||
|
height: 24,
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return Icon(
|
||||||
|
Icons.school,
|
||||||
|
size: 20,
|
||||||
|
color: AppColor.primary.withOpacity(0.7),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Text(
|
||||||
|
"Politeknik Negeri Jember",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColor.textDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Container(
|
||||||
|
height: 16,
|
||||||
|
width: 1,
|
||||||
|
color: AppColor.textDark.withOpacity(0.2),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
// Partner Logo (Mitra)
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Image.asset(
|
||||||
|
'assets/images/logo_mitra.png',
|
||||||
|
height: 24,
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
// Fallback to existing asset logo_irrigo.png
|
||||||
|
return Image.asset(
|
||||||
|
'assets/images/logo_mitra.png',
|
||||||
|
height: 24,
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return Icon(
|
||||||
|
Icons.handshake,
|
||||||
|
size: 20,
|
||||||
|
color: AppColor.primary.withOpacity(0.7),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Text(
|
||||||
|
"YUSUF BIBIT",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColor.textDark,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(
|
||||||
|
"by: Tugas Akhir",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppColor.primary.withOpacity(0.8),
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -95,33 +95,6 @@ class _LoginPageState extends State<LoginPage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleGoogleSignIn() async {
|
|
||||||
setState(() {
|
|
||||||
_isLoading = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await _authService.signInWithGoogle();
|
|
||||||
|
|
||||||
if (mounted) {
|
|
||||||
Navigator.pushReplacement(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(builder: (_) => const DashboardPage()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (mounted) {
|
|
||||||
_showError(e.toString());
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_isLoading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
|
|
@ -158,7 +131,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||||
),
|
),
|
||||||
child: ClipOval(
|
child: ClipOval(
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
'assets/images/logo_apsgo.png',
|
'assets/images/logo_irrigo.png',
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorBuilder: (context, error, stackTrace) {
|
errorBuilder: (context, error, stackTrace) {
|
||||||
return Icon(Icons.eco, size: 50, color: AppColor.primary);
|
return Icon(Icons.eco, size: 50, color: AppColor.primary);
|
||||||
|
|
@ -168,7 +141,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Text(
|
Text(
|
||||||
"Sign In",
|
"Login",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|
@ -230,47 +203,6 @@ class _LoginPageState extends State<LoginPage> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Divider with "or login with"
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const Expanded(child: Divider()),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: Text(
|
|
||||||
"or login with",
|
|
||||||
style: TextStyle(color: Colors.grey[600]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Expanded(child: Divider()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// Google Sign In Button
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: OutlinedButton.icon(
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
padding: const EdgeInsets.all(14),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
side: BorderSide(color: Colors.grey[300]!),
|
|
||||||
),
|
|
||||||
onPressed: _isLoading ? null : _handleGoogleSignIn,
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.g_mobiledata,
|
|
||||||
size: 32,
|
|
||||||
color: Colors.red,
|
|
||||||
),
|
|
||||||
label: const Text(
|
|
||||||
"Continue with Google",
|
|
||||||
style: TextStyle(color: Colors.black87, fontSize: 16),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// Register Link
|
// Register Link
|
||||||
Center(
|
Center(
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
|
|
@ -282,11 +214,11 @@ class _LoginPageState extends State<LoginPage> {
|
||||||
},
|
},
|
||||||
child: RichText(
|
child: RichText(
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
text: "Don't have an account? ",
|
text: "Belum punya akun? ",
|
||||||
style: TextStyle(color: Colors.grey[600]),
|
style: TextStyle(color: Colors.grey[600]),
|
||||||
children: [
|
children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: "Register",
|
text: "Daftar",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: AppColor.primary,
|
color: AppColor.primary,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,414 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
|
|
||||||
|
import '../services/notification_service.dart';
|
||||||
|
|
||||||
|
class NotificationDebugScreen extends StatefulWidget {
|
||||||
|
const NotificationDebugScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<NotificationDebugScreen> createState() =>
|
||||||
|
_NotificationDebugScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NotificationDebugScreenState extends State<NotificationDebugScreen> {
|
||||||
|
bool _isLoading = false;
|
||||||
|
bool? _canScheduleExactNotifications;
|
||||||
|
String? _lastStatus;
|
||||||
|
String? _fcmToken;
|
||||||
|
List<PendingNotificationRequest> _pendingNotifications = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadPendingNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _setStatus(String message) async {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_lastStatus = message;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadPendingNotifications() async {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await NotificationService.instance.initialize();
|
||||||
|
final canScheduleExact =
|
||||||
|
await NotificationService.instance.canScheduleExactNotifications();
|
||||||
|
final pending = await NotificationService.instance.pendingNotifications();
|
||||||
|
final token = await NotificationService.instance.getFcmToken();
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_pendingNotifications = pending;
|
||||||
|
_canScheduleExactNotifications = canScheduleExact;
|
||||||
|
_fcmToken = token;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
_lastStatus = 'Gagal memuat notifikasi tertunda: $e';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _sendImmediateSimple() async {
|
||||||
|
await _runAction(() async {
|
||||||
|
await NotificationService.instance.showInstantNotification(
|
||||||
|
title: 'Tes Notifikasi IrriGo',
|
||||||
|
body: 'Notifikasi lokal berhasil dikirim dari halaman debug.',
|
||||||
|
payload: 'debug:immediate',
|
||||||
|
);
|
||||||
|
}, 'Notifikasi langsung terkirim');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _sendModeChangedSample() async {
|
||||||
|
await _runAction(() async {
|
||||||
|
await NotificationService.instance.showModeChangedNotification(
|
||||||
|
modeLabel: 'berdasarkan jadwal',
|
||||||
|
enabled: true,
|
||||||
|
);
|
||||||
|
}, 'Notifikasi mode berhasil dikirim');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _sendSensorTriggeredSample() async {
|
||||||
|
await _runAction(() async {
|
||||||
|
await NotificationService.instance.showWateringNotification(
|
||||||
|
title: 'IrriGo - Penyiraman Otomatis',
|
||||||
|
body:
|
||||||
|
'Penyiraman dilakukan pada pot 3 karena kelembapan 28% di bawah ambang batas 40%.',
|
||||||
|
payload: {
|
||||||
|
'type': 'sensor_triggered',
|
||||||
|
'pot': 3,
|
||||||
|
'soil': 28,
|
||||||
|
'threshold': 40,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}, 'Notifikasi otomatis sensor terkirim');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _scheduleThirtySecondNotification() async {
|
||||||
|
await _runAction(() async {
|
||||||
|
final success = await NotificationService.instance
|
||||||
|
.scheduleStable30SecondNotification(
|
||||||
|
title: 'Tes Notifikasi 30 Detik',
|
||||||
|
body:
|
||||||
|
'Notifikasi ini dijadwalkan 30 detik. Minimize app atau tutup setelah ini untuk test background notification!',
|
||||||
|
payload: 'debug:scheduled_30s',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
await _setStatus(
|
||||||
|
'✅ Notifikasi 30 detik dijadwalkan STABIL! Cek ulang dibawah utk lihat status exact alarm dan pending notifikasi.',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await _setStatus(
|
||||||
|
'❌ GAGAL scheduling 30 detik. Periksa POST_NOTIFICATIONS permission di settings app!',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, 'Jadwalkan Notif 30 Detik Stabil');
|
||||||
|
|
||||||
|
await _loadPendingNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _requestExactAlarmPermission() async {
|
||||||
|
await _runAction(() async {
|
||||||
|
final granted =
|
||||||
|
await NotificationService.instance.requestExactAlarmsPermission();
|
||||||
|
if (!granted) {
|
||||||
|
await _setStatus(
|
||||||
|
'Exact alarm belum aktif/diizinkan oleh sistem. Aplikasi pakai inexact agar tetap stabil.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await _setStatus('Exact alarm aktif. Jadwal lokal akan lebih presisi.');
|
||||||
|
}, 'Izin exact alarm berhasil diminta');
|
||||||
|
|
||||||
|
await _loadPendingNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _revalidatePermissions() async {
|
||||||
|
await _runAction(() async {
|
||||||
|
final postNotifGranted =
|
||||||
|
await NotificationService.instance.revalidatePermissions();
|
||||||
|
final exactGranted =
|
||||||
|
await NotificationService.instance.canScheduleExactNotifications();
|
||||||
|
|
||||||
|
final status = '''
|
||||||
|
✅ Permission Status Fresh Check:
|
||||||
|
• POST_NOTIFICATIONS: ${postNotifGranted ? '✅ GRANTED' : '❌ DENIED'}
|
||||||
|
• SCHEDULE_EXACT_ALARM: ${exactGranted ? '✅ AVAILABLE' : '⚠️ NOT AVAILABLE'}
|
||||||
|
|
||||||
|
Jika ada yang DENIED, settings app → IrriGo → Izin → aktifkan permission.
|
||||||
|
''';
|
||||||
|
|
||||||
|
await _setStatus(status);
|
||||||
|
}, 'Permission berhasil di-revalidasi');
|
||||||
|
|
||||||
|
await _loadPendingNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _cancelAllNotifications() async {
|
||||||
|
await _runAction(() async {
|
||||||
|
await NotificationService.instance.cancelAll();
|
||||||
|
}, 'Semua notifikasi dibatalkan');
|
||||||
|
|
||||||
|
await _loadPendingNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _runAction(
|
||||||
|
Future<void> Function() action,
|
||||||
|
String successText,
|
||||||
|
) async {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await NotificationService.instance.initialize();
|
||||||
|
await action();
|
||||||
|
await _setStatus(successText);
|
||||||
|
} catch (e) {
|
||||||
|
await _setStatus('Error: $e');
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Debug Notifikasi'),
|
||||||
|
backgroundColor: Colors.orange,
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: _isLoading ? null : _loadPendingNotifications,
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
tooltip: 'Refresh',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body:
|
||||||
|
_isLoading
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
_buildInfoCard(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildActionCard(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildPendingCard(),
|
||||||
|
if (_lastStatus != null) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildStatusCard(),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInfoCard() {
|
||||||
|
return Card(
|
||||||
|
elevation: 2,
|
||||||
|
color: Colors.blue.shade50,
|
||||||
|
child: const Padding(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Cara kerja notifikasi IrriGo',
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'• Notifikasi langsung muncul saat app aktif atau background.\n'
|
||||||
|
'• Notifikasi terjadwal lokal tetap bisa muncul walau app diminimize/ditutup setelah dijadwalkan.\n'
|
||||||
|
'• Agar jadwal presisi, aktifkan exact alarm.\n'
|
||||||
|
'• Untuk event server saat app benar-benar mati, gunakan push notification dari Railway Worker + FCM (sudah dipasang).',
|
||||||
|
style: TextStyle(fontSize: 13, height: 1.4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildActionCard() {
|
||||||
|
return Card(
|
||||||
|
elevation: 3,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Aksi Debug',
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: _isLoading ? null : _sendImmediateSimple,
|
||||||
|
icon: const Icon(Icons.notifications_active),
|
||||||
|
label: const Text('Kirim Notifikasi Langsung'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: _isLoading ? null : _sendModeChangedSample,
|
||||||
|
icon: const Icon(Icons.settings),
|
||||||
|
label: const Text('Simulasi Notif Ubah Mode'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: _isLoading ? null : _sendSensorTriggeredSample,
|
||||||
|
icon: const Icon(Icons.water_drop),
|
||||||
|
label: const Text('Simulasi Notif Sensor Trigger'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: _isLoading ? null : _scheduleThirtySecondNotification,
|
||||||
|
icon: const Icon(Icons.schedule),
|
||||||
|
label: const Text('Jadwalkan Notif 30 Detik'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: _isLoading ? null : _requestExactAlarmPermission,
|
||||||
|
icon: const Icon(Icons.alarm_on),
|
||||||
|
label: const Text('Aktifkan Exact Alarm'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: _isLoading ? null : _loadPendingNotifications,
|
||||||
|
icon: const Icon(Icons.key),
|
||||||
|
label: const Text('Cek Ulang Token FCM'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: _isLoading ? null : _revalidatePermissions,
|
||||||
|
icon: const Icon(Icons.verified_user),
|
||||||
|
label: const Text('Validasi Ulang Permission'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: _isLoading ? null : _cancelAllNotifications,
|
||||||
|
icon: const Icon(Icons.cancel),
|
||||||
|
label: const Text('Batalkan Semua Notifikasi'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPendingCard() {
|
||||||
|
return Card(
|
||||||
|
elevation: 3,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Notifikasi Tertunda',
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Exact alarm: ${_canScheduleExactNotifications == null ? 'memuat...' : (_canScheduleExactNotifications! ? 'aktif' : 'belum aktif')}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color:
|
||||||
|
_canScheduleExactNotifications == true
|
||||||
|
? Colors.green.shade700
|
||||||
|
: Colors.red.shade700,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SelectableText(
|
||||||
|
'FCM token: ${_fcmToken == null ? 'belum tersedia' : _fcmToken!}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color:
|
||||||
|
_fcmToken == null
|
||||||
|
? Colors.red.shade700
|
||||||
|
: Colors.green.shade700,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
if (_pendingNotifications.isEmpty)
|
||||||
|
const Text('Belum ada notifikasi terjadwal.')
|
||||||
|
else
|
||||||
|
..._pendingNotifications.map(
|
||||||
|
(notification) => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(notification.title ?? '(tanpa judul)'),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(notification.body ?? '(tanpa isi)'),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'ID: ${notification.id}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatusCard() {
|
||||||
|
final success =
|
||||||
|
_lastStatus!.contains('terkirim') ||
|
||||||
|
_lastStatus!.contains('dijadwalkan') ||
|
||||||
|
_lastStatus!.contains('dibatalkan');
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
color: success ? Colors.green.shade50 : Colors.red.shade50,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Text(
|
||||||
|
_lastStatus!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: success ? Colors.green.shade800 : Colors.red.shade800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import '../widgets/kontrol_widgets.dart';
|
||||||
import '../services/kontrol_storage.dart';
|
import '../services/kontrol_storage.dart';
|
||||||
import '../services/firebase_database_service.dart';
|
import '../services/firebase_database_service.dart';
|
||||||
import '../services/kontrol_automation_service.dart';
|
import '../services/kontrol_automation_service.dart';
|
||||||
|
import '../services/notification_service.dart';
|
||||||
|
|
||||||
class SensorConfigPage extends StatefulWidget {
|
class SensorConfigPage extends StatefulWidget {
|
||||||
final String potName;
|
final String potName;
|
||||||
|
|
@ -222,6 +223,11 @@ class _SensorConfigPageState extends State<SensorConfigPage> {
|
||||||
setState(() => _isSensorModeActive = value);
|
setState(() => _isSensorModeActive = value);
|
||||||
try {
|
try {
|
||||||
await _dbService.setOtomatis(value);
|
await _dbService.setOtomatis(value);
|
||||||
|
await NotificationService.instance
|
||||||
|
.showModeChangedNotification(
|
||||||
|
modeLabel: 'otomatis berbasis sensor',
|
||||||
|
enabled: value,
|
||||||
|
);
|
||||||
if (value) {
|
if (value) {
|
||||||
_automationService.startSensorMode();
|
_automationService.startSensorMode();
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,11 @@ class _ThresholdFormPageState extends State<ThresholdFormPage> {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((_pompaAir || _pompaPupuk) && _selectedPots.isEmpty) {
|
||||||
|
_showError('Tidak dapat menyimpan threshold: Pompa aktif tanpa kran/solenoid (POT) yang terbuka.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (_batasBawah >= _batasAtas) {
|
if (_batasBawah >= _batasAtas) {
|
||||||
_showError('Batas bawah harus lebih kecil dari batas atas');
|
_showError('Batas bawah harus lebih kecil dari batas atas');
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -179,6 +184,28 @@ class _ThresholdFormPageState extends State<ThresholdFormPage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showAlertDialog(BuildContext context, {required String title, required String message}) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.warning_amber_rounded, color: Colors.orange, size: 28),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: Text(message),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('OK', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|
@ -337,25 +364,41 @@ class _ThresholdFormPageState extends State<ThresholdFormPage> {
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Smart Mode Toggle
|
// Smart Mode Toggle
|
||||||
SwitchListTile(
|
// SwitchListTile(
|
||||||
value: _smartMode,
|
// value: _smartMode,
|
||||||
onChanged: (value) {
|
// onChanged: (value) {
|
||||||
setState(() {
|
// setState(() {
|
||||||
_smartMode = value;
|
// _smartMode = value;
|
||||||
});
|
// });
|
||||||
},
|
// },
|
||||||
title: Text(
|
// title: Text(
|
||||||
_smartMode ? 'Smart Mode' : 'Fixed Duration',
|
// _smartMode ? 'Smart Mode' : 'Fixed Duration',
|
||||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
// style: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
|
// ),
|
||||||
|
// subtitle: Text(
|
||||||
|
// _smartMode
|
||||||
|
// ? 'Siram sampai kelembaban ${_batasAtas.toInt()}%'
|
||||||
|
// : 'Siram dengan durasi tetap',
|
||||||
|
// style: const TextStyle(fontSize: 12),
|
||||||
|
// ),
|
||||||
|
// activeColor: AppColor.primary,
|
||||||
|
// contentPadding: EdgeInsets.zero,
|
||||||
|
// ),
|
||||||
|
ListTile(
|
||||||
|
title: const Text(
|
||||||
|
'Smart Mode',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
_smartMode
|
'Siram sampai kelembaban ${_batasAtas.toInt()}%',
|
||||||
? 'Siram sampai kelembaban ${_batasAtas.toInt()}%'
|
|
||||||
: 'Siram dengan durasi tetap',
|
|
||||||
style: const TextStyle(fontSize: 12),
|
style: const TextStyle(fontSize: 12),
|
||||||
),
|
),
|
||||||
activeColor: AppColor.primary,
|
// contentPadding: EdgeInsets.zero,
|
||||||
contentPadding: EdgeInsets.zero,
|
// trailing: Switch(
|
||||||
|
// value: _smartMode,
|
||||||
|
// onChanged: null, // Disabled
|
||||||
|
// activeColor: AppColor.primary,
|
||||||
|
// ),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Duration input (hanya jika Fixed Duration)
|
// Duration input (hanya jika Fixed Duration)
|
||||||
|
|
@ -440,6 +483,17 @@ class _ThresholdFormPageState extends State<ThresholdFormPage> {
|
||||||
label: Text('Pot $potNumber'),
|
label: Text('Pot $potNumber'),
|
||||||
selected: isSelected,
|
selected: isSelected,
|
||||||
onSelected: (selected) {
|
onSelected: (selected) {
|
||||||
|
if (!selected) {
|
||||||
|
final selectedCount = _potSelection.where((p) => p).length;
|
||||||
|
if (selectedCount == 1 && (_pompaAir || _pompaPupuk)) {
|
||||||
|
_showAlertDialog(
|
||||||
|
context,
|
||||||
|
title: 'Solenoid Terakhir',
|
||||||
|
message: 'Tidak dapat menonaktifkan kran/solenoid terakhir saat pompa masih aktif. Matikan pompa terlebih dahulu.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_potSelection[index] = selected;
|
_potSelection[index] = selected;
|
||||||
});
|
});
|
||||||
|
|
@ -472,6 +526,14 @@ class _ThresholdFormPageState extends State<ThresholdFormPage> {
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
if (_pompaAir || _pompaPupuk) {
|
||||||
|
_showAlertDialog(
|
||||||
|
context,
|
||||||
|
title: 'Hapus Semua Pot',
|
||||||
|
message: 'Tidak dapat menonaktifkan seluruh kran/solenoid saat pompa masih aktif. Matikan pompa terlebih dahulu.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_potSelection = [false, false, false, false, false];
|
_potSelection = [false, false, false, false, false];
|
||||||
});
|
});
|
||||||
|
|
@ -515,6 +577,14 @@ class _ThresholdFormPageState extends State<ThresholdFormPage> {
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
value: _pompaAir,
|
value: _pompaAir,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
|
if (value && _selectedPots.isEmpty) {
|
||||||
|
_showAlertDialog(
|
||||||
|
context,
|
||||||
|
title: 'Pompa Aktif Tanpa Solenoid',
|
||||||
|
message: 'Harap aktifkan minimal 1 kran/solenoid (POT) sebelum mengaktifkan pompa untuk menghindari kerusakan!',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_pompaAir = value;
|
_pompaAir = value;
|
||||||
});
|
});
|
||||||
|
|
@ -528,6 +598,14 @@ class _ThresholdFormPageState extends State<ThresholdFormPage> {
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
value: _pompaPupuk,
|
value: _pompaPupuk,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
|
if (value && _selectedPots.isEmpty) {
|
||||||
|
_showAlertDialog(
|
||||||
|
context,
|
||||||
|
title: 'Pompa Aktif Tanpa Solenoid',
|
||||||
|
message: 'Harap aktifkan minimal 1 kran/solenoid (POT) sebelum mengaktifkan pompa untuk menghindari kerusakan!',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_pompaPupuk = value;
|
_pompaPupuk = value;
|
||||||
});
|
});
|
||||||
|
|
@ -539,14 +617,9 @@ class _ThresholdFormPageState extends State<ThresholdFormPage> {
|
||||||
activeColor: AppColor.primary,
|
activeColor: AppColor.primary,
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
),
|
),
|
||||||
|
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
value: _pompaPengaduk,
|
value: false,
|
||||||
onChanged: (value) {
|
onChanged: null, // <- tanpa ()
|
||||||
setState(() {
|
|
||||||
_pompaPengaduk = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
title: const Text('Pompa Pengaduk'),
|
title: const Text('Pompa Pengaduk'),
|
||||||
subtitle: const Text(
|
subtitle: const Text(
|
||||||
'Aktifkan pompa pengaduk untuk mengaduk larutan',
|
'Aktifkan pompa pengaduk untuk mengaduk larutan',
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import '../widgets/kontrol_widgets.dart';
|
||||||
import '../services/kontrol_storage.dart';
|
import '../services/kontrol_storage.dart';
|
||||||
import '../services/firebase_database_service.dart';
|
import '../services/firebase_database_service.dart';
|
||||||
import '../services/kontrol_automation_service.dart';
|
import '../services/kontrol_automation_service.dart';
|
||||||
|
import '../services/notification_service.dart';
|
||||||
|
|
||||||
class WaktuConfigPage extends StatefulWidget {
|
class WaktuConfigPage extends StatefulWidget {
|
||||||
final String potName;
|
final String potName;
|
||||||
|
|
@ -242,6 +243,11 @@ class _WaktuConfigPageState extends State<WaktuConfigPage> {
|
||||||
await _dbService.updateKontrolConfig({
|
await _dbService.updateKontrolConfig({
|
||||||
'waktu': value,
|
'waktu': value,
|
||||||
});
|
});
|
||||||
|
await NotificationService.instance
|
||||||
|
.showModeChangedNotification(
|
||||||
|
modeLabel: 'berdasarkan jadwal',
|
||||||
|
enabled: value,
|
||||||
|
);
|
||||||
if (value) {
|
if (value) {
|
||||||
_automationService.startWaktuMode();
|
_automationService.startWaktuMode();
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import 'package:firebase_database/firebase_database.dart';
|
import 'package:firebase_database/firebase_database.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
|
||||||
class FirebaseDatabaseService {
|
class FirebaseDatabaseService {
|
||||||
static final FirebaseDatabaseService _instance =
|
static final FirebaseDatabaseService _instance =
|
||||||
|
|
@ -139,7 +141,8 @@ class FirebaseDatabaseService {
|
||||||
|
|
||||||
/// Get real-time stream of kontrol configuration
|
/// Get real-time stream of kontrol configuration
|
||||||
Stream<Map<String, dynamic>> getKontrolStream() {
|
Stream<Map<String, dynamic>> getKontrolStream() {
|
||||||
return _database.child('kontrol').onValue.map((event) {
|
// Check kontrol_1 dulu (data terbaru)
|
||||||
|
return _database.child('kontrol_1').onValue.map((event) {
|
||||||
if (event.snapshot.value != null) {
|
if (event.snapshot.value != null) {
|
||||||
return Map<String, dynamic>.from(event.snapshot.value as Map);
|
return Map<String, dynamic>.from(event.snapshot.value as Map);
|
||||||
}
|
}
|
||||||
|
|
@ -147,24 +150,35 @@ class FirebaseDatabaseService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get kontrol configuration once
|
/// Get kontrol configuration once (checks kontrol_1 first, then kontrol for legacy)
|
||||||
Future<Map<String, dynamic>> getKontrolConfig() async {
|
Future<Map<String, dynamic>> getKontrolConfig() async {
|
||||||
try {
|
try {
|
||||||
|
// Try kontrol_1 first (current data structure)
|
||||||
|
final snapshot1 = await _database.child('kontrol_1').get();
|
||||||
|
if (snapshot1.exists && snapshot1.value != null) {
|
||||||
|
print('[DEBUG] Got kontrol config from /kontrol_1');
|
||||||
|
return Map<String, dynamic>.from(snapshot1.value as Map);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to kontrol (legacy)
|
||||||
final snapshot = await _database.child('kontrol').get();
|
final snapshot = await _database.child('kontrol').get();
|
||||||
if (snapshot.exists) {
|
if (snapshot.exists && snapshot.value != null) {
|
||||||
|
print('[DEBUG] Got kontrol config from /kontrol (legacy)');
|
||||||
return Map<String, dynamic>.from(snapshot.value as Map);
|
return Map<String, dynamic>.from(snapshot.value as Map);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
print('[WARN] Kontrol config not found in both /kontrol_1 and /kontrol');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error getting kontrol config: $e');
|
print('Error getting kontrol config: $e');
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update kontrol configuration
|
/// Update kontrol configuration (writes to kontrol_1)
|
||||||
Future<void> updateKontrolConfig(Map<String, dynamic> config) async {
|
Future<void> updateKontrolConfig(Map<String, dynamic> config) async {
|
||||||
try {
|
try {
|
||||||
print('🔥 [DEBUG] Updating Firebase kontrol config: $config');
|
print('🔥 [DEBUG] Updating Firebase kontrol config (kontrol_1): $config');
|
||||||
await _database.child('kontrol').update(config);
|
await _database.child('kontrol_1').update(config);
|
||||||
print('✅ [DEBUG] Firebase kontrol config updated successfully');
|
print('✅ [DEBUG] Firebase kontrol config updated successfully');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('❌ [DEBUG] Error updating kontrol config: $e');
|
print('❌ [DEBUG] Error updating kontrol config: $e');
|
||||||
|
|
@ -172,20 +186,20 @@ class FirebaseDatabaseService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set batas threshold (batas_atas, batas_bawah)
|
/// Set batas threshold (batas_atas, batas_bawah) - writes to kontrol_1
|
||||||
Future<void> setThreshold({int? batasAtas, int? batasBawah}) async {
|
Future<void> setThreshold({int? batasAtas, int? batasBawah}) async {
|
||||||
try {
|
try {
|
||||||
final updates = <String, dynamic>{};
|
final updates = <String, dynamic>{};
|
||||||
if (batasAtas != null) updates['batas_atas'] = batasAtas;
|
if (batasAtas != null) updates['batas_atas'] = batasAtas;
|
||||||
if (batasBawah != null) updates['batas_bawah'] = batasBawah;
|
if (batasBawah != null) updates['batas_bawah'] = batasBawah;
|
||||||
await _database.child('kontrol').update(updates);
|
await _database.child('kontrol_1').update(updates);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error setting threshold: $e');
|
print('Error setting threshold: $e');
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set waktu penyiraman
|
/// Set waktu penyiraman - writes to kontrol_1
|
||||||
Future<void> setWaktuPenyiraman({
|
Future<void> setWaktuPenyiraman({
|
||||||
String? waktu1,
|
String? waktu1,
|
||||||
String? waktu2,
|
String? waktu2,
|
||||||
|
|
@ -196,30 +210,30 @@ class FirebaseDatabaseService {
|
||||||
if (waktu1 != null) updates['waktu_1'] = waktu1;
|
if (waktu1 != null) updates['waktu_1'] = waktu1;
|
||||||
if (waktu2 != null) updates['waktu_2'] = waktu2;
|
if (waktu2 != null) updates['waktu_2'] = waktu2;
|
||||||
if (waktuEnabled != null) updates['waktu'] = waktuEnabled;
|
if (waktuEnabled != null) updates['waktu'] = waktuEnabled;
|
||||||
await _database.child('kontrol').update(updates);
|
await _database.child('kontrol_1').update(updates);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error setting waktu: $e');
|
print('Error setting waktu: $e');
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set durasi penyiraman
|
/// Set durasi penyiraman - writes to kontrol_1
|
||||||
Future<void> setDurasi({int? durasi1, int? durasi2}) async {
|
Future<void> setDurasi({int? durasi1, int? durasi2}) async {
|
||||||
try {
|
try {
|
||||||
final updates = <String, dynamic>{};
|
final updates = <String, dynamic>{};
|
||||||
if (durasi1 != null) updates['durasi_1'] = durasi1;
|
if (durasi1 != null) updates['durasi_1'] = durasi1;
|
||||||
if (durasi2 != null) updates['durasi_2'] = durasi2;
|
if (durasi2 != null) updates['durasi_2'] = durasi2;
|
||||||
await _database.child('kontrol').update(updates);
|
await _database.child('kontrol_1').update(updates);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error setting durasi: $e');
|
print('Error setting durasi: $e');
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toggle mode otomatis
|
/// Toggle mode otomatis - writes to kontrol_1
|
||||||
Future<void> setOtomatis(bool value) async {
|
Future<void> setOtomatis(bool value) async {
|
||||||
try {
|
try {
|
||||||
await _database.child('kontrol/otomatis').set(value);
|
await _database.child('kontrol_1/otomatis').set(value);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error setting otomatis: $e');
|
print('Error setting otomatis: $e');
|
||||||
rethrow;
|
rethrow;
|
||||||
|
|
@ -248,54 +262,143 @@ class FirebaseDatabaseService {
|
||||||
|
|
||||||
// ==================== HISTORY LOGGING ====================
|
// ==================== HISTORY LOGGING ====================
|
||||||
|
|
||||||
/// Save sensor data snapshot to history
|
/// Save sensor data snapshot to history (monitoring)
|
||||||
Future<void> saveHistory(Map<String, dynamic> sensorData) async {
|
Future<void> saveHistory(Map<String, dynamic> sensorData) async {
|
||||||
try {
|
try {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final dateKey =
|
|
||||||
'${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';
|
|
||||||
final timeKey =
|
|
||||||
'${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}';
|
|
||||||
|
|
||||||
await _database.child('history/$dateKey/$timeKey').set({
|
// Simpan per-pot menggunakan push() dengan jenis_histori: monitoring
|
||||||
...sensorData,
|
for (int pot = 1; pot <= 5; pot++) {
|
||||||
'timestamp': now.millisecondsSinceEpoch,
|
final soilKey = 'soil_$pot';
|
||||||
});
|
final soilValue = sensorData[soilKey];
|
||||||
|
if (soilValue == null) continue;
|
||||||
|
|
||||||
|
await _database.child('history').push().set({
|
||||||
|
'timestamp': now.millisecondsSinceEpoch,
|
||||||
|
'tanggal':
|
||||||
|
'${now.day}/${now.month}/${now.year}',
|
||||||
|
'waktu':
|
||||||
|
'${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}',
|
||||||
|
'jenis_histori': 'monitoring',
|
||||||
|
'sumber': 'app',
|
||||||
|
'mode': '-',
|
||||||
|
'pot': pot,
|
||||||
|
'suhu': sensorData['suhu'] ?? 0,
|
||||||
|
'kelembapan': sensorData['kelembapan'] ?? 0,
|
||||||
|
'soil': soilValue,
|
||||||
|
'durasi': 0,
|
||||||
|
'type': 'app_log',
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error saving history: $e');
|
print('Error saving history: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get history data for a specific date range
|
/// Save penyiraman manual ke histori
|
||||||
|
Future<void> saveHistoriPenyiramanManual({
|
||||||
|
required List<int> potNumbers,
|
||||||
|
required bool pompaAir,
|
||||||
|
required bool pompaNutrisi,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final sensorData = await getSensorData();
|
||||||
|
|
||||||
|
for (final pot in potNumbers) {
|
||||||
|
final soilKey = 'soil_$pot';
|
||||||
|
await _database.child('history').push().set({
|
||||||
|
'timestamp': now.millisecondsSinceEpoch,
|
||||||
|
'tanggal': '${now.day}/${now.month}/${now.year}',
|
||||||
|
'waktu':
|
||||||
|
'${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}',
|
||||||
|
'jenis_histori': 'penyiraman',
|
||||||
|
'sumber': 'app',
|
||||||
|
'mode': 'manual',
|
||||||
|
'pot': pot,
|
||||||
|
'suhu': sensorData['suhu'] ?? 0,
|
||||||
|
'kelembapan': sensorData['kelembapan'] ?? 0,
|
||||||
|
'soil': sensorData[soilKey] ?? 0,
|
||||||
|
'durasi': 0,
|
||||||
|
'type': 'manual',
|
||||||
|
'pompa_air': pompaAir,
|
||||||
|
'pompa_nutrisi': pompaNutrisi,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
print('✅ Histori penyiraman manual saved: ${potNumbers.length} pot');
|
||||||
|
} catch (e) {
|
||||||
|
print('Error saving manual watering history: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get history data for a specific date range.
|
||||||
|
/// Mendukung dua format:
|
||||||
|
/// 1. Push-key flat entries: /history/-key/ (ada field `timestamp`)
|
||||||
|
/// 2. Date-keyed nested entries: /history/YYYY-MM-DD/HH:mm/ (format ESP32 lama)
|
||||||
Future<Map<String, dynamic>> getHistoryByDateRange(
|
Future<Map<String, dynamic>> getHistoryByDateRange(
|
||||||
DateTime startDate,
|
DateTime startDate,
|
||||||
DateTime endDate,
|
DateTime endDate,
|
||||||
) async {
|
) async {
|
||||||
|
final result = <String, dynamic>{};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final allHistory = <String, dynamic>{};
|
// ── Query 1: Push-key entries dengan field `timestamp` ──────────────────
|
||||||
|
final startTs = startDate.millisecondsSinceEpoch;
|
||||||
|
final endTs =
|
||||||
|
endDate
|
||||||
|
.copyWith(hour: 23, minute: 59, second: 59)
|
||||||
|
.millisecondsSinceEpoch;
|
||||||
|
|
||||||
// Loop through each day in the range
|
final tsSnapshot =
|
||||||
for (
|
await _database
|
||||||
var date = startDate;
|
.child('history')
|
||||||
date.isBefore(endDate.add(const Duration(days: 1)));
|
.orderByChild('timestamp')
|
||||||
date = date.add(const Duration(days: 1))
|
.startAt(startTs)
|
||||||
) {
|
.endAt(endTs)
|
||||||
final dateKey =
|
.get();
|
||||||
'${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
|
||||||
|
|
||||||
final snapshot = await _database.child('history/$dateKey').get();
|
if (tsSnapshot.exists && tsSnapshot.value != null) {
|
||||||
if (snapshot.exists) {
|
final raw = Map<String, dynamic>.from(tsSnapshot.value as Map);
|
||||||
allHistory[dateKey] = snapshot.value;
|
result.addAll(raw);
|
||||||
}
|
debugPrint('[History] Push-key entries fetched: ${raw.length}');
|
||||||
}
|
}
|
||||||
|
|
||||||
return allHistory;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error getting history: $e');
|
debugPrint('Error fetching push-key history: $e');
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// ── Query 2: Date-keyed entries (format ESP32: YYYY-MM-DD/HH:mm) ───────
|
||||||
|
// Iterasi setiap tanggal dalam rentang
|
||||||
|
DateTime cursor = DateTime(startDate.year, startDate.month, startDate.day);
|
||||||
|
final endDay = DateTime(endDate.year, endDate.month, endDate.day);
|
||||||
|
|
||||||
|
while (!cursor.isAfter(endDay)) {
|
||||||
|
final dateStr =
|
||||||
|
'${cursor.year}-'
|
||||||
|
'${cursor.month.toString().padLeft(2, '0')}-'
|
||||||
|
'${cursor.day.toString().padLeft(2, '0')}';
|
||||||
|
|
||||||
|
final daySnapshot = await _database.child('history/$dateStr').get();
|
||||||
|
|
||||||
|
if (daySnapshot.exists && daySnapshot.value != null) {
|
||||||
|
// Simpan dengan prefix 'date|' agar bisa dikenali oleh parser
|
||||||
|
result['date|$dateStr'] = daySnapshot.value;
|
||||||
|
debugPrint('[History] Date-keyed entries for $dateStr fetched');
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = cursor.add(const Duration(days: 1));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error fetching date-keyed history: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('[History] Total raw keys returned: ${result.length}');
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Get latest history entries (limit)
|
/// Get latest history entries (limit)
|
||||||
Future<Map<String, dynamic>> getLatestHistory({int limit = 100}) async {
|
Future<Map<String, dynamic>> getLatestHistory({int limit = 100}) async {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -23,27 +23,9 @@ class HistoryLoggingService {
|
||||||
|
|
||||||
/// Start history logging service
|
/// Start history logging service
|
||||||
void start({Duration? interval}) {
|
void start({Duration? interval}) {
|
||||||
if (_isActive) {
|
// Disabled on mobile app to run exclusively on Railway/Worker
|
||||||
debugPrint('📊 History logging already active');
|
debugPrint('📊 History logging is disabled on mobile app (runs on Railway worker instead)');
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
if (interval != null) {
|
|
||||||
_loggingInterval = interval;
|
|
||||||
}
|
|
||||||
|
|
||||||
_isActive = true;
|
|
||||||
debugPrint(
|
|
||||||
'📊 History logging started (interval: ${_loggingInterval.inMinutes} min)',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Log immediately on start
|
|
||||||
_logCurrentData();
|
|
||||||
|
|
||||||
// Then log periodically
|
|
||||||
_loggingTimer = Timer.periodic(_loggingInterval, (timer) {
|
|
||||||
_logCurrentData();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stop history logging service
|
/// Stop history logging service
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:firebase_database/firebase_database.dart';
|
import 'package:firebase_database/firebase_database.dart';
|
||||||
import '../models/jadwal_model.dart';
|
import '../models/jadwal_model.dart';
|
||||||
|
import 'notification_service.dart';
|
||||||
|
|
||||||
/// Service untuk manage Jadwal di Firebase
|
/// Service untuk manage Jadwal di Firebase
|
||||||
class JadwalService {
|
class JadwalService {
|
||||||
|
|
@ -64,6 +65,9 @@ class JadwalService {
|
||||||
Future<bool> saveJadwal(JadwalModel jadwal) async {
|
Future<bool> saveJadwal(JadwalModel jadwal) async {
|
||||||
try {
|
try {
|
||||||
await _kontrolRef.child(jadwal.id).set(jadwal.toJson());
|
await _kontrolRef.child(jadwal.id).set(jadwal.toJson());
|
||||||
|
// Sync both reminder notifications and watering execution schedules
|
||||||
|
await NotificationService.instance.syncScheduleRemindersFromFirebase();
|
||||||
|
await NotificationService.instance.scheduleAllWateringExecutions();
|
||||||
print('✅ Jadwal ${jadwal.id} saved successfully');
|
print('✅ Jadwal ${jadwal.id} saved successfully');
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -76,6 +80,9 @@ class JadwalService {
|
||||||
Future<bool> deleteJadwal(String jadwalId) async {
|
Future<bool> deleteJadwal(String jadwalId) async {
|
||||||
try {
|
try {
|
||||||
await _kontrolRef.child(jadwalId).remove();
|
await _kontrolRef.child(jadwalId).remove();
|
||||||
|
// Sync both reminder notifications and watering execution schedules
|
||||||
|
await NotificationService.instance.syncScheduleRemindersFromFirebase();
|
||||||
|
await NotificationService.instance.scheduleAllWateringExecutions();
|
||||||
print('✅ Jadwal $jadwalId deleted successfully');
|
print('✅ Jadwal $jadwalId deleted successfully');
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -88,6 +95,9 @@ class JadwalService {
|
||||||
Future<bool> toggleJadwalAktif(String jadwalId, bool aktif) async {
|
Future<bool> toggleJadwalAktif(String jadwalId, bool aktif) async {
|
||||||
try {
|
try {
|
||||||
await _kontrolRef.child(jadwalId).child('aktif').set(aktif);
|
await _kontrolRef.child(jadwalId).child('aktif').set(aktif);
|
||||||
|
// Sync both reminder notifications and watering execution schedules
|
||||||
|
await NotificationService.instance.syncScheduleRemindersFromFirebase();
|
||||||
|
await NotificationService.instance.scheduleAllWateringExecutions();
|
||||||
print('✅ Jadwal $jadwalId set to ${aktif ? "aktif" : "nonaktif"}');
|
print('✅ Jadwal $jadwalId set to ${aktif ? "aktif" : "nonaktif"}');
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -135,7 +145,22 @@ class JadwalService {
|
||||||
Future<bool> setWaktuModeStatus(bool enabled) async {
|
Future<bool> setWaktuModeStatus(bool enabled) async {
|
||||||
try {
|
try {
|
||||||
await _kontrolRef.child('waktu').set(enabled);
|
await _kontrolRef.child('waktu').set(enabled);
|
||||||
|
|
||||||
|
// Sync reminder notifications
|
||||||
|
await NotificationService.instance.syncScheduleRemindersFromFirebase();
|
||||||
|
|
||||||
|
// Schedule/cancel watering executions based on mode
|
||||||
|
if (enabled) {
|
||||||
|
await NotificationService.instance.scheduleAllWateringExecutions();
|
||||||
|
} else {
|
||||||
|
await NotificationService.instance.cancelAllWateringSchedules();
|
||||||
|
}
|
||||||
|
|
||||||
print('✅ Waktu mode set to ${enabled ? "enabled" : "disabled"}');
|
print('✅ Waktu mode set to ${enabled ? "enabled" : "disabled"}');
|
||||||
|
await NotificationService.instance.showModeChangedNotification(
|
||||||
|
modeLabel: 'berdasarkan jadwal',
|
||||||
|
enabled: enabled,
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('❌ Error setting waktu mode: $e');
|
print('❌ Error setting waktu mode: $e');
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart';
|
||||||
import 'firebase_database_service.dart';
|
import 'firebase_database_service.dart';
|
||||||
import 'automation_constants.dart';
|
import 'automation_constants.dart';
|
||||||
import 'connection_monitor_service.dart';
|
import 'connection_monitor_service.dart';
|
||||||
|
import 'notification_service.dart';
|
||||||
|
|
||||||
/// Service untuk menghandle logika otomatis kontrol waktu dan sensor
|
/// Service untuk menghandle logika otomatis kontrol waktu dan sensor
|
||||||
/// Berjalan di background untuk monitoring dan eksekusi otomatis
|
/// Berjalan di background untuk monitoring dan eksekusi otomatis
|
||||||
|
|
@ -31,24 +32,29 @@ class KontrolAutomationService {
|
||||||
// ==================== KONTROL WAKTU ====================
|
// ==================== KONTROL WAKTU ====================
|
||||||
|
|
||||||
/// Start monitoring waktu mode
|
/// Start monitoring waktu mode
|
||||||
/// Cek setiap menit apakah ada jadwal yang harus dijalankan
|
/// Uses NotificationService exact alarm scheduling instead of polling
|
||||||
|
/// This eliminates dependency on worker and timer-based checking
|
||||||
void startWaktuMode() {
|
void startWaktuMode() {
|
||||||
|
// Disabled on mobile app to let Railway worker handle all scheduling/watering
|
||||||
|
debugPrint('🕐 Waktu Mode: Disabled on mobile app (handled by Railway worker instead)');
|
||||||
|
if (DateTime.now().year > 2000) return; // Early exit to prevent background scheduling/watering in app
|
||||||
|
|
||||||
if (_isWaktuModeActive) return;
|
if (_isWaktuModeActive) return;
|
||||||
|
|
||||||
// Start connection monitor
|
// Start connection monitor for sensor fallback
|
||||||
_connectionMonitor.start();
|
_connectionMonitor.start();
|
||||||
|
|
||||||
_isWaktuModeActive = true;
|
_isWaktuModeActive = true;
|
||||||
debugPrint('🕐 Waktu Mode: Started');
|
debugPrint('🕐 Waktu Mode: Started (using exact alarm scheduling)');
|
||||||
|
|
||||||
// Cek setiap 30 detik (gunakan constant)
|
// Setup callback to handle watering execution when alarm fires
|
||||||
_waktuCheckTimer = Timer.periodic(
|
NotificationService.setOnWateringScheduleCallback(
|
||||||
Duration(seconds: AutomationConstants.waktuCheckInterval),
|
_handleWateringScheduleExecution,
|
||||||
(timer) => _checkScheduledWatering(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Jalankan check pertama kali
|
// Schedule all watering executions using exact alarm
|
||||||
_checkScheduledWatering();
|
// This will fire even when app is minimized/killed
|
||||||
|
_scheduleWateringExecutions();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stop monitoring waktu mode
|
/// Stop monitoring waktu mode
|
||||||
|
|
@ -56,84 +62,72 @@ class KontrolAutomationService {
|
||||||
_waktuCheckTimer?.cancel();
|
_waktuCheckTimer?.cancel();
|
||||||
_waktuCheckTimer = null;
|
_waktuCheckTimer = null;
|
||||||
_isWaktuModeActive = false;
|
_isWaktuModeActive = false;
|
||||||
|
|
||||||
|
// Cancel all watering schedule executions
|
||||||
|
_cancelWateringSchedules();
|
||||||
|
|
||||||
|
// Clear callback
|
||||||
|
NotificationService.setOnWateringScheduleCallback(null);
|
||||||
|
|
||||||
debugPrint('🕐 Waktu Mode: Stopped');
|
debugPrint('🕐 Waktu Mode: Stopped');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check apakah ada jadwal penyiraman yang harus dijalankan
|
/// Schedule all watering executions from Firebase
|
||||||
Future<void> _checkScheduledWatering() async {
|
/// Called when waktu mode starts or when jadwal changes
|
||||||
|
Future<void> _scheduleWateringExecutions() async {
|
||||||
try {
|
try {
|
||||||
// Check connection first
|
await NotificationService.instance.scheduleAllWateringExecutions();
|
||||||
if (!_connectionMonitor.isConnected) {
|
debugPrint(
|
||||||
debugPrint('⚠️ No Firebase connection, skipping schedule check');
|
'✅ All watering executions scheduled via notification service',
|
||||||
return;
|
);
|
||||||
}
|
|
||||||
|
|
||||||
final kontrolConfig = await _dbService.getKontrolConfig();
|
|
||||||
final waktuEnabled = kontrolConfig['waktu'] ?? false;
|
|
||||||
|
|
||||||
if (!waktuEnabled || !_isWaktuModeActive) return;
|
|
||||||
|
|
||||||
final now = DateTime.now();
|
|
||||||
final currentTime =
|
|
||||||
'${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}';
|
|
||||||
|
|
||||||
final waktu1 = kontrolConfig['waktu_1'] ?? '';
|
|
||||||
final waktu2 = kontrolConfig['waktu_2'] ?? '';
|
|
||||||
final durasi1 =
|
|
||||||
kontrolConfig['durasi_1'] ?? AutomationConstants.defaultDurasiDetik;
|
|
||||||
final durasi2 =
|
|
||||||
kontrolConfig['durasi_2'] ?? AutomationConstants.defaultDurasiDetik;
|
|
||||||
|
|
||||||
// Check Jadwal 1
|
|
||||||
if (waktu1.isNotEmpty &&
|
|
||||||
_shouldStartWatering('jadwal_1', currentTime, waktu1)) {
|
|
||||||
debugPrint('🕐 Executing Jadwal 1 at $currentTime');
|
|
||||||
await _executeWatering(
|
|
||||||
scheduleId: 'jadwal_1',
|
|
||||||
pompaAir: true,
|
|
||||||
pompaPupuk: true,
|
|
||||||
pots: [1, 2, 3, 4, 5], // Semua pot
|
|
||||||
durasiDetik: durasi1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check Jadwal 2
|
|
||||||
if (waktu2.isNotEmpty &&
|
|
||||||
_shouldStartWatering('jadwal_2', currentTime, waktu2)) {
|
|
||||||
debugPrint('🕐 Executing Jadwal 2 at $currentTime');
|
|
||||||
await _executeWatering(
|
|
||||||
scheduleId: 'jadwal_2',
|
|
||||||
pompaAir: true,
|
|
||||||
pompaPupuk: true,
|
|
||||||
pots: [1, 2, 3, 4, 5], // Semua pot
|
|
||||||
durasiDetik: durasi2,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ Error checking scheduled watering: $e');
|
debugPrint('❌ Error scheduling watering executions: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check apakah jadwal harus dijalankan
|
/// Cancel all watering schedule executions
|
||||||
bool _shouldStartWatering(
|
Future<void> _cancelWateringSchedules() async {
|
||||||
String scheduleId,
|
try {
|
||||||
String currentTime,
|
await NotificationService.instance.cancelAllWateringSchedules();
|
||||||
String targetTime,
|
debugPrint('✅ All watering schedule executions cancelled');
|
||||||
) {
|
} catch (e) {
|
||||||
// Cek apakah waktu sekarang sama dengan target
|
debugPrint('❌ Error cancelling watering schedules: $e');
|
||||||
if (currentTime != targetTime) return false;
|
|
||||||
|
|
||||||
// Cek apakah sudah berjalan
|
|
||||||
if (_isWateringActive[scheduleId] == true) return false;
|
|
||||||
|
|
||||||
// Cek apakah sudah pernah dijalankan dalam 1 menit terakhir (prevent double trigger)
|
|
||||||
final lastTime = _lastWateringTime[scheduleId];
|
|
||||||
if (lastTime != null) {
|
|
||||||
final diff = DateTime.now().difference(lastTime);
|
|
||||||
if (diff.inSeconds < 60) return false;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
/// Handle watering execution when scheduled alarm fires
|
||||||
|
/// This is called from NotificationService when notification triggers
|
||||||
|
Future<void> _handleWateringScheduleExecution(dynamic scheduleData) async {
|
||||||
|
try {
|
||||||
|
if (scheduleData is! Map) {
|
||||||
|
debugPrint('⚠️ Invalid schedule data: $scheduleData');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = Map<String, dynamic>.from(scheduleData);
|
||||||
|
final scheduleId = (data['scheduleId'] ?? 'unknown') as String;
|
||||||
|
final pompaAir = data['pompa_air'] ?? true;
|
||||||
|
final pompaPupuk = data['pompa_pupuk'] ?? false;
|
||||||
|
final durasiDetik = data['durasi'] ?? 60;
|
||||||
|
final potAktif = List<int>.from(data['pot_aktif'] ?? []);
|
||||||
|
|
||||||
|
if (potAktif.isNotEmpty) {
|
||||||
|
debugPrint(
|
||||||
|
'💧 Executing scheduled watering: $scheduleId (${durasiDetik}s)',
|
||||||
|
);
|
||||||
|
await _executeWatering(
|
||||||
|
scheduleId: scheduleId,
|
||||||
|
pompaAir: pompaAir,
|
||||||
|
pompaPupuk: pompaPupuk,
|
||||||
|
pots: potAktif,
|
||||||
|
durasiDetik: durasiDetik,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
debugPrint('⚠️ No pots selected for $scheduleId');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ Error handling watering execution: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute penyiraman dengan durasi tertentu
|
/// Execute penyiraman dengan durasi tertentu
|
||||||
|
|
@ -171,6 +165,26 @@ class KontrolAutomationService {
|
||||||
await _dbService.setMultipleAktuator(offUpdates);
|
await _dbService.setMultipleAktuator(offUpdates);
|
||||||
|
|
||||||
debugPrint('✅ Watering completed for $scheduleId');
|
debugPrint('✅ Watering completed for $scheduleId');
|
||||||
|
|
||||||
|
// Send notification after watering is done
|
||||||
|
try {
|
||||||
|
final potText =
|
||||||
|
pots.length == 5 ? 'semua pot' : 'pot ${pots.join(', ')}';
|
||||||
|
await NotificationService.instance.showWateringNotification(
|
||||||
|
title: 'IrriGo - Penyiraman Selesai',
|
||||||
|
body:
|
||||||
|
'Penyiraman selesai untuk $potText selama ${durasiDetik} detik.',
|
||||||
|
payload: {
|
||||||
|
'type': 'watering_completed',
|
||||||
|
'scheduleId': scheduleId,
|
||||||
|
'pots': pots,
|
||||||
|
'duration': durasiDetik,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (notifError) {
|
||||||
|
debugPrint('⚠️ Error sending notification: $notifError');
|
||||||
|
}
|
||||||
|
|
||||||
_isWateringActive[scheduleId] = false;
|
_isWateringActive[scheduleId] = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ Error executing watering: $e');
|
debugPrint('❌ Error executing watering: $e');
|
||||||
|
|
@ -182,6 +196,10 @@ class KontrolAutomationService {
|
||||||
|
|
||||||
/// Start monitoring sensor mode
|
/// Start monitoring sensor mode
|
||||||
void startSensorMode() {
|
void startSensorMode() {
|
||||||
|
// Disabled on mobile app to let Railway worker handle all automation
|
||||||
|
debugPrint('🌡️ Sensor Mode: Disabled on mobile app (handled by Railway worker instead)');
|
||||||
|
if (DateTime.now().year > 2000) return; // Early exit to prevent background sensor check in app
|
||||||
|
|
||||||
if (_isSensorModeActive) return;
|
if (_isSensorModeActive) return;
|
||||||
|
|
||||||
// Start connection monitor
|
// Start connection monitor
|
||||||
|
|
@ -390,6 +408,24 @@ class KontrolAutomationService {
|
||||||
'✅ Sensor Mode: Watering completed for POT $potNumber (${elapsedSeconds}s, mode: $mode)',
|
'✅ Sensor Mode: Watering completed for POT $potNumber (${elapsedSeconds}s, mode: $mode)',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Send notification after watering is done
|
||||||
|
try {
|
||||||
|
await NotificationService.instance.showWateringNotification(
|
||||||
|
title: 'IrriGo - Penyiraman Sensor Selesai',
|
||||||
|
body:
|
||||||
|
'Penyiraman POT $potNumber selesai (kelembapan: $soilValue%, durasi: ${elapsedSeconds}s, mode: $mode).',
|
||||||
|
payload: {
|
||||||
|
'type': 'sensor_watering_completed',
|
||||||
|
'pot': potNumber,
|
||||||
|
'soil': soilValue,
|
||||||
|
'duration': elapsedSeconds,
|
||||||
|
'mode': mode,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (notifError) {
|
||||||
|
debugPrint('⚠️ Error sending notification: $notifError');
|
||||||
|
}
|
||||||
|
|
||||||
// Pastikan flag ter-reset dengan benar
|
// Pastikan flag ter-reset dengan benar
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
_isWateringActive[potKey] = false;
|
_isWateringActive[potKey] = false;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:firebase_database/firebase_database.dart';
|
import 'package:firebase_database/firebase_database.dart';
|
||||||
import '../models/threshold_model.dart';
|
import '../models/threshold_model.dart';
|
||||||
|
import 'notification_service.dart';
|
||||||
|
|
||||||
class ThresholdService {
|
class ThresholdService {
|
||||||
static const String kontrolPath = 'kontrol_1';
|
static const String kontrolPath = 'kontrol_1';
|
||||||
|
|
@ -232,6 +233,10 @@ class ThresholdService {
|
||||||
try {
|
try {
|
||||||
await _dbRef.child(kontrolPath).child('otomatis').set(enabled);
|
await _dbRef.child(kontrolPath).child('otomatis').set(enabled);
|
||||||
print('✅ Sensor mode set to ${enabled ? "enabled" : "disabled"}');
|
print('✅ Sensor mode set to ${enabled ? "enabled" : "disabled"}');
|
||||||
|
await NotificationService.instance.showModeChangedNotification(
|
||||||
|
modeLabel: 'otomatis berbasis sensor',
|
||||||
|
enabled: enabled,
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('❌ Error setting sensor mode: $e');
|
print('❌ Error setting sensor mode: $e');
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ import Foundation
|
||||||
import firebase_auth
|
import firebase_auth
|
||||||
import firebase_core
|
import firebase_core
|
||||||
import firebase_database
|
import firebase_database
|
||||||
|
import firebase_messaging
|
||||||
|
import flutter_local_notifications
|
||||||
import google_sign_in_ios
|
import google_sign_in_ios
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
|
|
||||||
|
|
@ -15,6 +17,8 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
|
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
|
||||||
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||||
FLTFirebaseDatabasePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseDatabasePlugin"))
|
FLTFirebaseDatabasePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseDatabasePlugin"))
|
||||||
|
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
|
||||||
|
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||||
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
|
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
156
pubspec.lock
|
|
@ -13,10 +13,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: archive
|
name: archive
|
||||||
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
|
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.7"
|
version: "4.0.9"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -45,10 +45,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.1"
|
||||||
checked_yaml:
|
checked_yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -81,14 +81,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.1"
|
version: "1.19.1"
|
||||||
crypto:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: crypto
|
|
||||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.0.7"
|
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -97,22 +89,38 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.8"
|
version: "1.0.8"
|
||||||
|
dbus:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dbus
|
||||||
|
sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.12"
|
||||||
|
equatable:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: equatable
|
||||||
|
sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.8"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: fake_async
|
name: fake_async
|
||||||
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
|
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.2"
|
version: "1.3.3"
|
||||||
ffi:
|
ffi:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: ffi
|
name: ffi
|
||||||
sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c
|
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.5"
|
version: "2.2.0"
|
||||||
file:
|
file:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -157,10 +165,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_core_platform_interface
|
name: firebase_core_platform_interface
|
||||||
sha256: cccb4f572325dc14904c02fcc7db6323ad62ba02536833dddb5c02cac7341c64
|
sha256: "0ecda14c1bfc9ed8cac303dd0f8d04a320811b479362a9a4efb14fd331a473ce"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.2"
|
version: "6.0.3"
|
||||||
firebase_core_web:
|
firebase_core_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -193,6 +201,38 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.6+16"
|
version: "0.2.6+16"
|
||||||
|
firebase_messaging:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: firebase_messaging
|
||||||
|
sha256: "60be38574f8b5658e2f22b7e311ff2064bea835c248424a383783464e8e02fcc"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "15.2.10"
|
||||||
|
firebase_messaging_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: firebase_messaging_platform_interface
|
||||||
|
sha256: "685e1771b3d1f9c8502771ccc9f91485b376ffe16d553533f335b9183ea99754"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.6.10"
|
||||||
|
firebase_messaging_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: firebase_messaging_web
|
||||||
|
sha256: "0d1be17bc89ed3ff5001789c92df678b2e963a51b6fa2bdb467532cc9dbed390"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.10.10"
|
||||||
|
fl_chart:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: fl_chart
|
||||||
|
sha256: "74959b99b92b9eebeed1a4049426fd67c4abc3c5a0f4d12e2877097d6a11ae08"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.69.2"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|
@ -214,6 +254,30 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.0"
|
version: "5.0.0"
|
||||||
|
flutter_local_notifications:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_local_notifications
|
||||||
|
sha256: "674173fd3c9eda9d4c8528da2ce0ea69f161577495a9cc835a2a4ecd7eadeb35"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "17.2.4"
|
||||||
|
flutter_local_notifications_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_local_notifications_linux
|
||||||
|
sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.1"
|
||||||
|
flutter_local_notifications_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_local_notifications_platform_interface
|
||||||
|
sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.2.0"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|
@ -292,10 +356,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image
|
name: image
|
||||||
sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c"
|
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.7.2"
|
version: "4.8.0"
|
||||||
json_annotation:
|
json_annotation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -308,26 +372,26 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker
|
name: leak_tracker
|
||||||
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.8"
|
version: "11.0.2"
|
||||||
leak_tracker_flutter_testing:
|
leak_tracker_flutter_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_flutter_testing
|
name: leak_tracker_flutter_testing
|
||||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.9"
|
version: "3.0.10"
|
||||||
leak_tracker_testing:
|
leak_tracker_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_testing
|
name: leak_tracker_testing
|
||||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.2"
|
||||||
lints:
|
lints:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -340,34 +404,34 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: logger
|
name: logger
|
||||||
sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3
|
sha256: "25aee487596a6257655a1e091ec2ae66bc30e7af663592cc3a27e6591e05035c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.6.2"
|
version: "2.7.0"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.17"
|
version: "0.12.19"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.11.1"
|
version: "0.13.0"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.16.0"
|
version: "1.18.0"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -428,10 +492,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: posix
|
name: posix
|
||||||
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
|
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.3"
|
version: "6.5.0"
|
||||||
rename:
|
rename:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -545,10 +609,18 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.4"
|
version: "0.7.11"
|
||||||
|
timezone:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: timezone
|
||||||
|
sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.4"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -558,13 +630,13 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: vector_math
|
name: vector_math
|
||||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.2.0"
|
||||||
vm_service:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -606,5 +678,5 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.7.2 <4.0.0"
|
dart: ">=3.10.0-0 <4.0.0"
|
||||||
flutter: ">=3.29.0"
|
flutter: ">=3.29.0"
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,11 @@ dependencies:
|
||||||
firebase_database: ^11.1.8
|
firebase_database: ^11.1.8
|
||||||
google_sign_in: ^6.2.1
|
google_sign_in: ^6.2.1
|
||||||
shared_preferences: ^2.2.2
|
shared_preferences: ^2.2.2
|
||||||
|
fl_chart: ^0.69.0
|
||||||
|
firebase_messaging: ^15.1.6
|
||||||
|
flutter_local_notifications: ^17.2.3
|
||||||
|
timezone: ^0.9.4
|
||||||
|
vector_math: ^2.1.4
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
@ -74,10 +79,10 @@ flutter:
|
||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
android: true
|
android: true
|
||||||
ios: false
|
ios: false
|
||||||
image_path: "assets/images/logo_apsgo.png"
|
image_path: "assets/images/logo_irrigo.png"
|
||||||
# Uncomment jika ingin adaptive icon (Android 8.0+)
|
# Uncomment jika ingin adaptive icon (Android 8.0+)
|
||||||
# adaptive_icon_background: "#4CAF50"
|
# adaptive_icon_background: "#4CAF50"
|
||||||
# adaptive_icon_foreground: "assets/images/logo_apsgo.png"
|
# adaptive_icon_foreground: "assets/images/logo_irrigo.png"
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
# https://flutter.dev/to/resolution-aware-images
|
# https://flutter.dev/to/resolution-aware-images
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Firebase Configuration
|
||||||
|
# Generated from Service Account JSON on 2026-02-11T00:11:59.245Z
|
||||||
|
|
||||||
|
FIREBASE_PROJECT_ID=project-ta-951b4
|
||||||
|
FIREBASE_CLIENT_EMAIL=firebase-adminsdk-fbsvc@project-ta-951b4.iam.gserviceaccount.com
|
||||||
|
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDiJXKVExyqW7iW\n1JiNP6qkMD25rjxLqA+/b9s1hnZCCuMkt51g85+52VvKDi55JgsnrEZuACB2bi+A\nPc7L9Gw+VHmqJQa4udngojvwN4gll2uYbgJ1DGd6gLrFGsXK3BgWx5/7jZU9YY4p\n2RiQqHSm60Jrn7f2tOen9ImBZveAmsZrGE/3+hkEdGZN/aL94ff+L1bqNLDb7mAP\nKErBjB/5uSjAYHQ8y/lMwQBLBrwhL+PGZtMqjlLTWLzO6LtA4RqLfncptI3lUFu5\nkIOztPM42WzHfvBSao02s2KKV8d/4HzUCY5hq89Wfx3fLzbjvDw/vkr/cwwcIexf\nNjj71eITAgMBAAECggEAPeV0/qNsAZ64H6RRmrt1x5mik1tCioyVvtZtP7GtVXco\nY2jULPCRY6s/ycZm5ubIP9QRtNLZD8EpxCZmnwEnzUmOwUiAFfhcT3ToBeAVJgDT\nRLW7e3DpM6jfNcqVn2ftOfLqcU33roSwhfizFgjdYMBcfhuJnP83tosiRaY6kNqI\nNWkMvpBNVvz2aJQYECracQzKGKlgh17GRzpMs6gw1DTMK5L4E1gCnsXsshqMv2jO\nXTdlSvtkY9/WPYxygVzApiKgxIQxE0N0eJhtqxJ1LIK5l6iGC4RI0J66TTzxWda9\nAQo/p4V2x/1FIJv2xbchQGzwijf0WcAq7SLY0VuffQKBgQD4BlCbrIZcPG/5RHfV\nXYybJcU7oyHVskJ+Y9amoCIzyNEwdK/RDWuAdmGt0IOXpTY3gwideefToWmL3BmD\nOZxz3DW9L3+vzOlTtI802ZFN0iCBtfljjtYCjMPdCPnz195RkMSlKtJqouvBocmL\n5ElaEvtsjoRtjcaLv+JZfl/QPwKBgQDpawjaTIHunT2OrNlOLPRXcePOmmvUrguB\n59KwAyxj2q5vXBuRDF3wdZoYA4oLh9lAKxwCjIL4FmTJ/IL2x9G94pR9VaOkzx3Q\nUr6yp5Y5OpEghmm7NCpj/Ri/JHQpLQb86xgzrjd2IpaBbTpKY769C8iV0vcLvUhD\n1siqsE/5LQKBgD9H1S8y4IEOBn+xwWVV3fcDtwPVRl+wgJigvnZ66t8NgJn9W5JZ\n+zCmg5uGVNzPETsumncbUFPFnhlKCdRaUZmZ+LgRM2HWRT3PFq6kSBdz4klV9ZA6\nNE4oPhzaGnPlgtqKtjFx8Ie2k7Iupi6kCzcJGs8CubT99EdZqNWukaGfAoGBAMWH\n++zpVAe/b4mfQyLCLmEWE4gS7HAGfdyId6bWeKlkNbwtwXJledX1X9s0m40YgMSp\n9sE9cdRK9y5sD9SR0zCTX8AVSjA9ymgyrgj4g8uYgZp5xV5UTg9h76BKVDdIUKKW\nvVt28p55tM6AxhSQBqlrIGuSJdj8bPxj9ltka7ldAoGBAN4Uzy6njLEDwvt4bK+m\npUwL9vOeH182SZ/gM26gh/4ov5qAdoyRa78Ek1EZMiTJ3xIJdckNtuNJB8/bIcdH\nh2DriUnxEOoU5gu6ErVIKcfVn/7JTvBfnkE+lVLnCed4IBr5Kh4Woaa74s5r7JMp\ndy24IcgxQ5sczYS5yrXeUqAs\n-----END PRIVATE KEY-----\n"
|
||||||
|
|
||||||
|
FIREBASE_DATABASE_URL=https://project-ta-951b4-default-rtdb.firebaseio.com
|
||||||
|
|
||||||
|
# Redis Configuration (Railway will auto-provide these)
|
||||||
|
# REDIS_HOST=localhost
|
||||||
|
# REDIS_PORT=6379
|
||||||
|
# REDIS_PASSWORD=
|
||||||
|
|
@ -1,27 +1,45 @@
|
||||||
# Dependencies
|
# Miscellaneous
|
||||||
node_modules/
|
*.class
|
||||||
package-lock.json
|
|
||||||
yarn.lock
|
|
||||||
|
|
||||||
# Environment variables
|
|
||||||
.env
|
|
||||||
.env.local
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
logs/
|
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
*.pyc
|
||||||
|
|
||||||
# OS files
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# IDE
|
|
||||||
.vscode/
|
|
||||||
.idea/
|
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.build/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
.swiftpm/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
# Firebase service account
|
# IntelliJ related
|
||||||
serviceAccount.json
|
*.iml
|
||||||
firebase-key.json
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
|
||||||
|
# Symbolication related
|
||||||
|
app.*.symbols
|
||||||
|
|
||||||
|
# Obfuscation related
|
||||||
|
app.*.map.json
|
||||||
|
|
||||||
|
# Android Studio will place build artifacts here
|
||||||
|
/android/app/debug
|
||||||
|
/android/app/profile
|
||||||
|
/android/app/release
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
# This file tracks properties of this Flutter project.
|
||||||
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
|
#
|
||||||
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
|
version:
|
||||||
|
revision: "ea121f8859e4b13e47a8f845e4586164519588bc"
|
||||||
|
channel: "stable"
|
||||||
|
|
||||||
|
project_type: app
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||||
|
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||||
|
- platform: android
|
||||||
|
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||||
|
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||||
|
- platform: ios
|
||||||
|
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||||
|
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||||
|
- platform: linux
|
||||||
|
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||||
|
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||||
|
- platform: macos
|
||||||
|
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||||
|
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||||
|
- platform: web
|
||||||
|
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||||
|
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||||
|
- platform: windows
|
||||||
|
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||||
|
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
|
||||||
|
|
||||||
|
# User provided section
|
||||||
|
|
||||||
|
# List of Local paths (relative to this file) that should be
|
||||||
|
# ignored by the migrate tool.
|
||||||
|
#
|
||||||
|
# Files that are not part of the templates will be ignored by default.
|
||||||
|
unmanaged_files:
|
||||||
|
- 'lib/main.dart'
|
||||||
|
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"chat.tools.terminal.autoApprove": {
|
||||||
|
"git init": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,148 +1,62 @@
|
||||||
# ApsGo Railway Worker
|
# ApsGo - Sistem Penjadwalan Penyiraman Otomatis
|
||||||
|
|
||||||
Background worker service untuk sistem otomasi IoT ApsGo. Service ini berjalan 24/7 di cloud untuk menjalankan penjadwalan dan automation bahkan ketika aplikasi mobile ditutup atau handphone pengguna mati.
|
Aplikasi Flutter untuk kontrol otomatis sistem penyiraman tanaman berbasis IoT dengan Firebase Realtime Database dan Railway Worker.
|
||||||
|
|
||||||
## Features
|
## Status Terkini
|
||||||
|
|
||||||
- ✅ **Waktu Mode**: Penjadwalan berdasarkan waktu (cron-based)
|
✅ **Sistem berjalan real-time dengan delay 20 detik/section**
|
||||||
- ✅ **Sensor Mode**: Otomasi berdasarkan threshold kelembapan tanah
|
⚠️ **Catatan:** 1 waktu penjadwalan digunakan untuk banyak pompa sekaligus
|
||||||
- ✅ **Auto History Logging**: Record data sensor setiap 30 menit
|
|
||||||
- ✅ **Redis Queue**: Prevent race conditions dan manage concurrent tasks
|
|
||||||
- ✅ **Graceful Shutdown**: Clean shutdown dengan safety turn-off semua aktuator
|
|
||||||
- ✅ **Health Monitoring**: Auto health check setiap 5 menit
|
|
||||||
- ✅ **Auto Cleanup**: Hapus history lama otomatis (retain 10 hari)
|
|
||||||
|
|
||||||
## Tech Stack
|
## Komponen Sistem
|
||||||
|
|
||||||
- **Node.js**: Runtime environment
|
### 1. Flutter Mobile App
|
||||||
- **Firebase Admin SDK**: Realtime Database integration
|
- Kontrol manual aktuator (pompa & mosvet)
|
||||||
- **BullMQ**: Robust job queue dengan Redis
|
- Penjadwalan otomatis (waktu mode)
|
||||||
- **Redis**: In-memory database untuk queue dan caching
|
- Monitoring sensor real-time
|
||||||
- **Cron**: Scheduled tasks
|
- History logging
|
||||||
|
|
||||||
## Setup Local Development
|
### 2. Railway Worker (Node.js)
|
||||||
|
- Background automation 24/7
|
||||||
|
- Scheduler dengan timezone Asia/Jakarta
|
||||||
|
- Firebase SDK dengan REST API fallback
|
||||||
|
- BullMQ job queue untuk reliability
|
||||||
|
- Auto-recovery dari timeout issues
|
||||||
|
|
||||||
1. Install dependencies:
|
## Fitur Utama
|
||||||
|
|
||||||
|
- **Dual Mode**: Kontrol manual + penjadwalan otomatis
|
||||||
|
- **Real-time Sync**: Firebase Realtime Database
|
||||||
|
- **Reliable Execution**: Timeout handling + REST API fallback
|
||||||
|
- **History Tracking**: Log semua aktivitas penyiraman
|
||||||
|
- **Safety Mechanisms**: Auto-shutoff jika error
|
||||||
|
|
||||||
|
## Teknologi
|
||||||
|
|
||||||
|
- **Frontend**: Flutter (Dart)
|
||||||
|
- **Backend Worker**: Node.js v18+
|
||||||
|
- **Database**: Firebase Realtime Database
|
||||||
|
- **Queue**: Redis + BullMQ
|
||||||
|
- **Deployment**: Railway.app
|
||||||
|
- **Authentication**: Firebase Auth + Google Sign-In
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Mobile App
|
||||||
```bash
|
```bash
|
||||||
|
flutter pub get
|
||||||
|
flutter run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Railway Worker
|
||||||
|
```bash
|
||||||
|
cd railway-worker
|
||||||
npm install
|
npm install
|
||||||
|
node worker.js
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Copy `.env.example` ke `.env` dan isi dengan credentials Firebase Anda:
|
## Dokumentasi
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Setup Redis lokal (gunakan Docker):
|
- [Railway Setup Guide](RAILWAY_QUICK_START.md)
|
||||||
```bash
|
- [Deployment Guide](DEPLOYMENT_GUIDE.md)
|
||||||
docker run -d -p 6379:6379 redis:latest
|
- [Debug & Testing Guide](DEBUG_TESTING_GUIDE.md)
|
||||||
```
|
- [Troubleshooting](RAILWAY_TROUBLESHOOTING.md)
|
||||||
|
|
||||||
4. Run worker:
|
|
||||||
```bash
|
|
||||||
npm run dev # Development mode dengan nodemon
|
|
||||||
# atau
|
|
||||||
npm start # Production mode
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deploy to Railway
|
|
||||||
|
|
||||||
Lihat file `DEPLOYMENT_GUIDE.md` untuk step-by-step deployment ke Railway.
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
| Variable | Description | Required |
|
|
||||||
|----------|-------------|----------|
|
|
||||||
| `FIREBASE_PROJECT_ID` | Firebase project ID | ✅ |
|
|
||||||
| `FIREBASE_CLIENT_EMAIL` | Firebase service account email | ✅ |
|
|
||||||
| `FIREBASE_PRIVATE_KEY` | Firebase service account private key | ✅ |
|
|
||||||
| `FIREBASE_DATABASE_URL` | Firebase Realtime Database URL | ✅ |
|
|
||||||
| `REDIS_HOST` | Redis hostname | ✅ |
|
|
||||||
| `REDIS_PORT` | Redis port (default: 6379) | ❌ |
|
|
||||||
| `REDIS_PASSWORD` | Redis password (if required) | ❌ |
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
Flutter App (Mobile)
|
|
||||||
↕
|
|
||||||
Firebase Realtime DB ← ESP32/Hardware
|
|
||||||
↕
|
|
||||||
Railway Worker (This service)
|
|
||||||
↕
|
|
||||||
Redis Queue
|
|
||||||
```
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
### Waktu Mode
|
|
||||||
- Worker check Firebase `/kontrol` setiap 30 detik
|
|
||||||
- Jika `waktu_1` atau `waktu_2` match dengan waktu sekarang, add job ke queue
|
|
||||||
- Job akan diprocess oleh worker untuk nyalakan pompa dan valve
|
|
||||||
- Setelah durasi selesai, otomatis matikan
|
|
||||||
|
|
||||||
### Sensor Mode
|
|
||||||
- Worker listen ke Firebase `/data` secara realtime
|
|
||||||
- Jika `soil_X` < `batas_bawah`, trigger watering untuk pot tersebut
|
|
||||||
- Ada cooldown 2 menit per pot untuk prevent over-watering
|
|
||||||
- Support 2 mode: `fixed` (durasi tetap) dan `smart` (sampai mencapai batas_atas)
|
|
||||||
|
|
||||||
### Safety Features
|
|
||||||
- Concurrency: 1 (hanya 1 job diprocess pada satu waktu)
|
|
||||||
- Debouncing: Minimum 2 menit antar penyiraman per pot
|
|
||||||
- Error handling: Jika error, otomatis turn OFF semua aktuator
|
|
||||||
- Graceful shutdown: Clean up resources saat restart/shutdown
|
|
||||||
|
|
||||||
## Monitoring
|
|
||||||
|
|
||||||
Worker akan log semua aktivitas ke console:
|
|
||||||
- ✅ Success operations
|
|
||||||
- ❌ Errors dengan details
|
|
||||||
- 💧 Watering jobs progress
|
|
||||||
- 📊 History logging
|
|
||||||
- 💚 Health check status
|
|
||||||
|
|
||||||
Di Railway dashboard, Anda bisa:
|
|
||||||
- View logs realtime
|
|
||||||
- Monitor CPU/Memory usage
|
|
||||||
- Setup alerts untuk failures
|
|
||||||
|
|
||||||
## Maintenance
|
|
||||||
|
|
||||||
### Manual Queue Management
|
|
||||||
|
|
||||||
Untuk clear queue (jika ada masalah):
|
|
||||||
```javascript
|
|
||||||
const { Queue } = require('bullmq');
|
|
||||||
const Redis = require('ioredis');
|
|
||||||
|
|
||||||
const redis = new Redis(process.env.REDIS_URL);
|
|
||||||
const queue = new Queue('watering', { connection: redis });
|
|
||||||
|
|
||||||
// Clear all jobs
|
|
||||||
await queue.obliterate();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database Cleanup
|
|
||||||
|
|
||||||
History otomatis di-cleanup setiap hari jam 2 pagi, hanya retain 10 hari terakhir.
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Worker tidak berjalan
|
|
||||||
1. Check environment variables
|
|
||||||
2. Check Firebase credentials
|
|
||||||
3. Check Redis connection
|
|
||||||
|
|
||||||
### Job tidak diprocess
|
|
||||||
1. Check queue status di logs
|
|
||||||
2. Verify Firebase rules mengizinkan admin access
|
|
||||||
3. Check concurrency setting
|
|
||||||
|
|
||||||
### Memory leak
|
|
||||||
- Worker menggunakan BullMQ yang sudah optimize untuk long-running process
|
|
||||||
- Auto cleanup completed jobs (retain last 100)
|
|
||||||
- Auto cleanup failed jobs (retain last 50)
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
# This file configures the analyzer, which statically analyzes Dart code to
|
||||||
|
# check for errors, warnings, and lints.
|
||||||
|
#
|
||||||
|
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||||
|
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||||
|
# invoked from the command line by running `flutter analyze`.
|
||||||
|
|
||||||
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
linter:
|
||||||
|
# The lint rules applied to this project can be customized in the
|
||||||
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||||
|
# included above or to enable additional rules. A list of all available lints
|
||||||
|
# and their documentation is published at https://dart.dev/lints.
|
||||||
|
#
|
||||||
|
# Instead of disabling a lint rule for the entire project in the
|
||||||
|
# section below, it can also be suppressed for a single line of code
|
||||||
|
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||||
|
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||||
|
# producing the lint.
|
||||||
|
rules:
|
||||||
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
gradle-wrapper.jar
|
||||||
|
/.gradle
|
||||||
|
/captures/
|
||||||
|
/gradlew
|
||||||
|
/gradlew.bat
|
||||||
|
/local.properties
|
||||||
|
GeneratedPluginRegistrant.java
|
||||||
|
.cxx/
|
||||||
|
|
||||||
|
# Remember to never publicly share your keystore.
|
||||||
|
# See https://flutter.dev/to/reference-keystore
|
||||||
|
key.properties
|
||||||
|
**/*.keystore
|
||||||
|
**/*.jks
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
id("kotlin-android")
|
||||||
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
|
id("com.google.gms.google-services")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.kampus.apsgo"
|
||||||
|
compileSdk = flutter.compileSdkVersion
|
||||||
|
ndkVersion = "27.0.12077973"
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
|
applicationId = "com.kampus.apsgo"
|
||||||
|
// You can update the following values to match your application needs.
|
||||||
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
|
minSdk = 23
|
||||||
|
targetSdk = flutter.targetSdkVersion
|
||||||
|
versionCode = flutter.versionCode
|
||||||
|
versionName = flutter.versionName
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
// TODO: Add your own signing config for the release build.
|
||||||
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter {
|
||||||
|
source = "../.."
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
{
|
||||||
|
"project_info": {
|
||||||
|
"project_number": "217854138058",
|
||||||
|
"project_id": "project-ta-951b4",
|
||||||
|
"storage_bucket": "project-ta-951b4.firebasestorage.app"
|
||||||
|
},
|
||||||
|
"client": [
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "1:217854138058:android:b8bd92665b1766ad0c4633",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "com.kampus.apsgo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [
|
||||||
|
{
|
||||||
|
"client_id": "217854138058-ml03se7pied9l9k3jcb5cpgl2bn3rto0.apps.googleusercontent.com",
|
||||||
|
"client_type": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "AIzaSyCrJAAUZAPettM5AwPbV3rAGlT4Jx922m8"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": [
|
||||||
|
{
|
||||||
|
"client_id": "217854138058-ml03se7pied9l9k3jcb5cpgl2bn3rto0.apps.googleusercontent.com",
|
||||||
|
"client_type": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configuration_version": "1"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<application
|
||||||
|
android:label="ApsGo"
|
||||||
|
android:name="${applicationName}"
|
||||||
|
android:icon="@mipmap/ic_launcher">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:taskAffinity=""
|
||||||
|
android:theme="@style/LaunchTheme"
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
|
the Android process has started. This theme is visible to the user
|
||||||
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
|
to determine the Window background behind the Flutter UI. -->
|
||||||
|
<meta-data
|
||||||
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
|
android:resource="@style/NormalTheme"
|
||||||
|
/>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<!-- Don't delete the meta-data below.
|
||||||
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
|
<meta-data
|
||||||
|
android:name="flutterEmbedding"
|
||||||
|
android:value="2" />
|
||||||
|
</application>
|
||||||
|
<!-- Required to query activities that can process text, see:
|
||||||
|
https://developer.android.com/training/package-visibility and
|
||||||
|
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||||
|
|
||||||
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
|
<data android:mimeType="text/plain"/>
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
</manifest>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.example.project_ta
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
class MainActivity : FlutterActivity()
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<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> -->
|
||||||
|
</layer-list>
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<!-- 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>
|
||||||
|
</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
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<!-- 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>
|
||||||
|
</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
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath("com.google.gms:google-services:4.4.2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
|
||||||
|
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||||
|
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||||
|
}
|
||||||
|
subprojects {
|
||||||
|
project.evaluationDependsOn(":app")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<Delete>("clean") {
|
||||||
|
delete(rootProject.layout.buildDirectory)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.enableJetifier=true
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
pluginManagement {
|
||||||
|
val flutterSdkPath = run {
|
||||||
|
val properties = java.util.Properties()
|
||||||
|
file("local.properties").inputStream().use { properties.load(it) }
|
||||||
|
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
|
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||||
|
flutterSdkPath
|
||||||
|
}
|
||||||
|
|
||||||
|
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
|
id("com.android.application") version "8.7.0" apply false
|
||||||
|
// START: FlutterFire Configuration
|
||||||
|
id("com.google.gms.google-services") version("4.3.15") apply false
|
||||||
|
// END: FlutterFire Configuration
|
||||||
|
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
include(":app")
|
||||||
|
After Width: | Height: | Size: 687 KiB |
|
|
@ -0,0 +1,160 @@
|
||||||
|
/**
|
||||||
|
* Script untuk membersihkan semua history data Firebase
|
||||||
|
* dan mengisinya dengan dummy data (40-80) untuk testing
|
||||||
|
*
|
||||||
|
* Jalankan dengan: node clear-and-populate-dummy-data.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
const admin = require('firebase-admin');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Initialize Firebase Admin
|
||||||
|
const serviceAccountPath = path.join(__dirname, 'service-account-key.json');
|
||||||
|
|
||||||
|
// Cek apakah file key ada
|
||||||
|
try {
|
||||||
|
const serviceAccount = require(serviceAccountPath);
|
||||||
|
admin.initializeApp({
|
||||||
|
credential: admin.credential.cert(serviceAccount),
|
||||||
|
databaseURL: process.env.FIREBASE_DATABASE_URL || 'https://your-project.firebaseio.com'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Service account key tidak ditemukan!');
|
||||||
|
console.error(' Pastikan file service-account-key.json ada di railway-worker/');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = admin.database();
|
||||||
|
|
||||||
|
// Fungsi untuk generate dummy data
|
||||||
|
function generateDummyData(startDate, endDate) {
|
||||||
|
const data = {};
|
||||||
|
|
||||||
|
let currentDate = new Date(startDate);
|
||||||
|
|
||||||
|
while (currentDate <= endDate) {
|
||||||
|
const dateKey = formatDate(currentDate);
|
||||||
|
const dateData = {};
|
||||||
|
|
||||||
|
// Generate data setiap 30 menit
|
||||||
|
for (let hour = 0; hour < 24; hour++) {
|
||||||
|
for (let minute = 0; minute < 60; minute += 30) {
|
||||||
|
const timeKey = `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
|
||||||
|
|
||||||
|
// Generate random values 40-80
|
||||||
|
const soil1 = 40 + Math.random() * 40;
|
||||||
|
const soil2 = 40 + Math.random() * 40;
|
||||||
|
const soil3 = 40 + Math.random() * 40;
|
||||||
|
const soil4 = 40 + Math.random() * 40;
|
||||||
|
const soil5 = 40 + Math.random() * 40;
|
||||||
|
|
||||||
|
dateData[timeKey] = {
|
||||||
|
soil_1: soil1.toFixed(1),
|
||||||
|
soil_2: soil2.toFixed(1),
|
||||||
|
soil_3: soil3.toFixed(1),
|
||||||
|
soil_4: soil4.toFixed(1),
|
||||||
|
soil_5: soil5.toFixed(1),
|
||||||
|
suhu: (20 + Math.random() * 15).toFixed(1), // 20-35°C
|
||||||
|
kelembapan: (40 + Math.random() * 40).toFixed(1), // 40-80%
|
||||||
|
ldr: Math.floor(300 + Math.random() * 700).toString(), // 300-1000
|
||||||
|
source: 'dummy-data',
|
||||||
|
timestamp: new Date(
|
||||||
|
currentDate.getFullYear(),
|
||||||
|
currentDate.getMonth(),
|
||||||
|
currentDate.getDate(),
|
||||||
|
hour,
|
||||||
|
minute
|
||||||
|
).getTime().toString(),
|
||||||
|
type: 'sensor_reading'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data[dateKey] = dateData;
|
||||||
|
currentDate.setDate(currentDate.getDate() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fungsi format date
|
||||||
|
function formatDate(date) {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main function
|
||||||
|
async function clearAndPopulateDummyData() {
|
||||||
|
try {
|
||||||
|
console.log('\n🔄 Mulai proses clear dan populate dummy data...\n');
|
||||||
|
|
||||||
|
// Step 1: Clear existing history data
|
||||||
|
console.log('⏳ Step 1: Menghapus data history yang sudah ada...');
|
||||||
|
const historyRef = db.ref('history');
|
||||||
|
|
||||||
|
const snapshot = await historyRef.once('value');
|
||||||
|
if (snapshot.exists()) {
|
||||||
|
await historyRef.remove();
|
||||||
|
console.log('✅ Data history berhasil dihapus!\n');
|
||||||
|
} else {
|
||||||
|
console.log('ℹ️ Tidak ada data history untuk dihapus\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Generate dummy data untuk 7 hari terakhir
|
||||||
|
console.log('⏳ Step 2: Generate dummy data untuk 7 hari terakhir...');
|
||||||
|
const endDate = new Date();
|
||||||
|
const startDate = new Date();
|
||||||
|
startDate.setDate(startDate.getDate() - 7);
|
||||||
|
|
||||||
|
const dummyData = generateDummyData(startDate, endDate);
|
||||||
|
const totalEntries = Object.values(dummyData).reduce(
|
||||||
|
(sum, dateData) => sum + Object.keys(dateData).length,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`✅ Generated ${totalEntries} data points untuk ${Object.keys(dummyData).length} hari\n`);
|
||||||
|
|
||||||
|
// Step 3: Upload dummy data ke Firebase
|
||||||
|
console.log('⏳ Step 3: Upload dummy data ke Firebase...');
|
||||||
|
await historyRef.set(dummyData);
|
||||||
|
console.log('✅ Dummy data berhasil diupload!\n');
|
||||||
|
|
||||||
|
// Step 4: Verify data
|
||||||
|
console.log('⏳ Step 4: Verifikasi data...');
|
||||||
|
const verifySnapshot = await historyRef.once('value');
|
||||||
|
const uploadedData = verifySnapshot.val();
|
||||||
|
const uploadedEntries = Object.values(uploadedData).reduce(
|
||||||
|
(sum, dateData) => sum + Object.keys(dateData).length,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`✅ Verifikasi: ${uploadedEntries} data points berhasil tersimpan\n`);
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
console.log('═══════════════════════════════════════════════════');
|
||||||
|
console.log('✨ SUMMARY');
|
||||||
|
console.log('═══════════════════════════════════════════════════');
|
||||||
|
console.log(`📅 Periode: ${formatDate(startDate)} hingga ${formatDate(endDate)}`);
|
||||||
|
console.log(`📊 Total data points: ${uploadedEntries}`);
|
||||||
|
console.log(`📈 Nilai range: 40-80`);
|
||||||
|
console.log(`🔄 Status: BERHASIL\n`);
|
||||||
|
|
||||||
|
console.log('🎯 Instruksi selanjutnya:');
|
||||||
|
console.log('1. Buka aplikasi di Flutter');
|
||||||
|
console.log('2. Buka halaman Histori');
|
||||||
|
console.log('3. Data dummy akan dimuat otomatis dari Firebase');
|
||||||
|
console.log('4. Grafik harus terlihat clean tanpa outliers\n');
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('\n❌ ERROR:', error.message);
|
||||||
|
console.error('\nDetail:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the script
|
||||||
|
clearAndPopulateDummyData();
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
{
|
||||||
|
"aktuator": {
|
||||||
|
"mosvet_1": false,
|
||||||
|
"mosvet_2": false,
|
||||||
|
"mosvet_3": false,
|
||||||
|
"mosvet_4": false,
|
||||||
|
"mosvet_5": false,
|
||||||
|
"mosvet_6": false,
|
||||||
|
"mosvet_7": false,
|
||||||
|
"mosvet_8": false
|
||||||
|
},
|
||||||
|
|
||||||
|
"data": {
|
||||||
|
"pot_1": {
|
||||||
|
"kelembaban": 50,
|
||||||
|
"suhu": 28,
|
||||||
|
"timestamp": 1708070400000
|
||||||
|
},
|
||||||
|
"pot_2": {
|
||||||
|
"kelembaban": 45,
|
||||||
|
"suhu": 27,
|
||||||
|
"timestamp": 1708070400000
|
||||||
|
},
|
||||||
|
"pot_3": {
|
||||||
|
"kelembaban": 52,
|
||||||
|
"suhu": 29,
|
||||||
|
"timestamp": 1708070400000
|
||||||
|
},
|
||||||
|
"pot_4": {
|
||||||
|
"kelembaban": 48,
|
||||||
|
"suhu": 28,
|
||||||
|
"timestamp": 1708070400000
|
||||||
|
},
|
||||||
|
"pot_5": {
|
||||||
|
"kelembaban": 51,
|
||||||
|
"suhu": 27,
|
||||||
|
"timestamp": 1708070400000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"history": {
|
||||||
|
"2026-02-16": {
|
||||||
|
"log_1": {
|
||||||
|
"timestamp": 1708070400000,
|
||||||
|
"type": "waktu_jadwal_1",
|
||||||
|
"pots": [1, 2, 3],
|
||||||
|
"durasi": 60,
|
||||||
|
"pompa_air": true,
|
||||||
|
"pompa_pupuk": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"kontrol": {
|
||||||
|
"waktu": true,
|
||||||
|
"sensor": false,
|
||||||
|
"otomatis": true,
|
||||||
|
|
||||||
|
"batas_atas": 32,
|
||||||
|
"batas_bawah": 11,
|
||||||
|
|
||||||
|
"mode_sensor": "smart",
|
||||||
|
"durasi_sensor": 600,
|
||||||
|
|
||||||
|
"jadwal_1": {
|
||||||
|
"aktif": true,
|
||||||
|
"waktu": "08:00",
|
||||||
|
"durasi": 60,
|
||||||
|
"pot_aktif": [1, 2, 3],
|
||||||
|
"pompa_air": true,
|
||||||
|
"pompa_pupuk": false
|
||||||
|
},
|
||||||
|
|
||||||
|
"jadwal_2": {
|
||||||
|
"aktif": true,
|
||||||
|
"waktu": "09:00",
|
||||||
|
"durasi": 45,
|
||||||
|
"pot_aktif": [4, 5],
|
||||||
|
"pompa_air": true,
|
||||||
|
"pompa_pupuk": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"jadwal_3": {
|
||||||
|
"aktif": true,
|
||||||
|
"waktu": "16:00",
|
||||||
|
"durasi": 30,
|
||||||
|
"pot_aktif": [1, 2, 3, 4, 5],
|
||||||
|
"pompa_air": true,
|
||||||
|
"pompa_pupuk": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"jadwal_4": {
|
||||||
|
"aktif": false,
|
||||||
|
"waktu": "12:00",
|
||||||
|
"durasi": 40,
|
||||||
|
"pot_aktif": [2, 4],
|
||||||
|
"pompa_air": true,
|
||||||
|
"pompa_pupuk": false
|
||||||
|
},
|
||||||
|
|
||||||
|
"jadwal_5": {
|
||||||
|
"aktif": false,
|
||||||
|
"waktu": "18:00",
|
||||||
|
"durasi": 50,
|
||||||
|
"pot_aktif": [1, 5],
|
||||||
|
"pompa_air": true,
|
||||||
|
"pompa_pupuk": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"flutter":{"platforms":{"android":{"default":{"projectId":"project-ta-951b4","appId":"1:217854138058:android:b8bd92665b1766ad0c4633","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"project-ta-951b4","configurations":{"android":"1:217854138058:android:b8bd92665b1766ad0c4633","ios":"1:217854138058:ios:aeee768efda8c5d80c4633","macos":"1:217854138058:ios:d9fb00752ef371fe0c4633","web":"1:217854138058:web:50a5bcd5a61ac1820c4633","windows":"1:217854138058:web:233d93e01272a0870c4633"}}}}}}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
**/dgph
|
||||||
|
*.mode1v3
|
||||||
|
*.mode2v3
|
||||||
|
*.moved-aside
|
||||||
|
*.pbxuser
|
||||||
|
*.perspectivev3
|
||||||
|
**/*sync/
|
||||||
|
.sconsign.dblite
|
||||||
|
.tags*
|
||||||
|
**/.vagrant/
|
||||||
|
**/DerivedData/
|
||||||
|
Icon?
|
||||||
|
**/Pods/
|
||||||
|
**/.symlinks/
|
||||||
|
profile
|
||||||
|
xcuserdata
|
||||||
|
**/.generated/
|
||||||
|
Flutter/App.framework
|
||||||
|
Flutter/Flutter.framework
|
||||||
|
Flutter/Flutter.podspec
|
||||||
|
Flutter/Generated.xcconfig
|
||||||
|
Flutter/ephemeral/
|
||||||
|
Flutter/app.flx
|
||||||
|
Flutter/app.zip
|
||||||
|
Flutter/flutter_assets/
|
||||||
|
Flutter/flutter_export_environment.sh
|
||||||
|
ServiceDefinitions.json
|
||||||
|
Runner/GeneratedPluginRegistrant.*
|
||||||
|
|
||||||
|
# Exceptions to above rules.
|
||||||
|
!default.mode1v3
|
||||||
|
!default.mode2v3
|
||||||
|
!default.pbxuser
|
||||||
|
!default.perspectivev3
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?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>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>App</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>io.flutter.flutter.app</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>App</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>FMWK</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>MinimumOSVersion</key>
|
||||||
|
<string>12.0</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
#include "Generated.xcconfig"
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
#include "Generated.xcconfig"
|
||||||
|
|
@ -0,0 +1,616 @@
|
||||||
|
// !$*UTF8*$!
|
||||||
|
{
|
||||||
|
archiveVersion = 1;
|
||||||
|
classes = {
|
||||||
|
};
|
||||||
|
objectVersion = 54;
|
||||||
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXBuildFile section */
|
||||||
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||||
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||||
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||||
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||||
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
||||||
|
remoteInfo = Runner;
|
||||||
|
};
|
||||||
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = "";
|
||||||
|
dstSubfolderSpec = 10;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
name = "Embed Frameworks";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXFileReference section */
|
||||||
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||||
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||||
|
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||||
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||||
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||||
|
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||||
|
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||||
|
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||||
|
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
|
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXGroup section */
|
||||||
|
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
331C807B294A618700263BE5 /* RunnerTests.swift */,
|
||||||
|
);
|
||||||
|
path = RunnerTests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||||
|
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||||
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||||
|
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||||
|
);
|
||||||
|
name = Flutter;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146E51CF9000F007C117D = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
9740EEB11CF90186004384FC /* Flutter */,
|
||||||
|
97C146F01CF9000F007C117D /* Runner */,
|
||||||
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
|
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||||
|
);
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146EF1CF9000F007C117D /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||||
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146F01CF9000F007C117D /* Runner */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||||
|
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||||
|
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||||
|
97C147021CF9000F007C117D /* Info.plist */,
|
||||||
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||||
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||||
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||||
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||||
|
);
|
||||||
|
path = Runner;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXNativeTarget section */
|
||||||
|
331C8080294A63A400263BE5 /* RunnerTests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||||
|
buildPhases = (
|
||||||
|
331C807D294A63A400263BE5 /* Sources */,
|
||||||
|
331C807F294A63A400263BE5 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
331C8086294A63A400263BE5 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
name = RunnerTests;
|
||||||
|
productName = RunnerTests;
|
||||||
|
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
|
};
|
||||||
|
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
|
buildPhases = (
|
||||||
|
9740EEB61CF901F6004384FC /* Run Script */,
|
||||||
|
97C146EA1CF9000F007C117D /* Sources */,
|
||||||
|
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||||
|
97C146EC1CF9000F007C117D /* Resources */,
|
||||||
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = Runner;
|
||||||
|
productName = Runner;
|
||||||
|
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXProject section */
|
||||||
|
97C146E61CF9000F007C117D /* Project object */ = {
|
||||||
|
isa = PBXProject;
|
||||||
|
attributes = {
|
||||||
|
BuildIndependentTargetsInParallel = YES;
|
||||||
|
LastUpgradeCheck = 1510;
|
||||||
|
ORGANIZATIONNAME = "";
|
||||||
|
TargetAttributes = {
|
||||||
|
331C8080294A63A400263BE5 = {
|
||||||
|
CreatedOnToolsVersion = 14.0;
|
||||||
|
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||||
|
};
|
||||||
|
97C146ED1CF9000F007C117D = {
|
||||||
|
CreatedOnToolsVersion = 7.3.1;
|
||||||
|
LastSwiftMigration = 1100;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||||
|
compatibilityVersion = "Xcode 9.3";
|
||||||
|
developmentRegion = en;
|
||||||
|
hasScannedForEncodings = 0;
|
||||||
|
knownRegions = (
|
||||||
|
en,
|
||||||
|
Base,
|
||||||
|
);
|
||||||
|
mainGroup = 97C146E51CF9000F007C117D;
|
||||||
|
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||||
|
projectDirPath = "";
|
||||||
|
projectRoot = "";
|
||||||
|
targets = (
|
||||||
|
97C146ED1CF9000F007C117D /* Runner */,
|
||||||
|
331C8080294A63A400263BE5 /* RunnerTests */,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
331C807F294A63A400263BE5 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
97C146EC1CF9000F007C117D /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||||
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||||
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||||
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||||
|
);
|
||||||
|
name = "Thin Binary";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||||
|
};
|
||||||
|
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
name = "Run Script";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||||
|
};
|
||||||
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
331C807D294A63A400263BE5 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||||
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||||
|
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
|
/* Begin PBXVariantGroup section */
|
||||||
|
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
97C146FB1CF9000F007C117D /* Base */,
|
||||||
|
);
|
||||||
|
name = Main.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
97C147001CF9000F007C117D /* Base */,
|
||||||
|
);
|
||||||
|
name = LaunchScreen.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
|
/* Begin XCBuildConfiguration section */
|
||||||
|
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
249021D4217E4FDB00AE95B9 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.apsgo;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
331C8088294A63A400263BE5 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.apsgo;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
331C8089294A63A400263BE5 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.apsgo;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
331C808A294A63A400263BE5 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.apsgo;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
97C147031CF9000F007C117D /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_TESTABILITY = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = 0;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"DEBUG=1",
|
||||||
|
"$(inherited)",
|
||||||
|
);
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
97C147041CF9000F007C117D /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
97C147061CF9000F007C117D /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.apsgo;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
97C147071CF9000F007C117D /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.apsgo;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
|
/* Begin XCConfigurationList section */
|
||||||
|
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
331C8088294A63A400263BE5 /* Debug */,
|
||||||
|
331C8089294A63A400263BE5 /* Release */,
|
||||||
|
331C808A294A63A400263BE5 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
97C147031CF9000F007C117D /* Debug */,
|
||||||
|
97C147041CF9000F007C117D /* Release */,
|
||||||
|
249021D3217E4FDB00AE95B9 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
97C147061CF9000F007C117D /* Debug */,
|
||||||
|
97C147071CF9000F007C117D /* Release */,
|
||||||
|
249021D4217E4FDB00AE95B9 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
/* End XCConfigurationList section */
|
||||||
|
};
|
||||||
|
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
|
}
|
||||||
7
railway-worker/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>PreviewsEnabled</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1510"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO"
|
||||||
|
parallelizable = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "331C8080294A63A400263BE5"
|
||||||
|
BuildableName = "RunnerTests.xctest"
|
||||||
|
BlueprintName = "RunnerTests"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
enableGPUValidationMode = "1"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Profile"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "group:Runner.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>PreviewsEnabled</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@main
|
||||||
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
|
override func application(
|
||||||
|
_ application: UIApplication,
|
||||||
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
|
) -> Bool {
|
||||||
|
GeneratedPluginRegistrant.register(with: self)
|
||||||
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||