Upload proyek monitoring pengering kopi IoT

This commit is contained in:
M.Nizar Fahrurrozi 2026-05-06 21:40:45 +07:00
parent 1cd4bb22ee
commit 1908432345
19 changed files with 1981 additions and 705 deletions

View File

@ -1,3 +1,9 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
@ -6,47 +12,27 @@ if (localPropertiesFile.exists()) {
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
def flutterVersionCode = localProperties.getProperty('flutter.versionCode') ?: '1'
def flutterVersionName = localProperties.getProperty('flutter.versionName') ?: '1.0'
android {
namespace "com.example.coffee_iot_flutter"
compileSdkVersion 34
compileSdk 35
ndkVersion flutter.ndkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
jvmTarget = '17'
}
defaultConfig {
applicationId "com.example.coffee_iot_flutter"
minSdkVersion flutter.minSdkVersion
targetSdkVersion 34
minSdk flutter.minSdkVersion
targetSdk 35
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
@ -58,10 +44,15 @@ android {
}
}
// Force pin versi androidx agar kompatibel AGP 8.6.1
configurations.all {
resolutionStrategy {
force 'androidx.browser:browser:1.8.0'
force 'androidx.core:core:1.15.0'
force 'androidx.core:core-ktx:1.15.0'
}
}
flutter {
source '../..'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}

View File

@ -1,16 +1,3 @@
buildscript {
ext.kotlin_version = '1.8.22'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.1.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
@ -18,10 +5,12 @@ allprojects {
}
}
rootProject.buildDir = '../build'
rootProject.buildDir = "../build"
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}

File diff suppressed because one or more lines are too long

View File

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip

View File

@ -1,17 +1,25 @@
include ':app'
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader("UTF-8") { reader ->
properties.load(reader)
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
def flutterSdkPath = properties.getProperty("flutter.sdk")
if (flutterSdkPath == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
plugins {
id "com.android.application" version "8.6.1" apply false
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
}
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
include ":app"

View File

@ -14,7 +14,7 @@ void main() async {
// Initialize Supabase with the provided URL and Publishable Key
await Supabase.initialize(
url: 'https://ddmhzzegejbsshihzext.supabase.co',
anonKey: 'sb_publishable_IJJ8fXD9CXfjMeu8vNJ8bw_NhWyTJKy',
anonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRkbWh6emVnZWpic3NoaWh6ZXh0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzcyMjIyNzAsImV4cCI6MjA5Mjc5ODI3MH0._r-viIQLMXuNNdg4tcn7sJTBO9QqSpSlF7hk43hu2Ks',
);
runApp(

View File

@ -34,9 +34,9 @@ class _AnalyticsScreenState extends State<AnalyticsScreen> {
@override
Widget build(BuildContext context) {
final args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
final String title = args['title'];
final Color color = args['color'];
final args = (ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?) ?? {};
final String title = args['title'] ?? 'Grafik';
final Color color = args['color'] ?? Colors.green;
final String sensorKey = title.contains('Suhu') ? 'suhu' : title.contains('Kelembaban') ? 'kelembapan' : 'intensitas';
return Scaffold(

View File

@ -12,7 +12,7 @@ class ControlSettingsScreen extends StatefulWidget {
}
class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
int _operatingMode = 1; // 0 for Auto, 1 for Manual
int _operatingMode = 1; // 0 = otomatis, 1 = manual
final TextEditingController _tempMinController = TextEditingController();
final TextEditingController _tempMaxController = TextEditingController();
final TextEditingController _humMinController = TextEditingController();
@ -25,33 +25,70 @@ class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
_loadSettings();
}
@override
void dispose() {
_tempMinController.dispose();
_tempMaxController.dispose();
_humMinController.dispose();
_humMaxController.dispose();
super.dispose();
}
Future<void> _loadSettings() async {
try {
final prefs = await SharedPreferences.getInstance();
final data = await SupabaseService().getBatasSensor();
if (data != null) {
if (data != null && mounted) {
setState(() {
_tempMinController.text = data['suhu_min'].toString();
_tempMaxController.text = data['suhu_max'].toString();
_humMinController.text = data['rh_min'].toString();
_humMaxController.text = data['rh_max'].toString();
// Load last saved mode from local memory
_operatingMode = prefs.getInt('op_mode') ?? 1;
_isLoading = false;
});
}
} catch (e) {
debugPrint('Error loading settings: $e');
setState(() => _isLoading = false);
if (mounted) setState(() => _isLoading = false);
}
}
void _saveSettings() async {
// 1. Validation: Prevent empty inputs
if (_tempMinController.text.isEmpty || _tempMaxController.text.isEmpty ||
_humMinController.text.isEmpty || _humMaxController.text.isEmpty) {
Future<void> _saveSettings() async {
if (_tempMinController.text.isEmpty ||
_tempMaxController.text.isEmpty ||
_humMinController.text.isEmpty ||
_humMaxController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Semua kolom batas harus diisi!'), backgroundColor: Colors.orange),
const SnackBar(
content: Text('Semua kolom batas harus diisi!'),
backgroundColor: Colors.orange,
),
);
return;
}
final suhuMin = double.tryParse(_tempMinController.text) ?? 30;
final suhuMax = double.tryParse(_tempMaxController.text) ?? 45;
final rhMin = double.tryParse(_humMinController.text) ?? 60;
final rhMax = double.tryParse(_humMaxController.text) ?? 70;
// Validasi logika
if (suhuMin >= suhuMax) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Suhu min harus lebih kecil dari suhu max!'),
backgroundColor: Colors.orange,
),
);
return;
}
if (rhMin >= rhMax) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('RH min harus lebih kecil dari RH max!'),
backgroundColor: Colors.orange,
),
);
return;
}
@ -62,31 +99,33 @@ class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
setState(() => _isLoading = true);
try {
// 2. Save Mode to Local Memory
// Simpan mode ke local storage
await prefs.setInt('op_mode', _operatingMode);
// 3. MQTT Publish
mqtt.publish('coffee/config/temp_max', _tempMaxController.text);
mqtt.publish('coffee/config/hum_max', _humMaxController.text);
mqtt.publish('coffee/config/mode', _operatingMode == 0 ? "AUTO" : "MANUAL");
// Kirim batas sensor ke ESP32 via MQTT Flow 5 simpan ke Supabase
mqtt.setBatasSensor(suhuMin, suhuMax, rhMin, rhMax);
// 4. Supabase Update
await SupabaseService().updateBatasSensor(
double.parse(_tempMinController.text),
double.parse(_tempMaxController.text),
double.parse(_humMinController.text),
double.parse(_humMaxController.text),
);
// Kirim mode ke ESP32
mqtt.gantiMode(_operatingMode == 0 ? "otomatis" : "manual");
// Update Supabase langsung juga sebagai backup
await SupabaseService().updateBatasSensor(suhuMin, suhuMax, rhMin, rhMax);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('✅ Pengaturan Tersimpan & Terkirim')),
const SnackBar(
content: Text('✅ Pengaturan Tersimpan & Terkirim ke ESP32'),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('❌ Gagal: $e'), backgroundColor: Colors.red),
SnackBar(
content: Text('❌ Gagal: $e'),
backgroundColor: Colors.red,
),
);
}
} finally {
@ -100,28 +139,52 @@ class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
backgroundColor: Colors.black,
body: SafeArea(
child: _isLoading
? const Center(child: CircularProgressIndicator(color: Colors.green))
? const Center(
child: CircularProgressIndicator(color: Colors.green))
: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Pengaturan',
style: TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.bold),
),
const Text(
'Batas Sensor & Mode Operasi',
style: TextStyle(color: Colors.grey, fontSize: 16),
),
const SizedBox(height: 30),
// Batas Suhu
Row(
children: [
Expanded(child: _buildInputCard('Suhu Min (°C)', '32', _tempMinController)),
Expanded(child: _buildInputCard(
'Suhu Min (°C)', '30', _tempMinController)),
const SizedBox(width: 10),
Expanded(child: _buildInputCard('Suhu Max (°C)', '48', _tempMaxController)),
],
),
const SizedBox(height: 20),
Row(
children: [
Expanded(child: _buildInputCard('Kelembaban Min (%)', '65', _humMinController)),
const SizedBox(width: 10),
Expanded(child: _buildInputCard('Kelembaban Max (%)', '75', _humMaxController)),
Expanded(child: _buildInputCard(
'Suhu Max (°C)', '45', _tempMaxController)),
],
),
const SizedBox(height: 15),
// Batas RH
Row(
children: [
Expanded(child: _buildInputCard(
'RH Min (%)', '60', _humMinController)),
const SizedBox(width: 10),
Expanded(child: _buildInputCard(
'RH Max (%)', '70', _humMaxController)),
],
),
const SizedBox(height: 20),
// Mode Operasi
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
@ -131,28 +194,46 @@ class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Mode Operasi', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
const Text('Mode Operasi',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16)),
const SizedBox(height: 10),
RadioListTile<int>(
value: 0,
groupValue: _operatingMode,
onChanged: (val) => setState(() => _operatingMode = val!),
title: const Text('Mode Otomatis', style: TextStyle(color: Colors.white)),
onChanged: (val) =>
setState(() => _operatingMode = val!),
title: const Text('Mode Otomatis',
style: TextStyle(color: Colors.white)),
activeColor: Colors.green,
subtitle: const Text('Kipas bekerja otomatis berdasarkan sensor', style: TextStyle(color: Colors.grey, fontSize: 10)),
subtitle: const Text(
'Kipas bekerja otomatis berdasarkan sensor',
style: TextStyle(
color: Colors.grey, fontSize: 10),
),
),
RadioListTile<int>(
value: 1,
groupValue: _operatingMode,
onChanged: (val) => setState(() => _operatingMode = val!),
title: const Text('Mode Manual', style: TextStyle(color: Colors.white)),
onChanged: (val) =>
setState(() => _operatingMode = val!),
title: const Text('Mode Manual',
style: TextStyle(color: Colors.white)),
activeColor: Colors.green,
subtitle: const Text('Kontrol penuh kipas melalui dashboard HP', style: TextStyle(color: Colors.grey, fontSize: 10)),
subtitle: const Text(
'Kontrol penuh kipas melalui dashboard HP',
style: TextStyle(
color: Colors.grey, fontSize: 10),
),
),
],
),
),
const SizedBox(height: 20),
// Tombol Simpan
SizedBox(
width: double.infinity,
height: 55,
@ -164,7 +245,13 @@ class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
borderRadius: BorderRadius.circular(15),
),
),
child: const Text('Simpan Pengaturan', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white)),
child: const Text(
'Simpan & Kirim ke ESP32',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white),
),
),
),
const SizedBox(height: 100),
@ -176,7 +263,8 @@ class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
);
}
Widget _buildInputCard(String title, String hint, TextEditingController controller) {
Widget _buildInputCard(
String title, String hint, TextEditingController controller) {
return Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
@ -186,7 +274,11 @@ class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold)),
Text(title,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
TextField(
controller: controller,

View File

@ -46,12 +46,32 @@ class ImageResultScreen extends StatelessWidget {
color: const Color(0xFF1A1A1A),
borderRadius: BorderRadius.circular(20),
),
child: Column(
clipBehavior: Clip.hardEdge,
child: args['url_foto'] != null && args['url_foto'].toString().isNotEmpty
? Image.network(
args['url_foto'],
fit: BoxFit.cover,
loadingBuilder: (context, child, progress) {
if (progress == null) return child;
return const Center(
child: CircularProgressIndicator(color: Colors.green),
);
},
errorBuilder: (context, error, stack) => Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.broken_image, size: 80, color: Colors.grey),
Text('Citra #${args['id']}',
style: const TextStyle(color: Colors.grey)),
],
),
)
: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.image, size: 80, color: Colors.grey),
const SizedBox(height: 10),
Text('Citra #${args['id']}', style: const TextStyle(color: Colors.grey)),
Text('Citra #${args['id']}',
style: const TextStyle(color: Colors.grey)),
],
),
),

View File

