add iot.cpp logics

This commit is contained in:
oxel 2026-04-23 11:56:25 +07:00
parent dba2f0e4a6
commit 9a68fd1492
8 changed files with 656 additions and 79 deletions

View File

@ -5,7 +5,9 @@
<application
android:label="monitoring_jamur"
android:name="${applicationName}"
android:icon="@mipmap/launcher_icon">
android:icon="@mipmap/launcher_icon"
android:usesCleartextTraffic="true"
android:networkSecurityConfig="@xml/network_security_config">
<activity
android:name=".MainActivity"
android:exported="true"

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">broker.emqx.io</domain>
<domain includeSubdomains="true">broker.hivemq.com</domain>
<domain includeSubdomains="true">3.120.100.223</domain>
</domain-config>
</network-security-config>

174
iot.cpp Normal file
View File

@ -0,0 +1,174 @@
#include <WiFi.h>
#include <PubSubClient.h>
#include <DHT.h>
// ================= WIFI =================
const char* ssid = "Shanum_4G";
const char* password = "12345678";
// ================= MQTT =================
const char* mqtt_server = "broker.hivemq.com";
WiFiClient espClient;
PubSubClient client(espClient);
// ================= DHT =================
#define DHTPIN 4
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);
// ================= RELAY =================
#define RELAY1 27
#define RELAY2 26
// Relay aktif LOW
#define RELAY_ON LOW
#define RELAY_OFF HIGH
// ================= THRESHOLD =================
float batasPanas = 70.0;
float batasLembab = 90.0;
// ================= GLOBAL STATE =================
unsigned long lastMsg = 0;
String statusRelay = "INIT";
bool isAutoMode = true;
bool manualPump = false;
bool manualLight = false;
// ================= WIFI =================
void setup_wifi() {
delay(10);
Serial.println("Connecting to WiFi...");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi connected!");
}
// ================= MQTT CALLBACK =================
void callback(char* topic, byte* payload, unsigned int length) {
String message = "";
for (int i = 0; i < length; i++) {
message += (char)payload[i];
}
Serial.print("Message arrived [");
Serial.print(topic);
Serial.print("] ");
Serial.println(message);
if (String(topic) == "esp32/control/mode") {
isAutoMode = (message == "auto");
Serial.println(isAutoMode ? "Mode: AUTO" : "Mode: MANUAL");
}
else if (String(topic) == "esp32/control/pump") {
manualPump = (message == "on");
Serial.println(manualPump ? "Manual Pump: ON" : "Manual Pump: OFF");
}
else if (String(topic) == "esp32/control/light") {
manualLight = (message == "on");
Serial.println(manualLight ? "Manual Light: ON" : "Manual Light: OFF");
}
}
// ================= MQTT RECONNECT =================
void reconnect() {
while (!client.connected()) {
Serial.print("Connecting MQTT...");
if (client.connect("ESP32Client", "esp32/status", 0, true, "offline")) {
Serial.println("connected");
client.publish("esp32/status", "online", true);
// Subscribe to control topics
client.subscribe("esp32/control/mode");
client.subscribe("esp32/control/pump");
client.subscribe("esp32/control/light");
} else {
Serial.print("failed, rc=");
Serial.print(client.state());
Serial.println(" coba lagi...");
delay(2000);
}
}
}
void setup() {
Serial.begin(115200);
pinMode(RELAY1, OUTPUT);
pinMode(RELAY2, OUTPUT);
digitalWrite(RELAY1, RELAY_OFF);
digitalWrite(RELAY2, RELAY_OFF);
dht.begin();
setup_wifi();
client.setServer(mqtt_server, 1883);
client.setCallback(callback);
}
void loop() {
if (!client.connected()) {
reconnect();
}
client.loop();
unsigned long now = millis();
if (now - lastMsg > 2000) {
lastMsg = now;
float suhu = dht.readTemperature();
float kelembapan = dht.readHumidity();
if (isnan(suhu) || isnan(kelembapan)) {
Serial.println("Gagal baca DHT!");
return;
}
// ================= CONTROL LOGIC =================
if (isAutoMode) {
// Automatic Mode (Sensor Based)
bool kondisiLembab = (kelembapan > batasLembab);
bool kondisiPanas = (suhu > batasPanas);
digitalWrite(RELAY1, kondisiLembab ? RELAY_ON : RELAY_OFF);
digitalWrite(RELAY2, kondisiPanas ? RELAY_ON : RELAY_OFF);
if (kondisiPanas && kondisiLembab) statusRelay = "PANAS+LEMBAB";
else if (kondisiPanas) statusRelay = "PANAS";
else if (kondisiLembab) statusRelay = "LEMBAB";
else statusRelay = "NORMAL";
} else {
// Manual Mode (App Based)
digitalWrite(RELAY1, manualPump ? RELAY_ON : RELAY_OFF);
digitalWrite(RELAY2, manualLight ? RELAY_ON : RELAY_OFF);
statusRelay = "MANUAL";
}
// ================= STATUS REPORTING =================
bool currentPump = (digitalRead(RELAY1) == RELAY_ON);
bool currentLight = (digitalRead(RELAY2) == RELAY_ON);
client.publish("esp32/dht/suhu", String(suhu).c_str());
client.publish("esp32/dht/kelembapan", String(kelembapan).c_str());
client.publish("esp32/relay/status", statusRelay.c_str());
client.publish("esp32/status/pump", currentPump ? "on" : "off", true);
client.publish("esp32/status/light", currentLight ? "on" : "off", true);
client.publish("esp32/status/mode", isAutoMode ? "auto" : "manual", true);
// Serial Debug
Serial.print("Mode: "); Serial.print(isAutoMode ? "AUTO" : "MANUAL");
Serial.print(" | Hum: "); Serial.print(kelembapan);
Serial.print("% | Pump: "); Serial.print(currentPump ? "ON" : "OFF");
Serial.print(" | Light: "); Serial.println(currentLight ? "ON" : "OFF");
}
}

