• This site uses cookies. By continuing to use this site, you are agreeing to our use of cookies. Learn more.

Experimental Arduino RC Plane Build Log

Would you use the hardware and/or software designed as part of this project for your own RC planes?

  • Yes

    Votes: 5 100.0%
  • No

    Votes: 0 0.0%

  • Total voters
In case it helps you out ... I am posting this. If not, no worries. I totally get wanting to do things yourself so you fully understand it.

For my own UAV project I developed a ground station in 2 parts. I went with the MIT open-source license so outside of maintain author/credit you can pretty much do whatever you want with it. I typically get 5hz update rates, although some of that depends on how fast the data comes down from the airplane.

Part 1: python script that acts as a data interlink/broker/connector. It reads the aircraft telemetry in via a radio modem + serial port (and a specific fairly well documented packet format.) It assembles the incoming data into internal structures and possibly computes some additional derived values itself.

Part 2: this same python script doubles as a simple web server ... it serves out some custom web pages and then also acts as a websocket host. The web pages run javascript under the hood and fetch and draw the flight data from the python script/server/glue/interlink thing.

Part 3: After starting the python script, you connect up with any web client to http://localhost:8888 and then you can open up a moving map, an instrument panel, and a debugging/text only display in 3 separate browser tabs. As soon as the airplane powers up and starts sending data, these pages come alive with the real data.

It's fairly customized towards my own purposes so it may or may not be helpful. You might find better traction figuring out mavlink and connecting up something like qgroundcontrol (px4) or mission planner (ardupilot). These popular opensource autopilots do everything for everyone so they carry a lot of extra weight and complexity and they also serve as the specific configuration/calibration tools for their respective systems.

You may find (like I often do) that the prospect of figuring out someone else's crazy system is extremely daunting and it seems easier to just write your own from scratch. That's how I ended up with my ground control system. :)

Anyway, if it helps, here it is. If it doesn't help, no worries ...

Python interlink part: https://github.com/AuraUAS/aura-core/tree/master/tools/auralink
Web pages/websocket/javascript/browser part: https://github.com/AuraUAS/aura-gcs

And here are a couple screen shots:


Here is the 'live' organized text/debugging display (in the actual page you would scroll up and down and these values would be live and changing.)


And here is my instrument panel, very loosely modeled after a C172S (with permission I sat in the real cockpit and took some pictures.) Then many years and interations and updates later, this is what's left ...

panel.png map.png props.png panel.png
Ok so I have to ask. I have been watching this for about 3 months now and watching your progress with this. How hard would it be for someone with a basic understanding of how computers and Arduinos work, minimal soldering skills, limited funding and a good understanding of aerodynamics to build something like this and get it to work?
Well, I have some good news and some bad news.

Bad news first:
It's not super cheap. If you make use of all the hardware the ArdUAV library expects to work with, it'll run you the following:
- PCBs - $35 (including boards for both the GS and IFC)
- 3DR Radios - $70 (set of 4: Use the extra two 3DRs to replace the XBees I use for telem - cheaper and higher range)
- GPS - $20
- LiDAR Altimeter - $130
- IMU - $35
- Teensy 3.5 - $70 (set of 2)
- Thumbsticks - $35 (set of two)
- Batteries, props, motor, ESC, BEC, servos, FPV equipment (the usual stuff)
- random electronic components (resistors, capacitors, female/male pin headers, connectors, switches, etc.)

Good News:
As long as the hardware is plugged in and wired correctly, the code will work out the box. One minor issue is configuring the 3DR radios, but it's easy to do with the SiK configuration GUI. I posted a link to the program and my radio config settings earlier in the thread. Also, you don't have to have any special airframe to fly these avionics on. You can put this on the planes you already build - you'll just have to calibrate it. I'm planning on making a Python GUI to make recalibrating as easy as pie, but that's still in the works.

BLUF: If you have a little extra money and know the basics of Arduino programming, this should be an easy project to put together as long as you use the ArdUAV library ;)
Last edited:
Just to put this in a small bit of perspective, any time you are prototyping and building new things, you do end up buying lots of parts and bits and pieces over time. The trick is to avoid doing the math so you don't get depressed. Also, you end up with boxes or drawers or bins of parts and pieces that you'll probably never use and are too out-of-date to sell for anything. You keep them around because you might still be able to work them into some future project. It's all part of the process.

There are lots of things a person could do ... but in my opinion, building something yourself (and doing all the learning required to get to the final working thing) is the best part of the whole process. It's empowering to learn how to program an arduino, or do a simple pcb design, or do simple pcb (0.1") soldering, or draw your own 3d model to be printed out. And then to see your own creation flying is by far the coolest thing ever!

Another route could be buying a pixhawk. You'd still be in it for probably $300-350 with all the bits and pieces you need, but for me that's far less interesting because you just velcro in the black box and upload the firmware ... there's nothing left to build or learn with that system (unless you dive into their code, but that is daunting due to it's size, complexity, and maturity.)

You just can't compare the costs of a DIY from scratch project with imported clone boards that run $9 or $29 dollars or whatever. In the end, it's really about what you want to learn and what you want to build. Our brains aren't big enough to know everything, so we have to pick and choose and even when we focus on one thing, it can take a lifetime to become something of an expert ... and by then you are well aware of all the things you don't know and don't consider yourself an expert anyway. ;-)
I'm wondering if anyone from the FT staff is interested in this. Tbh I want to use this thread to inspire and teach others about how to design avionics while providing them a basic framework to experiment with. I want others to use this project for themselves.

Just hope it doesn't get lost in the sea of FT forum threads, lol.

Curious, why do you have a dictionary with each entry containing a list of a single "sub" dictionary?

