Guides · 18 min read

MIT App Inventor Arduino Bluetooth: Build an HC-05 Android App

Control Arduino over Bluetooth with MIT App Inventor: HC-05 wiring, ListPicker, button and slider blocks, LED, relay, servo, sensor data, APK, Android 12 fix.

Controlling Arduino over Bluetooth with App Inventor is the fastest way to build your own Android app without writing a single line of Java or Kotlin. In MIT's free, browser-based MIT App Inventor, you drag buttons, sliders and text labels onto the screen and define what they do with blocks that snap together like puzzle pieces. The phone connects to an HC-05 Bluetooth module wired to the Arduino; every tap in the app travels to the Arduino as a short text command, and the Arduino sends its sensor readings back to the phone the same way.

By the end of this guide you'll have a remote-control app that really works: a ListPicker that connects to the HC-05 from a list, buttons that switch an LED and a relay on and off, two sliders that set the LED brightness and the servo angle, and labels that show live temperature and humidity from a DHT11. You'll go through the Arduino code, every App Inventor block, building the APK and the fix for the Bluetooth permission problem introduced in Android 12, step by step. If you've never used the HC-05 before, take a quick look at the HC-05 Bluetooth Module guide first.

HC-05 Bluetooth module
The HC-05 is a classic Bluetooth (SPP) module, exactly what App Inventor's BluetoothClient component talks to; it creates a wireless serial link between the phone and the Arduino.

What is App Inventor, and how does it talk to Arduino?

MIT App Inventor is a free app development environment created by the Massachusetts Institute of Technology (MIT); you sign in with your Google account and use it in the browser. It has two views: in the Designer you lay out the app by dragging components, and in the Blocks editor you write rules such as "when the button is clicked, do this" with colourful blocks. To try the app without installing it, you use the MIT AI2 Companion app from the Play Store; when you're done, a single click produces an installable APK file.

On the Bluetooth side, the BluetoothClient component does the work. It uses classic Bluetooth's SPP (Serial Port Profile), which is exactly what the HC-05 speaks. The result is an invisible serial cable between phone and Arduino: when the app sends "L1" with the SendText block, the Arduino reads those two characters with bt.read(), just as if you'd typed them into the Serial Monitor. Everything the Arduino sends with bt.print() arrives at the phone's ReceiveText block. No Bluetooth-specific library is needed on the Arduino side.

LayerJobIn this project
Interface (Designer)Components shown on screenListPicker, 5 Buttons, 2 Sliders, Labels
Logic (Blocks)Blocks that react to eventsClick, PositionChanged and Timer events
BluetoothClientSPP connection, sending and receiving dataConnect, SendText, ReceiveText
HC-05Bridge between Bluetooth and UART9600 baud, PIN 1234
ArduinoInterprets commands, drives outputs, reads the sensorLED, relay, servo, DHT11

What you'll need

PartQtyNote
Arduino Uno (or Nano)1Same code and pins on both
HC-05 or HC-06 Bluetooth module1Classic Bluetooth; the HM-10 (BLE) won't work this way
1 kΩ and 2 kΩ resistors1 eachVoltage divider for the HC-05 RXD pin (2.2 kΩ also works)
LED and 220 Ω resistor1 eachDimmable LED (D5, PWM)
5 V relay module1To switch loads such as a lamp or a fan
SG90 servo motor10–180° control with a slider
DHT11 temperature and humidity sensor1Data to show on the phone
Android phone1The HC-05 doesn't work with an iPhone
Breadboard and jumper wires—A common GND is a must
Arduino Uno
Arduino Uno
HC-06
HC-06
SG90
SG90
DHT11
DHT11
LED
LED

Wiring: HC-05, LED, relay, servo and DHT11

The HC-05 board has a regulator, so its VCC pin takes 5 V, but its data pins use 3.3 V logic. The Arduino reads the 3.3 V signal from the module's TXD pin without trouble; the Arduino's 5 V TX signal, however, should reach the RXD pin through a voltage divider: D11 → 1 kΩ → RXD, RXD → 2 kΩ → GND. That puts 5 × 2 / 3 ≈ 3.3 V on RXD. We connect the HC-05 to D10 and D11 with SoftwareSerial instead of the Uno's hardware serial pins (0 and 1), so you can upload code with the module attached and still use the Serial Monitor for debugging. You can check the LED's resistor with the LED resistor calculator.

