add iot.cpp logics
This commit is contained in:
parent
dba2f0e4a6
commit
9a68fd1492
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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,6 +144,9 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||
}
|
||||
|
||||
Widget _buildModeSelector() {
|
||||
return ValueListenableBuilder<bool>(
|
||||
valueListenable: _mqttService.isAutoMode,
|
||||
builder: (context, isAuto, _) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
|
|
@ -115,12 +163,12 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() => _isAutoMode = true),
|
||||
onTap: () => _mqttService.publishControl('mode', 'auto'),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: _isAutoMode ? AppTheme.primaryGreen : Colors.transparent,
|
||||
color: isAuto ? AppTheme.primaryGreen : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
|
|
@ -128,7 +176,7 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||
'Otomatis',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _isAutoMode ? Colors.white : AppTheme.textLight,
|
||||
color: isAuto ? Colors.white : AppTheme.textLight,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -136,12 +184,12 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||
),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() => _isAutoMode = false),
|
||||
onTap: () => _mqttService.publishControl('mode', 'manual'),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: !_isAutoMode ? AppTheme.primaryGreen : Colors.transparent,
|
||||
color: !isAuto ? AppTheme.primaryGreen : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
|
|
@ -149,7 +197,7 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||
'Manual',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: !_isAutoMode ? Colors.white : AppTheme.textLight,
|
||||
color: !isAuto ? Colors.white : AppTheme.textLight,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -158,24 +206,45 @@ class _DashboardPageState extends State<DashboardPage> {
|
|||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDeviceControls() {
|
||||
return ValueListenableBuilder<bool>(
|
||||
valueListenable: _mqttService.isAutoMode,
|
||||
builder: (context, isAuto, _) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildDeviceTile(
|
||||
ValueListenableBuilder<bool>(
|
||||
valueListenable: _mqttService.isPumpOn,
|
||||
builder: (context, isOn, _) {
|
||||
return _buildDeviceTile(
|
||||
title: 'Pompa Air',
|
||||
isOn: _pumpStatus,
|
||||
onChanged: _isAutoMode ? null : (val) => setState(() => _isPumpManual = val),
|
||||
isOn: isOn,
|
||||
onChanged: isAuto ? null : (val) {
|
||||
_mqttService.publishControl('pump', val ? 'on' : 'off');
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildDeviceTile(
|
||||
ValueListenableBuilder<bool>(
|
||||
valueListenable: _mqttService.isLightOn,
|
||||
builder: (context, isOn, _) {
|
||||
return _buildDeviceTile(
|
||||
title: 'Lampu Pemanas',
|
||||
isOn: _lightStatus,
|
||||
onChanged: _isAutoMode ? null : (val) => setState(() => _isLightManual = val),
|
||||
isOn: isOn,
|
||||
onChanged: isAuto ? null : (val) {
|
||||
_mqttService.publishControl('light', val ? 'on' : 'off');
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDeviceTile({
|
||||
|
|
@ -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',
|
||||
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: 14,
|
||||
color: AppTheme.textLight,
|
||||
fontSize: 13,
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.check_circle,
|
||||
color: AppTheme.primaryGreen,
|
||||
// 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,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
16
pubspec.lock
16
pubspec.lock
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
Loading…
Reference in New Issue