Created
October 27, 2018 00:29
-
-
Save dreilly369/0beed2ede88053ba2b94dca34d2103c4 to your computer and use it in GitHub Desktop.
Drone motor Dyno
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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