ConnectionArduino UnoESP32Raspberry Pi Pico
HC-05 VCC5VNot needed (built-in Bluetooth)VBUS (5 V)
HC-05 GNDGND—GND
HC-05 TXDD10 (SoftwareSerial RX)—GP1 (UART0 RX)
HC-05 RXDD11, via 1 kΩ / 2 kΩ divider—GP0 (UART0 TX), no divider
LED (+ 220 Ω)D5 (PWM)GPIO 2 (onboard LED)GP15
Relay module IND7GPIO 26GP14
Servo signal (orange)D9— (can be added with ESP32Servo)GP13
DHT11 dataD2GPIO 4GP16
Mind three rules: all GNDs (Arduino, HC-05, relay, servo and any external supply) must be tied together. Never feed 5 V without a divider into the HC-05's RXD pin; the module may survive for a while but can fail over time. A moving servo can briefly draw several hundred milliamps; if the Arduino runs from USB, that surge pulls the voltage down, the HC-05 drops the connection or the Arduino resets. Powering the servo and relay from a separate 5 V supply solves this at the root.

Communication protocol: what will the phone send?

Before writing code, decide which "language" the phone and the Arduino will speak. In this project every command is a letter, followed by a number and finally a newline character (\n, ASCII 10). The newline matters a lot: Bluetooth data can arrive in pieces, and when "S1" and "35" arrive separately, the Arduino waits for the newline and handles "S135" as one command. Because the commands are readable text, you can also try them by hand with any Bluetooth terminal app.

CommandDirectionMeaning
L1 / L0Phone → ArduinoTurn the LED fully on / off
P0 … P255Phone → ArduinoLED brightness (analogWrite value)
R1 / R0Phone → ArduinoEnergise / release the relay
S0 … S180Phone → ArduinoServo angle (degrees)
24.5;41Arduino → phoneTemperature (°C) and humidity (%), separated by a semicolon

Instead of text, App Inventor can also send a single byte (0–255) with the Send1ByteNumber block, which the Arduino reads directly as a number. It's short and fast, but only suits a single slider: if two sliders and four buttons all send bytes, the Arduino can't tell whether an incoming 90 is a servo angle or a brightness level. The letter + number + newline format costs a few more bytes, but at 9600 baud roughly 960 characters go through per second, so you'll never notice the difference.

The Arduino code

The Servo and SoftwareSerial libraries come with the Arduino IDE. For the DHT11, install DHT sensor library (Adafruit) from the Library Manager; when it asks about dependencies, choose Install all to add Adafruit Unified Sensor too. Since we don't use pins 0 and 1, there's no need to unplug the HC-05 while uploading.

