Drone Motor Thrust Tester

30 Jun.,2022

Drone Motor Thrust Tester 1. Introduction 2. Idea 3. Plan 4. Design Mechanical design Main construction - wooden planks Main Plank Leg Plank Vertical Plank Bracing Plank Offcut Load cell and motor mount Motor Adapter Motor Plate 3D printed...

 

LY-10KGF Thrust Stand

Drone Motor Thrust Tester

 

1. Introduction

     Hi! This will be my entry for the Data Conversion Project14. My initial idea when I saw that this theme will be picked for this month was to make a simple pocket signal generator since I don't already have a signal generator. But then I started looking at the Data Conversion page and saw a lot of project ideas based on data acquisition and charting and later on, the attack of the drones was announced. And that's how I got to this project. I wanted to build a custom designed drone for a few years now, with most of the parts just scattered in different bins. As I started planning out the drone, I of course grew my wish list for the things onboard, Raspberry, Raspberry HQ Camera, why not make a gimbal for the camera, retractable legs and I am sure the list will go on and on...

 

2. Idea

     While on one hand, this is a list of cool stuff that can be found on the drone, on the other hand, it's extra weight and flying things don't like that. A good place to be for a drone is to have thrust to weight ratio of 2. In other words, for every 100g of weight I add on the drone, I'll need an additional 200g of thrust to compensate for that. This is so that the drone can hover somewhere at around 50% throttle. My idea for this project, as the name suggests, is to make a drone motor thrust tester using some simple components. To measure the thrust, we need something that can measure force, such a thing would be a load cell which I already have laying around from one of my old projects. The whole project would be based around that, since that is the most critical measurement for us, but, since I'm already making a drone motor thrust tester, why not add a few more features to it.

If you plan on building a test rig like this, please be careful as the propellers are spinning incredibly fast and can cause serious injuries like that. The propeller can loosen up or it can break due to the fast rotation and cause serious injuries, you're doing that at your own risk so please be careful and smart about it!

3. Plan

     I usually love having a lot of physical buttons, dials, displays and so on, but I wanted to change it up a bit this time. Instead of having all of those physical controls, I decided to program a simple GUI using Python since I'm already collecting a lot of data which I would like to plot on some graphs later on. My plan is to have measurement of 4 different things for the motor, the current that the motor is drawing from the battery, battery voltage, RPM of the spinning motor and of course, the main one, the thrust of the motor.

     On the diagram, you can see the layout of the whole system. We have the loadcell onto which the motor is attached. The load cell is connected to the Arduino using a HX711 24bit ADC. Besides that, there is a big relay for connecting the battery to the system and a simple optical encoder consisting of an IR LED and an IR transistor. The ESC for the BLDC motors are easily controlled using a PWM signal, we're controlling the BLDC speed in the same way we are controlling a simple hobby servo position. And in the end, we have the GUI that will be on my laptop which will be connected with a USB cable to the Arduino Nano. Before I get into the design of everything, I'll make a short list of basic requirements that I want to satisfy with this build, I'll categorize them into few groups based on their priority for me:

 

  1. Priority

    1. Thrust measurement

    2. Current measurement

    3. Voltage measurement

    4. GUI control

    5. GUI live feed data

    6. Storing thrust/current/voltage data in a format that can be easily used later with MATLAB/Excel

  2. Priority


    1. RPM measurement

    2. Live graphing of the data

    3. Automated motor testing sequences

    4. Automated graph plotting

 

4. Design

     With my idea explained and my plan somewhat worked out, it's time to design, make and test this contraption. First of all, design. As usual, the design is separated into 3 segments, the mechanical design, the electrical design and the software design which will in this case consist of the code for the Arduino and the code for the GUI that I will be running on my laptop. Let's begin with the mechanical design.

 

Mechanical design

     To begin with the mechanical design, let's first talk about the materials I'm planning on using. For this project, I'm going to use wooden planks for the main construction bolted and glued together, with some additional 3D printed pieces, which will mostly be either brackets, or to tidy up the whole build to look a bit nicer. But before we get into that, let's take a look at the main construction made with the wooden planks.

 

Main construction - wooden planks

     I will be using some planks I found lying around which are around 37x17mm (they come as braces in the packaging of a dryer, looked nice, so I took them for this project). There was a couple of them and we need to cut them to length, here are their dimensions.

{tabbedtable} Tab Label Tab Content

Main Plank

First of all we have the main planks, we need 2 of these. They are the main construction of the whole thing as you will see soon, here are the dimensions.

 

Leg Plank

Now we have the leg planks. We also need 2 of them, these are shorter planks that attach to the main planks and as the name suggests, this is where the our feet are going to go later on.

 

Vertical Plank

This is the plank that will actually carry our load cell and motor. My plan is to have this long construction to stabilize everything, so I can make it tall enough to be able to experiment with bigger motors and propellers.


Bracing Plank

The last longer piece we need is the bracing plank. This is a blank that will attach between the 2 main planks and to vertical plank to keep the whole construction really sturdy, This is the only plank that will need some 45 degree cuts.

 

Offcut

The last thing we need is literally a small square offcut. This will go between the main planks at the end so we can more easily attach that side of the construction.

 

     Those would be all of the wooden pieces necessary for putting this whole thing together. As I've previously mentioned, I've attached everything together using some wood screws and some wood glue. One mistake I made at first while working on this was that I didn't drill pilot holes into the wood before driving in the screws, which caused a few cracks, but after gluing it and clamping it together, it turned out great. Let's now take a look at how this whole thing is supposed to go together.

