Komunikasi dua arah real-time — ESP32 push data sensor, Flutter terima langsung tanpa polling.
Polling terus bertanya → WebSocket langsung terima notifikasi dari ESP32
C++ (Arduino)#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <ArduinoJson.h>
const char* ssid = "ESP32_IoT_AP";
const char* password = "12345678";
#define LED_PIN 2
#define LDR_PIN 34
#define BTN_PIN 15
AsyncWebServer server(80);
AsyncWebSocket ws("/ws"); // WebSocket endpoint: ws://192.168.4.1/ws
unsigned long lastSend = 0;
const int SEND_INTERVAL = 500; // kirim data setiap 500ms
// ====================
// WebSocket Event Handler
// ====================
void onWsEvent(AsyncWebSocket *server,
AsyncWebSocketClient *client,
AwsEventType type,
void *arg, uint8_t *data, size_t len) {
switch (type) {
case WS_EVT_CONNECT:
Serial.printf("Client #%u connected\n", client->id());
// Kirim status awal ke client yang baru connect
sendStatus(client);
break;
case WS_EVT_DISCONNECT:
Serial.printf("Client #%u disconnected\n", client->id());
break;
case WS_EVT_DATA: {
// Parse pesan masuk dari Flutter
JsonDocument doc;
DeserializationError err = deserializeJson(doc, data, len);
if (err) return;
const char* action = doc["action"];
if (strcmp(action, "led") == 0) {
bool state = doc["value"];
digitalWrite(LED_PIN, state ? HIGH : LOW);
// Broadcast perubahan ke SEMUA client
broadcastStatus();
}
break;
}
case WS_EVT_ERROR:
Serial.printf("WS Error on client #%u\n", client->id());
break;
case WS_EVT_PONG:
break;
}
}
// Kirim status ke 1 client
void sendStatus(AsyncWebSocketClient *client) {
JsonDocument doc;
doc["led"] = digitalRead(LED_PIN) ? true : false;
doc["cahaya"] = analogRead(LDR_PIN);
doc["uptime"] = millis() / 1000;
String json;
serializeJson(doc, json);
client->text(json);
}
// Broadcast status ke SEMUA client
void broadcastStatus() {
JsonDocument doc;
doc["led"] = digitalRead(LED_PIN) ? true : false;
doc["cahaya"] = analogRead(LDR_PIN);
doc["uptime"] = millis() / 1000;
String json;
serializeJson(doc, json);
ws.textAll(json);
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
pinMode(BTN_PIN, INPUT_PULLUP);
WiFi.softAP(ssid, password);
Serial.print("AP IP: ");
Serial.println(WiFi.softAPIP());
ws.onEvent(onWsEvent);
server.addHandler(&ws);
server.begin();
Serial.println("WebSocket server ready!");
}
void loop() {
// Bersihkan client yang sudah disconnect
ws.cleanupClients();
// Kirim sensor data periodik
if (millis() - lastSend > SEND_INTERVAL) {
lastSend = millis();
broadcastStatus();
}
}
Terminalflutter pub add web_socket_channel
Dartimport 'dart:convert';
import 'package:web_socket_channel/web_socket_channel.dart';
class WsService {
static const String wsUrl = 'ws://192.168.4.1/ws';
WebSocketChannel? _channel;
bool _isConnected = false;
bool get isConnected => _isConnected;
/// Connect ke ESP32 WebSocket server
void connect({
required Function(Map<String, dynamic>) onData,
required Function() onDisconnect,
}) {
try {
_channel = WebSocketChannel.connect(Uri.parse(wsUrl));
_isConnected = true;
_channel!.stream.listen(
(message) {
// Setiap data masuk → parse JSON
final data = jsonDecode(message);
onData(data);
},
onDone: () {
_isConnected = false;
onDisconnect();
},
onError: (error) {
_isConnected = false;
onDisconnect();
},
);
} catch (e) {
_isConnected = false;
}
}
/// Kirim perintah ke ESP32
void send(Map<String, dynamic> data) {
if (_isConnected && _channel != null) {
_channel!.sink.add(jsonEncode(data));
}
}
/// Kirim perintah LED
void setLed(bool isOn) {
send({'action': 'led', 'value': isOn});
}
/// Disconnect
void disconnect() {
_channel?.sink.close();
_isConnected = false;
}
}
Dartimport 'package:flutter/material.dart';
class WsDashboard extends StatefulWidget {
const WsDashboard({super.key});
@override
State<WsDashboard> createState() => _WsDashboardState();
}
class _WsDashboardState extends State<WsDashboard> {
final WsService _ws = WsService();
bool _ledOn = false;
int _cahaya = 0;
int _uptime = 0;
// Simpan history cahaya untuk chart
final List<int> _cahayaHistory = [];
@override
void initState() {
super.initState();
_connectWs();
}
void _connectWs() {
_ws.connect(
onData: (data) {
if (!mounted) return;
setState(() {
_ledOn = data['led'] ?? false;
_cahaya = data['cahaya'] ?? 0;
_uptime = data['uptime'] ?? 0;
_cahayaHistory.add(_cahaya);
if (_cahayaHistory.length > 50) {
_cahayaHistory.removeAt(0);
}
});
},
onDisconnect: () {
if (!mounted) return;
setState(() {});
// Coba reconnect setelah 3 detik
Future.delayed(
const Duration(seconds: 3), () => _connectWs(),
);
},
);
}
@override
void dispose() {
_ws.disconnect();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0f172a),
appBar: AppBar(
title: const Text('WebSocket Dashboard'),
backgroundColor: const Color(0xFF1e293b),
actions: [
Container(
margin: const EdgeInsets.only(right: 16),
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: _ws.isConnected
? Colors.green.withValues(alpha: 0.2)
: Colors.red.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Icon(
_ws.isConnected
? Icons.sensors
: Icons.sensors_off,
color: _ws.isConnected
? Colors.green
: Colors.red,
size: 16,
),
const SizedBox(width: 4),
Text(
_ws.isConnected ? 'LIVE' : 'OFFLINE',
style: TextStyle(
color: _ws.isConnected
? Colors.green
: Colors.red,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// Status cards
Row(
children: [
_sensorCard('☀️', 'Cahaya', '$_cahaya',
Colors.amber),
const SizedBox(width: 12),
_sensorCard('⏱️', 'Uptime', '${_uptime}s',
Colors.cyan),
],
),
const SizedBox(height: 24),
// Mini chart (simple bar visualization)
Container(
height: 80,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: const Color(0xFF1e293b),
borderRadius: BorderRadius.circular(12),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: _cahayaHistory
.map((v) => Expanded(
child: Container(
margin: const EdgeInsets.symmetric(
horizontal: 0.5),
height: (v / 4095) * 64,
decoration: BoxDecoration(
color: Colors.amber
.withValues(alpha: 0.6),
borderRadius:
BorderRadius.circular(1),
),
),
))
.toList(),
),
),
const SizedBox(height: 24),
// LED toggle
GestureDetector(
onTap: () => _ws.setLed(!_ledOn),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.all(36),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _ledOn
? Colors.amber.withValues(alpha: 0.15)
: const Color(0xFF1e293b),
border: Border.all(
color: _ledOn ? Colors.amber : Colors.grey,
width: 3,
),
boxShadow: _ledOn
? [BoxShadow(
color: Colors.amber
.withValues(alpha: 0.3),
blurRadius: 30)]
: [],
),
child: Icon(Icons.lightbulb,
size: 50,
color: _ledOn ? Colors.amber : Colors.grey),
),
),
const SizedBox(height: 12),
Text(
'LED: ${_ledOn ? "ON" : "OFF"}',
style: TextStyle(
color: _ledOn ? Colors.amber : Colors.grey,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
}
Widget _sensorCard(
String emoji, String label, String value, Color color) {
return Expanded(
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF1e293b),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Text(emoji, style: const TextStyle(fontSize: 28)),
const SizedBox(height: 4),
Text(label,
style: TextStyle(
color: Colors.grey[400], fontSize: 12)),
Text(value,
style: TextStyle(
color: color,
fontSize: 22,
fontWeight: FontWeight.bold)),
],
),
),
);
}
}