Arduino (C++)
1#include <SoftwareSerial.h>
2#include <Servo.h>
3#include <DHT.h>
4
5SoftwareSerial bt(10, 11);       // RX, TX → HC-05 TXD, RXD (bölücü üzerinden / via divider)
6Servo servo;
7DHT dht(2, DHT11);               // DHT11 veri pini D2 / data pin D2
8
9const int LED_PIN = 5;           // PWM destekli pin / PWM-capable pin
10const int ROLE_PIN = 7;
11const int SERVO_PIN = 9;
12const int ROLE_ACIK = LOW;       // çoğu röle modülü LOW ile çeker / most relay modules are active LOW
13const int ROLE_KAPALI = HIGH;
14
15String komut = "";               // gelen satırın tamponu / buffer for the incoming line
16unsigned long sonGonderim = 0;
17
18void komutIsle(String k) {
19  k.trim();                                  // boşluk ve \r temizle / strip spaces and \r
20  if (k.length() == 0) return;
21  Serial.println(k);                         // hata ayıklama / debugging
22  char tur = k.charAt(0);                    // komut harfi / command letter
23  int deger = k.substring(1).toInt();        // harften sonraki sayı / number after the letter
24
25  switch (tur) {
26    case 'L':                                // L1 / L0 → LED aç, kapat / LED on, off
27      analogWrite(LED_PIN, deger == 1 ? 255 : 0);
28      break;
29    case 'P':                                // P0..P255 → parlaklık / brightness
30      analogWrite(LED_PIN, constrain(deger, 0, 255));
31      break;
32    case 'R':                                // R1 / R0 → röle / relay
33      digitalWrite(ROLE_PIN, deger == 1 ? ROLE_ACIK : ROLE_KAPALI);
34      break;
35    case 'S':                                // S0..S180 → servo açısı / servo angle
36      servo.write(constrain(deger, 0, 180));
37      break;
38  }
39}
40
41void setup() {
42  pinMode(LED_PIN, OUTPUT);
43  pinMode(ROLE_PIN, OUTPUT);
44  digitalWrite(ROLE_PIN, ROLE_KAPALI);       // açılışta röle kapalı / relay off at start
45  servo.attach(SERVO_PIN);
46  servo.write(90);                           // orta konum / centre position
47  dht.begin();
48  Serial.begin(9600);                        // Seri Monitör / Serial Monitor
49  bt.begin(9600);                            // HC-05 veri modu hızı / HC-05 data mode baud rate
50}
51
52void loop() {
53  // 1) Telefondan gelen komutları satır satır oku / read commands from the phone line by line
54  while (bt.available()) {
55    char c = bt.read();
56    if (c == '\n') {                         // satır bitti / line complete
57      komutIsle(komut);
58      komut = "";
59    } else if (komut.length() < 16) {        // aşırı uzun satırı kes / cap overlong lines
60      komut += c;
61    }
62  }
63
64  // 2) 2 saniyede bir sensör verisini gönder / send sensor data every 2 seconds
65  if (millis() - sonGonderim >= 2000) {
66    sonGonderim = millis();
67    float t = dht.readTemperature();
68    float h = dht.readHumidity();
69    if (!isnan(t) && !isnan(h)) {
70      bt.print(t, 1);                        // örnek / example: 24.5
71      bt.print(';');
72      bt.print(h, 0);                        // örnek / example: 41
73      bt.print('\n');                        // println değil, \r gitmesin / not println, no \r
74    }
75  }
76}
  • Line buffer: loop() appends every incoming character to the komut (command) variable; when \n arrives it passes the line to komutIsle() and clears the buffer. Anything beyond 16 characters is dropped, so garbage data can't fill the memory.
  • trim(): Some terminal apps add \r at the end of the line as well; trim() removes it along with spaces, so "L1\r" is still recognised as "L1".
  • Letter and number: charAt(0) gives the command letter and substring(1).toInt() the number after it. constrain() clamps the value to a safe range; even if the phone accidentally sends 300, the servo is never pushed beyond 180°.
  • Non-blocking sending: The sensor is read every 2 seconds using millis(). Because there's no delay(), commands arriving in the meantime are handled without lag; see millis vs delay for details.
  • print('\n') instead of println: println() ends the line with \r\n. App Inventor only uses \n as the delimiter, so the \r would stick to the end of the humidity value; sending \n alone keeps parsing on the phone clean.
  • Active-LOW relay: Most relay modules switch on when IN goes LOW. If yours switches on with HIGH, just swap the values of the ROLE_ACIK (on) and ROLE_KAPALI (off) constants.
Before moving to App Inventor, test the Arduino side on its own: connect to the HC-05 with an app such as Serial Bluetooth Terminal from the Play Store, type "L1" and send it; the LED should light up and L1 should appear in the Serial Monitor. If lines like "24.5;41" scroll by every 2 seconds, data is flowing back too. If something goes wrong later, you'll know straight away whether the fault is in the app or in the circuit.

Designing the interface in App Inventor (Designer)

  1. Go to ai2.appinventor.mit.edu in your browser, sign in with your Google account and create a project called BlueGraysRemote with Projects > Start new project.
  2. From the Layout group in the Palette, drag in a HorizontalArrangement; put a ListPicker (Text: Connect), a Button (Text: Disconnect) and a Label (Text: Not connected) inside it.
  3. Add two more HorizontalArrangements below: put LED On and LED Off buttons in the first, Relay On and Relay Off buttons in the second. Setting the buttons' Width to Fill parent makes them easier to tap.
  4. Add two Sliders: for brightness MinValue 0, MaxValue 255, ThumbPosition 0; for the servo MinValue 0, MaxValue 180, ThumbPosition 90. Put a Label above each one saying what it does.
  5. Add two more Labels for temperature and humidity and raise their FontSize to about 20.
  6. From the Connectivity group add a BluetoothClient; in the Properties panel set DelimiterByte to 10 and tick NoLocationNeeded.
  7. From the Sensors group add a Clock; set TimerInterval to 200 and untick TimerEnabled. We'll switch the timer on with blocks once the connection is up.
