The Verdict: Protocol Decision Tree

When bridging an ESP8266 to an Android Studio application, you must choose a communication protocol. While raw TCP sockets and MQTT are viable, HTTP REST over local Wi-Fi is the definitive default pick for 90% of hobbyist and commercial IoT dashboards. It requires no external broker, integrates natively with Android's OkHttp library, and is trivial to debug via a standard web browser.

ProtocolChoose When...Avoid When...Android Library
HTTP REST (Default Pick)You need standard LAN control, easy browser debugging, and stateless sensor polling.You need sub-10ms latency or push notifications without polling.OkHttp / Retrofit
Raw TCP SocketsYou are building a high-frequency oscilloscope or ultra-low latency motor controller.You want automatic reconnection handling and structured data payloads.java.net.Socket
MQTTYou are scaling to multiple devices, routing through a cloud broker, or need push updates.You want a zero-infrastructure local-only setup without running a Mosquitto broker.Eclipse Paho
Decision Path: If your project involves a single ESP8266 sending sensor data to a single Android phone on the same local network, stop evaluating and use HTTP REST. It minimizes infrastructure and maximizes debuggability.

Hardware Parts List and Pin Mapping

This build targets the NodeMCU v3 (LoLin variant) featuring the ESP-12E module. It includes a built-in CP2102 USB-to-serial chip, meaning you do not need an external FTDI programmer. We will read a 10k NTC thermistor to give the Android app real sensor data to fetch.

Bill of Materials

  • Microcontroller: NodeMCU v3 (LoLin) ESP8266 Development Board
  • Sensor: 10k NTC Thermistor (e.g., Vishay NTCLE100E3103)
  • Resistor: 10kΩ 1/4W through-hole (for voltage divider pull-up)
  • Android Device: Any phone running Android 12 (API 31) or higher

Pin Mapping Table

NodeMCU Silk ScreenESP8266 GPIOComponentNotes
A0ADC0Thermistor Divider MidpointMax 1.0V input. 10k pull-up to 3.3V, thermistor to GND.
D0GPIO16Onboard Status LEDActive LOW. Blinks during Wi-Fi connection.
3V3VCCVoltage Divider TopDo NOT use VIN for the divider; USB voltage fluctuates.
GNDGNDThermistor BottomCommon ground reference.

ESP8266 Firmware: HTTP Server Setup

Flash this firmware using the Arduino IDE. Ensure you have the ESP8266 Arduino Core installed via the Board Manager. This code sets up a captive HTTP server that returns JSON-formatted sensor data and handles connection timeouts gracefully.

#include 
#include 
#include 

// --- PIN DEFINITIONS ---
#define STATUS_LED 16      // D0 on NodeMCU (Active LOW)
#define THERMISTOR_PIN A0  // ADC0

// --- NETWORK CREDENTIALS ---
const char* ssid = "YourNetworkSSID";
const char* password = "YourNetworkPassword";

ESP8266WebServer server(80);

// --- SENSOR MATH CONSTANTS ---
const float SERIES_RESISTOR = 10000.0;
const float NOMINAL_RESISTANCE = 10000.0;
const float NOMINAL_TEMPERATURE = 25.0;
const float B_COEFFICIENT = 3950.0;

void setup() {
  Serial.begin(115200);
  pinMode(STATUS_LED, OUTPUT);
  digitalWrite(STATUS_LED, LOW); // Turn LED ON during connection

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  
  Serial.print("Connecting to ");
  Serial.println(ssid);
  
  unsigned long startAttemptTime = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < 15000) {
    delay(500);
    Serial.print(".");
  }

  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("\nFailed to connect. Restarting...");
    ESP.restart();
  }

  digitalWrite(STATUS_LED, HIGH); // Turn LED OFF when connected
  Serial.println("\nConnected! IP address:");
  Serial.println(WiFi.localIP());

  server.on("/api/sensor", HTTP_GET, handleSensorData);
  server.on("/api/ping", HTTP_GET, []() {
    server.send(200, "text/plain", "pong");
  });
  
  server.begin();
}

void loop() {
  server.handleClient();
}

void handleSensorData() {
  int adcValue = analogRead(THERMISTOR_PIN);
  
  // Prevent division by zero if sensor is disconnected
  if (adcValue == 0 || adcValue >= 1023) {
    server.send(500, "application/json", "{\"error\":\"Sensor disconnected or shorted\"}");
    return;
  }

  float resistance = SERIES_RESISTOR * ((1023.0 / adcValue) - 1.0);
  float steinhart = resistance / NOMINAL_RESISTANCE;
  steinhart = log(steinhart);
  steinhart /= B_COEFFICIENT;
  steinhart += 1.0 / (NOMINAL_TEMPERATURE + 273.15);
  steinhart = 1.0 / steinhart;
  steinhart -= 273.15;

  StaticJsonDocument<200> doc;
  doc["temp_c"] = roundf(steinhart * 10.0) / 10.0;
  doc["raw_adc"] = adcValue;
  doc["ip"] = WiFi.localIP().toString();
  
  String response;
  serializeJson(doc, response);
  
  server.sendHeader("Access-Control-Allow-Origin", "*");
  server.send(200, "application/json", response);
}

Android Studio Client: Kotlin and OkHttp

For the Android side, we use OkHttp, the industry-standard HTTP client for Android. Add the dependency to your app/build.gradle.kts: implementation("com.squareup.okhttp3:okhttp:4.12.0").