This is the how the whole construction looks like. It's roughly 400mm by 300mm which isn't that small, but I wanted it to be stable and I also wanted the option of being able to test out bigger motors and propellers. With that part done, it's time to take a look at the mounting for the motor and the load cell.

 

Load cell and motor mount

     Now we come for the main reason for the whole build, mounting the load cell. You've maybe noticed 2 holes in the vertical plank, those are for mounting the load cell. The load cell has to sets of holes, one set of M4 holes and one set of M5 holes. These holes are at 15mm from one another.

Mounting the load cell to the construction is easy. I just used a couple of bolts and nuts and attached it firmly to the construction, the question is, how do we attach the motor? I wanted to make something like a mounting plate where I can easily make customizable adapter for the motors so I can switch them around. To do this, I designed 2 pieces, a motor adapter which goes on to the load cell and a motor plate which attaches directly to the motor.

{tabbedtable} Tab Label Tab Content

Motor Adapter

This is the part that will attach to the load cell using the 2 M4 screw holes on the load cell. Later, the motor plate with the motor attached will be attached to this.


Motor Plate

I've designed this motor plate to fit the small drone motors I currently own, but the idea is to redesign this part whenever you have a motor that doesn't fit on the existing one. The only thing you need to look out for is to have 2 holes for M4 screws at 50mm apart and a motor that mounts somewhere in the middle.

You can see that the shape matched the motor adapter shape. The whole in the middle is a bit bigger so it can easily fit the all of the screw heads as you will se shortly.

All that's left now is to mount everything to each other. Let's first mount the load cell to the vertical plank.

Now we can mount the motor adapter to the load cell.

After that, the motor first needs to be attached to motor plater after which we can attach that whole thing to the motor adapter.

The plate and the adapter are held together using 2 M4 screws with some nuts and washers. I tend to use washers wherever I can when dealing with 3D printing so I can spread the load a bit better and not cave in the prints.

Here is how this looks like all put together in real life.

 

3D printed covers and feet

     To make this looks a bit better, I wanted to print some covers for the parts where the planks were joined together, as well as some feet to which I attach smaller rubber feet to keep everything stable on the ground. We need 4 feet and we need 2 covers for the both ends where we have the planks joined together. I don't want any screws on those covers or feet since they are just supposed to look pretty, so my plan is to make them so they can slide on easily but secure them on using a bit of super glue.

{tabbedtable} Tab Label Tab Content

Cover 1

This cover will go over the end that is further from the vertical plank and the load cell. It's a pretty simple piece that is just supposed to slide over that part and attach with some glue.

Cover 2

The second cover looks a lot like the first cover, but it just has one additional feature and that is so the vertical plank can go through the top. Beside that, all of the dimensions are the same.

The last thing here are the 4 feet that are needed for the leg planks

They just slide on, but they have a small square indentation on the bottom of them, this is the place where I glues in a small rubber foot. It would have worked without that small indentation for sure, but I just wanted to have a dedicated place for the small rubber feet.

With all of those designed, let's see how they all go together on our main construction.

And at the end, here is how our whole construction looks like now.

 

Electronic Mounts

     The last parts that need to be printed are some of the mounts for electronics. I was in a rush to wrap up this project so I went completely solderless using a small breadboard and a screw terminal shield for an Arduino Nano. Besides that I needed a mount for the optical encoder as well as the encoder itself, mount for the power switch and mount for the power connector. I'll begin first with those 2 mounts and then go to the encoder.

{tabbedtable} Tab Label Tab Content

Main switch holder

This will hold the main power switch which will be connected to the relay, but all of that, I'll cover in the electrical design section. This is one of those turn switches and it has a weird mounting mechanism with 2 screws going at a weird angle. So, instead of using that mounting method, I designed a mount with a tightening ring that will hold on to the main part of switch.

Power connector holder

This will be the mount which will hold onto 2 4mm banana plug connectors. It will resemble the main switch mount a lot, but will just have 2 holes in the top into which the banana plugs screw in.

Encoder

My idea for the encoder was to have a small IR LED and IR transistor which would detect blade passes between them. But my mounting idea for the encoder was to use a goose neck so I can adapt it to any kind of a propeller that I want. This part of the design will consist of 2 parts, the mount for the gooseneck and the encoder itself. Let's first take a look at the mount.

This is just supposed to screw into one of the planks using 2 wood screws and has a thread in the middle that matches the thread of the gooseneck. These are the same goosenecks I used in my 3D printed hands project ( 3D Printed Helping Hands V1  ) so at the top, there was a thread for the M4 screw. I'm of course going to use that to mount the encoder. Here is my encoder design.

The design is simple, it has a hole at the bottom that can fit a M4 screw with the hexagonal head and it has 2 holes at the top for the 3mm IR LED and IR transistor. And you would just place the encoder so the propeller goes between the 2 and that's all there is to it.

With all of that designed it's time to look at how all of this looks put together.

On the first picture above, you can see another 3D printed piece, that was the mount for the relay I originally planned on using, but it turned out it was a dead relay, so I had to use another one with a different form factor.

You can see all of the electronics mounted up as well as a glimpse of the GUI, but I will get to all of that shortly.

 

