In this guide
Start here: pick your wireless link
Almost every Android-plus-hardware build is the same architecture: a phone app sends small commands (turn relay on, set servo angle, request a sensor reading), and a microcontroller — Arduino Uno/Nano or an ESP32 — acts on them and replies with status. The wireless link in the middle is the decision that shapes everything else: code complexity, range, power draw, and how painful demo day gets.
There are four practical options for student builds. Pick one, commit to it early, and build the rest around it:
| Bluetooth Classic (HC-05) | BLE (ESP32 built-in) | WiFi direct (ESP32) | WiFi remote (MQTT) | |
|---|---|---|---|---|
| Typical range | ~10 m, line of sight helps | ~10–30 m | ~30–50 m on LAN | Anywhere with internet |
| Phone ↔ board talk | Serial stream over SPP | GATT services/characteristics | HTTP requests or raw TCP | Publish/subscribe via broker |
| Board needed | Arduino + HC-05 module | ESP32 (no extra module) | ESP32 (no extra module) | ESP32 (no extra module) |
| Setup pain | Pair once, then it just works | More code: scan, connect, discover services | WiFi credentials on the board; phone and board on the same network | Broker account (e.g. HiveMQ Cloud free tier); both sides need internet |
| Power on board | Moderate (~30–40 mA streaming) | Low (~10–15 mA average typical) | Higher (~80–160 mA transmitting) | Higher, plus keep-alive traffic |
| When it wins | Home automation buttons, robot cars, anything a few metres away | Battery devices, wearables, sensor beacons | Dashboards, camera feeds, local web UIs | Control your hostel-room rig from the classroom — genuine remote control |
If this is your first app-plus-hardware build, Bluetooth Classic with an HC-05 is the fastest route to a working demo: the phone sees the board as a serial port, you send text, the board reacts. If your build needs range beyond one room, WiFi on the ESP32 is the step up. MQTT over the internet is the showpiece option — it lets the app work from anywhere — but it adds a broker, authentication, and internet dependence to your demo-day risk list.
Rule of thumb: get the control loop working on the simplest link first, then upgrade the transport if your requirements demand it. A robot car that reliably moves over HC-05 beats a robot car that sometimes works over MQTT.
A full working example of the Bluetooth Classic path is the bluetooth-home-automation-using-hc-05 project — Android app, HC-05, relay board — which you can treat as a reference build while reading this guide.
Path 1: Bluetooth Classic with the HC-05
Wiring it up
The HC-05 is a 3.3 V-logic module that speaks to the Arduino over UART (TX/RX pins). The wiring every tutorial uses is:
- HC-05 VCC → Arduino 5V
- HC-05 GND → Arduino GND
- HC-05 TX → Arduino pin 10 (via SoftwareSerial)
- HC-05 RX → Arduino pin 11 through a voltage divider (two resistors, e.g. 1 kΩ + 2 kΩ, dropping the Arduino's 5 V TX to ~3.3 V)
The divider on RX is the detail most first builds skip. The HC-05 often tolerates 5 V on RX for a while and then behaves erratically or dies mid-semester — two resistors cost less than a replacement module.
Avoid the hardware UART pins (0 and 1) for the HC-05 during development, because those are shared with the USB serial monitor. Use SoftwareSerial on other digital pins so you can still debug over USB while the Bluetooth link is live.
AT mode: rename it and set the baud rate
Out of the box the HC-05 advertises itself as "HC-05" at 9600 baud. For a student demo, renaming it to your project name ("RoomController") makes pairing unambiguous when five other groups are also using HC-05s in the same lab. You do this in AT command mode:
- Power the module with the EN (sometimes labelled KEY) pin held HIGH (3.3 V).
- The onboard LED now blinks slowly (~2 s interval) instead of the fast pairing blink — that slow blink means AT mode.
- Open the serial monitor at 38400 baud (AT mode uses 38400, not the normal 9600).
- Send
AT— the module should replyOK. - Send
AT+NAME=RoomController→ replyOK. - Optionally set the role and password:
AT+ROLE=0(slave — the phone initiates),AT+PSWD=1234.
Then power-cycle with EN low (or disconnected) and the module returns to normal mode at your configured baud rate, discoverable under its new name.
The Arduino side: a minimal command loop
Keep the firmware stupid-simple: read a line from the HC-05, match it against known commands, act, reply. Here is a complete starting point for a two-relay controller:
#include <SoftwareSerial.h>
SoftwareSerial bt(10, 11); // RX, TX
const int RELAY1 = 7;
const int RELAY2 = 8;
void setup() {
pinMode(RELAY1, OUTPUT);
pinMode(RELAY2, OUTPUT);
digitalWrite(RELAY1, HIGH); // relay modules are usually active-LOW
digitalWrite(RELAY2, HIGH);
Serial.begin(9600);
bt.begin(9600); // match the HC-05 baud rate
}
void loop() {
if (bt.available()) {
String cmd = bt.readStringUntil('\n');
cmd.trim();
if (cmd == "R1ON") { digitalWrite(RELAY1, LOW); bt.println("R1:ON"); }
else if (cmd == "R1OFF") { digitalWrite(RELAY1, HIGH); bt.println("R1:OFF"); }
else if (cmd == "R2ON") { digitalWrite(RELAY2, LOW); bt.println("R2:ON"); }
else if (cmd == "R2OFF") { digitalWrite(RELAY2, HIGH); bt.println("R2:OFF"); }
else if (cmd == "STATUS") {
bt.print("R1:"); bt.print(digitalRead(RELAY1) == LOW ? "ON" : "OFF");
bt.print(",R2:"); bt.println(digitalRead(RELAY2) == LOW ? "ON" : "OFF");
}
Serial.println(cmd); // USB debug mirror
}
}
Two deliberate choices here. First, commands are short uppercase tokens terminated by \n — trivial to parse, trivial to type by hand in a Bluetooth terminal app while debugging. Second, every command gets a reply. That reply is what lets the app show the actual state of the hardware rather than the state the app assumes — the difference between a demo that looks right and one that is right.
The Android side of the HC-05 link
From the phone's perspective the HC-05 is just an RFCOMM serial socket. The classic development shortcut is a generic Bluetooth terminal app from the Play Store: pair the HC-05, connect, type R1ON, watch the relay click. Do this before writing a line of app code — it proves the firmware and wiring work, so any later failure is definitely in your app.
When you write the real app (Flutter vs native is covered below), the Bluetooth layer needs exactly four operations: scan/list paired devices, connect to the HC-05's MAC address over the SPP UUID (00001101-0000-1000-8000-00805F9B34FB), write bytes, and read the reply stream. Packages like flutter_bluetooth_serial wrap all of this for Flutter; native Kotlin uses BluetoothSocket directly.
For a polished reference of where this path leads, look at the voice-app-smart-home-automation-system project — it layers voice commands on top of the same phone-to-relay Bluetooth link.
Path 2: BLE with the ESP32
How BLE differs from Bluetooth Classic
BLE (Bluetooth Low Energy) is not "Bluetooth but newer" — it is a different protocol. Instead of an open serial pipe, the ESP32 advertises GATT services, each containing characteristics the phone can read, write, or subscribe to for notifications. Your "turn relay on" command becomes a write to a characteristic UUID rather than bytes down a stream.
This is more code than HC-05 serial, and for a simple remote-control build it is arguably overkill. BLE earns its place when:
- the board runs on batteries (BLE's sleep/wake duty cycle draws far less average current),
- the phone needs to find and identify the board by advertising data without pairing,
- multiple phones should read the same sensor data (one board, several subscribers),
- you want the board to push data on its own schedule via notifications rather than answering polls.
The mental model: services and characteristics
Design your GATT layout on paper before coding:
- Service
CONTROL(your own 128-bit UUID) → characteristicRelay State(read + write: phone writes0x01, board switches the relay and the characteristic now reads back0x01) - Service
SENSOR→ characteristicTemperature(read + notify: board updates the value every 5 s, subscribed phones get pushed updates)
On the Arduino/ESP32 side, the BLEDevice / BLEServer libraries that ship with the ESP32 Arduino core implement this with a few dozen lines: create server, create service, create characteristics with the right properties, set callbacks on write. On the phone side, flutter_blue_plus (Flutter) or the native BluetoothGatt API handles scanning, connecting, discovering services, and writing characteristics.
A health-monitoring build is the natural showcase for BLE's strengths — sensor data flowing to a phone app on a power budget. The iot-patient-health-monitoring-system project follows exactly that pattern (wearable sensors → phone/app dashboard).
BLE gotchas worth knowing upfront
- MTU is small. The default ATT MTU is 23 bytes, so a single characteristic write carries ~20 bytes of payload unless you negotiate a larger MTU. Keep commands tiny — another reason the short-token protocol from Path 1 ports well to BLE.
- iOS vs Android behaviour differs around scanning and background connections, but since your demo phone is Android, test only on your actual demo device.
- The ESP32's BLE and WiFi share one radio. You can run both, but throughput on each drops and power rises. For student builds, pick one radio per board.
Path 3: WiFi and MQTT on the ESP32
Three WiFi topologies, easiest first
1. ESP32 as its own access point. The board broadcasts a WiFi network ("RoomController-AP"); the phone joins it and talks to the board at a fixed IP like 192.168.4.1 over HTTP. No router, no internet, works in any classroom. The limitation is obvious: while the phone is on the board's network, it has no internet — fine for a demo, annoying for a daily-use device.
2. Board and phone on the same LAN. The ESP32 joins the lab/home router as a station; the phone, on the same WiFi, reaches the board at its DHCP address (or a static IP you assign, or an mDNS name like roomcontroller.local). This is the topology for dashboards and anything the whole room should see. The risk: college networks with client isolation or captive portals will silently block phone-to-board traffic — test on the actual demo network, not your home router.
3. MQTT through a cloud broker. Both the board and the phone connect out to a public broker; commands travel as published messages. Now the phone can be on mobile data in another building and still flip the relay. This is the only option of the four that gives genuine remote control.
Minimal HTTP control sketch (AP mode)
The ESP32 Arduino core ships a WebServer library. The whole control plane is a few routes:
#include <WiFi.h>
#include <WebServer.h>
const char* AP_SSID = "RoomController-AP";
const char* AP_PASS = "controller123";
WebServer server(80);
const int RELAY1 = 26;
void setup() {
pinMode(RELAY1, OUTPUT);
digitalWrite(RELAY1, HIGH);
WiFi.softAP(AP_SSID, AP_PASS);
server.on("/relay1/on", []() { digitalWrite(RELAY1, LOW); server.send(200, "text/plain", "R1:ON"); });
server.on("/relay1/off", []() { digitalWrite(RELAY1, HIGH); server.send(200, "text/plain", "R1:OFF"); });
server.on("/status", []() { server.send(200, "text/plain", digitalRead(RELAY1) == LOW ? "R1:ON" : "R1:OFF"); });
server.begin();
}
void loop() { server.handleClient(); }
The phone app is then just HTTP GETs — no Bluetooth permissions, no pairing, no special packages. Any HTTP client works, and you can test the entire firmware from a laptop browser before the app exists. That testability is WiFi's underrated advantage: curl http://192.168.4.1/relay1/on either clicks the relay or it doesn't, with no app code in the loop.
MQTT: the remote-control upgrade
MQTT adds a broker in the middle. The board subscribes to a command topic (home/room1/relay1/set) and publishes state to (home/room1/relay1/state); the app does the mirror image. The board code uses the PubSubClient library; the app uses an MQTT client package. Use a free public broker for development (HiveMQ Cloud's free tier works), with a username/password — never ship a demo on an anonymous public broker with predictable topic names, or someone else's client will find your relay.
Expectations to set honestly: round-trip latency over a cloud broker on decent networks is typically well under a second, but it is internet-dependent and variable — design the app UI to show "command sent, awaiting confirmation" and update only when the state topic confirms. That single UI habit hides almost all MQTT flakiness from the demo audience.
The iot-smart-energy-meter-dashboard project shows the WiFi/MQTT pattern applied to a full dashboard build — meter data published over the network, live values in the app.
If MQTT is new to you, read mqtt-for-final-year-iot-projects-a-practical-student-guide first — it covers brokers, topics, QoS levels, and the keep-alive behaviour that bites student builds.
Design your message protocol in ten minutes
Whatever transport you chose, the phone and the board must agree on a message format. You have two sane options:
Single-character or short-token commands (1/0, R1ON, FWD, STOP). Smallest possible payload, parseable with string comparison, debuggable by typing into a terminal app. Perfect for relays, motors, and on/off-style control. This is what most student builds should use.
JSON payloads ({"relay1": true, "fan_speed": 75}). Self-describing, extensible, and the obvious choice once you have more than a handful of commands or need structured sensor readings back. Costs: a JSON parser on the board (ArduinoJson is the standard library) and slightly larger messages — irrelevant on WiFi, worth noticing on BLE's ~20-byte writes.
Either way, follow three protocol rules and most "it works sometimes" bugs disappear:
- Terminate every message. Newline (
\n) for serial/BLE, or length-prefixing — never rely on timing gaps to frame messages. - Echo state back. Every command returns the resulting state; the app displays what the board reports, not what it asked for.
- Version it. Put a
v1in the topic name or the first byte of the protocol. When you add commands mid-semester, old firmware and new app stay distinguishable.
For general IoT transport choice beyond the phone link — when to use HTTP vs MQTT vs Firebase vs Blynk — see how-to-connect-your-iot-project-to-the-cloud-mqtt-http-firebase-and-blynk.
Building the Android app: Flutter vs native, and the permission trap
Flutter or native Kotlin?
| Flutter | Native Kotlin | |
|---|---|---|
| Bluetooth packages | flutter_bluetooth_serial (Classic), flutter_blue_plus (BLE) |
BluetoothAdapter / BluetoothGatt in the framework — no dependency risk |
| WiFi/HTTP/MQTT | http, mqtt_client packages |
OkHttp / Retrofit, Paho MQTT |
| UI speed | Fast: one codebase, hot reload, Material widgets | More verbose, but full control |
| When to choose | You want the app done quickly and it is primarily buttons + status displays | You already know Kotlin, or you need fine-grained control over scanning, bonding, or background connections |
For the builds in this guide — buttons, toggles, sensor readouts — Flutter gets you to a demo faster. The smart-attendance-tracker-android-app, expense-splitter-android-app, and gym-workout-tracker-android-app projects are all Android-app builds in this catalogue; browsing how they scope their app features is a good calibration exercise even though their backends differ from hardware control.
The Android 12+ permission gotcha
This is the single most common reason student Bluetooth apps "work on my old phone but not on the demo phone":
Android 12 (API 31) split Bluetooth permissions.
BLUETOOTH_CONNECT(andBLUETOOTH_SCANfor discovery) are now runtime permissions — they must be requested from the user at runtime like camera or location. Declaring them in the manifest alone does nothing, and the failure mode is silent: connection attempts just fail with no useful error.
Handle all three generations:
| Phone's Android version | What you need |
|---|---|
| Android 11 and below | BLUETOOTH + BLUETOOTH_ADMIN in the manifest (install-time, no runtime prompt) |
| Android 12+ | BLUETOOTH_CONNECT (+ BLUETOOTH_SCAN if you scan) requested at runtime; keep the manifest entries too |
| Android 10+ doing BLE scans | Location permission is also required for BLE scanning on many devices — request it or scanning returns nothing |
Test on the exact phone you will demo with, on its actual OS version, with the app freshly installed. Permissions granted during development on an older phone tell you nothing about the demo device.
WiFi/HTTP builds dodge this entirely — another quiet point in favour of the ESP32 AP-mode path when demo reliability matters more than wireless elegance.
Testing order and the demo-day checklist
Test in layers, and never skip one:
- Firmware alone. USB serial monitor: type commands by hand, confirm the hardware reacts and replies correctly.
- Transport alone. Bluetooth terminal app (HC-05),
nRF Connect(BLE), or a laptop browser/curl(WiFi). Confirm the link carries your protocol end to end. - App against known-good firmware. Only now write the app. If something breaks, the fault is in the app — the lower layers are already proven.
- The full loop on the demo phone, freshly installed, on the demo network, from across the room.
Demo-day checklist — the unglamorous items that decide whether the demo happens:
- Phone charged above 50%, and the charger in your bag.
- Board powered from a supply that can source the peak current (ESP32 + relay coils on a weak USB port brown-out mid-demo — use a proper 5 V adapter or a charged power bank).
- HC-05 already paired to the demo phone; know its exact advertised name.
- For WiFi builds: the AP credentials or LAN details written down; a phone hotspot configured as the fallback network, with the ESP32 firmware able to join it (hardcode the hotspot SSID/password as a fallback, or add a captive-portal WiFi manager).
- For MQTT builds: broker credentials verified working that morning; an offline fallback story if the venue has no internet (AP-mode HTTP as plan B is worth the extra hour).
- A 30-second "it works" script: the one button press that makes the relay click, in case time runs short.
- The USB cable and a laptop with the Arduino IDE, in case you need to reflash or read serial output live.
For the broader discipline of pre-submission testing — power budgets, sensor validation, enclosure checks — the companion guide how-to-test-and-debug-your-final-year-project-before-submission is worth a read once your build is assembled.
Common failures, and what actually fixes them
| Symptom | Likely cause | Fix |
|---|---|---|
| HC-05 pairs but app can't connect | Wrong UUID, or connecting before pairing completes | Use the SPP UUID 00001101-0000-1000-8000-00805F9B34FB; wait for the bonded state before opening the socket |
| App connects on Android 10, silently fails on Android 13 | Missing runtime BLUETOOTH_CONNECT permission |
Request it at runtime; check checkSelfPermission before connecting |
| BLE scan finds nothing | Missing location permission, or location services off | Request fine-location permission and confirm location services are enabled on the device |
| Commands work from terminal app but not your app | Missing \n terminator, or wrong character encoding |
Match the firmware's readStringUntil('\n'); send UTF-8 with explicit newline |
| Relay chatters or ESP32 resets when relay switches | Power brown-out: relay coil inrush sags the supply | Separate 5 V supply for the relay board; common ground with the controller; flyback-protected relay module |
| WiFi build works at home, not in the lab | Client isolation or captive portal on the college network | Demo in ESP32 AP mode, or carry your own hotspot as the network |
| MQTT commands sometimes lag several seconds | Broker keep-alive or QoS mismatch, or phone Doze throttling the app | Use QoS 1 for commands; exempt the app from battery optimisation during the demo; show "awaiting confirmation" UI |
| Everything worked yesterday, nothing works today | The HC-05 lost its pairing, or the ESP32 joined the wrong saved network | Re-pair / re-enter credentials; keep a written card of names, passwords, and IPs with the kit |
None of these are mysterious once you have seen them once — which is exactly why the layered testing order above exists. Prove each layer in isolation and the failure table shrinks to a five-minute diagnosis instead of a demo-day crisis.
Where to go from here
Pick the path that matches your build: HC-05 Bluetooth Classic for the fastest working remote control, ESP32 BLE for battery-powered sensing, ESP32 WiFi for dashboards and range, MQTT when the demo genuinely needs remote access. Build the firmware first, prove it with a terminal app or curl, then write the Android app against a link you already trust — and test the whole loop on the exact phone and network you will demo with.
If you want a complete reference implementation to study alongside your own build, the bluetooth-home-automation-using-hc-05 project covers the full HC-05 path end to end, and the branch hub at Android projects lists the other app builds in this catalogue.