Upload Tugas Akhir TKK
|
|
@ -1,5 +1,9 @@
|
|||
{
|
||||
"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 {
|
||||
namespace = "com.kampus.apsgo"
|
||||
namespace = "com.kampus.irrigo"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = "27.0.12077973"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
|
|
@ -22,10 +23,10 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
// 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.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = 23
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
|
|
@ -40,6 +41,10 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"project_info": {
|
||||
"project_number": "217854138058",
|
||||
"firebase_url": "https://project-ta-951b4-default-rtdb.firebaseio.com",
|
||||
"project_id": "project-ta-951b4",
|
||||
"storage_bucket": "project-ta-951b4.firebasestorage.app"
|
||||
},
|
||||
|
|
@ -9,10 +10,18 @@
|
|||
"client_info": {
|
||||
"mobilesdk_app_id": "1:217854138058:android:b8bd92665b1766ad0c4633",
|
||||
"android_client_info": {
|
||||
"package_name": "com.kampus.apsgo"
|
||||
"package_name": "com.kampus.irrigo"
|
||||
}
|
||||
},
|
||||
"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_type": 3
|
||||
|
|
@ -29,6 +38,13 @@
|
|||
{
|
||||
"client_id": "217854138058-ml03se7pied9l9k3jcb5cpgl2bn3rto0.apps.googleusercontent.com",
|
||||
"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">
|
||||
<!-- 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
|
||||
android:label="ApsGo"
|
||||
android:label="irrigo"
|
||||
android:name="${applicationName}"
|
||||
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
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
|
@ -25,6 +49,25 @@
|
|||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</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.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<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.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)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.apsgo;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.irrigo;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
|
@ -384,7 +384,7 @@
|
|||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.apsgo;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.irrigo;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
|
|
@ -401,7 +401,7 @@
|
|||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.apsgo;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.irrigo;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
|
|
@ -416,7 +416,7 @@
|
|||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.apsgo;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.irrigo;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
|
|
@ -547,7 +547,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.apsgo;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.irrigo;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
|
|
@ -569,7 +569,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.apsgo;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.kampus.irrigo;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>ApsGo</string>
|
||||
<string>IrriGo</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>ApsGo</string>
|
||||
<string>IrriGo</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ class DefaultFirebaseOptions {
|
|||
projectId: 'project-ta-951b4',
|
||||
storageBucket: 'project-ta-951b4.firebasestorage.app',
|
||||
databaseURL: 'https://project-ta-951b4-default-rtdb.firebaseio.com',
|
||||
iosBundleId: 'com.kampus.apsgo',
|
||||
iosBundleId: 'com.kampus.irrigo',
|
||||
);
|
||||
|
||||
static const FirebaseOptions web = FirebaseOptions(
|
||||
|
|
|
|||
|
|
@ -1,15 +1,23 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'firebase_options.dart';
|
||||
import 'screens/landing_page.dart';
|
||||
import 'screens/login_page.dart';
|
||||
import 'theme/app_color.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() {
|
||||
// Start history logging service as soon as app starts
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
|
|
@ -40,23 +48,30 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||
switch (state) {
|
||||
case AppLifecycleState.resumed:
|
||||
// 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;
|
||||
case AppLifecycleState.inactive:
|
||||
// App temporary inactive (misal: phone call)
|
||||
print('📱 App inactive');
|
||||
break;
|
||||
case AppLifecycleState.paused:
|
||||
// App di background, stop services untuk hemat battery
|
||||
print('📱 App paused - stopping background services');
|
||||
// App di background
|
||||
// ⚠️ 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();
|
||||
KontrolAutomationService().stopAll();
|
||||
// KontrolAutomationService().stopAll(); // ❌ REMOVED: Breaks scheduled notifications!
|
||||
break;
|
||||
case AppLifecycleState.detached:
|
||||
// App akan di-terminate
|
||||
print('📱 App detaching - cleanup services');
|
||||
HistoryLoggingService().dispose();
|
||||
KontrolAutomationService().dispose();
|
||||
// KontrolAutomationService().dispose(); // Keep running until truly terminated
|
||||
break;
|
||||
case AppLifecycleState.hidden:
|
||||
// App hidden (new in Flutter 3.13+)
|
||||
|
|
@ -69,7 +84,7 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'ApsGo',
|
||||
title: 'IrriGo',
|
||||
theme: ThemeData(
|
||||
primaryColor: AppColor.primary,
|
||||
scaffoldBackgroundColor: AppColor.background,
|
||||
|
|
@ -109,6 +124,15 @@ class _FirebaseInitializerState extends State<FirebaseInitializer> {
|
|||
});
|
||||
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
|
||||
final loggingService = HistoryLoggingService();
|
||||
loggingService.start();
|
||||
|
|
@ -121,6 +145,15 @@ class _FirebaseInitializerState extends State<FirebaseInitializer> {
|
|||
});
|
||||
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
|
||||
final loggingService = HistoryLoggingService();
|
||||
loggingService.start();
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import '../services/auth_service.dart';
|
|||
import '../services/firebase_database_service.dart';
|
||||
import 'kontrol_page.dart';
|
||||
import 'histori_page.dart';
|
||||
// import 'notification_debug_screen.dart';
|
||||
|
||||
class DashboardPage extends StatefulWidget {
|
||||
const DashboardPage({super.key});
|
||||
|
|
@ -14,6 +15,12 @@ class DashboardPage extends StatefulWidget {
|
|||
}
|
||||
|
||||
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;
|
||||
final _dbService = FirebaseDatabaseService();
|
||||
final _authService = AuthService();
|
||||
|
|
@ -170,7 +177,7 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||
),
|
||||
child: ClipOval(
|
||||
child: Image.asset(
|
||||
'assets/images/logo_apsgo.png',
|
||||
'assets/images/logo_irrigo.png',
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Icon(
|
||||
|
|
@ -184,7 +191,7 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'ApsGo',
|
||||
'IrriGo',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
|
|
@ -222,6 +229,26 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||
title: 'Histori',
|
||||
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(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.logout, color: Colors.red),
|
||||
|
|
@ -333,9 +360,11 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||
}
|
||||
|
||||
final data = snapshot.data!;
|
||||
final suhu = data['suhu'] ?? '0';
|
||||
final kelembapan = data['kelembapan'] ?? '0';
|
||||
final ldr = data['ldr'] ?? '0';
|
||||
// final suhu = data['suhu'] ?? '0';
|
||||
// final kelembapan = data['kelembapan'] ?? '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 soil1 = data['soil_1'] ?? '0';
|
||||
final soil2 = data['soil_2'] ?? '0';
|
||||
|
|
@ -376,7 +405,7 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||
_buildSensorCard(
|
||||
title: "Light",
|
||||
value: ldr,
|
||||
unit: "%",
|
||||
unit: "",
|
||||
icon: Icons.wb_sunny_outlined,
|
||||
color: Colors.amber,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -125,6 +125,11 @@ class _JadwalFormPageState extends State<JadwalFormPage> {
|
|||
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) {
|
||||
_showError('Durasi harus lebih dari 0');
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
|
@ -518,6 +545,14 @@ class _JadwalFormPageState extends State<JadwalFormPage> {
|
|||
),
|
||||
TextButton.icon(
|
||||
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(() {
|
||||
_potSelection = [false, false, false, false, false];
|
||||
});
|
||||
|
|
@ -536,6 +571,18 @@ class _JadwalFormPageState extends State<JadwalFormPage> {
|
|||
Widget _buildPotCheckbox(int potNumber, bool selected) {
|
||||
return InkWell(
|
||||
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(() {
|
||||
_potSelection[potNumber - 1] = !_potSelection[potNumber - 1];
|
||||
});
|
||||
|
|
@ -594,7 +641,17 @@ class _JadwalFormPageState extends State<JadwalFormPage> {
|
|||
children: [
|
||||
SwitchListTile(
|
||||
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'),
|
||||
subtitle: const Text('Mengalirkan air'),
|
||||
secondary: Icon(
|
||||
|
|
@ -606,7 +663,17 @@ class _JadwalFormPageState extends State<JadwalFormPage> {
|
|||
const Divider(),
|
||||
SwitchListTile(
|
||||
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'),
|
||||
subtitle: const Text('Mengalirkan larutan nutrisi cair'),
|
||||
secondary: Icon(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../theme/app_color.dart';
|
||||
import '../widgets/kontrol_widgets.dart';
|
||||
import 'pot_selection_page.dart';
|
||||
import 'jadwal_management_page.dart';
|
||||
import 'threshold_management_page.dart';
|
||||
import '../services/kontrol_storage.dart';
|
||||
|
|
@ -166,6 +165,15 @@ class _ManualControlPageState extends State<ManualControlPage> {
|
|||
}
|
||||
|
||||
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 {
|
||||
// Update Firebase with current state
|
||||
await _dbService.setMultipleAktuator({
|
||||
|
|
@ -186,6 +194,24 @@ class _ManualControlPageState extends State<ManualControlPage> {
|
|||
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
|
||||
setState(() {
|
||||
_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
|
||||
Widget build(BuildContext context) {
|
||||
if (_isLoading) {
|
||||
|
|
@ -237,6 +285,17 @@ class _ManualControlPageState extends State<ManualControlPage> {
|
|||
title: 'Pompa Air',
|
||||
isActive: _pompaAir,
|
||||
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(() {
|
||||
_pompaAir = !_pompaAir;
|
||||
_hasLocalChanges = true;
|
||||
|
|
@ -247,6 +306,17 @@ class _ManualControlPageState extends State<ManualControlPage> {
|
|||
title: 'Pompa Nutrisi',
|
||||
isActive: _pompaNutrisi,
|
||||
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(() {
|
||||
_pompaNutrisi = !_pompaNutrisi;
|
||||
_hasLocalChanges = true;
|
||||
|
|
@ -286,6 +356,18 @@ class _ManualControlPageState extends State<ManualControlPage> {
|
|||
title: 'POT ${index + 1}',
|
||||
isActive: _potStatus[index],
|
||||
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(() {
|
||||
_potStatus[index] = !_potStatus[index];
|
||||
_hasLocalChanges = true;
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ class _LandingPageState extends State<LandingPage> {
|
|||
),
|
||||
child: ClipOval(
|
||||
child: Image.asset(
|
||||
'assets/images/logo_apsgo.png',
|
||||
'assets/images/logo_irrigo.png',
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Icon(Icons.eco, size: 60, color: AppColor.primary);
|
||||
|
|
@ -93,7 +93,7 @@ class _LandingPageState extends State<LandingPage> {
|
|||
),
|
||||
const SizedBox(height: 40),
|
||||
Text(
|
||||
"ApsGo",
|
||||
"IrriGo",
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
@ -102,7 +102,7 @@ class _LandingPageState extends State<LandingPage> {
|
|||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
"Kelola Hidupmu\nDengan Lebih Mudah",
|
||||
"Siram Tanamanmu\nDengan Lebih Pintar",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
|
|
@ -113,7 +113,7 @@ class _LandingPageState extends State<LandingPage> {
|
|||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
"Semua yang kamu butuhkan dalam satu aplikasi",
|
||||
"Otomatisasi penyiraman untuk menjaga tanaman tetap segar dan sehat setiap hari",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
|
|
@ -121,7 +121,7 @@ class _LandingPageState extends State<LandingPage> {
|
|||
height: 1.4,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 60),
|
||||
const SizedBox(height: 40),
|
||||
// Tombol Next
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
|
|
@ -148,6 +148,8 @@ class _LandingPageState extends State<LandingPage> {
|
|||
),
|
||||
),
|
||||
const Spacer(),
|
||||
_buildCollaborationFooter(),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
@ -175,7 +177,7 @@ class _LandingPageState extends State<LandingPage> {
|
|||
),
|
||||
const SizedBox(height: 40),
|
||||
Text(
|
||||
"Siap Memulai?",
|
||||
"Siap Merawat Tanamanmu?",
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
@ -184,7 +186,7 @@ class _LandingPageState extends State<LandingPage> {
|
|||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
"Bergabunglah Dengan Kami\nDan Rasakan Kemudahannya",
|
||||
"Pantau kelembapan tanah dan atur jadwal penyiraman dalam satu genggaman",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
|
|
@ -195,7 +197,7 @@ class _LandingPageState extends State<LandingPage> {
|
|||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
"Daftar sekarang dan mulai perjalananmu",
|
||||
"Mulai otomatisasi sekarang untuk hasil panen terbaik",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
|
|
@ -203,7 +205,7 @@ class _LandingPageState extends State<LandingPage> {
|
|||
height: 1.4,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 60),
|
||||
const SizedBox(height: 40),
|
||||
// Tombol Get Started
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
|
|
@ -221,14 +223,112 @@ class _LandingPageState extends State<LandingPage> {
|
|||
),
|
||||
),
|
||||
child: const Text(
|
||||
"Get Started",
|
||||
"Mulai Dari Sekarang",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
|
|
@ -158,7 +131,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||
),
|
||||
child: ClipOval(
|
||||
child: Image.asset(
|
||||
'assets/images/logo_apsgo.png',
|
||||
'assets/images/logo_irrigo.png',
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Icon(Icons.eco, size: 50, color: AppColor.primary);
|
||||
|
|
@ -168,7 +141,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
"Sign In",
|
||||
"Login",
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
@ -230,47 +203,6 @@ class _LoginPageState extends State<LoginPage> {
|
|||
),
|
||||
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
|
||||
Center(
|
||||
child: TextButton(
|
||||
|
|
@ -282,11 +214,11 @@ class _LoginPageState extends State<LoginPage> {
|
|||
},
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
text: "Don't have an account? ",
|
||||
text: "Belum punya akun? ",
|
||||
style: TextStyle(color: Colors.grey[600]),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "Register",
|
||||
text: "Daftar",
|
||||
style: TextStyle(
|
||||
color: AppColor.primary,
|
||||
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/firebase_database_service.dart';
|
||||
import '../services/kontrol_automation_service.dart';
|
||||
import '../services/notification_service.dart';
|
||||
|
||||
class SensorConfigPage extends StatefulWidget {
|
||||
final String potName;
|
||||
|
|
@ -222,6 +223,11 @@ class _SensorConfigPageState extends State<SensorConfigPage> {
|
|||
setState(() => _isSensorModeActive = value);
|
||||
try {
|
||||
await _dbService.setOtomatis(value);
|
||||
await NotificationService.instance
|
||||
.showModeChangedNotification(
|
||||
modeLabel: 'otomatis berbasis sensor',
|
||||
enabled: value,
|
||||
);
|
||||
if (value) {
|
||||
_automationService.startSensorMode();
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -98,6 +98,11 @@ class _ThresholdFormPageState extends State<ThresholdFormPage> {
|
|||
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) {
|
||||
_showError('Batas bawah harus lebih kecil dari batas atas');
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
|
@ -337,25 +364,41 @@ class _ThresholdFormPageState extends State<ThresholdFormPage> {
|
|||
const SizedBox(height: 16),
|
||||
|
||||
// Smart Mode Toggle
|
||||
SwitchListTile(
|
||||
value: _smartMode,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_smartMode = value;
|
||||
});
|
||||
},
|
||||
title: Text(
|
||||
_smartMode ? 'Smart Mode' : 'Fixed Duration',
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
// SwitchListTile(
|
||||
// value: _smartMode,
|
||||
// onChanged: (value) {
|
||||
// setState(() {
|
||||
// _smartMode = value;
|
||||
// });
|
||||
// },
|
||||
// title: Text(
|
||||
// _smartMode ? 'Smart Mode' : 'Fixed Duration',
|
||||
// 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(
|
||||
_smartMode
|
||||
? 'Siram sampai kelembaban ${_batasAtas.toInt()}%'
|
||||
: 'Siram dengan durasi tetap',
|
||||
'Siram sampai kelembaban ${_batasAtas.toInt()}%',
|
||||
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)
|
||||
|
|
@ -440,6 +483,17 @@ class _ThresholdFormPageState extends State<ThresholdFormPage> {
|
|||
label: Text('Pot $potNumber'),
|
||||
selected: isSelected,
|
||||
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(() {
|
||||
_potSelection[index] = selected;
|
||||
});
|
||||
|
|
@ -472,6 +526,14 @@ class _ThresholdFormPageState extends State<ThresholdFormPage> {
|
|||
const SizedBox(width: 8),
|
||||
TextButton.icon(
|
||||
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(() {
|
||||
_potSelection = [false, false, false, false, false];
|
||||
});
|
||||
|
|
@ -515,6 +577,14 @@ class _ThresholdFormPageState extends State<ThresholdFormPage> {
|
|||
SwitchListTile(
|
||||
value: _pompaAir,
|
||||
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;
|
||||
});
|
||||
|
|
@ -528,6 +598,14 @@ class _ThresholdFormPageState extends State<ThresholdFormPage> {
|
|||
SwitchListTile(
|
||||
value: _pompaPupuk,
|
||||
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;
|
||||
});
|
||||
|
|
@ -539,14 +617,9 @@ class _ThresholdFormPageState extends State<ThresholdFormPage> {
|
|||
activeColor: AppColor.primary,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
|
||||
SwitchListTile(
|
||||
value: _pompaPengaduk,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_pompaPengaduk = value;
|
||||
});
|
||||
},
|
||||
value: false,
|
||||
onChanged: null, // <- tanpa ()
|
||||
title: const Text('Pompa Pengaduk'),
|
||||
subtitle: const Text(
|
||||
'Aktifkan pompa pengaduk untuk mengaduk larutan',
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import '../widgets/kontrol_widgets.dart';
|
|||
import '../services/kontrol_storage.dart';
|
||||
import '../services/firebase_database_service.dart';
|
||||
import '../services/kontrol_automation_service.dart';
|
||||
import '../services/notification_service.dart';
|
||||
|
||||
class WaktuConfigPage extends StatefulWidget {
|
||||
final String potName;
|
||||
|
|
@ -242,6 +243,11 @@ class _WaktuConfigPageState extends State<WaktuConfigPage> {
|
|||
await _dbService.updateKontrolConfig({
|
||||
'waktu': value,
|
||||
});
|
||||
await NotificationService.instance
|
||||
.showModeChangedNotification(
|
||||
modeLabel: 'berdasarkan jadwal',
|
||||
enabled: value,
|
||||
);
|
||||
if (value) {
|
||||
_automationService.startWaktuMode();
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import 'package:firebase_database/firebase_database.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
|
||||
class FirebaseDatabaseService {
|
||||
static final FirebaseDatabaseService _instance =
|
||||
|
|
@ -139,7 +141,8 @@ class FirebaseDatabaseService {
|
|||
|
||||
/// Get real-time stream of kontrol configuration
|
||||
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) {
|
||||
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 {
|
||||
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();
|
||||
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);
|
||||
}
|
||||
|
||||
print('[WARN] Kontrol config not found in both /kontrol_1 and /kontrol');
|
||||
} catch (e) {
|
||||
print('Error getting kontrol config: $e');
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/// Update kontrol configuration
|
||||
/// Update kontrol configuration (writes to kontrol_1)
|
||||
Future<void> updateKontrolConfig(Map<String, dynamic> config) async {
|
||||
try {
|
||||
print('🔥 [DEBUG] Updating Firebase kontrol config: $config');
|
||||
await _database.child('kontrol').update(config);
|
||||
print('🔥 [DEBUG] Updating Firebase kontrol config (kontrol_1): $config');
|
||||
await _database.child('kontrol_1').update(config);
|
||||
print('✅ [DEBUG] Firebase kontrol config updated successfully');
|
||||
} catch (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 {
|
||||
try {
|
||||
final updates = <String, dynamic>{};
|
||||
if (batasAtas != null) updates['batas_atas'] = batasAtas;
|
||||
if (batasBawah != null) updates['batas_bawah'] = batasBawah;
|
||||
await _database.child('kontrol').update(updates);
|
||||
await _database.child('kontrol_1').update(updates);
|
||||
} catch (e) {
|
||||
print('Error setting threshold: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set waktu penyiraman
|
||||
/// Set waktu penyiraman - writes to kontrol_1
|
||||
Future<void> setWaktuPenyiraman({
|
||||
String? waktu1,
|
||||
String? waktu2,
|
||||
|
|
@ -196,30 +210,30 @@ class FirebaseDatabaseService {
|
|||
if (waktu1 != null) updates['waktu_1'] = waktu1;
|
||||
if (waktu2 != null) updates['waktu_2'] = waktu2;
|
||||
if (waktuEnabled != null) updates['waktu'] = waktuEnabled;
|
||||
await _database.child('kontrol').update(updates);
|
||||
await _database.child('kontrol_1').update(updates);
|
||||
} catch (e) {
|
||||
print('Error setting waktu: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set durasi penyiraman
|
||||
/// Set durasi penyiraman - writes to kontrol_1
|
||||
Future<void> setDurasi({int? durasi1, int? durasi2}) async {
|
||||
try {
|
||||
final updates = <String, dynamic>{};
|
||||
if (durasi1 != null) updates['durasi_1'] = durasi1;
|
||||
if (durasi2 != null) updates['durasi_2'] = durasi2;
|
||||
await _database.child('kontrol').update(updates);
|
||||
await _database.child('kontrol_1').update(updates);
|
||||
} catch (e) {
|
||||
print('Error setting durasi: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle mode otomatis
|
||||
/// Toggle mode otomatis - writes to kontrol_1
|
||||
Future<void> setOtomatis(bool value) async {
|
||||
try {
|
||||
await _database.child('kontrol/otomatis').set(value);
|
||||
await _database.child('kontrol_1/otomatis').set(value);
|
||||
} catch (e) {
|
||||
print('Error setting otomatis: $e');
|
||||
rethrow;
|
||||
|
|
@ -248,54 +262,143 @@ class FirebaseDatabaseService {
|
|||
|
||||
// ==================== HISTORY LOGGING ====================
|
||||
|
||||
/// Save sensor data snapshot to history
|
||||
/// Save sensor data snapshot to history (monitoring)
|
||||
Future<void> saveHistory(Map<String, dynamic> sensorData) async {
|
||||
try {
|
||||
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({
|
||||
...sensorData,
|
||||
'timestamp': now.millisecondsSinceEpoch,
|
||||
});
|
||||
// Simpan per-pot menggunakan push() dengan jenis_histori: monitoring
|
||||
for (int pot = 1; pot <= 5; pot++) {
|
||||
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) {
|
||||
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(
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
) async {
|
||||
final result = <String, dynamic>{};
|
||||
|
||||
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
|
||||
for (
|
||||
var date = startDate;
|
||||
date.isBefore(endDate.add(const Duration(days: 1)));
|
||||
date = date.add(const Duration(days: 1))
|
||||
) {
|
||||
final dateKey =
|
||||
'${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
final tsSnapshot =
|
||||
await _database
|
||||
.child('history')
|
||||
.orderByChild('timestamp')
|
||||
.startAt(startTs)
|
||||
.endAt(endTs)
|
||||
.get();
|
||||
|
||||
final snapshot = await _database.child('history/$dateKey').get();
|
||||
if (snapshot.exists) {
|
||||
allHistory[dateKey] = snapshot.value;
|
||||
}
|
||||
if (tsSnapshot.exists && tsSnapshot.value != null) {
|
||||
final raw = Map<String, dynamic>.from(tsSnapshot.value as Map);
|
||||
result.addAll(raw);
|
||||
debugPrint('[History] Push-key entries fetched: ${raw.length}');
|
||||
}
|
||||
|
||||
return allHistory;
|
||||
} catch (e) {
|
||||
print('Error getting history: $e');
|
||||
return {};
|
||||
debugPrint('Error fetching push-key history: $e');
|
||||
}
|
||||
|
||||
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)
|
||||
Future<Map<String, dynamic>> getLatestHistory({int limit = 100}) async {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -23,27 +23,9 @@ class HistoryLoggingService {
|
|||
|
||||
/// Start history logging service
|
||||
void start({Duration? interval}) {
|
||||
if (_isActive) {
|
||||
debugPrint('📊 History logging already active');
|
||||
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();
|
||||
});
|
||||
// Disabled on mobile app to run exclusively on Railway/Worker
|
||||
debugPrint('📊 History logging is disabled on mobile app (runs on Railway worker instead)');
|
||||
return;
|
||||
}
|
||||
|
||||
/// Stop history logging service
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:firebase_database/firebase_database.dart';
|
||||
import '../models/jadwal_model.dart';
|
||||
import 'notification_service.dart';
|
||||
|
||||
/// Service untuk manage Jadwal di Firebase
|
||||
class JadwalService {
|
||||
|
|
@ -64,6 +65,9 @@ class JadwalService {
|
|||
Future<bool> saveJadwal(JadwalModel jadwal) async {
|
||||
try {
|
||||
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');
|
||||
return true;
|
||||
} catch (e) {
|
||||
|
|
@ -76,6 +80,9 @@ class JadwalService {
|
|||
Future<bool> deleteJadwal(String jadwalId) async {
|
||||
try {
|
||||
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');
|
||||
return true;
|
||||
} catch (e) {
|
||||
|
|
@ -88,6 +95,9 @@ class JadwalService {
|
|||
Future<bool> toggleJadwalAktif(String jadwalId, bool aktif) async {
|
||||
try {
|
||||
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"}');
|
||||
return true;
|
||||
} catch (e) {
|
||||
|
|
@ -135,7 +145,22 @@ class JadwalService {
|
|||
Future<bool> setWaktuModeStatus(bool enabled) async {
|
||||
try {
|
||||
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"}');
|
||||
await NotificationService.instance.showModeChangedNotification(
|
||||
modeLabel: 'berdasarkan jadwal',
|
||||
enabled: enabled,
|
||||
);
|
||||
return true;
|
||||
} catch (e) {
|
||||
print('❌ Error setting waktu mode: $e');
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart';
|
|||
import 'firebase_database_service.dart';
|
||||
import 'automation_constants.dart';
|
||||
import 'connection_monitor_service.dart';
|
||||
import 'notification_service.dart';
|
||||
|
||||
/// Service untuk menghandle logika otomatis kontrol waktu dan sensor
|
||||
/// Berjalan di background untuk monitoring dan eksekusi otomatis
|
||||
|
|
@ -31,24 +32,29 @@ class KontrolAutomationService {
|
|||
// ==================== KONTROL WAKTU ====================
|
||||
|
||||
/// 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() {
|
||||
// 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;
|
||||
|
||||
// Start connection monitor
|
||||
// Start connection monitor for sensor fallback
|
||||
_connectionMonitor.start();
|
||||
|
||||
_isWaktuModeActive = true;
|
||||
debugPrint('🕐 Waktu Mode: Started');
|
||||
debugPrint('🕐 Waktu Mode: Started (using exact alarm scheduling)');
|
||||
|
||||
// Cek setiap 30 detik (gunakan constant)
|
||||
_waktuCheckTimer = Timer.periodic(
|
||||
Duration(seconds: AutomationConstants.waktuCheckInterval),
|
||||
(timer) => _checkScheduledWatering(),
|
||||
// Setup callback to handle watering execution when alarm fires
|
||||
NotificationService.setOnWateringScheduleCallback(
|
||||
_handleWateringScheduleExecution,
|
||||
);
|
||||
|
||||
// Jalankan check pertama kali
|
||||
_checkScheduledWatering();
|
||||
// Schedule all watering executions using exact alarm
|
||||
// This will fire even when app is minimized/killed
|
||||
_scheduleWateringExecutions();
|
||||
}
|
||||
|
||||
/// Stop monitoring waktu mode
|
||||
|
|
@ -56,84 +62,72 @@ class KontrolAutomationService {
|
|||
_waktuCheckTimer?.cancel();
|
||||
_waktuCheckTimer = null;
|
||||
_isWaktuModeActive = false;
|
||||
|
||||
// Cancel all watering schedule executions
|
||||
_cancelWateringSchedules();
|
||||
|
||||
// Clear callback
|
||||
NotificationService.setOnWateringScheduleCallback(null);
|
||||
|
||||
debugPrint('🕐 Waktu Mode: Stopped');
|
||||
}
|
||||
|
||||
/// Check apakah ada jadwal penyiraman yang harus dijalankan
|
||||
Future<void> _checkScheduledWatering() async {
|
||||
/// Schedule all watering executions from Firebase
|
||||
/// Called when waktu mode starts or when jadwal changes
|
||||
Future<void> _scheduleWateringExecutions() async {
|
||||
try {
|
||||
// Check connection first
|
||||
if (!_connectionMonitor.isConnected) {
|
||||
debugPrint('⚠️ No Firebase connection, skipping schedule check');
|
||||
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,
|
||||
);
|
||||
}
|
||||
await NotificationService.instance.scheduleAllWateringExecutions();
|
||||
debugPrint(
|
||||
'✅ All watering executions scheduled via notification service',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error checking scheduled watering: $e');
|
||||
debugPrint('❌ Error scheduling watering executions: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Check apakah jadwal harus dijalankan
|
||||
bool _shouldStartWatering(
|
||||
String scheduleId,
|
||||
String currentTime,
|
||||
String targetTime,
|
||||
) {
|
||||
// Cek apakah waktu sekarang sama dengan target
|
||||
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;
|
||||
/// Cancel all watering schedule executions
|
||||
Future<void> _cancelWateringSchedules() async {
|
||||
try {
|
||||
await NotificationService.instance.cancelAllWateringSchedules();
|
||||
debugPrint('✅ All watering schedule executions cancelled');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error cancelling watering schedules: $e');
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -171,6 +165,26 @@ class KontrolAutomationService {
|
|||
await _dbService.setMultipleAktuator(offUpdates);
|
||||
|
||||
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;
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error executing watering: $e');
|
||||
|
|
@ -182,6 +196,10 @@ class KontrolAutomationService {
|
|||
|
||||
/// Start monitoring sensor mode
|
||||
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;
|
||||
|
||||
// Start connection monitor
|
||||
|
|
@ -390,6 +408,24 @@ class KontrolAutomationService {
|
|||
'✅ 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
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
_isWateringActive[potKey] = false;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:firebase_database/firebase_database.dart';
|
||||
import '../models/threshold_model.dart';
|
||||
import 'notification_service.dart';
|
||||
|
||||
class ThresholdService {
|
||||
static const String kontrolPath = 'kontrol_1';
|
||||
|
|
@ -232,6 +233,10 @@ class ThresholdService {
|
|||
try {
|
||||
await _dbRef.child(kontrolPath).child('otomatis').set(enabled);
|
||||
print('✅ Sensor mode set to ${enabled ? "enabled" : "disabled"}');
|
||||
await NotificationService.instance.showModeChangedNotification(
|
||||
modeLabel: 'otomatis berbasis sensor',
|
||||
enabled: enabled,
|
||||
);
|
||||
return true;
|
||||
} catch (e) {
|
||||
print('❌ Error setting sensor mode: $e');
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import Foundation
|
|||
import firebase_auth
|
||||
import firebase_core
|
||||
import firebase_database
|
||||
import firebase_messaging
|
||||
import flutter_local_notifications
|
||||
import google_sign_in_ios
|
||||
import shared_preferences_foundation
|
||||
|
||||
|
|
@ -15,6 +17,8 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
|
||||
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||
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"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
}
|
||||
|
|
|
|||
156
pubspec.lock
|
|
@ -13,10 +13,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
|
||||
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.7"
|
||||
version: "4.0.9"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -45,10 +45,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
version: "1.4.1"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -81,14 +81,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -97,22 +89,38 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
|
||||
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
version: "1.3.3"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c
|
||||
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
version: "2.2.0"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -157,10 +165,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_platform_interface
|
||||
sha256: cccb4f572325dc14904c02fcc7db6323ad62ba02536833dddb5c02cac7341c64
|
||||
sha256: "0ecda14c1bfc9ed8cac303dd0f8d04a320811b479362a9a4efb14fd331a473ce"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.2"
|
||||
version: "6.0.3"
|
||||
firebase_core_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -193,6 +201,38 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
|
|
@ -214,6 +254,30 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
|
|
@ -292,10 +356,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c"
|
||||
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.7.2"
|
||||
version: "4.8.0"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -308,26 +372,26 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
||||
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.8"
|
||||
version: "11.0.2"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
||||
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.9"
|
||||
version: "3.0.10"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
version: "3.0.2"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -340,34 +404,34 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: logger
|
||||
sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3
|
||||
sha256: "25aee487596a6257655a1e091ec2ae66bc30e7af663592cc3a27e6591e05035c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.2"
|
||||
version: "2.7.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.17"
|
||||
version: "0.12.19"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.11.1"
|
||||
version: "0.13.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
version: "1.18.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -428,10 +492,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: posix
|
||||
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
|
||||
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.3"
|
||||
version: "6.5.0"
|
||||
rename:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -545,10 +609,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||
url: "https://pub.dev"
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -558,13 +630,13 @@ packages:
|
|||
source: hosted
|
||||
version: "1.4.0"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
version: "2.2.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -606,5 +678,5 @@ packages:
|
|||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.7.2 <4.0.0"
|
||||
dart: ">=3.10.0-0 <4.0.0"
|
||||
flutter: ">=3.29.0"
|
||||
|
|
|
|||
|
|
@ -40,6 +40,11 @@ dependencies:
|
|||
firebase_database: ^11.1.8
|
||||
google_sign_in: ^6.2.1
|
||||
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:
|
||||
flutter_test:
|
||||
|
|
@ -74,10 +79,10 @@ flutter:
|
|||
flutter_launcher_icons:
|
||||
android: true
|
||||
ios: false
|
||||
image_path: "assets/images/logo_apsgo.png"
|
||||
image_path: "assets/images/logo_irrigo.png"
|
||||
# Uncomment jika ingin adaptive icon (Android 8.0+)
|
||||
# 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
|
||||
# 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
|
||||
node_modules/
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.pyc
|
||||
*.swp
|
||||
*.swo
|
||||
.DS_Store
|
||||
.atom/
|
||||
.build/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
# Firebase service account
|
||||
serviceAccount.json
|
||||
firebase-key.json
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.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)
|
||||
- ✅ **Sensor Mode**: Otomasi berdasarkan threshold kelembapan tanah
|
||||
- ✅ **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)
|
||||
✅ **Sistem berjalan real-time dengan delay 20 detik/section**
|
||||
⚠️ **Catatan:** 1 waktu penjadwalan digunakan untuk banyak pompa sekaligus
|
||||
|
||||
## Tech Stack
|
||||
## Komponen Sistem
|
||||
|
||||
- **Node.js**: Runtime environment
|
||||
- **Firebase Admin SDK**: Realtime Database integration
|
||||
- **BullMQ**: Robust job queue dengan Redis
|
||||
- **Redis**: In-memory database untuk queue dan caching
|
||||
- **Cron**: Scheduled tasks
|
||||
### 1. Flutter Mobile App
|
||||
- Kontrol manual aktuator (pompa & mosvet)
|
||||
- Penjadwalan otomatis (waktu mode)
|
||||
- Monitoring sensor real-time
|
||||
- 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
|
||||
flutter pub get
|
||||
flutter run
|
||||
```
|
||||
|
||||
### Railway Worker
|
||||
```bash
|
||||
cd railway-worker
|
||||
npm install
|
||||
node worker.js
|
||||
```
|
||||
|
||||
2. Copy `.env.example` ke `.env` dan isi dengan credentials Firebase Anda:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
## Dokumentasi
|
||||
|
||||
3. Setup Redis lokal (gunakan Docker):
|
||||
```bash
|
||||
docker run -d -p 6379:6379 redis:latest
|
||||
```
|
||||
|
||||
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
|
||||
- [Railway Setup Guide](RAILWAY_QUICK_START.md)
|
||||
- [Deployment Guide](DEPLOYMENT_GUIDE.md)
|
||||
- [Debug & Testing Guide](DEBUG_TESTING_GUIDE.md)
|
||||
- [Troubleshooting](RAILWAY_TROUBLESHOOTING.md)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||