TKK_E32220671/lib/main.dart

784 lines
27 KiB
Dart
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'history_page.dart';
import 'background_service.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Inisialisasi Firebase hanya jika belum ada instance
if (Firebase.apps.isEmpty) {
try {
await Firebase.initializeApp(
options: const FirebaseOptions(
apiKey: 'AIzaSyBoYp4GpkwF-aMPHVW1gs7PmOj5ucF4xZs',
appId: '1:289397695071:android:f6e034034faf366003b1ea',
messagingSenderId: '289397695071',
projectId: 'monitoring-hujan',
databaseURL: 'https://monitoring-hujan-default-rtdb.firebaseio.com',
),
);
print('✅ Firebase berhasil diinisialisasi di main');
} catch (e) {
print('❌ Error inisialisasi Firebase di main: $e');
}
} else {
print(' Firebase sudah diinisialisasi sebelumnya');
}
// Inisialisasi background service
try {
await initializeService();
print('✅ Background service berhasil diinisialisasi');
} catch (e) {
print('❌ Error inisialisasi background service: $e');
}
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Monitoring Kolam',
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('id', 'ID'),
Locale('en', 'US'),
],
locale: const Locale('id', 'ID'),
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF2196F3),
primary: const Color(0xFF2196F3),
secondary: const Color(0xFF03A9F4),
),
useMaterial3: true,
),
home: const MainPage(),
);
}
}
class MainPage extends StatefulWidget {
const MainPage({super.key});
@override
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
int _currentIndex = 0;
late List<Widget> _pages;
Timer? _saveTimer;
Timer? _countdownTimer;
int _remainingSeconds = 60;
Duration _saveInterval = const Duration(minutes: 1);
@override
void initState() {
super.initState();
_initializeApp();
_startSaveTimer();
_startCountdownTimer();
}
Future<void> _initializeApp() async {
_pages = [
MonitoringPage(
key: UniqueKey(),
onDataUpdate: (s, k, c, ka) => _updateSensorData(s, k, c, ka),
onIntervalChange: _changeSaveInterval,
currentInterval: _saveInterval,
),
const HistoryPage(),
];
setState(() {});
}
void _changeSaveInterval(Duration newInterval) {
print(
'🔄 Mengubah interval dari ${_saveInterval.inSeconds}s ke ${newInterval.inSeconds}s');
setState(() {
_saveInterval = newInterval;
_startSaveTimer();
});
print('✅ Interval berhasil diubah');
}
void _updateSensorData(
double suhu, double kelembaban, double cahaya, double rainVal) {
// No need to update state here as _saveToFirestore is handled by background service
}
void _startSaveTimer() {
_saveTimer?.cancel();
_remainingSeconds = _saveInterval.inSeconds;
print('⏰ Timer dimulai dengan interval: ${_saveInterval.inSeconds} detik');
_saveTimer = Timer.periodic(_saveInterval, (timer) async {
print('⏰ Timer terpanggil - mengirim data ke Firestore...');
await _saveFromRealtimeToFirestore();
setState(() {
_remainingSeconds = _saveInterval.inSeconds;
});
print('⏰ Timer selesai - menunggu interval berikutnya');
});
}
void _startCountdownTimer() {
_countdownTimer?.cancel();
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (!mounted) return;
setState(() {
if (_remainingSeconds > 0) {
_remainingSeconds--;
} else {
_remainingSeconds = _saveInterval.inSeconds;
}
});
});
}
Future<void> _saveFromRealtimeToFirestore() async {
try {
print('🔄 Memulai pengiriman data ke Firestore...');
final dbRef = FirebaseDatabase.instance.ref();
final snapshot = await dbRef.get();
if (snapshot.value != null) {
final data = snapshot.value as Map<dynamic, dynamic>;
final suhu = double.parse(data['temperature']?.toString() ?? '0');
final kelembaban = double.parse(data['humidity']?.toString() ?? '0');
final cahaya = double.parse(data['ldr']?.toString() ?? '0');
final rainValue = double.parse(data['rain']?.toString() ?? '0');
final statusHujan = data['statusHujan']?.toString() ?? '';
final statusCahaya = data['statusCahaya']?.toString() ?? '';
print(
'📊 Data sensor: Suhu=$suhu, Kelembaban=$kelembaban, Cahaya=$cahaya, Hujan=$rainValue');
final docRef =
await FirebaseFirestore.instance.collection('sensor_history').add({
'temperature': suhu,
'humidity': kelembaban,
'light': cahaya,
'rain': rainValue,
'rain_status': statusHujan,
'weather_status': statusCahaya,
'timestamp': FieldValue.serverTimestamp(),
});
print('✅ Data berhasil dikirim ke Firestore dengan ID: ${docRef.id}');
} else {
print('⚠️ Tidak ada data dari Realtime Database');
}
} catch (e) {
print('❌ Error auto save dari RTDB ke Firestore: $e');
print('🔍 Detail error: ${e.toString()}');
}
}
@override
void dispose() {
_saveTimer?.cancel();
_countdownTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final minutes = (_remainingSeconds ~/ 60).toString().padLeft(2, '0');
final seconds = (_remainingSeconds % 60).toString().padLeft(2, '0');
return Scaffold(
body: Stack(
children: [
if (_pages.isNotEmpty) _pages[_currentIndex],
if (_currentIndex == 0)
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
borderRadius:
const BorderRadius.vertical(top: Radius.circular(16)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.timer, color: Colors.blue),
const SizedBox(width: 8),
Text(
'Next save in: $minutes:$seconds',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
],
),
),
),
],
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (idx) => setState(() => _currentIndex = idx),
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.monitor_heart),
label: 'Monitoring',
),
BottomNavigationBarItem(
icon: Icon(Icons.history),
label: 'Riwayat',
),
],
),
);
}
}
class MonitoringPage extends StatefulWidget {
final Function(double, double, double, double) onDataUpdate;
final Function(Duration) onIntervalChange;
final Duration currentInterval;
const MonitoringPage(
{Key? key,
required this.onDataUpdate,
required this.onIntervalChange,
required this.currentInterval})
: super(key: key);
@override
State<MonitoringPage> createState() => _MonitoringPageState();
}
class _MonitoringPageState extends State<MonitoringPage> {
final DatabaseReference _database = FirebaseDatabase.instance.ref();
double suhu = 30.0;
double kelembaban = 70.0;
double cahaya = 800.0;
double _rainVal = 0.0;
String cuaca = 'Cerah';
bool isLoading = true;
String? error;
// --- Servo & Mode State ---
bool _modeManual = true;
bool _kontrolServo = true;
bool _servo = true;
bool _isUpdatingServo = false;
// --- Firebase Get Function ---
Future<void> firebaseGet() async {
try {
final snapshot = await _database.get();
if (snapshot.value != null) {
final data = snapshot.value as Map<dynamic, dynamic>;
setState(() {
_modeManual = data['modeManual'] ?? true;
_kontrolServo = data['kontrolServo'] ?? true;
_servo = data['servo'] ?? true;
});
}
} catch (e) {
debugPrint('firebaseGet error: $e');
}
}
// --- Update Servo in Firebase ---
Future<void> updateServo(bool value) async {
setState(() => _isUpdatingServo = true);
try {
await _database.child('servo').set(value);
setState(() => _servo = value);
} catch (e) {
debugPrint('updateServo error: $e');
}
setState(() => _isUpdatingServo = false);
}
// --- Update Mode in Firebase ---
Future<void> updateModeManual(bool value) async {
try {
await _database.child('modeManual').set(value);
setState(() => _modeManual = value);
} catch (e) {
debugPrint('updateModeManual error: $e');
}
}
// --- Update KontrolServo in Firebase ---
Future<void> updateKontrolServo(bool value) async {
try {
await _database.child('kontrolServo').set(value);
setState(() => _kontrolServo = value);
} catch (e) {
debugPrint('updateKontrolServo error: $e');
}
}
double _calculateRainPercent(double kadarAir) {
// Menghitung persentase hujan dari nilai kadar air
return ((4095 - (kadarAir / 100 * 4095)) / 4095) * 100;
}
@override
void initState() {
super.initState();
firebaseGet();
_setupRealtimeUpdates();
}
String _updateStatusCuaca(double cahaya) {
// Status cuaca berdasarkan nilai LDR
return (cahaya < 700) ? 'Terang' : 'Gelap';
}
Color _getWeatherStatusColor(String status) {
// Warna untuk setiap status cuaca
switch (status) {
case 'Terang':
return Colors.orange;
case 'Gelap':
return Colors.grey.shade800;
default:
return Colors.grey;
}
}
void _setupRealtimeUpdates() {
_database.onValue.listen((event) async {
if (!mounted) return;
if (event.snapshot.value != null) {
final data = event.snapshot.value as Map<dynamic, dynamic>;
setState(() {
isLoading = false;
suhu = double.parse(data['temperature']?.toString() ?? '30.0');
kelembaban = double.parse(data['humidity']?.toString() ?? '70.0');
cahaya = double.parse(data['ldr']?.toString() ?? '800.0');
_rainVal = double.parse(data['rain']?.toString() ?? '0');
_modeManual = data['modeManual'] ?? true;
_kontrolServo = data['kontrolServo'] ?? true;
_servo = data['servo'] ?? true;
});
// --- Servo Logic ---
if (_modeManual) {
// Manual: servo follows kontrolServo
if (_servo != _kontrolServo) {
await updateServo(_kontrolServo);
}
} else {
// Otomatis: servo reacts to rain
// Konsisten dengan _getStatusHujan: < 500 = Hujan
bool autoServo = _rainVal < 500; // Tertutup jika hujan (< 500)
if (_servo != autoServo) {
await updateServo(autoServo);
}
}
} else {
setState(() {
isLoading = false;
error = 'Tidak ada data';
});
}
}, onError: (e) {
if (mounted) {
setState(() {
error = 'Error: $e';
isLoading = false;
});
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Row(
children: [
Text('Monitoring Penjemuran Ikan',
style: TextStyle(
color: Colors.white, fontWeight: FontWeight.bold)),
],
),
backgroundColor: Theme.of(context).colorScheme.primary,
actions: [
PopupMenuButton<Duration>(
icon: const Icon(Icons.timer, color: Colors.white),
initialValue: widget.currentInterval,
onSelected: (value) {
widget.onIntervalChange(value);
},
itemBuilder: (context) => [
const PopupMenuItem(
value: Duration(seconds: 30),
child: Text('30 detik'),
),
const PopupMenuItem(
value: Duration(minutes: 1),
child: Text('1 menit'),
),
const PopupMenuItem(
value: Duration(minutes: 5),
child: Text('5 menit'),
),
const PopupMenuItem(
value: Duration(minutes: 10),
child: Text('10 menit'),
),
const PopupMenuItem(
value: Duration(minutes: 30),
child: Text('30 menit'),
),
const PopupMenuItem(
value: Duration(hours: 1),
child: Text('1 jam'),
),
],
),
],
),
body: isLoading
? const Center(child: CircularProgressIndicator())
: error != null
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
color: Colors.red,
size: 60,
),
const SizedBox(height: 16),
Text(
error!,
style: const TextStyle(color: Colors.red),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _setupRealtimeUpdates,
child: const Text('Coba Lagi'),
),
],
),
)
: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.blue.shade50, Colors.white],
),
),
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// --- Info Cards ---
_buildInfoCard(
'Suhu Udara',
'${suhu.toStringAsFixed(1)}°C',
Colors.red,
Icons.thermostat,
_getStatusSuhu(suhu),
),
const SizedBox(height: 16),
_buildInfoCard(
'Kelembaban',
'${kelembaban.toStringAsFixed(1)}%',
Colors.blue,
Icons.water_drop,
_getStatusKelembaban(kelembaban),
),
const SizedBox(height: 16),
_buildInfoCard(
'Intensitas Cahaya',
'${cahaya.toStringAsFixed(0)}',
Colors.orange,
Icons.wb_sunny,
_getStatusCuaca(cahaya),
),
const SizedBox(height: 16),
_buildInfoCard(
'Curah Hujan',
'${_rainVal.toStringAsFixed(0)}',
Colors.brown,
Icons.water_damage,
_getStatusHujan(_rainVal),
),
const SizedBox(height: 32),
// --- Manual/Otomatis Control (moved to bottom, improved UI) ---
Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
margin: const EdgeInsets.only(top: 8, bottom: 16),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.settings_remote,
color: Theme.of(context)
.colorScheme
.primary),
const SizedBox(width: 10),
const Text(
'Kontrol Servo & Mode',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
],
),
const SizedBox(height: 16),
Row(
children: [
const Text('Manual',
style: TextStyle(fontSize: 16)),
Switch(
value: _modeManual,
onChanged: (val) async {
await updateModeManual(val);
if (!val) {
// If switching to otomatis, set kontrolServo to false
await updateKontrolServo(false);
}
},
),
const Text('Otomatis',
style: TextStyle(fontSize: 16)),
],
),
if (_modeManual)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
children: [
const Text('Servo:',
style: TextStyle(fontSize: 16)),
const SizedBox(width: 8),
_servo
? const Icon(Icons.lock,
color: Colors.blue)
: const Icon(Icons.lock_open,
color: Colors.orange),
const SizedBox(width: 8),
Switch(
value: _kontrolServo,
onChanged: (val) async {
await updateKontrolServo(val);
},
),
_isUpdatingServo
? const SizedBox(
width: 16,
height: 16,
child:
CircularProgressIndicator(
strokeWidth: 2),
)
: const SizedBox.shrink(),
const SizedBox(width: 8),
Text(
_kontrolServo
? 'Tertutup'
: 'Terbuka',
style: TextStyle(
color: _kontrolServo
? Colors.blue
: Colors.orange,
fontWeight: FontWeight.bold,
)),
],
),
),
if (!_modeManual)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
children: [
const Icon(Icons.info_outline,
color: Colors.grey),
const SizedBox(width: 8),
Text(
'Servo otomatis berdasarkan curah hujan',
style: TextStyle(
color: Colors.grey.shade700,
fontSize: 10),
),
],
),
),
],
),
),
),
const SizedBox(height: 80), // Space for countdown
],
),
),
),
);
}
Widget _buildInfoCard(
String title, String value, Color color, IconData icon, String status) {
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Icon(
icon,
size: 40,
color: color,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Row(
children: [
Text(
title == 'Intensitas Cahaya'
? '${cahaya.toStringAsFixed(0)}'
: value,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: color,
),
),
],
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: title == 'Suhu Udara'
? _getStatusSuhuColor(status).withOpacity(0.2)
: _getStatusColor(status).withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Text(
status,
style: TextStyle(
color: title == 'Status Cuaca'
? _getWeatherStatusColor(status)
: title == 'Suhu Udara'
? _getStatusSuhuColor(status)
: _getStatusColor(status),
fontWeight: FontWeight.bold,
),
),
),
],
),
),
);
}
String _getStatusSuhu(double suhu) {
// Status suhu berdasarkan nilai sensor
if (suhu <= 20) return 'Dingin';
if (suhu <= 23) return 'Sejuk';
if (suhu <= 26) return 'Normal';
if (suhu <= 27) return 'Hangat';
return 'Panas';
}
String _getStatusKelembaban(double kelembaban) {
// Status kelembaban berdasarkan nilai sensor
if (kelembaban < 40) return 'Kering';
if (kelembaban > 80) return 'Basah';
return 'Normal';
}
String _getStatusHujan(double kadar) {
// Status hujan berdasarkan nilai sensor
return (kadar < 500) ? "Hujan" : "Tidak Hujan";
}
String _getStatusCuaca(double cahaya) {
// Status cahaya berdasarkan nilai LDR
return (cahaya < 700) ? "Terang" : "Gelap";
}
Color _getStatusColor(String status) {
// Warna untuk setiap status sensor
switch (status) {
case 'Hujan':
return Colors.blue.shade700;
case 'Tidak Hujan':
return Colors.orange;
case 'Cerah':
return Colors.orange;
default:
return Colors.grey;
}
}
Color _getStatusSuhuColor(String status) {
// Warna untuk setiap status suhu
switch (status) {
case 'Dingin':
return Colors.blue.shade700;
case 'Sejuk':
return Colors.blue.shade400;
case 'Normal':
return Colors.orange;
case 'Hangat':
return Colors.orange.shade700;
case 'Panas':
return Colors.red;
default:
return Colors.grey;
}
}
}