15. Interface and Application Programming

Individual Assignment: Application Interface for My Robotic Arm Lamp Prototype

Project Title: Web-Based User Interface for Controlling a Robotic Arm Lamp Prototype

Assignment Objective

For this assignment, I designed and programmed an application that interfaces a user with output devices connected to my embedded board. My final project is a robotic arm lamp, so I used this assignment as a functional prototype.

I created a web-based control interface that allows the user to control:

Link to Group Assignment

Group assignment page: https://fabacademy.org/2026/labs/chaihuo/docs/week15/chaihuo/week15_group_assignment

For implementation, I mainly used Cursor for writing and refining documentation and project structure, and Arduino IDE for compiling and uploading firmware to ESP32. I also used ChatGPT as a coding assistant, especially to speed up drafting the web server logic and HTTP route handlers. After generating initial code, I tested each feature on hardware and adjusted pin mapping, route behavior, and network setup until the interface and output devices worked reliably.

I compared different tool options for application and interface programming, including browser-based control methods, desktop-based workflows, and message-based IoT approaches. Through this comparison, I decided to use a web-based interface hosted directly on XIAO ESP32 because it gives a good balance of simplicity, portability, and real-time interaction for my prototype.

Project Overview

The user opens a webpage from a phone or laptop browser. The page includes:

XIAO ESP32 receives HTTP requests and then controls the servo motor and NeoPixel LED.

Relevance to Final Project

This prototype demonstrates the core interaction model of my final project.

Final Project FunctionAssignment Prototype
Robotic arm movementOne servo motor controls one joint
Lamp lightingOne NeoPixel controls color and brightness
User control systemWeb-based control interface
Communication systemHTTP requests over Wi-Fi

Input and Output Devices

Input

The input is the web user interface in the browser.

Output

Technology Track

CategoryTool / Technology
Embedded boardXIAO ESP32
Programming languageArduino C/C++
User interfaceHTML, CSS, JavaScript
Communication protocolHTTP over Wi-Fi
Servo controlESP32Servo library
LED controlAdafruit NeoPixel library
Application typeWeb-based interface

Method Comparison

MethodHow It WorksAdvantagesDisadvantages
HTTP Web Server ESP32 hosts webpage and receives control requests Simple, wireless, no extra app, easy to document Usually best on same Wi-Fi network
MQTT + Node-RED Node-RED publishes MQTT messages to ESP32 Scalable, good for multi-device systems Needs broker, more setup and debugging
USB Serial + Python Tkinter Desktop app sends serial commands by USB Stable and easy to debug Wired to computer, less suitable for smart lamp use
Method Comparison

Why I Chose HTTP Web Server

Example requests:

System Diagram

Phone / Laptop Browser
Webpage UI
sliders and buttons
HTTP request over Wi-Fi
XIAO ESP32 Web Server
Servo + NeoPixel LED

Hardware Used

XIAO ESP32
ComponentPurpose
XIAO ESP32Main embedded board
Servo motorControls lamp arm / lamp head position
NeoPixel LEDRepresents lamp light

Wiring

Servo

Servo WireConnection
SignalXIAO ESP32 D2
VCCExternal 5V
GNDCommon GND

NeoPixel

NeoPixel PinConnection
DINXIAO ESP32 D3
VCC5V
GNDCommon GND

Important note: XIAO ESP32 GND, servo GND, NeoPixel GND, and external power GND must be connected together.

UI Design

The webpage includes:

UI Design

The UI is intentionally simple so communication behavior is easy to test and debug.

Communication Between App and Board

The board runs as a web server. When the user moves sliders or clicks buttons, JavaScript fetch() sends HTTP requests to ESP32 routes. ESP32 parses parameters and updates servo or NeoPixel state.

User ActionHTTP RequestBoard Action
Move servo to 90/servo?angle=90Servo moves to 90
Set brightness to 180/brightness?value=180NeoPixel brightness changes
Set color/color?r=255&g=100&b=50NeoPixel color updates
Turn off light/offNeoPixel turns off
Reset/resetServo to 90, light reset

Programming Process

  1. Set up the development environment: Installed the ESP32 board package in Arduino IDE and selected the correct XIAO ESP32 board profile. ESP32 package ESP32 package
  2. Install required libraries: Added ESP32Servo and Adafruit NeoPixel so the firmware can control both actuator and lighting. Libraries Libraries
  3. Test each output device independently: Verified servo angle control (0/90/180) and NeoPixel color/brightness using separate test sketches before system integration.
  4. Implement Wi-Fi connection and web server: Developed the web server and HTTP route logic, connected the board to the hotspot, started the server, and verified the assigned IP address in Serial Monitor.
  5. Build the web interface: Created an HTML/CSS/JavaScript page with a servo slider, brightness slider, color control, and reset/off buttons.
  6. Map UI actions to HTTP routes: Implemented /servo, /brightness, /color, /off, and /reset handlers on the board.
  7. Integrate and debug end-to-end behavior: Validated that browser actions trigger correct hardware responses, then fixed pin mapping, hotspot/network, and NeoPixel color update issues.

Source Code (Full Firmware)

#include <WiFi.h>
#include <WebServer.h>
#include <ESP32Servo.h>
#include <Adafruit_NeoPixel.h>

// ==================================================
// Wi-Fi Settings
// Change these to your own Wi-Fi name and password
// ==================================================
const char* ssid = "YOUR_WIFI_NAME";
const char* password = "YOUR_WIFI_PASSWORD";

// ==================================================
// Pin Settings
// ==================================================
const int SERVO_PIN = D2;
const int NEOPIXEL_PIN = D3;
const int NUM_PIXELS = 1;

// ==================================================
// Objects
// ==================================================
WebServer server(80);
Servo lampServo;
Adafruit_NeoPixel pixels(NUM_PIXELS, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);

// ==================================================
// Current Lamp States
// ==================================================
int servoAngle = 90;
int brightness = 100;

int redValue = 255;
int greenValue = 255;
int blueValue = 255;

// ==================================================
// Function: Update NeoPixel
// ==================================================
void updateNeoPixel() {
  pixels.setBrightness(brightness);

  for (int i = 0; i < NUM_PIXELS; i++) {
    pixels.setPixelColor(i, pixels.Color(redValue, greenValue, blueValue));
  }

  pixels.show();
}

// ==================================================
// Function: Turn off NeoPixel
// ==================================================
void turnOffNeoPixel() {
  redValue = 0;
  greenValue = 0;
  blueValue = 0;

  updateNeoPixel();
}

// ==================================================
// Web Page
// ==================================================
String webPage() {
  String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Robotic Arm Lamp Controller</title>

  <style>
    body {
      margin: 0;
      padding: 30px;
      font-family: Arial, sans-serif;
      background: #f2f2f2;
      text-align: center;
    }

    .container {
      max-width: 480px;
      margin: auto;
      background: white;
      padding: 25px;
      border-radius: 20px;
      box-shadow: 0 4px 18px rgba(0, 0, 0, 0.15);
    }

    h1 {
      font-size: 24px;
      margin-bottom: 8px;
    }

    p {
      color: #555;
      font-size: 15px;
    }

    h2 {
      margin-top: 28px;
      font-size: 20px;
    }

    .value {
      font-size: 28px;
      font-weight: bold;
      margin: 12px;
    }

    input[type="range"] {
      width: 90%;
    }

    input[type="color"] {
      width: 110px;
      height: 60px;
      border: none;
      background: none;
      cursor: pointer;
      margin: 10px;
    }

    button {
      font-size: 16px;
      padding: 12px 18px;
      margin: 8px;
      border: none;
      border-radius: 10px;
      color: white;
      cursor: pointer;
    }

    .offButton {
      background: #333;
    }

    .resetButton {
      background: #673AB7;
    }

    .status {
      margin-top: 18px;
      font-size: 14px;
      color: #333;
    }
  </style>
</head>

<body>
  <div class="container">
    <h1>Robotic Arm Lamp Controller</h1>
    <p>Control one servo motor and one NeoPixel with XIAO ESP32.</p>

    <h2>Servo Angle</h2>
    <div class="value">
      <span id="servoValue">90</span>°
    </div>

    <input
      type="range"
      min="0"
      max="180"
      value="90"
      id="servoSlider"
      oninput="setServo(this.value)"
    >

    <h2>Light Brightness</h2>
    <div class="value">
      <span id="brightnessValue">100</span>
    </div>

    <input
      type="range"
      min="0"
      max="255"
      value="100"
      id="brightnessSlider"
      oninput="setBrightness(this.value)"
    >

    <h2>Light Color</h2>
    <input
      type="color"
      id="colorPicker"
      value="#ffffff"
      onchange="setColor(this.value)"
    >

    <br>

    <button class="offButton" onclick="turnOffLight()">Turn Off Light</button>
    <button class="resetButton" onclick="resetLamp()">Reset</button>

    <div class="status" id="statusText">
      Status: Ready
    </div>
  </div>

  <script>
    function setServo(angle) {
      document.getElementById("servoValue").innerText = angle;
      document.getElementById("statusText").innerText =
        "Status: Servo angle set to " + angle + "°";

      fetch("/servo?angle=" + angle);
    }

    function setBrightness(value) {
      document.getElementById("brightnessValue").innerText = value;
      document.getElementById("statusText").innerText =
        "Status: Brightness set to " + value;

      fetch("/brightness?value=" + value);
    }

    function setColor(hexColor) {
      let r = parseInt(hexColor.substring(1, 3), 16);
      let g = parseInt(hexColor.substring(3, 5), 16);
      let b = parseInt(hexColor.substring(5, 7), 16);

      document.getElementById("statusText").innerText =
        "Status: Color set to RGB(" + r + ", " + g + ", " + b + ")";

      fetch("/color?r=" + r + "&g=" + g + "&b=" + b);
    }

    function turnOffLight() {
      document.getElementById("statusText").innerText =
        "Status: Light turned off";

      fetch("/off");
    }

    function resetLamp() {
      document.getElementById("servoSlider").value = 90;
      document.getElementById("servoValue").innerText = 90;

      document.getElementById("brightnessSlider").value = 100;
      document.getElementById("brightnessValue").innerText = 100;

      document.getElementById("colorPicker").value = "#ffffff";

      document.getElementById("statusText").innerText =
        "Status: Lamp reset";

      fetch("/reset");
    }
  </script>
</body>
</html>
)rawliteral";

  return html;
}

void handleServo();
void handleBrightness();
void handleColor();
void handleOff();
void handleReset();

// ==================================================
// Route: Home Page
// ==================================================
void handleRoot() {
  server.send(200, "text/html", webPage());
}

// ==================================================
// Route: Servo Control
// Example: /servo?angle=90
// ==================================================
void handleServo() {
  if (!server.hasArg("angle")) {
    server.send(400, "text/plain", "Missing servo angle");
    return;
  }

  servoAngle = server.arg("angle").toInt();
  servoAngle = constrain(servoAngle, 0, 180);

  lampServo.write(servoAngle);

  Serial.print("Servo angle: ");
  Serial.println(servoAngle);

  server.send(200, "text/plain", "Servo angle updated");
}

// ==================================================
// Route: Brightness Control
// Example: /brightness?value=120
// ==================================================
void handleBrightness() {
  if (!server.hasArg("value")) {
    server.send(400, "text/plain", "Missing brightness value");
    return;
  }

  brightness = server.arg("value").toInt();
  brightness = constrain(brightness, 0, 255);

  updateNeoPixel();

  Serial.print("Brightness: ");
  Serial.println(brightness);

  server.send(200, "text/plain", "Brightness updated");
}

// ==================================================
// Route: Color Picker Control
// Example: /color?r=255&g=100&b=50
// ==================================================
void handleColor() {
  if (!server.hasArg("r") || !server.hasArg("g") || !server.hasArg("b")) {
    server.send(400, "text/plain", "Missing RGB values");
    return;
  }

  redValue = constrain(server.arg("r").toInt(), 0, 255);
  greenValue = constrain(server.arg("g").toInt(), 0, 255);
  blueValue = constrain(server.arg("b").toInt(), 0, 255);

  updateNeoPixel();

  Serial.print("Color RGB: ");
  Serial.print(redValue);
  Serial.print(", ");
  Serial.print(greenValue);
  Serial.print(", ");
  Serial.println(blueValue);

  server.send(200, "text/plain", "Color updated");
}

// ==================================================
// Route: Turn Off Light
// Example: /off
// ==================================================
void handleOff() {
  turnOffNeoPixel();

  Serial.println("Light turned off");

  server.send(200, "text/plain", "Light turned off");
}

// ==================================================
// Route: Reset Lamp
// Example: /reset
// ==================================================
void handleReset() {
  servoAngle = 90;
  brightness = 100;

  redValue = 255;
  greenValue = 255;
  blueValue = 255;

  lampServo.write(servoAngle);
  updateNeoPixel();

  Serial.println("Lamp reset: Servo 90°, white light, brightness 100");

  server.send(200, "text/plain", "Lamp reset");
}

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

  // Servo setup
  lampServo.setPeriodHertz(50);
  lampServo.attach(SERVO_PIN, 500, 2400);
  lampServo.write(servoAngle);

  // NeoPixel setup
  pixels.begin();
  pixels.clear();
  updateNeoPixel();

  // Wi-Fi setup
  Serial.println();
  Serial.println("Connecting to Wi-Fi...");

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println();
  Serial.println("Wi-Fi connected.");
  Serial.print("Open this IP address in your browser: ");
  Serial.println(WiFi.localIP());

  // Web server routes
  server.on("/", handleRoot);
  server.on("/servo", handleServo);
  server.on("/brightness", handleBrightness);
  server.on("/color", handleColor);
  server.on("/off", handleOff);
  server.on("/reset", handleReset);

  server.begin();

  Serial.println("Web server started.");
}

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

Problems Encountered and Fixes

Problem 1: Confusion about pin symbols (D1, A0, or GPIO number)

I was confused about whether to use board pin labels (like D1/A0) or GPIO numbers in code. I assigned the wrong pin at first, so the hardware did not respond as expected.

Fix

I checked the XIAO ESP32 pin mapping and used the correct pin definitions consistently in both wiring and code (servo on D2 and NeoPixel on D3). After correcting the pin mapping, the outputs worked normally.

Problem 2: Wi-Fi hotspot connection mismatch

The board connected to the hotspot, but the laptop was not on the same hotspot network, so I could not access the control webpage.

Fix

I verified the hotspot name and password in code and made sure the laptop joined the same hotspot. Then I used the IP printed in Serial Monitor to open the webpage successfully.

Problem 3: Website UI changed color, but NeoPixel color did not change

The browser UI updated correctly, but the NeoPixel stayed the same color, which meant the color command was not being applied properly on the board side.

Fix

I updated the color route code to parse RGB values correctly, then called the NeoPixel update function after setting the new values. After this code fix, UI color changes matched the real NeoPixel color.

Learning Reflection

Based on our group comparison in the Week 15 group page, my biggest takeaway is that tool choice should follow the real product context, not just technical preference. MQTT + Node-RED is powerful and scalable, and USB serial control is very stable for debugging, but for my robotic arm lamp prototype, HTTP on XIAO ESP32 gave the best balance of simplicity, portability, and user experience.

I also learned that a "working demo" is different from a reliable interaction system. The group tests highlighted trade-offs between setup complexity, network dependency, and maintainability. In my individual implementation, these lessons directly changed my decisions: I kept the interface minimal, used explicit route design, and added validation in handlers so user actions translate into predictable hardware behavior.

Another important reflection is that interface programming for embedded systems is not only about frontend controls. It also includes communication clarity, timing behavior, and power stability. Debugging servo jitter and inconsistent LED response made me realize that software structure and hardware constraints must be designed together. This gives me a clearer direction for my final project: build interaction features incrementally, test each layer independently, and only then integrate them into the full robotic arm lamp system.

Hero Shot

Final setup photo showing the web UI controlling the robotic arm lamp prototype in real time.

← Week 14 Back to Assignments Week 16 →