Electrical design

     Now we come to the electronics. Unfortunately, I didn't manage to finish up everything I wanted to here, I didn't have time to do the encoder. But I managed to wire up the rest of the system and to get it working. To start, let's take a look at the schematic for the whole project.

     As the main part I will use an Arduino Nano, small, cheap and I can easily communicate with it using a USB cable for the interface. The Arduino is connected to a current sensor which can measure up to 30A, there is a single analog pin used for reading that value. To control the BLDC motor we need an ESC. ESC stands for Electronic Speed Controller, it takes the same input as a small hobby servo motor and spins up our BLDC drone motor. As a main power switch I had a rotary switch which was connected to a powerful car relay. The one I intended to use has a rating of 70A, but it was dead, so I switched to an equally beefy one that worked. To monitor if the relay is on or not which we can later use on to know if it's safe to approach the machine or not, I added a small shifter circuit, which used a small optocoupler to step everything down to 5V so I don't burn down the Arduino. Here is all of the electronics mounted to our construction.

 

     Besides the data acquisition and charting that I've mentioned in the beginning, let's also take a look at the real data conversion part of this project, the ADC. The ADC that is most commonly used for load cells is the HX711. HX711 is a 24bit  sigma delta ADC which can either do 10 samples per second or 80 samples per second. While I would like 100SPS, for some first basic tests, this will work just great and it's really easy to use, with libraries developed for Arduino, Raspberry and so on.

     On the first picture you can see the big relay, Arduino Nano and the power switch. I liked the change of work from soldering to crimping everything. I crimped either the connector needed for the relay or crimped on little ferrules to keep all of the strands together and to keep it all nice and tidy. One neat thing I found out while making this was that those yellow small crimping connector fit perfectly into the small breadboard on the side. They are a bit harder to get in, but there's no way of the coming out like is the case with standard jumper wires. Glowing red on the second picture is the current sensor and you can also see where I've mounted the gooseneck.

     To power it all up I used my bench power supply which unfortunately can only supply 3A which is not close to enough, so the readings at the end won't be that high. The reason I'm using the bench power supply is because the Lipo battery hasn't arrived yet that I plan on using for the drone later on. But it will be a 3s one. This will do just fine for testing. All that's left now is to write some (a lot) of code and to test it out.

 

Software

     Software section has 2 parts, one will be the Arduino side which will communicate with all of the sensors and send that data over, and the other, more interesting one will be the GUI side, which will be our interface for interacting with this build. Let's first take a look at the Arduino side code. This is not the final code, but rather just a test code that I will show running later in the video.

 

Arduino

     The code is a work in progress, but to explain it quickly, it reads out data from the load cell and the current meter currently, and at set number of ms sends out that data over serial to our interface. It sends it out in a string package which I can easily later dismantle into data in Python as I will show in the next section. It need a bit more work, mostly on the receiving communication data from the interface as I'm having a bit of trouble there, but will post an update once I get all of that working.

 

// Libraries we need
#include <Servo.h>
#include "HX711.h"
#include "TimerOne.h"


// All of the pin connection on the Arduino
#define pinRelay 2
#define pinESC 3
#define pinDT 4
#define pinSCK 5
#define pinCurrent A0


// Defining our scale and ESC
HX711 scale;
Servo ESC;


// Variables
int power = 0;
volatile long previousMillis = 0;
long previousMillis1 = 0;
double thrust = 0;
int current = 0;
int throttle = 0;
bool rise = true;


void setup() {
  
  Serial.begin(9600); 
  pinMode(pinRelay, INPUT);


  // This part will setup our load cell to work properly
  scale.begin(pinDT, pinSCK);
  scale.set_scale(-205);            
  scale.tare();


  // This part will turn on our ESC and the delay will 
  // let it to it's start up procedure
  ESC.attach(pinESC);
  ESC.write(0);
  delay(5000);
   
}


// Function for sending power to ESC - not currently used
void ControlESC(int pwr){
    if(pwr > 100) pwr = 100;
    if(pwr < 0) pwr = 0;
    int pwr1 = map(pwr, 0, 100, 0, 180);
    ESC.write(pwr1);
  }


// Function for sending data over Serial in a specific format
void SendData(int g, int c){
    s = "";
    s = "t" + String(0) + "g" + String(g) + "r" + String(0) + "c" + String(c) + "v" + String(0) + "e";
    Serial.println(s);
  }


void loop() {
  // This will read out our thrust every set amount of ms
  if(millis() - previousMillis >= 100){
      
      previousMillis = millis();
      thrust = scale.get_units();
      current = analogRead(pinCurrent);
      SendData(thrust, current);
    }


  // This will create a sawtooth style throttle curve
  if(millis() - previousMillis1 >= 500){
      previousMillis1 = millis();
      if(throttle >= 100) rise = false;
      if(throttle <= 0) rise = true;
      if(rise == true){
          throttle += 1;
          ESC.write(throttle);
        }
        else{
            throttle -= 1;
            ESC.write(throttle);
          }
      
    }
}

 

GUI

     Now we come to the GUI. To make the GUI, I used Python and the PyQt5 library with the addition of the PyQtGraph library. Before I get into the code, I will show a quick layout which I wanted to achieve with this GUI. I wanted it to be a full screen, single screen GUI, which would offer controls as well as show our data in raw number and on a live updating graph.

The settings part would allow to change the number of blades on the propeller and choose if we want this to run in automatic or manual mode. The relay section is just a safety indicator which purpose is to show us if the relay is on or off. Dials are, as the name suggest, dials which can show our thrust, throttle, RPM and current (surprisingly, I couldn't find dial widgets so I kind of had to make my own). Manual controls would let us change the throttle manually using a slider and start/stop the recording of the data. Automatic controls would enable us to choose the test/procedures we would like to run automatically, where we can program throttle curves and stuff like that. And the last thing would be the live graph section, which is a graph that would show us our data and update live as our tests our going on. With the layout explained, here's the code.

from PyQt5.QtWidgets import QApplication, QPushButton, QLineEdit, QLabel, QWidget, QSlider, QSpinBox, QComboBox, QMainWindow
from PyQt5.QtGui import QPainter, QColor, QPen, QFont, QBrush
from PyQt5.QtCore import Qt, QTimer
from PyQt5 import QtWidgets
from pyqtgraph.Qt import QtGui, QtCore
from pyqtgraph import PlotWidget, plot
import pyqtgraph as pg
import os
import sys
import re
import threading
import time
import serial
from random import randint


xDials = 750
yDials = 50
spaceX = 300
pom = 1
val = -5
mode = 1
xThrust = xDials
yThrust = yDials
xThrottle = xDials + spaceX
yThrottle = yDials
xRPM = xDials + 2*spaceX
yRPM = yDials
xCurrent = xDials + 3*spaceX
yCurrent = yDials
xRelay = xDials - 320
xSettings = 20
blades = 2
arduino = serial.Serial("COM10", 9600)
throttleSet = 0
thrust = 0
record = -1
running = -1


def convertString(s):
    global time1
    global thrust
    try:
        pattern = "t(.*?)g"
        time1 = int(re.search(pattern, s).group(1))
    except:
        time1 = -1
    
    try:
        pattern = "g(.*?)r"
        thrust1 = re.search(pattern, s).group(1)
    except:
        thrust1 = -1
    
    try:
        pattern = "r(.*?)c"
        rpm = re.search(pattern, s).group(1)
    except:
        rpm = -1
    
    try:
        pattern = "c(.*?)v"
        current = re.search(pattern, s).group(1)
    except:
        current = -1
        
    try:
        pattern = "v(.*?)e"
        voltage = re.search(pattern, s).group(1)
    except:
        voltage = -1    
        
    thrust = int(thrust1)
    print("Current time: " + str(time1))
    print("Current thrust: " + str(thrust))
    print("Current rpm: " + str(rpm))
    print("Current current: " + str(current))
    print("Current voltage: " + str(voltage))
    
    


class App(QWidget):
    
    global val
    global xThrust, yThrust, xThrottle, yThrottle, xRPM, yRPM, xCurrent, yCurrent, xRelay, xDials, yDials, xSettings
    global thrust, record, throttleSet
    
    def __init__(self):       
        super().__init__()
        self.title = 'Thrust Tester'
        self.left = 200
        self.top = 200
        self.width = 1920
        self.height = 1080
        self.qTimer = QTimer()
        self.qTimer.setInterval(50)    
        self.qTimer.timeout.connect(self.updateEvent)
        self.qTimer.timeout.connect(self.update_plot_data)
        self.qTimer.start()
        
        
        
        self.initUI()
        
    def initUI(self):
        self.setWindowTitle(self.title)
        self.setGeometry(self.left, self.top, self.width, self.height)
        
        self.setAutoFillBackground(True)
        p = self.palette()
        p.setColor(self.backgroundRole(), QColor(25, 35, 40))
        self.setPalette(p)
        
        ### DIAL SECTION
        
        # THRUST DIAL
        self.labelThrust = QLabel(self)
        self.labelThrust.setText(str(thrust))
        self.labelThrust.setGeometry(xThrust + 62, yThrust + 75, 300, 50)
        self.labelThrust.setFont(QFont("Arial", 36))
        self.labelThrust.setStyleSheet("QLabel {color : cyan}")
        
        self.labelThrustName = QLabel(self)
        self.labelThrustName.setText("Thrust [g]")
        self.labelThrustName.setGeometry(xThrust + 20, yThrust + 210, 300, 50)
        self.labelThrustName.setFont(QFont("Arial", 24))
        self.labelThrustName.setStyleSheet("QLabel {color : cyan}")
        
        # Throttle DIAL
        self.labelThrottle = QLabel(self)
        self.labelThrottle.setText(str(-val))
        self.labelThrottle.setGeometry(xThrottle + 75, yThrottle + 75, 300, 50)
        self.labelThrottle.setFont(QFont("Arial", 36))
        self.labelThrottle.setStyleSheet("QLabel {color : rgb(255, 7, 58)}")
        
        self.labelThrottleName = QLabel(self)
        self.labelThrottleName.setText("Throttle [%]")
        self.labelThrottleName.setGeometry(xThrottle + 20, yThrottle + 210, 300, 50)
        self.labelThrottleName.setFont(QFont("Arial", 24))
        self.labelThrottleName.setStyleSheet("QLabel {color : rgb(255, 7, 58)}")
        
        # RPM DIAL
        self.labelRPM = QLabel(self)
        self.labelRPM.setText(str(-val))
        self.labelRPM.setGeometry(xRPM + 62, yRPM + 75, 300, 50)
        self.labelRPM.setFont(QFont("Arial", 36))
        self.labelRPM.setStyleSheet("QLabel {color : rgb(255, 131, 0)}")
        
        self.labelRPMName = QLabel(self)
        self.labelRPMName.setText("RPM [x1000]")
        self.labelRPMName.setGeometry(xRPM + 10, yRPM + 210, 300, 50)
        self.labelRPMName.setFont(QFont("Arial", 24))
        self.labelRPMName.setStyleSheet("QLabel {color : rgb(255, 131, 0)}")
        
        # CURRENT DIAL
        self.labelCurrent = QLabel(self)
        self.labelCurrent.setText(str(-val))
        self.labelCurrent.setGeometry(xCurrent + 75, yCurrent + 75, 300, 50)
        self.labelCurrent.setFont(QFont("Arial", 36))
        self.labelCurrent.setStyleSheet("QLabel {color : rgb(57, 255, 20)}")
        
        self.labelCurrentName = QLabel(self)
        self.labelCurrentName.setText("Current [A]")
        self.labelCurrentName.setGeometry(xCurrent + 20, yCurrent + 210, 300, 50)
        self.labelCurrentName.setFont(QFont("Arial", 24))
        self.labelCurrentName.setStyleSheet("QLabel {color : rgb(57, 255, 20)}")
        
        
        
        
        ### RELAY SECTION
        
        # RELAY NAME
        self.labellRelayName = QLabel(self)
        self.labellRelayName.setText("RELAY")
        self.labellRelayName.setGeometry(xRelay + 30, yDials - 15, 300, 50)
        self.labellRelayName.setFont(QFont("Arial", 43))
        self.labellRelayName.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        
        # RELAY STATUS ON
        self.labellRelayStatusON = QLabel(self)
        self.labellRelayStatusON.setText("ON")
        self.labellRelayStatusON.setGeometry(xRelay + 75, yDials + 200, 300, 50)
        self.labellRelayStatusON.setFont(QFont("Arial", 43))
        self.labellRelayStatusON.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.labellRelayStatusON.setHidden(False)
        
        # RELAY STATUS OFF
        self.labellRelayStatusON = QLabel(self)
        self.labellRelayStatusON.setText("OFF")
        self.labellRelayStatusON.setGeometry(xRelay + 70, yDials + 200, 300, 50)
        self.labellRelayStatusON.setFont(QFont("Arial", 43))
        self.labellRelayStatusON.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        self.labellRelayStatusON.setHidden(True)
        
        
        
        
        ### SETTINGS SECTION
        
        # SETTIGNS NAME
        self.labelSettingsName = QLabel(self)
        self.labelSettingsName.setText("SETTINGS")
        self.labelSettingsName.setGeometry(xSettings + 48, yDials - 15, 300, 50)
        self.labelSettingsName.setFont(QFont("Arial", 43))
        self.labelSettingsName.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        
        # NUMBER OF BLADES
        self.labelSettingsName = QLabel(self)
        self.labelSettingsName.setText("Number of Blades:")
        self.labelSettingsName.setGeometry(xSettings + 15, yDials + 70, 300, 50)
        self.labelSettingsName.setFont(QFont("Arial", 24))
        self.labelSettingsName.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        
        # NUMBER OF BLADES SPINBOX
        self.numberOfBlades = QSpinBox(self)
        self.numberOfBlades.valueChanged.connect(self.bladesValueChange)
        self.numberOfBlades.setRange(2, 5)
        self.numberOfBlades.setGeometry(xSettings + 290, yDials + 70, 70, 50)
        self.numberOfBlades.setFont(QFont("Arial", 30))
        self.numberOfBlades.setValue(2)
        self.numberOfBlades.setStyleSheet("QSpinBox {background-color : rgb(45, 50, 60); color : rgb(150, 150, 150)}")
        
        # MODE BUTTON
        self.buttonMenu = QPushButton("MODE", self)
        self.buttonMenu.resize(100, 50)
        self.buttonMenu.move(xSettings + 15, yDials + 175)
        self.buttonMenu.clicked.connect(self.buttonModeFunction)
        self.buttonMenu.setFont(QFont("Arial", 20))
        self.buttonMenu.setStyleSheet("QPushButton {background-color : rgb(45, 50, 60); color : rgb(150, 150, 150); font-weight: bold}")
        
        # MODE OPTIONS
        self.labelSettingsName = QLabel(self)
        self.labelSettingsName.setText("A        M")
        self.labelSettingsName.setGeometry(xSettings + 145, yDials + 175, 300, 50)
        self.labelSettingsName.setFont(QFont("Arial", 30))
        self.labelSettingsName.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        
        
        
        
        ### MANUAL CONTROLS
        
        # NAME
        self.labelManualName = QLabel(self)
        self.labelManualName.setText("MANUAL CONTROLS")
        self.labelManualName.setGeometry(xSettings + 45, yDials + 295 + 10, 600, 50)
        self.labelManualName.setFont(QFont("Arial", 43))
        self.labelManualName.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        
        # Throttle
        self.labelManualName = QLabel(self)
        self.labelManualName.setText("Throttle")
        self.labelManualName.setGeometry(xSettings + 15, yDials + 295 + 70 + 30, 600, 50)
        self.labelManualName.setFont(QFont("Arial", 30))
        self.labelManualName.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        
        # Slider1
        self.Slider1 = QSlider(Qt.Horizontal, self)
        self.Slider1.setGeometry(xSettings + 170, yDials + 295 + 70 + 30, 360, 50)
        self.Slider1.setRange(0,100)
        self.Slider1.valueChanged[int].connect(self.changeSliderValue)
        
        # Slider1 Label
        self.labelSlider1 = QLabel(self)
        self.labelSlider1.setText(str(throttleSet))
        self.labelSlider1.setGeometry(xSettings + 170 + 390, yDials + 295 + 70 + 30, 67, 50)
        self.labelSlider1.setFont(QFont("Arial", 30))
        self.labelSlider1.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        
        # RECORD BUTTON
        self.buttonRecord = QPushButton("RECORD", self)
        self.buttonRecord.resize(150, 50)
        self.buttonRecord.move(xSettings + 15, yDials + 495)
        self.buttonRecord.clicked.connect(self.buttonRecordFunction)
        self.buttonRecord.setFont(QFont("Arial", 20))
        self.buttonRecord.setStyleSheet("QPushButton {background-color : rgb(45, 50, 60); color : rgb(150, 150, 150); font-weight: bold}")
        
        # RECORD LABEL
        self.labelRecord = QLabel(self)
        self.labelRecord.setText("Recording:")
        self.labelRecord.setGeometry(xSettings + 190 , yDials + 495, 400, 50)
        self.labelRecord.setFont(QFont("Arial", 30))
        self.labelRecord.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        
        # STOP BUTTON
        self.buttonStop1 = QPushButton("STOP ALL", self)
        self.buttonStop1.resize(150, 50)
        self.buttonStop1.move(xSettings + 475, yDials + 495)
        self.buttonStop1.clicked.connect(self.buttonStopFunction)
        self.buttonStop1.setFont(QFont("Arial", 20))
        #self.buttonStop1.setStyleSheet("QPushButton {background-color : rgb(45, 50, 60); color : rgb(150, 150, 150)}")
        self.buttonStop1.setStyleSheet("QPushButton {background-color : red; color : yellow; font-weight: bold}")
        
        
        
        
        ### AUTO CONTROLS
        
        # NAME
        self.labelManualName = QLabel(self)
        self.labelManualName.setText("AUTO CONTROLS")
        self.labelManualName.setGeometry(xSettings + 80, yDials + 310 + 320, 600, 50)
        self.labelManualName.setFont(QFont("Arial", 43))
        self.labelManualName.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        
        # COMBO BOX LABEL
        self.labelRecord = QLabel(self)
        self.labelRecord.setText("Select test:")
        self.labelRecord.setGeometry(xSettings + 15 , yDials + 710, 400, 50)
        self.labelRecord.setFont(QFont("Arial", 30))
        self.labelRecord.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        
        # COMBO BOX
        self.testPickComboBox = QComboBox(self)
        self.testPickComboBox.addItems(["Linear Throttle Test", "Speed Up/Down Test", "Responsivness Test", "All Tests Sequentially"])
        self.testPickComboBox.setGeometry(xSettings + 230 , yDials + 710, 390, 50)
        self.testPickComboBox.setFont(QFont("Arial", 20))
        self.testPickComboBox.setStyleSheet("QComboBox {background-color : rgb(45, 50, 60); color : rgb(150, 150, 150)}")
        
        # RUN TEST BUTTON
        self.buttonRunTest = QPushButton("RUN TEST", self)
        self.buttonRunTest.resize(165, 50)
        self.buttonRunTest.move(xSettings + 15, yDials + 800)
        self.buttonRunTest.clicked.connect(self.buttonRunFunction)
        self.buttonRunTest.setFont(QFont("Arial", 20))
        self.buttonRunTest.setStyleSheet("QPushButton {background-color : rgb(45, 50, 60); color : rgb(150, 150, 150); font-weight: bold}")
        
        # RUNNING LABEL
        self.labelRecord = QLabel(self)
        self.labelRecord.setText("Running:")
        self.labelRecord.setGeometry(xSettings + 200 , yDials + 800, 400, 50)
        self.labelRecord.setFont(QFont("Arial", 30))
        self.labelRecord.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        
        # STOP BUTTON
        self.buttonStop2 = QPushButton("STOP ALL", self)
        self.buttonStop2.resize(150, 50)
        self.buttonStop2.move(xSettings + 475, yDials + 800)
        self.buttonStop2.clicked.connect(self.buttonStopFunction)
        self.buttonStop2.setFont(QFont("Arial", 20))
        self.buttonStop2.setStyleSheet("QPushButton {background-color : red; color : yellow; font-weight: bold}")
        
        # MESSAGE NAME LABEL
        self.labelMessageName = QLabel(self)
        self.labelMessageName.setText("Message:")
        self.labelMessageName.setGeometry(xSettings + 15 , yDials + 880, 400, 50)
        self.labelMessageName.setFont(QFont("Arial", 30))
        self.labelMessageName.setStyleSheet("QLabel {color : rgb(150, 150, 150)}")
        
        # MESSAGE LABEL
        self.labelMessage = QLabel(self)
        self.labelMessage.setText("")
        self.labelMessage.setGeometry(xSettings + 200 , yDials + 880, 420, 50)
        self.labelMessage.setFont(QFont("Arial", 30))
        self.labelMessage.setStyleSheet("QLabel {background-color : rgb(45, 50, 60); color : rgb(150, 150, 150)}")
        
        
        
        ### GRAPH
        
        self.x = list(range(100))  # 100 time points
        self.y = [randint(0,0) for _ in range(100)]  # 100 data points
        self.y1 = [randint(0,0) for _ in range(100)]
        #self.y = numpy.zeros(100)
        #self.y1 = numpy.zeros(100)
        
        self.graphWidget = pg.PlotWidget(self)
        #self.setCentralWidget(self.graphWidget)
        self.graphWidget.setGeometry(xDials - 50, yDials + 290, 1200, 670)
        self.graphWidget.setBackground(None)
        self.graphWidget.setYRange(0, 100, padding=0.04)
        pen = pg.mkPen(color=(255, 7, 58), width=3)
        
        
        self.graphWidget.getAxis("bottom").setFont(QFont("Arial", 30))
        
        styles = {'color':'rgb(150, 150, 150)', 'font-size':'24px'}
        self.graphWidget.setLabel('left', 'Thrust [%]', **styles)
        self.graphWidget.setLabel('bottom', 'Time [ms]', **styles)


        # plot data: x, y values
        self.data_line =  self.graphWidget.plot(self.x, self.y, pen=pen)
        #self.graphWidget.plot(hour, temperature, pen = pen)
        pen = pg.mkPen(color=(0, 255, 255), width=3)
        self.data_line1 =  self.graphWidget.plot(self.x, self.y1, pen=pen)
        
        
        self.show()
        
        
    def update_plot_data(self):
        global throttleSet, thrust
        self.x = self.x[1:]  # Remove the first y element.
        self.x.append(self.x[-1] + 1)  # Add a new value 1 higher than the last.


        self.y = self.y[1:]  # Remove the first 
        self.y.append(throttleSet)  # Add a new random value.
        
        self.y1 = self.y1[1:]  # Remove the first 
        self.y1.append(thrust/1.7)  # Add a new random value.


        self.data_line.setData(self.x, self.y)  # Update the data.
        self.data_line1.setData(self.x, self.y1)  # Update the data.
        
        
    def updateEvent(self):
        self.labelThrust.setText(str(thrust))
        self.labelThrottle.setText(str(throttleSet))
        self.labelRPM.setText(str(-val/10))
        self.labelCurrent.setText(str(-val/10))
        self.update()
        
    def bladesValueChange(self):
        global blades
        blades = self.numberOfBlades.value()
        
    def buttonModeFunction(self):
        global mode
        mode = mode * (-1)
        
    def changeSliderValue(self, value):
        global throttleSet
        throttleSet = value
        print(throttleSet)
        self.labelSlider1.setText(str(value))
        arduino.write(throttleSet.encode())
        
    def buttonRecordFunction(self):
        global record
        record = record * (-1)
        
    def buttonStopFunction(self):
        global record, throttleSet, running
        record = -1
        running = -1
        throttleSet = 0
        self.labelMessage.setText("ABORTED")
        self.Slider1.setValue(0)
        self.labelSlider1.setText(str(0))
        self.updateEvent(self)
        
    def buttonRunFunction(self):
        global running
        running = running * (-1)
        
        
    def paintEvent(self, event):


        global xThrust, yThrust, xThrottle, yThrottle, xRPM, yRPM, xCurrent, yCurrent, xDials, yDials, xRelay, xSettings
        global thrust, throttleSet, record, running
        
        global val
        global pom
        painter = QPainter()
        painter.begin(self)
        painter.setRenderHint(QPainter.Antialiasing)
        
        ### DIALS PAINTING
        
        painter.setPen(QPen(QColor(150, 150, 150), 5))
        painter.drawRect(xDials - 50, yDials - 30, 1200, 300)
        
        painter.setPen(QPen(QColor(45, 50, 60), 20))      
        painter.drawArc(xThrust, yThrust, 200, 200, -90 * 16, 360 * 16)
        painter.drawArc(xThrottle, yThrottle, 200, 200, -90 * 16, 360 * 16)
        painter.drawArc(xCurrent, yCurrent, 200, 200, -90 * 16, 360 * 16)
        painter.drawArc(xRPM, yRPM, 200, 200, -90 * 16, 360 * 16)
              
        painter.setPen(QPen(Qt.cyan, 20))            
        painter.drawArc(xThrust, yThrust, 200, 200, -90 * 16, -thrust * 16)
        
        painter.setPen(QPen(QColor(255, 7, 58), 20))
        painter.drawArc(xThrottle, yThrottle, 200, 200, -90 * 16, -throttleSet * 16 * 3.6)
        
        painter.setPen(QPen(QColor(255, 131, 0), 20))
        painter.drawArc(xRPM, yRPM, 200, 200, -90 * 16, val * 16)
        
        painter.setPen(QPen(QColor(57, 255, 20), 20))
        painter.drawArc(xCurrent, yCurrent, 200, 200, -90 * 16, val * 16)
        
        
        
        ### RELAY STATUS PAINTING
        painter.setPen(QPen(QColor(150, 150, 150), 5))
        painter.drawRect(xRelay, yDials - 30, 250, 300)      
        painter.setBrush(QBrush(QColor(57, 255, 20), Qt.SolidPattern))
        painter.drawEllipse(xRelay + 70, yDials + 60, 110, 110)
        
        
        
        ### SETTINGS PAINTING
        painter.setPen(QPen(QColor(150, 150, 150), 5))
        painter.setBrush(QBrush(Qt.transparent, Qt.SolidPattern))
        painter.drawRect(xSettings, yDials - 30, 390, 300)
        
        if mode == 1:
            painter.drawEllipse(xSettings + 185, yDials + 175, 50, 50)
            painter.setBrush(QBrush(QColor(57, 255, 20), Qt.SolidPattern))
            painter.drawEllipse(xSettings + 185 + 120, yDials + 175, 50, 50)
        else:
            painter.drawEllipse(xSettings + 185 + 120, yDials + 175, 50, 50)
            painter.setBrush(QBrush(QColor(57, 255, 20), Qt.SolidPattern))
            painter.drawEllipse(xSettings + 185, yDials + 175, 50, 50)
            
        
        
        ### MANUAL CONTROLS
        painter.setPen(QPen(QColor(150, 150, 150), 5))
        painter.setBrush(QBrush(Qt.transparent, Qt.SolidPattern))
        painter.drawRect(xSettings, yDials + 290, 660, 300)
        
        if record == 1:
            painter.setBrush(QBrush(QColor(57, 255, 20), Qt.SolidPattern))
        
        painter.drawEllipse(xSettings + 390 , yDials + 495, 50, 50)
        
        
        
        
        
        ### AUTOMATIC CONTROLS
        painter.setPen(QPen(QColor(150, 150, 150), 5))
        painter.setBrush(QBrush(Qt.transparent, Qt.SolidPattern))
        painter.drawRect(xSettings, yDials + 290 + 320, 660, 350)
        
        if running == 1:
            painter.setBrush(QBrush(QColor(57, 255, 20), Qt.SolidPattern))
        
        painter.drawEllipse(xSettings + 375 , yDials + 800, 50, 50)
               
        ### GRAPH
        painter.setPen(QPen(QColor(150, 150, 150), 5))
        painter.setBrush(QBrush(Qt.transparent, Qt.SolidPattern))
        painter.drawRect(xDials - 50, yDials + 290, 1200, 670)
        
        
        
        
        if (val == -360 or val == 0):
            pom = pom * (-1)
        
        if pom == 1:
            val = val - 1
        else:
            val = val + 1
        






def fun1():
    app = QApplication(sys.argv)
    ex = App()
    app.exec_()


   


def fun2():
    while True:
        time.sleep(0.5)
        global arduinoData
        val1 = arduino.readline()
        print(val1)
        convertString(str(val1))
        
        
t1 = threading.Thread(target = fun1)
t2 = threading.Thread(target = fun2)


t1.start()
t2.start()

 

To explain it simply, the code is running 2 threads, one thread is in charge of reading the data from the Arduino and the other thread is for the GUI itself. To explain a bit better how the interface reads the data from the Arduino, let's take a look at the function called convertString.

def convertString(s):
    global time1
    global thrust
    try:
        pattern = "t(.*?)g"
        time1 = int(re.search(pattern, s).group(1))
    except:
        time1 = -1
    
    try:
        pattern = "g(.*?)r"
        thrust1 = re.search(pattern, s).group(1)
    except:
        thrust1 = -1
    
    try:
        pattern = "r(.*?)c"
        rpm = re.search(pattern, s).group(1)
    except:
        rpm = -1
    
    try:
        pattern = "c(.*?)v"
        current = re.search(pattern, s).group(1)
    except:
        current = -1
        
    try:
        pattern = "v(.*?)e"
        voltage = re.search(pattern, s).group(1)
    except:
        voltage = -1    
        
    thrust = int(thrust1)
    print("Current time: " + str(time1))
    print("Current thrust: " + str(thrust))
    print("Current rpm: " + str(rpm))
    print("Current current: " + str(current))
    print("Current voltage: " + str(voltage))

 

I found this neat trick for extracting the data from a string using a pattern. We define a pattern like "r(.*?)c" for example and then using the "re.search(pattern,s).group(1)" we will extract the first string that starts with a r and ends with a c, or any other combination of letters. That's why I sent the data over from the Arduino sized, with each single piece of data sandwiched between 2 letters, so for example, between letters g and r we have thrust. Looking at it now, I should have made it more self explanatory words rather than just letters, but I will update that in the next iteration. At the end, we get a GUI that looks like this.

I wanted to go for the dark theme and neon-ish colors look. You can see all of the things I've pointed out in the layout above but I've also had space to add 2 emergency STOP ALL buttons if something goes wrong as well as a textbox for displaying status messages and so on. Here is a short video showing the GUI in action.

 

You don't have permission to edit metadata of this video.

Edit media

Subject (required) Brief Description Tags (separated by comma) Video visibility in search results Parent content Poster Upload Preview

 

All that's left now is to power the whole thing up, attach the propeller to the motor and let it spin!

 

5. Testing

     Now we can finally spin up the motor and see what it can do. To just clarify once again, I didn't have a Lipo unfortunately, but had to rely on my bench power supply for the power. The max it can do is 3.1A which isn't close to enough to reach the max thrust of this motor. You can see an interesting phenomenon during the tests because of that, It will reach it's peak thrust and then start dropping even through the throttle is increacing, because the power supply has to clip the voltage more and more to keep providing the 3.1A.

 

You don't have permission to edit metadata of this video.

Edit media

Subject (required) Brief Description Tags (separated by comma) Video visibility in search results Parent content Poster Upload Preview

 

We can see the motor spooling up and it reached around 150-160g peak thrust, which isn't a lot, but it just means that will be much higher once I get a higher current source. Even though that's not a lot, it was blowing stuff from across my room, I thought it would be a good idea to attach a piece of paper at the back to se how it would reach to the airflow.

 

You don't have permission to edit metadata of this video.

Edit media

Subject (required) Brief Description Tags (separated by comma) Video visibility in search results Parent content Poster Upload Preview

 

There is a bit of a delay in this video and there will be in the next one. The ESC needs to it's start up procedure before spinning up the motor, I didn't know how long that was, so I left a pretty huge delay. In this video, you can see that the paper is going all over the place. And here is just another simple test that is like the first one.

 

You don't have permission to edit metadata of this video.

Edit media

Subject (required) Brief Description Tags (separated by comma) Video visibility in search results Parent content Poster Upload Preview

 

6. What's next?

     Next for this project will be to get the communication sorted out in the direction that's not working correctly properly and after that get the encoder working as well as calibrate the current sensor a bit. I tested the current sensor with a multimeter and it's showing okay values, but I need to play around with it a bit. After that is sorted out, there are the main things left, which will go easily, saving the data to a CSV file (it's literally a few lines of code in Python) and programming some procedures for automatic testing. The main thing I needed from this was the thrust which I got, so I know what I can do when it comes to making the drone.

 

7. Summary

     I'm sad I had to rush the project in the end, but some other things couldn't wait. Either way, I got a lot of it working, specially the most important part, the thrust measurement, this will be really useful data when I start some more serious design work for the drone I'm planning on making. I'll keep working on this one the side, because I'm interested in things like the responsiveness of the motor as well as the power draw and RPM-s that I can reach. Thanks for reading my blog, I hope you like it! I've posted all of the code as well as all of the 3D models to my GitHub and you can find the link to that here: BLDC Thrust Tester. You are free to do with those files whatever you want!

 

Milos

 

Relevant links: