From 8aa455758b868023646e8018ea0f91af6b54efc4 Mon Sep 17 00:00:00 2001 From: Muhammad Iqbal Date: Sun, 18 May 2025 02:07:07 +0700 Subject: [PATCH] logic output thermal sensor --- android/app/build.gradle | 3 + android/app/google-services.json | 29 +++++ android/settings.gradle | 5 +- firebase.json | 1 + ios/Runner/GoogleService-Info.plist | 30 +++++ lib/firebase_options.dart | 62 ++++++++++ lib/main.dart | 16 ++- lib/screens/control/control_screen.dart | 13 +- lib/screens/dashboard/dashboard_screen.dart | 113 ++++++++++++++---- .../notification/notification_screen.dart | 62 ++++++++++ lib/widgets/thermal_grid.dart | 42 ------- lib/widgets/thermal_heatmap.dart | 80 +++++++++++++ pubspec.lock | 103 +++++++++++++++- pubspec.yaml | 3 + 14 files changed, 490 insertions(+), 72 deletions(-) create mode 100644 android/app/google-services.json create mode 100644 firebase.json create mode 100644 ios/Runner/GoogleService-Info.plist create mode 100644 lib/firebase_options.dart delete mode 100644 lib/widgets/thermal_grid.dart create mode 100644 lib/widgets/thermal_heatmap.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index a936555..87203d2 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,5 +1,8 @@ plugins { id "com.android.application" + // START: FlutterFire Configuration + id 'com.google.gms.google-services' + // END: FlutterFire Configuration id "kotlin-android" // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id "dev.flutter.flutter-gradle-plugin" diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 0000000..39008bf --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "144700563392", + "project_id": "hamaguard-e911d", + "storage_bucket": "hamaguard-e911d.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:144700563392:android:913dfe0c4205cca8e4b225", + "android_client_info": { + "package_name": "com.example.hamaguard" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyDgtq_bU0aNjKwErdns50EldaRRSdd5Sg0" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/settings.gradle b/android/settings.gradle index b9e43bd..5914426 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -18,7 +18,10 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.1.0" apply false + id "com.android.application" version "8.2.1" 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 } diff --git a/firebase.json b/firebase.json new file mode 100644 index 0000000..1ce47d3 --- /dev/null +++ b/firebase.json @@ -0,0 +1 @@ +{"flutter":{"platforms":{"android":{"default":{"projectId":"hamaguard-e911d","appId":"1:144700563392:android:913dfe0c4205cca8e4b225","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"hamaguard-e911d","configurations":{"android":"1:144700563392:android:913dfe0c4205cca8e4b225"}}}}}} \ No newline at end of file diff --git a/ios/Runner/GoogleService-Info.plist b/ios/Runner/GoogleService-Info.plist new file mode 100644 index 0000000..377842e --- /dev/null +++ b/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + AIzaSyBVDd8ZqMe_PK1So6pxng8zRHz8qzuToNE + GCM_SENDER_ID + 144700563392 + PLIST_VERSION + 1 + BUNDLE_ID + com.example.hamaguard + PROJECT_ID + hamaguard-e911d + STORAGE_BUCKET + hamaguard-e911d.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:144700563392:ios:76ebcf9db01d01cfe4b225 + + \ No newline at end of file diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 0000000..4909f3c --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,62 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for web - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for ios - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyDgtq_bU0aNjKwErdns50EldaRRSdd5Sg0', + appId: '1:144700563392:android:913dfe0c4205cca8e4b225', + messagingSenderId: '144700563392', + projectId: 'hamaguard-e911d', + storageBucket: 'hamaguard-e911d.firebasestorage.app', + ); +} diff --git a/lib/main.dart b/lib/main.dart index 3ee1741..a03060a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,19 @@ import 'package:flutter/material.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'firebase_options.dart'; import 'screens/splash/splash_screen.dart'; -void main() { +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + try { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + } catch (e) { + // Bisa log error atau tampilkan pesan khusus + debugPrint('Firebase init error: $e'); + } + runApp(const MyApp()); } @@ -17,7 +29,7 @@ class MyApp extends StatelessWidget { colorScheme: ColorScheme.fromSeed(seedColor: Colors.green), useMaterial3: true, ), - home: const SplashScreen(), // << mulai dari splash + home: const SplashScreen(), ); } } diff --git a/lib/screens/control/control_screen.dart b/lib/screens/control/control_screen.dart index d206b81..b55c96e 100644 --- a/lib/screens/control/control_screen.dart +++ b/lib/screens/control/control_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import '../../widgets/costum_header.dart'; +import '../../screens/notification/notification_screen.dart'; class ControlScreen extends StatefulWidget { const ControlScreen({super.key}); @@ -50,9 +51,19 @@ class _ControlScreenState extends State { Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.grey[100], - appBar: const CustomHeader( + appBar: CustomHeader( deviceName: 'HamaGuard', + notificationCount: 5, + onNotificationTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => NotificationScreen(), ), + ); + }, + ), + body: Padding( padding: const EdgeInsets.all(20), child: Column( diff --git a/lib/screens/dashboard/dashboard_screen.dart b/lib/screens/dashboard/dashboard_screen.dart index ec4c5d1..869959d 100644 --- a/lib/screens/dashboard/dashboard_screen.dart +++ b/lib/screens/dashboard/dashboard_screen.dart @@ -1,38 +1,103 @@ -import 'dart:math'; import 'package:flutter/material.dart'; -import '../../widgets/thermal_grid.dart'; +import 'package:firebase_database/firebase_database.dart'; +import '../../widgets/thermal_heatmap.dart'; import '../../widgets/costum_header.dart'; +import '../notification/notification_screen.dart'; -class DashboardScreen extends StatelessWidget { +class DashboardScreen extends StatefulWidget { const DashboardScreen({super.key}); + @override + State createState() => _DashboardScreenState(); +} + +class _DashboardScreenState extends State { + late DatabaseReference _sensorRef; + late DatabaseReference _thermalRef; + + Map sensorData = { + 'PIR': 'Memuat...', + 'Ultrasonik': 'Memuat...', + 'Status Pengusir': 'Memuat...', + }; + + List thermalData = List.filled(64, 0.0); + + @override + void initState() { + super.initState(); + + _sensorRef = FirebaseDatabase.instance.ref('sensor'); + _thermalRef = FirebaseDatabase.instance.ref('thermal/temperatures'); + + // Listener sensor data + _sensorRef.onValue.listen((event) { + final data = event.snapshot.value; + if (data != null && data is Map) { + setState(() { + sensorData = { + 'PIR': (data['pir'] == true) ? 'Terdeteksi' : 'Tidak', + 'Ultrasonik': + data['ultrasonik'] != null ? '${data['ultrasonik']} cm' : '-', + 'Status Pengusir': + (data['pengusir'] == true) ? 'Aktif' : 'Nonaktif', + }; + }); + } + }); + + // Listener thermal data + _thermalRef.onValue.listen((event) { + final data = event.snapshot.value; + if (data != null) { + // Firebase RTDB terkadang mengirim List atau Map + List temps = []; + if (data is List) { + temps = data.map((e) { + if (e is num) return e.toDouble(); + return 0.0; + }).toList(); + } else if (data is Map) { + // Jika data disimpan sebagai Map index:string -> value + temps = List.filled(64, 0); + data.forEach((key, value) { + int idx = int.tryParse(key) ?? -1; + if (idx >= 0 && idx < 64) { + if (value is num) { + temps[idx] = value.toDouble(); + } + } + }); + } + + if (temps.length == 64) { + setState(() { + thermalData = temps; + }); + } + } + }); + } + @override Widget build(BuildContext context) { - // Dummy data sementara - final List thermalData = - List.generate(64, (index) => 25 + Random().nextDouble() * 10); - - final Map sensorData = const { - 'PIR': 'Tidak', - 'Ultrasonik': '50 cm', - 'Status Pengusir': 'Nonaktif', - }; - return Scaffold( backgroundColor: Colors.grey[100], appBar: CustomHeader( deviceName: 'HamaGuard', notificationCount: 5, onNotificationTap: () { - // Aksi saat lonceng ditekan + Navigator.push( + context, + MaterialPageRoute(builder: (context) => NotificationScreen()), + ); }, ), - body: SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Column( children: [ - // Thermal Sensor dalam Card + // Thermal Sensor Card( elevation: 4, shape: RoundedRectangleBorder( @@ -49,27 +114,25 @@ class DashboardScreen extends StatelessWidget { Text( 'Thermal Sensor', style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), + fontWeight: FontWeight.bold, fontSize: 16), ), ], ), const SizedBox(height: 12), - ThermalGrid(temperatures: thermalData), + ThermalHeatmap(temperatures: thermalData), ], ), ), ), const SizedBox(height: 16), - // PIR & Ultrasonik side by side + // PIR & Ultrasonik Row( children: [ Expanded( child: SensorCard( title: 'PIR', - value: sensorData['PIR']!, + value: sensorData['PIR'] ?? '-', icon: Icons.motion_photos_on_outlined, color: Colors.orange, ), @@ -78,7 +141,7 @@ class DashboardScreen extends StatelessWidget { Expanded( child: SensorCard( title: 'Ultrasonik', - value: sensorData['Ultrasonik']!, + value: sensorData['Ultrasonik'] ?? '-', icon: Icons.straighten, color: Colors.blue, ), @@ -90,7 +153,7 @@ class DashboardScreen extends StatelessWidget { // Status Pengusir SensorCard( title: 'Status Pengusir', - value: sensorData['Status Pengusir']!, + value: sensorData['Status Pengusir'] ?? '-', icon: Icons.speaker, color: Colors.green, ), @@ -122,7 +185,7 @@ class SensorCard extends StatelessWidget { shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: ListTile( leading: CircleAvatar( - backgroundColor: color.withOpacity(0.15), + backgroundColor: color.withAlpha(15), child: Icon(icon, color: color), ), title: Text(title), diff --git a/lib/screens/notification/notification_screen.dart b/lib/screens/notification/notification_screen.dart index e69de29..71000f5 100644 --- a/lib/screens/notification/notification_screen.dart +++ b/lib/screens/notification/notification_screen.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +class NotificationScreen extends StatelessWidget { + final List> notifications = [ + { + "title": "Peringatan!", + "message": "Hama terdeteksi di area sawah A.", + "time": "16 Mei 2025 - 10:15" + }, + { + "title": "Status Aman", + "message": "Tidak ada aktivitas hama selama 2 jam terakhir.", + "time": "16 Mei 2025 - 09:00" + }, + { + "title": "Mode Otomatis Aktif", + "message": "Sistem kembali ke mode otomatis.", + "time": "15 Mei 2025 - 17:20" + }, + ]; + + NotificationScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Notifikasi'), + backgroundColor: Colors.white, + foregroundColor: Colors.black, + elevation: 1, + ), + body: notifications.isEmpty + ? const Center( + child: Text( + 'Tidak ada notifikasi', + style: TextStyle(fontSize: 16, color: Colors.grey), + ), + ) + : ListView.separated( + itemCount: notifications.length, + padding: const EdgeInsets.all(16), + separatorBuilder: (context, index) => const Divider(), + itemBuilder: (context, index) { + final notif = notifications[index]; + return ListTile( + leading: const Icon(Icons.notifications, color: Colors.blue), + title: Text( + notif["title"]!, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Text(notif["message"]!), + trailing: Text( + notif["time"]!, + style: const TextStyle(fontSize: 12, color: Colors.grey), + ), + ); + }, + ), + ); + } +} diff --git a/lib/widgets/thermal_grid.dart b/lib/widgets/thermal_grid.dart deleted file mode 100644 index efee162..0000000 --- a/lib/widgets/thermal_grid.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter/material.dart'; - -class ThermalGrid extends StatelessWidget { - final List temperatures; - - const ThermalGrid({super.key, required this.temperatures}); - - @override - Widget build(BuildContext context) { - return AspectRatio( - aspectRatio: 1, - child: GridView.builder( - physics: const NeverScrollableScrollPhysics(), - itemCount: 64, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 8, - ), - itemBuilder: (context, index) { - final temp = temperatures[index]; - final color = getColorForTemperature(temp); - - return Container( - margin: const EdgeInsets.all(1), - decoration: BoxDecoration( - color: color, - borderRadius: BorderRadius.circular(2), - ), - ); - }, - ), - ); - } - - Color getColorForTemperature(double temp) { - if (temp < 26) return Colors.blue[200]!; - if (temp < 28) return Colors.lightBlue; - if (temp < 30) return Colors.green; - if (temp < 32) return Colors.yellow; - if (temp < 34) return Colors.orange; - return Colors.red; - } -} diff --git a/lib/widgets/thermal_heatmap.dart b/lib/widgets/thermal_heatmap.dart new file mode 100644 index 0000000..45aa18f --- /dev/null +++ b/lib/widgets/thermal_heatmap.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; + +class ThermalHeatmap extends StatelessWidget { + final List temperatures; + + const ThermalHeatmap({super.key, required this.temperatures}); + + @override + Widget build(BuildContext context) { + return Center( + child: SizedBox( + width: 300, + height: 300, + child: CustomPaint( + painter: _ThermalPainter(temperatures), + ), + ), + ); + } +} + +class _ThermalPainter extends CustomPainter { + final List temps; + + _ThermalPainter(this.temps); + + @override + void paint(Canvas canvas, Size size) { + final cellWidth = size.width / 8; + final cellHeight = size.height / 8; + + final blurPaint = Paint() + ..isAntiAlias = true + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 2); + + for (int y = 0; y < 8; y++) { + for (int x = 0; x < 8; x++) { + int i = y * 8 + x; + double temp = i < temps.length ? temps[i] : 0.0; + final color = _getColorForTemp(temp); + + final rect = Rect.fromLTWH( + x * cellWidth, + y * cellHeight, + cellWidth, + cellHeight, + ); + + canvas.drawRect(rect, blurPaint..color = color.withOpacity(0.95)); + } + } + + final center = Offset(size.width / 2, size.height / 2); + final plusPaint = Paint() + ..color = Colors.white.withOpacity(0.95) + ..strokeWidth = 2 + ..strokeCap = StrokeCap.round; + canvas.drawLine(center - const Offset(10, 0), center + const Offset(10, 0), plusPaint); + canvas.drawLine(center - const Offset(0, 10), center + const Offset(0, 10), plusPaint); + } + + Color _getColorForTemp(double t) { + double norm = ((t - 20) / 20).clamp(0.0, 1.0); + return HSVColor.lerp( + HSVColor.fromColor(Colors.blue), + HSVColor.fromColor(Colors.red), + norm, + )! + .toColor(); + } + + @override + bool shouldRepaint(covariant _ThermalPainter oldDelegate) { + if (oldDelegate.temps.length != temps.length) return true; + for (int i = 0; i < temps.length; i++) { + if (oldDelegate.temps[i] != temps[i]) return true; + } + return false; + } +} diff --git a/pubspec.lock b/pubspec.lock index 4712809..4f31cb1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: de9ecbb3ddafd446095f7e833c853aff2fa1682b017921fe63a833f9d6f0e422 + url: "https://pub.dev" + source: hosted + version: "1.3.54" async: dependency: transitive description: @@ -33,6 +41,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + cloud_firestore: + dependency: "direct main" + description: + name: cloud_firestore + sha256: "6ca737017131a77940fdcca3e13bf6efb5633d50b83514bba24f5fd68e7f340f" + url: "https://pub.dev" + source: hosted + version: "5.6.7" + cloud_firestore_platform_interface: + dependency: transitive + description: + name: cloud_firestore_platform_interface + sha256: "082c9aa0602efc1ee8c44c4e8a29a456b198f79a7f62657df39144c97e364cfb" + url: "https://pub.dev" + source: hosted + version: "6.6.7" + cloud_firestore_web: + dependency: transitive + description: + name: cloud_firestore_web + sha256: e1ddeacedd1c5a2e54089bbeb2d95d03b0096eccf8889024598f09f092ed8746 + url: "https://pub.dev" + source: hosted + version: "4.4.7" collection: dependency: transitive description: @@ -57,6 +89,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "017d17d9915670e6117497e640b2859e0b868026ea36bf3a57feb28c3b97debe" + url: "https://pub.dev" + source: hosted + version: "3.13.0" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: d7253d255ff10f85cfd2adaba9ac17bae878fa3ba577462451163bd9f1d1f0bf + url: "https://pub.dev" + source: hosted + version: "5.4.0" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: "129a34d1e0fb62e2b488d988a1fc26cc15636357e50944ffee2862efe8929b23" + url: "https://pub.dev" + source: hosted + version: "2.22.0" + firebase_database: + dependency: "direct main" + description: + name: firebase_database + sha256: "182ce4713d47ffc5f19a5a7b934867d1fae9c33081febcec8c062cb89fc14652" + url: "https://pub.dev" + source: hosted + version: "11.3.5" + firebase_database_platform_interface: + dependency: transitive + description: + name: firebase_database_platform_interface + sha256: b65f416dd2c8ac2d5322241e5411a24ed3da43d0f38aaf9ab6c211d72e52261b + url: "https://pub.dev" + source: hosted + version: "0.2.6+5" + firebase_database_web: + dependency: transitive + description: + name: firebase_database_web + sha256: "5203141fe00a1edfaed5f8e0444b8e4ef807a8ec6eca925621b1cab69b6c06e4" + url: "https://pub.dev" + source: hosted + version: "0.2.6+11" flutter: dependency: "direct main" description: flutter @@ -75,6 +155,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" intl: dependency: "direct main" description: @@ -147,6 +232,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" sky_engine: dependency: transitive description: flutter @@ -216,6 +309,14 @@ packages: url: "https://pub.dev" source: hosted version: "14.3.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" sdks: dart: ">=3.6.2 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index 249cb63..eadfb99 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,9 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 + firebase_core: ^3.13.0 + firebase_database: ^11.3.5 + cloud_firestore: ^5.6.7 dev_dependencies: flutter_test: