import 'package:flutter/material.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_database/firebase_database.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:audioplayers/audioplayers.dart'; import '../widgets/moisture_gauge.dart'; import '../widgets/pir_status_card.dart'; import 'manual_screen.dart'; class DashboardScreen extends StatefulWidget { const DashboardScreen({super.key}); @override State createState() => _DashboardScreenState(); } class _DashboardScreenState extends State { final dbRef = FirebaseDatabase.instanceFor( app: Firebase.app(), databaseURL: "https://tomat2-default-rtdb.asia-southeast1.firebasedatabase.app", ).ref(); int moisture = 0; bool pirDetected = false; String mode = "auto"; int min = 60; int max = 80; int pump = 0; int index = 0; /// ===== CHART ===== List spots = []; int xIndex = 0; /// ===== EMA ===== double ema = 0; double alpha = 0.2; double getEMA(double value) { if (ema == 0) { ema = value; } else { ema = alpha * value + (1 - alpha) * ema; } return ema; } /// ===== AUDIO ===== final AudioPlayer player = AudioPlayer(); bool isPlaying = false; /// ===== PIR CONTROL ===== bool lastPir = false; DateTime lastTriggerTime = DateTime.now(); Future triggerAlarm() async { final now = DateTime.now(); if (now.difference(lastTriggerTime).inSeconds < 10) return; lastTriggerTime = now; if (isPlaying) return; isPlaying = true; await player.play(AssetSource('sounds/alert.mp3')); Future.delayed(const Duration(seconds: 15), () async { await player.stop(); isPlaying = false; }); } @override void initState() { super.initState(); /// ===== MOISTURE ===== dbRef.child("sensor/moisture").onValue.listen((event) { final val = int.tryParse(event.snapshot.value.toString()) ?? 0; final smooth = getEMA(val.toDouble()); setState(() { moisture = val; spots.add(FlSpot(xIndex.toDouble(), smooth)); xIndex++; if (spots.length > 20) { spots.removeAt(0); } }); }); /// ===== PIR ===== dbRef.child("sensor/pir").onValue.listen((event) { final detected = event.snapshot.value.toString() == "1"; setState(() { pirDetected = detected; }); if (detected && !lastPir) { triggerAlarm(); // 🔊 alarm lokal } lastPir = detected; }); /// ===== MODE ===== dbRef.child("control/mode").onValue.listen((event) { setState(() { mode = event.snapshot.value.toString(); }); }); /// ===== CONFIG ===== dbRef.child("config/min_moisture").onValue.listen((event) { setState(() { min = int.tryParse(event.snapshot.value.toString()) ?? 60; }); }); dbRef.child("config/max_moisture").onValue.listen((event) { setState(() { max = int.tryParse(event.snapshot.value.toString()) ?? 80; }); }); /// ===== PUMP ===== dbRef.child("control/pump").onValue.listen((event) { setState(() { pump = int.tryParse(event.snapshot.value.toString()) ?? 0; }); }); } Future setMode(String newMode) async { await dbRef.child("control/mode").set(newMode); } @override void dispose() { player.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, appBar: AppBar( title: const Text("Monitoring Tomat"), backgroundColor: Colors.black.withOpacity(0.6), elevation: 0, ), body: Stack( children: [ Positioned.fill( child: Image.asset( "assets/images/bg.jpg", fit: BoxFit.cover, ), ), Positioned.fill( child: Container( color: Colors.black.withOpacity(0.6), ), ), Padding( padding: const EdgeInsets.all(16), child: Column( children: [ Row( children: [ Expanded( child: _buildCard( child: MoistureGauge( value: moisture, min: min, max: max, ), ), ), const SizedBox(width: 12), Expanded( child: _buildCard( child: PirStatusCard( pirDetected: pirDetected, ), ), ), ], ), const SizedBox(height: 20), _buildCard( height: 180, child: spots.isEmpty ? const Center( child: Text( "Waiting Data...", style: TextStyle(color: Colors.white38), ), ) : LineChart( LineChartData( minY: 0, maxY: 100, gridData: FlGridData(show: false), titlesData: FlTitlesData(show: false), borderData: FlBorderData(show: false), extraLinesData: ExtraLinesData( horizontalLines: [ HorizontalLine( y: min.toDouble(), strokeWidth: 1, color: Colors.orangeAccent, ), HorizontalLine( y: max.toDouble(), strokeWidth: 1, color: Colors.blueAccent, ), ], ), lineBarsData: [ LineChartBarData( spots: spots, isCurved: true, barWidth: 3, gradient: const LinearGradient( colors: [ Colors.greenAccent, Colors.green ], ), dotData: FlDotData(show: false), belowBarData: BarAreaData( show: true, gradient: LinearGradient( colors: [ Colors.greenAccent.withOpacity(0.3), Colors.transparent, ], ), ), ), ], ), ), ), const SizedBox(height: 20), _buildCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( mode == "auto" ? Icons.smart_toy : Icons.touch_app, color: mode == "auto" ? Colors.greenAccent : Colors.orangeAccent, ), const SizedBox(width: 10), Text( "Mode: ${mode.toUpperCase()}", style: const TextStyle(color: Colors.white), ), ], ), const SizedBox(height: 10), Text( _getPumpStatus(), style: TextStyle(color: _getPumpColor()), ), const SizedBox(height: 10), Text( "Min: $min | Max: $max", style: const TextStyle(color: Colors.white54), ), ], ), ), ], ), ), ], ), bottomNavigationBar: BottomNavigationBar( currentIndex: index, backgroundColor: const Color(0xFF020617), selectedItemColor: Colors.greenAccent, unselectedItemColor: Colors.white54, onTap: (i) async { if (i == index) return; setState(() => index = i); if (i == 0) { await setMode("auto"); Navigator.popUntil(context, (route) => route.isFirst); } if (i == 1) { await setMode("manual"); Navigator.push( context, MaterialPageRoute( builder: (_) => const ManualScreen(), ), ); } }, items: const [ BottomNavigationBarItem( icon: Icon(Icons.home), label: "Auto", ), BottomNavigationBarItem( icon: Icon(Icons.settings_remote), label: "Manual", ), ], ), ); } Widget _buildCard({required Widget child, double? height}) { return Container( height: height, padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: const Color(0xFF1E293B).withOpacity(0.85), borderRadius: BorderRadius.circular(16), ), child: child, ); } String _getPumpStatus() { if (mode == "manual") { return pump == 1 ? "Pump ON (Manual)" : "Pump OFF (Manual)"; } if (moisture < min) return "Pump ON (Dry)"; if (moisture > max) return "Pump OFF (Wet)"; return pump == 1 ? "Pump ON (Holding)" : "Pump OFF (Holding)"; } Color _getPumpColor() { return pump == 1 ? Colors.greenAccent : Colors.redAccent; } }