View File

@ -0,0 +1,189 @@
import 'dart:async';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:mqtt_client/mqtt_client.dart';
import 'package:mqtt_client/mqtt_server_client.dart';
enum AppMqttStatus { disconnected, connecting, connected, error }
class MqttService {
static final MqttService _instance = MqttService._internal();
factory MqttService() => _instance;
MqttService._internal();
MqttServerClient? client;
StreamSubscription? _subscription;
bool _isInitializing = false;
final ValueNotifier<bool> isConnected = ValueNotifier(false);
final ValueNotifier<AppMqttStatus> connectionState =
ValueNotifier(AppMqttStatus.disconnected);
final ValueNotifier<bool> isHardwareOnline = ValueNotifier(false);
final ValueNotifier<double> humidity = ValueNotifier(1.0);
final ValueNotifier<String> relayStatus = ValueNotifier("NORMAL");
final ValueNotifier<bool> isAutoMode = ValueNotifier(true);
final ValueNotifier<bool> isPumpOn = ValueNotifier(false);
final ValueNotifier<bool> isLightOn = ValueNotifier(false);
Future<void> init() async {
if (_isInitializing) return;
_isInitializing = true;
connectionState.value = AppMqttStatus.connecting;
final clientId = 'mj_debug_${Random().nextInt(999999)}';
// FINAL ROBUST CONFIG: Back to EMQX Port 1883
client = MqttServerClient('broker.emqx.io', clientId);
client!.port = 1883;
client!.setProtocolV311();
client!.keepAlivePeriod = 60;
client!.autoReconnect = true;
client!.resubscribeOnAutoReconnect = true;
client!.logging(on: true);
client!.onConnected = _onConnected;
client!.onDisconnected = _onDisconnected;
client!.onSubscribed = _onSubscribed;
client!.onAutoReconnect = () => debugPrint('MQTT: Attempting auto-reconnect...');
client!.onAutoReconnected = () => debugPrint('MQTT: Auto-reconnect successful!');
client!.connectionMessage = MqttConnectMessage()
.withClientIdentifier(clientId)
.startClean();
try {
debugPrint('MQTT: Connecting as $clientId');
final status = await client!.connect();
if (status?.state != MqttConnectionState.connected) {
debugPrint('MQTT: FAILED -> ${status?.state}');
connectionState.value = AppMqttStatus.error;
client!.disconnect();
return;
}
debugPrint('MQTT: CONNECTED OK');
} catch (e) {
debugPrint('MQTT: EXCEPTION $e');
connectionState.value = AppMqttStatus.error;
client?.disconnect();
} finally {
_isInitializing = false;
}
}
void _onConnected() {
debugPrint('MQTT: Connected ✅');
isConnected.value = true;
connectionState.value = AppMqttStatus.connected;
_subscribeAll();
_subscription?.cancel();
_subscription = client!.updates!.listen((events) {
final recMess = events[0].payload as MqttPublishMessage;
final topic = events[0].topic;
final payload = MqttPublishPayload.bytesToStringAsString(
recMess.payload.message,
);
debugPrint('MQTT RX [$topic] -> $payload');
_processMessage(topic, payload);
});
}
void _subscribeAll() {
const topics = [
"esp32/dht/kelembapan",
"esp32/relay/status",
"esp32/status",
"esp32/status/mode",
"esp32/status/pump",
"esp32/status/light",
];
for (final t in topics) {
debugPrint('MQTT: Subscribing $t');
client!.subscribe(t, MqttQos.atLeastOnce);
}
}
void _processMessage(String topic, String payload) {
final pt = payload.trim().toLowerCase();
debugPrint('MQTT RX DEBUG: Topic=$topic, Payload=$pt');
switch (topic) {
case "esp32/dht/kelembapan":
double val = double.tryParse(payload) ?? humidity.value;
humidity.value = val < 1.0 ? 1.0 : val;
break;
case "esp32/relay/status":
relayStatus.value = payload.toUpperCase();
break;
case "esp32/status":
isHardwareOnline.value = (pt == "online");
debugPrint('MQTT: Hardware -> ${isHardwareOnline.value}');
break;
case "esp32/status/mode":
isAutoMode.value = (pt == "auto");
break;
case "esp32/status/pump":
isPumpOn.value = (pt == "on");
break;
case "esp32/status/light":
isLightOn.value = (pt == "on");
break;
}
}
void publishControl(String subTopic, String value) {
if (client == null ||
client!.connectionStatus?.state != MqttConnectionState.connected) {
debugPrint('MQTT: publish blocked (not connected)');
return;
}
final builder = MqttClientPayloadBuilder()..addString(value);
final topic = "esp32/status/$subTopic";
client!.publishMessage(
topic,
MqttQos.atLeastOnce,
builder.payload!,
retain: true,
);
debugPrint('MQTT TX [$topic] -> $value');
}
void _onDisconnected() {
debugPrint('MQTT: Disconnected ❌');
isConnected.value = false;
isHardwareOnline.value = false;
if (connectionState.value != AppMqttStatus.error) {
connectionState.value = AppMqttStatus.disconnected;
}
}
void _onSubscribed(String topic) {
debugPrint('MQTT: Subscribed $topic');
}
void disconnect() {
_subscription?.cancel();
client?.disconnect();
}
}

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:monitoring_jamur/core/theme/app_theme.dart';
import 'package:monitoring_jamur/features/home/presentation/pages/statistics_page.dart';
import 'package:monitoring_jamur/core/services/mqtt_service.dart';
import 'dart:math' as math;
class DashboardPage extends StatefulWidget {
@ -12,7 +13,7 @@ class DashboardPage extends StatefulWidget {
class _DashboardPageState extends State<DashboardPage> {
// Demo humidity value
double _humidity = 85.0;
double _humidity = 1;
bool _isAutoMode = true;
bool _isPumpManual = false;
bool _isLightManual = false;
@ -20,6 +21,50 @@ class _DashboardPageState extends State<DashboardPage> {
bool get _pumpStatus => _isAutoMode ? (_humidity < 80) : _isPumpManual;
bool get _lightStatus => _isAutoMode ? (_humidity > 90) : _isLightManual;
final MqttService _mqttService = MqttService();
@override
void initState() {
super.initState();
_mqttService.init();
// Listen to all changes
_mqttService.isConnected.addListener(_onMqttChanged);
_mqttService.isHardwareOnline.addListener(_onMqttChanged);
_mqttService.humidity.addListener(_onMqttChanged);
_mqttService.relayStatus.addListener(_onMqttChanged);
_mqttService.isAutoMode.addListener(_onMqttChanged);
_mqttService.isPumpOn.addListener(_onMqttChanged);
_mqttService.isLightOn.addListener(_onMqttChanged);
}
@override
void dispose() {
_mqttService.isConnected.removeListener(_onMqttChanged);
_mqttService.isHardwareOnline.removeListener(_onMqttChanged);
_mqttService.humidity.removeListener(_onMqttChanged);
_mqttService.relayStatus.removeListener(_onMqttChanged);
_mqttService.isAutoMode.removeListener(_onMqttChanged);
_mqttService.isPumpOn.removeListener(_onMqttChanged);
_mqttService.isLightOn.removeListener(_onMqttChanged);
super.dispose();
}
void _onMqttChanged() {
if (mounted) {
setState(() {
_humidity = _mqttService.humidity.value;
_isAutoMode = _mqttService.isAutoMode.value;
// In Auto mode, we follow the status reported by hardware
// In Manual mode, we show what we set locally (which will be confirmed by hardware status later)
if (_isAutoMode) {
_isPumpManual = _mqttService.isPumpOn.value;
_isLightManual = _mqttService.isLightOn.value;
}
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -99,82 +144,106 @@ class _DashboardPageState extends State<DashboardPage> {
}
Widget _buildModeSelector() {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(5),
blurRadius: 10,
offset: const Offset(0, 4),
return ValueListenableBuilder<bool>(
valueListenable: _mqttService.isAutoMode,
builder: (context, isAuto, _) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(5),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
],
),
child: Row(
children: [
Expanded(
child: GestureDetector(
onTap: () => setState(() => _isAutoMode = true),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: _isAutoMode ? AppTheme.primaryGreen : Colors.transparent,
borderRadius: BorderRadius.circular(16),
),
alignment: Alignment.center,
child: Text(
'Otomatis',
style: TextStyle(
fontWeight: FontWeight.bold,
color: _isAutoMode ? Colors.white : AppTheme.textLight,
child: Row(
children: [
Expanded(
child: GestureDetector(
onTap: () => _mqttService.publishControl('mode', 'auto'),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: isAuto ? AppTheme.primaryGreen : Colors.transparent,
borderRadius: BorderRadius.circular(16),
),
alignment: Alignment.center,
child: Text(
'Otomatis',
style: TextStyle(
fontWeight: FontWeight.bold,
color: isAuto ? Colors.white : AppTheme.textLight,
),
),
),
),
),
),
),
Expanded(
child: GestureDetector(
onTap: () => setState(() => _isAutoMode = false),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: !_isAutoMode ? AppTheme.primaryGreen : Colors.transparent,
borderRadius: BorderRadius.circular(16),
),
alignment: Alignment.center,
child: Text(
'Manual',
style: TextStyle(
fontWeight: FontWeight.bold,
color: !_isAutoMode ? Colors.white : AppTheme.textLight,
Expanded(
child: GestureDetector(
onTap: () => _mqttService.publishControl('mode', 'manual'),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: !isAuto ? AppTheme.primaryGreen : Colors.transparent,
borderRadius: BorderRadius.circular(16),
),
alignment: Alignment.center,
child: Text(
'Manual',
style: TextStyle(
fontWeight: FontWeight.bold,
color: !isAuto ? Colors.white : AppTheme.textLight,
),
),
),
),
),
),
],
),
],
),
);
},
);
}
Widget _buildDeviceControls() {
return Column(
children: [
_buildDeviceTile(
title: 'Pompa Air',
isOn: _pumpStatus,
onChanged: _isAutoMode ? null : (val) => setState(() => _isPumpManual = val),
),
const SizedBox(height: 16),
_buildDeviceTile(
title: 'Lampu Pemanas',
isOn: _lightStatus,
onChanged: _isAutoMode ? null : (val) => setState(() => _isLightManual = val),
),
],
return ValueListenableBuilder<bool>(
valueListenable: _mqttService.isAutoMode,
builder: (context, isAuto, _) {
return Column(
children: [
ValueListenableBuilder<bool>(
valueListenable: _mqttService.isPumpOn,
builder: (context, isOn, _) {
return _buildDeviceTile(
title: 'Pompa Air',
isOn: isOn,
onChanged: isAuto ? null : (val) {
_mqttService.publishControl('pump', val ? 'on' : 'off');
},
);
},
),
const SizedBox(height: 16),
ValueListenableBuilder<bool>(
valueListenable: _mqttService.isLightOn,
builder: (context, isOn, _) {
return _buildDeviceTile(
title: 'Lampu Pemanas',
isOn: isOn,
onChanged: isAuto ? null : (val) {
_mqttService.publishControl('light', val ? 'on' : 'off');
},
);
},
),
],
);
},
);
}
@ -283,7 +352,8 @@ class _DashboardPageState extends State<DashboardPage> {
Widget _buildStatusCard() {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
constraints: const BoxConstraints(minHeight: 104), // Fixed minimum height
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
decoration: BoxDecoration(
color: AppTheme.surfaceWhite,
borderRadius: BorderRadius.circular(24),
@ -296,7 +366,9 @@ class _DashboardPageState extends State<DashboardPage> {
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Fixed size image container
Container(
width: 64,
height: 64,
@ -311,32 +383,104 @@ class _DashboardPageState extends State<DashboardPage> {
),
),
const SizedBox(width: 20),
const Expanded(
// Scroll-stable text column
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
const Text(
'Monitor Jamur',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppTheme.primaryGreen,
height: 1.2, // Consistent leading
),
),
Text(
'Active & Monitoring',
style: TextStyle(
fontSize: 14,
color: AppTheme.textLight,
),
const SizedBox(height: 4),
ValueListenableBuilder<AppMqttStatus>(
valueListenable: _mqttService.connectionState,
builder: (context, state, _) {
return ValueListenableBuilder<bool>(
valueListenable: _mqttService.isHardwareOnline,
builder: (context, hwOnline, _) {
String statusText = "";
Color statusColor = Colors.red;
if (state == AppMqttStatus.connecting) {
statusText = "Connecting to Broker...";
statusColor = Colors.orange;
} else if (state == AppMqttStatus.connected) {
if (hwOnline) {
statusText = "All Online ✅";
statusColor = AppTheme.primaryGreen;
} else {
statusText = "Broker OK (Hardware Offline)";
statusColor = Colors.blue;
}
} else if (state == AppMqttStatus.error) {
statusText = "Connection Error ❌";
statusColor = Colors.red;
} else {
statusText = "MQTT Offline";
statusColor = Colors.red;
}
return SizedBox(
height: 20,
child: Text(
statusText,
style: TextStyle(
fontSize: 13,
color: statusColor,
fontWeight: FontWeight.bold,
height: 1.0,
),
),
);
},
);
},
),
],
),
),
const Icon(
Icons.check_circle,
color: AppTheme.primaryGreen,
size: 28,
// Fixed width icon slot
Container(
width: 32,
alignment: Alignment.center,
child: ValueListenableBuilder<AppMqttStatus>(
valueListenable: _mqttService.connectionState,
builder: (context, state, _) {
return ValueListenableBuilder<bool>(
valueListenable: _mqttService.isHardwareOnline,
builder: (context, hwOnline, _) {
IconData iconData = Icons.offline_bolt_rounded;
Color iconColor = Colors.red;
if (state == AppMqttStatus.connecting) {
iconData = Icons.hourglass_empty_rounded;
iconColor = Colors.orange;
} else if (state == AppMqttStatus.connected) {
if (hwOnline) {
iconData = Icons.check_circle;
iconColor = AppTheme.primaryGreen;
} else {
iconData = Icons.wifi_tethering_rounded;
iconColor = Colors.blue;
}
}
return Icon(
iconData,
color: iconColor,
size: 28,
);
},
);
},
),
),
],
),

View File

@ -145,6 +145,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.4.0"
event_bus:
dependency: transitive
description:
name: event_bus
sha256: "1a55e97923769c286d295240048fc180e7b0768902c3c2e869fe059aafa15304"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
fake_async:
dependency: transitive
description:
@ -360,6 +368,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0"
mqtt_client:
dependency: "direct main"
description:
name: mqtt_client
sha256: "41c8edd3bc8efc80c1c8ebfb40081c24d12d13085faca96b9280a624eca2d893"
url: "https://pub.dev"
source: hosted
version: "10.11.11"
native_toolchain_c:
dependency: transitive
description:

View File

@ -37,6 +37,7 @@ dependencies:
supabase_flutter: ^2.8.1
google_fonts: ^6.2.1
shared_preferences: ^2.5.5
mqtt_client: ^10.11.11
dev_dependencies:
flutter_test:

View File

@ -0,0 +1,38 @@
import 'dart:io';
import 'dart:math';
import 'package:mqtt_client/mqtt_client.dart';
import 'package:mqtt_client/mqtt_server_client.dart';
Future<void> main() async {
print('--- MQTT WS TEST (WITH PREFIX) START ---');
final String clientId = 'mj_test_${Random().nextInt(999)}';
// WAJIB pakai ws:// agar library tidak error
final client = MqttServerClient.withPort('ws://broker.hivemq.com', clientId, 8000);
client.useWebSocket = true;
client.keepAlivePeriod = 60;
client.logging(on: true);
final connMess = MqttConnectMessage()
.withClientIdentifier(clientId)
.startClean()
.withWillQos(MqttQos.atLeastOnce);
client.connectionMessage = connMess;
try {
print('Connecting to ws://broker.hivemq.com:8000...');
await client.connect();
if (client.connectionStatus?.state == MqttConnectionState.connected) {
print('✅✅✅ YES! CONNECTED VIA WEBSOCKET! ✅✅✅');
client.disconnect();
} else {
print('❌ FAILED: State is ${client.connectionStatus?.state}');
}
} catch (e) {
print('❌ EXCEPTION: $e');
}
print('--- TEST END ---');
exit(0);
}