ComponentName (Rename)Key setting
ListPickerLP_ConnectText: Connect
Button × 5BTN_Disconnect, BTN_LedOn, BTN_LedOff, BTN_RelayOn, BTN_RelayOffWidth: Fill parent
SliderSLD_BrightnessMinValue 0, MaxValue 255
SliderSLD_ServoMinValue 0, MaxValue 180, ThumbPosition 90
LabelLBL_Status, LBL_Temp, LBL_HumFontSize 20 (for the values)
BluetoothClientBluetoothClient1DelimiterByte 10, NoLocationNeeded ticked
ClockClock1TimerInterval 200, TimerEnabled off

Connecting to the HC-05 with a ListPicker

First pair the HC-05 in the phone's Bluetooth settings (the PIN is usually 1234, otherwise 0000). This step is essential: BluetoothClient1.AddressesAndNames only lists paired devices and doesn't scan for new ones nearby. Each entry is a text with the MAC address first and the device name after it, like "98:D3:31:F5:2A:10 HC-05". You can pass the whole text to the Connect block; App Inventor drops everything after the first space by itself. The block returns true if the connection succeeds and false if it fails.

  1. In the when LP_Connect.BeforePicking event, place set LP_Connect.Elements to BluetoothClient1.AddressesAndNames. The list refills with paired devices every time it opens.
  2. In the when LP_Connect.AfterPicking event, add an if block and plug call BluetoothClient1.Connect address: LP_Connect.Selection into its condition.
  3. In the then part of the if, set LBL_Status.Text to "Connected" and start reading data with set Clock1.TimerEnabled to true; in the else part, write "Connection failed".
  4. In the when BTN_Disconnect.Click event, first set Clock1.TimerEnabled to false, then call BluetoothClient1.Disconnect, and finally set LBL_Status.Text to "Not connected". Stopping the timer first prevents a read and an error message after the link is closed.
Once connected, the HC-05's LED stops its fast blinking and instead flashes twice briefly every 2 seconds. That tells you the connection state without looking at the app; if the LED goes back to fast blinking, the link has dropped.

Sending data: buttons and sliders

Sending takes a single block: call BluetoothClient1.SendText text. When you type "L1\n" into a text block, App Inventor turns the trailing \n into a newline character. Sending while not connected causes error 515, so wrapping every send in an if BluetoothClient1.IsConnected check is a good habit. For a slider, you attach the number to the letter and the newline with a join block: join("S", round(thumbPosition), "\n").

EventText sentResult on the Arduino
BTN_LedOn.ClickL1\nLED lights at full brightness
BTN_LedOff.ClickL0\nLED turns off
BTN_RelayOn.ClickR1\nRelay clicks on, the connected lamp lights
BTN_RelayOff.ClickR0\nRelay releases
SLD_Brightness.PositionChangedjoin("P", round(thumbPosition), "\n")analogWrite(5, value)
SLD_Servo.PositionChangedjoin("S", round(thumbPosition), "\n")servo.write(angle)

While you drag a slider with your finger, the PositionChanged event fires dozens of times per second and thumbPosition returns fractional values like 87.4. If you send on every event, the same angle goes out again and again, the HC-05's buffer fills up and the servo moves jerkily and late. The fix is simple: create a variable called global lastAngle; send only if the value rounded with round(thumbPosition) differs from it, then update the variable. That way data only goes out when the angle really changes. Apply the same trick to the brightness slider with global lastBrightness.

Receiving data: showing sensor values on the phone

The Arduino sends a "24.5;41" line every 2 seconds. App Inventor has no event that fires by itself when data arrives, so we use the Clock component to check "is there new data?" at regular intervals. Because we set DelimiterByte to 10 in the Designer, giving the ReceiveText block -1 as numberOfBytes makes it read up to the newline character and return the line without the \n. A 200 ms timer checks far more often than the data arrives (every 2000 ms), so nothing piles up in the buffer.

  1. Put an if BluetoothClient1.IsConnected condition inside when Clock1.Timer.
  2. Add a second condition inside it: call BluetoothClient1.BytesAvailableToReceive > 0.
  3. Store the incoming line in a local variable with initialize local line to call BluetoothClient1.ReceiveText numberOfBytes: -1.
  4. Turn the line into a list with the split text line at ";" block: "24.5;41" becomes the two-item list [24.5, 41]. Keep the result in a local variable called parts.
  5. if length of list parts = 2, set LBL_Temp.Text to join("Temperature: ", select list item parts index 1, " °C") and LBL_Hum.Text to join("Humidity: ", select list item parts index 2, " %"). Checking the item count stops half-received or corrupted lines from causing errors.
If you pass -1 to ReceiveText but forget to set DelimiterByte to 10 (the default is 0), the block waits for a 0 byte that never comes and the app looks frozen. And if the Arduino sends its line with println(), the \r character ends up at the end of the second value; that's why the code sends only '\n'.
Don't stop at showing values as text: you can turn LBL_Temp.TextColor red when the temperature goes above 30 °C, open an alert with the Notifier component, or draw a line graph of the latest readings on a Canvas.

The Bluetooth permission problem on Android 12 and later

Bluetooth permissions changed with Android 12 (API 31). The old BLUETOOTH permission, granted automatically at install time, was replaced by BLUETOOTH_CONNECT and BLUETOOTH_SCAN, which the user must approve while the app is running; in the phone's settings they appear as the Nearby devices permission. If it hasn't been granted, App Inventor opens the permission dialog on the first attempt but stops the block that was running. The result is a set of familiar symptoms: the list opens empty the first time you tap Connect, an "Error 908: The permission ... has been denied" message appears (naming BLUETOOTH_CONNECT or BLUETOOTH_SCAN), or, if the user refused once, the app never asks again.

  1. In the when Screen1.Initialize event, check the Android version: if select list item (split Screen1.PlatformVersion at ".") index 1 ≥ 12, give call Screen1.AskForPermission the BluetoothConnect value from the Permission list. These permissions don't exist on Android 11 and earlier, and asking for them there also triggers error 908, so the version check matters.
  2. In the when Screen1.PermissionGranted event, if the granted permission is BLUETOOTH_CONNECT (contains text: permissionName, piece: "CONNECT"), ask for BluetoothScan next. AskForPermission only takes one permission at a time, so you chain them like this.
  3. In the when Screen1.PermissionDenied event, use a Notifier to show a message with the path "Settings > Apps > BlueGraysRemote > Permissions > Nearby devices > Allow". Handling this event shows the user a clear explanation instead of a raw error box.
  4. Keep NoLocationNeeded ticked on the BluetoothClient in the Designer; it tells Android that the app doesn't use Bluetooth to work out the user's location.
  5. Always test with the APK as well: the AI2 Companion's permissions belong to the Companion app, while your APK asks for its own permissions after installation.
ErrorMeaningFix
Error 507: Unable to connect. Is the device turned on?Couldn't connect to the moduleCheck the HC-05's power, that it isn't connected to another phone, and the pairing; if it still fails, untick Secure on the BluetoothClient and try again
Error 515: Not connected to a Bluetooth device.Sending or reading without a connectionWrap the send and Clock blocks in an IsConnected check
Error 516: Unable to write: Broken pipeThe link dropped mid-way (power lost, out of range)Stop the Clock, call Disconnect and reconnect; give the servo and relay their own supply
Error 908: The permission … has been deniedNo Nearby devices permission on Android 12+Ask for it with blocks or grant it manually in Settings
The list opens emptyModule not paired, or the permission was only just requestedPair the HC-05 in the phone's settings; after granting the permission, tap Connect again

Testing the app and building the APK

You don't need to install an APK after every change while developing. Install the MIT AI2 Companion app from the Play Store; with your computer and phone on the same Wi-Fi network, choose Connect > AI Companion in App Inventor and scan the QR code or enter the 6-character code shown on screen. Every change you make in the Designer or the blocks shows up on the phone instantly. When the app is finished, build the installable file like this:

  1. Select Screen1 in the Designer and fill in AppName (the name shown on the phone), Icon (a square PNG), VersionCode and VersionName.
  2. From the top menu choose Build > Android App (.apk). The build usually takes a minute or two.
  3. When it's done, a window with a QR code and a download button appears. You can scan the QR code with your phone to download the file directly, or save the APK to your computer and copy it to the phone over USB. The QR link is only valid for a short time (about 2 hours).
  4. When you open the APK on the phone, Android asks you to allow your browser or file manager to install unknown apps. If Play Protect shows a warning, you can continue with the install-anyway option in the details; this warning is common for apps from outside the Play Store with an unfamiliar signature.
  5. On the app's first launch, approve the Nearby devices permission, tap Connect and pick the HC-05 from the list.
Back up your project with Projects > Export selected project (.aia) to my computer; you can share the .aia file with a friend or move it to another account. If you want to publish the app on Google Play, use Build > Android App Bundle (.aab) and be sure to keep your signing key safe with Projects > Export keystore: updates must be signed with the same key. Don't forget to increase VersionCode by 1 with every update.

The same app with an ESP32, no HC-05 needed

ESP32 development board
The classic ESP32 has Bluetooth Classic built in; App Inventor's BluetoothClient connects to it just as it does to an HC-05.

The classic ESP32 has both classic Bluetooth and BLE built in. The BluetoothSerial library presents the ESP32 to the phone as an SPP device, so you can use the app you just built without changing anything. Pair the device called "BlueGrays-ESP32" in the phone's Bluetooth settings, pick it from the list in the app, and that's it. Watch out: the ESP32-S3, ESP32-C3 and ESP32-C6 only support BLE; they have no BluetoothSerial, and on the App Inventor side you need the BluetoothLE extension. For working with BLE, see the ESP32 Bluetooth and BLE guide.

Arduino (C++)
1#include "BluetoothSerial.h"
2#include <DHT.h>
3
4BluetoothSerial SerialBT;
5DHT dht(4, DHT11);               // DHT11 veri pini GPIO 4 / data pin GPIO 4
6
7const int LED_PIN = 2;           // çoğu DevKit'te kart üstü LED / onboard LED on most DevKits
8const int ROLE_PIN = 26;
9
10String komut = "";
11unsigned long sonGonderim = 0;
12
13void setup() {
14  pinMode(ROLE_PIN, OUTPUT);
15  digitalWrite(ROLE_PIN, HIGH);              // aktif LOW röle kapalı / active-LOW relay off
16  dht.begin();
17  SerialBT.begin("BlueGrays-ESP32");         // telefonda görünecek ad / name shown on the phone
18}
19
20void loop() {
21  while (SerialBT.available()) {
22    char c = SerialBT.read();
23    if (c == '\n') {
24      komut.trim();
25      int deger = komut.substring(1).toInt();
26      if (komut.startsWith("L")) {
27        analogWrite(LED_PIN, deger == 1 ? 255 : 0);              // LED aç, kapat / LED on, off
28      } else if (komut.startsWith("P")) {
29        analogWrite(LED_PIN, constrain(deger, 0, 255));          // parlaklık / brightness
30      } else if (komut.startsWith("R")) {
31        digitalWrite(ROLE_PIN, deger == 1 ? LOW : HIGH);         // röle / relay
32      }
33      komut = "";
34    } else if (komut.length() < 16) {
35      komut += c;
36    }
37  }
38
39  if (millis() - sonGonderim >= 2000) {      // Uno koduyla aynı biçim / same format as the Uno
40    sonGonderim = millis();
41    float t = dht.readTemperature();
42    float h = dht.readHumidity();
43    if (!isnan(t) && !isnan(h)) {
44      SerialBT.print(t, 1);
45      SerialBT.print(';');
46      SerialBT.print(h, 0);
47      SerialBT.print('\n');
48    }
49  }
50}

The code understands the same commands as the Uno version and sends data in the same "temperature;humidity" format; it ignores S commands because no servo is attached. The Arduino Servo library doesn't work on the ESP32; to add a servo, install the ESP32Servo library and use the same attach() and write() calls on, say, GPIO 13. Power the relay module from the VIN (5 V) pin; if its IN input doesn't switch reliably with the ESP32's 3.3 V signal, choose a 3.3 V-compatible relay module. For details, see What Is a Relay Module?

With the Raspberry Pi Pico and MicroPython

Raspberry Pi Pico
The Raspberry Pi Pico runs at 3.3 V, so it connects straight to the HC-05's RXD pin; no voltage divider needed.

The standard Raspberry Pi Pico has no Bluetooth, so you connect the HC-05 to the UART0 pins (GP0 and GP1). Because the Pico's pins run at 3.3 V, this time no voltage divider is needed; power the module from the VBUS pin, which carries the 5 V from USB. The Pico W and Pico 2 W have built-in Bluetooth, but MicroPython only supports BLE on them; to talk to App Inventor's classic BluetoothClient, the HC-05 is still the easiest route. The code below understands the same commands, drives the servo with 50 Hz PWM and sends data in the same format; the dht module comes built into the Pico's MicroPython firmware.

MicroPython
1from machine import UART, Pin, PWM
2import dht
3import time
4
5bt = UART(0, baudrate=9600, tx=Pin(0), rx=Pin(1))   # GP0 → HC-05 RXD, GP1 ← HC-05 TXD
6led = PWM(Pin(15))
7led.freq(1000)
8role = Pin(14, Pin.OUT, value=1)                    # aktif LOW röle kapalı / active-LOW relay off
9servo = PWM(Pin(13))
10servo.freq(50)                                      # servo 50 Hz ister / servos need 50 Hz
11sensor = dht.DHT11(Pin(16))
12
13def komut_isle(k):
14    if not k:
15        return
16    tur, deger = k[0], int(k[1:] or 0)              # harf + sayı / letter + number
17    if tur == 'L':
18        led.duty_u16(65535 if deger == 1 else 0)
19    elif tur == 'P':
20        led.duty_u16(min(max(deger, 0), 255) * 257) # 0-255 → 0-65535
21    elif tur == 'R':
22        role.value(0 if deger == 1 else 1)
23    elif tur == 'S':
24        aci = min(max(deger, 0), 180)
25        servo.duty_ns(544000 + aci * 1856000 // 180)  # 544-2400 µs darbe / pulse
26
27tampon = b''
28son = time.ticks_ms()
29while True:
30    if bt.any():
31        tampon += bt.read()
32        while b'\n' in tampon:                      # tam satır geldi mi? / full line received?
33            satir, tampon = tampon.split(b'\n', 1)
34            try:
35                komut_isle(satir.decode().strip())
36            except ValueError:
37                pass                                # bozuk komutu atla / skip malformed command
38        if len(tampon) > 32:
39            tampon = b''                            # satır sonu hiç gelmedi / no newline arrived
40    if time.ticks_diff(time.ticks_ms(), son) >= 2000:
41        son = time.ticks_ms()
42        try:
43            sensor.measure()
44            bt.write('{:.1f};{}\n'.format(sensor.temperature(), sensor.humidity()))
45        except OSError:
46            pass                                    # sensör okunamadı / sensor read failed
47    time.sleep_ms(10)

Project ideas with App Inventor and Arduino

  • Bluetooth-controlled car: Map the direction buttons to the letters F, B, L and R and drive the motors with an L298N; every step is in the Arduino robot car guide.
  • Smart home panel: Control a lamp, a socket and a fan from one screen with a four-channel relay module; for ideas, see the Arduino smart home project.
  • Colour picker lamp: Set the red, green and blue values of an RGB LED separately with three sliders.
  • Robot arm: Add one slider per joint and steer the servo motors from your phone.
  • Greenhouse monitor: Show DHT11 and soil moisture sensor readings on the phone and add a button that starts the pump when the soil gets dry.
  • Voice control: Use App Inventor's SpeechRecognizer component to build an app that sends L1 when you say "lights on".

Common mistakes and how to fix them

  • The app connects but the LED won't light: The HC-05's TXD and RXD wires are swapped, or the SoftwareSerial pins in the code don't match the circuit. The module's TXD must go to the Arduino's RX (D10).
  • Commands are ignored: The \n is missing from the text block; the Arduino waits for the newline. Check whether incoming commands show up in the Serial Monitor.
  • Garbled characters: The HC-05's baud rate differs from bt.begin(). The module ships at 9600 baud; if it was changed with AT commands, adjust the code to match.
  • The servo jitters or the connection drops: The servo is dragging down the Arduino's 5 V rail; power it from a separate 5 V supply and join the GNDs. Slight jitter can also come from SoftwareSerial briefly blocking interrupts while it receives; for a permanent fix, use an Arduino Mega or an ESP32, which have spare hardware serial ports. Now and then the servo may also jump to a wrong angle for a moment: the DHT library switches interrupts off for about 5 ms on every reading, and a command arriving right then can get corrupted; the slider's next command puts it right.
  • Values on the phone lag behind: The Clock interval is longer than the Arduino's sending interval, so lines pile up. Keep TimerInterval shorter than the sending interval.
  • The app freezes: DelimiterByte is still 0, or the Arduino doesn't end its lines with \n; ReceiveText(-1) waits for a delimiter that never comes.
  • It doesn't work on iPhone: The HC-05 uses classic Bluetooth, which iOS doesn't allow. An iPhone needs a BLE module (HM-10) or an ESP32.
When troubleshooting, split the chain in two: first test the Arduino side with Serial Bluetooth Terminal, then the App Inventor app. If the terminal works but the app doesn't, the problem is in the blocks; if the terminal fails too, check the wiring or the Arduino code. To double-check pin numbers, use the pinout tool.

Combining App Inventor and Arduino is the most fun way to learn mobile app development and electronics in one project. Once you've set up the connection and the protocol, the same skeleton can become a car, a smart home or a greenhouse remote; the only thing that changes is which letters the buttons send. In the BlueGrays app you'll find the Bluetooth LED control and the L298N Bluetooth-controlled car projects step by step, with circuit diagrams and code.

Frequently asked questions

Does an App Inventor Bluetooth app work on iPhone?

Not with an HC-05 or HC-06. These modules use classic Bluetooth (SPP), and iOS doesn't let ordinary apps open this kind of serial connection. To talk to an iPhone you need to switch to a BLE module such as the HM-10 or to an ESP32; for HC-05 projects, an Android phone is the most trouble-free choice.

Why is the Bluetooth device list empty in App Inventor?

AddressesAndNames only lists devices already paired with the phone, so pair the HC-05 in the Bluetooth settings first using PIN 1234. On Android 12 and later, the list also opens empty the first time if the Nearby devices permission hasn't been granted; grant it and tap Connect again. Make sure Bluetooth is switched on, too.

How do I fix App Inventor error 507?

Error 507 means "Unable to connect. Is the device turned on?": the phone couldn't open a connection to the module. Check that the HC-05 has power, that its LED is blinking fast and that it isn't connected to another phone. If the problem persists, remove the pairing and pair again, or try turning off the BluetoothClient's Secure property.

How do I build an APK in App Inventor?

Open the project and click Build > Android App (.apk) in the top menu. When the build finishes, scan the QR code with your phone or download the APK to your computer and copy it over. During installation you'll need to allow installing unknown apps.

Can App Inventor connect to an ESP32 over Bluetooth?

Yes. With the BluetoothSerial library, the classic ESP32 behaves like an SPP device, and App Inventor's BluetoothClient connects to it just as it does to an HC-05. The ESP32-S3 and ESP32-C3 only support BLE, so on those boards you need the BluetoothLE extension.

Can I publish my App Inventor app on Google Play?

Yes. The Android App Bundle (.aab) option in the Build menu produces a file you can upload to Google Play; you'll need a Google Play developer account. Updates must be signed with the same key, so back up your keystore with Projects > Export keystore.

See all projects
LED Blink
#1Beginner

LED Blink

The very first hello of electronics: switching a single LED on and off. The Arduino turns the LED on and off at short intervals, so the light blinks quickly. You see in the simplest way how the board

4.6(179)
978
Temperature and Humidity with DHT11
#55Intermediate

Temperature and Humidity with DHT11

How warm is your room, and is the air humid or dry? The DHT11 sensor measures temperature and humidity, and the Arduino writes the values on a two-line LCD screen, refreshing every couple of seconds.

4.7(47)
64
Control an LED from Your Phone via Bluetooth
#34Beginner
Premium

Control an LED from Your Phone via Bluetooth

A small rehearsal for switching the lights in your room on and off from your phone. The Bluetooth module passes the letters sent from your phone to the Arduino; one letter turns on the red LED, anothe

4.8(68)
88
Bluetooth Controlled Car with L298N
#80Advanced
Premium

Bluetooth Controlled Car with L298N

Turn your phone into a remote and drive your own robot car. The Arduino receives commands from a remote-control app on your phone through a Bluetooth module and spins the wheels with a motor driver; t

5.0(38)
65
Using a Servo Motor
#35Beginner
Premium

Using a Servo Motor

The arm of a small servo motor turns to three positions in turn: all the way to the start, right to the middle and all the way to the end, waiting two seconds at each stop. The Arduino tells the motor

4.7(48)
67
Using an RGB LED
#39Beginner
Premium

Using an RGB LED

Three colours hide inside a single LED: the RGB LED lights up red, then green, then blue, each colour on for half a second and off for half a second, before the cycle starts again. All you need is one

4.9(73)
98
Lighting 220V Bulb with Push Button
#19Advanced
Premium

Lighting 220V Bulb with Push Button

As long as you hold the button, a real 220-volt bulb stays lit, and it goes out the moment you let go. The Arduino's tiny signal cannot light the bulb directly, so a relay, an electrically controlled

4.4(30)
38
One-Way Communication Between 2 Arduinos
#57Intermediate
Premium

One-Way Communication Between 2 Arduinos

You get two Arduinos talking over a single wire: press the button on one and the LED on the other lights up, press it again and it goes off. The sending board reports the button press with a short mes

4.4(37)
50