Building the Cheapest 3-Channel RC Plane

by Misfit Maker in Circuits > Microcontrollers

1181 Views, 18 Favorites, 0 Comments

Building the Cheapest 3-Channel RC Plane

DIY Low-Cost 3-Channel RC Plane with ESP32 Wi-Fi Control | Code & Templates in Description!
Screenshot_2024-11-11_100932-removebg-preview.png

I’ve always wanted to build an RC plane, but as many know, while constructing the frame is cheap, the real cost is in the electronics—especially the transmitter and receiver, which make up a significant portion of the expense. So to create an RC plane on a super-low budget, I focused on reducing costs for the transmitter and receiver.

After doing some research, I found a few videos demonstrating how to control RC planes with ESP32 boards using their Wi-Fi capabilities. Most examples were simple paper planes with two coreless motors to control pitch and direction, but I wanted to take it a step further by adding actual control surfaces for pitch and yaw control.

Now we know that the ESP32 could work as both a transmitter and receiver, so with some coding adjustments, we could customize it to our needs. Luckily, I had a Xiao ESP32-C3 board, which I set up as the receiver, while a webpage on my phone served as the transmitter through the C3’s Wi-Fi.

To handle pitch and yaw, I used magnetic actuators [coils salvaged from a clock mechanism]. To keep costs even lower, I constructed the plane’s frame from thermocol boards salvaged from packaging materials. If you’re planning to make one, I’d recommend using Depron sheets [or sheets specifically for RC planes] instead—they’re stronger and more durable, as thermocol can be fragile and couldn't survive crashes.

For thrust, I used a 720 coreless motor with a 55mm propeller, controlled by an SI2302 N-channel MOSFET. The complete circuit diagram and code are provided in the following steps. With all these elements, I was able to build a 3-channel RC plane within a $12 USD budget.

So, for everyone who’s wanted to build an RC plane but felt held back by the cost, here’s my gift to you! I’ve lowered the budget bar enough that you, too, can make your own RC plane on a shoestring budget.

Supplies

Electronics parts

  1. 1 x Xiao esp32 C3 [ ₹437 ]
  2. 1 x DRV 8833 [ ₹65 ]
  3. 1 x 3.7V 360 mAh 30C [ ₹250 ]
  4. 1 x SI2302 N channel Mosfet [ ₹1.9 ]
  5. 1 x 10K SMD resistor [ ₹0.5 ]
  6. 1 x SS14 Schottky diode [ ₹0.88 ]
  7. 1 x 720 Coreless motor [ ₹50 ]
  8. Coil for the magnetic actuator ( from a clock mechanism )

RC Plane parts

  1. 1 x 55mm propeller [ ₹7.5 ]
  2. Thermocol or Depron sheet (3mm)
  3. Glue ( I used Araldite klear epoxy) [ ₹68 ]
  4. Two side tape ( optional )
  5. PVC Card
  6. 1 x OHP sheets (100 microns) [ ₹2 ]
  7. 2 x 3mm Neodymium magnet [ ₹12 ]
  8. 1 x 10mm candle [ ₹ 3 ]
  9. Nylon thread

Tools for the build

  1. Craft knife
  2. Emery paper (220 grit)
  3. 3mm Drill Bit (For Magnet Placement Holes)
  4. Soldering iron

If you’re planning to use thermocol sheets to build your RC plane like I did, you’ll need to set up a jig with nichrome wire to cut 3mm thin sheets from the bulk thermocol. However, if you’re using Depron sheets, you can skip this hassle entirely.

Total cost = ₹897.78 + miscellaneous expenses = ₹1000 (approximately $12 USD).

Magnetic Actuator Part 1

rc11_converted (2).gif
Screenshot (634).png
Screenshot (635).png
Screenshot (636).png
Screenshot (637).png
Screenshot (638).png

We'll begin by building the actuator for our RC plane. First, I’m cutting the moving part of the actuator from a PVC card, then gluing a 3mm neodymium magnet into the pre-drilled holes.

Magnetic Actuator Part 2

How I Crafted the Coil for My RC Plane’s Magnetic Actuator
Screenshot (639).png
Screenshot (640).png
Screenshot (641).png
Screenshot (642).png
Screenshot (643).png
Screenshot (644).png

Now, we’ll be making the coil for the magnetic actuator. I’m using a 10mm candle as a guide to wind it. For a better understanding, check out the detailed video included in this step.

Assembling the Circuit

rctempcir.png

Following the provided circuit diagram, connect and solder all components.

Libraries to Install

Screenshot 2024-11-09 074839.png
Screenshot 2024-11-09 075003.png
Screenshot 2024-11-09 075024.png

Required Libraries: AsyncTCP, ESPAsyncWebServer

Before uploading the code, ensure these libraries are installed. Also, make sure to install only version 2.0.17 of the ESP32 board by Espressif Systems, as using a newer version may cause compilation issues with the code I provided in the below step.

Uploading the Code

Screenshot (654).png
photo_2024-11-09_20-36-33.jpg


After installing the necessary libraries, copy the code below and upload it to the ESP32-C3 board using the Arduino IDE.

#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <AsyncTCP.h>

// Pin definitions
const int motorAPin = D10;// PWM pin for Motor A (thrust control)
const int motorBPin1 = D5; // IN1 for Motor B (clockwise)
const int motorBPin2 = D4; // IN2 for Motor B (counterclockwise)
const int motorCPin1 = D3; // IN1 for Motor C (clockwise)
const int motorCPin2 = D2; // IN2 for Motor C (counterclockwise)

// PWM properties for Motor A (Throttle)
const int pwmChannel = 0;  // PWM channel
const int pwmFreq = 30000;  // PWM frequency
const int pwmResolution = 8; // PWM resolution (8-bit)

// Wi-Fi credentials
const char* ssid = "MISFIT-RC-PLANE"; // Name of your wifi network
const char* password = "12345678"; // Your wifi network password

AsyncWebServer server(80); // Web server on port 80
bool isArmed = false;    // Variable to track armed/disarmed state

