Skip to content

Instantly share code, notes, and snippets.

@dreilly369
Created October 27, 2018 00:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dreilly369/0beed2ede88053ba2b94dca34d2103c4 to your computer and use it in GitHub Desktop.
Save dreilly369/0beed2ede88053ba2b94dca34d2103c4 to your computer and use it in GitHub Desktop.
Drone motor Dyno
import math
import numpy as np
import matplotlib.pyplot as plt
from optparse import OptionParser
from random import choice
import matplotlib.colors as mcol
import matplotlib.cm as cm
# Blade pitch acts much like the gearing of the final drive of a car. Low
# pitch yields good low speed acceleration (and climb rate in an aircraft)
# while high pitch optimizes high speed performance and economy.
# A propeller blade's "lift", or its thrust, depends on the angle of attack
# combined with its speed. Because the velocity of a propeller blade varies
# from the hub to the tip, it is of twisted form in order for the thrust to
# remain approximately constant along the length of the blade; this is called
# washout. This is typical of all but the crudest propellers.
COLORS = ['b', 'c', 'm', 'k']
STYLES = ['-', '--', '-.', ':']
MARKERS = ['+', ',', '.', '1', '2', '3', '4']
THRUST_TO_FLY = 0
HOVER_THRUST = 0
DRONE_WGHT = 0
PAYLOAD_WGHT = 0
NUM_MOTORS = 4
class Propeller():
def __init__(self, prop_dia, prop_pitch, thrust_unit='N'):
self.dia = prop_dia
self.pitch = prop_pitch
self.thrust_unit = thrust_unit
self.speed = 0 # RPM
self.thrust = 0
# From http://www.electricrcaircraftguy.com/2013/09/propeller-static-dynamic-thrust-equation.html
def set_speed(self, speed):
self.speed = speed
self.thrust = 4.392e-8 * self.speed * math.pow(self.dia,3.5)/(math.sqrt(self.pitch))
self.thrust = self.thrust * (4.23e-4 * self.speed * self.pitch)
if self.thrust_unit == 'Kg':
self.thrust = self.thrust * 0.101972
#print(("%.4f %s thrust per prop" % (self.thrust, self.thrust_unit)))
return self.thrust
def rand_styles(count, max_tries=None):
used = []
if max_tries is None:
max_tries = count * 4
on = 0
while len(used) < count:
c = choice(COLORS)
s = choice(STYLES)
m = choice(MARKERS)
pos = "%s%s%s" % (c, s, m)
if pos not in used:
used.append(pos)
on = 0
else:
on += 1
return used
def graph_motors(data, max_x, change):
used = rand_styles(len(list(data.keys())))
# evenly sampled time at *change* rpm intervals
x_axis = np.arange(0., max_x, change)
sorted_keys = sorted(
list(data.keys()),
key=lambda ps: (
int(ps.split("x")[0]), float(ps.split("x")[1])
)
)
data_len = len(data[sorted_keys[0]])
min_t = data[sorted_keys[0]][-1] - 10
max_t = data[sorted_keys[-1]][-1] + 10
t_range = (max_t - min_t)
Blues = plt.get_cmap('Greens')
Reds = plt.get_cmap('Reds')
# Add data to graph
pcm = None
for prop in sorted_keys:
d = data[prop]
p = (max_t - d[-1]) / (t_range)
if d[-1] >= HOVER_THRUST / 1000:
pcm = Blues(p)
else:
pcm = Reds(p)
plt.plot(x_axis, data[prop], used.pop(), color=pcm)
# horizontal lines
hover_line_data = np.array([(HOVER_THRUST / 1000) for i in range(data_len)])
fly_line_data = np.array([(THRUST_TO_FLY / 1000) for i in range(data_len)])
plt.plot(x_axis, hover_line_data, 'b-')
plt.plot(x_axis, fly_line_data, 'y-')
plt.legend(sorted_keys + ["Hover Line", "Fly Line"])
plt.xlabel("Motor RPMs")
plt.ylabel("Total Thrust (Kg)")
plt.title(
'Drone: %.2fKg Cargo: %.2fKg Motors:%d' % (DRONE_WGHT / 1000, PAYLOAD_WGHT / 1000, NUM_MOTORS),
bbox={
'boxstyle': 'sawtooth,pad=0.30',
'facecolor': 'blue',
'alpha': 0.4
}
)
mng = plt.get_current_fig_manager()
mng.resize(*mng.window.maxsize())
plt.show()
if __name__ == "__main__":
parser = OptionParser()
parser.add_option("-m", "--motor-rpm", dest="rpms", default=None,
help="Estimated Max RPMs for motors")
parser.add_option("-k", "--motor-kv", dest="kvs", default=None,
help="Advertise k/v of Motor (e.g. 920=920kv)")
parser.add_option("-d", "--drone-weight", dest="drone_grams", default=1000,
help="Drone weight in grams")
parser.add_option("-c", "--carry", dest="carry_grams", default=1000,
help="The desired payload weight in grams")
parser.add_option("-n", "--num-motors", dest="motors", default=4,
help="The desired payload weight in grams")
parser.add_option("-r", "--rpm-step", dest="rpm_step", default=20,
help="The number of RPMs increased between calcs")
parser.add_option("-b", "--batt-cells", dest="batt", default=None,
help="The number of cells in the battery (for KV mode only)")
(options, args) = parser.parse_args()
DRONE_WGHT = float(options.drone_grams)
PAYLOAD_WGHT = float(options.carry_grams)
# Hover and thrust from:
# http://www.tomsguide.com/faq/id-2384509/find-perfect-motor-fit-drone.html
THRUST_TO_FLY = 2 * (DRONE_WGHT + PAYLOAD_WGHT) # Thrust in Grams for flight
HOVER_THRUST = (0.2 * THRUST_TO_FLY) + THRUST_TO_FLY # Higher thrust to hover
if options.rpms is None and options.kvs is None:
print("Need either KV (-k and -b) or Max RPMs (-m) for motors")
exit()
if options.kvs is not None and options.batt is not None:
print("Using K/V calc")
volts = 3.7 * int(options.batt)
max_rpm = (
math.floor(volts * float(options.kvs))
) + int(options.rpm_step)
elif options.kvs is not None and options.batt is None:
print("Need a battery size (-b) for K/V mode")
exit()
else:
max_rpm = int(options.rpms)
motor_counts = [1, 2, 3, 4, 6, 8, 16]
if int(options.motors) not in motor_counts:
print(("Supported motor counts: "))
print((",".join([str(i) for i in motor_counts])))
exit()
# All from RayCorp catalog
prop_sizes = (
[3, 3.0],
[4, 4.0],
[4, 4.5],
[5, 3.0],
[5, 4.0],
[5, 4.5],
[5, 5.0],
[6, 3.0],
[6, 4.0],
[6, 4.5],
[7, 4.5],
[8, 3.8],
[8, 4.5],
[9, 4.7],
[10, 4.5],
[10, 4.7],
[11, 4.7],
[12, 3.8],
[12, 4.5]
)
NUM_MOTORS = int(options.motors)
AUW = (PAYLOAD_WGHT + DRONE_WGHT) / 1000
print(("Drone System Weight: %.2f Kg" % (DRONE_WGHT / 1000)))
print(("Payload Weight: %.2f Kg" % (PAYLOAD_WGHT / 1000)))
print(("All Up Weight: %.2f Kg" % AUW))
# For each propeller, go through the accelleration range
print(("Required flight thrust (per motor): %2.f grams" % (THRUST_TO_FLY / NUM_MOTORS)))
print(("Required hover thrust (per motor): %2.f grams" % (HOVER_THRUST / NUM_MOTORS)))
thrusts_sets = {}
if int(options.rpm_step) < 10:
print("RPM step size to low (min 10). Setting to default of 20 rpms")
options.rpm_step = 20
else:
options.rpm_step = int(options.rpm_step)
for Dia, pitch in prop_sizes:
mcn = "%dx%.1f" % (Dia, pitch)
print(("Testing: %s" % mcn))
liftoff = False
hover = False
prop = Propeller(Dia, pitch, "Kg")
cur_thrust = 0
mcd = []
for i in range(0, max_rpm, options.rpm_step):
cur_thrust = NUM_MOTORS * prop.set_speed(i)
#print(("Current total thrust: %.4f" % cur_thrust))
if cur_thrust >= (THRUST_TO_FLY / 1000) and not liftoff:
liftoff = True
print(("\tFlight at %d RPMs ( %.2f percent throttle)" % (i, (i / max_rpm) * 100)))
if cur_thrust >= (HOVER_THRUST / 1000) and not hover:
hover = True
print(("\tHover at %d RPMs ( %.2f percent throttle)" % (i, (i / max_rpm) * 100)))
mcd.append(cur_thrust)
thrusts_sets[mcn] = mcd
if not liftoff or not hover:
print(("\t***Motor/Prop combo to weak for platform***"))
print(("\tFinal thrust: %.4f Kg at %d RPM" % (cur_thrust, i)))
print(("\tThrust to weight ratio: %.2f:1" % (cur_thrust / AUW)))
graph_motors(thrusts_sets, max_rpm, options.rpm_step)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment