Fase 4 · Minggu 12

Flutter HTTP ↔ ESP32 AP

Flutter berkomunikasi dengan ESP32 via HTTP REST API di jaringan lokal.

Arsitektur Komunikasi HTTP

📱 Flutter App http package GET /api/status POST /api/led JSON encode/decode dart:convert HTTP GET JSON resp HTTP POST WiFi (192.168.4.x) ESP32 AP ESPAsyncWebServer IP: 192.168.4.1 Port: 80 ArduinoJson LED + Sensor HTTP Flow 1. Flutter → GET 2. ESP32 → JSON response 3. Flutter → POST + body 4. ESP32 → aksi + confirm Polling setiap 1-2 detik untuk update sensor data

Flutter HTTP Client ↔ ESP32 HTTP Server (lokal network, tanpa internet)

Flutter: Setup Package HTTP

Terminalflutter pub add http

pubspec.yaml

YAMLdependencies:
  flutter:
    sdk: flutter
  http: ^1.2.0

Android: Izinkan HTTP (cleartext)

Buka android/app/src/main/AndroidManifest.xml:

XML<manifest ...>
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />

    <application
        android:usesCleartextTraffic="true"
        ...>
⚠️ Penting: android:usesCleartextTraffic="true" diperlukan karena ESP32 menggunakan HTTP (bukan HTTPS). Ini hanya untuk development / jaringan lokal.

Flutter: ESP32 HTTP Service

Dartimport 'dart:convert';
import 'package:http/http.dart' as http;

class Esp32Service {
  // IP default ESP32 AP mode
  static const String baseUrl = 'http://192.168.4.1';
  static const Duration timeout = Duration(seconds: 5);

  /// GET /api/status — baca status sensor + LED
  static Future<Map<String, dynamic>?> getStatus() async {
    try {
      final response = await http
          .get(Uri.parse('$baseUrl/api/status'))
          .timeout(timeout);

      if (response.statusCode == 200) {
        return jsonDecode(response.body);
      }
    } catch (e) {
      print('Error getStatus: $e');
    }
    return null;
  }

  /// POST /api/led — kontrol LED ON/OFF
  static Future<bool> setLed(bool isOn) async {
    try {
      final response = await http
          .post(
            Uri.parse('$baseUrl/api/led'),
            headers: {'Content-Type': 'application/json'},
            body: jsonEncode({'state': isOn ? 'ON' : 'OFF'}),
          )
          .timeout(timeout);

      return response.statusCode == 200;
    } catch (e) {
      print('Error setLed: $e');
    }
    return false;
  }

  /// Cek apakah ESP32 online
  static Future<bool> ping() async {
    try {
      final response = await http
          .get(Uri.parse('$baseUrl/api/status'))
          .timeout(const Duration(seconds: 2));
      return response.statusCode == 200;
    } catch (_) {
      return false;
    }
  }
}

Flutter: WiFi Controller UI

Dartimport 'dart:async';
import 'package:flutter/material.dart';

// import esp32_service.dart dari step sebelumnya

class WifiControlPage extends StatefulWidget {
  const WifiControlPage({super.key});
  @override
  State<WifiControlPage> createState() => _WifiControlPageState();
}

class _WifiControlPageState extends State<WifiControlPage> {
  bool _isConnected = false;
  bool _ledOn = false;
  int _cahaya = 0;
  int _uptime = 0;
  int _clients = 0;
  Timer? _pollTimer;

  @override
  void initState() {
    super.initState();
    _checkConnection();
  }

  @override
  void dispose() {
    _pollTimer?.cancel();
    super.dispose();
  }

  // Cek koneksi & mulai polling
  Future<void> _checkConnection() async {
    final online = await Esp32Service.ping();
    setState(() => _isConnected = online);

    if (online) {
      _fetchStatus();
      // Polling setiap 2 detik
      _pollTimer?.cancel();
      _pollTimer = Timer.periodic(
        const Duration(seconds: 2),
        (_) => _fetchStatus(),
      );
    }
  }

  // Ambil data sensor dari ESP32
  Future<void> _fetchStatus() async {
    final data = await Esp32Service.getStatus();
    if (data != null && mounted) {
      setState(() {
        _ledOn = data['led'] == 'ON';
        _cahaya = data['cahaya'] ?? 0;
        _uptime = data['uptime'] ?? 0;
        _clients = data['clients'] ?? 0;
      });
    }
  }

  // Toggle LED
  Future<void> _toggleLed() async {
    final success = await Esp32Service.setLed(!_ledOn);
    if (success) {
      setState(() => _ledOn = !_ledOn);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFF0f172a),
      appBar: AppBar(
        title: const Text('WiFi Controller'),
        backgroundColor: const Color(0xFF1e293b),
        actions: [
          Icon(
            _isConnected ? Icons.wifi : Icons.wifi_off,
            color: _isConnected ? Colors.green : Colors.red,
          ),
          const SizedBox(width: 16),
        ],
      ),
      body: _isConnected ? _buildDashboard() : _buildOffline(),
    );
  }

  Widget _buildOffline() {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const Icon(Icons.wifi_off, size: 80, color: Colors.red),
          const SizedBox(height: 16),
          const Text(
            'Tidak terhubung ke ESP32',
            style: TextStyle(color: Colors.white, fontSize: 18),
          ),
          const SizedBox(height: 8),
          Text(
            'Pastikan HP terkoneksi ke WiFi\n"ESP32_IoT_AP"',
            textAlign: TextAlign.center,
            style: TextStyle(color: Colors.grey[500]),
          ),
          const SizedBox(height: 24),
          ElevatedButton.icon(
            onPressed: _checkConnection,
            icon: const Icon(Icons.refresh),
            label: const Text('Coba Lagi'),
          ),
        ],
      ),
    );
  }

  Widget _buildDashboard() {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        children: [
          // Status cards
          Row(
            children: [
              _card('☀️ Cahaya', '$_cahaya', Colors.amber),
              const SizedBox(width: 12),
              _card('⏱️ Uptime', '${_uptime}s', Colors.cyan),
              const SizedBox(width: 12),
              _card('📱 Clients', '$_clients', Colors.purple),
            ],
          ),
          const SizedBox(height: 32),

          // LED Control
          GestureDetector(
            onTap: _toggleLed,
            child: Container(
              padding: const EdgeInsets.all(40),
              decoration: BoxDecoration(
                shape: BoxShape.circle,
                color: _ledOn
                    ? Colors.amber.withValues(alpha: 0.2)
                    : const Color(0xFF1e293b),
                border: Border.all(
                  color: _ledOn ? Colors.amber : Colors.grey,
                  width: 3,
                ),
              ),
              child: Icon(
                Icons.lightbulb,
                size: 60,
                color: _ledOn ? Colors.amber : Colors.grey,
              ),
            ),
          ),
          const SizedBox(height: 16),
          Text(
            'LED: ${_ledOn ? "ON" : "OFF"}',
            style: TextStyle(
              color: _ledOn ? Colors.amber : Colors.grey,
              fontSize: 20,
              fontWeight: FontWeight.bold,
            ),
          ),
          const Text(
            'Tap untuk toggle',
            style: TextStyle(color: Colors.grey),
          ),
        ],
      ),
    );
  }

  Widget _card(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),
          border: Border.all(color: color.withValues(alpha: 0.3)),
        ),
        child: Column(
          children: [
            Text(label,
                style: TextStyle(color: color, fontSize: 12)),
            const SizedBox(height: 8),
            Text(value,
                style: TextStyle(
                    color: Colors.white,
                    fontSize: 24,
                    fontWeight: FontWeight.bold)),
          ],
        ),
      ),
    );
  }
}

Cara Kerja Polling

Flutter ESP32 GET /api/status {"led":"OFF","cahaya":2048} 2 detik GET /api/status {"led":"OFF","cahaya":2100} User tap POST /api/led {"state":"ON"} {"status":"OK"}

Sequence Diagram: Polling + kontrol LED via HTTP

ℹ️ Polling vs Push: HTTP bersifat request-response — Flutter harus terus bertanya (polling) setiap 2 detik. Minggu depan kita belajar WebSocket yang bisa "push" data otomatis dari ESP32!

Checklist Minggu 12