// HTML code for the control interface (stored in flash memory)
const char controlPage[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
 <title>Misfit RC Plane Control</title>
 <style>
  h1 { font-size: 80px; border: thick solid #000; padding: 10px; border-radius: 10px; background-color: #fff; display: inline-block; }
  h2 { font-size: 45px; }
  body { font-family: Impact; text-align: center; background-color: #f0f0f0; }
  #scrollbar-container { width: 260px; height: 600px; border: thick solid #000; background-color: #ddd; border-radius: 30px; overflow: hidden; position: relative; margin: 0px; box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.3); }
  #scrollbar-fill { width: 100%; height: 0; background-color: #8cff00 ; position: absolute; bottom: 0; z-index: 0; }
  #scrollbar-handle { width: 260px; height: 150px; background-color: #888; position: absolute; bottom: 0; border-radius: 30px; display: flex; justify-content: center; align-items: center; z-index: 1; cursor: pointer; transition: background-color 0.3s; }
  #scrollbar-handle.active { background-color: red; }
  #scroll-tab { width: 160px; height: 30px; background-color: #444; border-radius: 4px; position: absolute; top: 64px; left: 50%; transform: translateX(-50%); cursor: pointer; }
  .button { padding: 70px 120px; margin: 10px; font-size: 60px; font-weight: bold; cursor: pointer; border: thick solid #000; border-radius: 12px; background-color: #818181; color: white; transition: background-color 0.3s; user-select: none; }
  .button:active { background-color: #ff0000; }
  .controls, .cross, .horizontal, .vertical-buttons { display: flex; justify-content: center; align-items: center; }
  .vertical-buttons { flex-direction: column; }
  .cross { flex-direction: column; }
  .horizontal { margin: 0; }
  #armButton { background-color: #28a745; color: white; transition: background-color 0.3s; user-select: none; margin-left: 150px; width: 260px; height: 150px; display: flex; justify-content: center; align-items: center; font-size: 45px; font-weight: bold; border-radius: 12px;}
  #armButton.active { background-color: red; }
  #motorAValue { font-size: 45px; font-weight: bold; margin-top: 0px; color: #333; }
 </style>
</head>
<body>
 <h1>Misfit - RC - Plane</h1>
 <div class="controls">
  <div class="vertical">
   <h2>THRUST</h2>
   <div id="scrollbar-container">
    <div id="scrollbar-fill"></div>
    <div id="scrollbar-handle"><div id="scroll-tab"></div></div>
   </div>
   <p id="motorAValue">Speed: 0%</p>
  </div>
  <div class="arm-container">
   <button class="button" id="armButton" onclick="toggleArm()">Arm</button>
  </div>
 </div>
 <div class="cross">
  <h2>DIRECTIONS</h2>
  <div class="vertical-buttons">
   <button class="button motor-btn" onmousedown="updateMotorB(1)" onmouseup="stopMotorB()" ontouchstart="updateMotorB(1)" ontouchend="stopMotorB()">&#8679;</button>
   <div class="horizontal">
    <button class="button motor-btn" onmousedown="updateMotorC(0)" onmouseup="stopMotorC()" ontouchstart="updateMotorC(0)" ontouchend="stopMotorC()">&#8678;</button>
    <button class="button motor-btn" onmousedown="updateMotorC(1)" onmouseup="stopMotorC()" ontouchstart="updateMotorC(1)" ontouchend="stopMotorC()">&#8680;</button>
   </div>
   <button class="button motor-btn" onmousedown="updateMotorB(0)" onmouseup="stopMotorB()" ontouchstart="updateMotorB(0)" ontouchend="stopMotorB()">&#8681;</button>
  </div>
 </div>
 <script>
  let armed = false, handle = document.getElementById('scrollbar-handle'), container = document.getElementById('scrollbar-container'),
    motorAValueDisplay = document.getElementById('motorAValue'), fill = document.getElementById('scrollbar-fill'), maxScroll = container.clientHeight - handle.clientHeight;

  handle.style.top = maxScroll + 'px';
  motorAValueDisplay.innerText = '0%';

  function updateMotorA(value) {
   motorAValueDisplay.innerText = `${Math.round((value / 255) * 100)}%`;
   fetch(`/motorA?speed=${value}`).catch(console.error);
   fill.style.height = `${(value / 255) * container.clientHeight}px`;
  }

  handle.addEventListener('mousedown', event => handleDrag(event, 'mousemove', 'mouseup'));
  handle.addEventListener('touchstart', event => handleDrag(event, 'touchmove', 'touchend'));

  function handleDrag(event, moveEvent, endEvent) {
   event.preventDefault();
   handle.classList.add('active');
   const initialY = event.clientY || event.touches[0].clientY, initialTop = parseInt(handle.style.top, 10);

   function onMove(e) {
    const newY = (e.clientY || e.touches[0].clientY) - initialY + initialTop;
    handle.style.top = `${Math.max(0, Math.min(newY, maxScroll))}px`;
    updateMotorA(Math.round(255 - (parseInt(handle.style.top) / maxScroll) * 255));
   }

   function onEnd() {
    handle.classList.remove('active');
    window.removeEventListener(moveEvent, onMove);
    window.removeEventListener(endEvent, onEnd);
   }

   window.addEventListener(moveEvent, onMove);
   window.addEventListener(endEvent, onEnd);
  }

  function toggleArm() {
   armed = !armed;
   document.getElementById('armButton').innerText = armed ? 'Disarm' : 'Arm';
   document.getElementById('armButton').classList.toggle('active');
   fetch(`/toggleArm?state=${armed ? 'true' : 'false'}`).catch(console.error);
  }

  function updateMotorB(direction) { fetch(`/motorB?dir=${direction}`).catch(console.error); }
  function stopMotorB() { fetch('/motorB?stop=1').catch(console.error); }
  function updateMotorC(direction) { fetch(`/motorC?dir=${direction}`).catch(console.error); }
  function stopMotorC() { fetch('/motorC?stop=1').catch(console.error); }
 </script>
</body>
</html>
)rawliteral";

void stopAllMotors() {
 // Function to stop all motors
 ledcWrite(pwmChannel, 0); // Stop Motor A (Throttle)
 digitalWrite(motorBPin1, LOW);
 digitalWrite(motorBPin2, LOW); // Stop Motor B
 digitalWrite(motorCPin1, LOW);
 digitalWrite(motorCPin2, LOW); // Stop Motor C
}

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

 // Motor control pin setup
 pinMode(motorBPin1, OUTPUT);
 pinMode(motorBPin2, OUTPUT);
 pinMode(motorCPin1, OUTPUT);
 pinMode(motorCPin2, OUTPUT);

 // PWM setup for Motor A (Throttle control)
 ledcSetup(pwmChannel, pwmFreq, pwmResolution);
 ledcAttachPin(motorAPin, pwmChannel);

 // Wi-Fi Access Point setup
 WiFi.softAP(ssid, password);
 Serial.println();
 Serial.print("Access Point IP Address: ");
 Serial.println(WiFi.softAPIP());

 // Serve the control page
 server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
  request->send_P(200, "text/html", controlPage);
 });

 // Motor A control (Throttle)
 server.on("/motorA", HTTP_GET, [](AsyncWebServerRequest *request) {
  if (isArmed && request->hasParam("speed")) { // Only update speed if armed
   int motorASpeed = request->getParam("speed")->value().toInt();
   ledcWrite(pwmChannel, motorASpeed);
   Serial.println("Motor A Speed: " + String(motorASpeed));
   request->send(200, "text/plain", "Motor A Updated");
  } else {
   stopAllMotors(); // Ensure safety by stopping all motors if disarmed
   request->send(200, "text/plain", "Plane is disarmed. Motor A not updated.");
  }
 });

  // Motor B (pitch) control
 server.on("/motorB", HTTP_GET, [](AsyncWebServerRequest *request) {
  if (isArmed && request->hasParam("dir")) {
   String direction = request->getParam("dir")->value();
   if (direction == "1") {
    digitalWrite(motorBPin1, HIGH); // Set Motor B to move clockwise
    digitalWrite(motorBPin2, LOW);
    Serial.println("Motor B: Moving Clockwise");
   } else {
    digitalWrite(motorBPin1, LOW); // Set Motor B to move counterclockwise
    digitalWrite(motorBPin2, HIGH);
    Serial.println("Motor B: Moving Counterclockwise");
   }
  } else if (request->hasParam("stop")) {
   digitalWrite(motorBPin1, LOW);
   digitalWrite(motorBPin2, LOW);
   Serial.println("Motor B Stopped");
  }
  request->send(200, "text/plain", "OK");
 });

 // Motor C (yaw) control
 server.on("/motorC", HTTP_GET, [](AsyncWebServerRequest *request) {
  if (isArmed && request->hasParam("dir")) {
   String direction = request->getParam("dir")->value();
   if (direction == "1") {
    digitalWrite(motorCPin1, HIGH); // Set Motor C to move clockwise
    digitalWrite(motorCPin2, LOW);
    Serial.println("Motor C: Moving Clockwise");
   } else {
    digitalWrite(motorCPin1, LOW); // Set Motor C to move counterclockwise
    digitalWrite(motorCPin2, HIGH);
    Serial.println("Motor C: Moving Counterclockwise");
   }
  } else if (request->hasParam("stop")) {
   digitalWrite(motorCPin1, LOW);
   digitalWrite(motorCPin2, LOW);
   Serial.println("Motor C Stopped");
  }
  request->send(200, "text/plain", "OK");
 });
 // Toggle arm/disarm state
 server.on("/toggleArm", HTTP_GET, [](AsyncWebServerRequest *request) {
  if (request->hasParam("state")) {
   String state = request->getParam("state")->value();
   isArmed = (state == "true");
   Serial.println(isArmed ? "Plane Armed" : "Plane Disarmed");
   if (!isArmed) {
    stopAllMotors(); // Stop all motors when disarmed
   }
   request->send(200, "text/plain", isArmed ? "Plane Armed" : "Plane Disarmed");
  }
 });

 // Start the server
 server.begin();
}
void loop() {
}


In our project, the ESP32-C3 functions as an access point. To open the control webpage on your mobile device, first connect to the ESP32 Wi-Fi using the WIFI credentials provided in our code. Then, open a new page in Google Chrome and enter the IP address in the search bar (The IP address can be found in the Serial Monitor section in the Arduino IDE under the Tools tab). For a more detailed walkthrough, check out the video I uploaded in the introductory step.

Cutting Out Templates

rctemp.png
rc6_converted.gif
Screenshot (610).png
Screenshot (614).png
Screenshot (612).png
Screenshot (613).png

With all the electronic components ready, you can now build any RC plane design you envision. Here, I’m creating a simple Cessna-style RC plane. Using the provided PDF template, cut out the necessary parts for your plane. Since I used thermocol, I first set up a jig to slice 3mm sheets from the bulk material. I’d recommend using Depron sheets or applying a suitable coating to strengthen the thermocol, as it’s quite fragile and prone to breaking. Then, I used an emery paper to smooth the rough edges and tapered the wing trailing edges to improve it's aerodynamics.

Downloads

Shaping the Wings

rc2_converted.gif
Screenshot (616).png

A simple shaping of the wings, as shown in the GIFs, can enhance it's structural strength and provide a pseudo-airfoil effect.

Gluing Parts Together

rc3_converted.gif
Screenshot (619).png

I used Araldite epoxy to glue the parts together, as it sets quickly and doesn’t react with foam boards. Before attaching the wings, I sanded the joining sections for a perfect fit.

Mounting the Control Surfaces

Screenshot (620).png
Screenshot (622).png
rc4_converted.gif
Screenshot (628).png

To mount the control surfaces, I cut 1.5mm-wide strips from a 100-micron thick OHP sheet and used them to attach the rudder and elevator to the RC plane’s frame with glue as shown in the above GIFs / Images.

Mounting the Motor

Screenshot (629).png
Screenshot (630).png

Now, I secured the 720 coreless motor to the frame using Araldite glue. Before gluing, ensure the motor is angled slightly to the right (about 2-3 degrees)[Direction of propeller- Clockwise when viewed from the back] as shown in the above pictures.

Final Setup

rc5_converted.gif
Screenshot (652).png
Screenshot (651).png

With the frame complete, all that’s left is to attach the electronic components and solder the necessary connections. I attached the components to the frame using double-sided tape, though a hot glue gun is also a great option for securing them. Ensure you position the lithium-ion battery on the frame so that the plane balances slightly nose-down when held 1/4 to 1/3 of the way back from the wing’s leading edge.

Wing Support

RC7_converted.gif

The final step is to reinforce the wings using nylon threads. I made small holes in the wings with a safety pin, guided by the PDF template from step 6, and threaded them as shown in the GIF above. Once the threads were set, I applied a dab of glue around each hole to keep them firmly in place and to strengthen the area, helping prevent any tearing. With that, the build is complete!. You can choose to paint it or add a protective coating if desired.

From Scratch to Sky

DIY Low-Cost 3-Channel RC Plane with ESP32 Wi-Fi Control | Code &amp; Templates in Description!

This project has proven that building an RC plane doesn’t have to be expensive or out of reach. By focusing on reducing costs where it matters most—electronics and materials—I was able to create a fully functional 3-channel RC plane on a budget of just $12. Whether you're a beginner or someone looking for a low-cost DIY project, I hope this build inspires you to try your hand at making your own RC plane. With a bit of creativity, resourcefulness, and the right tools, you can bring your ideas to life without breaking the bank. The sky’s the limit—happy building!

Thank you for following along with this project! I hope you enjoyed reading about it as much as I enjoyed sharing it with you.