@ -12,6 +12,7 @@ class _LoginScreenState extends State<LoginScreen> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isLoading = false;
bool _obscurePassword = true; // Tambahan: State untuk mata password
Future<void> _handleLogin() async {
setState(() => _isLoading = true);
@ -80,13 +81,24 @@ class _LoginScreenState extends State<LoginScreen> {
const Divider(color: Colors.grey),
TextField(
controller: _passwordController,
obscureText: true,
obscureText: _obscurePassword, // Menggunakan variabel state
style: const TextStyle(color: Colors.white),
decoration: const InputDecoration(
prefixIcon: Icon(Icons.lock, color: Colors.green),
suffixIcon: Icon(Icons.visibility, color: Colors.grey),
decoration: InputDecoration(
prefixIcon: const Icon(Icons.lock, color: Colors.green),
// Menggunakan IconButton untuk aksi klik
suffixIcon: IconButton(
icon: Icon(
_obscurePassword ? Icons.visibility : Icons.visibility_off,
color: Colors.grey,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
hintText: 'Password',
hintStyle: TextStyle(color: Colors.grey),
hintStyle: const TextStyle(color: Colors.grey),
border: InputBorder.none,
),
),

View File

@ -23,30 +23,9 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
Future<void> _initData() async {
setState(() => _isInitialLoading = true);
await _loadLimits();
await _loadLastSensorData();
if (mounted) setState(() => _isInitialLoading = false);
}
Future<void> _loadLastSensorData() async {
try {
final lastLog = await SupabaseService().getLatestLog();
if (lastLog != null && mounted) {
final mqtt = Provider.of<MqttService>(context, listen: false);
// Update local MQTT service state with last DB values if currently "0" or default
// This ensures the user sees the last known data immediately
mqtt.updateFromLastLog(
lastLog['suhu'].toString(),
lastLog['kelembapan'].toString(),
lastLog['intensitas'].toString(),
lastLog['kipas1'] == 'ON',
lastLog['kipas2'] == 'ON'
);
}
} catch (e) {
debugPrint('Error loading last log: $e');
}
}
Future<void> _loadLimits() async {
try {
final data = await SupabaseService().getBatasSensor();
@ -64,14 +43,15 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
@override
Widget build(BuildContext context) {
final mqtt = Provider.of<MqttService>(context);
final curTemp = double.tryParse(mqtt.temp) ?? 0;
final curHum = double.tryParse(mqtt.humidity) ?? 0;
final curTemp = double.tryParse(mqtt.suhu) ?? 0;
final curHum = double.tryParse(mqtt.kelembapan) ?? 0;
return Scaffold(
backgroundColor: Colors.black,
body: SafeArea(
child: _isInitialLoading
? const Center(child: CircularProgressIndicator(color: Colors.green))
? const Center(
child: CircularProgressIndicator(color: Colors.green))
: RefreshIndicator(
onRefresh: _initData,
color: Colors.green,
@ -82,6 +62,7 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@ -90,11 +71,15 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
children: [
const Text(
'Monitoring',
style: TextStyle(color: Colors.white, fontSize: 32, fontWeight: FontWeight.bold),
style: TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.bold),
),
const Text(
'Sistem Pengeringan Kopi',
style: TextStyle(color: Colors.grey, fontSize: 16),
style: TextStyle(
color: Colors.grey, fontSize: 16),
),
const SizedBox(height: 5),
Row(
@ -104,17 +89,25 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: mqtt.client?.connectionStatus?.state == MqttConnectionState.connected
? Colors.green : Colors.red,
color: mqtt.client?.connectionStatus
?.state ==
MqttConnectionState.connected
? Colors.green
: Colors.red,
),
),
const SizedBox(width: 8),
Text(
mqtt.client?.connectionStatus?.state == MqttConnectionState.connected
? 'Connected' : 'Disconnected',
mqtt.client?.connectionStatus?.state ==
MqttConnectionState.connected
? 'Connected'
: 'Disconnected',
style: TextStyle(
color: mqtt.client?.connectionStatus?.state == MqttConnectionState.connected
? Colors.green : Colors.red,
color: mqtt.client?.connectionStatus
?.state ==
MqttConnectionState.connected
? Colors.green
: Colors.red,
fontSize: 12,
),
),
@ -125,11 +118,50 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
const CircleAvatar(
backgroundColor: Color(0xFF1A1A1A),
radius: 25,
child: Icon(Icons.person, color: Colors.green),
child:
Icon(Icons.person, color: Colors.green),
),
],
),
const SizedBox(height: 30),
const SizedBox(height: 10),
// Timestamp
Text(
'Update: ${mqtt.timestamp}',
style: const TextStyle(
color: Colors.grey, fontSize: 11),
),
const SizedBox(height: 20),
// Mode Badge
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: mqtt.mode == 'manual'
? Colors.orange.withOpacity(0.2)
: Colors.green.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: mqtt.mode == 'manual'
? Colors.orange
: Colors.green,
),
),
child: Text(
'Mode: ${mqtt.mode.toUpperCase()}',
style: TextStyle(
color: mqtt.mode == 'manual'
? Colors.orange
: Colors.green,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 20),
// Grid Sensor + Kontrol
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
@ -139,50 +171,90 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
children: [
_buildSensorCard(
'Suhu',
'${mqtt.temp}°C',
'${mqtt.suhu}°C',
Icons.thermostat,
curTemp > mqtt.maxTemp ? Colors.red : Colors.redAccent,
isAlert: curTemp > mqtt.maxTemp,
onTap: () => Navigator.pushNamed(context, '/analytics', arguments: {'title': 'Grafik Suhu (°C)', 'color': Colors.redAccent, 'value': '${mqtt.temp}°C'}),
curTemp > mqtt.maxSuhu
? Colors.red
: Colors.redAccent,
isAlert: curTemp > mqtt.maxSuhu,
onTap: () => Navigator.pushNamed(
context,
'/analytics',
arguments: {
'title': 'Grafik Suhu (°C)',
'color': Colors.redAccent,
'value': '${mqtt.suhu}°C',
},
),
),
_buildSensorCard(
'Kelembaban',
'${mqtt.humidity}%',
'${mqtt.kelembapan}%',
Icons.water_drop,
curHum > mqtt.maxHum ? Colors.red : Colors.cyan,
isAlert: curHum > mqtt.maxHum,
onTap: () => Navigator.pushNamed(context, '/analytics', arguments: {'title': 'Grafik Kelembaban (%)', 'color': Colors.cyan, 'value': '${mqtt.humidity}%'}),
curHum > mqtt.maxRh
? Colors.red
: Colors.cyan,
isAlert: curHum > mqtt.maxRh,
onTap: () => Navigator.pushNamed(
context,
'/analytics',
arguments: {
'title': 'Grafik Kelembaban (%)',
'color': Colors.cyan,
'value': '${mqtt.kelembapan}%',
},
),
),
_buildSensorCard(
'Cahaya',
'${mqtt.light} Lux',
'${mqtt.intensitas} Lux',
Icons.wb_sunny,
Colors.orange,
onTap: () => Navigator.pushNamed(context, '/analytics', arguments: {'title': 'Grafik Cahaya (Lux)', 'color': Colors.orange, 'value': '${mqtt.light} Lux'}),
onTap: () => Navigator.pushNamed(
context,
'/analytics',
arguments: {
'title': 'Grafik Cahaya (Lux)',
'color': Colors.orange,
'value': '${mqtt.intensitas} Lux',
},
),
),
_buildControlCard(
'Intake',
mqtt.isIntakeOn,
'Kipas 1 (Exhaust)',
mqtt.isKipas1On,
(val) async {
final status = val ? 'ON' : 'OFF';
mqtt.publish('coffee/intake', status);
await SupabaseService().logFanAction(status, mqtt.isExhaustOn ? 'ON' : 'OFF', curTemp, curHum, double.tryParse(mqtt.light) ?? 0);
mqtt.perintahKipas("1", val);
await SupabaseService().logFanAction(
val ? 'ON' : 'OFF',
mqtt.isKipas2On ? 'ON' : 'OFF',
curTemp,
curHum,
double.tryParse(mqtt.intensitas) ?? 0,
);
},
mqtt,
),
_buildControlCard(
'Exhaust',
mqtt.isExhaustOn,
'Kipas 2 (Intake)',
mqtt.isKipas2On,
(val) async {
final status = val ? 'ON' : 'OFF';
mqtt.publish('coffee/exhaust', status);
await SupabaseService().logFanAction(mqtt.isIntakeOn ? 'ON' : 'OFF', status, curTemp, curHum, double.tryParse(mqtt.light) ?? 0);
mqtt.perintahKipas("2", val);
await SupabaseService().logFanAction(
mqtt.isKipas1On ? 'ON' : 'OFF',
val ? 'ON' : 'OFF',
curTemp,
curHum,
double.tryParse(mqtt.intensitas) ?? 0,
);
},
mqtt,
),
],
),
if (curTemp > mqtt.maxTemp || curHum > mqtt.maxHum)
// Alert Banner
if (curTemp > mqtt.maxSuhu || curHum > mqtt.maxRh)
Container(
margin: const EdgeInsets.only(top: 20),
padding: const EdgeInsets.all(15),
@ -191,19 +263,24 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
borderRadius: BorderRadius.circular(15),
border: Border.all(color: Colors.red),
),
child: Row(
children: const [
Icon(Icons.warning_amber_rounded, color: Colors.red),
child: const Row(
children: [
Icon(Icons.warning_amber_rounded,
color: Colors.red),
SizedBox(width: 10),
Expanded(
child: Text(
'Peringatan: Parameter pengeringan melewati batas aman!',
style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold, fontSize: 12),
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
fontSize: 12),
),
),
],
),
),
const SizedBox(height: 20),
],
),
),
@ -213,31 +290,52 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
);
}
Widget _buildSensorCard(String title, String value, IconData icon, Color color, {bool isAlert = false, VoidCallback? onTap}) {
Widget _buildSensorCard(
String title,
String value,
IconData icon,
Color color, {
bool isAlert = false,
VoidCallback? onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: isAlert ? Colors.red.withOpacity(0.1) : const Color(0xFF1A1A1A),
color: isAlert
? Colors.red.withOpacity(0.1)
: const Color(0xFF1A1A1A),
borderRadius: BorderRadius.circular(20),
border: isAlert ? Border.all(color: Colors.red, width: 2) : null,
border:
isAlert ? Border.all(color: Colors.red, width: 2) : null,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color, size: 40),
const SizedBox(height: 10),
Text(title, style: const TextStyle(color: Colors.grey, fontSize: 14)),
Text(title,
style:
const TextStyle(color: Colors.grey, fontSize: 14)),
const SizedBox(height: 5),
Text(value, style: const TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold)),
Text(value,
style: const TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.bold)),
],
),
),
);
}
Widget _buildControlCard(String title, bool isOn, Function(bool) onChanged, MqttService mqtt) {
Widget _buildControlCard(
String title,
bool isOn,
Function(bool) onChanged,
MqttService mqtt,
) {
return Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
@ -249,20 +347,40 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
children: [
Row(
children: [
const Icon(Icons.settings_input_component, color: Colors.green, size: 20),
const Icon(Icons.settings_input_component,
color: Colors.green, size: 20),
const SizedBox(width: 8),
Text(title, style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold)),
Expanded(
child: Text(title,
style: const TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.bold)),
),
],
),
const Spacer(),
Text('Mode: ${mqtt.currentMode}', style: const TextStyle(color: Colors.green, fontSize: 9, fontWeight: FontWeight.bold)),
Text(
'Mode: ${mqtt.mode.toUpperCase()}',
style: const TextStyle(
color: Colors.green,
fontSize: 9,
fontWeight: FontWeight.bold),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(isOn ? 'ON' : 'OFF', style: const TextStyle(color: Colors.grey, fontSize: 12)),
Text(isOn ? 'ON' : 'OFF',
style: const TextStyle(
color: Colors.grey, fontSize: 12)),
SizedBox(
height: 30,
child: Switch(value: isOn, onChanged: onChanged, activeColor: Colors.green),
child: Switch(
value: isOn,
onChanged:
mqtt.mode == 'manual' ? onChanged : null,
activeColor: Colors.green,
),
),
],
),

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/mqtt_service.dart';
import '../services/supabase_service.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
class RecordingScreen extends StatefulWidget {
const RecordingScreen({super.key});
@ -14,43 +15,91 @@ class _RecordingScreenState extends State<RecordingScreen> {
final TextEditingController _intervalController = TextEditingController();
List<Map<String, dynamic>> _gallery = [];
bool _isLoading = true;
bool _isRecording = false;
late final RealtimeChannel _channel;
@override
void initState() {
super.initState();
_loadData();
// 🔥 REALTIME LISTENER SUPABASE (AUTO REFRESH GALERI)
_channel = Supabase.instance.client
.channel('realtime-foto')
.onPostgresChanges(
event: PostgresChangeEvent.insert,
schema: 'public',
table: 'foto_dataset',
callback: (payload) {
_loadData(); // reload gallery otomatis saat foto baru masuk
},
)
.subscribe();
}
@override
void dispose() {
_intervalController.dispose();
// 🔥 MATIKAN REALTIME LISTENER
Supabase.instance.client.removeChannel(_channel);
super.dispose();
}
Future<void> _loadData() async {
setState(() => _isLoading = true);
try {
final interval = await SupabaseService().getIntervalSetting();
final images = await SupabaseService().getFotoDataset();
if (mounted) {
setState(() {
_intervalController.text = interval.toString();
_gallery = images;
_isLoading = false;
});
}
} catch (e) {
debugPrint('Error loading recording data: $e');
setState(() => _isLoading = false);
if (mounted) setState(() => _isLoading = false);
}
}
void _controlRecording(bool start) async {
Future<void> _controlRecording(bool start) async {
final mqtt = Provider.of<MqttService>(context, listen: false);
final intervalVal = int.tryParse(_intervalController.text) ?? 30;
if (intervalVal <= 0) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Interval harus lebih dari 0 menit!'),
backgroundColor: Colors.orange,
),
);
return;
}
if (start) {
mqtt.publish('coffee/record/interval', intervalVal.toString());
mqtt.publish('coffee/record/cmd', 'START');
// Simpan interval ke Supabase
await SupabaseService().updateIntervalSetting(intervalVal);
// Kirim interval ke ESP32 via MQTT
mqtt.setIntervalFoto(intervalVal);
// Trigger foto pertama langsung
mqtt.triggerFoto();
setState(() => _isRecording = true);
} else {
mqtt.publish('coffee/record/cmd', 'STOP');
setState(() => _isRecording = false);
}
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(start ? 'Perekaman Dimulai' : 'Perekaman Berhenti')),
SnackBar(
content: Text(start
? '📸 Perekaman dimulai — interval ${intervalVal} menit'
: '⏹ Perekaman dihentikan'),
backgroundColor: start ? Colors.green : Colors.red,
),
);
}
}
@ -61,12 +110,28 @@ class _RecordingScreenState extends State<RecordingScreen> {
backgroundColor: Colors.black,
body: SafeArea(
child: _isLoading
? const Center(child: CircularProgressIndicator(color: Colors.green))
? const Center(
child: CircularProgressIndicator(color: Colors.green))
: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
const Text(
'Perekaman',
style: TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.bold),
),
const Text(
'Dataset Citra Kopi',
style: TextStyle(color: Colors.grey, fontSize: 16),
),
const SizedBox(height: 20),
// Panel Kontrol
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
@ -76,7 +141,12 @@ class _RecordingScreenState extends State<RecordingScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Interval Waktu (menit)', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
const Text(
'Interval Waktu (menit)',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
TextField(
controller: _intervalController,
@ -84,55 +154,126 @@ class _RecordingScreenState extends State<RecordingScreen> {
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
hintText: '30',
hintStyle: const TextStyle(color: Colors.grey),
hintStyle:
const TextStyle(color: Colors.grey),
filled: true,
fillColor: Colors.black,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: const BorderSide(color: Colors.grey),
borderSide:
const BorderSide(color: Colors.grey),
),
),
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: () => _controlRecording(true),
style: ElevatedButton.styleFrom(backgroundColor: Colors.green),
child: const Text('Mulai Perekaman', style: TextStyle(color: Colors.white)),
const SizedBox(height: 15),
// Status perekaman
if (_isRecording)
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.green),
),
child: const Row(
children: [
Icon(Icons.circle,
color: Colors.green, size: 10),
SizedBox(width: 8),
Text('Perekaman berjalan...',
style: TextStyle(
color: Colors.green,
fontSize: 12)),
],
),
),
const SizedBox(height: 10),
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: () => _controlRecording(false),
style: ElevatedButton.styleFrom(backgroundColor: Colors.redAccent),
child: const Text('Berhenti Perekaman', style: TextStyle(color: Colors.white)),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: _isRecording
? null
: () => _controlRecording(true),
icon: const Icon(Icons.play_arrow,
color: Colors.white),
label: const Text('Mulai',
style:
TextStyle(color: Colors.white)),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
disabledBackgroundColor:
Colors.green.withOpacity(0.3),
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(12),
),
),
),
),
const SizedBox(width: 10),
Expanded(
child: ElevatedButton.icon(
onPressed: !_isRecording
? null
: () => _controlRecording(false),
icon: const Icon(Icons.stop,
color: Colors.white),
label: const Text('Berhenti',
style:
TextStyle(color: Colors.white)),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.redAccent,
disabledBackgroundColor:
Colors.redAccent.withOpacity(0.3),
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(12),
),
),
),
),
],
),
],
),
const SizedBox(height: 30),
),
const SizedBox(height: 20),
// Header Galeri
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Galeri Citra Dataset', style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)),
const Text(
'Galeri Citra Dataset',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold),
),
IconButton(
icon: const Icon(Icons.refresh, color: Colors.green),
icon: const Icon(Icons.refresh,
color: Colors.green),
onPressed: _loadData,
)
),
],
),
const SizedBox(height: 20),
const SizedBox(height: 10),
// Grid Galeri
Expanded(
child: _gallery.isEmpty
? const Center(child: Text('Belum ada data citra', style: TextStyle(color: Colors.grey)))
? const Center(
child: Text(
'Belum ada data citra',
style: TextStyle(color: Colors.grey),
),
)
: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
@ -141,28 +282,47 @@ class _RecordingScreenState extends State<RecordingScreen> {
itemBuilder: (context, index) {
final item = _gallery[index];
return GestureDetector(
onTap: () => Navigator.pushNamed(context, '/image_result', arguments: {
onTap: () => Navigator.pushNamed(
context,
'/image_result',
arguments: {
'id': item['id'].toString(),
'date': item['timestamp']?.split(' ')[0] ?? '',
'time': item['timestamp']?.split(' ')[1] ?? '',
'date': item['timestamp']
?.split(' ')[0] ??
'',
'time': item['timestamp']
?.split(' ')[1] ??
'',
'temp': '${item['suhu']} °C',
'humidity': '${item['kelembapan']} %',
'light': '${item['intensitas']} Lux',
'humidity':
'${item['kelembapan']} %',
'light':
'${item['intensitas']} Lux',
'location': 'Gudang Pengering',
'intake': item['kipas1'] ?? 'OFF',
'exhaust': item['kipas2'] ?? 'OFF'
}),
'exhaust': item['kipas2'] ?? 'OFF',
'url_foto': item['url_foto'] ?? '',
},
),
child: Container(
decoration: BoxDecoration(
color: const Color(0xFF1A1A1A),
borderRadius: BorderRadius.circular(15),
image: item['url_foto'] != null ? DecorationImage(
image: NetworkImage(item['url_foto']),
borderRadius:
BorderRadius.circular(15),
image: item['url_foto'] != null
? DecorationImage(
image: NetworkImage(
item['url_foto']),
fit: BoxFit.cover,
) : null,
)
: null,
),
child: item['url_foto'] == null
? const Icon(Icons.image, color: Colors.grey, size: 50)
? const Center(
child: Icon(Icons.image,
color: Colors.grey,
size: 50),
)
: null,
),
);

View File

@ -13,6 +13,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isLoading = false;
bool _obscurePassword = true; // Tambahan: State untuk mata password
Future<void> _handleRegister() async {
setState(() => _isLoading = true);
@ -99,13 +100,24 @@ class _RegisterScreenState extends State<RegisterScreen> {
const Divider(color: Colors.grey),
TextField(
controller: _passwordController,
obscureText: true,
obscureText: _obscurePassword, // Menggunakan variabel state
style: const TextStyle(color: Colors.white),
decoration: const InputDecoration(
prefixIcon: Icon(Icons.lock, color: Colors.green),
suffixIcon: Icon(Icons.visibility, color: Colors.grey),
decoration: InputDecoration(
prefixIcon: const Icon(Icons.lock, color: Colors.green),
// Menggunakan IconButton untuk aksi klik
suffixIcon: IconButton(
icon: Icon(
_obscurePassword ? Icons.visibility : Icons.visibility_off,
color: Colors.grey,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
hintText: 'Password',
hintStyle: TextStyle(color: Colors.grey),
hintStyle: const TextStyle(color: Colors.grey),
border: InputBorder.none,
),
),

View File

@ -1,44 +1,53 @@
import 'dart:io';
import 'dart:convert';
import 'package:mqtt_client/mqtt_client.dart';
import 'package:mqtt_client/mqtt_server_client.dart';
import 'package:flutter/foundation.dart';
class MqttService extends ChangeNotifier {
MqttServerClient? client;
String _temp = "25";
String _humidity = "55";
String _light = "125";
bool _isIntakeOn = false;
bool _isExhaustOn = false;
String _currentMode = "MANUAL";
double _maxTemp = 48.0;
double _maxHum = 75.0;
String get temp => _temp;
String get humidity => _humidity;
String get light => _light;
bool get isIntakeOn => _isIntakeOn;
bool get isExhaustOn => _isExhaustOn;
String get currentMode => _currentMode;
double get maxTemp => _maxTemp;
double get maxHum => _maxHum;
// Data sensor
String _suhu = "0";
String _kelembapan = "0";
String _intensitas = "0";
bool _isKipas1On = false;
bool _isKipas2On = false;
String _mode = "otomatis";
String _timestamp = "-";
void setLimits(double t, double h) {
_maxTemp = t;
_maxHum = h;
// Batas sensor
double _maxSuhu = 45.0;
double _maxRh = 70.0;
// Getter sensor
String get suhu => _suhu;
String get kelembapan => _kelembapan;
String get intensitas => _intensitas;
bool get isKipas1On => _isKipas1On;
bool get isKipas2On => _isKipas2On;
String get mode => _mode;
String get timestamp => _timestamp;
// Getter batas
double get maxSuhu => _maxSuhu;
double get maxRh => _maxRh;
Function(String, bool)? onConnectionResult;
void setLimits(double suhuMax, double rhMax) {
_maxSuhu = suhuMax;
_maxRh = rhMax;
notifyListeners();
}
// Callback for UI notifications
Function(String, bool)? onConnectionResult;
Future<bool> connect() async {
final String clientId = 'cli_kopi_${DateTime.now().millisecondsSinceEpoch % 10000}';
final String clientId =
'flutter_kopi_${DateTime.now().millisecondsSinceEpoch % 10000}';
client = MqttServerClient.withPort(
'1f7929299fd649388da5f2e234152531.s1.eu.hivemq.cloud',
's1fd277a.ala.asia-southeast1.emqxsl.com', // EMQX host baru
clientId,
8883
8883,
);
client!.secure = true;
@ -46,90 +55,140 @@ class MqttService extends ChangeNotifier {
client!.onBadCertificate = (dynamic cert) => true;
client!.keepAlivePeriod = 20;
client!.connectTimeoutPeriod = 10000;
client!.onDisconnected = onDisconnected;
client!.onConnected = onConnected;
client!.logging(on: true);
client!.logging(on: kDebugMode);
final connMess = MqttConnectMessage()
.withClientIdentifier(clientId)
.authenticateAs('kopi_user', 'Kopikopi1')
.startClean();
client!.connectionMessage = connMess;
try {
debugPrint('MQTT: Connecting to HiveMQ...');
await client!.connect();
} catch (e) {
debugPrint('MQTT Exception: $e');
onConnectionResult?.call('Koneksi Gagal: $e', false);
client!.disconnect();
return false;
}
if (client?.connectionStatus?.state == MqttConnectionState.connected) {
debugPrint('MQTT: SUCCESS!');
debugPrint('MQTT: Terhubung!');
onConnectionResult?.call('✅ Terhubung ke Server Kopi', true);
_subscribeToTopics();
notifyListeners();
return true;
} else {
onConnectionResult?.call('❌ Koneksi Terputus', false);
onConnectionResult?.call('❌ Koneksi Gagal', false);
notifyListeners();
return false;
}
}
void _subscribeToTopics() {
client!.subscribe("coffee/temp", MqttQos.atMostOnce);
client!.subscribe("coffee/humidity", MqttQos.atMostOnce);
client!.subscribe("coffee/light", MqttQos.atMostOnce);
client!.subscribe("coffee/intake", MqttQos.atMostOnce);
client!.subscribe("coffee/exhaust", MqttQos.atMostOnce);
client!.subscribe("coffee/config/mode", MqttQos.atMostOnce);
client!.subscribe("kopi/sensor", MqttQos.atMostOnce);
client!.subscribe("kopi/relay/kipas1", MqttQos.atMostOnce);
client!.subscribe("kopi/relay/kipas2", MqttQos.atMostOnce);
client!.subscribe("kopi/relay/mode", MqttQos.atMostOnce);
client!.updates!.listen((List<MqttReceivedMessage<MqttMessage?>>? c) {
final recMess = c![0].payload as MqttPublishMessage;
final pt = MqttPublishPayload.bytesToStringAsString(recMess.payload.message);
if (c == null || c.isEmpty) return;
final recMess = c[0].payload as MqttPublishMessage;
final pt = MqttPublishPayload.bytesToStringAsString(
recMess.payload.message);
final topic = c[0].topic;
if (topic == "coffee/temp") _temp = pt;
if (topic == "coffee/humidity") _humidity = pt;
if (topic == "coffee/light") _light = pt;
if (topic == "coffee/intake") _isIntakeOn = (pt == "ON");
if (topic == "coffee/exhaust") _isExhaustOn = (pt == "ON");
if (topic == "coffee/config/mode") _currentMode = pt;
if (topic == "kopi/sensor") {
try {
final data = jsonDecode(pt);
_suhu = data['suhu'].toString();
_kelembapan = data['kelembapan'].toString();
_intensitas = data['intensitas'].toString();
_isKipas1On = data['kipas1'] == 'ON';
_isKipas2On = data['kipas2'] == 'ON';
_mode = data['mode'] ?? 'otomatis';
_timestamp = data['timestamp'] ?? '-';
} catch (e) {
debugPrint('MQTT parse error: $e');
}
}
if (topic == "kopi/relay/kipas1") _isKipas1On = (pt == "ON");
if (topic == "kopi/relay/kipas2") _isKipas2On = (pt == "ON");
if (topic == "kopi/relay/mode") _mode = pt;
notifyListeners();
});
}
void publish(String topic, String message) {
if (topic == "coffee/intake") _isIntakeOn = (message == "ON");
if (topic == "coffee/exhaust") _isExhaustOn = (message == "ON");
notifyListeners();
// Kirim perintah kipas manual
void perintahKipas(String nomorKipas, bool nyala) {
final payload = jsonEncode({
"kipas": nomorKipas,
"status": nyala ? "ON" : "OFF",
});
_publish("kopi/relay/manual", payload);
}
if (client?.connectionStatus?.state == MqttConnectionState.connected) {
// Ganti mode otomatis/manual
void gantiMode(String mode) {
_publish("kopi/relay/mode", mode);
}
// Kirim interval foto dalam menit
void setIntervalFoto(int menit) {
_publish("kopi/setting/interval", menit.toString());
}
// Trigger foto manual
void triggerFoto() {
_publish("kopi/trigger/foto", "AMBIL");
}
// Kirim batas sensor ke ESP32 dan Node-RED
void setBatasSensor(
double suhuMin, double suhuMax, double rhMin, double rhMax) {
final payload = jsonEncode({
"suhu_min": suhuMin,
"suhu_max": suhuMax,
"rh_min": rhMin,
"rh_max": rhMax,
});
_publish("kopi/setting/batas", payload);
}
void _publish(String topic, String message) {
if (client?.connectionStatus?.state != MqttConnectionState.connected) {
debugPrint('MQTT: Tidak terhubung, tidak bisa publish');
return;
}
final builder = MqttClientPayloadBuilder();
builder.addString(message);
client!.publishMessage(topic, MqttQos.exactlyOnce, builder.payload!);
}
client!.publishMessage(topic, MqttQos.atMostOnce, builder.payload!);
debugPrint('MQTT publish → $topic : $message');
}
void onConnected() {
debugPrint('MQTT: onConnected');
notifyListeners();
}
void onDisconnected() {
debugPrint('MQTT: onDisconnected');
// 🔥 AUTO RECONNECT
Future.delayed(const Duration(seconds: 3), () {
debugPrint('MQTT: mencoba reconnect...');
connect();
});
notifyListeners();
}
void updateFromLastLog(String t, String h, String l, bool i, bool e) {
_temp = t;
_humidity = h;
_light = l;
_isIntakeOn = i;
_isExhaustOn = e;
notifyListeners();
void disconnect() {
client?.disconnect();
}
}

View File

@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
jni
)
set(PLUGIN_BUNDLED_LIBRARIES)

View File

@ -6,13 +6,11 @@ import FlutterMacOS
import Foundation
import app_links
import path_provider_foundation
import shared_preferences_foundation
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}

View File

@ -5,82 +5,122 @@ packages:
dependency: transitive
description:
name: app_links
sha256: "3ced568a5d9e309e99af71285666f1f3117bddd0bd5b3317979dccc1a40cada4"
sha256: "3462d9defc61565fde4944858b59bec5be2b9d5b05f20aed190adb3ad08a7abc"
url: "https://pub.dev"
source: hosted
version: "3.5.1"
version: "7.0.0"
app_links_linux:
dependency: transitive
description:
name: app_links_linux
sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81
url: "https://pub.dev"
source: hosted
version: "1.0.3"
app_links_platform_interface:
dependency: transitive
description:
name: app_links_platform_interface
sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
app_links_web:
dependency: transitive
description:
name: app_links_web
sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555
url: "https://pub.dev"
source: hosted
version: "1.0.4"
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.11.0"
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.3.0"
charcode:
dependency: transitive
description:
name: charcode
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version: "1.4.1"
clock:
dependency: transitive
description:
name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.1"
version: "1.1.2"
code_assets:
dependency: transitive
description:
name: code_assets
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
collection:
dependency: transitive
description:
name: collection
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.17.2"
version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
crypto:
dependency: transitive
description:
name: crypto
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.3"
csslib:
dependency: transitive
description:
name: csslib
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
version: "3.0.7"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
url: "https://pub.dev"
source: hosted
version: "1.0.8"
version: "1.0.9"
dart_jsonwebtoken:
dependency: transitive
description:
name: dart_jsonwebtoken
sha256: ad84e60181696513d04d5f2078e0bbc20365b911f46f647797317414bdc88fbe
url: "https://pub.dev"
source: hosted
version: "3.4.1"
equatable:
dependency: transitive
description:
@ -101,18 +141,26 @@ packages:
dependency: transitive
description:
name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878"
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "2.2.0"
ffi_leak_tracker:
dependency: transitive
description:
name: ffi_leak_tracker
sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97"
url: "https://pub.dev"
source: hosted
version: "0.1.2"
file:
dependency: transitive
description:
@ -125,10 +173,10 @@ packages:
dependency: "direct main"
description:
name: fl_chart
sha256: c1e26c7e48496be85104c16c040950b0436674cdf0737f3f6e95511b2529b592
sha256: b938f77d042cbcd822936a7a359a7235bad8bd72070de1f827efc2cc297ac888
url: "https://pub.dev"
source: hosted
version: "0.63.0"
version: "1.2.0"
flutter:
dependency: "direct main"
description: flutter
@ -138,10 +186,10 @@ packages:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
version: "6.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
@ -156,58 +204,82 @@ packages:
dependency: transitive
description:
name: functions_client
sha256: a49876ebae32a50eb62483c5c5ac80ed0d8da34f98ccc23986b03a8d28cee07c
sha256: "94074d62167ae634127ef6095f536835063a7dc80f2b1aa306d2346ff9023996"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
version: "2.5.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
gotrue:
dependency: transitive
description:
name: gotrue
sha256: d6362dff9a54f8c1c372bb137c858b4024c16407324d34e6473e59623c9b9f50
sha256: "7a4172601553e61716f5c3dd243aa3297e13308e07eb85b7853c941ba585dcf5"
url: "https://pub.dev"
source: hosted
version: "2.11.1"
version: "2.20.0"
gtk:
dependency: transitive
description:
name: gtk
sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c
sha256: "4ff85b2a16724029dd9e5bbb5a94b6918f9973f74ba571c949d2002801879cf5"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
html:
version: "2.2.0"
hooks:
dependency: transitive
description:
name: html
sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a"
name: hooks
sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e"
url: "https://pub.dev"
source: hosted
version: "0.15.4"
version: "1.0.3"
http:
dependency: transitive
description:
name: http
sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525"
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
version: "1.6.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
version: "4.1.2"
intl:
dependency: "direct main"
description:
name: intl
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.18.1"
version: "0.20.2"
jni:
dependency: transitive
description:
name: jni
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
url: "https://pub.dev"
source: hosted
version: "1.0.0"
jni_flutter:
dependency: transitive
description:
name: jni_flutter
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
jwt_decode:
dependency: transitive
description:
@ -216,62 +288,94 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.3.1"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
version: "6.1.0"
logging:
dependency: transitive
description:
name: logging
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.2.0"
version: "1.3.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.16"
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.5.0"
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
version: "1.17.0"
mime:
dependency: transitive
description:
name: mime
sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "1.0.4"
version: "2.0.0"
mqtt_client:
dependency: "direct main"
description:
name: mqtt_client
sha256: "930a951a748a3feacd57bde9a608740c724f7afb0e1be1ef56f3e1fe9f689cf9"
sha256: "41c8edd3bc8efc80c1c8ebfb40081c24d12d13085faca96b9280a624eca2d893"
url: "https://pub.dev"
source: hosted
version: "10.3.0"
version: "10.11.11"
native_toolchain_c:
dependency: transitive
description:
name: native_toolchain_c
sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
url: "https://pub.dev"
source: hosted
version: "0.17.6"
nested:
dependency: transitive
description:
@ -280,38 +384,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
url: "https://pub.dev"
source: hosted
version: "9.3.0"
package_config:
dependency: transitive
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev"
source: hosted
version: "2.2.0"
path:
dependency: transitive
description:
name: path
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.8.3"
version: "1.9.1"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "51f0d2c554cfbc9d6a312ab35152fc77e2f0b758ce9f1a444a3a1e5b8f3c6b7f"
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
url: "https://pub.dev"
source: hosted
version: "2.2.3"
version: "2.3.1"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f"
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
version: "2.6.0"
path_provider_linux:
dependency: transitive
description:
@ -332,18 +452,18 @@ packages:
dependency: transitive
description:
name: path_provider_windows
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.2.1"
version: "2.3.0"
platform:
dependency: transitive
description:
name: platform
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.4"
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
@ -352,14 +472,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pointycastle:
dependency: transitive
description:
name: pointycastle
sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
postgrest:
dependency: transitive
description:
name: postgrest
sha256: b74dc0f57b5dca5ce9f57a54b08110bf41d6fc8a0483c0fec10c79e9aa0fb2bb
sha256: "9d61b3d4a88fcf9424d400127c54d49ed1b56ec30838fc0a33a64f31d4e694cc"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
version: "2.7.0"
provider:
dependency: "direct main"
description:
@ -368,14 +496,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.1.5+1"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
realtime_client:
dependency: transitive
description:
name: realtime_client
sha256: e3089dac2121917cc0c72d42ab056fea0abbaf3c2229048fc50e64bafc731adf
sha256: "7dfccf372d2f55aacfeefb6186f65a06f3ffae383fe042dbeef9d85d33487576"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
version: "2.7.3"
record_use:
dependency: transitive
description:
name: record_use
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
retry:
dependency: transitive
description:
@ -396,199 +540,183 @@ packages:
dependency: "direct main"
description:
name: shared_preferences
sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
url: "https://pub.dev"
source: hosted
version: "2.2.3"
version: "2.5.5"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06"
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
url: "https://pub.dev"
source: hosted
version: "2.2.1"
version: "2.4.23"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c"
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
url: "https://pub.dev"
source: hosted
version: "2.3.5"
version: "2.5.6"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b"
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
version: "2.4.2"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
source: hosted
version: "2.2.1"
version: "2.4.3"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
version: "2.4.1"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.0"
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.11.0"
version: "1.12.1"
storage_client:
dependency: transitive
description:
name: storage_client
sha256: "9f9ed283943313b23a1b27139bb18986e9b152a6d34530232c702c468d98e91a"
sha256: "4801e8ca219a35e51cbb30589aba5306667ae8935b792504595a45273cef0b18"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
version: "2.5.2"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8"
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
version: "1.4.1"
supabase:
dependency: transitive
description:
name: supabase
sha256: c3ebddba69ddcf16d8b78e8c44c4538b0193d1cf944fde3b72eb5b279892a370
sha256: "40e5a8833c8834e140ef53b60a6181849667eba9ca125acb7f8e24c6a769d418"
url: "https://pub.dev"
source: hosted
version: "2.6.3"
version: "2.10.6"
supabase_flutter:
dependency: "direct main"
description:
name: supabase_flutter
sha256: "3b5b5b492e342f63f301605d0c66f6528add285b5744f53c9fd9abd5ffdbce5b"
sha256: c02ce58abcaf86cb8055ad40bfd98bbf5b93fed3b5b56b8220d88ed03842818b
url: "https://pub.dev"
source: hosted
version: "2.8.4"
version: "2.12.4"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
version: "0.7.10"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.3.2"
universal_html:
dependency: transitive
description:
name: universal_html
sha256: "56536254004e24d9d8cfdb7dbbf09b74cf8df96729f38a2f5c238163e3d58971"
url: "https://pub.dev"
source: hosted
version: "2.2.4"
universal_io:
dependency: transitive
description:
name: universal_io
sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad"
url: "https://pub.dev"
source: hosted
version: "2.2.2"
version: "1.4.0"
url_launcher:
dependency: transitive
description:
name: url_launcher
sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
url: "https://pub.dev"
source: hosted
version: "6.2.4"
version: "6.3.2"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745
sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572"
url: "https://pub.dev"
source: hosted
version: "6.3.0"
version: "6.3.29"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03"
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
url: "https://pub.dev"
source: hosted
version: "6.2.4"
version: "6.4.1"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
url: "https://pub.dev"
source: hosted
version: "3.1.1"
version: "3.2.2"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de"
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
version: "3.2.5"
url_launcher_platform_interface:
dependency: transitive
description:
@ -601,66 +729,90 @@ packages:
dependency: transitive
description:
name: url_launcher_web
sha256: "7fd2f55fe86cea2897b963e864dc01a7eb0719ecc65fcef4c1cc3d686d718bb2"
sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.4.2"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
version: "3.1.5"
vector_math:
dependency: transitive
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:
name: vm_service
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev"
source: hosted
version: "15.2.0"
web:
dependency: transitive
description:
name: web
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "0.1.4-beta"
version: "1.1.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
url: "https://pub.dev"
source: hosted
version: "2.4.0"
version: "3.0.3"
win32:
dependency: "direct overridden"
description:
name: win32
sha256: c97defd418eef4ec88c0d1652cdce84b9f7b63dd7198e266d06ac1710d527067
sha256: ba7d5750e3441caa1bbe31d9e516348fcf8dfcb32aa29ef87a844a59f4d1f1d0
url: "https://pub.dev"
source: hosted
version: "5.0.8"
version: "6.1.0"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.0.4"
version: "1.1.0"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
yet_another_json_isolate:
dependency: transitive
description:
name: yet_another_json_isolate
sha256: "56155e9e0002cc51ea7112857bbcdc714d4c35e176d43e4d3ee233009ff410c9"
sha256: fe45897501fa156ccefbfb9359c9462ce5dec092f05e8a56109db30be864f01e
url: "https://pub.dev"
source: hosted
version: "2.0.3"
version: "2.1.0"
sdks:
dart: ">=3.1.0 <4.0.0"
flutter: ">=3.13.0"
dart: ">=3.10.3 <4.0.0"
flutter: ">=3.38.4"

View File

@ -30,16 +30,16 @@ environment:
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
supabase_flutter: ^2.5.0
mqtt_client: ^10.2.0
cupertino_icons: ^1.0.9
supabase_flutter: ^2.12.4
mqtt_client: ^10.11.11
provider: ^6.1.1
fl_chart: 0.63.0
intl: ^0.18.1
shared_preferences: ^2.2.0
fl_chart: ^1.2.0
intl: ^0.20.2
shared_preferences: ^2.5.5
dependency_overrides:
win32: 5.0.8
win32: ^6.1.0
dev_dependencies:
flutter_test:
@ -50,7 +50,7 @@ dev_dependencies:
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^3.0.0
flutter_lints: ^6.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

View File

@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
jni
)
set(PLUGIN_BUNDLED_LIBRARIES)