Critical Android 9+ Requirement: Android blocks cleartext HTTP (non-HTTPS) traffic by default. Because your local ESP8266 does not have an SSL certificate, you must explicitly allow cleartext traffic for the ESP's IP address in your AndroidManifest.xml by adding android:usesCleartextTraffic="true" inside the <application> tag, or by configuring a Network Security Config XML file.

Below is the complete, compilable Kotlin repository class to fetch the data. It includes strict timeout handling and error parsing.

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject
import java.util.concurrent.TimeUnit

class Esp8266Repository {
    // Hardcode the IP printed by the ESP8266 Serial Monitor
    private val baseUrl = "http://192.168.1.50" 

    private val client = OkHttpClient.Builder()
        .connectTimeout(3, TimeUnit.SECONDS)
        .readTimeout(3, TimeUnit.SECONDS)
        .build()

    suspend fun fetchSensorData(): Result = withContext(Dispatchers.IO) {
        val request = Request.Builder()
            .url("$baseUrl/api/sensor")
            .get()
            .build()

        try {
            client.newCall(request).execute().use { response ->
                if (!response.isSuccessful) {
                    return@withContext Result.failure(Exception("HTTP ${response.code}"))
                }
                
                val body = response.body?.string() ?: return@withContext Result.failure(Exception("Empty body"))
                val json = JSONObject(body)
                
                if (json.has("error")) {
                    return@withResult Result.failure(Exception(json.getString("error")))
                }

                val temp = json.getDouble("temp_c")
                val rawAdc = json.getInt("raw_adc")
                Result.success(SensorData(temp, rawAdc))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

data class SensorData(val tempC: Double, val rawAdc: Int)

Debugging: Exact Error Strings and Ranked Causes

When bridging hardware and mobile software, failures are inevitable. Here is your diagnostic decision path for the most common roadblocks.

The First Three Things to Check When It Fails

  1. Network Isolation: Ensure the Android phone and the ESP8266 are on the exact same 2.4GHz SSID. Many modern mesh routers isolate 5GHz and 2.4GHz bands, or enable 'AP Isolation' on guest networks, blocking local LAN traffic.
  2. IP Address Drift: Check the ESP8266 Serial Monitor. If your router rebooted, the ESP's DHCP IP likely changed from 192.168.1.50 to something else. Update the baseUrl in your Kotlin code.
  3. Cleartext Manifest Flag: Verify android:usesCleartextTraffic="true" is actually inside the <application> tag, not the <manifest> tag, in your AndroidManifest.xml.

Ranked Causes for Exact Error Strings

Exact Error StringOriginRanked Causes & Fixes
java.net.UnknownHostException: Unable to resolve host "192.168.x.x" Android (OkHttp) 1. Typo in the IP string (e.g., extra space).
2. Phone is on cellular data and Wi-Fi is disabled.
3. Phone is on a 5GHz band while ESP is on 2.4GHz with client isolation enabled.
CLEARTEXT communication to 192.168.x.x not permitted by network security policy Android (System) 1. Missing usesCleartextTraffic flag in Manifest.
2. Network Security Config XML is overriding the manifest flag.
Fix: Add the manifest flag or create a network_security_config.xml allowing the specific IP domain.
java.net.SocketTimeoutException: timeout Android (OkHttp) 1. ESP8266 is stuck in a blocking delay() loop and missing the HTTP request.
2. Router firewall is blocking port 80 between subnets.
Fix: Ensure server.handleClient() is called frequently in the ESP loop().
wl_status_t: WL_NO_SSID_AVAIL (1) ESP8266 (Serial) 1. SSID string has a typo or hidden whitespace.
2. Router is set to hide SSID broadcast (ESP8266 struggles with hidden SSIDs).
3. Router is enforcing WPA3-Only; ESP8266 requires WPA2-PSK.

Extending and Simplifying the Build

Once the baseline HTTP REST connection is stable, you can scale the architecture up or strip it down based on your production requirements.

How to Extend (Scale Up)

  • Add POST Endpoints: Expand the ESP8266 server to accept HTTP_POST requests. Use this to send configuration payloads (like new Wi-Fi credentials or PID tuning values) from the Android app to the microcontroller.
  • Implement mDNS: Include <ESP8266mDNS.h> in your firmware. This allows the ESP8266 to broadcast a local hostname (e.g., esp-sensor.local). Your Android app can then resolve the hostname instead of relying on a hardcoded, drifting DHCP IP address.
  • Switch to Retrofit: As your API grows beyond two endpoints, replace raw OkHttp calls in Android Studio with Retrofit. It uses annotations to map Kotlin interfaces directly to your ESP8266 REST routes, eliminating manual JSON parsing.

How to Simplify (Strip Down)

  • Drop the Sensor: If you only need to toggle a relay, delete the thermistor math and the JSON library. Change the endpoint to server.on("/relay/on", HTTP_GET, ...) and return a simple text/plain "OK". This reduces flash memory usage by roughly 15%.
  • Use ESP8266HTTPClient (Reverse Direction): If the ESP8266 needs to push data to an Android app running a local NanoHTTPD server, reverse the roles. The ESP becomes the client using ESP8266HTTPClient, and the phone becomes the server. This eliminates the need for the Android app to poll the ESP continuously.