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:
- Priority
- Thrust measurement
- Current measurement
- Voltage measurement
- GUI control
- GUI live feed data
- Storing thrust/current/voltage data in a format that can be easily used later with MATLAB/Excel
- Priority
- RPM measurement
- Live graphing of the data
- Automated motor testing sequences
- 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. |
Foot |
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.
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.
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.
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.
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:
- Data Conversion Project14 - Data Conversion
- All of the models and code that I've used for this project - Drone Motor Thrust Tester
- My other projects - Milos Rasic - Project Collection
Top Comments