2-Axis Aircraft Attitude Simulator
Build a Wi-Fi-controlled gimbal that demonstrates aircraft pitch and yaw.
Introduction
30 Second Summary
Ever noticed how a pilot tilts the nose of a plane up to climb or turns it left to change direction? Those movements have specific names in aviation: pitch and yaw.
In this project, you will build a physical aircraft attitude simulator using two servo motors controlled wirelessly from your phone or PC browser via an ESP32 microcontroller.
What You'll Build
A 2-axis servo gimbal connected to an ESP32 that hosts a Wi-Fi web server, allowing real-time pitch and yaw control through browser-based sliders.
By the end of this project, you'll have:
- A working 2-axis gimbal that physically demonstrates aircraft pitch and yaw movements using two servo motors.
- An ESP32-hosted web interface accessible from your phone or PC that controls the gimbal in real time via sliders.
- A complete embedded IoT project suitable for a final year university presentation.
- Secret Mission: Add preset flight attitude buttons with smooth animated transitions.
Are there any prerequisites?
You should have Arduino IDE installed on your PC, a basic understanding of C/C++ syntax, and familiarity with breadboard wiring.
You will also need hardware components: an ESP32 DevKit, two SG90 micro servo motors, a breadboard, jumper wires, a micro-USB cable, and a 5V external power supply (1A minimum).
Set Up Arduino IDE for ESP32 Development
The ESP32 is a microcontroller with built-in Wi-Fi, but Arduino IDE doesn't support it out of the box. You need to tell the IDE where to find ESP32 board definitions so it can compile and upload code to your board.
In this step, you'll add ESP32 board support, install the servo control library, and confirm your board is communicating with your PC.
In this step, get ready to:
- Add ESP32 board support to Arduino IDE.
- Install the ESP32Servo library for servo motor control.
- Test the ESP32 connection with a Wi-Fi scan.
Add ESP32 board support to Arduino IDE
Arduino IDE needs a board support package to understand how to compile code for the ESP32's processor. You install this by pointing the IDE at Espressif's official package URL.
- In Arduino IDE, click File in the top menu bar.
- Click Preferences.
- Find the Additional Board Manager URLs field near the bottom of the window.
- Paste the following URL into that field:
https://espressif.github.io/arduino-esp32/package_esp32_index.json
- Click OK to close Preferences.
What does this URL do?
This URL points Arduino IDE to Espressif's package index. It tells the IDE where to download the compiler, uploader tools, and board definitions needed to build firmware for ESP32 chips.
- Go to Tools > Board > Boards Manager.
- In the search field, type ESP32.
- Find esp32 by Espressif Systems in the results list.
- Click Install. This download may take a few minutes.
Once installation finishes, you need to select the correct board type for your ESP32 DevKit.
- Go to Tools > Board > ESP32 Arduino.
- Select ESP32 Dev Module from the list.
Verify that Tools > Board now shows ESP32 Dev Module selected.
Don't see ESP32 in Boards Manager?
Double-check that you pasted the full URL correctly in File > Preferences. Make sure there are no extra spaces before or after the URL.
If you already had other URLs in the field, separate them with a comma.
Help me troubleshoot ESP32 board installation in Arduino IDE
Install the ESP32Servo library
The standard Arduino Servo library doesn't work on ESP32 because the ESP32 uses a different PWM system. The ESP32Servo library handles this automatically so you can control servo motors with familiar commands.
- Go to Sketch > Include Library > Manage Libraries.
- In the search field, type ESP32Servo.
- Find the library by Kevin Harrington, John K. Bennett (madhephaestus).
- Install version 3.2.0.
Verify that Sketch > Include Library now lists ESP32Servo under Contributed Libraries.
What is ESP32Servo?
ESP32Servo translates simple angle commands (like "move to 90 degrees") into the precise PWM timing signals that servo motors expect. It supports up to 16 servos on the ESP32's hardware channels.
Can't find ESP32Servo in Library Manager?
Make sure you type ESP32Servo as one word with no space. The correct library author is Kevin Harrington / madhephaestus.
Help me find and install ESP32Servo
Test the ESP32 connection
Before writing any custom code, you should confirm that your PC can talk to the ESP32 board. You'll upload a built-in example sketch that scans for nearby Wi-Fi networks and prints the results to the Serial Monitor.
- Connect your ESP32 DevKit to your PC using the micro-USB cable.
- Go to Tools > Port and select the COM port that appeared (e.g., COM3 or COM4).
Which COM port is my ESP32?
If multiple COM ports appear, unplug the ESP32, check which port disappears, then plug it back in. The port that reappears is your ESP32.
- Go to File > Examples > WiFi > WiFiScan.
- Click the Upload button (the right-pointing arrow icon in the toolbar). Wait for the upload to complete.
- Go to Tools > Serial Monitor.
- Set the baud rate dropdown (bottom-right of the Serial Monitor window) to 115200.
You should see a list of nearby Wi-Fi networks appearing in the Serial Monitor output. This confirms your ESP32 is powered, communicating over USB, and its Wi-Fi radio is working.
✔️ I see Wi-Fi networks listed
Your ESP32 is connected and working. You're ready to move on to wiring the servos.
ⓧ I see garbled text or nothing at all
- Make sure the baud rate in Serial Monitor is set to 115200. A mismatch produces garbled characters.
- Try pressing the EN (reset) button on your ESP32 board to restart the sketch.
- If the Serial Monitor is completely blank, verify you selected the correct COM port under Tools > Port.
Help me troubleshoot my ESP32 Serial Monitor connection
ⓧ Upload failed with an error
- Check that Tools > Board is set to ESP32 Dev Module.
- Confirm Tools > Port shows a COM port (not grayed out). If no port appears, try a different USB cable. Some micro-USB cables are charge-only and don't carry data.
- On some ESP32 boards, you need to hold the BOOT button while the IDE says "Connecting..." and release it once upload begins.
Help me fix my ESP32 upload error
Your development environment is ready. Next up, you'll wire the two servo motors to the ESP32 and run a sweep test to confirm they move smoothly.
Wire the Servos and Run a Sweep Test
Your ESP32 is set up and communicating with your PC. Now it is time to connect the servo motors that will physically move your gimbal.
In this step, you will wire two SG90 servo motors to the ESP32 using an external power supply, then upload a sweep test sketch to confirm everything moves smoothly.
In this step, get ready to:
- Wire two SG90 servos to the ESP32 with an external 5V power supply.
- Upload a sweep test sketch that moves both servos back and forth.
- Confirm both servos operate reliably within a safe working range.
Wire two SG90 servos to the ESP32
Each SG90 servo has three wires: a signal wire (orange or yellow), a power wire (red), and a ground wire (brown or black). You will connect the signal wires to specific GPIO pins on the ESP32.
Do NOT power servos from the ESP32
The ESP32's 3.3V pin cannot supply enough current for servo motors. Powering servos directly from the board can cause it to reset or become damaged. Always use an external 5V power supply (1A minimum) for your servos.
Wire the pitch servo first:
- Connect the signal wire (orange/yellow) to GPIO 18 on the ESP32.
- Connect the red wire to the positive terminal of your external 5V power supply.
- Connect the brown/black wire to the ground terminal of your external 5V power supply.
Now wire the yaw servo:
- Connect the signal wire (orange/yellow) to GPIO 19 on the ESP32.
- Connect the red wire to the positive terminal of your external 5V power supply (same supply as the pitch servo).
- Connect the brown/black wire to the ground terminal of your external 5V power supply.
Finally, establish the common ground:
- Connect the external power supply's ground terminal to a GND pin on the ESP32 using a jumper wire.
Why is common ground essential?
The ESP32 sends control signals to the servos through the GPIO pins. For the servos to correctly interpret these signals, both the ESP32 and the servos need to share the same ground reference point. Without this shared ground, the signal voltage levels become meaningless and the servos will not respond.
You will not see any movement yet. The next substep uploads code that commands the servos to move, which confirms your wiring is correct.
Upload and run a sweep test sketch
A sweep test moves both servos smoothly from one end of their range to the other and back again in a loop. This confirms both wiring and power are correct.
- In Arduino IDE, create a new sketch by clicking File then New Sketch.
- Delete any code in the new sketch and paste the following sweep test code:
#include <ESP32Servo.h>
// Create servo objects for each axis
Servo pitchServo;
Servo yawServo;
// Define GPIO pins for servo signal wires
const int PITCH_PIN = 18;
const int YAW_PIN = 19;
void setup() {
// Attach each servo to its designated pin
pitchServo.attach(PITCH_PIN);
yawServo.attach(YAW_PIN);
}
void loop() {
// Sweep both servos from 10 to 170 degrees
for (int pos = 10; pos <= 170; pos++) {
pitchServo.write(pos);
yawServo.write(pos);
delay(10);
}
// Sweep both servos back from 170 to 10 degrees
for (int pos = 170; pos >= 10; pos--) {
pitchServo.write(pos);
yawServo.write(pos);
delay(10);
}
}
What does this code do?
The #include <ESP32Servo.h> line imports the servo library that generates the correct PWM signals for the ESP32 chip.
In setup(), each servo object is attached to its GPIO pin. This tells the library which pin to send control signals through.
In loop(), two for loops sweep both servos between 10 and 170 degrees. The 10ms delay between each degree creates a smooth, visible motion. The range is intentionally limited to avoid the jittery behaviour SG90 servos exhibit near 0 and 180 degrees.
- Save the sketch by pressing Ctrl+S. When prompted for a name, type attitude_simulator and click Save.
- Click the Upload button (right arrow icon) in the toolbar to compile and upload the sketch to your ESP32.
- Watch your servos. Both should begin sweeping smoothly back and forth continuously.
Servos not moving?
If you see an upload error, confirm the correct COM port is still selected under Tools then Port.
If the upload succeeds but servos do not move, check that your signal wires are firmly seated in GPIO 18 and GPIO 19. Also confirm your external power supply is switched on.
If the ESP32 resets repeatedly (the Serial Monitor shows the startup message over and over), your power supply cannot deliver enough current. Switch to a supply rated at 1A or higher.
Help me troubleshoot my servo sweep test
✔️ Awesome, I've got everything!
Great. Double check you have saved your sketch before continuing.
ⓧ I'd like to double check the full code
#include <ESP32Servo.h>
Servo pitchServo;
Servo yawServo;
const int PITCH_PIN = 18;
const int YAW_PIN = 19;
void setup() {
pitchServo.attach(PITCH_PIN);
yawServo.attach(YAW_PIN);
}
void loop() {
for (int pos = 10; pos <= 170; pos++) {
pitchServo.write(pos);
yawServo.write(pos);
delay(10);
}
for (int pos = 170; pos >= 10; pos--) {
pitchServo.write(pos);
yawServo.write(pos);
delay(10);
}
}
Troubleshoot common issues
If your servos are sweeping but you notice odd behaviour at certain positions, here are two common causes and their fixes.
Why constrain to 10-170 degrees?
Cheap SG90 servos often jitter, buzz, or stall at the absolute ends of their range (near 0 and 180 degrees). The sweep test code already uses 10-170 to stay within the reliable zone. This gives you 160 degrees of smooth, predictable movement per axis.
If you still see jitter at the edges, you can tighten the range further to 20-160 degrees, though this is rarely necessary.
ESP32 resets during servo movement?
This is almost always a power issue. Double-check that both servo power wires connect to the external 5V supply (not the ESP32) and that the external supply's ground connects to an ESP32 GND pin.
If the resets happen only when both servos move simultaneously, your power supply cannot deliver enough current. Upgrade to a supply rated at 1A or higher.
Help me fix my ESP32 resetting during servo movement
Both servos are sweeping smoothly across their full range. Next up, you will build the physical gimbal frame that arranges these two servos perpendicular to each other, giving you independent pitch and yaw axes.
Build the 2-Axis Gimbal Frame
Your servos are wired and sweeping smoothly. Now it's time to give them a mechanical purpose.
In this step, you'll build a gimbal frame that arranges your two servos on perpendicular axes. This creates two independent rotations: yaw (left/right) at the base, and pitch (up/down) on top.
In this step, get ready to:
- Design the gimbal layout with two perpendicular rotation axes.
- Assemble the yaw and pitch servos into a working gimbal frame.
- Verify both axes move freely and independently using the sweep test.
Design the gimbal structure
A gimbal is a mechanical frame that allows rotation around multiple axes independently. Your gimbal needs exactly two perpendicular axes to simulate aircraft pitch and yaw.
Here's how the two axes relate to each other: the yaw servo sits at the base and rotates the entire upper assembly left and right. The pitch servo is mounted on top of the yaw servo's output arm and tilts a platform up and down.
- Decide on your frame material. If you have a 3D printer, search for a "pan-tilt bracket SG90" model (standard SG90 mounting holes are 27.5mm apart). If building from scratch, gather stiff cardboard, plywood scraps, or rigid plastic sheets.
- Sketch or plan the layout: a flat base holds the yaw servo upright, with the pitch servo mounted perpendicular on the yaw servo's horn.
- Gather hot glue, tape, or small screws for mounting the servos.
What is a pan-tilt bracket?
A pan-tilt bracket is a ready-made plastic frame designed to hold two servos at 90 degrees to each other. "Pan" is the horizontal rotation (yaw) and "tilt" is the vertical rotation (pitch). These are cheap and widely available online for SG90-sized servos.
If you don't have one, a scratch-built frame from stiff cardboard works perfectly for a demonstration.
Assemble the gimbal
Now you'll physically build the two-axis frame. The key principle is that the yaw servo's rotation must carry the entire pitch assembly with it.
- Mount the yaw servo vertically to your stable base. The servo body should be fixed (glued, taped, or screwed) with its output shaft pointing upward. You can tape or glue the servo directly to a flat surface like a piece of wood, a small box, or a desk corner.
- Attach a cross-shaped or single-arm servo horn to the yaw servo's output shaft.
- Mount the pitch servo on top of the yaw servo horn so that it rotates at 90 degrees to the yaw axis. The pitch servo body attaches to the yaw horn, and its own output shaft points sideways.
- Attach a servo horn to the pitch servo's output shaft.
- Fix a small platform or aircraft silhouette (cut from cardboard) to the pitch servo horn. This represents the aircraft body that tilts and turns.
- Check that the servo cables have enough slack to allow full rotation on both axes without pulling tight or binding.
Test each joint before finalizing
Before gluing everything permanently, hold the pieces in place and manually rotate each axis by hand. Confirm the yaw rotation carries the upper assembly smoothly, and the pitch rotation tilts the platform without wobbling. Only secure joints once you're happy with the range of motion.
- Manually rotate the yaw servo horn. The entire upper assembly (pitch servo and platform) should move left and right together.
- Manually rotate the pitch servo horn. Only the platform should tilt up and down, independently of the yaw position.
Platform wobbles or axes aren't independent?
If the platform wobbles, your mounting points may be too flexible. Reinforce with extra glue or a second layer of material at the joint.
If both axes move together, the pitch servo is not properly fixed to the yaw horn. The pitch servo's body (not its shaft) must be rigidly attached to the yaw servo's horn.
My gimbal axes aren't moving independently. How should I fix the mounting?
Run the final mechanical check
Your gimbal is assembled, but you need to confirm it works under power. The sweep test sketch from Step 2 is still loaded on your ESP32, so you can use it immediately to test the gimbal's full range of motion.
- Make sure your external 5V power supply is connected and switched on.
- Plug the ESP32 into your PC via the micro-USB cable. The sweep test sketch from Step 2 runs automatically on power-up.
- Watch the gimbal sweep both axes simultaneously from 10 to 170 degrees and back.
- Look for any binding, cable snagging, or mechanical interference between the two axes as they move through their full range.
- Adjust any mounting points or cable routing if you notice interference. Re-run the sweep test after adjustments.
Both axes should move freely through their full range without interference. The gimbal should not topple over when both servos move at the same time.
Servo cables getting caught during movement?
Route the cables so they loop away from the moving parts. A small piece of tape to secure cables to the base (with enough slack for full rotation) prevents snagging.
If the ESP32 resets during the test, double-check your common ground connection between the external power supply and the ESP32 GND pin.
My gimbal binds or my ESP32 resets during the sweep test. What should I check?
Your gimbal is mechanically complete and tested. Next up, you'll write the firmware that turns this into a wireless aircraft attitude simulator, controllable from any device on your network.
Program the Web-Controlled Attitude Simulator
Your gimbal hardware is built and both servo axes move freely. Now it is time to give it a brain.
In this step you will replace the simple sweep test with a full web-controlled firmware. The ESP32 will connect to your home Wi-Fi, host its own web server, and serve a control page with pitch and yaw sliders that move the gimbal in real time from your phone or PC browser.
In this step, get ready to:
- Write the ESP32 firmware with Wi-Fi, a web server, and servo mapping logic.
- Create a mobile-responsive HTML control interface with pitch and yaw sliders.
- Upload, connect, and test real-time gimbal control from your browser.
Write the ESP32 firmware declarations
The sweep test currently running on your ESP32 does not know anything about Wi-Fi or web requests. You need to replace it entirely with new firmware that sets up networking, a web server, and servo angle mapping.
- In the Arduino IDE, select all the code in your attitude_simulator.ino sketch and delete it.
- Replace it with the following firmware foundation:
#include <WiFi.h>
#include <WebServer.h>
#include <ESP32Servo.h>
// Replace these with your actual Wi-Fi network name and password
const char* ssid = "your-wifi-ssid-here";
const char* password = "your-wifi-password-here";
// Servo objects for pitch and yaw axes
Servo pitchServo;
Servo yawServo;
// GPIO pins matching your wiring from Step 2
const int PITCH_PIN = 18;
const int YAW_PIN = 19;
// Track current servo positions (start centered)
int currentPitch = 90;
int currentYaw = 90;
// Create a web server on port 80 (standard HTTP)
WebServer server(80);
// Convert a user-facing angle (-90 to +90) into a safe servo range (10 to 170)
int mapAngleToServo(int angle) {
return map(angle, -90, 90, 10, 170);
}
What does this code do?
This block sets up everything the firmware needs before it can serve web pages or move servos.
- WiFi.h and WebServer.h are built into the ESP32 Arduino core. They let the ESP32 connect to your router and serve HTTP pages.
- ssid and password are your home Wi-Fi credentials so the ESP32 can join your network.
- mapAngleToServo() converts the -90 to +90 degree range (intuitive for the user) into the 10 to 170 degree safe servo range (avoids jitter at the extremes).
- Replace your-wifi-ssid-here with the name of your Wi-Fi network (the one your phone or PC is connected to).
- Replace your-wifi-password-here with your Wi-Fi password.
Keep your credentials safe
Do not share your sketch publicly with your real Wi-Fi password in it. For this project the credentials stay on your local board, so this approach is fine.
You will not see a change yet. The next chunk adds the web interface that this server will host.
Create the web interface
The ESP32 does not serve files from a filesystem. Instead, the entire HTML page lives as a string constant directly in your firmware. When a browser requests the page, the ESP32 sends this string as the HTTP response.
- Directly below the mapAngleToServo function, paste the following HTML page string:
const char* htmlPage = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Aircraft Attitude Simulator</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
margin: 0;
padding: 20px;
background: #1a1a2e;
color: #eaeaea;
}
h1 {
color: #00d4ff;
margin-bottom: 30px;
}
.slider-container {
margin: 20px auto;
max-width: 400px;
}
.slider-label {
font-size: 18px;
margin-bottom: 5px;
}
.angle-display {
font-size: 24px;
font-weight: bold;
color: #00d4ff;
}
input[type="range"] {
width: 100%;
height: 30px;
margin: 10px 0;
cursor: pointer;
}
.status {
margin-top: 20px;
padding: 10px;
background: #16213e;
border-radius: 8px;
display: inline-block;
}
</style>
</head>
<body>
<h1>Aircraft Attitude Simulator</h1>
<div class="slider-container">
<div class="slider-label">Pitch (Nose Up/Down)</div>
<input type="range" id="pitch" min="-90" max="90" value="0" oninput="sendAngles()">
<div class="angle-display"><span id="pitchVal">0</span> degrees</div>
</div>
<div class="slider-container">
<div class="slider-label">Yaw (Nose Left/Right)</div>
<input type="range" id="yaw" min="-90" max="90" value="0" oninput="sendAngles()">
<div class="angle-display"><span id="yawVal">0</span> degrees</div>
</div>
<div class="status">
<p>Connected to ESP32 Attitude Simulator</p>
</div>
<script>
function sendAngles() {
var pitch = document.getElementById("pitch").value;
var yaw = document.getElementById("yaw").value;
document.getElementById("pitchVal").textContent = pitch;
document.getElementById("yawVal").textContent = yaw;
fetch("/set?pitch=" + pitch + "&yaw=" + yaw);
}
</script>
</body>
</html>
)rawliteral";
What does this HTML page do?
This is a complete web page that the ESP32 sends to any browser that connects to it.
- The viewport meta tag makes the page mobile-responsive so sliders are usable on a phone screen.
- Each slider ranges from -90 to +90 degrees, representing aircraft pitch (nose up/down) and yaw (nose left/right).
- The oninput event fires continuously as you drag (not just on release), so the servo moves in real time.
- The sendAngles() JavaScript function sends a GET request to /set?pitch=X&yaw=Y every time either slider moves. The ESP32 reads these values and moves the servos.
The R"rawliteral(...)rawliteral" syntax is a C++ raw string literal. It lets you embed the entire HTML with quotes and special characters without escaping them.
You still cannot compile yet. The next chunk adds the functions that handle incoming requests and the main program logic.
Add request handlers, upload, and test
The web page sends requests to /set with pitch and yaw values. Now you need the ESP32-side code that receives those requests, maps the angles, and writes them to the servos.
- Below the htmlPage string, paste the request handlers and main program functions:
// Serve the HTML control page when a browser visits the root URL
void handleRoot() {
server.send(200, "text/html", htmlPage);
}
// Handle incoming angle commands from the sliders
void handleSet() {
if (server.hasArg("pitch") && server.hasArg("yaw")) {
int pitch = server.arg("pitch").toInt();
int yaw = server.arg("yaw").toInt();
// Clamp values to safe range
pitch = constrain(pitch, -90, 90);
yaw = constrain(yaw, -90, 90);
// Map user-facing angles to servo positions and move
currentPitch = mapAngleToServo(pitch);
currentYaw = mapAngleToServo(yaw);
pitchServo.write(currentPitch);
yawServo.write(currentYaw);
}
server.send(200, "text/plain", "OK");
}
void setup() {
Serial.begin(115200);
// Attach servos and center them
pitchServo.attach(PITCH_PIN);
yawServo.attach(YAW_PIN);
pitchServo.write(90);
yawServo.write(90);
// Connect to Wi-Fi
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println();
Serial.print("Connected! IP address: ");
Serial.println(WiFi.localIP());
// Register URL handlers and start the server
server.on("/", handleRoot);
server.on("/set", handleSet);
server.begin();
Serial.println("Web server started");
}
void loop() {
// Continuously listen for incoming HTTP requests
server.handleClient();
}
What does this code do?
- handleRoot() sends the full HTML page to any browser that visits the ESP32's IP address.
- handleSet() reads the pitch and yaw values from the URL parameters, clamps them to -90 to +90, maps them to servo-safe values (10 to 170), and writes them to the servos.
- setup() initializes serial communication, attaches and centers both servos, connects to Wi-Fi, then registers the URL routes and starts the web server.
- loop() calls server.handleClient() repeatedly so the ESP32 keeps listening for browser requests. Without this, the server would never respond.
- Verify the sketch compiles by clicking **Sketch** > **Verify/Compile** (or press **Ctrl+R**).
- You should see a "Done compiling" message in the output panel at the bottom of the Arduino IDE with no errors.
Seeing compile errors?
- If you see an error about WiFi.h or WebServer.h not found, make sure **ESP32 Dev Module** is selected under **Tools** > **Board**. These libraries are built into the ESP32 core and only appear when an ESP32 board is selected.
- If ESP32Servo.h is not found, revisit Step 1 and install the ESP32Servo library via **Sketch** > **Include Library** > **Manage Libraries**.
- If you see a syntax error near rawliteral, check that the opening and closing delimiters match exactly: R"rawliteral( at the start and )rawliteral" at the end.
Help me fix a compile error in my ESP32 attitude simulator sketch
- Upload the sketch to your ESP32 by clicking **Sketch** > **Upload** (or press **Ctrl+U**).
- Open the Serial Monitor by clicking **Tools** > **Serial Monitor**.
- Set the baud rate to 115200 in the dropdown at the bottom right of the Serial Monitor.
You should see dots printing as the ESP32 connects to Wi-Fi, followed by a line showing the IP address (for example, 192.168.1.100).
- On your phone or PC (connected to the same Wi-Fi network), open a browser and type the IP address shown in the Serial Monitor into the address bar.
- Drag the **Pitch** slider up and down. Watch the pitch servo tilt the gimbal platform.
- Drag the **Yaw** slider left and right. Watch the yaw servo rotate the gimbal base.
Both servos should respond immediately as you drag each slider. The angle display on the page updates to show the current degrees.
Servos not responding to sliders?
- If the Serial Monitor shows repeating dots and never prints an IP address, double-check that your ssid and password strings exactly match your Wi-Fi network name and password (both are case-sensitive).
- If the page loads but servos do not move, confirm your external 5V power supply is on and the common ground wire between the power supply and ESP32 is still connected.
- If you cannot load the page at all, make sure your phone or PC is on the same Wi-Fi network as the ESP32 (not mobile data).
Help me troubleshoot my ESP32 web server not connecting
Why oninput instead of onchange?
The oninput event fires continuously as you drag the slider. The onchange event only fires when you release the slider. Using oninput gives you true real-time control where the servo tracks your finger movement instantly.
✔️ Awesome, I've got everything!
Great. Double check you have saved your sketch and both servos respond smoothly to the web sliders.
ⓧ I'd like to double check the full code
Here is the complete attitude_simulator.ino at this point:
#include <WiFi.h>
#include <WebServer.h>
#include <ESP32Servo.h>
const char* ssid = "your-wifi-ssid-here";
const char* password = "your-wifi-password-here";
Servo pitchServo;
Servo yawServo;
const int PITCH_PIN = 18;
const int YAW_PIN = 19;
int currentPitch = 90;
int currentYaw = 90;
WebServer server(80);
int mapAngleToServo(int angle) {
return map(angle, -90, 90, 10, 170);
}
const char* htmlPage = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Aircraft Attitude Simulator</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
margin: 0;
padding: 20px;
background: #1a1a2e;
color: #eaeaea;
}
h1 {
color: #00d4ff;
margin-bottom: 30px;
}
.slider-container {
margin: 20px auto;
max-width: 400px;
}
.slider-label {
font-size: 18px;
margin-bottom: 5px;
}
.angle-display {
font-size: 24px;
font-weight: bold;
color: #00d4ff;
}
input[type="range"] {
width: 100%;
height: 30px;
margin: 10px 0;
cursor: pointer;
}
.status {
margin-top: 20px;
padding: 10px;
background: #16213e;
border-radius: 8px;
display: inline-block;
}
</style>
</head>
<body>
<h1>Aircraft Attitude Simulator</h1>
<div class="slider-container">
<div class="slider-label">Pitch (Nose Up/Down)</div>
<input type="range" id="pitch" min="-90" max="90" value="0" oninput="sendAngles()">
<div class="angle-display"><span id="pitchVal">0</span> degrees</div>
</div>
<div class="slider-container">
<div class="slider-label">Yaw (Nose Left/Right)</div>
<input type="range" id="yaw" min="-90" max="90" value="0" oninput="sendAngles()">
<div class="angle-display"><span id="yawVal">0</span> degrees</div>
</div>
<div class="status">
<p>Connected to ESP32 Attitude Simulator</p>
</div>
<script>
function sendAngles() {
var pitch = document.getElementById("pitch").value;
var yaw = document.getElementById("yaw").value;
document.getElementById("pitchVal").textContent = pitch;
document.getElementById("yawVal").textContent = yaw;
fetch("/set?pitch=" + pitch + "&yaw=" + yaw);
}
</script>
</body>
</html>
)rawliteral";
void handleRoot() {
server.send(200, "text/html", htmlPage);
}
void handleSet() {
if (server.hasArg("pitch") && server.hasArg("yaw")) {
int pitch = server.arg("pitch").toInt();
int yaw = server.arg("yaw").toInt();
pitch = constrain(pitch, -90, 90);
yaw = constrain(yaw, -90, 90);
currentPitch = mapAngleToServo(pitch);
currentYaw = mapAngleToServo(yaw);
pitchServo.write(currentPitch);
yawServo.write(currentYaw);
}
server.send(200, "text/plain", "OK");
}
void setup() {
Serial.begin(115200);
pitchServo.attach(PITCH_PIN);
yawServo.attach(YAW_PIN);
pitchServo.write(90);
yawServo.write(90);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println();
Serial.print("Connected! IP address: ");
Serial.println(WiFi.localIP());
server.on("/", handleRoot);
server.on("/set", handleSet);
server.begin();
Serial.println("Web server started");
}
void loop() {
server.handleClient();
}
Your aircraft attitude simulator is fully operational. You can control your gimbal wirelessly from any device on your network. Next up, tackle the Secret Mission to add preset flight attitudes with smooth animated transitions.
Secret mission
Add Preset Attitudes and Smooth Transitions
Your gimbal snaps instantly to new positions when you drag a slider. Real aircraft don't move that way. In this secret mission, you'll add preset flight attitude buttons and implement smooth servo interpolation so transitions look realistic and animated.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on local hardware you own, so there are no ongoing cloud costs or subscriptions to worry about. Decide whether to keep your simulator assembled, pause it for later, or disassemble it entirely.
Resources you used:
- ESP32Servo library installed in Arduino IDE.
- ESP32 board support package installed in Arduino IDE.
- attitude_simulator.ino sketch file.
- Physical gimbal assembly (ESP32, servos, breadboard, wiring, frame).
Keep everything running
No action needed. Choose this if you plan to use the simulator for a university presentation or want to keep experimenting with the web interface.
- Leave the gimbal assembled and wired on your desk.
- Keep the ESP32Servo library and ESP32 board support installed for future ESP32 projects.
- Disconnect the external 5V power supply when not in use to avoid unnecessary power draw.
- Unplug the micro-USB cable from the ESP32 when you are done demonstrating.
Pause - I'll come back to this later
Shut down the simulator to free up your desk space while keeping everything ready to reassemble quickly.
- Disconnect the external 5V power supply from the breadboard.
- Unplug the micro-USB cable from the ESP32.
- Leave the gimbal wiring intact so you can reconnect quickly.
- Save your attitude_simulator.ino sketch to a known location on your PC (e.g. your Desktop) so you can find it later.
- Keep the ESP32Servo library and board support installed in Arduino IDE.
Delete - I don't want to use this again
Remove all project software and disassemble the hardware so the components can be reused for other projects.
- Disconnect the external 5V power supply and unplug the micro-USB cable from the ESP32.
- Remove all jumper wires from the breadboard and servos.
- Disassemble the gimbal frame. Store the servos, breadboard, and ESP32 separately for future projects.
- Delete the attitude_simulator.ino sketch file from your PC.
- Remove the ESP32Servo library: go to Sketch > Include Library > Manage Libraries, search for ESP32Servo, and click Remove.
- Remove the ESP32 board support: go to Tools > Board > Boards Manager, search for ESP32, and click Remove on the "esp32 by Espressif Systems" entry.
That's a Wrap!
That's a Wrap!
Nice work! You just built a fully functional IoT-connected aircraft attitude simulator that you can control wirelessly from any device on your network.
You've learned how to:
- Build a 2-axis gimbal with two SG90 servos controlling pitch and yaw independently using external power and common ground wiring.
- Program an ESP32 to host a Wi-Fi web server that serves a responsive HTML control interface and handles real-time HTTP requests to move servos.
- Create a mobile-friendly web page with sliders that send angle data via GET requests, demonstrating aircraft attitude concepts through a physical, interactive device.
- Secret Mission: Added preset flight attitude buttons with smooth animated servo transitions using an interpolation function that moves 2 degrees per 20ms loop iteration.
Ready to quiz yourself?