As in why is it:
dataDict = {"gps": [
                "altitude_m": 0,
                "fixType": 1
instead of:
dataDict = {"gps":
                "altitude_m": 0,
                "fixType": 1


Active member
Just saw this project today! You were right, I guess it had been lost in the sea of threads. I used to be big into this sort of thing, got too busy with farming and mechanics, but still tinker with stuff in the winter.

That price is not bad considering that thumbsticks are on the BOM... a quick scroll about the thread leads me to believe that this is a self-contained system that doesn't need you to supply a TX/RX - that's about $500 right there, assuming you didn't have a TX already, that is.

Power_broker, lately I'm a big fan of LoRa for telemetry links after getting some experience working with it over the last couple years. Have you ever looked into it? You can swap your Teensy (Atmega32U4) for a Lora Feather with the same processor, and you get the Lora radio onboard. You can also step up to the M0 for the same price to get much more flash and RAM (and a ton more clock cycles to run the radio library at the same time as the flight controller). The Radiohead library works out of the box for both addressed, routed and broadcast packet. There are also now knockoff Lora Feathers on Amazon for around $10, though I always question knockoff RF equipment.

LoRa goes incredibly far with a tiny, tiny link budget, if you are willing to go slow. Perfect for telemetry. In initial testing, we hit ~5 miles on flat ground, with 100mW Feathers, rubber duck antennas, no masts and no groundplane. They were literally just laying on the dash in two trucks.

Honestly I love these things. https://www.adafruit.com/product/3178

Curious, why do you have a dictionary with each entry containing a list of a single "sub" dictionary?

As in why is it:
dataDict = {"gps": [
                "altitude_m": 0,
                "fixType": 1
instead of:
dataDict = {"gps":
                "altitude_m": 0,
                "fixType": 1
If I understand your question, then my thinking is that there may be times when I'll have more than one gps ... so I made the higher level container a list (but one entry so far.) The reality is I've never flown an aircraft yet with more than one gps. It could happen, but so far it hasn't ... so maybe allowing for multiple gps's or multiples of other sensors is overkill?

This is your thread, so I don't want to dilute it with a bunch of other nonesense, but I'm also happy to chat about my flight controller architecture as much as you like. I could pm you my email address, or you might be able to find me at the U of MN UAV lab (Curtis Olson). I have lots of crazy ideas that are a little off the beaten path, but I really like python, I really like linux, and I really like airplanes. A few of my crazy ideas work and I can do things like this auto-land 30 minutes after dusk in nearly calm winds:

The computer graphics and augmented reality (flight path, hud, sun position, etc.) were overlaid in post process (not real time sadly).

Last edited:
Ahhh, that makes sense - I like that sort of foresight in software design...Beginner coders, take this lesson to heart ;)

Yes, no TX box needed - the transmitter (Ground Station) is also built from scratch.

I'd be interested in the LoRa radio itself, but I'm going to have to take a hard pass on the Feather due to its processor.

I think you mistook my Teensy 3.5 for the Teensy base model. From Paul's website:
Version 3.5 features a 32 bit 120 MHz ARM Cortex-M4 processor with floating point unit. All digital pins are 5 volt tolerant.
This 3.5 is an absolute beast of a microcontroller and flies through the code with ease (not to mention way more storage than I need). Not to mention that it has somewhere around 3 I2C ports and 6 hardware serial ports.


Active member
Yep, I was just looking at your BOM on this last page of the thread where it says Teensy x2. Thought you had really written some lean and mean code to get it to run on the Atmega32U4. As I mentioned, I like the Cortex-M0 unit a lot more than the 32, but I haven't fully utilized one yet (thought I haven't written a flight controller, either...) Definitely the M4+FPU on your board will eat either for breakfast. That's quite the embedded platform for $25, I'm surprised they are selling it under the Teensy name.
Update #38.)

Fixed the Python GUI code to datalogg the telemetry AND update GUI telemetry values at 10Hz. Although this GUI is still in its infancy, I'm going to post the working code rn in case anyone is interested:

import traceback
import sys
from datetime import datetime
import glob
import serial
import cv2
from PyQt5.QtGui import QImage, QPixmap
from PyQt5.QtCore import Qt, QThread, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import QApplication, QDialog, QPushButton
from PyQt5.uic import loadUi

# lets the GUI know which camera to use - camera = 0 is built in webcam, camera = 1 is FPV
camera = 0

# variables to control GUI initial size and placement on screen
winLeft = 400
winTop = 300
winWidth = 1900
winHeight = 900

# serial object to connect to ground station
ser = serial.Serial()

# determines if GS port is open/connected or not
portOpen = False

# datalogging file name
dataloggerName = r"testFlight_" + str(datetime.now().isoformat())[:19].replace("-", "_").replace(":", "_").replace("T", "_") + r".txt"

def serial_ports():
    Lists serial port names

    :raises EnvironmentError:
        On unsupported or unknown platforms
        A list of the serial ports available on the system
    if sys.platform.startswith('win'):
        ports = ['COM%s' % (i + 1) for i in range(256)]
    elif sys.platform.startswith('linux') or sys.platform.startswith('cygwin'):
        # this excludes your current terminal "/dev/tty"
        ports = glob.glob('/dev/tty[A-Za-z]*')
    elif sys.platform.startswith('darwin'):
        ports = glob.glob('/dev/tty.*')
        raise EnvironmentError('Unsupported platform')

    result = []
    for port in ports:
            s = serial.Serial(port)
        except (OSError, serial.SerialException):
    return result

class CVThread(QThread):
    changePixmap = pyqtSignal(QImage)

    def run(self):
            # get a new frame from the camera
            cap = cv2.VideoCapture(camera)

            # infinite loop
            while True:
                ret, frame = cap.read()
                if ret:
                    rgbImage = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
                    convertToQtFormat = QImage(rgbImage.data, rgbImage.shape[1], rgbImage.shape[0],
                    p = convertToQtFormat.scaled(640 * 2, 480 * 2, Qt.KeepAspectRatio)

        except Exception:

class radioThread(QThread):
    # create a pySignal to tell GUI when new data has arrived
    updateVals = pyqtSignal(str)

    def run(self):
        # run forever
                # open and append telemetry to logging .txt
                with open(dataloggerName, 'a') as f:
                    # run forever
                    while (True):
                        # test if serial port is open
                        if (ser.is_open):
                            # get the data
                            data = str(ser.readline())
                            data = data.replace("b'", "")
                            data = data.replace("'", "")
                            data = data.replace(r"\r\n", "\n")

                            # write the data to the datalogging file

                            if(data != '\n'):
                                # optinal debugging print

            except Exception:

# noinspection SpellCheckingInspection
class App(QDialog):
    def __init__(self):
        # initialize the GUI
        loadUi('GS_GUI.ui', self)

        #echo AT commands on by default
        self.echo = True

        #set GUI window geometry
        self.left = winLeft
        self.top = winTop
        self.width = winWidth
        self.height = winHeight

        #find all available serial ports

        #make sure sensor control only is set as default

        #connect signals

        #initialize threads and show GUI

    def setImage(self, image):

    def initUI(self):
        # set window shape and size
        self.setGeometry(self.left, self.top, self.width, self.height)

        # do video processing on a separate thread (parallel processing for speed)
        vidThead = CVThread(self)

        # take care of serial port/data handling
        dataThread = radioThread(self)

        # display the GUI

    def updateTelem(self, data):
        # get rid of newline chars
        data.replace("\n", "")

        # get the name
        dataName = data.split(" ")[0]

        # get the numerical data
        dataValue = data.split(" ")[1]

        if(dataName == "Alt:"):
        elif(dataName == "Roll:"):
        elif (dataName == "Pitch:"):
        elif (dataName == "Vel:"):
        elif (dataName == "Lat:"):
        elif (dataName == "Lon:"):
        elif (dataName == "UTC_y:"):
        elif (dataName == "UTC_M:"):
        elif (dataName == "UTC_d:"):
        elif (dataName == "UTC_h:"):
        elif (dataName == "UTC_m:"):
        elif (dataName == "UTC_s:"):
        elif (dataName == "SOG:"):
        elif (dataName == "COG:"):

    def processAT(self):
        Type "Echo on" or "Echo Off" to toggle whether or not the typed commands show up in the output
        if (self.UAV_AT_Command_Line.text().lower().split(" ")[0] == "echo" and
                self.UAV_AT_Command_Line.text().lower().split(" ")[1] == "off"):
            self.echo = False
        elif (self.UAV_AT_Command_Line.text().lower().split(" ")[0] == "echo" and
              self.UAV_AT_Command_Line.text().lower().split(" ")[1] == "on"):
            self.echo = False

        commands = "Echo: " + self.UAV_AT_Command_Line.text() + "\n";

        #test if GS is connected
                #echo command
            #print error
            self.Command_Output.append("GS NOT CONNECTED")

        #clear user input

    def refreshPorts(self):
            portList = []
            ports = serial_ports()

            #only execute if you don't already have a connection to the GS
            if(not ser.is_open):
                if (len(ports) > 0):
                    for port in range(len(ports)):


                if (len(ports) > 0):
                    self.COM_Select.addItem("None Available")
        except Exception:

    def connectPort(self):
        port = self.COM_Select.currentText()

        if (not (port == "None Available")):
                if(ser.port != str(port) or not ser.is_open):
                    ser.baudrate = 115200
                    ser.port = str(port)
                    self.Command_Output.setText("Connected to radio on %s\n" % port)

                self.Command_Output.append("ERROR - CANNOT CONNECT TO COM PORT\n")
            self.Command_Output.append("No Radio COM Port Available - Check Device Manager and/or Wiring\n")

if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = App()
Update #39.)

I took yet another deep look into how the ground station and plane transfer data between each other.

Over the past few weeks I was aware that the packet checksum calculations were always incorrect (every single packet!) yet the data seemed to be present and correct. The simple temporary fix was to just ignore the checksum part of the packet entirely and take the data "as is" and everything worked for the most part. I decided to take a look at what was going wrong in terms of calculating checksum values when I noticed the servos erratically twitch once every 30 min or so when connected to the hand controller.

After doing some debugging prints I found that as soon as each packet was stuffed into the string buffer, the packet had all correct values. But once the code got to the point where the string buffer was processed to extract the internal data, almost all of the buffer's values had changed!

Note that when I transmit data, the values are in raw integer format, not in ASCII characters. Because of this, it is possible for the string buffer to contain a character with the binary value of 0 (aka '\0') somewhere in the middle. This is a problem because once a 0 is stored in a string, it terminates it. In my case, if any of the data I'm trying to send is a 0, the string is accidentally terminated early and any data trying to be stored after that value of 0 is lost.

In order to fix this, I converted the buffer to an array of characters and both the checksum calculations work and the smoothness of the control surface movements have been improved.

I'll probably update the ArdUAV library sometime this weekend to reflect these bug fixes.
The ArdUAV GitHub repository has been updated with all the bug fixes, improvements, and feature implementations mentioned earlier in this build log. I'll update the website soon...
Update #40.)

Now that I've verified ArdUAV handles full manual control I'm starting to look at how to include more autonomous features.

Currently developing and debugging a primitive bank and pitch limiter for the plane. The maximum pitch and bank angles will be configurable and the user will also be able to toggle the use of the bank/pitch limiter (i.e. toggle feature on or off). I should be finished with the first draft of this new feature in a week (two weeks at most).

In order to get an idea as to how autopilot algorithms for fixed wing aircraft are designed, I looked at some of ArduPilot's documentation. In the documentation I found some system block diagrams that map out the design of ArduPilot's roll, pitch, and yaw controllers (link). I'll definitely be studying these diagrams and using them as a baseline design for my own autopilot controllers:



Update #41.)

Finished the pitch and bank limiter early, updated github with the latest and greatest version of the code (fully tested), and then updated the API website.

Stay tuned for a flowchart explanation of how the pitch and bank limiter works...;)
Update #42.)

Below is the main code for the bank and pitch limiter. The "kill chain" for how/when this function gets called is:

1.) User sketch calls grabDataRadio()
2.) grabDataRadio() calls bankPitchLimiter()
3.) bankPitchLimiter() calls two instances of updateControlsLimiter(), one call for pitch and one call for roll

- The logic flowcharts for grabDataRadio() and bankPitchLimiter() are attached to the end of this post
- The biggest room for improvement in terms of the bank and pitch limiter is that right now the rudder isn't used to adjust the pitch angle. For instance, if the nose is too low and is in a steep right bank, BOTH the rudder and the elevator should work together to bring the nose up so that the plane doesn't veer too far off course during the correction. I'll probably add this fix at some point in the not too far distant future.

Flowchart for grabDataRadio()

Flowchart for bankPitchLimiter()


Code for updateControlsLimiter()
//update struct based on euler angles
void IFC_Class::updateControlsLimiter(bool axis)
    //minimum servo command to get back to safe flight
    uint16_t minServoCommand;

    //determine if the current IMU data is old - if so, get new IMU data
    if ((millis() - dataTimestamp_IMU) >= LIMITER_PERIOD)
        //grab IMU data

    //determine if pitch or roll should be tweaked (axis==true --> pitch, axis==false --> roll)
    if (axis == PITCH_AXIS)
        //determine if the current pitch angle is too low
        if (telemetry.pitchAngle <= UNSAFE_PITCH_UP)
            //determine minimum servo command to get back to safe flight
            minServoCommand = constrain(map(abs(telemetry.pitchAngle), abs(UNSAFE_PITCH_UP), abs(MAX_PITCH_UP), ELEVATOR_MID, ELEVATOR_MIN), ELEVATOR_MIN, ELEVATOR_MID);
            //determine if minimum servo command is less than current servo command - if so, replace current command with minimum servo command
            if (minServoCommand < controlInputs.pitch_command)
                //update controlInputs struct
                controlInputs.pitch_command = minServoCommand;
        //determine if the current pitch angle is too high
        else if (telemetry.pitchAngle >= UNSAFE_PITCH_DOWN)
            //determine minimum servo command to get back to safe flight
            minServoCommand = constrain(map(abs(telemetry.pitchAngle), abs(UNSAFE_PITCH_DOWN), abs(MAX_PITCH_DOWN), ELEVATOR_MID, ELEVATOR_MAX), ELEVATOR_MID, ELEVATOR_MAX);
            //determine if minimum servo command is more than current servo command - if so, replace current command with minimum servo command
            if (minServoCommand > controlInputs.pitch_command)
                //update controlInputs struct
                controlInputs.pitch_command = minServoCommand;
    else if (axis == ROLL_AXIS)
        //determine if the current roll angle is too low
        if (telemetry.rollAngle <= UNSAFE_ROLL_R)
            //determine minimum servo command to get back to safe flight
            minServoCommand = constrain(map(abs(telemetry.rollAngle), abs(UNSAFE_ROLL_R), abs(MAX_ROLL_R), AILERON_MID, AILERON_MAX), AILERON_MID, AILERON_MAX);

            //determine if minimum servo command is more than current servo command - if so, replace current command with minimum servo command
            if (minServoCommand > controlInputs.roll_command)
                //update controlInputs struct
                controlInputs.roll_command = minServoCommand;
        //determine if the current roll angle is too high
        else if (telemetry.rollAngle >= UNSAFE_ROLL_L)
            //determine minimum servo command to get back to safe flight
            minServoCommand = constrain(map(abs(telemetry.rollAngle), abs(UNSAFE_ROLL_L), abs(MAX_ROLL_L), AILERON_MID, AILERON_MIN), AILERON_MIN, AILERON_MID);

            //determine if minimum servo command is less than current servo command - if so, replace current command with minimum servo command
            if (minServoCommand < controlInputs.roll_command)
                //update controlInputs struct
                controlInputs.roll_command = minServoCommand;
I know this doesn't pertain to RC planes, but I'm going to try and make a custom Arduino car heads up display/diagnostics tool if anyone is interested. Build log link here.

It'll basically be a side project while I wait for weather warm enough for my first test flight!
I know I haven't done much for this project lately, but I think the weather is finally going to get good enough this coming weekend for a text flight - should be fun :cool: