INTERFACE AND APPLICATION PROGRAMMING

Individual Assignment

Group Assignment

You can see our Group Assignment here.

Ways Devices Can Communicate to an Interface

Before choosing WiFi for this project, I looked at the different options available for getting a phone or computer to talk to a microcontroller. Each method has different trade-offs in range, speed, complexity, and whether you need extra hardware.

Method Range Needs router? Best for
USB Serial Cable length (~5 m) No Desktop apps, debugging, wired control
Bluetooth (BLE) ~10 m No Phone-to-device, wearables, low power
WiFi (HTTP/REST) ~50 m indoors Yes (or AP mode) Web/mobile apps, multiple clients, fast data
WebSockets Network range Yes Real-time live data, sensor dashboards
MQTT Internet-wide Yes + broker IoT fleets, remote monitoring, publish-subscribe
ESP-NOW ~200 m open air No Board-to-board, no infrastructure needed

I chose WiFi with a simple HTTP REST API because my phone and the ESP32 were on the same network. It needed no extra pairing step, and the React Native fetch() API makes it trivial to call HTTP endpoints. The trade-off is that both devices must be on the same network. It would not work over mobile data without extra setup.

Individual Assignment

For this week, the task was to make an app that talks with a board I made. I reused the output board I made in Week 10 as part of the assignment. The one with the Xiao ESP32-S3, a DRV8825 stepper driver, and a passive buzzer.

I used Wifi to talk to the board from my mobile.

I wrote a small REST API that runs on the ESP32 itself. Then I built a phone app that sends HTTP requests to it. When I hit "Start Motor" on the app, the ESP32 gets the request and drives the stepper. When I hit "Buzzer On", it beeps.

Click here to go to the output devices week page.

Final schematic

Prompt

I used Claude to generate the ESP32 firmware. The full prompt I gave it:

Build a complete system to control a stepper motor and buzzer from an Android app over WiFi.

HARDWARE:
- XIAO ESP32-S3 (Arduino framework)
- DRV8825 stepper driver controlling a NEMA 17 motor (needs separate 12V VMOT supply)
- Passive buzzer
- Pin mapping (use Arduino "D" pin aliases):
  - STEP = D0
  - DIR  = D1
  - ENABLE = D2 (DRV8825 enable is ACTIVE-LOW: LOW = enabled, HIGH = disabled)
  - M0 = D3, M1 = D4, M2 = D5 (microstep select; keep all LOW for full step)
  - BUZZER = D7
- ESP32 joins an existing WiFi network (station mode), not AP mode.
Final schematic

I used Claude in Visual Studio code text ditor to generate the code.

Then I copied each of the files that claude gave me. It gave me two files. One was for the arduino program. And the other was the app file.

ESP32 Firmware

This is the Arduino code that I flashed to the ESP32-S3.

/// motor_buzzer_control.ino
// XIAO ESP32-S3 — DRV8825 stepper + passive buzzer controlled via WiFi REST API
// Driver locked (coils energized) at idle.

#include 
#include 

const char* SSID     = "your-ssid-here";
const char* PASSWORD = "your-password-here";

#define PIN_STEP   D0
#define PIN_DIR    D1
#define PIN_ENABLE D2
#define PIN_M0     D3
#define PIN_M1     D4
#define PIN_M2     D5
#define PIN_BUZZER D7

#define STEP_HALF_PULSE_US 800

WebServer server(80);

volatile bool motorRunning = false;

void addCors() {
  server.sendHeader("Access-Control-Allow-Origin", "*");
  server.sendHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
  server.sendHeader("Access-Control-Allow-Headers", "Content-Type");
}

void handleMotorStart() {
  motorRunning = true;
  digitalWrite(PIN_ENABLE, LOW);   // enable driver
  addCors();
  server.send(200, "application/json", "{\"motor\":\"running\"}");
}

void handleMotorStop() {
  motorRunning = false;
  digitalWrite(PIN_ENABLE, LOW);   // stay enabled = stay locked (holding torque)
  addCors();
  server.send(200, "application/json", "{\"motor\":\"stopped\"}");
}

void handleBuzzerOn() {
  ledcWriteTone(PIN_BUZZER, 1000);
  addCors();
  server.send(200, "application/json", "{\"buzzer\":\"on\"}");
}

void handleBuzzerOff() {
  ledcWriteTone(PIN_BUZZER, 0);
  addCors();
  server.send(200, "application/json", "{\"buzzer\":\"off\"}");
}

void handleNotFound() {
  addCors();
  if (server.method() == HTTP_OPTIONS) { server.send(204); return; }
  server.send(404, "application/json", "{\"error\":\"not found\"}");
}

void setup() {
  Serial.begin(115200);

  pinMode(PIN_STEP,   OUTPUT);
  pinMode(PIN_DIR,    OUTPUT);
  pinMode(PIN_ENABLE, OUTPUT);
  pinMode(PIN_M0,     OUTPUT);
  pinMode(PIN_M1,     OUTPUT);
  pinMode(PIN_M2,     OUTPUT);

  ledcAttach(PIN_BUZZER, 1000, 10);

  digitalWrite(PIN_ENABLE, LOW);   // enable driver at boot = motor locked
  digitalWrite(PIN_DIR,    HIGH);
  digitalWrite(PIN_M0,     LOW);
  digitalWrite(PIN_M1,     LOW);
  digitalWrite(PIN_M2,     LOW);
  digitalWrite(PIN_STEP,   LOW);

  Serial.printf("\nConnecting to %s", SSID);
  WiFi.begin(SSID, PASSWORD);
  int tries = 0;
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    if (++tries > 20) {
      Serial.printf("\nStatus: %d\n", WiFi.status());
      tries = 0;
    }
  }
  Serial.printf("\nConnected! IP: %s\n", WiFi.localIP().toString().c_str());

  server.on("/motor/start", HTTP_GET, handleMotorStart);
  server.on("/motor/stop",  HTTP_GET, handleMotorStop);
  server.on("/buzzer/on",   HTTP_GET, handleBuzzerOn);
  server.on("/buzzer/off",  HTTP_GET, handleBuzzerOff);
  server.onNotFound(handleNotFound);

  server.begin();
  Serial.println("HTTP server started on port 80");
}

void loop() {
  server.handleClient();

  if (motorRunning) {
    digitalWrite(PIN_STEP, HIGH);
    delayMicroseconds(STEP_HALF_PULSE_US);
    digitalWrite(PIN_STEP, LOW);
    delayMicroseconds(STEP_HALF_PULSE_US);
  }
}

After flashing the code successfully, I got the IP address of the ESP32 from the Serial Monitor.

The App

App screenshot

How it works

The ESP32 runs a small HTTP server. The app lets you type in the board's IP address (printed to Serial Monitor at 115200 baud on power-on) and then tap buttons to trigger actions:

Button HTTP endpoint Action
START /motor/start Begin continuous stepper rotation (DRV8825 + NEMA 17)
STOP /motor/stop Halt the stepper
Buzzer ON /buzzer/on Emit a 1 kHz tone
Buzzer OFF /buzzer/off Silence the buzzer

App Code

// App.js — ESP32 Motor & Buzzer Controller
// Expo managed workflow, React Native

import React, { useState } from 'react';
import {
  StyleSheet, View, Text, TextInput, TouchableOpacity,
  SafeAreaView, StatusBar, ScrollView,
} from 'react-native';

const DEFAULT_IP = '192.168.1.50';

export default function App() {
  const [ip, setIp]         = useState('');
  const [status, setStatus] = useState('Idle — enter IP and tap a button.');

  const callEndpoint = async (path) => {
    const host = ip.trim() || DEFAULT_IP;
    const url  = `http://${host}${path}`;
    setStatus(`→ ${url}`);
    try {
      const response = await fetch(url, { method: 'GET' });
      if (!response.ok) {
        setStatus(`HTTP ${response.status} from ${url}`);
        return;
      }
      const json = await response.json();
      setStatus(JSON.stringify(json, null, 2));
    } catch (err) {
      setStatus(`Error: ${err.message}`);
    }
  };

  return (
    <SafeAreaView style={styles.safe}>
      <StatusBar barStyle="light-content" backgroundColor="#0a0a0a" />
      <ScrollView contentContainerStyle={styles.container} keyboardShouldPersistTaps="handled">

        <Text style={styles.title}>ESP32 Controller</Text>
        <Text style={styles.subtitle}>XIAO ESP32-S3 · Motor & Buzzer</Text>

        <View style={styles.card}>
          <Text style={styles.label}>ESP32 IP Address</Text>
          <TextInput
            style={styles.input}
            placeholder={DEFAULT_IP}
            placeholderTextColor="#555"
            value={ip}
            onChangeText={setIp}
            keyboardType="numeric"
            autoCapitalize="none"
            autoCorrect={false}
          />
          <Text style={styles.hint}>
            Find it in Serial Monitor after power-on (115200 baud).
          </Text>
        </View>

        <View style={styles.card}>
          <Text style={styles.label}>Motor (NEMA 17 via DRV8825)</Text>
          <View style={styles.row}>
            <TouchableOpacity style={[styles.btn, styles.btnGreen]} activeOpacity={0.75}
              onPress={() => callEndpoint('/motor/start')}>
              <Text style={styles.btnText}>▶  START</Text>
            </TouchableOpacity>
            <TouchableOpacity style={[styles.btn, styles.btnRed]} activeOpacity={0.75}
              onPress={() => callEndpoint('/motor/stop')}>
              <Text style={styles.btnText}>■  STOP</Text>
            </TouchableOpacity>
          </View>
        </View>

        <View style={styles.card}>
          <Text style={styles.label}>Buzzer (1 kHz tone)</Text>
          <View style={styles.row}>
            <TouchableOpacity style={[styles.btn, styles.btnGreen]} activeOpacity={0.75}
              onPress={() => callEndpoint('/buzzer/on')}>
              <Text style={styles.btnText}>🔔  ON</Text>
            </TouchableOpacity>
            <TouchableOpacity style={[styles.btn, styles.btnOrange]} activeOpacity={0.75}
              onPress={() => callEndpoint('/buzzer/off')}>
              <Text style={styles.btnText}>🔕  OFF</Text>
            </TouchableOpacity>
          </View>
        </View>

        <View style={styles.statusCard}>
          <Text style={styles.statusLabel}>RESPONSE</Text>
          <Text style={styles.statusText}>{status}</Text>
        </View>

      </ScrollView>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  safe:        { flex: 1, backgroundColor: '#0a0a0a' },
  container:   { padding: 20, paddingBottom: 40 },
  title:       { color: '#ffffff', fontSize: 26, fontWeight: '700', textAlign: 'center', marginTop: 8 },
  subtitle:    { color: '#555', fontSize: 12, textAlign: 'center', marginBottom: 24, letterSpacing: 0.8 },
  card:        { backgroundColor: '#181818', borderRadius: 12, padding: 16, marginBottom: 16,
                 borderWidth: 1, borderColor: '#2a2a2a' },
  label:       { color: '#888', fontSize: 11, fontWeight: '600', letterSpacing: 1.2,
                 textTransform: 'uppercase', marginBottom: 10 },
  input:       { backgroundColor: '#111', color: '#fff', borderRadius: 8,
                 paddingHorizontal: 14, paddingVertical: 12, fontSize: 16,
                 borderWidth: 1, borderColor: '#333', marginBottom: 6 },
  hint:        { color: '#444', fontSize: 11, fontStyle: 'italic' },
  row:         { flexDirection: 'row', gap: 12 },
  btn:         { flex: 1, paddingVertical: 18, borderRadius: 10, alignItems: 'center', justifyContent: 'center' },
  btnGreen:    { backgroundColor: '#1a5c1a' },
  btnRed:      { backgroundColor: '#6b1010' },
  btnOrange:   { backgroundColor: '#7a3800' },
  btnText:     { color: '#fff', fontSize: 15, fontWeight: '700', letterSpacing: 0.6 },
  statusCard:  { backgroundColor: '#0d1a0d', borderRadius: 12, padding: 16,
                 borderWidth: 1, borderColor: '#1a3a1a', minHeight: 80 },
  statusLabel: { color: '#3a7a3a', fontSize: 10, fontWeight: '600', letterSpacing: 1.5, marginBottom: 8 },
  statusText:  { color: '#4caf50', fontSize: 13, fontFamily: 'monospace', lineHeight: 20 },
});

1. Imports

import React, { useState } from 'react';
import {
  StyleSheet, View, Text, TextInput, TouchableOpacity,
  SafeAreaView, StatusBar, ScrollView,
} from 'react-native';

These lines of codes are used to import different files to the main code.

2. State Variables

const DEFAULT_IP = '192.168.1.50';

export default function App() {
  const [ip, setIp]         = useState('');
  const [status, setStatus] = useState('Idle — enter IP and tap a button.');

Variable are like containers used to store data like numbers, names etc.

'const' is used to create a variable whose value cannot be changed later.

3. The HTTP Request Function

This is the part that actually sends the request to the board.

  const callEndpoint = async (path) => {
    const host = ip.trim() || DEFAULT_IP;
    const url  = `http://${host}${path}`;
    setStatus(`→ ${url}`);
    try {
      const response = await fetch(url, { method: 'GET' });
      if (!response.ok) {
        setStatus(`HTTP ${response.status} from ${url}`);
        return;
      }
      const json = await response.json();
      setStatus(JSON.stringify(json, null, 2));
    } catch (err) {
      setStatus(`Error: ${err.message}`);
    }
  };

This function is responsible for sending a request from the app to the ESP32 and displaying the response.

It takes the IP address the user gives it and creates a website url to that address. ANd waits to get a response from the device.

Button Pressed
      │
      ▼
Get IP address
      │
      ▼
Use entered IP
      │
      └──► If empty, use DEFAULT_IP
      │
      ▼
Build URL
      │
      ▼
Show URL on screen
      │
      ▼
Send GET request to ESP32
      │
      ▼
Did the request succeed?
      │
 ┌────┴─────┐
 │          │
Yes         No
 │          │
 ▼          ▼
Read JSON   Show HTTP error
 │
 ▼
Display response
 │
 ▼
If any network error occurs
 │
 ▼
Show error message

4. UI — App Shell and Title

  return (
    <SafeAreaView style={styles.safe}>
      <StatusBar barStyle="light-content" backgroundColor="#0a0a0a" />
      <ScrollView contentContainerStyle={styles.container} keyboardShouldPersistTaps="handled">

        <Text style={styles.title}>ESP32 Controller</Text>
        <Text style={styles.subtitle}>XIAO ESP32-S3 · Motor & Buzzer</Text>

This section creates the user interface (UI) that the user can see on the screen.

It contains the status bar, sets the color of background and text and a scrollwheel.

5. UI — IP Address Input

        <View style={styles.card}>
          <Text style={styles.label}>ESP32 IP Address</Text>
          <TextInput
            style={styles.input}
            placeholder={DEFAULT_IP}
            placeholderTextColor="#555"
            value={ip}
            onChangeText={setIp}
            keyboardType="numeric"
            autoCapitalize="none"
            autoCorrect={false}
          />
          <Text style={styles.hint}>
            Find it in Serial Monitor after power-on (115200 baud).
          </Text>
        </View>

This section creates the IP Address input card. It allows the user to type the IP address of the ESP32

It opens a numeric keyboard to type in the IP address.

6. UI — Motor Buttons

        <View style={styles.card}>
          <Text style={styles.label}>Motor (NEMA 17 via DRV8825)</Text>
          <View style={styles.row}>
            <TouchableOpacity style={[styles.btn, styles.btnGreen]} activeOpacity={0.75}
              onPress={() => callEndpoint('/motor/start')}>
              <Text style={styles.btnText}>▶  START</Text>
            </TouchableOpacity>
            <TouchableOpacity style={[styles.btn, styles.btnRed]} activeOpacity={0.75}
              onPress={() => callEndpoint('/motor/stop')}>
              <Text style={styles.btnText}>■  STOP</Text>
            </TouchableOpacity>
          </View>
        </View>

This is the section that contains the buttons to control the motor.. It contains two buttons 'START' and 'STOP' for both.

7. UI — Buzzer Buttons

        <View style={styles.card}>
          <Text style={styles.label}>Buzzer (1 kHz tone)</Text>
          <View style={styles.row}>
            <TouchableOpacity style={[styles.btn, styles.btnGreen]} activeOpacity={0.75}
              onPress={() => callEndpoint('/buzzer/on')}>
              <Text style={styles.btnText}>🔔  ON</Text>
            </TouchableOpacity>
            <TouchableOpacity style={[styles.btn, styles.btnOrange]} activeOpacity={0.75}
              onPress={() => callEndpoint('/buzzer/off')}>
              <Text style={styles.btnText}>🔕  OFF</Text>
            </TouchableOpacity>
          </View>
        </View>

This section controls the functioning of the buzzer.

8. UI — Status Box (where the board's reply appears)

        <View style={styles.statusCard}>
          <Text style={styles.statusLabel}>RESPONSE</Text>
          <Text style={styles.statusText}>{status}</Text>
        </View>

      </ScrollView>
    </SafeAreaView>
  );

It creates a Response section that displays messages returned by the ESP32.

9. Styles

const styles = StyleSheet.create({
  safe:        { flex: 1, backgroundColor: '#0a0a0a' },
  container:   { padding: 20, paddingBottom: 40 },
  title:       { color: '#ffffff', fontSize: 26, fontWeight: '700', textAlign: 'center', marginTop: 8 },
  subtitle:    { color: '#555', fontSize: 12, textAlign: 'center', marginBottom: 24, letterSpacing: 0.8 },
  card:        { backgroundColor: '#181818', borderRadius: 12, padding: 16, marginBottom: 16,
                 borderWidth: 1, borderColor: '#2a2a2a' },
  label:       { color: '#888', fontSize: 11, fontWeight: '600', letterSpacing: 1.2,
                 textTransform: 'uppercase', marginBottom: 10 },
  input:       { backgroundColor: '#111', color: '#fff', borderRadius: 8,
                 paddingHorizontal: 14, paddingVertical: 12, fontSize: 16,
                 borderWidth: 1, borderColor: '#333', marginBottom: 6 },
  hint:        { color: '#444', fontSize: 11, fontStyle: 'italic' },
  row:         { flexDirection: 'row', gap: 12 },
  btn:         { flex: 1, paddingVertical: 18, borderRadius: 10, alignItems: 'center', justifyContent: 'center' },
  btnGreen:    { backgroundColor: '#1a5c1a' },
  btnRed:      { backgroundColor: '#6b1010' },
  btnOrange:   { backgroundColor: '#7a3800' },
  btnText:     { color: '#fff', fontSize: 15, fontWeight: '700', letterSpacing: 0.6 },
  statusCard:  { backgroundColor: '#0d1a0d', borderRadius: 12, padding: 16,
                 borderWidth: 1, borderColor: '#1a3a1a', minHeight: 80 },
  statusLabel: { color: '#3a7a3a', fontSize: 10, fontWeight: '600', letterSpacing: 1.5, marginBottom: 8 },
  statusText:  { color: '#4caf50', fontSize: 13, fontFamily: 'monospace', lineHeight: 20 },
});

Responsible for the appearance of the app

Making the App Work on My Phone

Writing the code is only half the job. To actually run it on my Android phone I needed to convert i into a apk file.

Step 1 — Install Android Studio

Android Studio is the official IDE from Google for building Android apps.

Even though I'm not writing Java or Kotlin, I still need it to compile the native Android project that Expo generates. I downloaded it from developer.android.com/studio and ran the installer. The setup wizard installs the Android SDK and all the build tools automatically. I just clicked through it and let it download what it needed.

Android Studio installer

Step 2 — Generate the Android project files

I went to the folder where I stored the code files and opened terminal. Then I typed the following line of code:

npx expo prebuild --platform android

This created a full Android project inside the folder, Gradle files, manifest, all of it. I only had to do this once.

Step 3 — Bundle the JavaScript code

React Native apps are split into two parts: the native Android shell, and the JavaScript bundle that runs inside it. This command packages all the app code into a single bundle file and drops it where Android Studio can find it:

npx react-native bundle --platform android --dev false --entry-file node_modules/expo/AppEntry.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res

Without this step, the APK would build but crash on launch because the JavaScript wouldn't be inside it.

Step 4 — Open the project in Android Studio

I opened Android Studio, clicked Open, and selected the android folder that Expo had generated inside my project.

Android Studio with the android folder open

Android Studio then spent a few minutes syncing Gradle and downloading any missing SDK components.

I just waited for the loading bar at the bottom to finish. It shows "Gradle sync finished" when it's done.

Step 5 — Build the APK

Once the project loaded, I went to the top menu: Build → Generate App Bundle / APK. I chose APK.

Not Bundle. a Bundle is for the Play Store, an APK is what you sideload directly onto a phone.

Then I clicked through the wizard and hit Build.

Build APK menu in Android Studio

Android Studio compiled everything and saved the APK file to android/app/build/outputs/apk/release/. There's a small notification at the bottom of the screen with a locate link when it finishes.

Step 6 — Install on my phone

I sent the APK file to my phone over WhatsApp. Tapped it to open it. My phone warned me that it's from an unknown source since it didn't come from the Play Store.

App installed and running on the phone

I tapped Allow from this source in the settings it opened, then went back and tapped Install.

Android unknown sources permission screen

The app installed and opened.

Phone showing the APK file received on WhatsApp

Results

Group Assignment

This week our group explored different interface types and communication methods with our hardware projects. Most members built an interface application connected to their final project, spanning web apps, mobile apps, and varied device communication protocols.

Project Member Interface Microcontroller Communication Direction
Butterflies (music player) Architha Web app XIAO ESP32-S3 WebSocket over WiFi (port 81) Two-way
DODO (task list) Kurian Web app (HTML/CSS/JS) XIAO ESP32-S3 HTTP REST (GET/POST), 3s polling Two-way
Pixel Game Ashtami Godot 2D game XIAO RP2040 USB HID (keyboard emulation) One-way (device → PC)
Pixel Plant Kevin Pygame desktop game Microcontroller board Serial (PySerial) Two-way
Smart Piggy Michael Mobile app (Kodular) XIAO ESP32-C6 WiFi via Firebase Realtime DB Two-way (cloud sync)
Time Sync Ardhra Web app ESP32-S3 + ATtiny1614 BLE → UART → I2C (DS1307 RTC) One-way (app → device)
SmartToolBox Nadec Mobile app ESP32 WiFi via Firebase Firestore Two-way (cloud sync)
Group assignment photo
Image source

View full group assignment page →

Download Arduino Code

Download APK File

Board Design