Fase 2 · Minggu 5

Flutter ↔ ESP32 via USB Serial

Hubungkan Flutter app ke ESP32 menggunakan kabel USB OTG. Kontrol LED dari smartphone!

Konsep USB OTG

USB OTG (On-The-Go) memungkinkan smartphone Android bertindak sebagai host USB, sehingga bisa berkomunikasi langsung dengan ESP32 via kabel.

Android Flutter App USB-C OTG USB Cable ESP32 Serial Listener Alur: 1. Phone → OTG → USB 2. USB → ESP32 Serial 3. Komunikasi 2 arah 4. Baud rate: 115200

Koneksi: Android (USB-C) → OTG Adapter → USB Cable → ESP32

⚠️ Yang dibutuhkan: Smartphone Android dengan USB OTG support + adapter OTG (USB-C ke USB-A atau Micro USB ke USB-A). Tidak semua HP support — cek di pengaturan atau coba langsung.

Setup Flutter Project

1. Buat project baru:

Terminalflutter create usb_led_control
cd usb_led_control

2. Tambahkan dependency di pubspec.yaml:

YAML — pubspec.yamldependencies:
  flutter:
    sdk: flutter
  usb_serial: ^0.5.1

3. Tambahkan permission di android/app/src/main/AndroidManifest.xml:

XML — AndroidManifest.xml<manifest ...>
    <!-- USB Permission -->
    <uses-feature android:name="android.hardware.usb.host" />

    <application ...>
        <activity ...>
            <intent-filter>
                <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
            </intent-filter>
            <meta-data
                android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
                android:resource="@xml/device_filter" />
        </activity>
    </application>
</manifest>

4. Buat file android/app/src/main/res/xml/device_filter.xml:

XML — device_filter.xml<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- CP2102 -->
    <usb-device vendor-id="0x10C4" product-id="0xEA60" />
    <!-- CH340 -->
    <usb-device vendor-id="0x1A86" product-id="0x7523" />
</resources>

Kode ESP32: Serial Listener

Upload kode ini ke ESP32. Ia akan mendengarkan perintah JSON via Serial dan mengontrol LED.

C++ (Arduino)#include <ArduinoJson.h>

#define LED_PIN 2

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);
}

void loop() {
  if (Serial.available()) {
    String input = Serial.readStringUntil('\n');
    input.trim();

    JsonDocument doc;
    DeserializationError err = deserializeJson(doc, input);

    if (!err) {
      const char* cmd = doc["command"];

      if (strcmp(cmd, "ON") == 0) {
        digitalWrite(LED_PIN, HIGH);
        Serial.println("{\"status\":\"OK\",\"led\":\"ON\"}");
      }
      else if (strcmp(cmd, "OFF") == 0) {
        digitalWrite(LED_PIN, LOW);
        Serial.println("{\"status\":\"OK\",\"led\":\"OFF\"}");
      }
      else if (strcmp(cmd, "STATUS") == 0) {
        String state = digitalRead(LED_PIN) ? "ON" : "OFF";
        Serial.println("{\"status\":\"OK\",\"led\":\"" + state + "\"}");
      }
    }
  }
}

Kode Flutter: USB LED Controller

Aplikasi Flutter yang menghubungkan ke ESP32 via USB dan mengirim perintah ON/OFF.

Dart — lib/main.dartimport 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:usb_serial/usb_serial.dart';
import 'package:usb_serial/transaction.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark().copyWith(
        scaffoldBackgroundColor: const Color(0xFF0F172A),
      ),
      home: const UsbControlPage(),
    );
  }
}

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

class _UsbControlPageState extends State<UsbControlPage> {
  UsbPort? _port;
  String _status = "Disconnected";
  bool _isConnected = false;
  bool _isLedOn = false;
  String _lastResponse = "";

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

  Future<void> _connectToDevice() async {
    List<UsbDevice> devices = await UsbSerial.listDevices();

    if (devices.isEmpty) {
      setState(() => _status = "No USB device found");
      return;
    }

    UsbPort? port = await devices[0].create();
    if (port == null) return;

    bool opened = await port.open();
    if (!opened) {
      setState(() => _status = "Failed to open port");
      return;
    }

    await port.setDTR(true);
    await port.setRTS(true);
    await port.setPortParameters(
      115200,
      UsbPort.DATABITS_8,
      UsbPort.STOPBITS_1,
      UsbPort.PARITY_NONE,
    );

    // Listen for data from ESP32
    port.inputStream?.listen((Uint8List data) {
      String response = String.fromCharCodes(data).trim();
      if (response.isNotEmpty) {
        setState(() => _lastResponse = response);
        _parseResponse(response);
      }
    });

    setState(() {
      _port = port;
      _isConnected = true;
      _status = "Connected to ${devices[0].productName}";
    });
  }

  void _parseResponse(String response) {
    try {
      Map<String, dynamic> json = jsonDecode(response);
      if (json['led'] == 'ON') {
        setState(() => _isLedOn = true);
      } else if (json['led'] == 'OFF') {
        setState(() => _isLedOn = false);
      }
    } catch (_) {}
  }

  void _sendCommand(String command) {
    if (_port == null) return;
    String json = jsonEncode({"command": command});
    _port!.write(Uint8List.fromList("$json\n".codeUnits));
  }

  @override
  void dispose() {
    _port?.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('USB LED Controller'),
        backgroundColor: const Color(0xFF1E293B),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // Connection status
            Container(
              padding: const EdgeInsets.symmetric(
                horizontal: 16, vertical: 8),
              decoration: BoxDecoration(
                color: _isConnected
                    ? Colors.green.withAlpha(50)
                    : Colors.red.withAlpha(50),
                borderRadius: BorderRadius.circular(20),
                border: Border.all(
                  color: _isConnected
                      ? Colors.green
                      : Colors.red),
              ),
              child: Text(_status,
                style: TextStyle(
                  color: _isConnected
                      ? Colors.greenAccent
                      : Colors.redAccent)),
            ),
            const SizedBox(height: 40),
            // LED icon
            Icon(Icons.lightbulb,
              size: 120,
              color: _isLedOn ? Colors.amber : Colors.grey[700]),
            const SizedBox(height: 20),
            Text(_isLedOn ? 'LED: ON' : 'LED: OFF',
              style: TextStyle(fontSize: 28,
                fontWeight: FontWeight.bold,
                color: _isLedOn
                    ? Colors.greenAccent
                    : Colors.redAccent)),
            const SizedBox(height: 40),
            // Buttons
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: _isConnected
                      ? () => _sendCommand('ON') : null,
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.greenAccent,
                    padding: const EdgeInsets.symmetric(
                      horizontal: 32, vertical: 16)),
                  child: const Text('ON',
                    style: TextStyle(color: Colors.black,
                      fontSize: 18,
                      fontWeight: FontWeight.bold)),
                ),
                const SizedBox(width: 20),
                ElevatedButton(
                  onPressed: _isConnected
                      ? () => _sendCommand('OFF') : null,
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.redAccent,
                    padding: const EdgeInsets.symmetric(
                      horizontal: 32, vertical: 16)),
                  child: const Text('OFF',
                    style: TextStyle(color: Colors.white,
                      fontSize: 18,
                      fontWeight: FontWeight.bold)),
                ),
              ],
            ),
            const SizedBox(height: 30),
            // Response
            Text('Response: $_lastResponse',
              style: const TextStyle(
                color: Colors.grey, fontSize: 12)),
          ],
        ),
      ),
    );
  }
}
USB LED Controller Connected ✓ 💡 ON OFF USB ESP32 LED Flow: 1. Tap "ON" 2. {"command":"ON"} 3. ESP32 nyalakan LED 4. {"status":"OK"} 5. UI update ke ON

Full flow: Flutter App → USB → ESP32 → LED ON → Response → UI Update

Checklist Minggu 5